diff options
author | Chris Schlaeger <chris@linux.com> | 2014-08-12 21:56:44 +0200 |
---|---|---|
committer | Chris Schlaeger <chris@linux.com> | 2014-08-12 21:56:44 +0200 |
commit | ea346a785dc1b3f7c156f6fc33da634e1f1a627b (patch) | |
tree | af67530553d20b6e82ad60fd79593e9c4abf5565 /misc/openlayers/OpenLayers.mobile.debug.js | |
parent | 59741cd535c47f25971bf8c32b25da25ceadc6d5 (diff) | |
download | postrunner-0.0.4.zip |
Adding jquery, flot and openlayers to be included with the GEM.v0.0.4
Diffstat (limited to 'misc/openlayers/OpenLayers.mobile.debug.js')
-rw-r--r-- | misc/openlayers/OpenLayers.mobile.debug.js | 41831 |
1 files changed, 41831 insertions, 0 deletions
diff --git a/misc/openlayers/OpenLayers.mobile.debug.js b/misc/openlayers/OpenLayers.mobile.debug.js new file mode 100644 index 0000000..5cb37cd --- /dev/null +++ b/misc/openlayers/OpenLayers.mobile.debug.js @@ -0,0 +1,41831 @@ +/* + + OpenLayers.js -- OpenLayers Map Viewer Library + + Copyright (c) 2006-2013 by OpenLayers Contributors + Published under the 2-clause BSD license. + See http://openlayers.org/dev/license.txt for the full text of the license, and http://openlayers.org/dev/authors.txt for full list of contributors. + + Includes compressed code under the following licenses: + + (For uncompressed versions of the code used, please see the + OpenLayers Github repository: <https://github.com/openlayers/openlayers>) + +*/ + +/** + * Contains XMLHttpRequest.js <http://code.google.com/p/xmlhttprequest/> + * Copyright 2007 Sergey Ilinsky (http://www.ilinsky.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * OpenLayers.Util.pagePosition is based on Yahoo's getXY method, which is + * Copyright (c) 2006, Yahoo! Inc. + * All rights reserved. + * + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Yahoo! Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission of Yahoo! Inc. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/* ====================================================================== + OpenLayers/SingleFile.js + ====================================================================== */ + +/* 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. */ + +var OpenLayers = { + /** + * Constant: VERSION_NUMBER + */ + VERSION_NUMBER: "Release 2.13.1", + + /** + * Constant: singleFile + * TODO: remove this in 3.0 when we stop supporting build profiles that + * include OpenLayers.js + */ + singleFile: true, + + /** + * Method: _getScriptLocation + * Return the path to this script. This is also implemented in + * OpenLayers.js + * + * Returns: + * {String} Path to this script + */ + _getScriptLocation: (function() { + var r = new RegExp("(^|(.*?\\/))(OpenLayers[^\\/]*?\\.js)(\\?|$)"), + s = document.getElementsByTagName('script'), + src, m, l = ""; + for(var i=0, len=s.length; i<len; i++) { + src = s[i].getAttribute('src'); + if(src) { + m = src.match(r); + if(m) { + l = m[1]; + break; + } + } + } + return (function() { return l; }); + })(), + + /** + * Property: ImgPath + * {String} Set this to the path where control images are stored, a path + * given here must end with a slash. If set to '' (which is the default) + * OpenLayers will use its script location + "img/". + * + * You will need to set this property when you have a singlefile build of + * OpenLayers that either is not named "OpenLayers.js" or if you move + * the file in a way such that the image directory cannot be derived from + * the script location. + * + * If your custom OpenLayers build is named "my-custom-ol.js" and the images + * of OpenLayers are in a folder "/resources/external/images/ol" a correct + * way of including OpenLayers in your HTML would be: + * + * (code) + * <script src="/path/to/my-custom-ol.js" type="text/javascript"></script> + * <script type="text/javascript"> + * // tell OpenLayers where the control images are + * // remember the trailing slash + * OpenLayers.ImgPath = "/resources/external/images/ol/"; + * </script> + * (end code) + * + * Please remember that when your OpenLayers script is not named + * "OpenLayers.js" you will have to make sure that the default theme is + * loaded into the page by including an appropriate <link>-tag, + * e.g.: + * + * (code) + * <link rel="stylesheet" href="/path/to/default/style.css" type="text/css"> + * (end code) + */ + ImgPath : '' +}; +/* ====================================================================== + OpenLayers/BaseTypes.js + ====================================================================== */ + +/* 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/SingleFile.js + */ + +/** + * Header: OpenLayers Base Types + * OpenLayers custom string, number and function functions are described here. + */ + +/** + * Namespace: OpenLayers.String + * Contains convenience functions for string manipulation. + */ +OpenLayers.String = { + + /** + * APIFunction: startsWith + * Test whether a string starts with another string. + * + * Parameters: + * str - {String} The string to test. + * sub - {String} The substring to look for. + * + * Returns: + * {Boolean} The first string starts with the second. + */ + startsWith: function(str, sub) { + return (str.indexOf(sub) == 0); + }, + + /** + * APIFunction: contains + * Test whether a string contains another string. + * + * Parameters: + * str - {String} The string to test. + * sub - {String} The substring to look for. + * + * Returns: + * {Boolean} The first string contains the second. + */ + contains: function(str, sub) { + return (str.indexOf(sub) != -1); + }, + + /** + * APIFunction: trim + * Removes leading and trailing whitespace characters from a string. + * + * Parameters: + * str - {String} The (potentially) space padded string. This string is not + * modified. + * + * Returns: + * {String} A trimmed version of the string with all leading and + * trailing spaces removed. + */ + trim: function(str) { + return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }, + + /** + * APIFunction: camelize + * Camel-case a hyphenated string. + * Ex. "chicken-head" becomes "chickenHead", and + * "-chicken-head" becomes "ChickenHead". + * + * Parameters: + * str - {String} The string to be camelized. The original is not modified. + * + * Returns: + * {String} The string, camelized + */ + camelize: function(str) { + var oStringList = str.split('-'); + var camelizedString = oStringList[0]; + for (var i=1, len=oStringList.length; i<len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + return camelizedString; + }, + + /** + * APIFunction: format + * Given a string with tokens in the form ${token}, return a string + * with tokens replaced with properties from the given context + * object. Represent a literal "${" by doubling it, e.g. "${${". + * + * Parameters: + * template - {String} A string with tokens to be replaced. A template + * has the form "literal ${token}" where the token will be replaced + * by the value of context["token"]. + * context - {Object} An optional object with properties corresponding + * to the tokens in the format string. If no context is sent, the + * window object will be used. + * args - {Array} Optional arguments to pass to any functions found in + * the context. If a context property is a function, the token + * will be replaced by the return from the function called with + * these arguments. + * + * Returns: + * {String} A string with tokens replaced from the context object. + */ + format: function(template, context, args) { + if(!context) { + context = window; + } + + // Example matching: + // str = ${foo.bar} + // match = foo.bar + var replacer = function(str, match) { + var replacement; + + // Loop through all subs. Example: ${a.b.c} + // 0 -> replacement = context[a]; + // 1 -> replacement = context[a][b]; + // 2 -> replacement = context[a][b][c]; + var subs = match.split(/\.+/); + for (var i=0; i< subs.length; i++) { + if (i == 0) { + replacement = context; + } + if (replacement === undefined) { + break; + } + replacement = replacement[subs[i]]; + } + + if(typeof replacement == "function") { + replacement = args ? + replacement.apply(null, args) : + replacement(); + } + + // If replacement is undefined, return the string 'undefined'. + // This is a workaround for a bugs in browsers not properly + // dealing with non-participating groups in regular expressions: + // http://blog.stevenlevithan.com/archives/npcg-javascript + if (typeof replacement == 'undefined') { + return 'undefined'; + } else { + return replacement; + } + }; + + return template.replace(OpenLayers.String.tokenRegEx, replacer); + }, + + /** + * Property: tokenRegEx + * Used to find tokens in a string. + * Examples: ${a}, ${a.b.c}, ${a-b}, ${5} + */ + tokenRegEx: /\$\{([\w.]+?)\}/g, + + /** + * Property: numberRegEx + * Used to test strings as numbers. + */ + numberRegEx: /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/, + + /** + * APIFunction: isNumeric + * Determine whether a string contains only a numeric value. + * + * Examples: + * (code) + * OpenLayers.String.isNumeric("6.02e23") // true + * OpenLayers.String.isNumeric("12 dozen") // false + * OpenLayers.String.isNumeric("4") // true + * OpenLayers.String.isNumeric(" 4 ") // false + * (end) + * + * Returns: + * {Boolean} String contains only a number. + */ + isNumeric: function(value) { + return OpenLayers.String.numberRegEx.test(value); + }, + + /** + * APIFunction: numericIf + * Converts a string that appears to be a numeric value into a number. + * + * Parameters: + * value - {String} + * trimWhitespace - {Boolean} + * + * Returns: + * {Number|String} a Number if the passed value is a number, a String + * otherwise. + */ + numericIf: function(value, trimWhitespace) { + var originalValue = value; + if (trimWhitespace === true && value != null && value.replace) { + value = value.replace(/^\s*|\s*$/g, ""); + } + return OpenLayers.String.isNumeric(value) ? parseFloat(value) : originalValue; + } + +}; + +/** + * Namespace: OpenLayers.Number + * Contains convenience functions for manipulating numbers. + */ +OpenLayers.Number = { + + /** + * Property: decimalSeparator + * Decimal separator to use when formatting numbers. + */ + decimalSeparator: ".", + + /** + * Property: thousandsSeparator + * Thousands separator to use when formatting numbers. + */ + thousandsSeparator: ",", + + /** + * APIFunction: limitSigDigs + * Limit the number of significant digits on a float. + * + * Parameters: + * num - {Float} + * sig - {Integer} + * + * Returns: + * {Float} The number, rounded to the specified number of significant + * digits. + */ + limitSigDigs: function(num, sig) { + var fig = 0; + if (sig > 0) { + fig = parseFloat(num.toPrecision(sig)); + } + return fig; + }, + + /** + * APIFunction: format + * Formats a number for output. + * + * Parameters: + * num - {Float} + * dec - {Integer} Number of decimal places to round to. + * Defaults to 0. Set to null to leave decimal places unchanged. + * tsep - {String} Thousands separator. + * Default is ",". + * dsep - {String} Decimal separator. + * Default is ".". + * + * Returns: + * {String} A string representing the formatted number. + */ + format: function(num, dec, tsep, dsep) { + dec = (typeof dec != "undefined") ? dec : 0; + tsep = (typeof tsep != "undefined") ? tsep : + OpenLayers.Number.thousandsSeparator; + dsep = (typeof dsep != "undefined") ? dsep : + OpenLayers.Number.decimalSeparator; + + if (dec != null) { + num = parseFloat(num.toFixed(dec)); + } + + var parts = num.toString().split("."); + if (parts.length == 1 && dec == null) { + // integer where we do not want to touch the decimals + dec = 0; + } + + var integer = parts[0]; + if (tsep) { + var thousands = /(-?[0-9]+)([0-9]{3})/; + while(thousands.test(integer)) { + integer = integer.replace(thousands, "$1" + tsep + "$2"); + } + } + + var str; + if (dec == 0) { + str = integer; + } else { + var rem = parts.length > 1 ? parts[1] : "0"; + if (dec != null) { + rem = rem + new Array(dec - rem.length + 1).join("0"); + } + str = integer + dsep + rem; + } + return str; + }, + + /** + * Method: zeroPad + * Create a zero padded string optionally with a radix for casting numbers. + * + * Parameters: + * num - {Number} The number to be zero padded. + * len - {Number} The length of the string to be returned. + * radix - {Number} An integer between 2 and 36 specifying the base to use + * for representing numeric values. + */ + zeroPad: function(num, len, radix) { + var str = num.toString(radix || 10); + while (str.length < len) { + str = "0" + str; + } + return str; + } +}; + +/** + * Namespace: OpenLayers.Function + * Contains convenience functions for function manipulation. + */ +OpenLayers.Function = { + /** + * APIFunction: bind + * Bind a function to an object. Method to easily create closures with + * 'this' altered. + * + * Parameters: + * func - {Function} Input function. + * object - {Object} The object to bind to the input function (as this). + * + * Returns: + * {Function} A closure with 'this' set to the passed in object. + */ + bind: function(func, object) { + // create a reference to all arguments past the second one + var args = Array.prototype.slice.apply(arguments, [2]); + return function() { + // Push on any additional arguments from the actual function call. + // These will come after those sent to the bind call. + var newArgs = args.concat( + Array.prototype.slice.apply(arguments, [0]) + ); + return func.apply(object, newArgs); + }; + }, + + /** + * APIFunction: bindAsEventListener + * Bind a function to an object, and configure it to receive the event + * object as first parameter when called. + * + * Parameters: + * func - {Function} Input function to serve as an event listener. + * object - {Object} A reference to this. + * + * Returns: + * {Function} + */ + bindAsEventListener: function(func, object) { + return function(event) { + return func.call(object, event || window.event); + }; + }, + + /** + * APIFunction: False + * A simple function to that just does "return false". We use this to + * avoid attaching anonymous functions to DOM event handlers, which + * causes "issues" on IE<8. + * + * Usage: + * document.onclick = OpenLayers.Function.False; + * + * Returns: + * {Boolean} + */ + False : function() { + return false; + }, + + /** + * APIFunction: True + * A simple function to that just does "return true". We use this to + * avoid attaching anonymous functions to DOM event handlers, which + * causes "issues" on IE<8. + * + * Usage: + * document.onclick = OpenLayers.Function.True; + * + * Returns: + * {Boolean} + */ + True : function() { + return true; + }, + + /** + * APIFunction: Void + * A reusable function that returns ``undefined``. + * + * Returns: + * {undefined} + */ + Void: function() {} + +}; + +/** + * Namespace: OpenLayers.Array + * Contains convenience functions for array manipulation. + */ +OpenLayers.Array = { + + /** + * APIMethod: filter + * Filter an array. Provides the functionality of the + * Array.prototype.filter extension to the ECMA-262 standard. Where + * available, Array.prototype.filter will be used. + * + * Based on well known example from http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array/filter + * + * Parameters: + * array - {Array} The array to be filtered. This array is not mutated. + * Elements added to this array by the callback will not be visited. + * callback - {Function} A function that is called for each element in + * the array. If this function returns true, the element will be + * included in the return. The function will be called with three + * arguments: the element in the array, the index of that element, and + * the array itself. If the optional caller parameter is specified + * the callback will be called with this set to caller. + * caller - {Object} Optional object to be set as this when the callback + * is called. + * + * Returns: + * {Array} An array of elements from the passed in array for which the + * callback returns true. + */ + filter: function(array, callback, caller) { + var selected = []; + if (Array.prototype.filter) { + selected = array.filter(callback, caller); + } else { + var len = array.length; + if (typeof callback != "function") { + throw new TypeError(); + } + for(var i=0; i<len; i++) { + if (i in array) { + var val = array[i]; + if (callback.call(caller, val, i, array)) { + selected.push(val); + } + } + } + } + return selected; + } + +}; +/* ====================================================================== + OpenLayers/BaseTypes/Class.js + ====================================================================== */ + +/* 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/SingleFile.js + */ + +/** + * Constructor: OpenLayers.Class + * Base class used to construct all other classes. Includes support for + * multiple inheritance. + * + * This constructor is new in OpenLayers 2.5. At OpenLayers 3.0, the old + * syntax for creating classes and dealing with inheritance + * will be removed. + * + * To create a new OpenLayers-style class, use the following syntax: + * (code) + * var MyClass = OpenLayers.Class(prototype); + * (end) + * + * To create a new OpenLayers-style class with multiple inheritance, use the + * following syntax: + * (code) + * var MyClass = OpenLayers.Class(Class1, Class2, prototype); + * (end) + * + * Note that instanceof reflection will only reveal Class1 as superclass. + * + */ +OpenLayers.Class = function() { + var len = arguments.length; + var P = arguments[0]; + var F = arguments[len-1]; + + var C = typeof F.initialize == "function" ? + F.initialize : + function(){ P.prototype.initialize.apply(this, arguments); }; + + if (len > 1) { + var newArgs = [C, P].concat( + Array.prototype.slice.call(arguments).slice(1, len-1), F); + OpenLayers.inherit.apply(null, newArgs); + } else { + C.prototype = F; + } + return C; +}; + +/** + * Function: OpenLayers.inherit + * + * Parameters: + * C - {Object} the class that inherits + * P - {Object} the superclass to inherit from + * + * In addition to the mandatory C and P parameters, an arbitrary number of + * objects can be passed, which will extend C. + */ +OpenLayers.inherit = function(C, P) { + var F = function() {}; + F.prototype = P.prototype; + C.prototype = new F; + var i, l, o; + for(i=2, l=arguments.length; i<l; i++) { + o = arguments[i]; + if(typeof o === "function") { + o = o.prototype; + } + OpenLayers.Util.extend(C.prototype, o); + } +}; + +/** + * APIFunction: extend + * Copy all properties of a source object to a destination object. Modifies + * the passed in destination object. Any properties on the source object + * that are set to undefined will not be (re)set on the destination object. + * + * Parameters: + * destination - {Object} The object that will be modified + * source - {Object} The object with properties to be set on the destination + * + * Returns: + * {Object} The destination object. + */ +OpenLayers.Util = OpenLayers.Util || {}; +OpenLayers.Util.extend = function(destination, source) { + destination = destination || {}; + if (source) { + for (var property in source) { + var value = source[property]; + if (value !== undefined) { + destination[property] = value; + } + } + + /** + * IE doesn't include the toString property when iterating over an object's + * properties with the for(property in object) syntax. Explicitly check if + * the source has its own toString property. + */ + + /* + * FF/Windows < 2.0.0.13 reports "Illegal operation on WrappedNative + * prototype object" when calling hawOwnProperty if the source object + * is an instance of window.Event. + */ + + var sourceIsEvt = typeof window.Event == "function" + && source instanceof window.Event; + + if (!sourceIsEvt + && source.hasOwnProperty && source.hasOwnProperty("toString")) { + destination.toString = source.toString; + } + } + return destination; +}; +/* ====================================================================== + OpenLayers/BaseTypes/Bounds.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Bounds + * Instances of this class represent bounding boxes. Data stored as left, + * bottom, right, top floats. All values are initialized to null, however, + * you should make sure you set them before using the bounds for anything. + * + * Possible use case: + * (code) + * bounds = new OpenLayers.Bounds(); + * bounds.extend(new OpenLayers.LonLat(4,5)); + * bounds.extend(new OpenLayers.LonLat(5,6)); + * bounds.toBBOX(); // returns 4,5,5,6 + * (end) + */ +OpenLayers.Bounds = OpenLayers.Class({ + + /** + * Property: left + * {Number} Minimum horizontal coordinate. + */ + left: null, + + /** + * Property: bottom + * {Number} Minimum vertical coordinate. + */ + bottom: null, + + /** + * Property: right + * {Number} Maximum horizontal coordinate. + */ + right: null, + + /** + * Property: top + * {Number} Maximum vertical coordinate. + */ + top: null, + + /** + * Property: centerLonLat + * {<OpenLayers.LonLat>} A cached center location. This should not be + * accessed directly. Use <getCenterLonLat> instead. + */ + centerLonLat: null, + + /** + * Constructor: OpenLayers.Bounds + * Construct a new bounds object. Coordinates can either be passed as four + * arguments, or as a single argument. + * + * Parameters (four arguments): + * left - {Number} The left bounds of the box. Note that for width + * calculations, this is assumed to be less than the right value. + * bottom - {Number} The bottom bounds of the box. Note that for height + * calculations, this is assumed to be less than the top value. + * right - {Number} The right bounds. + * top - {Number} The top bounds. + * + * Parameters (single argument): + * bounds - {Array(Number)} [left, bottom, right, top] + */ + initialize: function(left, bottom, right, top) { + if (OpenLayers.Util.isArray(left)) { + top = left[3]; + right = left[2]; + bottom = left[1]; + left = left[0]; + } + if (left != null) { + this.left = OpenLayers.Util.toFloat(left); + } + if (bottom != null) { + this.bottom = OpenLayers.Util.toFloat(bottom); + } + if (right != null) { + this.right = OpenLayers.Util.toFloat(right); + } + if (top != null) { + this.top = OpenLayers.Util.toFloat(top); + } + }, + + /** + * Method: clone + * Create a cloned instance of this bounds. + * + * Returns: + * {<OpenLayers.Bounds>} A fresh copy of the bounds + */ + clone:function() { + return new OpenLayers.Bounds(this.left, this.bottom, + this.right, this.top); + }, + + /** + * Method: equals + * Test a two bounds for equivalence. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * + * Returns: + * {Boolean} The passed-in bounds object has the same left, + * right, top, bottom components as this. Note that if bounds + * passed in is null, returns false. + */ + equals:function(bounds) { + var equals = false; + if (bounds != null) { + equals = ((this.left == bounds.left) && + (this.right == bounds.right) && + (this.top == bounds.top) && + (this.bottom == bounds.bottom)); + } + return equals; + }, + + /** + * APIMethod: toString + * Returns a string representation of the bounds object. + * + * Returns: + * {String} String representation of bounds object. + */ + toString:function() { + return [this.left, this.bottom, this.right, this.top].join(","); + }, + + /** + * APIMethod: toArray + * Returns an array representation of the bounds object. + * + * Returns an array of left, bottom, right, top properties, or -- when the + * optional parameter is true -- an array of the bottom, left, top, + * right properties. + * + * Parameters: + * reverseAxisOrder - {Boolean} Should we reverse the axis order? + * + * Returns: + * {Array} array of left, bottom, right, top + */ + toArray: function(reverseAxisOrder) { + if (reverseAxisOrder === true) { + return [this.bottom, this.left, this.top, this.right]; + } else { + return [this.left, this.bottom, this.right, this.top]; + } + }, + + /** + * APIMethod: toBBOX + * Returns a boundingbox-string representation of the bounds object. + * + * Parameters: + * decimal - {Integer} How many significant digits in the bbox coords? + * Default is 6 + * reverseAxisOrder - {Boolean} Should we reverse the axis order? + * + * Returns: + * {String} Simple String representation of bounds object. + * (e.g. "5,42,10,45") + */ + toBBOX:function(decimal, reverseAxisOrder) { + if (decimal== null) { + decimal = 6; + } + var mult = Math.pow(10, decimal); + var xmin = Math.round(this.left * mult) / mult; + var ymin = Math.round(this.bottom * mult) / mult; + var xmax = Math.round(this.right * mult) / mult; + var ymax = Math.round(this.top * mult) / mult; + if (reverseAxisOrder === true) { + return ymin + "," + xmin + "," + ymax + "," + xmax; + } else { + return xmin + "," + ymin + "," + xmax + "," + ymax; + } + }, + + /** + * APIMethod: toGeometry + * Create a new polygon geometry based on this bounds. + * + * Returns: + * {<OpenLayers.Geometry.Polygon>} A new polygon with the coordinates + * of this bounds. + */ + toGeometry: function() { + return new OpenLayers.Geometry.Polygon([ + new OpenLayers.Geometry.LinearRing([ + new OpenLayers.Geometry.Point(this.left, this.bottom), + new OpenLayers.Geometry.Point(this.right, this.bottom), + new OpenLayers.Geometry.Point(this.right, this.top), + new OpenLayers.Geometry.Point(this.left, this.top) + ]) + ]); + }, + + /** + * APIMethod: getWidth + * Returns the width of the bounds. + * + * Returns: + * {Float} The width of the bounds (right minus left). + */ + getWidth:function() { + return (this.right - this.left); + }, + + /** + * APIMethod: getHeight + * Returns the height of the bounds. + * + * Returns: + * {Float} The height of the bounds (top minus bottom). + */ + getHeight:function() { + return (this.top - this.bottom); + }, + + /** + * APIMethod: getSize + * Returns an <OpenLayers.Size> object of the bounds. + * + * Returns: + * {<OpenLayers.Size>} The size of the bounds. + */ + getSize:function() { + return new OpenLayers.Size(this.getWidth(), this.getHeight()); + }, + + /** + * APIMethod: getCenterPixel + * Returns the <OpenLayers.Pixel> object which represents the center of the + * bounds. + * + * Returns: + * {<OpenLayers.Pixel>} The center of the bounds in pixel space. + */ + getCenterPixel:function() { + return new OpenLayers.Pixel( (this.left + this.right) / 2, + (this.bottom + this.top) / 2); + }, + + /** + * APIMethod: getCenterLonLat + * Returns the <OpenLayers.LonLat> object which represents the center of the + * bounds. + * + * Returns: + * {<OpenLayers.LonLat>} The center of the bounds in map space. + */ + getCenterLonLat:function() { + if(!this.centerLonLat) { + this.centerLonLat = new OpenLayers.LonLat( + (this.left + this.right) / 2, (this.bottom + this.top) / 2 + ); + } + return this.centerLonLat; + }, + + /** + * APIMethod: scale + * Scales the bounds around a pixel or lonlat. Note that the new + * bounds may return non-integer properties, even if a pixel + * is passed. + * + * Parameters: + * ratio - {Float} + * origin - {<OpenLayers.Pixel> or <OpenLayers.LonLat>} + * Default is center. + * + * Returns: + * {<OpenLayers.Bounds>} A new bounds that is scaled by ratio + * from origin. + */ + scale: function(ratio, origin){ + if(origin == null){ + origin = this.getCenterLonLat(); + } + + var origx,origy; + + // get origin coordinates + if(origin.CLASS_NAME == "OpenLayers.LonLat"){ + origx = origin.lon; + origy = origin.lat; + } else { + origx = origin.x; + origy = origin.y; + } + + var left = (this.left - origx) * ratio + origx; + var bottom = (this.bottom - origy) * ratio + origy; + var right = (this.right - origx) * ratio + origx; + var top = (this.top - origy) * ratio + origy; + + return new OpenLayers.Bounds(left, bottom, right, top); + }, + + /** + * APIMethod: add + * Shifts the coordinates of the bound by the given horizontal and vertical + * deltas. + * + * (start code) + * var bounds = new OpenLayers.Bounds(0, 0, 10, 10); + * bounds.toString(); + * // => "0,0,10,10" + * + * bounds.add(-1.5, 4).toString(); + * // => "-1.5,4,8.5,14" + * (end) + * + * This method will throw a TypeError if it is passed null as an argument. + * + * Parameters: + * x - {Float} horizontal delta + * y - {Float} vertical delta + * + * Returns: + * {<OpenLayers.Bounds>} A new bounds whose coordinates are the same as + * this, but shifted by the passed-in x and y values. + */ + add:function(x, y) { + if ( (x == null) || (y == null) ) { + throw new TypeError('Bounds.add cannot receive null values'); + } + return new OpenLayers.Bounds(this.left + x, this.bottom + y, + this.right + x, this.top + y); + }, + + /** + * APIMethod: extend + * Extend the bounds to include the <OpenLayers.LonLat>, + * <OpenLayers.Geometry.Point> or <OpenLayers.Bounds> specified. + * + * Please note that this function assumes that left < right and + * bottom < top. + * + * Parameters: + * object - {<OpenLayers.LonLat>, <OpenLayers.Geometry.Point> or + * <OpenLayers.Bounds>} The object to be included in the new bounds + * object. + */ + extend:function(object) { + if (object) { + switch(object.CLASS_NAME) { + case "OpenLayers.LonLat": + this.extendXY(object.lon, object.lat); + break; + case "OpenLayers.Geometry.Point": + this.extendXY(object.x, object.y); + break; + + case "OpenLayers.Bounds": + // clear cached center location + this.centerLonLat = null; + + if ( (this.left == null) || (object.left < this.left)) { + this.left = object.left; + } + if ( (this.bottom == null) || (object.bottom < this.bottom) ) { + this.bottom = object.bottom; + } + if ( (this.right == null) || (object.right > this.right) ) { + this.right = object.right; + } + if ( (this.top == null) || (object.top > this.top) ) { + this.top = object.top; + } + break; + } + } + }, + + /** + * APIMethod: extendXY + * Extend the bounds to include the XY coordinate specified. + * + * Parameters: + * x - {number} The X part of the the coordinate. + * y - {number} The Y part of the the coordinate. + */ + extendXY:function(x, y) { + // clear cached center location + this.centerLonLat = null; + + if ((this.left == null) || (x < this.left)) { + this.left = x; + } + if ((this.bottom == null) || (y < this.bottom)) { + this.bottom = y; + } + if ((this.right == null) || (x > this.right)) { + this.right = x; + } + if ((this.top == null) || (y > this.top)) { + this.top = y; + } + }, + + /** + * APIMethod: containsLonLat + * Returns whether the bounds object contains the given <OpenLayers.LonLat>. + * + * Parameters: + * ll - {<OpenLayers.LonLat>|Object} OpenLayers.LonLat or an + * object with a 'lon' and 'lat' properties. + * options - {Object} Optional parameters + * + * Acceptable options: + * inclusive - {Boolean} Whether or not to include the border. + * Default is true. + * worldBounds - {<OpenLayers.Bounds>} If a worldBounds is provided, the + * ll will be considered as contained if it exceeds the world bounds, + * but can be wrapped around the dateline so it is contained by this + * bounds. + * + * Returns: + * {Boolean} The passed-in lonlat is within this bounds. + */ + containsLonLat: function(ll, options) { + if (typeof options === "boolean") { + options = {inclusive: options}; + } + options = options || {}; + var contains = this.contains(ll.lon, ll.lat, options.inclusive), + worldBounds = options.worldBounds; + if (worldBounds && !contains) { + var worldWidth = worldBounds.getWidth(); + var worldCenterX = (worldBounds.left + worldBounds.right) / 2; + var worldsAway = Math.round((ll.lon - worldCenterX) / worldWidth); + contains = this.containsLonLat({ + lon: ll.lon - worldsAway * worldWidth, + lat: ll.lat + }, {inclusive: options.inclusive}); + } + return contains; + }, + + /** + * APIMethod: containsPixel + * Returns whether the bounds object contains the given <OpenLayers.Pixel>. + * + * Parameters: + * px - {<OpenLayers.Pixel>} + * inclusive - {Boolean} Whether or not to include the border. Default is + * true. + * + * Returns: + * {Boolean} The passed-in pixel is within this bounds. + */ + containsPixel:function(px, inclusive) { + return this.contains(px.x, px.y, inclusive); + }, + + /** + * APIMethod: contains + * Returns whether the bounds object contains the given x and y. + * + * Parameters: + * x - {Float} + * y - {Float} + * inclusive - {Boolean} Whether or not to include the border. Default is + * true. + * + * Returns: + * {Boolean} Whether or not the passed-in coordinates are within this + * bounds. + */ + contains:function(x, y, inclusive) { + //set default + if (inclusive == null) { + inclusive = true; + } + + if (x == null || y == null) { + return false; + } + + x = OpenLayers.Util.toFloat(x); + y = OpenLayers.Util.toFloat(y); + + var contains = false; + if (inclusive) { + contains = ((x >= this.left) && (x <= this.right) && + (y >= this.bottom) && (y <= this.top)); + } else { + contains = ((x > this.left) && (x < this.right) && + (y > this.bottom) && (y < this.top)); + } + return contains; + }, + + /** + * APIMethod: intersectsBounds + * Determine whether the target bounds intersects this bounds. Bounds are + * considered intersecting if any of their edges intersect or if one + * bounds contains the other. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} The target bounds. + * options - {Object} Optional parameters. + * + * Acceptable options: + * inclusive - {Boolean} Treat coincident borders as intersecting. Default + * is true. If false, bounds that do not overlap but only touch at the + * border will not be considered as intersecting. + * worldBounds - {<OpenLayers.Bounds>} If a worldBounds is provided, two + * bounds will be considered as intersecting if they intersect when + * shifted to within the world bounds. This applies only to bounds that + * cross or are completely outside the world bounds. + * + * Returns: + * {Boolean} The passed-in bounds object intersects this bounds. + */ + intersectsBounds:function(bounds, options) { + if (typeof options === "boolean") { + options = {inclusive: options}; + } + options = options || {}; + if (options.worldBounds) { + var self = this.wrapDateLine(options.worldBounds); + bounds = bounds.wrapDateLine(options.worldBounds); + } else { + self = this; + } + if (options.inclusive == null) { + options.inclusive = true; + } + var intersects = false; + var mightTouch = ( + self.left == bounds.right || + self.right == bounds.left || + self.top == bounds.bottom || + self.bottom == bounds.top + ); + + // if the two bounds only touch at an edge, and inclusive is false, + // then the bounds don't *really* intersect. + if (options.inclusive || !mightTouch) { + // otherwise, if one of the boundaries even partially contains another, + // inclusive of the edges, then they do intersect. + var inBottom = ( + ((bounds.bottom >= self.bottom) && (bounds.bottom <= self.top)) || + ((self.bottom >= bounds.bottom) && (self.bottom <= bounds.top)) + ); + var inTop = ( + ((bounds.top >= self.bottom) && (bounds.top <= self.top)) || + ((self.top > bounds.bottom) && (self.top < bounds.top)) + ); + var inLeft = ( + ((bounds.left >= self.left) && (bounds.left <= self.right)) || + ((self.left >= bounds.left) && (self.left <= bounds.right)) + ); + var inRight = ( + ((bounds.right >= self.left) && (bounds.right <= self.right)) || + ((self.right >= bounds.left) && (self.right <= bounds.right)) + ); + intersects = ((inBottom || inTop) && (inLeft || inRight)); + } + // document me + if (options.worldBounds && !intersects) { + var world = options.worldBounds; + var width = world.getWidth(); + var selfCrosses = !world.containsBounds(self); + var boundsCrosses = !world.containsBounds(bounds); + if (selfCrosses && !boundsCrosses) { + bounds = bounds.add(-width, 0); + intersects = self.intersectsBounds(bounds, {inclusive: options.inclusive}); + } else if (boundsCrosses && !selfCrosses) { + self = self.add(-width, 0); + intersects = bounds.intersectsBounds(self, {inclusive: options.inclusive}); + } + } + return intersects; + }, + + /** + * APIMethod: containsBounds + * Returns whether the bounds object contains the given <OpenLayers.Bounds>. + * + * bounds - {<OpenLayers.Bounds>} The target bounds. + * partial - {Boolean} If any of the target corners is within this bounds + * consider the bounds contained. Default is false. If false, the + * entire target bounds must be contained within this bounds. + * inclusive - {Boolean} Treat shared edges as contained. Default is + * true. + * + * Returns: + * {Boolean} The passed-in bounds object is contained within this bounds. + */ + containsBounds:function(bounds, partial, inclusive) { + if (partial == null) { + partial = false; + } + if (inclusive == null) { + inclusive = true; + } + var bottomLeft = this.contains(bounds.left, bounds.bottom, inclusive); + var bottomRight = this.contains(bounds.right, bounds.bottom, inclusive); + var topLeft = this.contains(bounds.left, bounds.top, inclusive); + var topRight = this.contains(bounds.right, bounds.top, inclusive); + + return (partial) ? (bottomLeft || bottomRight || topLeft || topRight) + : (bottomLeft && bottomRight && topLeft && topRight); + }, + + /** + * APIMethod: determineQuadrant + * Returns the the quadrant ("br", "tr", "tl", "bl") in which the given + * <OpenLayers.LonLat> lies. + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>} + * + * Returns: + * {String} The quadrant ("br" "tr" "tl" "bl") of the bounds in which the + * coordinate lies. + */ + determineQuadrant: function(lonlat) { + + var quadrant = ""; + var center = this.getCenterLonLat(); + + quadrant += (lonlat.lat < center.lat) ? "b" : "t"; + quadrant += (lonlat.lon < center.lon) ? "l" : "r"; + + return quadrant; + }, + + /** + * APIMethod: transform + * Transform the Bounds object from source to dest. + * + * Parameters: + * source - {<OpenLayers.Projection>} Source projection. + * dest - {<OpenLayers.Projection>} Destination projection. + * + * Returns: + * {<OpenLayers.Bounds>} Itself, for use in chaining operations. + */ + transform: function(source, dest) { + // clear cached center location + this.centerLonLat = null; + var ll = OpenLayers.Projection.transform( + {'x': this.left, 'y': this.bottom}, source, dest); + var lr = OpenLayers.Projection.transform( + {'x': this.right, 'y': this.bottom}, source, dest); + var ul = OpenLayers.Projection.transform( + {'x': this.left, 'y': this.top}, source, dest); + var ur = OpenLayers.Projection.transform( + {'x': this.right, 'y': this.top}, source, dest); + this.left = Math.min(ll.x, ul.x); + this.bottom = Math.min(ll.y, lr.y); + this.right = Math.max(lr.x, ur.x); + this.top = Math.max(ul.y, ur.y); + return this; + }, + + /** + * APIMethod: wrapDateLine + * Wraps the bounds object around the dateline. + * + * Parameters: + * maxExtent - {<OpenLayers.Bounds>} + * options - {Object} Some possible options are: + * + * Allowed Options: + * leftTolerance - {float} Allow for a margin of error + * with the 'left' value of this + * bound. + * Default is 0. + * rightTolerance - {float} Allow for a margin of error + * with the 'right' value of + * this bound. + * Default is 0. + * + * Returns: + * {<OpenLayers.Bounds>} A copy of this bounds, but wrapped around the + * "dateline" (as specified by the borders of + * maxExtent). Note that this function only returns + * a different bounds value if this bounds is + * *entirely* outside of the maxExtent. If this + * bounds straddles the dateline (is part in/part + * out of maxExtent), the returned bounds will always + * cross the left edge of the given maxExtent. + *. + */ + wrapDateLine: function(maxExtent, options) { + options = options || {}; + + var leftTolerance = options.leftTolerance || 0; + var rightTolerance = options.rightTolerance || 0; + + var newBounds = this.clone(); + + if (maxExtent) { + var width = maxExtent.getWidth(); + + //shift right? + while (newBounds.left < maxExtent.left && + newBounds.right - rightTolerance <= maxExtent.left ) { + newBounds = newBounds.add(width, 0); + } + + //shift left? + while (newBounds.left + leftTolerance >= maxExtent.right && + newBounds.right > maxExtent.right ) { + newBounds = newBounds.add(-width, 0); + } + + // crosses right only? force left + var newLeft = newBounds.left + leftTolerance; + if (newLeft < maxExtent.right && newLeft > maxExtent.left && + newBounds.right - rightTolerance > maxExtent.right) { + newBounds = newBounds.add(-width, 0); + } + } + + return newBounds; + }, + + CLASS_NAME: "OpenLayers.Bounds" +}); + +/** + * APIFunction: fromString + * Alternative constructor that builds a new OpenLayers.Bounds from a + * parameter string. + * + * (begin code) + * OpenLayers.Bounds.fromString("5,42,10,45"); + * // => equivalent to ... + * new OpenLayers.Bounds(5, 42, 10, 45); + * (end) + * + * Parameters: + * str - {String} Comma-separated bounds string. (e.g. "5,42,10,45") + * reverseAxisOrder - {Boolean} Does the string use reverse axis order? + * + * Returns: + * {<OpenLayers.Bounds>} New bounds object built from the + * passed-in String. + */ +OpenLayers.Bounds.fromString = function(str, reverseAxisOrder) { + var bounds = str.split(","); + return OpenLayers.Bounds.fromArray(bounds, reverseAxisOrder); +}; + +/** + * APIFunction: fromArray + * Alternative constructor that builds a new OpenLayers.Bounds from an array. + * + * (begin code) + * OpenLayers.Bounds.fromArray( [5, 42, 10, 45] ); + * // => equivalent to ... + * new OpenLayers.Bounds(5, 42, 10, 45); + * (end) + * + * Parameters: + * bbox - {Array(Float)} Array of bounds values (e.g. [5,42,10,45]) + * reverseAxisOrder - {Boolean} Does the array use reverse axis order? + * + * Returns: + * {<OpenLayers.Bounds>} New bounds object built from the passed-in Array. + */ +OpenLayers.Bounds.fromArray = function(bbox, reverseAxisOrder) { + return reverseAxisOrder === true ? + new OpenLayers.Bounds(bbox[1], bbox[0], bbox[3], bbox[2]) : + new OpenLayers.Bounds(bbox[0], bbox[1], bbox[2], bbox[3]); +}; + +/** + * APIFunction: fromSize + * Alternative constructor that builds a new OpenLayers.Bounds from a size. + * + * (begin code) + * OpenLayers.Bounds.fromSize( new OpenLayers.Size(10, 20) ); + * // => equivalent to ... + * new OpenLayers.Bounds(0, 20, 10, 0); + * (end) + * + * Parameters: + * size - {<OpenLayers.Size> or Object} <OpenLayers.Size> or an object with + * both 'w' and 'h' properties. + * + * Returns: + * {<OpenLayers.Bounds>} New bounds object built from the passed-in size. + */ +OpenLayers.Bounds.fromSize = function(size) { + return new OpenLayers.Bounds(0, + size.h, + size.w, + 0); +}; + +/** + * Function: oppositeQuadrant + * Get the opposite quadrant for a given quadrant string. + * + * (begin code) + * OpenLayers.Bounds.oppositeQuadrant( "tl" ); + * // => "br" + * + * OpenLayers.Bounds.oppositeQuadrant( "tr" ); + * // => "bl" + * (end) + * + * Parameters: + * quadrant - {String} two character quadrant shortstring + * + * Returns: + * {String} The opposing quadrant ("br" "tr" "tl" "bl"). For Example, if + * you pass in "bl" it returns "tr", if you pass in "br" it + * returns "tl", etc. + */ +OpenLayers.Bounds.oppositeQuadrant = function(quadrant) { + var opp = ""; + + opp += (quadrant.charAt(0) == 't') ? 'b' : 't'; + opp += (quadrant.charAt(1) == 'l') ? 'r' : 'l'; + + return opp; +}; +/* ====================================================================== + OpenLayers/BaseTypes/Element.js + ====================================================================== */ + +/* 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/Util.js + * @requires OpenLayers/BaseTypes.js + */ + +/** + * Namespace: OpenLayers.Element + */ +OpenLayers.Element = { + + /** + * APIFunction: visible + * + * Parameters: + * element - {DOMElement} + * + * Returns: + * {Boolean} Is the element visible? + */ + visible: function(element) { + return OpenLayers.Util.getElement(element).style.display != 'none'; + }, + + /** + * APIFunction: toggle + * Toggle the visibility of element(s) passed in + * + * Parameters: + * element - {DOMElement} Actually user can pass any number of elements + */ + toggle: function() { + for (var i=0, len=arguments.length; i<len; i++) { + var element = OpenLayers.Util.getElement(arguments[i]); + var display = OpenLayers.Element.visible(element) ? 'none' + : ''; + element.style.display = display; + } + }, + + /** + * APIFunction: remove + * Remove the specified element from the DOM. + * + * Parameters: + * element - {DOMElement} + */ + remove: function(element) { + element = OpenLayers.Util.getElement(element); + element.parentNode.removeChild(element); + }, + + /** + * APIFunction: getHeight + * + * Parameters: + * element - {DOMElement} + * + * Returns: + * {Integer} The offset height of the element passed in + */ + getHeight: function(element) { + element = OpenLayers.Util.getElement(element); + return element.offsetHeight; + }, + + /** + * Function: hasClass + * Tests if an element has the given CSS class name. + * + * Parameters: + * element - {DOMElement} A DOM element node. + * name - {String} The CSS class name to search for. + * + * Returns: + * {Boolean} The element has the given class name. + */ + hasClass: function(element, name) { + var names = element.className; + return (!!names && new RegExp("(^|\\s)" + name + "(\\s|$)").test(names)); + }, + + /** + * Function: addClass + * Add a CSS class name to an element. Safe where element already has + * the class name. + * + * Parameters: + * element - {DOMElement} A DOM element node. + * name - {String} The CSS class name to add. + * + * Returns: + * {DOMElement} The element. + */ + addClass: function(element, name) { + if(!OpenLayers.Element.hasClass(element, name)) { + element.className += (element.className ? " " : "") + name; + } + return element; + }, + + /** + * Function: removeClass + * Remove a CSS class name from an element. Safe where element does not + * have the class name. + * + * Parameters: + * element - {DOMElement} A DOM element node. + * name - {String} The CSS class name to remove. + * + * Returns: + * {DOMElement} The element. + */ + removeClass: function(element, name) { + var names = element.className; + if(names) { + element.className = OpenLayers.String.trim( + names.replace( + new RegExp("(^|\\s+)" + name + "(\\s+|$)"), " " + ) + ); + } + return element; + }, + + /** + * Function: toggleClass + * Remove a CSS class name from an element if it exists. Add the class name + * if it doesn't exist. + * + * Parameters: + * element - {DOMElement} A DOM element node. + * name - {String} The CSS class name to toggle. + * + * Returns: + * {DOMElement} The element. + */ + toggleClass: function(element, name) { + if(OpenLayers.Element.hasClass(element, name)) { + OpenLayers.Element.removeClass(element, name); + } else { + OpenLayers.Element.addClass(element, name); + } + return element; + }, + + /** + * APIFunction: getStyle + * + * Parameters: + * element - {DOMElement} + * style - {?} + * + * Returns: + * {?} + */ + getStyle: function(element, style) { + element = OpenLayers.Util.getElement(element); + + var value = null; + if (element && element.style) { + value = element.style[OpenLayers.String.camelize(style)]; + if (!value) { + if (document.defaultView && + document.defaultView.getComputedStyle) { + + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[OpenLayers.String.camelize(style)]; + } + } + + var positions = ['left', 'top', 'right', 'bottom']; + if (window.opera && + (OpenLayers.Util.indexOf(positions,style) != -1) && + (OpenLayers.Element.getStyle(element, 'position') == 'static')) { + value = 'auto'; + } + } + + return value == 'auto' ? null : value; + } + +}; +/* ====================================================================== + OpenLayers/BaseTypes/LonLat.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.LonLat + * This class represents a longitude and latitude pair + */ +OpenLayers.LonLat = OpenLayers.Class({ + + /** + * APIProperty: lon + * {Float} The x-axis coodinate in map units + */ + lon: 0.0, + + /** + * APIProperty: lat + * {Float} The y-axis coordinate in map units + */ + lat: 0.0, + + /** + * Constructor: OpenLayers.LonLat + * Create a new map location. Coordinates can be passed either as two + * arguments, or as a single argument. + * + * Parameters (two arguments): + * lon - {Number} The x-axis coordinate in map units. If your map is in + * a geographic projection, this will be the Longitude. Otherwise, + * it will be the x coordinate of the map location in your map units. + * lat - {Number} The y-axis coordinate in map units. If your map is in + * a geographic projection, this will be the Latitude. Otherwise, + * it will be the y coordinate of the map location in your map units. + * + * Parameters (single argument): + * location - {Array(Float)} [lon, lat] + */ + initialize: function(lon, lat) { + if (OpenLayers.Util.isArray(lon)) { + lat = lon[1]; + lon = lon[0]; + } + this.lon = OpenLayers.Util.toFloat(lon); + this.lat = OpenLayers.Util.toFloat(lat); + }, + + /** + * Method: toString + * Return a readable string version of the lonlat + * + * Returns: + * {String} String representation of OpenLayers.LonLat object. + * (e.g. <i>"lon=5,lat=42"</i>) + */ + toString:function() { + return ("lon=" + this.lon + ",lat=" + this.lat); + }, + + /** + * APIMethod: toShortString + * + * Returns: + * {String} Shortened String representation of OpenLayers.LonLat object. + * (e.g. <i>"5, 42"</i>) + */ + toShortString:function() { + return (this.lon + ", " + this.lat); + }, + + /** + * APIMethod: clone + * + * Returns: + * {<OpenLayers.LonLat>} New OpenLayers.LonLat object with the same lon + * and lat values + */ + clone:function() { + return new OpenLayers.LonLat(this.lon, this.lat); + }, + + /** + * APIMethod: add + * + * Parameters: + * lon - {Float} + * lat - {Float} + * + * Returns: + * {<OpenLayers.LonLat>} A new OpenLayers.LonLat object with the lon and + * lat passed-in added to this's. + */ + add:function(lon, lat) { + if ( (lon == null) || (lat == null) ) { + throw new TypeError('LonLat.add cannot receive null values'); + } + return new OpenLayers.LonLat(this.lon + OpenLayers.Util.toFloat(lon), + this.lat + OpenLayers.Util.toFloat(lat)); + }, + + /** + * APIMethod: equals + * + * Parameters: + * ll - {<OpenLayers.LonLat>} + * + * Returns: + * {Boolean} Boolean value indicating whether the passed-in + * <OpenLayers.LonLat> object has the same lon and lat + * components as this. + * Note: if ll passed in is null, returns false + */ + equals:function(ll) { + var equals = false; + if (ll != null) { + equals = ((this.lon == ll.lon && this.lat == ll.lat) || + (isNaN(this.lon) && isNaN(this.lat) && isNaN(ll.lon) && isNaN(ll.lat))); + } + return equals; + }, + + /** + * APIMethod: transform + * Transform the LonLat object from source to dest. This transformation is + * *in place*: if you want a *new* lonlat, use .clone() first. + * + * Parameters: + * source - {<OpenLayers.Projection>} Source projection. + * dest - {<OpenLayers.Projection>} Destination projection. + * + * Returns: + * {<OpenLayers.LonLat>} Itself, for use in chaining operations. + */ + transform: function(source, dest) { + var point = OpenLayers.Projection.transform( + {'x': this.lon, 'y': this.lat}, source, dest); + this.lon = point.x; + this.lat = point.y; + return this; + }, + + /** + * APIMethod: wrapDateLine + * + * Parameters: + * maxExtent - {<OpenLayers.Bounds>} + * + * Returns: + * {<OpenLayers.LonLat>} A copy of this lonlat, but wrapped around the + * "dateline" (as specified by the borders of + * maxExtent) + */ + wrapDateLine: function(maxExtent) { + + var newLonLat = this.clone(); + + if (maxExtent) { + //shift right? + while (newLonLat.lon < maxExtent.left) { + newLonLat.lon += maxExtent.getWidth(); + } + + //shift left? + while (newLonLat.lon > maxExtent.right) { + newLonLat.lon -= maxExtent.getWidth(); + } + } + + return newLonLat; + }, + + CLASS_NAME: "OpenLayers.LonLat" +}); + +/** + * Function: fromString + * Alternative constructor that builds a new <OpenLayers.LonLat> from a + * parameter string + * + * Parameters: + * str - {String} Comma-separated Lon,Lat coordinate string. + * (e.g. <i>"5,40"</i>) + * + * Returns: + * {<OpenLayers.LonLat>} New <OpenLayers.LonLat> object built from the + * passed-in String. + */ +OpenLayers.LonLat.fromString = function(str) { + var pair = str.split(","); + return new OpenLayers.LonLat(pair[0], pair[1]); +}; + +/** + * Function: fromArray + * Alternative constructor that builds a new <OpenLayers.LonLat> from an + * array of two numbers that represent lon- and lat-values. + * + * Parameters: + * arr - {Array(Float)} Array of lon/lat values (e.g. [5,-42]) + * + * Returns: + * {<OpenLayers.LonLat>} New <OpenLayers.LonLat> object built from the + * passed-in array. + */ +OpenLayers.LonLat.fromArray = function(arr) { + var gotArr = OpenLayers.Util.isArray(arr), + lon = gotArr && arr[0], + lat = gotArr && arr[1]; + return new OpenLayers.LonLat(lon, lat); +}; +/* ====================================================================== + OpenLayers/BaseTypes/Pixel.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Pixel + * This class represents a screen coordinate, in x and y coordinates + */ +OpenLayers.Pixel = OpenLayers.Class({ + + /** + * APIProperty: x + * {Number} The x coordinate + */ + x: 0.0, + + /** + * APIProperty: y + * {Number} The y coordinate + */ + y: 0.0, + + /** + * Constructor: OpenLayers.Pixel + * Create a new OpenLayers.Pixel instance + * + * Parameters: + * x - {Number} The x coordinate + * y - {Number} The y coordinate + * + * Returns: + * An instance of OpenLayers.Pixel + */ + initialize: function(x, y) { + this.x = parseFloat(x); + this.y = parseFloat(y); + }, + + /** + * Method: toString + * Cast this object into a string + * + * Returns: + * {String} The string representation of Pixel. ex: "x=200.4,y=242.2" + */ + toString:function() { + return ("x=" + this.x + ",y=" + this.y); + }, + + /** + * APIMethod: clone + * Return a clone of this pixel object + * + * Returns: + * {<OpenLayers.Pixel>} A clone pixel + */ + clone:function() { + return new OpenLayers.Pixel(this.x, this.y); + }, + + /** + * APIMethod: equals + * Determine whether one pixel is equivalent to another + * + * Parameters: + * px - {<OpenLayers.Pixel>|Object} An OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * + * Returns: + * {Boolean} The point passed in as parameter is equal to this. Note that + * if px passed in is null, returns false. + */ + equals:function(px) { + var equals = false; + if (px != null) { + equals = ((this.x == px.x && this.y == px.y) || + (isNaN(this.x) && isNaN(this.y) && isNaN(px.x) && isNaN(px.y))); + } + return equals; + }, + + /** + * APIMethod: distanceTo + * Returns the distance to the pixel point passed in as a parameter. + * + * Parameters: + * px - {<OpenLayers.Pixel>} + * + * Returns: + * {Float} The pixel point passed in as parameter to calculate the + * distance to. + */ + distanceTo:function(px) { + return Math.sqrt( + Math.pow(this.x - px.x, 2) + + Math.pow(this.y - px.y, 2) + ); + }, + + /** + * APIMethod: add + * + * Parameters: + * x - {Integer} + * y - {Integer} + * + * Returns: + * {<OpenLayers.Pixel>} A new Pixel with this pixel's x&y augmented by the + * values passed in. + */ + add:function(x, y) { + if ( (x == null) || (y == null) ) { + throw new TypeError('Pixel.add cannot receive null values'); + } + return new OpenLayers.Pixel(this.x + x, this.y + y); + }, + + /** + * APIMethod: offset + * + * Parameters + * px - {<OpenLayers.Pixel>|Object} An OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * + * Returns: + * {<OpenLayers.Pixel>} A new Pixel with this pixel's x&y augmented by the + * x&y values of the pixel passed in. + */ + offset:function(px) { + var newPx = this.clone(); + if (px) { + newPx = this.add(px.x, px.y); + } + return newPx; + }, + + CLASS_NAME: "OpenLayers.Pixel" +}); +/* ====================================================================== + OpenLayers/BaseTypes/Size.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Size + * Instances of this class represent a width/height pair + */ +OpenLayers.Size = OpenLayers.Class({ + + /** + * APIProperty: w + * {Number} width + */ + w: 0.0, + + /** + * APIProperty: h + * {Number} height + */ + h: 0.0, + + + /** + * Constructor: OpenLayers.Size + * Create an instance of OpenLayers.Size + * + * Parameters: + * w - {Number} width + * h - {Number} height + */ + initialize: function(w, h) { + this.w = parseFloat(w); + this.h = parseFloat(h); + }, + + /** + * Method: toString + * Return the string representation of a size object + * + * Returns: + * {String} The string representation of OpenLayers.Size object. + * (e.g. <i>"w=55,h=66"</i>) + */ + toString:function() { + return ("w=" + this.w + ",h=" + this.h); + }, + + /** + * APIMethod: clone + * Create a clone of this size object + * + * Returns: + * {<OpenLayers.Size>} A new OpenLayers.Size object with the same w and h + * values + */ + clone:function() { + return new OpenLayers.Size(this.w, this.h); + }, + + /** + * + * APIMethod: equals + * Determine where this size is equal to another + * + * Parameters: + * sz - {<OpenLayers.Size>|Object} An OpenLayers.Size or an object with + * a 'w' and 'h' properties. + * + * Returns: + * {Boolean} The passed in size has the same h and w properties as this one. + * Note that if sz passed in is null, returns false. + */ + equals:function(sz) { + var equals = false; + if (sz != null) { + equals = ((this.w == sz.w && this.h == sz.h) || + (isNaN(this.w) && isNaN(this.h) && isNaN(sz.w) && isNaN(sz.h))); + } + return equals; + }, + + CLASS_NAME: "OpenLayers.Size" +}); +/* ====================================================================== + OpenLayers/Console.js + ====================================================================== */ + +/* 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 + */ + +/** + * Namespace: OpenLayers.Console + * The OpenLayers.Console namespace is used for debugging and error logging. + * If the Firebug Lite (../Firebug/firebug.js) is included before this script, + * calls to OpenLayers.Console methods will get redirected to window.console. + * This makes use of the Firebug extension where available and allows for + * cross-browser debugging Firebug style. + * + * Note: + * Note that behavior will differ with the Firebug extention and Firebug Lite. + * Most notably, the Firebug Lite console does not currently allow for + * hyperlinks to code or for clicking on object to explore their properties. + * + */ +OpenLayers.Console = { + /** + * Create empty functions for all console methods. The real value of these + * properties will be set if Firebug Lite (../Firebug/firebug.js script) is + * included. We explicitly require the Firebug Lite script to trigger + * functionality of the OpenLayers.Console methods. + */ + + /** + * APIFunction: log + * Log an object in the console. The Firebug Lite console logs string + * representation of objects. Given multiple arguments, they will + * be cast to strings and logged with a space delimiter. If the first + * argument is a string with printf-like formatting, subsequent arguments + * will be used in string substitution. Any additional arguments (beyond + * the number substituted in a format string) will be appended in a space- + * delimited line. + * + * Parameters: + * object - {Object} + */ + log: function() {}, + + /** + * APIFunction: debug + * Writes a message to the console, including a hyperlink to the line + * where it was called. + * + * May be called with multiple arguments as with OpenLayers.Console.log(). + * + * Parameters: + * object - {Object} + */ + debug: function() {}, + + /** + * APIFunction: info + * Writes a message to the console with the visual "info" icon and color + * coding and a hyperlink to the line where it was called. + * + * May be called with multiple arguments as with OpenLayers.Console.log(). + * + * Parameters: + * object - {Object} + */ + info: function() {}, + + /** + * APIFunction: warn + * Writes a message to the console with the visual "warning" icon and + * color coding and a hyperlink to the line where it was called. + * + * May be called with multiple arguments as with OpenLayers.Console.log(). + * + * Parameters: + * object - {Object} + */ + warn: function() {}, + + /** + * APIFunction: error + * Writes a message to the console with the visual "error" icon and color + * coding and a hyperlink to the line where it was called. + * + * May be called with multiple arguments as with OpenLayers.Console.log(). + * + * Parameters: + * object - {Object} + */ + error: function() {}, + + /** + * APIFunction: userError + * A single interface for showing error messages to the user. The default + * behavior is a Javascript alert, though this can be overridden by + * reassigning OpenLayers.Console.userError to a different function. + * + * Expects a single error message + * + * Parameters: + * error - {Object} + */ + userError: function(error) { + alert(error); + }, + + /** + * APIFunction: assert + * Tests that an expression is true. If not, it will write a message to + * the console and throw an exception. + * + * May be called with multiple arguments as with OpenLayers.Console.log(). + * + * Parameters: + * object - {Object} + */ + assert: function() {}, + + /** + * APIFunction: dir + * Prints an interactive listing of all properties of the object. This + * looks identical to the view that you would see in the DOM tab. + * + * Parameters: + * object - {Object} + */ + dir: function() {}, + + /** + * APIFunction: dirxml + * Prints the XML source tree of an HTML or XML element. This looks + * identical to the view that you would see in the HTML tab. You can click + * on any node to inspect it in the HTML tab. + * + * Parameters: + * object - {Object} + */ + dirxml: function() {}, + + /** + * APIFunction: trace + * Prints an interactive stack trace of JavaScript execution at the point + * where it is called. The stack trace details the functions on the stack, + * as well as the values that were passed as arguments to each function. + * You can click each function to take you to its source in the Script tab, + * and click each argument value to inspect it in the DOM or HTML tabs. + * + */ + trace: function() {}, + + /** + * APIFunction: group + * Writes a message to the console and opens a nested block to indent all + * future messages sent to the console. Call OpenLayers.Console.groupEnd() + * to close the block. + * + * May be called with multiple arguments as with OpenLayers.Console.log(). + * + * Parameters: + * object - {Object} + */ + group: function() {}, + + /** + * APIFunction: groupEnd + * Closes the most recently opened block created by a call to + * OpenLayers.Console.group + */ + groupEnd: function() {}, + + /** + * APIFunction: time + * Creates a new timer under the given name. Call + * OpenLayers.Console.timeEnd(name) + * with the same name to stop the timer and print the time elapsed. + * + * Parameters: + * name - {String} + */ + time: function() {}, + + /** + * APIFunction: timeEnd + * Stops a timer created by a call to OpenLayers.Console.time(name) and + * writes the time elapsed. + * + * Parameters: + * name - {String} + */ + timeEnd: function() {}, + + /** + * APIFunction: profile + * Turns on the JavaScript profiler. The optional argument title would + * contain the text to be printed in the header of the profile report. + * + * This function is not currently implemented in Firebug Lite. + * + * Parameters: + * title - {String} Optional title for the profiler + */ + profile: function() {}, + + /** + * APIFunction: profileEnd + * Turns off the JavaScript profiler and prints its report. + * + * This function is not currently implemented in Firebug Lite. + */ + profileEnd: function() {}, + + /** + * APIFunction: count + * Writes the number of times that the line of code where count was called + * was executed. The optional argument title will print a message in + * addition to the number of the count. + * + * This function is not currently implemented in Firebug Lite. + * + * Parameters: + * title - {String} Optional title to be printed with count + */ + count: function() {}, + + CLASS_NAME: "OpenLayers.Console" +}; + +/** + * Execute an anonymous function to extend the OpenLayers.Console namespace + * if the firebug.js script is included. This closure is used so that the + * "scripts" and "i" variables don't pollute the global namespace. + */ +(function() { + /** + * If Firebug Lite is included (before this script), re-route all + * OpenLayers.Console calls to the console object. + */ + var scripts = document.getElementsByTagName("script"); + for(var i=0, len=scripts.length; i<len; ++i) { + if(scripts[i].src.indexOf("firebug.js") != -1) { + if(console) { + OpenLayers.Util.extend(OpenLayers.Console, console); + break; + } + } + } +})(); +/* ====================================================================== + OpenLayers/Lang.js + ====================================================================== */ + +/* 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.js + * @requires OpenLayers/Console.js + */ + +/** + * Namespace: OpenLayers.Lang + * Internationalization namespace. Contains dictionaries in various languages + * and methods to set and get the current language. + */ +OpenLayers.Lang = { + + /** + * Property: code + * {String} Current language code to use in OpenLayers. Use the + * <setCode> method to set this value and the <getCode> method to + * retrieve it. + */ + code: null, + + /** + * APIProperty: defaultCode + * {String} Default language to use when a specific language can't be + * found. Default is "en". + */ + defaultCode: "en", + + /** + * APIFunction: getCode + * Get the current language code. + * + * Returns: + * {String} The current language code. + */ + getCode: function() { + if(!OpenLayers.Lang.code) { + OpenLayers.Lang.setCode(); + } + return OpenLayers.Lang.code; + }, + + /** + * APIFunction: setCode + * Set the language code for string translation. This code is used by + * the <OpenLayers.Lang.translate> method. + * + * Parameters: + * code - {String} These codes follow the IETF recommendations at + * http://www.ietf.org/rfc/rfc3066.txt. If no value is set, the + * browser's language setting will be tested. If no <OpenLayers.Lang> + * dictionary exists for the code, the <OpenLayers.String.defaultLang> + * will be used. + */ + setCode: function(code) { + var lang; + if(!code) { + code = (OpenLayers.BROWSER_NAME == "msie") ? + navigator.userLanguage : navigator.language; + } + var parts = code.split('-'); + parts[0] = parts[0].toLowerCase(); + if(typeof OpenLayers.Lang[parts[0]] == "object") { + lang = parts[0]; + } + + // check for regional extensions + if(parts[1]) { + var testLang = parts[0] + '-' + parts[1].toUpperCase(); + if(typeof OpenLayers.Lang[testLang] == "object") { + lang = testLang; + } + } + if(!lang) { + OpenLayers.Console.warn( + 'Failed to find OpenLayers.Lang.' + parts.join("-") + + ' dictionary, falling back to default language' + ); + lang = OpenLayers.Lang.defaultCode; + } + + OpenLayers.Lang.code = lang; + }, + + /** + * APIMethod: translate + * Looks up a key from a dictionary based on the current language string. + * The value of <getCode> will be used to determine the appropriate + * dictionary. Dictionaries are stored in <OpenLayers.Lang>. + * + * Parameters: + * key - {String} The key for an i18n string value in the dictionary. + * context - {Object} Optional context to be used with + * <OpenLayers.String.format>. + * + * Returns: + * {String} A internationalized string. + */ + translate: function(key, context) { + var dictionary = OpenLayers.Lang[OpenLayers.Lang.getCode()]; + var message = dictionary && dictionary[key]; + if(!message) { + // Message not found, fall back to message key + message = key; + } + if(context) { + message = OpenLayers.String.format(message, context); + } + return message; + } + +}; + + +/** + * APIMethod: OpenLayers.i18n + * Alias for <OpenLayers.Lang.translate>. Looks up a key from a dictionary + * based on the current language string. The value of + * <OpenLayers.Lang.getCode> will be used to determine the appropriate + * dictionary. Dictionaries are stored in <OpenLayers.Lang>. + * + * Parameters: + * key - {String} The key for an i18n string value in the dictionary. + * context - {Object} Optional context to be used with + * <OpenLayers.String.format>. + * + * Returns: + * {String} A internationalized string. + */ +OpenLayers.i18n = OpenLayers.Lang.translate; +/* ====================================================================== + OpenLayers/Util.js + ====================================================================== */ + +/* 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.js + * @requires OpenLayers/BaseTypes/Bounds.js + * @requires OpenLayers/BaseTypes/Element.js + * @requires OpenLayers/BaseTypes/LonLat.js + * @requires OpenLayers/BaseTypes/Pixel.js + * @requires OpenLayers/BaseTypes/Size.js + * @requires OpenLayers/Lang.js + */ + +/** + * Namespace: Util + */ +OpenLayers.Util = OpenLayers.Util || {}; + +/** + * Function: getElement + * This is the old $() from prototype + * + * Parameters: + * e - {String or DOMElement or Window} + * + * Returns: + * {Array(DOMElement) or DOMElement} + */ +OpenLayers.Util.getElement = function() { + var elements = []; + + for (var i=0, len=arguments.length; i<len; i++) { + var element = arguments[i]; + if (typeof element == 'string') { + element = document.getElementById(element); + } + if (arguments.length == 1) { + return element; + } + elements.push(element); + } + return elements; +}; + +/** + * Function: isElement + * A cross-browser implementation of "e instanceof Element". + * + * Parameters: + * o - {Object} The object to test. + * + * Returns: + * {Boolean} + */ +OpenLayers.Util.isElement = function(o) { + return !!(o && o.nodeType === 1); +}; + +/** + * Function: isArray + * Tests that the provided object is an array. + * This test handles the cross-IFRAME case not caught + * by "a instanceof Array" and should be used instead. + * + * Parameters: + * a - {Object} the object test. + * + * Returns: + * {Boolean} true if the object is an array. + */ +OpenLayers.Util.isArray = function(a) { + return (Object.prototype.toString.call(a) === '[object Array]'); +}; + +/** + * Function: removeItem + * Remove an object from an array. Iterates through the array + * to find the item, then removes it. + * + * Parameters: + * array - {Array} + * item - {Object} + * + * Returns: + * {Array} A reference to the array + */ +OpenLayers.Util.removeItem = function(array, item) { + for(var i = array.length - 1; i >= 0; i--) { + if(array[i] == item) { + array.splice(i,1); + //break;more than once?? + } + } + return array; +}; + +/** + * Function: indexOf + * Seems to exist already in FF, but not in MOZ. + * + * Parameters: + * array - {Array} + * obj - {*} + * + * Returns: + * {Integer} The index at which the first object was found in the array. + * If not found, returns -1. + */ +OpenLayers.Util.indexOf = function(array, obj) { + // use the build-in function if available. + if (typeof array.indexOf == "function") { + return array.indexOf(obj); + } else { + for (var i = 0, len = array.length; i < len; i++) { + if (array[i] == obj) { + return i; + } + } + return -1; + } +}; + + +/** + * Property: dotless + * {RegExp} + * Compiled regular expression to match dots ("."). This is used for replacing + * dots in identifiers. Because object identifiers are frequently used for + * DOM element identifiers by the library, we avoid using dots to make for + * more sensible CSS selectors. + * + * TODO: Use a module pattern to avoid bloating the API with stuff like this. + */ +OpenLayers.Util.dotless = /\./g; + +/** + * Function: modifyDOMElement + * + * Modifies many properties of a DOM element all at once. Passing in + * null to an individual parameter will avoid setting the attribute. + * + * Parameters: + * element - {DOMElement} DOM element to modify. + * id - {String} The element id attribute to set. Note that dots (".") will be + * replaced with underscore ("_") in setting the element id. + * px - {<OpenLayers.Pixel>|Object} The element left and top position, + * OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * sz - {<OpenLayers.Size>|Object} The element width and height, + * OpenLayers.Size or an object with a + * 'w' and 'h' properties. + * position - {String} The position attribute. eg: absolute, + * relative, etc. + * border - {String} The style.border attribute. eg: + * solid black 2px + * overflow - {String} The style.overview attribute. + * opacity - {Float} Fractional value (0.0 - 1.0) + */ +OpenLayers.Util.modifyDOMElement = function(element, id, px, sz, position, + border, overflow, opacity) { + + if (id) { + element.id = id.replace(OpenLayers.Util.dotless, "_"); + } + if (px) { + element.style.left = px.x + "px"; + element.style.top = px.y + "px"; + } + if (sz) { + element.style.width = sz.w + "px"; + element.style.height = sz.h + "px"; + } + if (position) { + element.style.position = position; + } + if (border) { + element.style.border = border; + } + if (overflow) { + element.style.overflow = overflow; + } + if (parseFloat(opacity) >= 0.0 && parseFloat(opacity) < 1.0) { + element.style.filter = 'alpha(opacity=' + (opacity * 100) + ')'; + element.style.opacity = opacity; + } else if (parseFloat(opacity) == 1.0) { + element.style.filter = ''; + element.style.opacity = ''; + } +}; + +/** + * Function: createDiv + * Creates a new div and optionally set some standard attributes. + * Null may be passed to each parameter if you do not wish to + * set a particular attribute. + * Note - zIndex is NOT set on the resulting div. + * + * Parameters: + * id - {String} An identifier for this element. If no id is + * passed an identifier will be created + * automatically. Note that dots (".") will be replaced with + * underscore ("_") when generating ids. + * px - {<OpenLayers.Pixel>|Object} The element left and top position, + * OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * sz - {<OpenLayers.Size>|Object} The element width and height, + * OpenLayers.Size or an object with a + * 'w' and 'h' properties. + * imgURL - {String} A url pointing to an image to use as a + * background image. + * position - {String} The style.position value. eg: absolute, + * relative etc. + * border - {String} The the style.border value. + * eg: 2px solid black + * overflow - {String} The style.overflow value. Eg. hidden + * opacity - {Float} Fractional value (0.0 - 1.0) + * + * Returns: + * {DOMElement} A DOM Div created with the specified attributes. + */ +OpenLayers.Util.createDiv = function(id, px, sz, imgURL, position, + border, overflow, opacity) { + + var dom = document.createElement('div'); + + if (imgURL) { + dom.style.backgroundImage = 'url(' + imgURL + ')'; + } + + //set generic properties + if (!id) { + id = OpenLayers.Util.createUniqueID("OpenLayersDiv"); + } + if (!position) { + position = "absolute"; + } + OpenLayers.Util.modifyDOMElement(dom, id, px, sz, position, + border, overflow, opacity); + + return dom; +}; + +/** + * Function: createImage + * Creates an img element with specific attribute values. + * + * Parameters: + * id - {String} The id field for the img. If none assigned one will be + * automatically generated. + * px - {<OpenLayers.Pixel>|Object} The element left and top position, + * OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * sz - {<OpenLayers.Size>|Object} The element width and height, + * OpenLayers.Size or an object with a + * 'w' and 'h' properties. + * imgURL - {String} The url to use as the image source. + * position - {String} The style.position value. + * border - {String} The border to place around the image. + * opacity - {Float} Fractional value (0.0 - 1.0) + * delayDisplay - {Boolean} If true waits until the image has been + * loaded. + * + * Returns: + * {DOMElement} A DOM Image created with the specified attributes. + */ +OpenLayers.Util.createImage = function(id, px, sz, imgURL, position, border, + opacity, delayDisplay) { + + var image = document.createElement("img"); + + //set generic properties + if (!id) { + id = OpenLayers.Util.createUniqueID("OpenLayersDiv"); + } + if (!position) { + position = "relative"; + } + OpenLayers.Util.modifyDOMElement(image, id, px, sz, position, + border, null, opacity); + + if (delayDisplay) { + image.style.display = "none"; + function display() { + image.style.display = ""; + OpenLayers.Event.stopObservingElement(image); + } + OpenLayers.Event.observe(image, "load", display); + OpenLayers.Event.observe(image, "error", display); + } + + //set special properties + image.style.alt = id; + image.galleryImg = "no"; + if (imgURL) { + image.src = imgURL; + } + + return image; +}; + +/** + * Property: IMAGE_RELOAD_ATTEMPTS + * {Integer} How many times should we try to reload an image before giving up? + * Default is 0 + */ +OpenLayers.IMAGE_RELOAD_ATTEMPTS = 0; + +/** + * Property: alphaHackNeeded + * {Boolean} true if the png alpha hack is necessary and possible, false otherwise. + */ +OpenLayers.Util.alphaHackNeeded = null; + +/** + * Function: alphaHack + * Checks whether it's necessary (and possible) to use the png alpha + * hack which allows alpha transparency for png images under Internet + * Explorer. + * + * Returns: + * {Boolean} true if the png alpha hack is necessary and possible, false otherwise. + */ +OpenLayers.Util.alphaHack = function() { + if (OpenLayers.Util.alphaHackNeeded == null) { + var arVersion = navigator.appVersion.split("MSIE"); + var version = parseFloat(arVersion[1]); + var filter = false; + + // IEs4Lin dies when trying to access document.body.filters, because + // the property is there, but requires a DLL that can't be provided. This + // means that we need to wrap this in a try/catch so that this can + // continue. + + try { + filter = !!(document.body.filters); + } catch (e) {} + + OpenLayers.Util.alphaHackNeeded = (filter && + (version >= 5.5) && (version < 7)); + } + return OpenLayers.Util.alphaHackNeeded; +}; + +/** + * Function: modifyAlphaImageDiv + * + * Parameters: + * div - {DOMElement} Div containing Alpha-adjusted Image + * id - {String} + * px - {<OpenLayers.Pixel>|Object} OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * sz - {<OpenLayers.Size>|Object} OpenLayers.Size or an object with + * a 'w' and 'h' properties. + * imgURL - {String} + * position - {String} + * border - {String} + * sizing - {String} 'crop', 'scale', or 'image'. Default is "scale" + * opacity - {Float} Fractional value (0.0 - 1.0) + */ +OpenLayers.Util.modifyAlphaImageDiv = function(div, id, px, sz, imgURL, + position, border, sizing, + opacity) { + + OpenLayers.Util.modifyDOMElement(div, id, px, sz, position, + null, null, opacity); + + var img = div.childNodes[0]; + + if (imgURL) { + img.src = imgURL; + } + OpenLayers.Util.modifyDOMElement(img, div.id + "_innerImage", null, sz, + "relative", border); + + if (OpenLayers.Util.alphaHack()) { + if(div.style.display != "none") { + div.style.display = "inline-block"; + } + if (sizing == null) { + sizing = "scale"; + } + + div.style.filter = "progid:DXImageTransform.Microsoft" + + ".AlphaImageLoader(src='" + img.src + "', " + + "sizingMethod='" + sizing + "')"; + if (parseFloat(div.style.opacity) >= 0.0 && + parseFloat(div.style.opacity) < 1.0) { + div.style.filter += " alpha(opacity=" + div.style.opacity * 100 + ")"; + } + + img.style.filter = "alpha(opacity=0)"; + } +}; + +/** + * Function: createAlphaImageDiv + * + * Parameters: + * id - {String} + * px - {<OpenLayers.Pixel>|Object} OpenLayers.Pixel or an object with + * a 'x' and 'y' properties. + * sz - {<OpenLayers.Size>|Object} OpenLayers.Size or an object with + * a 'w' and 'h' properties. + * imgURL - {String} + * position - {String} + * border - {String} + * sizing - {String} 'crop', 'scale', or 'image'. Default is "scale" + * opacity - {Float} Fractional value (0.0 - 1.0) + * delayDisplay - {Boolean} If true waits until the image has been + * loaded. + * + * Returns: + * {DOMElement} A DOM Div created with a DOM Image inside it. If the hack is + * needed for transparency in IE, it is added. + */ +OpenLayers.Util.createAlphaImageDiv = function(id, px, sz, imgURL, + position, border, sizing, + opacity, delayDisplay) { + + var div = OpenLayers.Util.createDiv(); + var img = OpenLayers.Util.createImage(null, null, null, null, null, null, + null, delayDisplay); + img.className = "olAlphaImg"; + div.appendChild(img); + + OpenLayers.Util.modifyAlphaImageDiv(div, id, px, sz, imgURL, position, + border, sizing, opacity); + + return div; +}; + + +/** + * Function: upperCaseObject + * Creates a new hashtable and copies over all the keys from the + * passed-in object, but storing them under an uppercased + * version of the key at which they were stored. + * + * Parameters: + * object - {Object} + * + * Returns: + * {Object} A new Object with all the same keys but uppercased + */ +OpenLayers.Util.upperCaseObject = function (object) { + var uObject = {}; + for (var key in object) { + uObject[key.toUpperCase()] = object[key]; + } + return uObject; +}; + +/** + * Function: applyDefaults + * Takes an object and copies any properties that don't exist from + * another properties, by analogy with OpenLayers.Util.extend() from + * Prototype.js. + * + * Parameters: + * to - {Object} The destination object. + * from - {Object} The source object. Any properties of this object that + * are undefined in the to object will be set on the to object. + * + * Returns: + * {Object} A reference to the to object. Note that the to argument is modified + * in place and returned by this function. + */ +OpenLayers.Util.applyDefaults = function (to, from) { + to = to || {}; + /* + * FF/Windows < 2.0.0.13 reports "Illegal operation on WrappedNative + * prototype object" when calling hawOwnProperty if the source object is an + * instance of window.Event. + */ + var fromIsEvt = typeof window.Event == "function" + && from instanceof window.Event; + + for (var key in from) { + if (to[key] === undefined || + (!fromIsEvt && from.hasOwnProperty + && from.hasOwnProperty(key) && !to.hasOwnProperty(key))) { + to[key] = from[key]; + } + } + /** + * IE doesn't include the toString property when iterating over an object's + * properties with the for(property in object) syntax. Explicitly check if + * the source has its own toString property. + */ + if(!fromIsEvt && from && from.hasOwnProperty + && from.hasOwnProperty('toString') && !to.hasOwnProperty('toString')) { + to.toString = from.toString; + } + + return to; +}; + +/** + * Function: getParameterString + * + * Parameters: + * params - {Object} + * + * Returns: + * {String} A concatenation of the properties of an object in + * http parameter notation. + * (ex. <i>"key1=value1&key2=value2&key3=value3"</i>) + * If a parameter is actually a list, that parameter will then + * be set to a comma-seperated list of values (foo,bar) instead + * of being URL escaped (foo%3Abar). + */ +OpenLayers.Util.getParameterString = function(params) { + var paramsArray = []; + + for (var key in params) { + var value = params[key]; + if ((value != null) && (typeof value != 'function')) { + var encodedValue; + if (typeof value == 'object' && value.constructor == Array) { + /* value is an array; encode items and separate with "," */ + var encodedItemArray = []; + var item; + for (var itemIndex=0, len=value.length; itemIndex<len; itemIndex++) { + item = value[itemIndex]; + encodedItemArray.push(encodeURIComponent( + (item === null || item === undefined) ? "" : item) + ); + } + encodedValue = encodedItemArray.join(","); + } + else { + /* value is a string; simply encode */ + encodedValue = encodeURIComponent(value); + } + paramsArray.push(encodeURIComponent(key) + "=" + encodedValue); + } + } + + return paramsArray.join("&"); +}; + +/** + * Function: urlAppend + * Appends a parameter string to a url. This function includes the logic for + * using the appropriate character (none, & or ?) to append to the url before + * appending the param string. + * + * Parameters: + * url - {String} The url to append to + * paramStr - {String} The param string to append + * + * Returns: + * {String} The new url + */ +OpenLayers.Util.urlAppend = function(url, paramStr) { + var newUrl = url; + if(paramStr) { + var parts = (url + " ").split(/[?&]/); + newUrl += (parts.pop() === " " ? + paramStr : + parts.length ? "&" + paramStr : "?" + paramStr); + } + return newUrl; +}; + +/** + * Function: getImagesLocation + * + * Returns: + * {String} The fully formatted image location string + */ +OpenLayers.Util.getImagesLocation = function() { + return OpenLayers.ImgPath || (OpenLayers._getScriptLocation() + "img/"); +}; + +/** + * Function: getImageLocation + * + * Returns: + * {String} The fully formatted location string for a specified image + */ +OpenLayers.Util.getImageLocation = function(image) { + return OpenLayers.Util.getImagesLocation() + image; +}; + + +/** + * Function: Try + * Execute functions until one of them doesn't throw an error. + * Capitalized because "try" is a reserved word in JavaScript. + * Taken directly from OpenLayers.Util.Try() + * + * Parameters: + * [*] - {Function} Any number of parameters may be passed to Try() + * It will attempt to execute each of them until one of them + * successfully executes. + * If none executes successfully, returns null. + * + * Returns: + * {*} The value returned by the first successfully executed function. + */ +OpenLayers.Util.Try = function() { + var returnValue = null; + + for (var i=0, len=arguments.length; i<len; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; +}; + +/** + * Function: getXmlNodeValue + * + * Parameters: + * node - {XMLNode} + * + * Returns: + * {String} The text value of the given node, without breaking in firefox or IE + */ +OpenLayers.Util.getXmlNodeValue = function(node) { + var val = null; + OpenLayers.Util.Try( + function() { + val = node.text; + if (!val) { + val = node.textContent; + } + if (!val) { + val = node.firstChild.nodeValue; + } + }, + function() { + val = node.textContent; + }); + return val; +}; + +/** + * Function: mouseLeft + * + * Parameters: + * evt - {Event} + * div - {HTMLDivElement} + * + * Returns: + * {Boolean} + */ +OpenLayers.Util.mouseLeft = function (evt, div) { + // start with the element to which the mouse has moved + var target = (evt.relatedTarget) ? evt.relatedTarget : evt.toElement; + // walk up the DOM tree. + while (target != div && target != null) { + target = target.parentNode; + } + // if the target we stop at isn't the div, then we've left the div. + return (target != div); +}; + +/** + * Property: precision + * {Number} The number of significant digits to retain to avoid + * floating point precision errors. + * + * We use 14 as a "safe" default because, although IEEE 754 double floats + * (standard on most modern operating systems) support up to about 16 + * significant digits, 14 significant digits are sufficient to represent + * sub-millimeter accuracy in any coordinate system that anyone is likely to + * use with OpenLayers. + * + * If DEFAULT_PRECISION is set to 0, the original non-truncating behavior + * of OpenLayers <2.8 is preserved. Be aware that this will cause problems + * with certain projections, e.g. spherical Mercator. + * + */ +OpenLayers.Util.DEFAULT_PRECISION = 14; + +/** + * Function: toFloat + * Convenience method to cast an object to a Number, rounded to the + * desired floating point precision. + * + * Parameters: + * number - {Number} The number to cast and round. + * precision - {Number} An integer suitable for use with + * Number.toPrecision(). Defaults to OpenLayers.Util.DEFAULT_PRECISION. + * If set to 0, no rounding is performed. + * + * Returns: + * {Number} The cast, rounded number. + */ +OpenLayers.Util.toFloat = function (number, precision) { + if (precision == null) { + precision = OpenLayers.Util.DEFAULT_PRECISION; + } + if (typeof number !== "number") { + number = parseFloat(number); + } + return precision === 0 ? number : + parseFloat(number.toPrecision(precision)); +}; + +/** + * Function: rad + * + * Parameters: + * x - {Float} + * + * Returns: + * {Float} + */ +OpenLayers.Util.rad = function(x) {return x*Math.PI/180;}; + +/** + * Function: deg + * + * Parameters: + * x - {Float} + * + * Returns: + * {Float} + */ +OpenLayers.Util.deg = function(x) {return x*180/Math.PI;}; + +/** + * Property: VincentyConstants + * {Object} Constants for Vincenty functions. + */ +OpenLayers.Util.VincentyConstants = { + a: 6378137, + b: 6356752.3142, + f: 1/298.257223563 +}; + +/** + * APIFunction: distVincenty + * Given two objects representing points with geographic coordinates, this + * calculates the distance between those points on the surface of an + * ellipsoid. + * + * Parameters: + * p1 - {<OpenLayers.LonLat>} (or any object with both .lat, .lon properties) + * p2 - {<OpenLayers.LonLat>} (or any object with both .lat, .lon properties) + * + * Returns: + * {Float} The distance (in km) between the two input points as measured on an + * ellipsoid. Note that the input point objects must be in geographic + * coordinates (decimal degrees) and the return distance is in kilometers. + */ +OpenLayers.Util.distVincenty = function(p1, p2) { + var ct = OpenLayers.Util.VincentyConstants; + var a = ct.a, b = ct.b, f = ct.f; + + var L = OpenLayers.Util.rad(p2.lon - p1.lon); + var U1 = Math.atan((1-f) * Math.tan(OpenLayers.Util.rad(p1.lat))); + var U2 = Math.atan((1-f) * Math.tan(OpenLayers.Util.rad(p2.lat))); + var sinU1 = Math.sin(U1), cosU1 = Math.cos(U1); + var sinU2 = Math.sin(U2), cosU2 = Math.cos(U2); + var lambda = L, lambdaP = 2*Math.PI; + var iterLimit = 20; + while (Math.abs(lambda-lambdaP) > 1e-12 && --iterLimit>0) { + var sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda); + var sinSigma = Math.sqrt((cosU2*sinLambda) * (cosU2*sinLambda) + + (cosU1*sinU2-sinU1*cosU2*cosLambda) * (cosU1*sinU2-sinU1*cosU2*cosLambda)); + if (sinSigma==0) { + return 0; // co-incident points + } + var cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda; + var sigma = Math.atan2(sinSigma, cosSigma); + var alpha = Math.asin(cosU1 * cosU2 * sinLambda / sinSigma); + var cosSqAlpha = Math.cos(alpha) * Math.cos(alpha); + var cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha; + var C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha)); + lambdaP = lambda; + lambda = L + (1-C) * f * Math.sin(alpha) * + (sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM))); + } + if (iterLimit==0) { + return NaN; // formula failed to converge + } + var uSq = cosSqAlpha * (a*a - b*b) / (b*b); + var A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq))); + var B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq))); + var deltaSigma = B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)- + B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM))); + var s = b*A*(sigma-deltaSigma); + var d = s.toFixed(3)/1000; // round to 1mm precision + return d; +}; + +/** + * APIFunction: destinationVincenty + * Calculate destination point given start point lat/long (numeric degrees), + * bearing (numeric degrees) & distance (in m). + * Adapted from Chris Veness work, see + * http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>} (or any object with both .lat, .lon + * properties) The start point. + * brng - {Float} The bearing (degrees). + * dist - {Float} The ground distance (meters). + * + * Returns: + * {<OpenLayers.LonLat>} The destination point. + */ +OpenLayers.Util.destinationVincenty = function(lonlat, brng, dist) { + var u = OpenLayers.Util; + var ct = u.VincentyConstants; + var a = ct.a, b = ct.b, f = ct.f; + + var lon1 = lonlat.lon; + var lat1 = lonlat.lat; + + var s = dist; + var alpha1 = u.rad(brng); + var sinAlpha1 = Math.sin(alpha1); + var cosAlpha1 = Math.cos(alpha1); + + var tanU1 = (1-f) * Math.tan(u.rad(lat1)); + var cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1*cosU1; + var sigma1 = Math.atan2(tanU1, cosAlpha1); + var sinAlpha = cosU1 * sinAlpha1; + var cosSqAlpha = 1 - sinAlpha*sinAlpha; + var uSq = cosSqAlpha * (a*a - b*b) / (b*b); + var A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq))); + var B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq))); + + var sigma = s / (b*A), sigmaP = 2*Math.PI; + while (Math.abs(sigma-sigmaP) > 1e-12) { + var cos2SigmaM = Math.cos(2*sigma1 + sigma); + var sinSigma = Math.sin(sigma); + var cosSigma = Math.cos(sigma); + var deltaSigma = B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)- + B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM))); + sigmaP = sigma; + sigma = s / (b*A) + deltaSigma; + } + + var tmp = sinU1*sinSigma - cosU1*cosSigma*cosAlpha1; + var lat2 = Math.atan2(sinU1*cosSigma + cosU1*sinSigma*cosAlpha1, + (1-f)*Math.sqrt(sinAlpha*sinAlpha + tmp*tmp)); + var lambda = Math.atan2(sinSigma*sinAlpha1, cosU1*cosSigma - sinU1*sinSigma*cosAlpha1); + var C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha)); + var L = lambda - (1-C) * f * sinAlpha * + (sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM))); + + var revAz = Math.atan2(sinAlpha, -tmp); // final bearing + + return new OpenLayers.LonLat(lon1+u.deg(L), u.deg(lat2)); +}; + +/** + * Function: getParameters + * Parse the parameters from a URL or from the current page itself into a + * JavaScript Object. Note that parameter values with commas are separated + * out into an Array. + * + * Parameters: + * url - {String} Optional url used to extract the query string. + * If url is null or is not supplied, query string is taken + * from the page location. + * options - {Object} Additional options. Optional. + * + * Valid options: + * splitArgs - {Boolean} Split comma delimited params into arrays? Default is + * true. + * + * Returns: + * {Object} An object of key/value pairs from the query string. + */ +OpenLayers.Util.getParameters = function(url, options) { + options = options || {}; + // if no url specified, take it from the location bar + url = (url === null || url === undefined) ? window.location.href : url; + + //parse out parameters portion of url string + var paramsString = ""; + if (OpenLayers.String.contains(url, '?')) { + var start = url.indexOf('?') + 1; + var end = OpenLayers.String.contains(url, "#") ? + url.indexOf('#') : url.length; + paramsString = url.substring(start, end); + } + + var parameters = {}; + var pairs = paramsString.split(/[&;]/); + for(var i=0, len=pairs.length; i<len; ++i) { + var keyValue = pairs[i].split('='); + if (keyValue[0]) { + + var key = keyValue[0]; + try { + key = decodeURIComponent(key); + } catch (err) { + key = unescape(key); + } + + // being liberal by replacing "+" with " " + var value = (keyValue[1] || '').replace(/\+/g, " "); + + try { + value = decodeURIComponent(value); + } catch (err) { + value = unescape(value); + } + + // follow OGC convention of comma delimited values + if (options.splitArgs !== false) { + value = value.split(","); + } + + //if there's only one value, do not return as array + if (value.length == 1) { + value = value[0]; + } + + parameters[key] = value; + } + } + return parameters; +}; + +/** + * Property: lastSeqID + * {Integer} The ever-incrementing count variable. + * Used for generating unique ids. + */ +OpenLayers.Util.lastSeqID = 0; + +/** + * Function: createUniqueID + * Create a unique identifier for this session. Each time this function + * is called, a counter is incremented. The return will be the optional + * prefix (defaults to "id_") appended with the counter value. + * + * Parameters: + * prefix - {String} Optional string to prefix unique id. Default is "id_". + * Note that dots (".") in the prefix will be replaced with underscore ("_"). + * + * Returns: + * {String} A unique id string, built on the passed in prefix. + */ +OpenLayers.Util.createUniqueID = function(prefix) { + if (prefix == null) { + prefix = "id_"; + } else { + prefix = prefix.replace(OpenLayers.Util.dotless, "_"); + } + OpenLayers.Util.lastSeqID += 1; + return prefix + OpenLayers.Util.lastSeqID; +}; + +/** + * Constant: INCHES_PER_UNIT + * {Object} Constant inches per unit -- borrowed from MapServer mapscale.c + * derivation of nautical miles from http://en.wikipedia.org/wiki/Nautical_mile + * Includes the full set of units supported by CS-MAP (http://trac.osgeo.org/csmap/) + * and PROJ.4 (http://trac.osgeo.org/proj/) + * The hardcoded table is maintain in a CS-MAP source code module named CSdataU.c + * The hardcoded table of PROJ.4 units are in pj_units.c. + */ +OpenLayers.INCHES_PER_UNIT = { + 'inches': 1.0, + 'ft': 12.0, + 'mi': 63360.0, + 'm': 39.37, + 'km': 39370, + 'dd': 4374754, + 'yd': 36 +}; +OpenLayers.INCHES_PER_UNIT["in"]= OpenLayers.INCHES_PER_UNIT.inches; +OpenLayers.INCHES_PER_UNIT["degrees"] = OpenLayers.INCHES_PER_UNIT.dd; +OpenLayers.INCHES_PER_UNIT["nmi"] = 1852 * OpenLayers.INCHES_PER_UNIT.m; + +// Units from CS-Map +OpenLayers.METERS_PER_INCH = 0.02540005080010160020; +OpenLayers.Util.extend(OpenLayers.INCHES_PER_UNIT, { + "Inch": OpenLayers.INCHES_PER_UNIT.inches, + "Meter": 1.0 / OpenLayers.METERS_PER_INCH, //EPSG:9001 + "Foot": 0.30480060960121920243 / OpenLayers.METERS_PER_INCH, //EPSG:9003 + "IFoot": 0.30480000000000000000 / OpenLayers.METERS_PER_INCH, //EPSG:9002 + "ClarkeFoot": 0.3047972651151 / OpenLayers.METERS_PER_INCH, //EPSG:9005 + "SearsFoot": 0.30479947153867624624 / OpenLayers.METERS_PER_INCH, //EPSG:9041 + "GoldCoastFoot": 0.30479971018150881758 / OpenLayers.METERS_PER_INCH, //EPSG:9094 + "IInch": 0.02540000000000000000 / OpenLayers.METERS_PER_INCH, + "MicroInch": 0.00002540000000000000 / OpenLayers.METERS_PER_INCH, + "Mil": 0.00000002540000000000 / OpenLayers.METERS_PER_INCH, + "Centimeter": 0.01000000000000000000 / OpenLayers.METERS_PER_INCH, + "Kilometer": 1000.00000000000000000000 / OpenLayers.METERS_PER_INCH, //EPSG:9036 + "Yard": 0.91440182880365760731 / OpenLayers.METERS_PER_INCH, + "SearsYard": 0.914398414616029 / OpenLayers.METERS_PER_INCH, //EPSG:9040 + "IndianYard": 0.91439853074444079983 / OpenLayers.METERS_PER_INCH, //EPSG:9084 + "IndianYd37": 0.91439523 / OpenLayers.METERS_PER_INCH, //EPSG:9085 + "IndianYd62": 0.9143988 / OpenLayers.METERS_PER_INCH, //EPSG:9086 + "IndianYd75": 0.9143985 / OpenLayers.METERS_PER_INCH, //EPSG:9087 + "IndianFoot": 0.30479951 / OpenLayers.METERS_PER_INCH, //EPSG:9080 + "IndianFt37": 0.30479841 / OpenLayers.METERS_PER_INCH, //EPSG:9081 + "IndianFt62": 0.3047996 / OpenLayers.METERS_PER_INCH, //EPSG:9082 + "IndianFt75": 0.3047995 / OpenLayers.METERS_PER_INCH, //EPSG:9083 + "Mile": 1609.34721869443738887477 / OpenLayers.METERS_PER_INCH, + "IYard": 0.91440000000000000000 / OpenLayers.METERS_PER_INCH, //EPSG:9096 + "IMile": 1609.34400000000000000000 / OpenLayers.METERS_PER_INCH, //EPSG:9093 + "NautM": 1852.00000000000000000000 / OpenLayers.METERS_PER_INCH, //EPSG:9030 + "Lat-66": 110943.316488932731 / OpenLayers.METERS_PER_INCH, + "Lat-83": 110946.25736872234125 / OpenLayers.METERS_PER_INCH, + "Decimeter": 0.10000000000000000000 / OpenLayers.METERS_PER_INCH, + "Millimeter": 0.00100000000000000000 / OpenLayers.METERS_PER_INCH, + "Dekameter": 10.00000000000000000000 / OpenLayers.METERS_PER_INCH, + "Decameter": 10.00000000000000000000 / OpenLayers.METERS_PER_INCH, + "Hectometer": 100.00000000000000000000 / OpenLayers.METERS_PER_INCH, + "GermanMeter": 1.0000135965 / OpenLayers.METERS_PER_INCH, //EPSG:9031 + "CaGrid": 0.999738 / OpenLayers.METERS_PER_INCH, + "ClarkeChain": 20.1166194976 / OpenLayers.METERS_PER_INCH, //EPSG:9038 + "GunterChain": 20.11684023368047 / OpenLayers.METERS_PER_INCH, //EPSG:9033 + "BenoitChain": 20.116782494375872 / OpenLayers.METERS_PER_INCH, //EPSG:9062 + "SearsChain": 20.11676512155 / OpenLayers.METERS_PER_INCH, //EPSG:9042 + "ClarkeLink": 0.201166194976 / OpenLayers.METERS_PER_INCH, //EPSG:9039 + "GunterLink": 0.2011684023368047 / OpenLayers.METERS_PER_INCH, //EPSG:9034 + "BenoitLink": 0.20116782494375872 / OpenLayers.METERS_PER_INCH, //EPSG:9063 + "SearsLink": 0.2011676512155 / OpenLayers.METERS_PER_INCH, //EPSG:9043 + "Rod": 5.02921005842012 / OpenLayers.METERS_PER_INCH, + "IntnlChain": 20.1168 / OpenLayers.METERS_PER_INCH, //EPSG:9097 + "IntnlLink": 0.201168 / OpenLayers.METERS_PER_INCH, //EPSG:9098 + "Perch": 5.02921005842012 / OpenLayers.METERS_PER_INCH, + "Pole": 5.02921005842012 / OpenLayers.METERS_PER_INCH, + "Furlong": 201.1684023368046 / OpenLayers.METERS_PER_INCH, + "Rood": 3.778266898 / OpenLayers.METERS_PER_INCH, + "CapeFoot": 0.3047972615 / OpenLayers.METERS_PER_INCH, + "Brealey": 375.00000000000000000000 / OpenLayers.METERS_PER_INCH, + "ModAmFt": 0.304812252984505969011938 / OpenLayers.METERS_PER_INCH, + "Fathom": 1.8288 / OpenLayers.METERS_PER_INCH, + "NautM-UK": 1853.184 / OpenLayers.METERS_PER_INCH, + "50kilometers": 50000.0 / OpenLayers.METERS_PER_INCH, + "150kilometers": 150000.0 / OpenLayers.METERS_PER_INCH +}); + +//unit abbreviations supported by PROJ.4 +OpenLayers.Util.extend(OpenLayers.INCHES_PER_UNIT, { + "mm": OpenLayers.INCHES_PER_UNIT["Meter"] / 1000.0, + "cm": OpenLayers.INCHES_PER_UNIT["Meter"] / 100.0, + "dm": OpenLayers.INCHES_PER_UNIT["Meter"] * 100.0, + "km": OpenLayers.INCHES_PER_UNIT["Meter"] * 1000.0, + "kmi": OpenLayers.INCHES_PER_UNIT["nmi"], //International Nautical Mile + "fath": OpenLayers.INCHES_PER_UNIT["Fathom"], //International Fathom + "ch": OpenLayers.INCHES_PER_UNIT["IntnlChain"], //International Chain + "link": OpenLayers.INCHES_PER_UNIT["IntnlLink"], //International Link + "us-in": OpenLayers.INCHES_PER_UNIT["inches"], //U.S. Surveyor's Inch + "us-ft": OpenLayers.INCHES_PER_UNIT["Foot"], //U.S. Surveyor's Foot + "us-yd": OpenLayers.INCHES_PER_UNIT["Yard"], //U.S. Surveyor's Yard + "us-ch": OpenLayers.INCHES_PER_UNIT["GunterChain"], //U.S. Surveyor's Chain + "us-mi": OpenLayers.INCHES_PER_UNIT["Mile"], //U.S. Surveyor's Statute Mile + "ind-yd": OpenLayers.INCHES_PER_UNIT["IndianYd37"], //Indian Yard + "ind-ft": OpenLayers.INCHES_PER_UNIT["IndianFt37"], //Indian Foot + "ind-ch": 20.11669506 / OpenLayers.METERS_PER_INCH //Indian Chain +}); + +/** + * Constant: DOTS_PER_INCH + * {Integer} 72 (A sensible default) + */ +OpenLayers.DOTS_PER_INCH = 72; + +/** + * Function: normalizeScale + * + * Parameters: + * scale - {float} + * + * Returns: + * {Float} A normalized scale value, in 1 / X format. + * This means that if a value less than one ( already 1/x) is passed + * in, it just returns scale directly. Otherwise, it returns + * 1 / scale + */ +OpenLayers.Util.normalizeScale = function (scale) { + var normScale = (scale > 1.0) ? (1.0 / scale) + : scale; + return normScale; +}; + +/** + * Function: getResolutionFromScale + * + * Parameters: + * scale - {Float} + * units - {String} Index into OpenLayers.INCHES_PER_UNIT hashtable. + * Default is degrees + * + * Returns: + * {Float} The corresponding resolution given passed-in scale and unit + * parameters. If the given scale is falsey, the returned resolution will + * be undefined. + */ +OpenLayers.Util.getResolutionFromScale = function (scale, units) { + var resolution; + if (scale) { + if (units == null) { + units = "degrees"; + } + var normScale = OpenLayers.Util.normalizeScale(scale); + resolution = 1 / (normScale * OpenLayers.INCHES_PER_UNIT[units] + * OpenLayers.DOTS_PER_INCH); + } + return resolution; +}; + +/** + * Function: getScaleFromResolution + * + * Parameters: + * resolution - {Float} + * units - {String} Index into OpenLayers.INCHES_PER_UNIT hashtable. + * Default is degrees + * + * Returns: + * {Float} The corresponding scale given passed-in resolution and unit + * parameters. + */ +OpenLayers.Util.getScaleFromResolution = function (resolution, units) { + + if (units == null) { + units = "degrees"; + } + + var scale = resolution * OpenLayers.INCHES_PER_UNIT[units] * + OpenLayers.DOTS_PER_INCH; + return scale; +}; + +/** + * Function: pagePosition + * Calculates the position of an element on the page (see + * http://code.google.com/p/doctype/wiki/ArticlePageOffset) + * + * OpenLayers.Util.pagePosition is based on Yahoo's getXY method, which is + * Copyright (c) 2006, Yahoo! Inc. + * All rights reserved. + * + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Yahoo! Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission of Yahoo! Inc. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Parameters: + * forElement - {DOMElement} + * + * Returns: + * {Array} two item array, Left value then Top value. + */ +OpenLayers.Util.pagePosition = function(forElement) { + // NOTE: If element is hidden (display none or disconnected or any the + // ancestors are hidden) we get (0,0) by default but we still do the + // accumulation of scroll position. + + var pos = [0, 0]; + var viewportElement = OpenLayers.Util.getViewportElement(); + if (!forElement || forElement == window || forElement == viewportElement) { + // viewport is always at 0,0 as that defined the coordinate system for + // this function - this avoids special case checks in the code below + return pos; + } + + // Gecko browsers normally use getBoxObjectFor to calculate the position. + // When invoked for an element with an implicit absolute position though it + // can be off by one. Therefore the recursive implementation is used in + // those (relatively rare) cases. + var BUGGY_GECKO_BOX_OBJECT = + OpenLayers.IS_GECKO && document.getBoxObjectFor && + OpenLayers.Element.getStyle(forElement, 'position') == 'absolute' && + (forElement.style.top == '' || forElement.style.left == ''); + + var parent = null; + var box; + + if (forElement.getBoundingClientRect) { // IE + box = forElement.getBoundingClientRect(); + var scrollTop = window.pageYOffset || viewportElement.scrollTop; + var scrollLeft = window.pageXOffset || viewportElement.scrollLeft; + + pos[0] = box.left + scrollLeft; + pos[1] = box.top + scrollTop; + + } else if (document.getBoxObjectFor && !BUGGY_GECKO_BOX_OBJECT) { // gecko + // Gecko ignores the scroll values for ancestors, up to 1.9. See: + // https://bugzilla.mozilla.org/show_bug.cgi?id=328881 and + // https://bugzilla.mozilla.org/show_bug.cgi?id=330619 + + box = document.getBoxObjectFor(forElement); + var vpBox = document.getBoxObjectFor(viewportElement); + pos[0] = box.screenX - vpBox.screenX; + pos[1] = box.screenY - vpBox.screenY; + + } else { // safari/opera + pos[0] = forElement.offsetLeft; + pos[1] = forElement.offsetTop; + parent = forElement.offsetParent; + if (parent != forElement) { + while (parent) { + pos[0] += parent.offsetLeft; + pos[1] += parent.offsetTop; + parent = parent.offsetParent; + } + } + + var browser = OpenLayers.BROWSER_NAME; + + // opera & (safari absolute) incorrectly account for body offsetTop + if (browser == "opera" || (browser == "safari" && + OpenLayers.Element.getStyle(forElement, 'position') == 'absolute')) { + pos[1] -= document.body.offsetTop; + } + + // accumulate the scroll positions for everything but the body element + parent = forElement.offsetParent; + while (parent && parent != document.body) { + pos[0] -= parent.scrollLeft; + // see https://bugs.opera.com/show_bug.cgi?id=249965 + if (browser != "opera" || parent.tagName != 'TR') { + pos[1] -= parent.scrollTop; + } + parent = parent.offsetParent; + } + } + + return pos; +}; + +/** + * Function: getViewportElement + * Returns die viewport element of the document. The viewport element is + * usually document.documentElement, except in IE,where it is either + * document.body or document.documentElement, depending on the document's + * compatibility mode (see + * http://code.google.com/p/doctype/wiki/ArticleClientViewportElement) + * + * Returns: + * {DOMElement} + */ +OpenLayers.Util.getViewportElement = function() { + var viewportElement = arguments.callee.viewportElement; + if (viewportElement == undefined) { + viewportElement = (OpenLayers.BROWSER_NAME == "msie" && + document.compatMode != 'CSS1Compat') ? document.body : + document.documentElement; + arguments.callee.viewportElement = viewportElement; + } + return viewportElement; +}; + +/** + * Function: isEquivalentUrl + * Test two URLs for equivalence. + * + * Setting 'ignoreCase' allows for case-independent comparison. + * + * Comparison is based on: + * - Protocol + * - Host (evaluated without the port) + * - Port (set 'ignorePort80' to ignore "80" values) + * - Hash ( set 'ignoreHash' to disable) + * - Pathname (for relative <-> absolute comparison) + * - Arguments (so they can be out of order) + * + * Parameters: + * url1 - {String} + * url2 - {String} + * options - {Object} Allows for customization of comparison: + * 'ignoreCase' - Default is True + * 'ignorePort80' - Default is True + * 'ignoreHash' - Default is True + * + * Returns: + * {Boolean} Whether or not the two URLs are equivalent + */ +OpenLayers.Util.isEquivalentUrl = function(url1, url2, options) { + options = options || {}; + + OpenLayers.Util.applyDefaults(options, { + ignoreCase: true, + ignorePort80: true, + ignoreHash: true, + splitArgs: false + }); + + var urlObj1 = OpenLayers.Util.createUrlObject(url1, options); + var urlObj2 = OpenLayers.Util.createUrlObject(url2, options); + + //compare all keys except for "args" (treated below) + for(var key in urlObj1) { + if(key !== "args") { + if(urlObj1[key] != urlObj2[key]) { + return false; + } + } + } + + // compare search args - irrespective of order + for(var key in urlObj1.args) { + if(urlObj1.args[key] != urlObj2.args[key]) { + return false; + } + delete urlObj2.args[key]; + } + // urlObj2 shouldn't have any args left + for(var key in urlObj2.args) { + return false; + } + + return true; +}; + +/** + * Function: createUrlObject + * + * Parameters: + * url - {String} + * options - {Object} A hash of options. + * + * Valid options: + * ignoreCase - {Boolean} lowercase url, + * ignorePort80 - {Boolean} don't include explicit port if port is 80, + * ignoreHash - {Boolean} Don't include part of url after the hash (#). + * splitArgs - {Boolean} Split comma delimited params into arrays? Default is + * true. + * + * Returns: + * {Object} An object with separate url, a, port, host, and args parsed out + * and ready for comparison + */ +OpenLayers.Util.createUrlObject = function(url, options) { + options = options || {}; + + // deal with relative urls first + if(!(/^\w+:\/\//).test(url)) { + var loc = window.location; + var port = loc.port ? ":" + loc.port : ""; + var fullUrl = loc.protocol + "//" + loc.host.split(":").shift() + port; + if(url.indexOf("/") === 0) { + // full pathname + url = fullUrl + url; + } else { + // relative to current path + var parts = loc.pathname.split("/"); + parts.pop(); + url = fullUrl + parts.join("/") + "/" + url; + } + } + + if (options.ignoreCase) { + url = url.toLowerCase(); + } + + var a = document.createElement('a'); + a.href = url; + + var urlObject = {}; + + //host (without port) + urlObject.host = a.host.split(":").shift(); + + //protocol + urlObject.protocol = a.protocol; + + //port (get uniform browser behavior with port 80 here) + if(options.ignorePort80) { + urlObject.port = (a.port == "80" || a.port == "0") ? "" : a.port; + } else { + urlObject.port = (a.port == "" || a.port == "0") ? "80" : a.port; + } + + //hash + urlObject.hash = (options.ignoreHash || a.hash === "#") ? "" : a.hash; + + //args + var queryString = a.search; + if (!queryString) { + var qMark = url.indexOf("?"); + queryString = (qMark != -1) ? url.substr(qMark) : ""; + } + urlObject.args = OpenLayers.Util.getParameters(queryString, + {splitArgs: options.splitArgs}); + + // pathname + // + // This is a workaround for Internet Explorer where + // window.location.pathname has a leading "/", but + // a.pathname has no leading "/". + urlObject.pathname = (a.pathname.charAt(0) == "/") ? a.pathname : "/" + a.pathname; + + return urlObject; +}; + +/** + * Function: removeTail + * Takes a url and removes everything after the ? and # + * + * Parameters: + * url - {String} The url to process + * + * Returns: + * {String} The string with all queryString and Hash removed + */ +OpenLayers.Util.removeTail = function(url) { + var head = null; + + var qMark = url.indexOf("?"); + var hashMark = url.indexOf("#"); + + if (qMark == -1) { + head = (hashMark != -1) ? url.substr(0,hashMark) : url; + } else { + head = (hashMark != -1) ? url.substr(0,Math.min(qMark, hashMark)) + : url.substr(0, qMark); + } + return head; +}; + +/** + * Constant: IS_GECKO + * {Boolean} True if the userAgent reports the browser to use the Gecko engine + */ +OpenLayers.IS_GECKO = (function() { + var ua = navigator.userAgent.toLowerCase(); + return ua.indexOf("webkit") == -1 && ua.indexOf("gecko") != -1; +})(); + +/** + * Constant: CANVAS_SUPPORTED + * {Boolean} True if canvas 2d is supported. + */ +OpenLayers.CANVAS_SUPPORTED = (function() { + var elem = document.createElement('canvas'); + return !!(elem.getContext && elem.getContext('2d')); +})(); + +/** + * Constant: BROWSER_NAME + * {String} + * A substring of the navigator.userAgent property. Depending on the userAgent + * property, this will be the empty string or one of the following: + * * "opera" -- Opera + * * "msie" -- Internet Explorer + * * "safari" -- Safari + * * "firefox" -- Firefox + * * "mozilla" -- Mozilla + */ +OpenLayers.BROWSER_NAME = (function() { + var name = ""; + var ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf("opera") != -1) { + name = "opera"; + } else if (ua.indexOf("msie") != -1) { + name = "msie"; + } else if (ua.indexOf("safari") != -1) { + name = "safari"; + } else if (ua.indexOf("mozilla") != -1) { + if (ua.indexOf("firefox") != -1) { + name = "firefox"; + } else { + name = "mozilla"; + } + } + return name; +})(); + +/** + * Function: getBrowserName + * + * Returns: + * {String} A string which specifies which is the current + * browser in which we are running. + * + * Currently-supported browser detection and codes: + * * 'opera' -- Opera + * * 'msie' -- Internet Explorer + * * 'safari' -- Safari + * * 'firefox' -- Firefox + * * 'mozilla' -- Mozilla + * + * If we are unable to property identify the browser, we + * return an empty string. + */ +OpenLayers.Util.getBrowserName = function() { + return OpenLayers.BROWSER_NAME; +}; + +/** + * Method: getRenderedDimensions + * Renders the contentHTML offscreen to determine actual dimensions for + * popup sizing. As we need layout to determine dimensions the content + * is rendered -9999px to the left and absolute to ensure the + * scrollbars do not flicker + * + * Parameters: + * contentHTML + * size - {<OpenLayers.Size>} If either the 'w' or 'h' properties is + * specified, we fix that dimension of the div to be measured. This is + * useful in the case where we have a limit in one dimension and must + * therefore meaure the flow in the other dimension. + * options - {Object} + * + * Allowed Options: + * displayClass - {String} Optional parameter. A CSS class name(s) string + * to provide the CSS context of the rendered content. + * containerElement - {DOMElement} Optional parameter. Insert the HTML to + * this node instead of the body root when calculating dimensions. + * + * Returns: + * {<OpenLayers.Size>} + */ +OpenLayers.Util.getRenderedDimensions = function(contentHTML, size, options) { + + var w, h; + + // create temp container div with restricted size + var container = document.createElement("div"); + container.style.visibility = "hidden"; + + var containerElement = (options && options.containerElement) + ? options.containerElement : document.body; + + // Opera and IE7 can't handle a node with position:aboslute if it inherits + // position:absolute from a parent. + var parentHasPositionAbsolute = false; + var superContainer = null; + var parent = containerElement; + while (parent && parent.tagName.toLowerCase()!="body") { + var parentPosition = OpenLayers.Element.getStyle(parent, "position"); + if(parentPosition == "absolute") { + parentHasPositionAbsolute = true; + break; + } else if (parentPosition && parentPosition != "static") { + break; + } + parent = parent.parentNode; + } + if(parentHasPositionAbsolute && (containerElement.clientHeight === 0 || + containerElement.clientWidth === 0) ){ + superContainer = document.createElement("div"); + superContainer.style.visibility = "hidden"; + superContainer.style.position = "absolute"; + superContainer.style.overflow = "visible"; + superContainer.style.width = document.body.clientWidth + "px"; + superContainer.style.height = document.body.clientHeight + "px"; + superContainer.appendChild(container); + } + container.style.position = "absolute"; + + //fix a dimension, if specified. + if (size) { + if (size.w) { + w = size.w; + container.style.width = w + "px"; + } else if (size.h) { + h = size.h; + container.style.height = h + "px"; + } + } + + //add css classes, if specified + if (options && options.displayClass) { + container.className = options.displayClass; + } + + // create temp content div and assign content + var content = document.createElement("div"); + content.innerHTML = contentHTML; + + // we need overflow visible when calculating the size + content.style.overflow = "visible"; + if (content.childNodes) { + for (var i=0, l=content.childNodes.length; i<l; i++) { + if (!content.childNodes[i].style) continue; + content.childNodes[i].style.overflow = "visible"; + } + } + + // add content to restricted container + container.appendChild(content); + + // append container to body for rendering + if (superContainer) { + containerElement.appendChild(superContainer); + } else { + containerElement.appendChild(container); + } + + // calculate scroll width of content and add corners and shadow width + if (!w) { + w = parseInt(content.scrollWidth); + + // update container width to allow height to adjust + container.style.width = w + "px"; + } + // capture height and add shadow and corner image widths + if (!h) { + h = parseInt(content.scrollHeight); + } + + // remove elements + container.removeChild(content); + if (superContainer) { + superContainer.removeChild(container); + containerElement.removeChild(superContainer); + } else { + containerElement.removeChild(container); + } + + return new OpenLayers.Size(w, h); +}; + +/** + * APIFunction: getScrollbarWidth + * This function has been modified by the OpenLayers from the original version, + * written by Matthew Eernisse and released under the Apache 2 + * license here: + * + * http://www.fleegix.org/articles/2006/05/30/getting-the-scrollbar-width-in-pixels + * + * It has been modified simply to cache its value, since it is physically + * impossible that this code could ever run in more than one browser at + * once. + * + * Returns: + * {Integer} + */ +OpenLayers.Util.getScrollbarWidth = function() { + + var scrollbarWidth = OpenLayers.Util._scrollbarWidth; + + if (scrollbarWidth == null) { + var scr = null; + var inn = null; + var wNoScroll = 0; + var wScroll = 0; + + // Outer scrolling div + scr = document.createElement('div'); + scr.style.position = 'absolute'; + scr.style.top = '-1000px'; + scr.style.left = '-1000px'; + scr.style.width = '100px'; + scr.style.height = '50px'; + // Start with no scrollbar + scr.style.overflow = 'hidden'; + + // Inner content div + inn = document.createElement('div'); + inn.style.width = '100%'; + inn.style.height = '200px'; + + // Put the inner div in the scrolling div + scr.appendChild(inn); + // Append the scrolling div to the doc + document.body.appendChild(scr); + + // Width of the inner div sans scrollbar + wNoScroll = inn.offsetWidth; + + // Add the scrollbar + scr.style.overflow = 'scroll'; + // Width of the inner div width scrollbar + wScroll = inn.offsetWidth; + + // Remove the scrolling div from the doc + document.body.removeChild(document.body.lastChild); + + // Pixel width of the scroller + OpenLayers.Util._scrollbarWidth = (wNoScroll - wScroll); + scrollbarWidth = OpenLayers.Util._scrollbarWidth; + } + + return scrollbarWidth; +}; + +/** + * APIFunction: getFormattedLonLat + * This function will return latitude or longitude value formatted as + * + * Parameters: + * coordinate - {Float} the coordinate value to be formatted + * axis - {String} value of either 'lat' or 'lon' to indicate which axis is to + * to be formatted (default = lat) + * dmsOption - {String} specify the precision of the output can be one of: + * 'dms' show degrees minutes and seconds + * 'dm' show only degrees and minutes + * 'd' show only degrees + * + * Returns: + * {String} the coordinate value formatted as a string + */ +OpenLayers.Util.getFormattedLonLat = function(coordinate, axis, dmsOption) { + if (!dmsOption) { + dmsOption = 'dms'; //default to show degree, minutes, seconds + } + + coordinate = (coordinate+540)%360 - 180; // normalize for sphere being round + + var abscoordinate = Math.abs(coordinate); + var coordinatedegrees = Math.floor(abscoordinate); + + var coordinateminutes = (abscoordinate - coordinatedegrees)/(1/60); + var tempcoordinateminutes = coordinateminutes; + coordinateminutes = Math.floor(coordinateminutes); + var coordinateseconds = (tempcoordinateminutes - coordinateminutes)/(1/60); + coordinateseconds = Math.round(coordinateseconds*10); + coordinateseconds /= 10; + + if( coordinateseconds >= 60) { + coordinateseconds -= 60; + coordinateminutes += 1; + if( coordinateminutes >= 60) { + coordinateminutes -= 60; + coordinatedegrees += 1; + } + } + + if( coordinatedegrees < 10 ) { + coordinatedegrees = "0" + coordinatedegrees; + } + var str = coordinatedegrees + "\u00B0"; + + if (dmsOption.indexOf('dm') >= 0) { + if( coordinateminutes < 10 ) { + coordinateminutes = "0" + coordinateminutes; + } + str += coordinateminutes + "'"; + + if (dmsOption.indexOf('dms') >= 0) { + if( coordinateseconds < 10 ) { + coordinateseconds = "0" + coordinateseconds; + } + str += coordinateseconds + '"'; + } + } + + if (axis == "lon") { + str += coordinate < 0 ? OpenLayers.i18n("W") : OpenLayers.i18n("E"); + } else { + str += coordinate < 0 ? OpenLayers.i18n("S") : OpenLayers.i18n("N"); + } + return str; +}; + +/* ====================================================================== + OpenLayers/Events.js + ====================================================================== */ + +/* 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/Util.js + */ + +/** + * Namespace: OpenLayers.Event + * Utility functions for event handling. + */ +OpenLayers.Event = { + + /** + * Property: observers + * {Object} A hashtable cache of the event observers. Keyed by + * element._eventCacheID + */ + observers: false, + + /** + * Constant: KEY_SPACE + * {int} + */ + KEY_SPACE: 32, + + /** + * Constant: KEY_BACKSPACE + * {int} + */ + KEY_BACKSPACE: 8, + + /** + * Constant: KEY_TAB + * {int} + */ + KEY_TAB: 9, + + /** + * Constant: KEY_RETURN + * {int} + */ + KEY_RETURN: 13, + + /** + * Constant: KEY_ESC + * {int} + */ + KEY_ESC: 27, + + /** + * Constant: KEY_LEFT + * {int} + */ + KEY_LEFT: 37, + + /** + * Constant: KEY_UP + * {int} + */ + KEY_UP: 38, + + /** + * Constant: KEY_RIGHT + * {int} + */ + KEY_RIGHT: 39, + + /** + * Constant: KEY_DOWN + * {int} + */ + KEY_DOWN: 40, + + /** + * Constant: KEY_DELETE + * {int} + */ + KEY_DELETE: 46, + + + /** + * Method: element + * Cross browser event element detection. + * + * Parameters: + * event - {Event} + * + * Returns: + * {DOMElement} The element that caused the event + */ + element: function(event) { + return event.target || event.srcElement; + }, + + /** + * Method: isSingleTouch + * Determine whether event was caused by a single touch + * + * Parameters: + * event - {Event} + * + * Returns: + * {Boolean} + */ + isSingleTouch: function(event) { + return event.touches && event.touches.length == 1; + }, + + /** + * Method: isMultiTouch + * Determine whether event was caused by a multi touch + * + * Parameters: + * event - {Event} + * + * Returns: + * {Boolean} + */ + isMultiTouch: function(event) { + return event.touches && event.touches.length > 1; + }, + + /** + * Method: isLeftClick + * Determine whether event was caused by a left click. + * + * Parameters: + * event - {Event} + * + * Returns: + * {Boolean} + */ + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + /** + * Method: isRightClick + * Determine whether event was caused by a right mouse click. + * + * Parameters: + * event - {Event} + * + * Returns: + * {Boolean} + */ + isRightClick: function(event) { + return (((event.which) && (event.which == 3)) || + ((event.button) && (event.button == 2))); + }, + + /** + * Method: stop + * Stops an event from propagating. + * + * Parameters: + * event - {Event} + * allowDefault - {Boolean} If true, we stop the event chain but + * still allow the default browser behaviour (text selection, + * radio-button clicking, etc). Default is false. + */ + stop: function(event, allowDefault) { + + if (!allowDefault) { + OpenLayers.Event.preventDefault(event); + } + + if (event.stopPropagation) { + event.stopPropagation(); + } else { + event.cancelBubble = true; + } + }, + + /** + * Method: preventDefault + * Cancels the event if it is cancelable, without stopping further + * propagation of the event. + * + * Parameters: + * event - {Event} + */ + preventDefault: function(event) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + }, + + /** + * Method: findElement + * + * Parameters: + * event - {Event} + * tagName - {String} + * + * Returns: + * {DOMElement} The first node with the given tagName, starting from the + * node the event was triggered on and traversing the DOM upwards + */ + findElement: function(event, tagName) { + var element = OpenLayers.Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))){ + element = element.parentNode; + } + return element; + }, + + /** + * Method: observe + * + * Parameters: + * elementParam - {DOMElement || String} + * name - {String} + * observer - {function} + * useCapture - {Boolean} + */ + observe: function(elementParam, name, observer, useCapture) { + var element = OpenLayers.Util.getElement(elementParam); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) { + name = 'keydown'; + } + + //if observers cache has not yet been created, create it + if (!this.observers) { + this.observers = {}; + } + + //if not already assigned, make a new unique cache ID + if (!element._eventCacheID) { + var idPrefix = "eventCacheID_"; + if (element.id) { + idPrefix = element.id + "_" + idPrefix; + } + element._eventCacheID = OpenLayers.Util.createUniqueID(idPrefix); + } + + var cacheID = element._eventCacheID; + + //if there is not yet a hash entry for this element, add one + if (!this.observers[cacheID]) { + this.observers[cacheID] = []; + } + + //add a new observer to this element's list + this.observers[cacheID].push({ + 'element': element, + 'name': name, + 'observer': observer, + 'useCapture': useCapture + }); + + //add the actual browser event listener + if (element.addEventListener) { + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + element.attachEvent('on' + name, observer); + } + }, + + /** + * Method: stopObservingElement + * Given the id of an element to stop observing, cycle through the + * element's cached observers, calling stopObserving on each one, + * skipping those entries which can no longer be removed. + * + * parameters: + * elementParam - {DOMElement || String} + */ + stopObservingElement: function(elementParam) { + var element = OpenLayers.Util.getElement(elementParam); + var cacheID = element._eventCacheID; + + this._removeElementObservers(OpenLayers.Event.observers[cacheID]); + }, + + /** + * Method: _removeElementObservers + * + * Parameters: + * elementObservers - {Array(Object)} Array of (element, name, + * observer, usecapture) objects, + * taken directly from hashtable + */ + _removeElementObservers: function(elementObservers) { + if (elementObservers) { + for(var i = elementObservers.length-1; i >= 0; i--) { + var entry = elementObservers[i]; + OpenLayers.Event.stopObserving.apply(this, [ + entry.element, entry.name, entry.observer, entry.useCapture + ]); + } + } + }, + + /** + * Method: stopObserving + * + * Parameters: + * elementParam - {DOMElement || String} + * name - {String} + * observer - {function} + * useCapture - {Boolean} + * + * Returns: + * {Boolean} Whether or not the event observer was removed + */ + stopObserving: function(elementParam, name, observer, useCapture) { + useCapture = useCapture || false; + + var element = OpenLayers.Util.getElement(elementParam); + var cacheID = element._eventCacheID; + + if (name == 'keypress') { + if ( navigator.appVersion.match(/Konqueror|Safari|KHTML/) || + element.detachEvent) { + name = 'keydown'; + } + } + + // find element's entry in this.observers cache and remove it + var foundEntry = false; + var elementObservers = OpenLayers.Event.observers[cacheID]; + if (elementObservers) { + + // find the specific event type in the element's list + var i=0; + while(!foundEntry && i < elementObservers.length) { + var cacheEntry = elementObservers[i]; + + if ((cacheEntry.name == name) && + (cacheEntry.observer == observer) && + (cacheEntry.useCapture == useCapture)) { + + elementObservers.splice(i, 1); + if (elementObservers.length == 0) { + delete OpenLayers.Event.observers[cacheID]; + } + foundEntry = true; + break; + } + i++; + } + } + + //actually remove the event listener from browser + if (foundEntry) { + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element && element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } + return foundEntry; + }, + + /** + * Method: unloadCache + * Cycle through all the element entries in the events cache and call + * stopObservingElement on each. + */ + unloadCache: function() { + // check for OpenLayers.Event before checking for observers, because + // OpenLayers.Event may be undefined in IE if no map instance was + // created + if (OpenLayers.Event && OpenLayers.Event.observers) { + for (var cacheID in OpenLayers.Event.observers) { + var elementObservers = OpenLayers.Event.observers[cacheID]; + OpenLayers.Event._removeElementObservers.apply(this, + [elementObservers]); + } + OpenLayers.Event.observers = false; + } + }, + + CLASS_NAME: "OpenLayers.Event" +}; + +/* prevent memory leaks in IE */ +OpenLayers.Event.observe(window, 'unload', OpenLayers.Event.unloadCache, false); + +/** + * Class: OpenLayers.Events + */ +OpenLayers.Events = OpenLayers.Class({ + + /** + * Constant: BROWSER_EVENTS + * {Array(String)} supported events + */ + BROWSER_EVENTS: [ + "mouseover", "mouseout", + "mousedown", "mouseup", "mousemove", + "click", "dblclick", "rightclick", "dblrightclick", + "resize", "focus", "blur", + "touchstart", "touchmove", "touchend", + "keydown" + ], + + /** + * Property: listeners + * {Object} Hashtable of Array(Function): events listener functions + */ + listeners: null, + + /** + * Property: object + * {Object} the code object issuing application events + */ + object: null, + + /** + * Property: element + * {DOMElement} the DOM element receiving browser events + */ + element: null, + + /** + * Property: eventHandler + * {Function} bound event handler attached to elements + */ + eventHandler: null, + + /** + * APIProperty: fallThrough + * {Boolean} + */ + fallThrough: null, + + /** + * APIProperty: includeXY + * {Boolean} Should the .xy property automatically be created for browser + * mouse events? In general, this should be false. If it is true, then + * mouse events will automatically generate a '.xy' property on the + * event object that is passed. (Prior to OpenLayers 2.7, this was true + * by default.) Otherwise, you can call the getMousePosition on the + * relevant events handler on the object available via the 'evt.object' + * property of the evt object. So, for most events, you can call: + * function named(evt) { + * this.xy = this.object.events.getMousePosition(evt) + * } + * + * This option typically defaults to false for performance reasons: + * when creating an events object whose primary purpose is to manage + * relatively positioned mouse events within a div, it may make + * sense to set it to true. + * + * This option is also used to control whether the events object caches + * offsets. If this is false, it will not: the reason for this is that + * it is only expected to be called many times if the includeXY property + * is set to true. If you set this to true, you are expected to clear + * the offset cache manually (using this.clearMouseCache()) if: + * the border of the element changes + * the location of the element in the page changes + */ + includeXY: false, + + /** + * APIProperty: extensions + * {Object} Event extensions registered with this instance. Keys are + * event types, values are {OpenLayers.Events.*} extension instances or + * {Boolean} for events that an instantiated extension provides in + * addition to the one it was created for. + * + * Extensions create an event in addition to browser events, which usually + * fires when a sequence of browser events is completed. Extensions are + * automatically instantiated when a listener is registered for an event + * provided by an extension. + * + * Extensions are created in the <OpenLayers.Events> namespace using + * <OpenLayers.Class>, and named after the event they provide. + * The constructor receives the target <OpenLayers.Events> instance as + * argument. Extensions that need to capture browser events before they + * propagate can register their listeners events using <register>, with + * {extension: true} as 4th argument. + * + * If an extension creates more than one event, an alias for each event + * type should be created and reference the same class. The constructor + * should set a reference in the target's extensions registry to itself. + * + * Below is a minimal extension that provides the "foostart" and "fooend" + * event types, which replace the native "click" event type if clicked on + * an element with the css class "foo": + * + * (code) + * OpenLayers.Events.foostart = OpenLayers.Class({ + * initialize: function(target) { + * this.target = target; + * this.target.register("click", this, this.doStuff, {extension: true}); + * // only required if extension provides more than one event type + * this.target.extensions["foostart"] = true; + * this.target.extensions["fooend"] = true; + * }, + * destroy: function() { + * var target = this.target; + * target.unregister("click", this, this.doStuff); + * delete this.target; + * // only required if extension provides more than one event type + * delete target.extensions["foostart"]; + * delete target.extensions["fooend"]; + * }, + * doStuff: function(evt) { + * var propagate = true; + * if (OpenLayers.Event.element(evt).className === "foo") { + * propagate = false; + * var target = this.target; + * target.triggerEvent("foostart"); + * window.setTimeout(function() { + * target.triggerEvent("fooend"); + * }, 1000); + * } + * return propagate; + * } + * }); + * // only required if extension provides more than one event type + * OpenLayers.Events.fooend = OpenLayers.Events.foostart; + * (end) + * + */ + extensions: null, + + /** + * Property: extensionCount + * {Object} Keys are event types (like in <listeners>), values are the + * number of extension listeners for each event type. + */ + extensionCount: null, + + /** + * Method: clearMouseListener + * A version of <clearMouseCache> that is bound to this instance so that + * it can be used with <OpenLayers.Event.observe> and + * <OpenLayers.Event.stopObserving>. + */ + clearMouseListener: null, + + /** + * Constructor: OpenLayers.Events + * Construct an OpenLayers.Events object. + * + * Parameters: + * object - {Object} The js object to which this Events object is being added + * element - {DOMElement} A dom element to respond to browser events + * eventTypes - {Array(String)} Deprecated. Array of custom application + * events. A listener may be registered for any named event, regardless + * of the values provided here. + * fallThrough - {Boolean} Allow events to fall through after these have + * been handled? + * options - {Object} Options for the events object. + */ + initialize: function (object, element, eventTypes, fallThrough, options) { + OpenLayers.Util.extend(this, options); + this.object = object; + this.fallThrough = fallThrough; + this.listeners = {}; + this.extensions = {}; + this.extensionCount = {}; + this._msTouches = []; + + // if a dom element is specified, add a listeners list + // for browser events on the element and register them + if (element != null) { + this.attachToElement(element); + } + }, + + /** + * APIMethod: destroy + */ + destroy: function () { + for (var e in this.extensions) { + if (typeof this.extensions[e] !== "boolean") { + this.extensions[e].destroy(); + } + } + this.extensions = null; + if (this.element) { + OpenLayers.Event.stopObservingElement(this.element); + if(this.element.hasScrollEvent) { + OpenLayers.Event.stopObserving( + window, "scroll", this.clearMouseListener + ); + } + } + this.element = null; + + this.listeners = null; + this.object = null; + this.fallThrough = null; + this.eventHandler = null; + }, + + /** + * APIMethod: addEventType + * Deprecated. Any event can be triggered without adding it first. + * + * Parameters: + * eventName - {String} + */ + addEventType: function(eventName) { + }, + + /** + * Method: attachToElement + * + * Parameters: + * element - {HTMLDOMElement} a DOM element to attach browser events to + */ + attachToElement: function (element) { + if (this.element) { + OpenLayers.Event.stopObservingElement(this.element); + } else { + // keep a bound copy of handleBrowserEvent() so that we can + // pass the same function to both Event.observe() and .stopObserving() + this.eventHandler = OpenLayers.Function.bindAsEventListener( + this.handleBrowserEvent, this + ); + + // to be used with observe and stopObserving + this.clearMouseListener = OpenLayers.Function.bind( + this.clearMouseCache, this + ); + } + this.element = element; + var msTouch = !!window.navigator.msMaxTouchPoints; + var type; + for (var i = 0, len = this.BROWSER_EVENTS.length; i < len; i++) { + type = this.BROWSER_EVENTS[i]; + // register the event cross-browser + OpenLayers.Event.observe(element, type, this.eventHandler + ); + if (msTouch && type.indexOf('touch') === 0) { + this.addMsTouchListener(element, type, this.eventHandler); + } + } + // disable dragstart in IE so that mousedown/move/up works normally + OpenLayers.Event.observe(element, "dragstart", OpenLayers.Event.stop); + }, + + /** + * APIMethod: on + * Convenience method for registering listeners with a common scope. + * Internally, this method calls <register> as shown in the examples + * below. + * + * Example use: + * (code) + * // register a single listener for the "loadstart" event + * events.on({"loadstart": loadStartListener}); + * + * // this is equivalent to the following + * events.register("loadstart", undefined, loadStartListener); + * + * // register multiple listeners to be called with the same `this` object + * events.on({ + * "loadstart": loadStartListener, + * "loadend": loadEndListener, + * scope: object + * }); + * + * // this is equivalent to the following + * events.register("loadstart", object, loadStartListener); + * events.register("loadend", object, loadEndListener); + * (end) + * + * Parameters: + * object - {Object} + */ + on: function(object) { + for(var type in object) { + if(type != "scope" && object.hasOwnProperty(type)) { + this.register(type, object.scope, object[type]); + } + } + }, + + /** + * APIMethod: register + * Register an event on the events object. + * + * When the event is triggered, the 'func' function will be called, in the + * context of 'obj'. Imagine we were to register an event, specifying an + * OpenLayers.Bounds Object as 'obj'. When the event is triggered, the + * context in the callback function will be our Bounds object. This means + * that within our callback function, we can access the properties and + * methods of the Bounds object through the "this" variable. So our + * callback could execute something like: + * : leftStr = "Left: " + this.left; + * + * or + * + * : centerStr = "Center: " + this.getCenterLonLat(); + * + * Parameters: + * type - {String} Name of the event to register + * obj - {Object} The object to bind the context to for the callback#. + * If no object is specified, default is the Events's 'object' property. + * func - {Function} The callback function. If no callback is + * specified, this function does nothing. + * priority - {Boolean|Object} If true, adds the new listener to the + * *front* of the events queue instead of to the end. + * + * Valid options for priority: + * extension - {Boolean} If true, then the event will be registered as + * extension event. Extension events are handled before all other + * events. + */ + register: function (type, obj, func, priority) { + if (type in OpenLayers.Events && !this.extensions[type]) { + this.extensions[type] = new OpenLayers.Events[type](this); + } + if (func != null) { + if (obj == null) { + obj = this.object; + } + var listeners = this.listeners[type]; + if (!listeners) { + listeners = []; + this.listeners[type] = listeners; + this.extensionCount[type] = 0; + } + var listener = {obj: obj, func: func}; + if (priority) { + listeners.splice(this.extensionCount[type], 0, listener); + if (typeof priority === "object" && priority.extension) { + this.extensionCount[type]++; + } + } else { + listeners.push(listener); + } + } + }, + + /** + * APIMethod: registerPriority + * Same as register() but adds the new listener to the *front* of the + * events queue instead of to the end. + * + * TODO: get rid of this in 3.0 - Decide whether listeners should be + * called in the order they were registered or in reverse order. + * + * + * Parameters: + * type - {String} Name of the event to register + * obj - {Object} The object to bind the context to for the callback#. + * If no object is specified, default is the Events's + * 'object' property. + * func - {Function} The callback function. If no callback is + * specified, this function does nothing. + */ + registerPriority: function (type, obj, func) { + this.register(type, obj, func, true); + }, + + /** + * APIMethod: un + * Convenience method for unregistering listeners with a common scope. + * Internally, this method calls <unregister> as shown in the examples + * below. + * + * Example use: + * (code) + * // unregister a single listener for the "loadstart" event + * events.un({"loadstart": loadStartListener}); + * + * // this is equivalent to the following + * events.unregister("loadstart", undefined, loadStartListener); + * + * // unregister multiple listeners with the same `this` object + * events.un({ + * "loadstart": loadStartListener, + * "loadend": loadEndListener, + * scope: object + * }); + * + * // this is equivalent to the following + * events.unregister("loadstart", object, loadStartListener); + * events.unregister("loadend", object, loadEndListener); + * (end) + */ + un: function(object) { + for(var type in object) { + if(type != "scope" && object.hasOwnProperty(type)) { + this.unregister(type, object.scope, object[type]); + } + } + }, + + /** + * APIMethod: unregister + * + * Parameters: + * type - {String} + * obj - {Object} If none specified, defaults to this.object + * func - {Function} + */ + unregister: function (type, obj, func) { + if (obj == null) { + obj = this.object; + } + var listeners = this.listeners[type]; + if (listeners != null) { + for (var i=0, len=listeners.length; i<len; i++) { + if (listeners[i].obj == obj && listeners[i].func == func) { + listeners.splice(i, 1); + break; + } + } + } + }, + + /** + * Method: remove + * Remove all listeners for a given event type. If type is not registered, + * does nothing. + * + * Parameters: + * type - {String} + */ + remove: function(type) { + if (this.listeners[type] != null) { + this.listeners[type] = []; + } + }, + + /** + * APIMethod: triggerEvent + * Trigger a specified registered event. + * + * Parameters: + * type - {String} + * evt - {Event || Object} will be passed to the listeners. + * + * Returns: + * {Boolean} The last listener return. If a listener returns false, the + * chain of listeners will stop getting called. + */ + triggerEvent: function (type, evt) { + var listeners = this.listeners[type]; + + // fast path + if(!listeners || listeners.length == 0) { + return undefined; + } + + // prep evt object with object & div references + if (evt == null) { + evt = {}; + } + evt.object = this.object; + evt.element = this.element; + if(!evt.type) { + evt.type = type; + } + + // execute all callbacks registered for specified type + // get a clone of the listeners array to + // allow for splicing during callbacks + listeners = listeners.slice(); + var continueChain; + for (var i=0, len=listeners.length; i<len; i++) { + var callback = listeners[i]; + // bind the context to callback.obj + continueChain = callback.func.apply(callback.obj, [evt]); + + if ((continueChain != undefined) && (continueChain == false)) { + // if callback returns false, execute no more callbacks. + break; + } + } + // don't fall through to other DOM elements + if (!this.fallThrough) { + OpenLayers.Event.stop(evt, true); + } + return continueChain; + }, + + /** + * Method: handleBrowserEvent + * Basically just a wrapper to the triggerEvent() function, but takes + * care to set a property 'xy' on the event with the current mouse + * position. + * + * Parameters: + * evt - {Event} + */ + handleBrowserEvent: function (evt) { + var type = evt.type, listeners = this.listeners[type]; + if(!listeners || listeners.length == 0) { + // noone's listening, bail out + return; + } + // add clientX & clientY to all events - corresponds to average x, y + var touches = evt.touches; + if (touches && touches[0]) { + var x = 0; + var y = 0; + var num = touches.length; + var touch; + for (var i=0; i<num; ++i) { + touch = this.getTouchClientXY(touches[i]); + x += touch.clientX; + y += touch.clientY; + } + evt.clientX = x / num; + evt.clientY = y / num; + } + if (this.includeXY) { + evt.xy = this.getMousePosition(evt); + } + this.triggerEvent(type, evt); + }, + + /** + * Method: getTouchClientXY + * WebKit has a few bugs for clientX/clientY. This method detects them + * and calculate the correct values. + * + * Parameters: + * evt - {Touch} a Touch object from a TouchEvent + * + * Returns: + * {Object} An object with only clientX and clientY properties with the + * calculated values. + */ + getTouchClientXY: function (evt) { + // olMochWin is to override window, used for testing + var win = window.olMockWin || window, + winPageX = win.pageXOffset, + winPageY = win.pageYOffset, + x = evt.clientX, + y = evt.clientY; + + if (evt.pageY === 0 && Math.floor(y) > Math.floor(evt.pageY) || + evt.pageX === 0 && Math.floor(x) > Math.floor(evt.pageX)) { + // iOS4 include scroll offset in clientX/Y + x = x - winPageX; + y = y - winPageY; + } else if (y < (evt.pageY - winPageY) || x < (evt.pageX - winPageX) ) { + // Some Android browsers have totally bogus values for clientX/Y + // when scrolling/zooming a page + x = evt.pageX - winPageX; + y = evt.pageY - winPageY; + } + + evt.olClientX = x; + evt.olClientY = y; + + return { + clientX: x, + clientY: y + }; + }, + + /** + * APIMethod: clearMouseCache + * Clear cached data about the mouse position. This should be called any + * time the element that events are registered on changes position + * within the page. + */ + clearMouseCache: function() { + this.element.scrolls = null; + this.element.lefttop = null; + this.element.offsets = null; + }, + + /** + * Method: getMousePosition + * + * Parameters: + * evt - {Event} + * + * Returns: + * {<OpenLayers.Pixel>} The current xy coordinate of the mouse, adjusted + * for offsets + */ + getMousePosition: function (evt) { + if (!this.includeXY) { + this.clearMouseCache(); + } else if (!this.element.hasScrollEvent) { + OpenLayers.Event.observe(window, "scroll", this.clearMouseListener); + this.element.hasScrollEvent = true; + } + + if (!this.element.scrolls) { + var viewportElement = OpenLayers.Util.getViewportElement(); + this.element.scrolls = [ + window.pageXOffset || viewportElement.scrollLeft, + window.pageYOffset || viewportElement.scrollTop + ]; + } + + if (!this.element.lefttop) { + this.element.lefttop = [ + (document.documentElement.clientLeft || 0), + (document.documentElement.clientTop || 0) + ]; + } + + if (!this.element.offsets) { + this.element.offsets = OpenLayers.Util.pagePosition(this.element); + } + + return new OpenLayers.Pixel( + (evt.clientX + this.element.scrolls[0]) - this.element.offsets[0] + - this.element.lefttop[0], + (evt.clientY + this.element.scrolls[1]) - this.element.offsets[1] + - this.element.lefttop[1] + ); + }, + + /** + * Method: addMsTouchListener + * + * Parameters: + * element - {DOMElement} The DOM element to register the listener on + * type - {String} The event type + * handler - {Function} the handler + */ + addMsTouchListener: function (element, type, handler) { + var eventHandler = this.eventHandler; + var touches = this._msTouches; + + function msHandler(evt) { + handler(OpenLayers.Util.applyDefaults({ + stopPropagation: function() { + for (var i=touches.length-1; i>=0; --i) { + touches[i].stopPropagation(); + } + }, + preventDefault: function() { + for (var i=touches.length-1; i>=0; --i) { + touches[i].preventDefault(); + } + }, + type: type + }, evt)); + } + + switch (type) { + case 'touchstart': + return this.addMsTouchListenerStart(element, type, msHandler); + case 'touchend': + return this.addMsTouchListenerEnd(element, type, msHandler); + case 'touchmove': + return this.addMsTouchListenerMove(element, type, msHandler); + default: + throw 'Unknown touch event type'; + } + }, + + /** + * Method: addMsTouchListenerStart + * + * Parameters: + * element - {DOMElement} The DOM element to register the listener on + * type - {String} The event type + * handler - {Function} the handler + */ + addMsTouchListenerStart: function(element, type, handler) { + var touches = this._msTouches; + + var cb = function(e) { + + var alreadyInArray = false; + for (var i=0, ii=touches.length; i<ii; ++i) { + if (touches[i].pointerId == e.pointerId) { + alreadyInArray = true; + break; + } + } + if (!alreadyInArray) { + touches.push(e); + } + + e.touches = touches.slice(); + handler(e); + }; + + OpenLayers.Event.observe(element, 'MSPointerDown', cb); + + // Need to also listen for end events to keep the _msTouches list + // accurate + var internalCb = function(e) { + for (var i=0, ii=touches.length; i<ii; ++i) { + if (touches[i].pointerId == e.pointerId) { + touches.splice(i, 1); + break; + } + } + }; + OpenLayers.Event.observe(element, 'MSPointerUp', internalCb); + }, + + /** + * Method: addMsTouchListenerMove + * + * Parameters: + * element - {DOMElement} The DOM element to register the listener on + * type - {String} The event type + * handler - {Function} the handler + */ + addMsTouchListenerMove: function (element, type, handler) { + var touches = this._msTouches; + var cb = function(e) { + + //Don't fire touch moves when mouse isn't down + if (e.pointerType == e.MSPOINTER_TYPE_MOUSE && e.buttons == 0) { + return; + } + + if (touches.length == 1 && touches[0].pageX == e.pageX && + touches[0].pageY == e.pageY) { + // don't trigger event when pointer has not moved + return; + } + for (var i=0, ii=touches.length; i<ii; ++i) { + if (touches[i].pointerId == e.pointerId) { + touches[i] = e; + break; + } + } + + e.touches = touches.slice(); + handler(e); + }; + + OpenLayers.Event.observe(element, 'MSPointerMove', cb); + }, + + /** + * Method: addMsTouchListenerEnd + * + * Parameters: + * element - {DOMElement} The DOM element to register the listener on + * type - {String} The event type + * handler - {Function} the handler + */ + addMsTouchListenerEnd: function (element, type, handler) { + var touches = this._msTouches; + + var cb = function(e) { + + for (var i=0, ii=touches.length; i<ii; ++i) { + if (touches[i].pointerId == e.pointerId) { + touches.splice(i, 1); + break; + } + } + + e.touches = touches.slice(); + handler(e); + }; + + OpenLayers.Event.observe(element, 'MSPointerUp', cb); + }, + + CLASS_NAME: "OpenLayers.Events" +}); +/* ====================================================================== + OpenLayers/Events/buttonclick.js + ====================================================================== */ + +/* 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/Events.js + */ + +/** + * Class: OpenLayers.Events.buttonclick + * Extension event type for handling buttons on top of a dom element. This + * event type fires "buttonclick" on its <target> when a button was + * clicked. Buttons are detected by the "olButton" class. + * + * This event type makes sure that button clicks do not interfere with other + * events that are registered on the same <element>. + * + * Event types provided by this extension: + * - *buttonclick* Triggered when a button is clicked. Listeners receive an + * object with a *buttonElement* property referencing the dom element of + * the clicked button, and an *buttonXY* property with the click position + * relative to the button. + */ +OpenLayers.Events.buttonclick = OpenLayers.Class({ + + /** + * Property: target + * {<OpenLayers.Events>} The events instance that the buttonclick event will + * be triggered on. + */ + target: null, + + /** + * Property: events + * {Array} Events to observe and conditionally stop from propagating when + * an element with the olButton class (or its olAlphaImg child) is + * clicked. + */ + events: [ + 'mousedown', 'mouseup', 'click', 'dblclick', + 'touchstart', 'touchmove', 'touchend', 'keydown' + ], + + /** + * Property: startRegEx + * {RegExp} Regular expression to test Event.type for events that start + * a buttonclick sequence. + */ + startRegEx: /^mousedown|touchstart$/, + + /** + * Property: cancelRegEx + * {RegExp} Regular expression to test Event.type for events that cancel + * a buttonclick sequence. + */ + cancelRegEx: /^touchmove$/, + + /** + * Property: completeRegEx + * {RegExp} Regular expression to test Event.type for events that complete + * a buttonclick sequence. + */ + completeRegEx: /^mouseup|touchend$/, + + /** + * Property: startEvt + * {Event} The event that started the click sequence + */ + + /** + * Constructor: OpenLayers.Events.buttonclick + * Construct a buttonclick event type. Applications are not supposed to + * create instances of this class - they are created on demand by + * <OpenLayers.Events> instances. + * + * Parameters: + * target - {<OpenLayers.Events>} The events instance that the buttonclick + * event will be triggered on. + */ + initialize: function(target) { + this.target = target; + for (var i=this.events.length-1; i>=0; --i) { + this.target.register(this.events[i], this, this.buttonClick, { + extension: true + }); + } + }, + + /** + * Method: destroy + */ + destroy: function() { + for (var i=this.events.length-1; i>=0; --i) { + this.target.unregister(this.events[i], this, this.buttonClick); + } + delete this.target; + }, + + /** + * Method: getPressedButton + * Get the pressed button, if any. Returns undefined if no button + * was pressed. + * + * Arguments: + * element - {DOMElement} The event target. + * + * Returns: + * {DOMElement} The button element, or undefined. + */ + getPressedButton: function(element) { + var depth = 3, // limit the search depth + button; + do { + if(OpenLayers.Element.hasClass(element, "olButton")) { + // hit! + button = element; + break; + } + element = element.parentNode; + } while(--depth > 0 && element); + return button; + }, + + /** + * Method: ignore + * Check for event target elements that should be ignored by OpenLayers. + * + * Parameters: + * element - {DOMElement} The event target. + */ + ignore: function(element) { + var depth = 3, + ignore = false; + do { + if (element.nodeName.toLowerCase() === 'a') { + ignore = true; + break; + } + element = element.parentNode; + } while (--depth > 0 && element); + return ignore; + }, + + /** + * Method: buttonClick + * Check if a button was clicked, and fire the buttonclick event + * + * Parameters: + * evt - {Event} + */ + buttonClick: function(evt) { + var propagate = true, + element = OpenLayers.Event.element(evt); + if (element && (OpenLayers.Event.isLeftClick(evt) || !~evt.type.indexOf("mouse"))) { + // was a button pressed? + var button = this.getPressedButton(element); + if (button) { + if (evt.type === "keydown") { + switch (evt.keyCode) { + case OpenLayers.Event.KEY_RETURN: + case OpenLayers.Event.KEY_SPACE: + this.target.triggerEvent("buttonclick", { + buttonElement: button + }); + OpenLayers.Event.stop(evt); + propagate = false; + break; + } + } else if (this.startEvt) { + if (this.completeRegEx.test(evt.type)) { + var pos = OpenLayers.Util.pagePosition(button); + var viewportElement = OpenLayers.Util.getViewportElement(); + var scrollTop = window.pageYOffset || viewportElement.scrollTop; + var scrollLeft = window.pageXOffset || viewportElement.scrollLeft; + pos[0] = pos[0] - scrollLeft; + pos[1] = pos[1] - scrollTop; + + this.target.triggerEvent("buttonclick", { + buttonElement: button, + buttonXY: { + x: this.startEvt.clientX - pos[0], + y: this.startEvt.clientY - pos[1] + } + }); + } + if (this.cancelRegEx.test(evt.type)) { + delete this.startEvt; + } + OpenLayers.Event.stop(evt); + propagate = false; + } + if (this.startRegEx.test(evt.type)) { + this.startEvt = evt; + OpenLayers.Event.stop(evt); + propagate = false; + } + } else { + propagate = !this.ignore(OpenLayers.Event.element(evt)); + delete this.startEvt; + } + } + return propagate; + } + +}); +/* ====================================================================== + OpenLayers/Control.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Control + * Controls affect the display or behavior of the map. They allow everything + * from panning and zooming to displaying a scale indicator. Controls by + * default are added to the map they are contained within however it is + * possible to add a control to an external div by passing the div in the + * options parameter. + * + * Example: + * The following example shows how to add many of the common controls + * to a map. + * + * > var map = new OpenLayers.Map('map', { controls: [] }); + * > + * > map.addControl(new OpenLayers.Control.PanZoomBar()); + * > map.addControl(new OpenLayers.Control.LayerSwitcher({'ascending':false})); + * > map.addControl(new OpenLayers.Control.Permalink()); + * > map.addControl(new OpenLayers.Control.Permalink('permalink')); + * > map.addControl(new OpenLayers.Control.MousePosition()); + * > map.addControl(new OpenLayers.Control.OverviewMap()); + * > map.addControl(new OpenLayers.Control.KeyboardDefaults()); + * + * The next code fragment is a quick example of how to intercept + * shift-mouse click to display the extent of the bounding box + * dragged out by the user. Usually controls are not created + * in exactly this manner. See the source for a more complete + * example: + * + * > var control = new OpenLayers.Control(); + * > OpenLayers.Util.extend(control, { + * > draw: function () { + * > // this Handler.Box will intercept the shift-mousedown + * > // before Control.MouseDefault gets to see it + * > this.box = new OpenLayers.Handler.Box( control, + * > {"done": this.notice}, + * > {keyMask: OpenLayers.Handler.MOD_SHIFT}); + * > this.box.activate(); + * > }, + * > + * > notice: function (bounds) { + * > OpenLayers.Console.userError(bounds); + * > } + * > }); + * > map.addControl(control); + * + */ +OpenLayers.Control = OpenLayers.Class({ + + /** + * Property: id + * {String} + */ + id: null, + + /** + * Property: map + * {<OpenLayers.Map>} this gets set in the addControl() function in + * OpenLayers.Map + */ + map: null, + + /** + * APIProperty: div + * {DOMElement} The element that contains the control, if not present the + * control is placed inside the map. + */ + div: null, + + /** + * APIProperty: type + * {Number} Controls can have a 'type'. The type determines the type of + * interactions which are possible with them when they are placed in an + * <OpenLayers.Control.Panel>. + */ + type: null, + + /** + * Property: allowSelection + * {Boolean} By default, controls do not allow selection, because + * it may interfere with map dragging. If this is true, OpenLayers + * will not prevent selection of the control. + * Default is false. + */ + allowSelection: false, + + /** + * Property: displayClass + * {string} This property is used for CSS related to the drawing of the + * Control. + */ + displayClass: "", + + /** + * APIProperty: title + * {string} This property is used for showing a tooltip over the + * Control. + */ + title: "", + + /** + * APIProperty: autoActivate + * {Boolean} Activate the control when it is added to a map. Default is + * false. + */ + autoActivate: false, + + /** + * APIProperty: active + * {Boolean} The control is active (read-only). Use <activate> and + * <deactivate> to change control state. + */ + active: null, + + /** + * Property: handlerOptions + * {Object} Used to set non-default properties on the control's handler + */ + handlerOptions: null, + + /** + * Property: handler + * {<OpenLayers.Handler>} null + */ + handler: 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: events + * {<OpenLayers.Events>} Events instance for listeners and triggering + * control specific events. + * + * Register a listener for a particular event with the following syntax: + * (code) + * control.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 control.events.object (a reference + * to the control). + * element - {DOMElement} A reference to control.events.element (which + * will be null unless documented otherwise). + * + * Supported map event types: + * activate - Triggered when activated. + * deactivate - Triggered when deactivated. + */ + events: null, + + /** + * Constructor: OpenLayers.Control + * Create an OpenLayers Control. The options passed as a parameter + * directly extend the control. For example passing the following: + * + * > var control = new OpenLayers.Control({div: myDiv}); + * + * Overrides the default div attribute value of null. + * + * Parameters: + * options - {Object} + */ + initialize: function (options) { + // We do this before the extend so that instances can override + // className in options. + this.displayClass = + this.CLASS_NAME.replace("OpenLayers.", "ol").replace(/\./g, ""); + + OpenLayers.Util.extend(this, options); + + this.events = new OpenLayers.Events(this); + if(this.eventListeners instanceof Object) { + this.events.on(this.eventListeners); + } + if (this.id == null) { + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + } + }, + + /** + * Method: destroy + * The destroy method is used to perform any clean up before the control + * is dereferenced. Typically this is where event listeners are removed + * to prevent memory leaks. + */ + destroy: function () { + if(this.events) { + if(this.eventListeners) { + this.events.un(this.eventListeners); + } + this.events.destroy(); + this.events = null; + } + this.eventListeners = null; + + // eliminate circular references + if (this.handler) { + this.handler.destroy(); + this.handler = null; + } + if(this.handlers) { + for(var key in this.handlers) { + if(this.handlers.hasOwnProperty(key) && + typeof this.handlers[key].destroy == "function") { + this.handlers[key].destroy(); + } + } + this.handlers = null; + } + if (this.map) { + this.map.removeControl(this); + this.map = null; + } + this.div = null; + }, + + /** + * Method: setMap + * Set the map property for the control. This is done through an accessor + * so that subclasses can override this and take special action once + * they have their map variable set. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + this.map = map; + if (this.handler) { + this.handler.setMap(map); + } + }, + + /** + * Method: draw + * The draw method is called when the control is ready to be displayed + * on the page. If a div has not been created one is created. Controls + * with a visual component will almost always want to override this method + * to customize the look of control. + * + * Parameters: + * px - {<OpenLayers.Pixel>} The top-left pixel position of the control + * or null. + * + * Returns: + * {DOMElement} A reference to the DIV DOMElement containing the control + */ + draw: function (px) { + if (this.div == null) { + this.div = OpenLayers.Util.createDiv(this.id); + this.div.className = this.displayClass; + if (!this.allowSelection) { + this.div.className += " olControlNoSelect"; + this.div.setAttribute("unselectable", "on", 0); + this.div.onselectstart = OpenLayers.Function.False; + } + if (this.title != "") { + this.div.title = this.title; + } + } + if (px != null) { + this.position = px.clone(); + } + this.moveTo(this.position); + return this.div; + }, + + /** + * Method: moveTo + * Sets the left and top style attributes to the passed in pixel + * coordinates. + * + * Parameters: + * px - {<OpenLayers.Pixel>} + */ + moveTo: function (px) { + if ((px != null) && (this.div != null)) { + this.div.style.left = px.x + "px"; + this.div.style.top = px.y + "px"; + } + }, + + /** + * APIMethod: activate + * Explicitly activates a control and it's associated + * handler if one has been set. Controls can be + * deactivated by calling the deactivate() method. + * + * Returns: + * {Boolean} True if the control was successfully activated or + * false if the control was already active. + */ + activate: function () { + if (this.active) { + return false; + } + if (this.handler) { + this.handler.activate(); + } + this.active = true; + if(this.map) { + OpenLayers.Element.addClass( + this.map.viewPortDiv, + this.displayClass.replace(/ /g, "") + "Active" + ); + } + this.events.triggerEvent("activate"); + return true; + }, + + /** + * APIMethod: deactivate + * Deactivates a control and it's associated handler if any. The exact + * effect of this depends on the control itself. + * + * Returns: + * {Boolean} True if the control was effectively deactivated or false + * if the control was already inactive. + */ + deactivate: function () { + if (this.active) { + if (this.handler) { + this.handler.deactivate(); + } + this.active = false; + if(this.map) { + OpenLayers.Element.removeClass( + this.map.viewPortDiv, + this.displayClass.replace(/ /g, "") + "Active" + ); + } + this.events.triggerEvent("deactivate"); + return true; + } + return false; + }, + + CLASS_NAME: "OpenLayers.Control" +}); + +/** + * Constant: OpenLayers.Control.TYPE_BUTTON + */ +OpenLayers.Control.TYPE_BUTTON = 1; + +/** + * Constant: OpenLayers.Control.TYPE_TOGGLE + */ +OpenLayers.Control.TYPE_TOGGLE = 2; + +/** + * Constant: OpenLayers.Control.TYPE_TOOL + */ +OpenLayers.Control.TYPE_TOOL = 3; +/* ====================================================================== + OpenLayers/Geometry.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Geometry + * A Geometry is a description of a geographic object. Create an instance of + * this class with the <OpenLayers.Geometry> constructor. This is a base class, + * typical geometry types are described by subclasses of this class. + * + * Note that if you use the <OpenLayers.Geometry.fromWKT> method, you must + * explicitly include the OpenLayers.Format.WKT in your build. + */ +OpenLayers.Geometry = OpenLayers.Class({ + + /** + * Property: id + * {String} A unique identifier for this geometry. + */ + id: null, + + /** + * Property: parent + * {<OpenLayers.Geometry>}This is set when a Geometry is added as component + * of another geometry + */ + parent: null, + + /** + * Property: bounds + * {<OpenLayers.Bounds>} The bounds of this geometry + */ + bounds: null, + + /** + * Constructor: OpenLayers.Geometry + * Creates a geometry object. + */ + initialize: function() { + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME+ "_"); + }, + + /** + * Method: destroy + * Destroy this geometry. + */ + destroy: function() { + this.id = null; + this.bounds = null; + }, + + /** + * APIMethod: clone + * Create a clone of this geometry. Does not set any non-standard + * properties of the cloned geometry. + * + * Returns: + * {<OpenLayers.Geometry>} An exact clone of this geometry. + */ + clone: function() { + return new OpenLayers.Geometry(); + }, + + /** + * Method: setBounds + * Set the bounds for this Geometry. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + */ + setBounds: function(bounds) { + if (bounds) { + this.bounds = bounds.clone(); + } + }, + + /** + * Method: clearBounds + * Nullify this components bounds and that of its parent as well. + */ + clearBounds: function() { + this.bounds = null; + if (this.parent) { + this.parent.clearBounds(); + } + }, + + /** + * Method: extendBounds + * Extend the existing bounds to include the new bounds. + * If geometry's bounds is not yet set, then set a new Bounds. + * + * Parameters: + * newBounds - {<OpenLayers.Bounds>} + */ + extendBounds: function(newBounds){ + var bounds = this.getBounds(); + if (!bounds) { + this.setBounds(newBounds); + } else { + this.bounds.extend(newBounds); + } + }, + + /** + * APIMethod: getBounds + * Get the bounds for this Geometry. If bounds is not set, it + * is calculated again, this makes queries faster. + * + * Returns: + * {<OpenLayers.Bounds>} + */ + getBounds: function() { + if (this.bounds == null) { + this.calculateBounds(); + } + return this.bounds; + }, + + /** + * APIMethod: calculateBounds + * Recalculate the bounds for the geometry. + */ + calculateBounds: function() { + // + // This should be overridden by subclasses. + // + }, + + /** + * APIMethod: distanceTo + * Calculate the closest distance between two geometries (on the x-y plane). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Optional properties for configuring the distance + * calculation. + * + * Valid options depend on the specific geometry type. + * + * Returns: + * {Number | Object} The distance between this geometry and the target. + * If details is true, the return will be an object with distance, + * x0, y0, x1, and x2 properties. The x0 and y0 properties represent + * the coordinates of the closest point on this geometry. The x1 and y1 + * properties represent the coordinates of the closest point on the + * target geometry. + */ + distanceTo: function(geometry, options) { + }, + + /** + * APIMethod: getVertices + * Return a list of all points in this geometry. + * + * Parameters: + * nodes - {Boolean} For lines, only return vertices that are + * endpoints. If false, for lines, only vertices that are not + * endpoints will be returned. If not provided, all vertices will + * be returned. + * + * Returns: + * {Array} A list of all vertices in the geometry. + */ + getVertices: function(nodes) { + }, + + /** + * Method: atPoint + * Note - This is only an approximation based on the bounds of the + * geometry. + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>|Object} OpenLayers.LonLat or an + * object with a 'lon' and 'lat' properties. + * toleranceLon - {float} Optional tolerance in Geometric Coords + * toleranceLat - {float} Optional tolerance in Geographic Coords + * + * Returns: + * {Boolean} Whether or not the geometry is at the specified location + */ + atPoint: function(lonlat, toleranceLon, toleranceLat) { + var atPoint = false; + var bounds = this.getBounds(); + if ((bounds != null) && (lonlat != null)) { + + var dX = (toleranceLon != null) ? toleranceLon : 0; + var dY = (toleranceLat != null) ? toleranceLat : 0; + + var toleranceBounds = + new OpenLayers.Bounds(this.bounds.left - dX, + this.bounds.bottom - dY, + this.bounds.right + dX, + this.bounds.top + dY); + + atPoint = toleranceBounds.containsLonLat(lonlat); + } + return atPoint; + }, + + /** + * Method: getLength + * Calculate the length of this geometry. This method is defined in + * subclasses. + * + * Returns: + * {Float} The length of the collection by summing its parts + */ + getLength: function() { + //to be overridden by geometries that actually have a length + // + return 0.0; + }, + + /** + * Method: getArea + * Calculate the area of this geometry. This method is defined in subclasses. + * + * Returns: + * {Float} The area of the collection by summing its parts + */ + getArea: function() { + //to be overridden by geometries that actually have an area + // + return 0.0; + }, + + /** + * APIMethod: getCentroid + * Calculate the centroid of this geometry. This method is defined in subclasses. + * + * Returns: + * {<OpenLayers.Geometry.Point>} The centroid of the collection + */ + getCentroid: function() { + return null; + }, + + /** + * Method: toString + * Returns a text representation of the geometry. If the WKT format is + * included in a build, this will be the Well-Known Text + * representation. + * + * Returns: + * {String} String representation of this geometry. + */ + toString: function() { + var string; + if (OpenLayers.Format && OpenLayers.Format.WKT) { + string = OpenLayers.Format.WKT.prototype.write( + new OpenLayers.Feature.Vector(this) + ); + } else { + string = Object.prototype.toString.call(this); + } + return string; + }, + + CLASS_NAME: "OpenLayers.Geometry" +}); + +/** + * Function: OpenLayers.Geometry.fromWKT + * Generate a geometry given a Well-Known Text string. For this method to + * work, you must include the OpenLayers.Format.WKT in your build + * explicitly. + * + * Parameters: + * wkt - {String} A string representing the geometry in Well-Known Text. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry of the appropriate class. + */ +OpenLayers.Geometry.fromWKT = function(wkt) { + var geom; + if (OpenLayers.Format && OpenLayers.Format.WKT) { + var format = OpenLayers.Geometry.fromWKT.format; + if (!format) { + format = new OpenLayers.Format.WKT(); + OpenLayers.Geometry.fromWKT.format = format; + } + var result = format.read(wkt); + if (result instanceof OpenLayers.Feature.Vector) { + geom = result.geometry; + } else if (OpenLayers.Util.isArray(result)) { + var len = result.length; + var components = new Array(len); + for (var i=0; i<len; ++i) { + components[i] = result[i].geometry; + } + geom = new OpenLayers.Geometry.Collection(components); + } + } + return geom; +}; + +/** + * Method: OpenLayers.Geometry.segmentsIntersect + * Determine whether two line segments intersect. Optionally calculates + * and returns the intersection point. This function is optimized for + * cases where seg1.x2 >= seg2.x1 || seg2.x2 >= seg1.x1. In those + * obvious cases where there is no intersection, the function should + * not be called. + * + * Parameters: + * seg1 - {Object} Object representing a segment with properties x1, y1, x2, + * and y2. The start point is represented by x1 and y1. The end point + * is represented by x2 and y2. Start and end are ordered so that x1 < x2. + * seg2 - {Object} Object representing a segment with properties x1, y1, x2, + * and y2. The start point is represented by x1 and y1. The end point + * is represented by x2 and y2. Start and end are ordered so that x1 < x2. + * options - {Object} Optional properties for calculating the intersection. + * + * Valid options: + * point - {Boolean} Return the intersection point. If false, the actual + * intersection point will not be calculated. If true and the segments + * intersect, the intersection point will be returned. If true and + * the segments do not intersect, false will be returned. If true and + * the segments are coincident, true will be returned. + * tolerance - {Number} If a non-null value is provided, if the segments are + * within the tolerance distance, this will be considered an intersection. + * In addition, if the point option is true and the calculated intersection + * is within the tolerance distance of an end point, the endpoint will be + * returned instead of the calculated intersection. Further, if the + * intersection is within the tolerance of endpoints on both segments, or + * if two segment endpoints are within the tolerance distance of eachother + * (but no intersection is otherwise calculated), an endpoint on the + * first segment provided will be returned. + * + * Returns: + * {Boolean | <OpenLayers.Geometry.Point>} The two segments intersect. + * If the point argument is true, the return will be the intersection + * point or false if none exists. If point is true and the segments + * are coincident, return will be true (and the instersection is equal + * to the shorter segment). + */ +OpenLayers.Geometry.segmentsIntersect = function(seg1, seg2, options) { + var point = options && options.point; + var tolerance = options && options.tolerance; + var intersection = false; + var x11_21 = seg1.x1 - seg2.x1; + var y11_21 = seg1.y1 - seg2.y1; + var x12_11 = seg1.x2 - seg1.x1; + var y12_11 = seg1.y2 - seg1.y1; + var y22_21 = seg2.y2 - seg2.y1; + var x22_21 = seg2.x2 - seg2.x1; + var d = (y22_21 * x12_11) - (x22_21 * y12_11); + var n1 = (x22_21 * y11_21) - (y22_21 * x11_21); + var n2 = (x12_11 * y11_21) - (y12_11 * x11_21); + if(d == 0) { + // parallel + if(n1 == 0 && n2 == 0) { + // coincident + intersection = true; + } + } else { + var along1 = n1 / d; + var along2 = n2 / d; + if(along1 >= 0 && along1 <= 1 && along2 >=0 && along2 <= 1) { + // intersect + if(!point) { + intersection = true; + } else { + // calculate the intersection point + var x = seg1.x1 + (along1 * x12_11); + var y = seg1.y1 + (along1 * y12_11); + intersection = new OpenLayers.Geometry.Point(x, y); + } + } + } + if(tolerance) { + var dist; + if(intersection) { + if(point) { + var segs = [seg1, seg2]; + var seg, x, y; + // check segment endpoints for proximity to intersection + // set intersection to first endpoint within the tolerance + outer: for(var i=0; i<2; ++i) { + seg = segs[i]; + for(var j=1; j<3; ++j) { + x = seg["x" + j]; + y = seg["y" + j]; + dist = Math.sqrt( + Math.pow(x - intersection.x, 2) + + Math.pow(y - intersection.y, 2) + ); + if(dist < tolerance) { + intersection.x = x; + intersection.y = y; + break outer; + } + } + } + + } + } else { + // no calculated intersection, but segments could be within + // the tolerance of one another + var segs = [seg1, seg2]; + var source, target, x, y, p, result; + // check segment endpoints for proximity to intersection + // set intersection to first endpoint within the tolerance + outer: for(var i=0; i<2; ++i) { + source = segs[i]; + target = segs[(i+1)%2]; + for(var j=1; j<3; ++j) { + p = {x: source["x"+j], y: source["y"+j]}; + result = OpenLayers.Geometry.distanceToSegment(p, target); + if(result.distance < tolerance) { + if(point) { + intersection = new OpenLayers.Geometry.Point(p.x, p.y); + } else { + intersection = true; + } + break outer; + } + } + } + } + } + return intersection; +}; + +/** + * Function: OpenLayers.Geometry.distanceToSegment + * + * Parameters: + * point - {Object} An object with x and y properties representing the + * point coordinates. + * segment - {Object} An object with x1, y1, x2, and y2 properties + * representing endpoint coordinates. + * + * Returns: + * {Object} An object with distance, along, x, and y properties. The distance + * will be the shortest distance between the input point and segment. + * The x and y properties represent the coordinates along the segment + * where the shortest distance meets the segment. The along attribute + * describes how far between the two segment points the given point is. + */ +OpenLayers.Geometry.distanceToSegment = function(point, segment) { + var result = OpenLayers.Geometry.distanceSquaredToSegment(point, segment); + result.distance = Math.sqrt(result.distance); + return result; +}; + +/** + * Function: OpenLayers.Geometry.distanceSquaredToSegment + * + * Usually the distanceToSegment function should be used. This variant however + * can be used for comparisons where the exact distance is not important. + * + * Parameters: + * point - {Object} An object with x and y properties representing the + * point coordinates. + * segment - {Object} An object with x1, y1, x2, and y2 properties + * representing endpoint coordinates. + * + * Returns: + * {Object} An object with squared distance, along, x, and y properties. + * The distance will be the shortest distance between the input point and + * segment. The x and y properties represent the coordinates along the + * segment where the shortest distance meets the segment. The along + * attribute describes how far between the two segment points the given + * point is. + */ +OpenLayers.Geometry.distanceSquaredToSegment = function(point, segment) { + var x0 = point.x; + var y0 = point.y; + var x1 = segment.x1; + var y1 = segment.y1; + var x2 = segment.x2; + var y2 = segment.y2; + var dx = x2 - x1; + var dy = y2 - y1; + var along = ((dx * (x0 - x1)) + (dy * (y0 - y1))) / + (Math.pow(dx, 2) + Math.pow(dy, 2)); + var x, y; + if(along <= 0.0) { + x = x1; + y = y1; + } else if(along >= 1.0) { + x = x2; + y = y2; + } else { + x = x1 + along * dx; + y = y1 + along * dy; + } + return { + distance: Math.pow(x - x0, 2) + Math.pow(y - y0, 2), + x: x, y: y, + along: along + }; +}; +/* ====================================================================== + OpenLayers/Geometry/Collection.js + ====================================================================== */ + +/* 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/Geometry.js + */ + +/** + * Class: OpenLayers.Geometry.Collection + * A Collection is exactly what it sounds like: A collection of different + * Geometries. These are stored in the local parameter <components> (which + * can be passed as a parameter to the constructor). + * + * As new geometries are added to the collection, they are NOT cloned. + * When removing geometries, they need to be specified by reference (ie you + * have to pass in the *exact* geometry to be removed). + * + * The <getArea> and <getLength> functions here merely iterate through + * the components, summing their respective areas and lengths. + * + * Create a new instance with the <OpenLayers.Geometry.Collection> constructor. + * + * Inherits from: + * - <OpenLayers.Geometry> + */ +OpenLayers.Geometry.Collection = OpenLayers.Class(OpenLayers.Geometry, { + + /** + * APIProperty: components + * {Array(<OpenLayers.Geometry>)} The component parts of this geometry + */ + components: null, + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null value means the + * component types are not restricted. + */ + componentTypes: null, + + /** + * Constructor: OpenLayers.Geometry.Collection + * Creates a Geometry Collection -- a list of geoms. + * + * Parameters: + * components - {Array(<OpenLayers.Geometry>)} Optional array of geometries + * + */ + initialize: function (components) { + OpenLayers.Geometry.prototype.initialize.apply(this, arguments); + this.components = []; + if (components != null) { + this.addComponents(components); + } + }, + + /** + * APIMethod: destroy + * Destroy this geometry. + */ + destroy: function () { + this.components.length = 0; + this.components = null; + OpenLayers.Geometry.prototype.destroy.apply(this, arguments); + }, + + /** + * APIMethod: clone + * Clone this geometry. + * + * Returns: + * {<OpenLayers.Geometry.Collection>} An exact clone of this collection + */ + clone: function() { + var geometry = eval("new " + this.CLASS_NAME + "()"); + for(var i=0, len=this.components.length; i<len; i++) { + geometry.addComponent(this.components[i].clone()); + } + + // catch any randomly tagged-on properties + OpenLayers.Util.applyDefaults(geometry, this); + + return geometry; + }, + + /** + * Method: getComponentsString + * Get a string representing the components for this collection + * + * Returns: + * {String} A string representation of the components of this geometry + */ + getComponentsString: function(){ + var strings = []; + for(var i=0, len=this.components.length; i<len; i++) { + strings.push(this.components[i].toShortString()); + } + return strings.join(","); + }, + + /** + * APIMethod: calculateBounds + * Recalculate the bounds by iterating through the components and + * calling calling extendBounds() on each item. + */ + calculateBounds: function() { + this.bounds = null; + var bounds = new OpenLayers.Bounds(); + var components = this.components; + if (components) { + for (var i=0, len=components.length; i<len; i++) { + bounds.extend(components[i].getBounds()); + } + } + // to preserve old behavior, we only set bounds if non-null + // in the future, we could add bounds.isEmpty() + if (bounds.left != null && bounds.bottom != null && + bounds.right != null && bounds.top != null) { + this.setBounds(bounds); + } + }, + + /** + * APIMethod: addComponents + * Add components to this geometry. + * + * Parameters: + * components - {Array(<OpenLayers.Geometry>)} An array of geometries to add + */ + addComponents: function(components){ + if(!(OpenLayers.Util.isArray(components))) { + components = [components]; + } + for(var i=0, len=components.length; i<len; i++) { + this.addComponent(components[i]); + } + }, + + /** + * Method: addComponent + * Add a new component (geometry) to the collection. If this.componentTypes + * is set, then the component class name must be in the componentTypes array. + * + * The bounds cache is reset. + * + * Parameters: + * component - {<OpenLayers.Geometry>} A geometry to add + * index - {int} Optional index into the array to insert the component + * + * Returns: + * {Boolean} The component geometry was successfully added + */ + addComponent: function(component, index) { + var added = false; + if(component) { + if(this.componentTypes == null || + (OpenLayers.Util.indexOf(this.componentTypes, + component.CLASS_NAME) > -1)) { + + if(index != null && (index < this.components.length)) { + var components1 = this.components.slice(0, index); + var components2 = this.components.slice(index, + this.components.length); + components1.push(component); + this.components = components1.concat(components2); + } else { + this.components.push(component); + } + component.parent = this; + this.clearBounds(); + added = true; + } + } + return added; + }, + + /** + * APIMethod: removeComponents + * Remove components from this geometry. + * + * Parameters: + * components - {Array(<OpenLayers.Geometry>)} The components to be removed + * + * Returns: + * {Boolean} A component was removed. + */ + removeComponents: function(components) { + var removed = false; + + if(!(OpenLayers.Util.isArray(components))) { + components = [components]; + } + for(var i=components.length-1; i>=0; --i) { + removed = this.removeComponent(components[i]) || removed; + } + return removed; + }, + + /** + * Method: removeComponent + * Remove a component from this geometry. + * + * Parameters: + * component - {<OpenLayers.Geometry>} + * + * Returns: + * {Boolean} The component was removed. + */ + removeComponent: function(component) { + + OpenLayers.Util.removeItem(this.components, component); + + // clearBounds() so that it gets recalculated on the next call + // to this.getBounds(); + this.clearBounds(); + return true; + }, + + /** + * APIMethod: getLength + * Calculate the length of this geometry + * + * Returns: + * {Float} The length of the geometry + */ + getLength: function() { + var length = 0.0; + for (var i=0, len=this.components.length; i<len; i++) { + length += this.components[i].getLength(); + } + return length; + }, + + /** + * APIMethod: getArea + * Calculate the area of this geometry. Note how this function is overridden + * in <OpenLayers.Geometry.Polygon>. + * + * Returns: + * {Float} The area of the collection by summing its parts + */ + getArea: function() { + var area = 0.0; + for (var i=0, len=this.components.length; i<len; i++) { + area += this.components[i].getArea(); + } + return area; + }, + + /** + * APIMethod: getGeodesicArea + * Calculate the approximate area of the polygon were it projected onto + * the earth. + * + * Parameters: + * projection - {<OpenLayers.Projection>} The spatial reference system + * for the geometry coordinates. If not provided, Geographic/WGS84 is + * assumed. + * + * Reference: + * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for + * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion + * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409 + * + * Returns: + * {float} The approximate geodesic area of the geometry in square meters. + */ + getGeodesicArea: function(projection) { + var area = 0.0; + for(var i=0, len=this.components.length; i<len; i++) { + area += this.components[i].getGeodesicArea(projection); + } + return area; + }, + + /** + * APIMethod: getCentroid + * + * Compute the centroid for this geometry collection. + * + * Parameters: + * weighted - {Boolean} Perform the getCentroid computation recursively, + * returning an area weighted average of all geometries in this collection. + * + * Returns: + * {<OpenLayers.Geometry.Point>} The centroid of the collection + */ + getCentroid: function(weighted) { + if (!weighted) { + return this.components.length && this.components[0].getCentroid(); + } + var len = this.components.length; + if (!len) { + return false; + } + + var areas = []; + var centroids = []; + var areaSum = 0; + var minArea = Number.MAX_VALUE; + var component; + for (var i=0; i<len; ++i) { + component = this.components[i]; + var area = component.getArea(); + var centroid = component.getCentroid(true); + if (isNaN(area) || isNaN(centroid.x) || isNaN(centroid.y)) { + continue; + } + areas.push(area); + areaSum += area; + minArea = (area < minArea && area > 0) ? area : minArea; + centroids.push(centroid); + } + len = areas.length; + if (areaSum === 0) { + // all the components in this collection have 0 area + // probably a collection of points -- weight all the points the same + for (var i=0; i<len; ++i) { + areas[i] = 1; + } + areaSum = areas.length; + } else { + // normalize all the areas where the smallest area will get + // a value of 1 + for (var i=0; i<len; ++i) { + areas[i] /= minArea; + } + areaSum /= minArea; + } + + var xSum = 0, ySum = 0, centroid, area; + for (var i=0; i<len; ++i) { + centroid = centroids[i]; + area = areas[i]; + xSum += centroid.x * area; + ySum += centroid.y * area; + } + + return new OpenLayers.Geometry.Point(xSum/areaSum, ySum/areaSum); + }, + + /** + * APIMethod: getGeodesicLength + * Calculate the approximate length of the geometry were it projected onto + * the earth. + * + * projection - {<OpenLayers.Projection>} The spatial reference system + * for the geometry coordinates. If not provided, Geographic/WGS84 is + * assumed. + * + * Returns: + * {Float} The appoximate geodesic length of the geometry in meters. + */ + getGeodesicLength: function(projection) { + var length = 0.0; + for(var i=0, len=this.components.length; i<len; i++) { + length += this.components[i].getGeodesicLength(projection); + } + return length; + }, + + /** + * APIMethod: move + * Moves a geometry by the given displacement along positive x and y axes. + * This modifies the position of the geometry and clears the cached + * bounds. + * + * Parameters: + * x - {Float} Distance to move geometry in positive x direction. + * y - {Float} Distance to move geometry in positive y direction. + */ + move: function(x, y) { + for(var i=0, len=this.components.length; i<len; i++) { + this.components[i].move(x, y); + } + }, + + /** + * APIMethod: rotate + * Rotate a geometry around some origin + * + * Parameters: + * angle - {Float} Rotation angle in degrees (measured counterclockwise + * from the positive x-axis) + * origin - {<OpenLayers.Geometry.Point>} Center point for the rotation + */ + rotate: function(angle, origin) { + for(var i=0, len=this.components.length; i<len; ++i) { + this.components[i].rotate(angle, origin); + } + }, + + /** + * APIMethod: resize + * Resize a geometry relative to some origin. Use this method to apply + * a uniform scaling to a geometry. + * + * Parameters: + * scale - {Float} Factor by which to scale the geometry. A scale of 2 + * doubles the size of the geometry in each dimension + * (lines, for example, will be twice as long, and polygons + * will have four times the area). + * origin - {<OpenLayers.Geometry.Point>} Point of origin for resizing + * ratio - {Float} Optional x:y ratio for resizing. Default ratio is 1. + * + * Returns: + * {<OpenLayers.Geometry>} - The current geometry. + */ + resize: function(scale, origin, ratio) { + for(var i=0; i<this.components.length; ++i) { + this.components[i].resize(scale, origin, ratio); + } + return this; + }, + + /** + * APIMethod: distanceTo + * Calculate the closest distance between two geometries (on the x-y plane). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Optional properties for configuring the distance + * calculation. + * + * Valid options: + * details - {Boolean} Return details from the distance calculation. + * Default is false. + * edge - {Boolean} Calculate the distance from this geometry to the + * nearest edge of the target geometry. Default is true. If true, + * calling distanceTo from a geometry that is wholly contained within + * the target will result in a non-zero distance. If false, whenever + * geometries intersect, calling distanceTo will return 0. If false, + * details cannot be returned. + * + * Returns: + * {Number | Object} The distance between this geometry and the target. + * If details is true, the return will be an object with distance, + * x0, y0, x1, and y1 properties. The x0 and y0 properties represent + * the coordinates of the closest point on this geometry. The x1 and y1 + * properties represent the coordinates of the closest point on the + * target geometry. + */ + distanceTo: function(geometry, options) { + var edge = !(options && options.edge === false); + var details = edge && options && options.details; + var result, best, distance; + var min = Number.POSITIVE_INFINITY; + for(var i=0, len=this.components.length; i<len; ++i) { + result = this.components[i].distanceTo(geometry, options); + distance = details ? result.distance : result; + if(distance < min) { + min = distance; + best = result; + if(min == 0) { + break; + } + } + } + return best; + }, + + /** + * APIMethod: equals + * Determine whether another geometry is equivalent to this one. Geometries + * are considered equivalent if all components have the same coordinates. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The geometry to test. + * + * Returns: + * {Boolean} The supplied geometry is equivalent to this geometry. + */ + equals: function(geometry) { + var equivalent = true; + if(!geometry || !geometry.CLASS_NAME || + (this.CLASS_NAME != geometry.CLASS_NAME)) { + equivalent = false; + } else if(!(OpenLayers.Util.isArray(geometry.components)) || + (geometry.components.length != this.components.length)) { + equivalent = false; + } else { + for(var i=0, len=this.components.length; i<len; ++i) { + if(!this.components[i].equals(geometry.components[i])) { + equivalent = false; + break; + } + } + } + return equivalent; + }, + + /** + * APIMethod: transform + * Reproject the components geometry from source to dest. + * + * Parameters: + * source - {<OpenLayers.Projection>} + * dest - {<OpenLayers.Projection>} + * + * Returns: + * {<OpenLayers.Geometry>} + */ + transform: function(source, dest) { + if (source && dest) { + for (var i=0, len=this.components.length; i<len; i++) { + var component = this.components[i]; + component.transform(source, dest); + } + this.bounds = null; + } + return this; + }, + + /** + * APIMethod: intersects + * Determine if the input geometry intersects this one. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} Any type of geometry. + * + * Returns: + * {Boolean} The input geometry intersects this one. + */ + intersects: function(geometry) { + var intersect = false; + for(var i=0, len=this.components.length; i<len; ++ i) { + intersect = geometry.intersects(this.components[i]); + if(intersect) { + break; + } + } + return intersect; + }, + + /** + * APIMethod: getVertices + * Return a list of all points in this geometry. + * + * Parameters: + * nodes - {Boolean} For lines, only return vertices that are + * endpoints. If false, for lines, only vertices that are not + * endpoints will be returned. If not provided, all vertices will + * be returned. + * + * Returns: + * {Array} A list of all vertices in the geometry. + */ + getVertices: function(nodes) { + var vertices = []; + for(var i=0, len=this.components.length; i<len; ++i) { + Array.prototype.push.apply( + vertices, this.components[i].getVertices(nodes) + ); + } + return vertices; + }, + + + CLASS_NAME: "OpenLayers.Geometry.Collection" +}); +/* ====================================================================== + OpenLayers/Geometry/Point.js + ====================================================================== */ + +/* 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/Geometry.js + */ + +/** + * Class: OpenLayers.Geometry.Point + * Point geometry class. + * + * Inherits from: + * - <OpenLayers.Geometry> + */ +OpenLayers.Geometry.Point = OpenLayers.Class(OpenLayers.Geometry, { + + /** + * APIProperty: x + * {float} + */ + x: null, + + /** + * APIProperty: y + * {float} + */ + y: null, + + /** + * Constructor: OpenLayers.Geometry.Point + * Construct a point geometry. + * + * Parameters: + * x - {float} + * y - {float} + * + */ + initialize: function(x, y) { + OpenLayers.Geometry.prototype.initialize.apply(this, arguments); + + this.x = parseFloat(x); + this.y = parseFloat(y); + }, + + /** + * APIMethod: clone + * + * Returns: + * {<OpenLayers.Geometry.Point>} An exact clone of this OpenLayers.Geometry.Point + */ + clone: function(obj) { + if (obj == null) { + obj = new OpenLayers.Geometry.Point(this.x, this.y); + } + + // catch any randomly tagged-on properties + OpenLayers.Util.applyDefaults(obj, this); + + return obj; + }, + + /** + * Method: calculateBounds + * Create a new Bounds based on the lon/lat + */ + calculateBounds: function () { + this.bounds = new OpenLayers.Bounds(this.x, this.y, + this.x, this.y); + }, + + /** + * APIMethod: distanceTo + * Calculate the closest distance between two geometries (on the x-y plane). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Optional properties for configuring the distance + * calculation. + * + * Valid options: + * details - {Boolean} Return details from the distance calculation. + * Default is false. + * edge - {Boolean} Calculate the distance from this geometry to the + * nearest edge of the target geometry. Default is true. If true, + * calling distanceTo from a geometry that is wholly contained within + * the target will result in a non-zero distance. If false, whenever + * geometries intersect, calling distanceTo will return 0. If false, + * details cannot be returned. + * + * Returns: + * {Number | Object} The distance between this geometry and the target. + * If details is true, the return will be an object with distance, + * x0, y0, x1, and x2 properties. The x0 and y0 properties represent + * the coordinates of the closest point on this geometry. The x1 and y1 + * properties represent the coordinates of the closest point on the + * target geometry. + */ + distanceTo: function(geometry, options) { + var edge = !(options && options.edge === false); + var details = edge && options && options.details; + var distance, x0, y0, x1, y1, result; + if(geometry instanceof OpenLayers.Geometry.Point) { + x0 = this.x; + y0 = this.y; + x1 = geometry.x; + y1 = geometry.y; + distance = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); + result = !details ? + distance : {x0: x0, y0: y0, x1: x1, y1: y1, distance: distance}; + } else { + result = geometry.distanceTo(this, options); + if(details) { + // switch coord order since this geom is target + result = { + x0: result.x1, y0: result.y1, + x1: result.x0, y1: result.y0, + distance: result.distance + }; + } + } + return result; + }, + + /** + * APIMethod: equals + * Determine whether another geometry is equivalent to this one. Geometries + * are considered equivalent if all components have the same coordinates. + * + * Parameters: + * geom - {<OpenLayers.Geometry.Point>} The geometry to test. + * + * Returns: + * {Boolean} The supplied geometry is equivalent to this geometry. + */ + equals: function(geom) { + var equals = false; + if (geom != null) { + equals = ((this.x == geom.x && this.y == geom.y) || + (isNaN(this.x) && isNaN(this.y) && isNaN(geom.x) && isNaN(geom.y))); + } + return equals; + }, + + /** + * Method: toShortString + * + * Returns: + * {String} Shortened String representation of Point object. + * (ex. <i>"5, 42"</i>) + */ + toShortString: function() { + return (this.x + ", " + this.y); + }, + + /** + * APIMethod: move + * Moves a geometry by the given displacement along positive x and y axes. + * This modifies the position of the geometry and clears the cached + * bounds. + * + * Parameters: + * x - {Float} Distance to move geometry in positive x direction. + * y - {Float} Distance to move geometry in positive y direction. + */ + move: function(x, y) { + this.x = this.x + x; + this.y = this.y + y; + this.clearBounds(); + }, + + /** + * APIMethod: rotate + * Rotate a point around another. + * + * Parameters: + * angle - {Float} Rotation angle in degrees (measured counterclockwise + * from the positive x-axis) + * origin - {<OpenLayers.Geometry.Point>} Center point for the rotation + */ + rotate: function(angle, origin) { + angle *= Math.PI / 180; + var radius = this.distanceTo(origin); + var theta = angle + Math.atan2(this.y - origin.y, this.x - origin.x); + this.x = origin.x + (radius * Math.cos(theta)); + this.y = origin.y + (radius * Math.sin(theta)); + this.clearBounds(); + }, + + /** + * APIMethod: getCentroid + * + * Returns: + * {<OpenLayers.Geometry.Point>} The centroid of the collection + */ + getCentroid: function() { + return new OpenLayers.Geometry.Point(this.x, this.y); + }, + + /** + * APIMethod: resize + * Resize a point relative to some origin. For points, this has the effect + * of scaling a vector (from the origin to the point). This method is + * more useful on geometry collection subclasses. + * + * Parameters: + * scale - {Float} Ratio of the new distance from the origin to the old + * distance from the origin. A scale of 2 doubles the + * distance between the point and origin. + * origin - {<OpenLayers.Geometry.Point>} Point of origin for resizing + * ratio - {Float} Optional x:y ratio for resizing. Default ratio is 1. + * + * Returns: + * {<OpenLayers.Geometry>} - The current geometry. + */ + resize: function(scale, origin, ratio) { + ratio = (ratio == undefined) ? 1 : ratio; + this.x = origin.x + (scale * ratio * (this.x - origin.x)); + this.y = origin.y + (scale * (this.y - origin.y)); + this.clearBounds(); + return this; + }, + + /** + * APIMethod: intersects + * Determine if the input geometry intersects this one. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} Any type of geometry. + * + * Returns: + * {Boolean} The input geometry intersects this one. + */ + intersects: function(geometry) { + var intersect = false; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + intersect = this.equals(geometry); + } else { + intersect = geometry.intersects(this); + } + return intersect; + }, + + /** + * APIMethod: transform + * Translate the x,y properties of the point from source to dest. + * + * Parameters: + * source - {<OpenLayers.Projection>} + * dest - {<OpenLayers.Projection>} + * + * Returns: + * {<OpenLayers.Geometry>} + */ + transform: function(source, dest) { + if ((source && dest)) { + OpenLayers.Projection.transform( + this, source, dest); + this.bounds = null; + } + return this; + }, + + /** + * APIMethod: getVertices + * Return a list of all points in this geometry. + * + * Parameters: + * nodes - {Boolean} For lines, only return vertices that are + * endpoints. If false, for lines, only vertices that are not + * endpoints will be returned. If not provided, all vertices will + * be returned. + * + * Returns: + * {Array} A list of all vertices in the geometry. + */ + getVertices: function(nodes) { + return [this]; + }, + + CLASS_NAME: "OpenLayers.Geometry.Point" +}); +/* ====================================================================== + OpenLayers/Geometry/MultiPoint.js + ====================================================================== */ + +/* 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/Geometry/Collection.js + * @requires OpenLayers/Geometry/Point.js + */ + +/** + * Class: OpenLayers.Geometry.MultiPoint + * MultiPoint is a collection of Points. Create a new instance with the + * <OpenLayers.Geometry.MultiPoint> constructor. + * + * Inherits from: + * - <OpenLayers.Geometry.Collection> + * - <OpenLayers.Geometry> + */ +OpenLayers.Geometry.MultiPoint = OpenLayers.Class( + OpenLayers.Geometry.Collection, { + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null value means the + * component types are not restricted. + */ + componentTypes: ["OpenLayers.Geometry.Point"], + + /** + * Constructor: OpenLayers.Geometry.MultiPoint + * Create a new MultiPoint Geometry + * + * Parameters: + * components - {Array(<OpenLayers.Geometry.Point>)} + * + * Returns: + * {<OpenLayers.Geometry.MultiPoint>} + */ + + /** + * APIMethod: addPoint + * Wrapper for <OpenLayers.Geometry.Collection.addComponent> + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} Point to be added + * index - {Integer} Optional index + */ + addPoint: function(point, index) { + this.addComponent(point, index); + }, + + /** + * APIMethod: removePoint + * Wrapper for <OpenLayers.Geometry.Collection.removeComponent> + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} Point to be removed + */ + removePoint: function(point){ + this.removeComponent(point); + }, + + CLASS_NAME: "OpenLayers.Geometry.MultiPoint" +}); +/* ====================================================================== + OpenLayers/Geometry/Curve.js + ====================================================================== */ + +/* 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/Geometry/MultiPoint.js + */ + +/** + * Class: OpenLayers.Geometry.Curve + * A Curve is a MultiPoint, whose points are assumed to be connected. To + * this end, we provide a "getLength()" function, which iterates through + * the points, summing the distances between them. + * + * Inherits: + * - <OpenLayers.Geometry.MultiPoint> + */ +OpenLayers.Geometry.Curve = OpenLayers.Class(OpenLayers.Geometry.MultiPoint, { + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null + * value means the component types are not restricted. + */ + componentTypes: ["OpenLayers.Geometry.Point"], + + /** + * Constructor: OpenLayers.Geometry.Curve + * + * Parameters: + * point - {Array(<OpenLayers.Geometry.Point>)} + */ + + /** + * APIMethod: getLength + * + * Returns: + * {Float} The length of the curve + */ + getLength: function() { + var length = 0.0; + if ( this.components && (this.components.length > 1)) { + for(var i=1, len=this.components.length; i<len; i++) { + length += this.components[i-1].distanceTo(this.components[i]); + } + } + return length; + }, + + /** + * APIMethod: getGeodesicLength + * Calculate the approximate length of the geometry were it projected onto + * the earth. + * + * projection - {<OpenLayers.Projection>} The spatial reference system + * for the geometry coordinates. If not provided, Geographic/WGS84 is + * assumed. + * + * Returns: + * {Float} The appoximate geodesic length of the geometry in meters. + */ + getGeodesicLength: function(projection) { + var geom = this; // so we can work with a clone if needed + if(projection) { + var gg = new OpenLayers.Projection("EPSG:4326"); + if(!gg.equals(projection)) { + geom = this.clone().transform(projection, gg); + } + } + var length = 0.0; + if(geom.components && (geom.components.length > 1)) { + var p1, p2; + for(var i=1, len=geom.components.length; i<len; i++) { + p1 = geom.components[i-1]; + p2 = geom.components[i]; + // this returns km and requires lon/lat properties + length += OpenLayers.Util.distVincenty( + {lon: p1.x, lat: p1.y}, {lon: p2.x, lat: p2.y} + ); + } + } + // convert to m + return length * 1000; + }, + + CLASS_NAME: "OpenLayers.Geometry.Curve" +}); +/* ====================================================================== + OpenLayers/Geometry/LineString.js + ====================================================================== */ + +/* 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/Geometry/Curve.js + */ + +/** + * Class: OpenLayers.Geometry.LineString + * A LineString is a Curve which, once two points have been added to it, can + * never be less than two points long. + * + * Inherits from: + * - <OpenLayers.Geometry.Curve> + */ +OpenLayers.Geometry.LineString = OpenLayers.Class(OpenLayers.Geometry.Curve, { + + /** + * Constructor: OpenLayers.Geometry.LineString + * Create a new LineString geometry + * + * Parameters: + * points - {Array(<OpenLayers.Geometry.Point>)} An array of points used to + * generate the linestring + * + */ + + /** + * APIMethod: removeComponent + * Only allows removal of a point if there are three or more points in + * the linestring. (otherwise the result would be just a single point) + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} The point to be removed + * + * Returns: + * {Boolean} The component was removed. + */ + removeComponent: function(point) { + var removed = this.components && (this.components.length > 2); + if (removed) { + OpenLayers.Geometry.Collection.prototype.removeComponent.apply(this, + arguments); + } + return removed; + }, + + /** + * APIMethod: intersects + * Test for instersection between two geometries. This is a cheapo + * implementation of the Bently-Ottmann algorigithm. It doesn't + * really keep track of a sweep line data structure. It is closer + * to the brute force method, except that segments are sorted and + * potential intersections are only calculated when bounding boxes + * intersect. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {Boolean} The input geometry intersects this geometry. + */ + intersects: function(geometry) { + var intersect = false; + var type = geometry.CLASS_NAME; + if(type == "OpenLayers.Geometry.LineString" || + type == "OpenLayers.Geometry.LinearRing" || + type == "OpenLayers.Geometry.Point") { + var segs1 = this.getSortedSegments(); + var segs2; + if(type == "OpenLayers.Geometry.Point") { + segs2 = [{ + x1: geometry.x, y1: geometry.y, + x2: geometry.x, y2: geometry.y + }]; + } else { + segs2 = geometry.getSortedSegments(); + } + var seg1, seg1x1, seg1x2, seg1y1, seg1y2, + seg2, seg2y1, seg2y2; + // sweep right + outer: for(var i=0, len=segs1.length; i<len; ++i) { + seg1 = segs1[i]; + seg1x1 = seg1.x1; + seg1x2 = seg1.x2; + seg1y1 = seg1.y1; + seg1y2 = seg1.y2; + inner: for(var j=0, jlen=segs2.length; j<jlen; ++j) { + seg2 = segs2[j]; + if(seg2.x1 > seg1x2) { + // seg1 still left of seg2 + break; + } + if(seg2.x2 < seg1x1) { + // seg2 still left of seg1 + continue; + } + seg2y1 = seg2.y1; + seg2y2 = seg2.y2; + if(Math.min(seg2y1, seg2y2) > Math.max(seg1y1, seg1y2)) { + // seg2 above seg1 + continue; + } + if(Math.max(seg2y1, seg2y2) < Math.min(seg1y1, seg1y2)) { + // seg2 below seg1 + continue; + } + if(OpenLayers.Geometry.segmentsIntersect(seg1, seg2)) { + intersect = true; + break outer; + } + } + } + } else { + intersect = geometry.intersects(this); + } + return intersect; + }, + + /** + * Method: getSortedSegments + * + * Returns: + * {Array} An array of segment objects. Segment objects have properties + * x1, y1, x2, and y2. The start point is represented by x1 and y1. + * The end point is represented by x2 and y2. Start and end are + * ordered so that x1 < x2. + */ + getSortedSegments: function() { + var numSeg = this.components.length - 1; + var segments = new Array(numSeg), point1, point2; + for(var i=0; i<numSeg; ++i) { + point1 = this.components[i]; + point2 = this.components[i + 1]; + if(point1.x < point2.x) { + segments[i] = { + x1: point1.x, + y1: point1.y, + x2: point2.x, + y2: point2.y + }; + } else { + segments[i] = { + x1: point2.x, + y1: point2.y, + x2: point1.x, + y2: point1.y + }; + } + } + // more efficient to define this somewhere static + function byX1(seg1, seg2) { + return seg1.x1 - seg2.x1; + } + return segments.sort(byX1); + }, + + /** + * Method: splitWithSegment + * Split this geometry with the given segment. + * + * Parameters: + * seg - {Object} An object with x1, y1, x2, and y2 properties referencing + * segment endpoint coordinates. + * options - {Object} Properties of this object will be used to determine + * how the split is conducted. + * + * Valid options: + * edge - {Boolean} Allow splitting when only edges intersect. Default is + * true. If false, a vertex on the source segment must be within the + * tolerance distance of the intersection to be considered a split. + * tolerance - {Number} If a non-null value is provided, intersections + * within the tolerance distance of one of the source segment's + * endpoints will be assumed to occur at the endpoint. + * + * Returns: + * {Object} An object with *lines* and *points* properties. If the given + * segment intersects this linestring, the lines array will reference + * geometries that result from the split. The points array will contain + * all intersection points. Intersection points are sorted along the + * segment (in order from x1,y1 to x2,y2). + */ + splitWithSegment: function(seg, options) { + var edge = !(options && options.edge === false); + var tolerance = options && options.tolerance; + var lines = []; + var verts = this.getVertices(); + var points = []; + var intersections = []; + var split = false; + var vert1, vert2, point; + var node, vertex, target; + var interOptions = {point: true, tolerance: tolerance}; + var result = null; + for(var i=0, stop=verts.length-2; i<=stop; ++i) { + vert1 = verts[i]; + points.push(vert1.clone()); + vert2 = verts[i+1]; + target = {x1: vert1.x, y1: vert1.y, x2: vert2.x, y2: vert2.y}; + point = OpenLayers.Geometry.segmentsIntersect( + seg, target, interOptions + ); + if(point instanceof OpenLayers.Geometry.Point) { + if((point.x === seg.x1 && point.y === seg.y1) || + (point.x === seg.x2 && point.y === seg.y2) || + point.equals(vert1) || point.equals(vert2)) { + vertex = true; + } else { + vertex = false; + } + if(vertex || edge) { + // push intersections different than the previous + if(!point.equals(intersections[intersections.length-1])) { + intersections.push(point.clone()); + } + if(i === 0) { + if(point.equals(vert1)) { + continue; + } + } + if(point.equals(vert2)) { + continue; + } + split = true; + if(!point.equals(vert1)) { + points.push(point); + } + lines.push(new OpenLayers.Geometry.LineString(points)); + points = [point.clone()]; + } + } + } + if(split) { + points.push(vert2.clone()); + lines.push(new OpenLayers.Geometry.LineString(points)); + } + if(intersections.length > 0) { + // sort intersections along segment + var xDir = seg.x1 < seg.x2 ? 1 : -1; + var yDir = seg.y1 < seg.y2 ? 1 : -1; + result = { + lines: lines, + points: intersections.sort(function(p1, p2) { + return (xDir * p1.x - xDir * p2.x) || (yDir * p1.y - yDir * p2.y); + }) + }; + } + return result; + }, + + /** + * Method: split + * Use this geometry (the source) to attempt to split a target geometry. + * + * Parameters: + * target - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Properties of this object will be used to determine + * how the split is conducted. + * + * Valid options: + * mutual - {Boolean} Split the source geometry in addition to the target + * geometry. Default is false. + * edge - {Boolean} Allow splitting when only edges intersect. Default is + * true. If false, a vertex on the source must be within the tolerance + * distance of the intersection to be considered a split. + * tolerance - {Number} If a non-null value is provided, intersections + * within the tolerance distance of an existing vertex on the source + * will be assumed to occur at the vertex. + * + * Returns: + * {Array} A list of geometries (of this same type as the target) that + * result from splitting the target with the source geometry. The + * source and target geometry will remain unmodified. If no split + * results, null will be returned. If mutual is true and a split + * results, return will be an array of two arrays - the first will be + * all geometries that result from splitting the source geometry and + * the second will be all geometries that result from splitting the + * target geometry. + */ + split: function(target, options) { + var results = null; + var mutual = options && options.mutual; + var sourceSplit, targetSplit, sourceParts, targetParts; + if(target instanceof OpenLayers.Geometry.LineString) { + var verts = this.getVertices(); + var vert1, vert2, seg, splits, lines, point; + var points = []; + sourceParts = []; + for(var i=0, stop=verts.length-2; i<=stop; ++i) { + vert1 = verts[i]; + vert2 = verts[i+1]; + seg = { + x1: vert1.x, y1: vert1.y, + x2: vert2.x, y2: vert2.y + }; + targetParts = targetParts || [target]; + if(mutual) { + points.push(vert1.clone()); + } + for(var j=0; j<targetParts.length; ++j) { + splits = targetParts[j].splitWithSegment(seg, options); + if(splits) { + // splice in new features + lines = splits.lines; + if(lines.length > 0) { + lines.unshift(j, 1); + Array.prototype.splice.apply(targetParts, lines); + j += lines.length - 2; + } + if(mutual) { + for(var k=0, len=splits.points.length; k<len; ++k) { + point = splits.points[k]; + if(!point.equals(vert1)) { + points.push(point); + sourceParts.push(new OpenLayers.Geometry.LineString(points)); + if(point.equals(vert2)) { + points = []; + } else { + points = [point.clone()]; + } + } + } + } + } + } + } + if(mutual && sourceParts.length > 0 && points.length > 0) { + points.push(vert2.clone()); + sourceParts.push(new OpenLayers.Geometry.LineString(points)); + } + } else { + results = target.splitWith(this, options); + } + if(targetParts && targetParts.length > 1) { + targetSplit = true; + } else { + targetParts = []; + } + if(sourceParts && sourceParts.length > 1) { + sourceSplit = true; + } else { + sourceParts = []; + } + if(targetSplit || sourceSplit) { + if(mutual) { + results = [sourceParts, targetParts]; + } else { + results = targetParts; + } + } + return results; + }, + + /** + * Method: splitWith + * Split this geometry (the target) with the given geometry (the source). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} A geometry used to split this + * geometry (the source). + * options - {Object} Properties of this object will be used to determine + * how the split is conducted. + * + * Valid options: + * mutual - {Boolean} Split the source geometry in addition to the target + * geometry. Default is false. + * edge - {Boolean} Allow splitting when only edges intersect. Default is + * true. If false, a vertex on the source must be within the tolerance + * distance of the intersection to be considered a split. + * tolerance - {Number} If a non-null value is provided, intersections + * within the tolerance distance of an existing vertex on the source + * will be assumed to occur at the vertex. + * + * Returns: + * {Array} A list of geometries (of this same type as the target) that + * result from splitting the target with the source geometry. The + * source and target geometry will remain unmodified. If no split + * results, null will be returned. If mutual is true and a split + * results, return will be an array of two arrays - the first will be + * all geometries that result from splitting the source geometry and + * the second will be all geometries that result from splitting the + * target geometry. + */ + splitWith: function(geometry, options) { + return geometry.split(this, options); + + }, + + /** + * APIMethod: getVertices + * Return a list of all points in this geometry. + * + * Parameters: + * nodes - {Boolean} For lines, only return vertices that are + * endpoints. If false, for lines, only vertices that are not + * endpoints will be returned. If not provided, all vertices will + * be returned. + * + * Returns: + * {Array} A list of all vertices in the geometry. + */ + getVertices: function(nodes) { + var vertices; + if(nodes === true) { + vertices = [ + this.components[0], + this.components[this.components.length-1] + ]; + } else if (nodes === false) { + vertices = this.components.slice(1, this.components.length-1); + } else { + vertices = this.components.slice(); + } + return vertices; + }, + + /** + * APIMethod: distanceTo + * Calculate the closest distance between two geometries (on the x-y plane). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Optional properties for configuring the distance + * calculation. + * + * Valid options: + * details - {Boolean} Return details from the distance calculation. + * Default is false. + * edge - {Boolean} Calculate the distance from this geometry to the + * nearest edge of the target geometry. Default is true. If true, + * calling distanceTo from a geometry that is wholly contained within + * the target will result in a non-zero distance. If false, whenever + * geometries intersect, calling distanceTo will return 0. If false, + * details cannot be returned. + * + * Returns: + * {Number | Object} The distance between this geometry and the target. + * If details is true, the return will be an object with distance, + * x0, y0, x1, and x2 properties. The x0 and y0 properties represent + * the coordinates of the closest point on this geometry. The x1 and y1 + * properties represent the coordinates of the closest point on the + * target geometry. + */ + distanceTo: function(geometry, options) { + var edge = !(options && options.edge === false); + var details = edge && options && options.details; + var result, best = {}; + var min = Number.POSITIVE_INFINITY; + if(geometry instanceof OpenLayers.Geometry.Point) { + var segs = this.getSortedSegments(); + var x = geometry.x; + var y = geometry.y; + var seg; + for(var i=0, len=segs.length; i<len; ++i) { + seg = segs[i]; + result = OpenLayers.Geometry.distanceToSegment(geometry, seg); + if(result.distance < min) { + min = result.distance; + best = result; + if(min === 0) { + break; + } + } else { + // if distance increases and we cross y0 to the right of x0, no need to keep looking. + if(seg.x2 > x && ((y > seg.y1 && y < seg.y2) || (y < seg.y1 && y > seg.y2))) { + break; + } + } + } + if(details) { + best = { + distance: best.distance, + x0: best.x, y0: best.y, + x1: x, y1: y + }; + } else { + best = best.distance; + } + } else if(geometry instanceof OpenLayers.Geometry.LineString) { + var segs0 = this.getSortedSegments(); + var segs1 = geometry.getSortedSegments(); + var seg0, seg1, intersection, x0, y0; + var len1 = segs1.length; + var interOptions = {point: true}; + outer: for(var i=0, len=segs0.length; i<len; ++i) { + seg0 = segs0[i]; + x0 = seg0.x1; + y0 = seg0.y1; + for(var j=0; j<len1; ++j) { + seg1 = segs1[j]; + intersection = OpenLayers.Geometry.segmentsIntersect(seg0, seg1, interOptions); + if(intersection) { + min = 0; + best = { + distance: 0, + x0: intersection.x, y0: intersection.y, + x1: intersection.x, y1: intersection.y + }; + break outer; + } else { + result = OpenLayers.Geometry.distanceToSegment({x: x0, y: y0}, seg1); + if(result.distance < min) { + min = result.distance; + best = { + distance: min, + x0: x0, y0: y0, + x1: result.x, y1: result.y + }; + } + } + } + } + if(!details) { + best = best.distance; + } + if(min !== 0) { + // check the final vertex in this line's sorted segments + if(seg0) { + result = geometry.distanceTo( + new OpenLayers.Geometry.Point(seg0.x2, seg0.y2), + options + ); + var dist = details ? result.distance : result; + if(dist < min) { + if(details) { + best = { + distance: min, + x0: result.x1, y0: result.y1, + x1: result.x0, y1: result.y0 + }; + } else { + best = dist; + } + } + } + } + } else { + best = geometry.distanceTo(this, options); + // swap since target comes from this line + if(details) { + best = { + distance: best.distance, + x0: best.x1, y0: best.y1, + x1: best.x0, y1: best.y0 + }; + } + } + return best; + }, + + /** + * APIMethod: simplify + * This function will return a simplified LineString. + * Simplification is based on the Douglas-Peucker algorithm. + * + * + * Parameters: + * tolerance - {number} threshhold for simplification in map units + * + * Returns: + * {OpenLayers.Geometry.LineString} the simplified LineString + */ + simplify: function(tolerance){ + if (this && this !== null) { + var points = this.getVertices(); + if (points.length < 3) { + return this; + } + + var compareNumbers = function(a, b){ + return (a-b); + }; + + /** + * Private function doing the Douglas-Peucker reduction + */ + var douglasPeuckerReduction = function(points, firstPoint, lastPoint, tolerance){ + var maxDistance = 0; + var indexFarthest = 0; + + for (var index = firstPoint, distance; index < lastPoint; index++) { + distance = perpendicularDistance(points[firstPoint], points[lastPoint], points[index]); + if (distance > maxDistance) { + maxDistance = distance; + indexFarthest = index; + } + } + + if (maxDistance > tolerance && indexFarthest != firstPoint) { + //Add the largest point that exceeds the tolerance + pointIndexsToKeep.push(indexFarthest); + douglasPeuckerReduction(points, firstPoint, indexFarthest, tolerance); + douglasPeuckerReduction(points, indexFarthest, lastPoint, tolerance); + } + }; + + /** + * Private function calculating the perpendicular distance + * TODO: check whether OpenLayers.Geometry.LineString::distanceTo() is faster or slower + */ + var perpendicularDistance = function(point1, point2, point){ + //Area = |(1/2)(x1y2 + x2y3 + x3y1 - x2y1 - x3y2 - x1y3)| *Area of triangle + //Base = v((x1-x2)²+(x1-x2)²) *Base of Triangle* + //Area = .5*Base*H *Solve for height + //Height = Area/.5/Base + + var area = Math.abs(0.5 * (point1.x * point2.y + point2.x * point.y + point.x * point1.y - point2.x * point1.y - point.x * point2.y - point1.x * point.y)); + var bottom = Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)); + var height = area / bottom * 2; + + return height; + }; + + var firstPoint = 0; + var lastPoint = points.length - 1; + var pointIndexsToKeep = []; + + //Add the first and last index to the keepers + pointIndexsToKeep.push(firstPoint); + pointIndexsToKeep.push(lastPoint); + + //The first and the last point cannot be the same + while (points[firstPoint].equals(points[lastPoint])) { + lastPoint--; + //Addition: the first point not equal to first point in the LineString is kept as well + pointIndexsToKeep.push(lastPoint); + } + + douglasPeuckerReduction(points, firstPoint, lastPoint, tolerance); + var returnPoints = []; + pointIndexsToKeep.sort(compareNumbers); + for (var index = 0; index < pointIndexsToKeep.length; index++) { + returnPoints.push(points[pointIndexsToKeep[index]]); + } + return new OpenLayers.Geometry.LineString(returnPoints); + + } + else { + return this; + } + }, + + CLASS_NAME: "OpenLayers.Geometry.LineString" +}); +/* ====================================================================== + OpenLayers/Geometry/LinearRing.js + ====================================================================== */ + +/* 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/Geometry/LineString.js + */ + +/** + * Class: OpenLayers.Geometry.LinearRing + * + * A Linear Ring is a special LineString which is closed. It closes itself + * automatically on every addPoint/removePoint by adding a copy of the first + * point as the last point. + * + * Also, as it is the first in the line family to close itself, a getArea() + * function is defined to calculate the enclosed area of the linearRing + * + * Inherits: + * - <OpenLayers.Geometry.LineString> + */ +OpenLayers.Geometry.LinearRing = OpenLayers.Class( + OpenLayers.Geometry.LineString, { + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null + * value means the component types are not restricted. + */ + componentTypes: ["OpenLayers.Geometry.Point"], + + /** + * Constructor: OpenLayers.Geometry.LinearRing + * Linear rings are constructed with an array of points. This array + * can represent a closed or open ring. If the ring is open (the last + * point does not equal the first point), the constructor will close + * the ring. If the ring is already closed (the last point does equal + * the first point), it will be left closed. + * + * Parameters: + * points - {Array(<OpenLayers.Geometry.Point>)} points + */ + + /** + * APIMethod: addComponent + * Adds a point to geometry components. If the point is to be added to + * the end of the components array and it is the same as the last point + * already in that array, the duplicate point is not added. This has + * the effect of closing the ring if it is not already closed, and + * doing the right thing if it is already closed. This behavior can + * be overridden by calling the method with a non-null index as the + * second argument. + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * index - {Integer} Index into the array to insert the component + * + * Returns: + * {Boolean} Was the Point successfully added? + */ + addComponent: function(point, index) { + var added = false; + + //remove last point + var lastPoint = this.components.pop(); + + // given an index, add the point + // without an index only add non-duplicate points + if(index != null || !point.equals(lastPoint)) { + added = OpenLayers.Geometry.Collection.prototype.addComponent.apply(this, + arguments); + } + + //append copy of first point + var firstPoint = this.components[0]; + OpenLayers.Geometry.Collection.prototype.addComponent.apply(this, + [firstPoint]); + + return added; + }, + + /** + * APIMethod: removeComponent + * Removes a point from geometry components. + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns: + * {Boolean} The component was removed. + */ + removeComponent: function(point) { + var removed = this.components && (this.components.length > 3); + if (removed) { + //remove last point + this.components.pop(); + + //remove our point + OpenLayers.Geometry.Collection.prototype.removeComponent.apply(this, + arguments); + //append copy of first point + var firstPoint = this.components[0]; + OpenLayers.Geometry.Collection.prototype.addComponent.apply(this, + [firstPoint]); + } + return removed; + }, + + /** + * APIMethod: move + * Moves a geometry by the given displacement along positive x and y axes. + * This modifies the position of the geometry and clears the cached + * bounds. + * + * Parameters: + * x - {Float} Distance to move geometry in positive x direction. + * y - {Float} Distance to move geometry in positive y direction. + */ + move: function(x, y) { + for(var i = 0, len=this.components.length; i<len - 1; i++) { + this.components[i].move(x, y); + } + }, + + /** + * APIMethod: rotate + * Rotate a geometry around some origin + * + * Parameters: + * angle - {Float} Rotation angle in degrees (measured counterclockwise + * from the positive x-axis) + * origin - {<OpenLayers.Geometry.Point>} Center point for the rotation + */ + rotate: function(angle, origin) { + for(var i=0, len=this.components.length; i<len - 1; ++i) { + this.components[i].rotate(angle, origin); + } + }, + + /** + * APIMethod: resize + * Resize a geometry relative to some origin. Use this method to apply + * a uniform scaling to a geometry. + * + * Parameters: + * scale - {Float} Factor by which to scale the geometry. A scale of 2 + * doubles the size of the geometry in each dimension + * (lines, for example, will be twice as long, and polygons + * will have four times the area). + * origin - {<OpenLayers.Geometry.Point>} Point of origin for resizing + * ratio - {Float} Optional x:y ratio for resizing. Default ratio is 1. + * + * Returns: + * {<OpenLayers.Geometry>} - The current geometry. + */ + resize: function(scale, origin, ratio) { + for(var i=0, len=this.components.length; i<len - 1; ++i) { + this.components[i].resize(scale, origin, ratio); + } + return this; + }, + + /** + * APIMethod: transform + * Reproject the components geometry from source to dest. + * + * Parameters: + * source - {<OpenLayers.Projection>} + * dest - {<OpenLayers.Projection>} + * + * Returns: + * {<OpenLayers.Geometry>} + */ + transform: function(source, dest) { + if (source && dest) { + for (var i=0, len=this.components.length; i<len - 1; i++) { + var component = this.components[i]; + component.transform(source, dest); + } + this.bounds = null; + } + return this; + }, + + /** + * APIMethod: getCentroid + * + * Returns: + * {<OpenLayers.Geometry.Point>} The centroid of the collection + */ + getCentroid: function() { + if (this.components) { + var len = this.components.length; + if (len > 0 && len <= 2) { + return this.components[0].clone(); + } else if (len > 2) { + var sumX = 0.0; + var sumY = 0.0; + var x0 = this.components[0].x; + var y0 = this.components[0].y; + var area = -1 * this.getArea(); + if (area != 0) { + for (var i = 0; i < len - 1; i++) { + var b = this.components[i]; + var c = this.components[i+1]; + sumX += (b.x + c.x - 2 * x0) * ((b.x - x0) * (c.y - y0) - (c.x - x0) * (b.y - y0)); + sumY += (b.y + c.y - 2 * y0) * ((b.x - x0) * (c.y - y0) - (c.x - x0) * (b.y - y0)); + } + var x = x0 + sumX / (6 * area); + var y = y0 + sumY / (6 * area); + } else { + for (var i = 0; i < len - 1; i++) { + sumX += this.components[i].x; + sumY += this.components[i].y; + } + var x = sumX / (len - 1); + var y = sumY / (len - 1); + } + return new OpenLayers.Geometry.Point(x, y); + } else { + return null; + } + } + }, + + /** + * APIMethod: getArea + * Note - The area is positive if the ring is oriented CW, otherwise + * it will be negative. + * + * Returns: + * {Float} The signed area for a ring. + */ + getArea: function() { + var area = 0.0; + if ( this.components && (this.components.length > 2)) { + var sum = 0.0; + for (var i=0, len=this.components.length; i<len - 1; i++) { + var b = this.components[i]; + var c = this.components[i+1]; + sum += (b.x + c.x) * (c.y - b.y); + } + area = - sum / 2.0; + } + return area; + }, + + /** + * APIMethod: getGeodesicArea + * Calculate the approximate area of the polygon were it projected onto + * the earth. Note that this area will be positive if ring is oriented + * clockwise, otherwise it will be negative. + * + * Parameters: + * projection - {<OpenLayers.Projection>} The spatial reference system + * for the geometry coordinates. If not provided, Geographic/WGS84 is + * assumed. + * + * Reference: + * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for + * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion + * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409 + * + * Returns: + * {float} The approximate signed geodesic area of the polygon in square + * meters. + */ + getGeodesicArea: function(projection) { + var ring = this; // so we can work with a clone if needed + if(projection) { + var gg = new OpenLayers.Projection("EPSG:4326"); + if(!gg.equals(projection)) { + ring = this.clone().transform(projection, gg); + } + } + var area = 0.0; + var len = ring.components && ring.components.length; + if(len > 2) { + var p1, p2; + for(var i=0; i<len-1; i++) { + p1 = ring.components[i]; + p2 = ring.components[i+1]; + area += OpenLayers.Util.rad(p2.x - p1.x) * + (2 + Math.sin(OpenLayers.Util.rad(p1.y)) + + Math.sin(OpenLayers.Util.rad(p2.y))); + } + area = area * 6378137.0 * 6378137.0 / 2.0; + } + return area; + }, + + /** + * Method: containsPoint + * Test if a point is inside a linear ring. For the case where a point + * is coincident with a linear ring edge, returns 1. Otherwise, + * returns boolean. + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns: + * {Boolean | Number} The point is inside the linear ring. Returns 1 if + * the point is coincident with an edge. Returns boolean otherwise. + */ + containsPoint: function(point) { + var approx = OpenLayers.Number.limitSigDigs; + var digs = 14; + var px = approx(point.x, digs); + var py = approx(point.y, digs); + function getX(y, x1, y1, x2, y2) { + return (y - y2) * ((x2 - x1) / (y2 - y1)) + x2; + } + var numSeg = this.components.length - 1; + var start, end, x1, y1, x2, y2, cx, cy; + var crosses = 0; + for(var i=0; i<numSeg; ++i) { + start = this.components[i]; + x1 = approx(start.x, digs); + y1 = approx(start.y, digs); + end = this.components[i + 1]; + x2 = approx(end.x, digs); + y2 = approx(end.y, digs); + + /** + * The following conditions enforce five edge-crossing rules: + * 1. points coincident with edges are considered contained; + * 2. an upward edge includes its starting endpoint, and + * excludes its final endpoint; + * 3. a downward edge excludes its starting endpoint, and + * includes its final endpoint; + * 4. horizontal edges are excluded; and + * 5. the edge-ray intersection point must be strictly right + * of the point P. + */ + if(y1 == y2) { + // horizontal edge + if(py == y1) { + // point on horizontal line + if(x1 <= x2 && (px >= x1 && px <= x2) || // right or vert + x1 >= x2 && (px <= x1 && px >= x2)) { // left or vert + // point on edge + crosses = -1; + break; + } + } + // ignore other horizontal edges + continue; + } + cx = approx(getX(py, x1, y1, x2, y2), digs); + if(cx == px) { + // point on line + if(y1 < y2 && (py >= y1 && py <= y2) || // upward + y1 > y2 && (py <= y1 && py >= y2)) { // downward + // point on edge + crosses = -1; + break; + } + } + if(cx <= px) { + // no crossing to the right + continue; + } + if(x1 != x2 && (cx < Math.min(x1, x2) || cx > Math.max(x1, x2))) { + // no crossing + continue; + } + if(y1 < y2 && (py >= y1 && py < y2) || // upward + y1 > y2 && (py < y1 && py >= y2)) { // downward + ++crosses; + } + } + var contained = (crosses == -1) ? + // on edge + 1 : + // even (out) or odd (in) + !!(crosses & 1); + + return contained; + }, + + /** + * APIMethod: intersects + * Determine if the input geometry intersects this one. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} Any type of geometry. + * + * Returns: + * {Boolean} The input geometry intersects this one. + */ + intersects: function(geometry) { + var intersect = false; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + intersect = this.containsPoint(geometry); + } else if(geometry.CLASS_NAME == "OpenLayers.Geometry.LineString") { + intersect = geometry.intersects(this); + } else if(geometry.CLASS_NAME == "OpenLayers.Geometry.LinearRing") { + intersect = OpenLayers.Geometry.LineString.prototype.intersects.apply( + this, [geometry] + ); + } else { + // check for component intersections + for(var i=0, len=geometry.components.length; i<len; ++ i) { + intersect = geometry.components[i].intersects(this); + if(intersect) { + break; + } + } + } + return intersect; + }, + + /** + * APIMethod: getVertices + * Return a list of all points in this geometry. + * + * Parameters: + * nodes - {Boolean} For lines, only return vertices that are + * endpoints. If false, for lines, only vertices that are not + * endpoints will be returned. If not provided, all vertices will + * be returned. + * + * Returns: + * {Array} A list of all vertices in the geometry. + */ + getVertices: function(nodes) { + return (nodes === true) ? [] : this.components.slice(0, this.components.length-1); + }, + + CLASS_NAME: "OpenLayers.Geometry.LinearRing" +}); +/* ====================================================================== + OpenLayers/Util/vendorPrefix.js + ====================================================================== */ + +/* 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/SingleFile.js + */ + +OpenLayers.Util = OpenLayers.Util || {}; +/** + * Namespace: OpenLayers.Util.vendorPrefix + * A collection of utility functions to detect vendor prefixed features + */ +OpenLayers.Util.vendorPrefix = (function() { + "use strict"; + + var VENDOR_PREFIXES = ["", "O", "ms", "Moz", "Webkit"], + divStyle = document.createElement("div").style, + cssCache = {}, + jsCache = {}; + + + /** + * Function: domToCss + * Converts a upper camel case DOM style property name to a CSS property + * i.e. transformOrigin -> transform-origin + * or WebkitTransformOrigin -> -webkit-transform-origin + * + * Parameters: + * prefixedDom - {String} The property to convert + * + * Returns: + * {String} The CSS property + */ + function domToCss(prefixedDom) { + if (!prefixedDom) { return null; } + return prefixedDom. + replace(/([A-Z])/g, function(c) { return "-" + c.toLowerCase(); }). + replace(/^ms-/, "-ms-"); + } + + /** + * APIMethod: css + * Detect which property is used for a CSS property + * + * Parameters: + * property - {String} The standard (unprefixed) CSS property name + * + * Returns: + * {String} The standard CSS property, prefixed property or null if not + * supported + */ + function css(property) { + if (cssCache[property] === undefined) { + var domProperty = property. + replace(/(-[\s\S])/g, function(c) { return c.charAt(1).toUpperCase(); }); + var prefixedDom = style(domProperty); + cssCache[property] = domToCss(prefixedDom); + } + return cssCache[property]; + } + + /** + * APIMethod: js + * Detect which property is used for a JS property/method + * + * Parameters: + * obj - {Object} The object to test on + * property - {String} The standard (unprefixed) JS property name + * + * Returns: + * {String} The standard JS property, prefixed property or null if not + * supported + */ + function js(obj, property) { + if (jsCache[property] === undefined) { + var tmpProp, + i = 0, + l = VENDOR_PREFIXES.length, + prefix, + isStyleObj = (typeof obj.cssText !== "undefined"); + + jsCache[property] = null; + for(; i<l; i++) { + prefix = VENDOR_PREFIXES[i]; + if(prefix) { + if (!isStyleObj) { + // js prefix should be lower-case, while style + // properties have upper case on first character + prefix = prefix.toLowerCase(); + } + tmpProp = prefix + property.charAt(0).toUpperCase() + property.slice(1); + } else { + tmpProp = property; + } + + if(obj[tmpProp] !== undefined) { + jsCache[property] = tmpProp; + break; + } + } + } + return jsCache[property]; + } + + /** + * APIMethod: style + * Detect which property is used for a DOM style property + * + * Parameters: + * property - {String} The standard (unprefixed) style property name + * + * Returns: + * {String} The standard style property, prefixed property or null if not + * supported + */ + function style(property) { + return js(divStyle, property); + } + + return { + css: css, + js: js, + style: style, + + // used for testing + cssCache: cssCache, + jsCache: jsCache + }; +}()); +/* ====================================================================== + OpenLayers/Animation.js + ====================================================================== */ + +/* 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/SingleFile.js + * @requires OpenLayers/Util/vendorPrefix.js + */ + +/** + * Namespace: OpenLayers.Animation + * A collection of utility functions for executing methods that repaint a + * portion of the browser window. These methods take advantage of the + * browser's scheduled repaints where requestAnimationFrame is available. + */ +OpenLayers.Animation = (function(window) { + + /** + * Property: isNative + * {Boolean} true if a native requestAnimationFrame function is available + */ + var requestAnimationFrame = OpenLayers.Util.vendorPrefix.js(window, "requestAnimationFrame"); + var isNative = !!(requestAnimationFrame); + + /** + * Function: requestFrame + * Schedule a function to be called at the next available animation frame. + * Uses the native method where available. Where requestAnimationFrame is + * not available, setTimeout will be called with a 16ms delay. + * + * Parameters: + * callback - {Function} The function to be called at the next animation frame. + * element - {DOMElement} Optional element that visually bounds the animation. + */ + var requestFrame = (function() { + var request = window[requestAnimationFrame] || + function(callback, element) { + window.setTimeout(callback, 16); + }; + // bind to window to avoid illegal invocation of native function + return function(callback, element) { + request.apply(window, [callback, element]); + }; + })(); + + // private variables for animation loops + var counter = 0; + var loops = {}; + + /** + * Function: start + * Executes a method with <requestFrame> in series for some + * duration. + * + * Parameters: + * callback - {Function} The function to be called at the next animation frame. + * duration - {Number} Optional duration for the loop. If not provided, the + * animation loop will execute indefinitely. + * element - {DOMElement} Optional element that visually bounds the animation. + * + * Returns: + * {Number} Identifier for the animation loop. Used to stop animations with + * <stop>. + */ + function start(callback, duration, element) { + duration = duration > 0 ? duration : Number.POSITIVE_INFINITY; + var id = ++counter; + var start = +new Date; + loops[id] = function() { + if (loops[id] && +new Date - start <= duration) { + callback(); + if (loops[id]) { + requestFrame(loops[id], element); + } + } else { + delete loops[id]; + } + }; + requestFrame(loops[id], element); + return id; + } + + /** + * Function: stop + * Terminates an animation loop started with <start>. + * + * Parameters: + * id - {Number} Identifier returned from <start>. + */ + function stop(id) { + delete loops[id]; + } + + return { + isNative: isNative, + requestFrame: requestFrame, + start: start, + stop: stop + }; + +})(window); +/* ====================================================================== + OpenLayers/Tween.js + ====================================================================== */ + +/* 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/Animation.js + */ + +/** + * Namespace: OpenLayers.Tween + */ +OpenLayers.Tween = OpenLayers.Class({ + + /** + * APIProperty: easing + * {<OpenLayers.Easing>(Function)} Easing equation used for the animation + * Defaultly set to OpenLayers.Easing.Expo.easeOut + */ + easing: null, + + /** + * APIProperty: begin + * {Object} Values to start the animation with + */ + begin: null, + + /** + * APIProperty: finish + * {Object} Values to finish the animation with + */ + finish: null, + + /** + * APIProperty: duration + * {int} duration of the tween (number of steps) + */ + duration: null, + + /** + * APIProperty: callbacks + * {Object} An object with start, eachStep and done properties whose values + * are functions to be call during the animation. They are passed the + * current computed value as argument. + */ + callbacks: null, + + /** + * Property: time + * {int} Step counter + */ + time: null, + + /** + * APIProperty: minFrameRate + * {Number} The minimum framerate for animations in frames per second. After + * each step, the time spent in the animation is compared to the calculated + * time at this frame rate. If the animation runs longer than the calculated + * time, the next step is skipped. Default is 30. + */ + minFrameRate: null, + + /** + * Property: startTime + * {Number} The timestamp of the first execution step. Used for skipping + * frames + */ + startTime: null, + + /** + * Property: animationId + * {int} Loop id returned by OpenLayers.Animation.start + */ + animationId: null, + + /** + * Property: playing + * {Boolean} Tells if the easing is currently playing + */ + playing: false, + + /** + * Constructor: OpenLayers.Tween + * Creates a Tween. + * + * Parameters: + * easing - {<OpenLayers.Easing>(Function)} easing function method to use + */ + initialize: function(easing) { + this.easing = (easing) ? easing : OpenLayers.Easing.Expo.easeOut; + }, + + /** + * APIMethod: start + * Plays the Tween, and calls the callback method on each step + * + * Parameters: + * begin - {Object} values to start the animation with + * finish - {Object} values to finish the animation with + * duration - {int} duration of the tween (number of steps) + * options - {Object} hash of options (callbacks (start, eachStep, done), + * minFrameRate) + */ + start: function(begin, finish, duration, options) { + this.playing = true; + this.begin = begin; + this.finish = finish; + this.duration = duration; + this.callbacks = options.callbacks; + this.minFrameRate = options.minFrameRate || 30; + this.time = 0; + this.startTime = new Date().getTime(); + OpenLayers.Animation.stop(this.animationId); + this.animationId = null; + if (this.callbacks && this.callbacks.start) { + this.callbacks.start.call(this, this.begin); + } + this.animationId = OpenLayers.Animation.start( + OpenLayers.Function.bind(this.play, this) + ); + }, + + /** + * APIMethod: stop + * Stops the Tween, and calls the done callback + * Doesn't do anything if animation is already finished + */ + stop: function() { + if (!this.playing) { + return; + } + + if (this.callbacks && this.callbacks.done) { + this.callbacks.done.call(this, this.finish); + } + OpenLayers.Animation.stop(this.animationId); + this.animationId = null; + this.playing = false; + }, + + /** + * Method: play + * Calls the appropriate easing method + */ + play: function() { + var value = {}; + for (var i in this.begin) { + var b = this.begin[i]; + var f = this.finish[i]; + if (b == null || f == null || isNaN(b) || isNaN(f)) { + throw new TypeError('invalid value for Tween'); + } + + var c = f - b; + value[i] = this.easing.apply(this, [this.time, b, c, this.duration]); + } + this.time++; + + if (this.callbacks && this.callbacks.eachStep) { + // skip frames if frame rate drops below threshold + if ((new Date().getTime() - this.startTime) / this.time <= 1000 / this.minFrameRate) { + this.callbacks.eachStep.call(this, value); + } + } + + if (this.time > this.duration) { + this.stop(); + } + }, + + /** + * Create empty functions for all easing methods. + */ + CLASS_NAME: "OpenLayers.Tween" +}); + +/** + * Namespace: OpenLayers.Easing + * + * Credits: + * Easing Equations by Robert Penner, <http://www.robertpenner.com/easing/> + */ +OpenLayers.Easing = { + /** + * Create empty functions for all easing methods. + */ + CLASS_NAME: "OpenLayers.Easing" +}; + +/** + * Namespace: OpenLayers.Easing.Linear + */ +OpenLayers.Easing.Linear = { + + /** + * Function: easeIn + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeIn: function(t, b, c, d) { + return c*t/d + b; + }, + + /** + * Function: easeOut + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeOut: function(t, b, c, d) { + return c*t/d + b; + }, + + /** + * Function: easeInOut + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeInOut: function(t, b, c, d) { + return c*t/d + b; + }, + + CLASS_NAME: "OpenLayers.Easing.Linear" +}; + +/** + * Namespace: OpenLayers.Easing.Expo + */ +OpenLayers.Easing.Expo = { + + /** + * Function: easeIn + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeIn: function(t, b, c, d) { + return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; + }, + + /** + * Function: easeOut + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeOut: function(t, b, c, d) { + return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + }, + + /** + * Function: easeInOut + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeInOut: function(t, b, c, d) { + if (t==0) return b; + if (t==d) return b+c; + if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; + return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; + }, + + CLASS_NAME: "OpenLayers.Easing.Expo" +}; + +/** + * Namespace: OpenLayers.Easing.Quad + */ +OpenLayers.Easing.Quad = { + + /** + * Function: easeIn + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeIn: function(t, b, c, d) { + return c*(t/=d)*t + b; + }, + + /** + * Function: easeOut + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeOut: function(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + }, + + /** + * Function: easeInOut + * + * Parameters: + * t - {Float} time + * b - {Float} beginning position + * c - {Float} total change + * d - {Float} duration of the transition + * + * Returns: + * {Float} + */ + easeInOut: function(t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + }, + + CLASS_NAME: "OpenLayers.Easing.Quad" +}; +/* ====================================================================== + OpenLayers/Projection.js + ====================================================================== */ + +/* 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 + */ + +/** + * Namespace: OpenLayers.Projection + * Methods for coordinate transforms between coordinate systems. By default, + * OpenLayers ships with the ability to transform coordinates between + * geographic (EPSG:4326) and web or spherical mercator (EPSG:900913 et al.) + * coordinate reference systems. See the <transform> method for details + * on usage. + * + * Additional transforms may be added by using the <proj4js at http://proj4js.org/> + * library. If the proj4js library is included, the <transform> method + * will work between any two coordinate reference systems with proj4js + * definitions. + * + * If the proj4js library is not included, or if you wish to allow transforms + * between arbitrary coordinate reference systems, use the <addTransform> + * method to register a custom transform method. + */ +OpenLayers.Projection = OpenLayers.Class({ + + /** + * Property: proj + * {Object} Proj4js.Proj instance. + */ + proj: null, + + /** + * Property: projCode + * {String} + */ + projCode: null, + + /** + * Property: titleRegEx + * {RegExp} regular expression to strip the title from a proj4js definition + */ + titleRegEx: /\+title=[^\+]*/, + + /** + * Constructor: OpenLayers.Projection + * This class offers several methods for interacting with a wrapped + * pro4js projection object. + * + * Parameters: + * projCode - {String} A string identifying the Well Known Identifier for + * the projection. + * options - {Object} An optional object to set additional properties + * on the projection. + * + * Returns: + * {<OpenLayers.Projection>} A projection object. + */ + initialize: function(projCode, options) { + OpenLayers.Util.extend(this, options); + this.projCode = projCode; + if (typeof Proj4js == "object") { + this.proj = new Proj4js.Proj(projCode); + } + }, + + /** + * APIMethod: getCode + * Get the string SRS code. + * + * Returns: + * {String} The SRS code. + */ + getCode: function() { + return this.proj ? this.proj.srsCode : this.projCode; + }, + + /** + * APIMethod: getUnits + * Get the units string for the projection -- returns null if + * proj4js is not available. + * + * Returns: + * {String} The units abbreviation. + */ + getUnits: function() { + return this.proj ? this.proj.units : null; + }, + + /** + * Method: toString + * Convert projection to string (getCode wrapper). + * + * Returns: + * {String} The projection code. + */ + toString: function() { + return this.getCode(); + }, + + /** + * Method: equals + * Test equality of two projection instances. Determines equality based + * soley on the projection code. + * + * Returns: + * {Boolean} The two projections are equivalent. + */ + equals: function(projection) { + var p = projection, equals = false; + if (p) { + if (!(p instanceof OpenLayers.Projection)) { + p = new OpenLayers.Projection(p); + } + if ((typeof Proj4js == "object") && this.proj.defData && p.proj.defData) { + equals = this.proj.defData.replace(this.titleRegEx, "") == + p.proj.defData.replace(this.titleRegEx, ""); + } else if (p.getCode) { + var source = this.getCode(), target = p.getCode(); + equals = source == target || + !!OpenLayers.Projection.transforms[source] && + OpenLayers.Projection.transforms[source][target] === + OpenLayers.Projection.nullTransform; + } + } + return equals; + }, + + /* Method: destroy + * Destroy projection object. + */ + destroy: function() { + delete this.proj; + delete this.projCode; + }, + + CLASS_NAME: "OpenLayers.Projection" +}); + +/** + * Property: transforms + * {Object} Transforms is an object, with from properties, each of which may + * have a to property. This allows you to define projections without + * requiring support for proj4js to be included. + * + * This object has keys which correspond to a 'source' projection object. The + * keys should be strings, corresponding to the projection.getCode() value. + * Each source projection object should have a set of destination projection + * keys included in the object. + * + * Each value in the destination object should be a transformation function, + * where the function is expected to be passed an object with a .x and a .y + * property. The function should return the object, with the .x and .y + * transformed according to the transformation function. + * + * Note - Properties on this object should not be set directly. To add a + * transform method to this object, use the <addTransform> method. For an + * example of usage, see the OpenLayers.Layer.SphericalMercator file. + */ +OpenLayers.Projection.transforms = {}; + +/** + * APIProperty: defaults + * {Object} Defaults for the SRS codes known to OpenLayers (currently + * EPSG:4326, CRS:84, urn:ogc:def:crs:EPSG:6.6:4326, EPSG:900913, EPSG:3857, + * EPSG:102113 and EPSG:102100). Keys are the SRS code, values are units, + * maxExtent (the validity extent for the SRS) and yx (true if this SRS is + * known to have a reverse axis order). + */ +OpenLayers.Projection.defaults = { + "EPSG:4326": { + units: "degrees", + maxExtent: [-180, -90, 180, 90], + yx: true + }, + "CRS:84": { + units: "degrees", + maxExtent: [-180, -90, 180, 90] + }, + "EPSG:900913": { + units: "m", + maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34] + } +}; + +/** + * APIMethod: addTransform + * Set a custom transform method between two projections. Use this method in + * cases where the proj4js lib is not available or where custom projections + * need to be handled. + * + * Parameters: + * from - {String} The code for the source projection + * to - {String} the code for the destination projection + * method - {Function} A function that takes a point as an argument and + * transforms that point from the source to the destination projection + * in place. The original point should be modified. + */ +OpenLayers.Projection.addTransform = function(from, to, method) { + if (method === OpenLayers.Projection.nullTransform) { + var defaults = OpenLayers.Projection.defaults[from]; + if (defaults && !OpenLayers.Projection.defaults[to]) { + OpenLayers.Projection.defaults[to] = defaults; + } + } + if(!OpenLayers.Projection.transforms[from]) { + OpenLayers.Projection.transforms[from] = {}; + } + OpenLayers.Projection.transforms[from][to] = method; +}; + +/** + * APIMethod: transform + * Transform a point coordinate from one projection to another. Note that + * the input point is transformed in place. + * + * Parameters: + * point - {<OpenLayers.Geometry.Point> | Object} An object with x and y + * properties representing coordinates in those dimensions. + * source - {OpenLayers.Projection} Source map coordinate system + * dest - {OpenLayers.Projection} Destination map coordinate system + * + * Returns: + * point - {object} A transformed coordinate. The original point is modified. + */ +OpenLayers.Projection.transform = function(point, source, dest) { + if (source && dest) { + if (!(source instanceof OpenLayers.Projection)) { + source = new OpenLayers.Projection(source); + } + if (!(dest instanceof OpenLayers.Projection)) { + dest = new OpenLayers.Projection(dest); + } + if (source.proj && dest.proj) { + point = Proj4js.transform(source.proj, dest.proj, point); + } else { + var sourceCode = source.getCode(); + var destCode = dest.getCode(); + var transforms = OpenLayers.Projection.transforms; + if (transforms[sourceCode] && transforms[sourceCode][destCode]) { + transforms[sourceCode][destCode](point); + } + } + } + return point; +}; + +/** + * APIFunction: nullTransform + * A null transformation - useful for defining projection aliases when + * proj4js is not available: + * + * (code) + * OpenLayers.Projection.addTransform("EPSG:3857", "EPSG:900913", + * OpenLayers.Projection.nullTransform); + * OpenLayers.Projection.addTransform("EPSG:900913", "EPSG:3857", + * OpenLayers.Projection.nullTransform); + * (end) + */ +OpenLayers.Projection.nullTransform = function(point) { + return point; +}; + +/** + * Note: Transforms for web mercator <-> geographic + * OpenLayers recognizes EPSG:3857, EPSG:900913, EPSG:102113 and EPSG:102100. + * OpenLayers originally started referring to EPSG:900913 as web mercator. + * The EPSG has declared EPSG:3857 to be web mercator. + * ArcGIS 10 recognizes the EPSG:3857, EPSG:102113, and EPSG:102100 as + * equivalent. See http://blogs.esri.com/Dev/blogs/arcgisserver/archive/2009/11/20/ArcGIS-Online-moving-to-Google-_2F00_-Bing-tiling-scheme_3A00_-What-does-this-mean-for-you_3F00_.aspx#12084. + * For geographic, OpenLayers recognizes EPSG:4326, CRS:84 and + * urn:ogc:def:crs:EPSG:6.6:4326. OpenLayers also knows about the reverse axis + * order for EPSG:4326. + */ +(function() { + + var pole = 20037508.34; + + function inverseMercator(xy) { + xy.x = 180 * xy.x / pole; + xy.y = 180 / Math.PI * (2 * Math.atan(Math.exp((xy.y / pole) * Math.PI)) - Math.PI / 2); + return xy; + } + + function forwardMercator(xy) { + xy.x = xy.x * pole / 180; + var y = Math.log(Math.tan((90 + xy.y) * Math.PI / 360)) / Math.PI * pole; + xy.y = Math.max(-20037508.34, Math.min(y, 20037508.34)); + return xy; + } + + function map(base, codes) { + var add = OpenLayers.Projection.addTransform; + var same = OpenLayers.Projection.nullTransform; + var i, len, code, other, j; + for (i=0, len=codes.length; i<len; ++i) { + code = codes[i]; + add(base, code, forwardMercator); + add(code, base, inverseMercator); + for (j=i+1; j<len; ++j) { + other = codes[j]; + add(code, other, same); + add(other, code, same); + } + } + } + + // list of equivalent codes for web mercator + var mercator = ["EPSG:900913", "EPSG:3857", "EPSG:102113", "EPSG:102100"], + geographic = ["CRS:84", "urn:ogc:def:crs:EPSG:6.6:4326", "EPSG:4326"], + i; + for (i=mercator.length-1; i>=0; --i) { + map(mercator[i], geographic); + } + for (i=geographic.length-1; i>=0; --i) { + map(geographic[i], mercator); + } + +})(); +/* ====================================================================== + OpenLayers/Map.js + ====================================================================== */ + +/* 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; +/* ====================================================================== + OpenLayers/Layer.js + ====================================================================== */ + +/* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. */ + + +/** + * @requires OpenLayers/BaseTypes/Class.js + * @requires OpenLayers/Map.js + * @requires OpenLayers/Projection.js + */ + +/** + * Class: OpenLayers.Layer + */ +OpenLayers.Layer = OpenLayers.Class({ + + /** + * APIProperty: id + * {String} + */ + id: null, + + /** + * APIProperty: name + * {String} + */ + name: null, + + /** + * APIProperty: div + * {DOMElement} + */ + div: null, + + /** + * APIProperty: opacity + * {Float} The layer's opacity. Float number between 0.0 and 1.0. Default + * is 1. + */ + opacity: 1, + + /** + * APIProperty: alwaysInRange + * {Boolean} If a layer's display should not be scale-based, this should + * be set to true. This will cause the layer, as an overlay, to always + * be 'active', by always returning true from the calculateInRange() + * function. + * + * If not explicitly specified for a layer, its value will be + * determined on startup in initResolutions() based on whether or not + * any scale-specific properties have been set as options on the + * layer. If no scale-specific options have been set on the layer, we + * assume that it should always be in range. + * + * See #987 for more info. + */ + alwaysInRange: null, + + /** + * Constant: RESOLUTION_PROPERTIES + * {Array} The properties that are used for calculating resolutions + * information. + */ + RESOLUTION_PROPERTIES: [ + 'scales', 'resolutions', + 'maxScale', 'minScale', + 'maxResolution', 'minResolution', + 'numZoomLevels', 'maxZoomLevel' + ], + + /** + * APIProperty: events + * {<OpenLayers.Events>} + * + * Register a listener for a particular event with the following syntax: + * (code) + * layer.events.register(type, obj, listener); + * (end) + * + * Listeners will be called with a reference to an event object. The + * properties of this event depends on exactly what happened. + * + * All event objects have at least the following properties: + * object - {Object} A reference to layer.events.object. + * element - {DOMElement} A reference to layer.events.element. + * + * Supported map event types: + * loadstart - Triggered when layer loading starts. When using a Vector + * layer with a Fixed or BBOX strategy, the event object includes + * a *filter* property holding the OpenLayers.Filter used when + * calling read on the protocol. + * loadend - Triggered when layer loading ends. When using a Vector layer + * with a Fixed or BBOX strategy, the event object includes a + * *response* property holding an OpenLayers.Protocol.Response object. + * visibilitychanged - Triggered when the layer's visibility property is + * changed, e.g. by turning the layer on or off in the layer switcher. + * Note that the actual visibility of the layer can also change if it + * gets out of range (see <calculateInRange>). If you also want to catch + * these cases, register for the map's 'changelayer' event instead. + * move - Triggered when layer moves (triggered with every mousemove + * during a drag). + * moveend - Triggered when layer is done moving, object passed as + * argument has a zoomChanged boolean property which tells that the + * zoom has changed. + * added - Triggered after the layer is added to a map. Listeners will + * receive an object with a *map* property referencing the map and a + * *layer* property referencing the layer. + * removed - Triggered after the layer is removed from the map. Listeners + * will receive an object with a *map* property referencing the map and + * a *layer* property referencing the layer. + */ + events: null, + + /** + * APIProperty: map + * {<OpenLayers.Map>} This variable is set when the layer is added to + * the map, via the accessor function setMap(). + */ + map: null, + + /** + * APIProperty: isBaseLayer + * {Boolean} Whether or not the layer is a base layer. This should be set + * individually by all subclasses. Default is false + */ + isBaseLayer: false, + + /** + * Property: alpha + * {Boolean} The layer's images have an alpha channel. Default is false. + */ + alpha: false, + + /** + * APIProperty: displayInLayerSwitcher + * {Boolean} Display the layer's name in the layer switcher. Default is + * true. + */ + displayInLayerSwitcher: true, + + /** + * APIProperty: visibility + * {Boolean} The layer should be displayed in the map. Default is true. + */ + visibility: true, + + /** + * APIProperty: attribution + * {String} Attribution string, displayed when an + * <OpenLayers.Control.Attribution> has been added to the map. + */ + attribution: null, + + /** + * Property: inRange + * {Boolean} The current map resolution is within the layer's min/max + * range. This is set in <OpenLayers.Map.setCenter> whenever the zoom + * changes. + */ + inRange: false, + + /** + * Propery: imageSize + * {<OpenLayers.Size>} For layers with a gutter, the image is larger than + * the tile by twice the gutter in each dimension. + */ + imageSize: null, + + // OPTIONS + + /** + * Property: options + * {Object} An optional object whose properties will be set on the layer. + * Any of the layer properties can be set as a property of the options + * object and sent to the constructor when the layer is created. + */ + options: null, + + /** + * APIProperty: eventListeners + * {Object} If set as an option at construction, the eventListeners + * object will be registered with <OpenLayers.Events.on>. Object + * structure must be a listeners object as shown in the example for + * the events.on method. + */ + eventListeners: null, + + /** + * APIProperty: gutter + * {Integer} Determines the width (in pixels) of the gutter around image + * tiles to ignore. By setting this property to a non-zero value, + * images will be requested that are wider and taller than the tile + * size by a value of 2 x gutter. This allows artifacts of rendering + * at tile edges to be ignored. Set a gutter value that is equal to + * half the size of the widest symbol that needs to be displayed. + * Defaults to zero. Non-tiled layers always have zero gutter. + */ + gutter: 0, + + /** + * APIProperty: projection + * {<OpenLayers.Projection>} or {<String>} Specifies the projection of the layer. + * Can be set in the layer options. If not specified in the layer options, + * it is set to the default projection specified in the map, + * when the layer is added to the map. + * Projection along with default maxExtent and resolutions + * are set automatically with commercial baselayers in EPSG:3857, + * such as Google, Bing and OpenStreetMap, and do not need to be specified. + * Otherwise, if specifying projection, also set maxExtent, + * maxResolution or resolutions as appropriate. + * When using vector layers with strategies, layer projection should be set + * to the projection of the source data if that is different from the map default. + * + * Can be either a string or an <OpenLayers.Projection> object; + * if a string is passed, will be converted to an object when + * the layer is added to the map. + * + */ + projection: null, + + /** + * APIProperty: units + * {String} The layer map units. Defaults to null. Possible values + * are 'degrees' (or 'dd'), 'm', 'ft', 'km', 'mi', 'inches'. + * Normally taken from the projection. + * Only required if both map and layers do not define a projection, + * or if they define a projection which does not define units. + */ + units: null, + + /** + * APIProperty: scales + * {Array} An array of map scales in descending order. The values in the + * array correspond to the map scale denominator. Note that these + * values only make sense if the display (monitor) resolution of the + * client is correctly guessed by whomever is configuring the + * application. In addition, the units property must also be set. + * Use <resolutions> instead wherever possible. + */ + scales: null, + + /** + * APIProperty: resolutions + * {Array} A list of map resolutions (map units per pixel) in descending + * order. If this is not set in the layer constructor, it will be set + * based on other resolution related properties (maxExtent, + * maxResolution, maxScale, etc.). + */ + resolutions: null, + + /** + * APIProperty: maxExtent + * {<OpenLayers.Bounds>|Array} If provided as an array, the array + * should consist of four values (left, bottom, right, top). + * The maximum extent for the layer. Defaults to null. + * + * The center of these bounds will not stray outside + * of the viewport extent during panning. In addition, if + * <displayOutsideMaxExtent> is set to false, data will not be + * requested that falls completely outside of these bounds. + */ + maxExtent: null, + + /** + * APIProperty: minExtent + * {<OpenLayers.Bounds>|Array} If provided as an array, the array + * should consist of four values (left, bottom, right, top). + * The minimum extent for the layer. Defaults to null. + */ + minExtent: null, + + /** + * APIProperty: maxResolution + * {Float} Default max is 360 deg / 256 px, which corresponds to + * zoom level 0 on gmaps. Specify a different value in the layer + * options if you are not using the default <OpenLayers.Map.tileSize> + * and displaying the whole world. + */ + maxResolution: null, + + /** + * APIProperty: minResolution + * {Float} + */ + minResolution: null, + + /** + * APIProperty: numZoomLevels + * {Integer} + */ + numZoomLevels: null, + + /** + * APIProperty: minScale + * {Float} + */ + minScale: null, + + /** + * APIProperty: maxScale + * {Float} + */ + maxScale: null, + + /** + * APIProperty: displayOutsideMaxExtent + * {Boolean} Request map tiles that are completely outside of the max + * extent for this layer. Defaults to false. + */ + displayOutsideMaxExtent: false, + + /** + * APIProperty: wrapDateLine + * {Boolean} Wraps the world at the international dateline, so the map can + * be panned infinitely in longitudinal direction. Only use this on the + * base layer, and only if the layer's maxExtent equals the world bounds. + * #487 for more info. + */ + wrapDateLine: false, + + /** + * Property: metadata + * {Object} This object can be used to store additional information on a + * layer object. + */ + metadata: null, + + /** + * Constructor: OpenLayers.Layer + * + * Parameters: + * name - {String} The layer name + * options - {Object} Hashtable of extra options to tag onto the layer + */ + initialize: function(name, options) { + + this.metadata = {}; + + options = OpenLayers.Util.extend({}, options); + // make sure we respect alwaysInRange if set on the prototype + if (this.alwaysInRange != null) { + options.alwaysInRange = this.alwaysInRange; + } + this.addOptions(options); + + this.name = name; + + if (this.id == null) { + + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + + this.div = OpenLayers.Util.createDiv(this.id); + this.div.style.width = "100%"; + this.div.style.height = "100%"; + this.div.dir = "ltr"; + + this.events = new OpenLayers.Events(this, this.div); + if(this.eventListeners instanceof Object) { + this.events.on(this.eventListeners); + } + + } + }, + + /** + * Method: destroy + * Destroy is a destructor: this is to alleviate cyclic references which + * the Javascript garbage cleaner can not take care of on its own. + * + * Parameters: + * setNewBaseLayer - {Boolean} Set a new base layer when this layer has + * been destroyed. Default is true. + */ + destroy: function(setNewBaseLayer) { + if (setNewBaseLayer == null) { + setNewBaseLayer = true; + } + if (this.map != null) { + this.map.removeLayer(this, setNewBaseLayer); + } + this.projection = null; + this.map = null; + this.name = null; + this.div = null; + this.options = null; + + if (this.events) { + if(this.eventListeners) { + this.events.un(this.eventListeners); + } + this.events.destroy(); + } + this.eventListeners = null; + this.events = null; + }, + + /** + * Method: clone + * + * Parameters: + * obj - {<OpenLayers.Layer>} The layer to be cloned + * + * Returns: + * {<OpenLayers.Layer>} An exact clone of this <OpenLayers.Layer> + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer(this.name, this.getOptions()); + } + + // catch any randomly tagged-on properties + OpenLayers.Util.applyDefaults(obj, this); + + // a cloned layer should never have its map property set + // because it has not been added to a map yet. + obj.map = null; + + return obj; + }, + + /** + * Method: getOptions + * Extracts an object from the layer with the properties that were set as + * options, but updates them with the values currently set on the + * instance. + * + * Returns: + * {Object} the <options> of the layer, representing the current state. + */ + getOptions: function() { + var options = {}; + for(var o in this.options) { + options[o] = this[o]; + } + return options; + }, + + /** + * APIMethod: setName + * Sets the new layer name for this layer. Can trigger a changelayer event + * on the map. + * + * Parameters: + * newName - {String} The new name. + */ + setName: function(newName) { + if (newName != this.name) { + this.name = newName; + if (this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "name" + }); + } + } + }, + + /** + * APIMethod: addOptions + * + * Parameters: + * newOptions - {Object} + * reinitialize - {Boolean} If set to true, and if resolution options of the + * current baseLayer were changed, the map will be recentered to make + * sure that it is displayed with a valid resolution, and a + * changebaselayer event will be triggered. + */ + addOptions: function (newOptions, reinitialize) { + + if (this.options == null) { + this.options = {}; + } + + if (newOptions) { + // make sure this.projection references a projection object + if(typeof newOptions.projection == "string") { + newOptions.projection = new OpenLayers.Projection(newOptions.projection); + } + if (newOptions.projection) { + // get maxResolution, units and maxExtent from projection defaults if + // they are not defined already + OpenLayers.Util.applyDefaults(newOptions, + OpenLayers.Projection.defaults[newOptions.projection.getCode()]); + } + // allow array for extents + if (newOptions.maxExtent && !(newOptions.maxExtent instanceof OpenLayers.Bounds)) { + newOptions.maxExtent = new OpenLayers.Bounds(newOptions.maxExtent); + } + if (newOptions.minExtent && !(newOptions.minExtent instanceof OpenLayers.Bounds)) { + newOptions.minExtent = new OpenLayers.Bounds(newOptions.minExtent); + } + } + + // update our copy for clone + OpenLayers.Util.extend(this.options, newOptions); + + // add new options to this + OpenLayers.Util.extend(this, newOptions); + + // get the units from the projection, if we have a projection + // and it it has units + if(this.projection && this.projection.getUnits()) { + this.units = this.projection.getUnits(); + } + + // re-initialize resolutions if necessary, i.e. if any of the + // properties of the "properties" array defined below is set + // in the new options + if(this.map) { + // store current resolution so we can try to restore it later + var resolution = this.map.getResolution(); + var properties = this.RESOLUTION_PROPERTIES.concat( + ["projection", "units", "minExtent", "maxExtent"] + ); + for(var o in newOptions) { + if(newOptions.hasOwnProperty(o) && + OpenLayers.Util.indexOf(properties, o) >= 0) { + + this.initResolutions(); + if (reinitialize && this.map.baseLayer === this) { + // update map position, and restore previous resolution + this.map.setCenter(this.map.getCenter(), + this.map.getZoomForResolution(resolution), + false, true + ); + // trigger a changebaselayer event to make sure that + // all controls (especially + // OpenLayers.Control.PanZoomBar) get notified of the + // new options + this.map.events.triggerEvent("changebaselayer", { + layer: this + }); + } + break; + } + } + } + }, + + /** + * APIMethod: onMapResize + * This function can be implemented by subclasses + */ + onMapResize: function() { + //this function can be implemented by subclasses + }, + + /** + * APIMethod: redraw + * Redraws the layer. Returns true if the layer was redrawn, false if not. + * + * Returns: + * {Boolean} The layer was redrawn. + */ + redraw: function() { + var redrawn = false; + if (this.map) { + + // min/max Range may have changed + this.inRange = this.calculateInRange(); + + // map's center might not yet be set + var extent = this.getExtent(); + + if (extent && this.inRange && this.visibility) { + var zoomChanged = true; + this.moveTo(extent, zoomChanged, false); + this.events.triggerEvent("moveend", + {"zoomChanged": zoomChanged}); + redrawn = true; + } + } + return redrawn; + }, + + /** + * Method: moveTo + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * zoomChanged - {Boolean} Tells when zoom has changed, as layers have to + * do some init work in that case. + * dragging - {Boolean} + */ + moveTo:function(bounds, zoomChanged, dragging) { + var display = this.visibility; + if (!this.isBaseLayer) { + display = display && this.inRange; + } + this.display(display); + }, + + /** + * Method: moveByPx + * Move the layer based on pixel vector. To be implemented by subclasses. + * + * Parameters: + * dx - {Number} The x coord of the displacement vector. + * dy - {Number} The y coord of the displacement vector. + */ + moveByPx: function(dx, dy) { + }, + + /** + * Method: setMap + * Set the map property for the layer. This is done through an accessor + * so that subclasses can override this and take special action once + * they have their map variable set. + * + * Here we take care to bring over any of the necessary default + * properties from the map. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + if (this.map == null) { + + this.map = map; + + // grab some essential layer data from the map if it hasn't already + // been set + this.maxExtent = this.maxExtent || this.map.maxExtent; + this.minExtent = this.minExtent || this.map.minExtent; + + this.projection = this.projection || this.map.projection; + if (typeof this.projection == "string") { + this.projection = new OpenLayers.Projection(this.projection); + } + + // Check the projection to see if we can get units -- if not, refer + // to properties. + this.units = this.projection.getUnits() || + this.units || this.map.units; + + this.initResolutions(); + + if (!this.isBaseLayer) { + this.inRange = this.calculateInRange(); + var show = ((this.visibility) && (this.inRange)); + this.div.style.display = show ? "" : "none"; + } + + // deal with gutters + this.setTileSize(); + } + }, + + /** + * Method: afterAdd + * Called at the end of the map.addLayer sequence. At this point, the map + * will have a base layer. To be overridden by subclasses. + */ + afterAdd: function() { + }, + + /** + * APIMethod: removeMap + * Just as setMap() allows each layer the possibility to take a + * personalized action on being added to the map, removeMap() allows + * each layer to take a personalized action on being removed from it. + * For now, this will be mostly unused, except for the EventPane layer, + * which needs this hook so that it can remove the special invisible + * pane. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + removeMap: function(map) { + //to be overridden by subclasses + }, + + /** + * APIMethod: getImageSize + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} optional tile bounds, can be used + * by subclasses that have to deal with different tile sizes at the + * layer extent edges (e.g. Zoomify) + * + * Returns: + * {<OpenLayers.Size>} The size that the image should be, taking into + * account gutters. + */ + getImageSize: function(bounds) { + return (this.imageSize || this.tileSize); + }, + + /** + * APIMethod: setTileSize + * Set the tile size based on the map size. This also sets layer.imageSize + * or use by Tile.Image. + * + * Parameters: + * size - {<OpenLayers.Size>} + */ + setTileSize: function(size) { + var tileSize = (size) ? size : + ((this.tileSize) ? this.tileSize : + this.map.getTileSize()); + this.tileSize = tileSize; + if(this.gutter) { + // layers with gutters need non-null tile sizes + //if(tileSize == null) { + // OpenLayers.console.error("Error in layer.setMap() for " + + // this.name + ": layers with " + + // "gutters need non-null tile sizes"); + //} + this.imageSize = new OpenLayers.Size(tileSize.w + (2*this.gutter), + tileSize.h + (2*this.gutter)); + } + }, + + /** + * APIMethod: getVisibility + * + * Returns: + * {Boolean} The layer should be displayed (if in range). + */ + getVisibility: function() { + return this.visibility; + }, + + /** + * APIMethod: setVisibility + * Set the visibility flag for the layer and hide/show & redraw + * accordingly. Fire event unless otherwise specified + * + * Note that visibility is no longer simply whether or not the layer's + * style.display is set to "block". Now we store a 'visibility' state + * property on the layer class, this allows us to remember whether or + * not we *desire* for a layer to be visible. In the case where the + * map's resolution is out of the layer's range, this desire may be + * subverted. + * + * Parameters: + * visibility - {Boolean} Whether or not to display the layer (if in range) + */ + setVisibility: function(visibility) { + if (visibility != this.visibility) { + this.visibility = visibility; + this.display(visibility); + this.redraw(); + if (this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "visibility" + }); + } + this.events.triggerEvent("visibilitychanged"); + } + }, + + /** + * APIMethod: display + * Hide or show the Layer. This is designed to be used internally, and + * is not generally the way to enable or disable the layer. For that, + * use the setVisibility function instead.. + * + * Parameters: + * display - {Boolean} + */ + display: function(display) { + if (display != (this.div.style.display != "none")) { + this.div.style.display = (display && this.calculateInRange()) ? "block" : "none"; + } + }, + + /** + * APIMethod: calculateInRange + * + * Returns: + * {Boolean} The layer is displayable at the current map's current + * resolution. Note that if 'alwaysInRange' is true for the layer, + * this function will always return true. + */ + calculateInRange: function() { + var inRange = false; + + if (this.alwaysInRange) { + inRange = true; + } else { + if (this.map) { + var resolution = this.map.getResolution(); + inRange = ( (resolution >= this.minResolution) && + (resolution <= this.maxResolution) ); + } + } + return inRange; + }, + + /** + * APIMethod: setIsBaseLayer + * + * Parameters: + * isBaseLayer - {Boolean} + */ + setIsBaseLayer: function(isBaseLayer) { + if (isBaseLayer != this.isBaseLayer) { + this.isBaseLayer = isBaseLayer; + if (this.map != null) { + this.map.events.triggerEvent("changebaselayer", { + layer: this + }); + } + } + }, + + /********************************************************/ + /* */ + /* Baselayer Functions */ + /* */ + /********************************************************/ + + /** + * Method: initResolutions + * This method's responsibility is to set up the 'resolutions' array + * for the layer -- this array is what the layer will use to interface + * between the zoom levels of the map and the resolution display + * of the layer. + * + * The user has several options that determine how the array is set up. + * + * For a detailed explanation, see the following wiki from the + * openlayers.org homepage: + * http://trac.openlayers.org/wiki/SettingZoomLevels + */ + initResolutions: function() { + + // ok we want resolutions, here's our strategy: + // + // 1. if resolutions are defined in the layer config, use them + // 2. else, if scales are defined in the layer config then derive + // resolutions from these scales + // 3. else, attempt to calculate resolutions from maxResolution, + // minResolution, numZoomLevels, maxZoomLevel set in the + // layer config + // 4. if we still don't have resolutions, and if resolutions + // are defined in the same, use them + // 5. else, if scales are defined in the map then derive + // resolutions from these scales + // 6. else, attempt to calculate resolutions from maxResolution, + // minResolution, numZoomLevels, maxZoomLevel set in the + // map + // 7. hope for the best! + + var i, len, p; + var props = {}, alwaysInRange = true; + + // get resolution data from layer config + // (we also set alwaysInRange in the layer as appropriate) + for(i=0, len=this.RESOLUTION_PROPERTIES.length; i<len; i++) { + p = this.RESOLUTION_PROPERTIES[i]; + props[p] = this.options[p]; + if(alwaysInRange && this.options[p]) { + alwaysInRange = false; + } + } + if(this.options.alwaysInRange == null) { + this.alwaysInRange = alwaysInRange; + } + + // if we don't have resolutions then attempt to derive them from scales + if(props.resolutions == null) { + props.resolutions = this.resolutionsFromScales(props.scales); + } + + // if we still don't have resolutions then attempt to calculate them + if(props.resolutions == null) { + props.resolutions = this.calculateResolutions(props); + } + + // if we couldn't calculate resolutions then we look at we have + // in the map + if(props.resolutions == null) { + for(i=0, len=this.RESOLUTION_PROPERTIES.length; i<len; i++) { + p = this.RESOLUTION_PROPERTIES[i]; + props[p] = this.options[p] != null ? + this.options[p] : this.map[p]; + } + if(props.resolutions == null) { + props.resolutions = this.resolutionsFromScales(props.scales); + } + if(props.resolutions == null) { + props.resolutions = this.calculateResolutions(props); + } + } + + // ok, we new need to set properties in the instance + + // get maxResolution from the config if it's defined there + var maxResolution; + if(this.options.maxResolution && + this.options.maxResolution !== "auto") { + maxResolution = this.options.maxResolution; + } + if(this.options.minScale) { + maxResolution = OpenLayers.Util.getResolutionFromScale( + this.options.minScale, this.units); + } + + // get minResolution from the config if it's defined there + var minResolution; + if(this.options.minResolution && + this.options.minResolution !== "auto") { + minResolution = this.options.minResolution; + } + if(this.options.maxScale) { + minResolution = OpenLayers.Util.getResolutionFromScale( + this.options.maxScale, this.units); + } + + if(props.resolutions) { + + //sort resolutions array descendingly + props.resolutions.sort(function(a, b) { + return (b - a); + }); + + // if we still don't have a maxResolution get it from the + // resolutions array + if(!maxResolution) { + maxResolution = props.resolutions[0]; + } + + // if we still don't have a minResolution get it from the + // resolutions array + if(!minResolution) { + var lastIdx = props.resolutions.length - 1; + minResolution = props.resolutions[lastIdx]; + } + } + + this.resolutions = props.resolutions; + if(this.resolutions) { + len = this.resolutions.length; + this.scales = new Array(len); + for(i=0; i<len; i++) { + this.scales[i] = OpenLayers.Util.getScaleFromResolution( + this.resolutions[i], this.units); + } + this.numZoomLevels = len; + } + this.minResolution = minResolution; + if(minResolution) { + this.maxScale = OpenLayers.Util.getScaleFromResolution( + minResolution, this.units); + } + this.maxResolution = maxResolution; + if(maxResolution) { + this.minScale = OpenLayers.Util.getScaleFromResolution( + maxResolution, this.units); + } + }, + + /** + * Method: resolutionsFromScales + * Derive resolutions from scales. + * + * Parameters: + * scales - {Array(Number)} Scales + * + * Returns + * {Array(Number)} Resolutions + */ + resolutionsFromScales: function(scales) { + if(scales == null) { + return; + } + var resolutions, i, len; + len = scales.length; + resolutions = new Array(len); + for(i=0; i<len; i++) { + resolutions[i] = OpenLayers.Util.getResolutionFromScale( + scales[i], this.units); + } + return resolutions; + }, + + /** + * Method: calculateResolutions + * Calculate resolutions based on the provided properties. + * + * Parameters: + * props - {Object} Properties + * + * Returns: + * {Array({Number})} Array of resolutions. + */ + calculateResolutions: function(props) { + + var viewSize, wRes, hRes; + + // determine maxResolution + var maxResolution = props.maxResolution; + if(props.minScale != null) { + maxResolution = + OpenLayers.Util.getResolutionFromScale(props.minScale, + this.units); + } else if(maxResolution == "auto" && this.maxExtent != null) { + viewSize = this.map.getSize(); + wRes = this.maxExtent.getWidth() / viewSize.w; + hRes = this.maxExtent.getHeight() / viewSize.h; + maxResolution = Math.max(wRes, hRes); + } + + // determine minResolution + var minResolution = props.minResolution; + if(props.maxScale != null) { + minResolution = + OpenLayers.Util.getResolutionFromScale(props.maxScale, + this.units); + } else if(props.minResolution == "auto" && this.minExtent != null) { + viewSize = this.map.getSize(); + wRes = this.minExtent.getWidth() / viewSize.w; + hRes = this.minExtent.getHeight()/ viewSize.h; + minResolution = Math.max(wRes, hRes); + } + + if(typeof maxResolution !== "number" && + typeof minResolution !== "number" && + this.maxExtent != null) { + // maxResolution for default grid sets assumes that at zoom + // level zero, the whole world fits on one tile. + var tileSize = this.map.getTileSize(); + maxResolution = Math.max( + this.maxExtent.getWidth() / tileSize.w, + this.maxExtent.getHeight() / tileSize.h + ); + } + + // determine numZoomLevels + var maxZoomLevel = props.maxZoomLevel; + var numZoomLevels = props.numZoomLevels; + if(typeof minResolution === "number" && + typeof maxResolution === "number" && numZoomLevels === undefined) { + var ratio = maxResolution / minResolution; + numZoomLevels = Math.floor(Math.log(ratio) / Math.log(2)) + 1; + } else if(numZoomLevels === undefined && maxZoomLevel != null) { + numZoomLevels = maxZoomLevel + 1; + } + + // are we able to calculate resolutions? + if(typeof numZoomLevels !== "number" || numZoomLevels <= 0 || + (typeof maxResolution !== "number" && + typeof minResolution !== "number")) { + return; + } + + // now we have numZoomLevels and at least one of maxResolution + // or minResolution, we can populate the resolutions array + + var resolutions = new Array(numZoomLevels); + var base = 2; + if(typeof minResolution == "number" && + typeof maxResolution == "number") { + // if maxResolution and minResolution are set, we calculate + // the base for exponential scaling that starts at + // maxResolution and ends at minResolution in numZoomLevels + // steps. + base = Math.pow( + (maxResolution / minResolution), + (1 / (numZoomLevels - 1)) + ); + } + + var i; + if(typeof maxResolution === "number") { + for(i=0; i<numZoomLevels; i++) { + resolutions[i] = maxResolution / Math.pow(base, i); + } + } else { + for(i=0; i<numZoomLevels; i++) { + resolutions[numZoomLevels - 1 - i] = + minResolution * Math.pow(base, i); + } + } + + return resolutions; + }, + + /** + * APIMethod: getResolution + * + * Returns: + * {Float} The currently selected resolution of the map, taken from the + * resolutions array, indexed by current zoom level. + */ + getResolution: function() { + var zoom = this.map.getZoom(); + return this.getResolutionForZoom(zoom); + }, + + /** + * APIMethod: getExtent + * + * Returns: + * {<OpenLayers.Bounds>} A Bounds object which represents the lon/lat + * bounds of the current viewPort. + */ + getExtent: function() { + // just use stock map calculateBounds function -- passing no arguments + // means it will user map's current center & resolution + // + return this.map.calculateBounds(); + }, + + /** + * APIMethod: getZoomForExtent + * + * Parameters: + * extent - {<OpenLayers.Bounds>} + * closest - {Boolean} Find the zoom level that most closely fits the + * specified bounds. Note that this may result in a zoom that does + * not exactly contain the entire extent. + * Default is false. + * + * Returns: + * {Integer} The index of the zoomLevel (entry in the resolutions array) + * for the passed-in extent. We do this by calculating the ideal + * resolution for the given extent (based on the map size) and then + * calling getZoomForResolution(), passing along the 'closest' + * parameter. + */ + getZoomForExtent: function(extent, closest) { + var viewSize = this.map.getSize(); + var idealResolution = Math.max( extent.getWidth() / viewSize.w, + extent.getHeight() / viewSize.h ); + + return this.getZoomForResolution(idealResolution, closest); + }, + + /** + * Method: getDataExtent + * Calculates the max extent which includes all of the data for the layer. + * This function is to be implemented by subclasses. + * + * Returns: + * {<OpenLayers.Bounds>} + */ + getDataExtent: function () { + //to be implemented by subclasses + }, + + /** + * APIMethod: getResolutionForZoom + * + * Parameters: + * zoom - {Float} + * + * Returns: + * {Float} A suitable resolution for the specified zoom. + */ + getResolutionForZoom: function(zoom) { + zoom = Math.max(0, Math.min(zoom, this.resolutions.length - 1)); + var resolution; + if(this.map.fractionalZoom) { + var low = Math.floor(zoom); + var high = Math.ceil(zoom); + resolution = this.resolutions[low] - + ((zoom-low) * (this.resolutions[low]-this.resolutions[high])); + } else { + resolution = this.resolutions[Math.round(zoom)]; + } + return resolution; + }, + + /** + * APIMethod: getZoomForResolution + * + * Parameters: + * resolution - {Float} + * closest - {Boolean} Find the zoom level that corresponds to the absolute + * closest resolution, which may result in a zoom whose corresponding + * resolution is actually smaller than we would have desired (if this + * is being called from a getZoomForExtent() call, then this means that + * the returned zoom index might not actually contain the entire + * extent specified... but it'll be close). + * Default is false. + * + * Returns: + * {Integer} The index of the zoomLevel (entry in the resolutions array) + * that corresponds to the best fit resolution given the passed in + * value and the 'closest' specification. + */ + getZoomForResolution: function(resolution, closest) { + var zoom, i, len; + if(this.map.fractionalZoom) { + var lowZoom = 0; + var highZoom = this.resolutions.length - 1; + var highRes = this.resolutions[lowZoom]; + var lowRes = this.resolutions[highZoom]; + var res; + for(i=0, len=this.resolutions.length; i<len; ++i) { + res = this.resolutions[i]; + if(res >= resolution) { + highRes = res; + lowZoom = i; + } + if(res <= resolution) { + lowRes = res; + highZoom = i; + break; + } + } + var dRes = highRes - lowRes; + if(dRes > 0) { + zoom = lowZoom + ((highRes - resolution) / dRes); + } else { + zoom = lowZoom; + } + } else { + var diff; + var minDiff = Number.POSITIVE_INFINITY; + for(i=0, len=this.resolutions.length; i<len; i++) { + if (closest) { + diff = Math.abs(this.resolutions[i] - resolution); + if (diff > minDiff) { + break; + } + minDiff = diff; + } else { + if (this.resolutions[i] < resolution) { + break; + } + } + } + zoom = Math.max(0, i-1); + } + return zoom; + }, + + /** + * APIMethod: getLonLatFromViewPortPx + * + * Parameters: + * viewPortPx - {<OpenLayers.Pixel>|Object} An OpenLayers.Pixel or + * an object with a 'x' + * and 'y' properties. + * + * Returns: + * {<OpenLayers.LonLat>} An OpenLayers.LonLat which is the passed-in + * view port <OpenLayers.Pixel>, translated into lon/lat by the layer. + */ + getLonLatFromViewPortPx: function (viewPortPx) { + var lonlat = null; + var map = this.map; + if (viewPortPx != null && map.minPx) { + var res = map.getResolution(); + var maxExtent = map.getMaxExtent({restricted: true}); + var lon = (viewPortPx.x - map.minPx.x) * res + maxExtent.left; + var lat = (map.minPx.y - viewPortPx.y) * res + maxExtent.top; + lonlat = new OpenLayers.LonLat(lon, lat); + + if (this.wrapDateLine) { + lonlat = lonlat.wrapDateLine(this.maxExtent); + } + } + return lonlat; + }, + + /** + * APIMethod: getViewPortPxFromLonLat + * Returns a pixel location given a map location. This method will return + * fractional pixel values. + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>|Object} An OpenLayers.LonLat or + * an object with a 'lon' + * and 'lat' properties. + * + * Returns: + * {<OpenLayers.Pixel>} An <OpenLayers.Pixel> which is the passed-in + * lonlat translated into view port pixels. + */ + getViewPortPxFromLonLat: function (lonlat, resolution) { + var px = null; + if (lonlat != null) { + resolution = resolution || this.map.getResolution(); + var extent = this.map.calculateBounds(null, resolution); + px = new OpenLayers.Pixel( + (1/resolution * (lonlat.lon - extent.left)), + (1/resolution * (extent.top - lonlat.lat)) + ); + } + return px; + }, + + /** + * APIMethod: setOpacity + * Sets the opacity for the entire layer (all images) + * + * Parameters: + * opacity - {Float} + */ + setOpacity: function(opacity) { + if (opacity != this.opacity) { + this.opacity = opacity; + var childNodes = this.div.childNodes; + for(var i = 0, len = childNodes.length; i < len; ++i) { + var element = childNodes[i].firstChild || childNodes[i]; + var lastChild = childNodes[i].lastChild; + //TODO de-uglify this + if (lastChild && lastChild.nodeName.toLowerCase() === "iframe") { + element = lastChild.parentNode; + } + OpenLayers.Util.modifyDOMElement(element, null, null, null, + null, null, null, opacity); + } + if (this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "opacity" + }); + } + } + }, + + /** + * Method: getZIndex + * + * Returns: + * {Integer} the z-index of this layer + */ + getZIndex: function () { + return this.div.style.zIndex; + }, + + /** + * Method: setZIndex + * + * Parameters: + * zIndex - {Integer} + */ + setZIndex: function (zIndex) { + this.div.style.zIndex = zIndex; + }, + + /** + * Method: adjustBounds + * This function will take a bounds, and if wrapDateLine option is set + * on the layer, it will return a bounds which is wrapped around the + * world. We do not wrap for bounds which *cross* the + * maxExtent.left/right, only bounds which are entirely to the left + * or entirely to the right. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + */ + adjustBounds: function (bounds) { + + if (this.gutter) { + // Adjust the extent of a bounds in map units by the + // layer's gutter in pixels. + var mapGutter = this.gutter * this.map.getResolution(); + bounds = new OpenLayers.Bounds(bounds.left - mapGutter, + bounds.bottom - mapGutter, + bounds.right + mapGutter, + bounds.top + mapGutter); + } + + if (this.wrapDateLine) { + // wrap around the date line, within the limits of rounding error + var wrappingOptions = { + 'rightTolerance':this.getResolution(), + 'leftTolerance':this.getResolution() + }; + bounds = bounds.wrapDateLine(this.maxExtent, wrappingOptions); + + } + return bounds; + }, + + CLASS_NAME: "OpenLayers.Layer" +}); +/* ====================================================================== + OpenLayers/Layer/HTTPRequest.js + ====================================================================== */ + +/* 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/Layer.js + */ + +/** + * Class: OpenLayers.Layer.HTTPRequest + * + * Inherits from: + * - <OpenLayers.Layer> + */ +OpenLayers.Layer.HTTPRequest = OpenLayers.Class(OpenLayers.Layer, { + + /** + * Constant: URL_HASH_FACTOR + * {Float} Used to hash URL param strings for multi-WMS server selection. + * Set to the Golden Ratio per Knuth's recommendation. + */ + URL_HASH_FACTOR: (Math.sqrt(5) - 1) / 2, + + /** + * Property: url + * {Array(String) or String} This is either an array of url strings or + * a single url string. + */ + url: null, + + /** + * Property: params + * {Object} Hashtable of key/value parameters + */ + params: null, + + /** + * APIProperty: reproject + * *Deprecated*. See http://docs.openlayers.org/library/spherical_mercator.html + * for information on the replacement for this functionality. + * {Boolean} Whether layer should reproject itself based on base layer + * locations. This allows reprojection onto commercial layers. + * Default is false: Most layers can't reproject, but layers + * which can create non-square geographic pixels can, like WMS. + * + */ + reproject: false, + + /** + * Constructor: OpenLayers.Layer.HTTPRequest + * + * Parameters: + * name - {String} + * url - {Array(String) or String} + * params - {Object} + * options - {Object} Hashtable of extra options to tag onto the layer + */ + initialize: function(name, url, params, options) { + OpenLayers.Layer.prototype.initialize.apply(this, [name, options]); + this.url = url; + if (!this.params) { + this.params = OpenLayers.Util.extend({}, params); + } + }, + + /** + * APIMethod: destroy + */ + destroy: function() { + this.url = null; + this.params = null; + OpenLayers.Layer.prototype.destroy.apply(this, arguments); + }, + + /** + * APIMethod: clone + * + * Parameters: + * obj - {Object} + * + * Returns: + * {<OpenLayers.Layer.HTTPRequest>} An exact clone of this + * <OpenLayers.Layer.HTTPRequest> + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer.HTTPRequest(this.name, + this.url, + this.params, + this.getOptions()); + } + + //get all additions from superclasses + obj = OpenLayers.Layer.prototype.clone.apply(this, [obj]); + + // copy/set any non-init, non-simple values here + + return obj; + }, + + /** + * APIMethod: setUrl + * + * Parameters: + * newUrl - {String} + */ + setUrl: function(newUrl) { + this.url = newUrl; + }, + + /** + * APIMethod: mergeNewParams + * + * Parameters: + * newParams - {Object} + * + * Returns: + * redrawn: {Boolean} whether the layer was actually redrawn. + */ + mergeNewParams:function(newParams) { + this.params = OpenLayers.Util.extend(this.params, newParams); + var ret = this.redraw(); + if(this.map != null) { + this.map.events.triggerEvent("changelayer", { + layer: this, + property: "params" + }); + } + return ret; + }, + + /** + * APIMethod: redraw + * Redraws the layer. Returns true if the layer was redrawn, false if not. + * + * Parameters: + * force - {Boolean} Force redraw by adding random parameter. + * + * Returns: + * {Boolean} The layer was redrawn. + */ + redraw: function(force) { + if (force) { + return this.mergeNewParams({"_olSalt": Math.random()}); + } else { + return OpenLayers.Layer.prototype.redraw.apply(this, []); + } + }, + + /** + * Method: selectUrl + * selectUrl() implements the standard floating-point multiplicative + * hash function described by Knuth, and hashes the contents of the + * given param string into a float between 0 and 1. This float is then + * scaled to the size of the provided urls array, and used to select + * a URL. + * + * Parameters: + * paramString - {String} + * urls - {Array(String)} + * + * Returns: + * {String} An entry from the urls array, deterministically selected based + * on the paramString. + */ + selectUrl: function(paramString, urls) { + var product = 1; + for (var i=0, len=paramString.length; i<len; i++) { + product *= paramString.charCodeAt(i) * this.URL_HASH_FACTOR; + product -= Math.floor(product); + } + return urls[Math.floor(product * urls.length)]; + }, + + /** + * Method: getFullRequestString + * Combine url with layer's params and these newParams. + * + * does checking on the serverPath variable, allowing for cases when it + * is supplied with trailing ? or &, as well as cases where not. + * + * return in formatted string like this: + * "server?key1=value1&key2=value2&key3=value3" + * + * WARNING: The altUrl parameter is deprecated and will be removed in 3.0. + * + * Parameters: + * newParams - {Object} + * altUrl - {String} Use this as the url instead of the layer's url + * + * Returns: + * {String} + */ + getFullRequestString:function(newParams, altUrl) { + + // if not altUrl passed in, use layer's url + var url = altUrl || this.url; + + // create a new params hashtable with all the layer params and the + // new params together. then convert to string + var allParams = OpenLayers.Util.extend({}, this.params); + allParams = OpenLayers.Util.extend(allParams, newParams); + var paramsString = OpenLayers.Util.getParameterString(allParams); + + // if url is not a string, it should be an array of strings, + // in which case we will deterministically select one of them in + // order to evenly distribute requests to different urls. + // + if (OpenLayers.Util.isArray(url)) { + url = this.selectUrl(paramsString, url); + } + + // ignore parameters that are already in the url search string + var urlParams = + OpenLayers.Util.upperCaseObject(OpenLayers.Util.getParameters(url)); + for(var key in allParams) { + if(key.toUpperCase() in urlParams) { + delete allParams[key]; + } + } + paramsString = OpenLayers.Util.getParameterString(allParams); + + return OpenLayers.Util.urlAppend(url, paramsString); + }, + + CLASS_NAME: "OpenLayers.Layer.HTTPRequest" +}); +/* ====================================================================== + OpenLayers/Tile.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Tile + * This is a class designed to designate a single tile, however + * it is explicitly designed to do relatively little. Tiles store + * information about themselves -- such as the URL that they are related + * to, and their size - but do not add themselves to the layer div + * automatically, for example. Create a new tile with the + * <OpenLayers.Tile> constructor, or a subclass. + * + * TBD 3.0 - remove reference to url in above paragraph + * + */ +OpenLayers.Tile = OpenLayers.Class({ + + /** + * APIProperty: events + * {<OpenLayers.Events>} An events object that handles all + * events on the tile. + * + * Register a listener for a particular event with the following syntax: + * (code) + * tile.events.register(type, obj, listener); + * (end) + * + * Supported event types: + * beforedraw - Triggered before the tile is drawn. Used to defer + * drawing to an animation queue. To defer drawing, listeners need + * to return false, which will abort drawing. The queue handler needs + * to call <draw>(true) to actually draw the tile. + * loadstart - Triggered when tile loading starts. + * loadend - Triggered when tile loading ends. + * loaderror - Triggered before the loadend event (i.e. when the tile is + * still hidden) if the tile could not be loaded. + * reload - Triggered when an already loading tile is reloaded. + * unload - Triggered before a tile is unloaded. + */ + events: 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. + * + * This options can be set in the ``tileOptions`` option from + * <OpenLayers.Layer.Grid>. For example, to be notified of the + * ``loadend`` event of each tiles: + * (code) + * new OpenLayers.Layer.OSM('osm', 'http://tile.openstreetmap.org/${z}/${x}/${y}.png', { + * tileOptions: { + * eventListeners: { + * 'loadend': function(evt) { + * // do something on loadend + * } + * } + * } + * }); + * (end) + */ + eventListeners: null, + + /** + * Property: id + * {String} null + */ + id: null, + + /** + * Property: layer + * {<OpenLayers.Layer>} layer the tile is attached to + */ + layer: null, + + /** + * Property: url + * {String} url of the request. + * + * TBD 3.0 + * Deprecated. The base tile class does not need an url. This should be + * handled in subclasses. Does not belong here. + */ + url: null, + + /** + * APIProperty: bounds + * {<OpenLayers.Bounds>} null + */ + bounds: null, + + /** + * Property: size + * {<OpenLayers.Size>} null + */ + size: null, + + /** + * Property: position + * {<OpenLayers.Pixel>} Top Left pixel of the tile + */ + position: null, + + /** + * Property: isLoading + * {Boolean} Is the tile loading? + */ + isLoading: false, + + /** TBD 3.0 -- remove 'url' from the list of parameters to the constructor. + * there is no need for the base tile class to have a url. + */ + + /** + * Constructor: OpenLayers.Tile + * Constructor for a new <OpenLayers.Tile> instance. + * + * Parameters: + * layer - {<OpenLayers.Layer>} layer that the tile will go in. + * position - {<OpenLayers.Pixel>} + * bounds - {<OpenLayers.Bounds>} + * url - {<String>} + * size - {<OpenLayers.Size>} + * options - {Object} + */ + initialize: function(layer, position, bounds, url, size, options) { + this.layer = layer; + this.position = position.clone(); + this.setBounds(bounds); + this.url = url; + if (size) { + this.size = size.clone(); + } + + //give the tile a unique id based on its BBOX. + this.id = OpenLayers.Util.createUniqueID("Tile_"); + + OpenLayers.Util.extend(this, options); + + this.events = new OpenLayers.Events(this); + if (this.eventListeners instanceof Object) { + this.events.on(this.eventListeners); + } + }, + + /** + * Method: unload + * Call immediately before destroying if you are listening to tile + * events, so that counters are properly handled if tile is still + * loading at destroy-time. Will only fire an event if the tile is + * still loading. + */ + unload: function() { + if (this.isLoading) { + this.isLoading = false; + this.events.triggerEvent("unload"); + } + }, + + /** + * APIMethod: destroy + * Nullify references to prevent circular references and memory leaks. + */ + destroy:function() { + this.layer = null; + this.bounds = null; + this.size = null; + this.position = null; + + if (this.eventListeners) { + this.events.un(this.eventListeners); + } + this.events.destroy(); + this.eventListeners = null; + this.events = null; + }, + + /** + * Method: draw + * Clear whatever is currently in the tile, then return whether or not + * it should actually be re-drawn. This is an example implementation + * that can be overridden by subclasses. The minimum thing to do here + * is to call <clear> and return the result from <shouldDraw>. + * + * Parameters: + * force - {Boolean} If true, the tile will not be cleared and no beforedraw + * event will be fired. This is used for drawing tiles asynchronously + * after drawing has been cancelled by returning false from a beforedraw + * listener. + * + * Returns: + * {Boolean} Whether or not the tile should actually be drawn. Returns null + * if a beforedraw listener returned false. + */ + draw: function(force) { + if (!force) { + //clear tile's contents and mark as not drawn + this.clear(); + } + var draw = this.shouldDraw(); + if (draw && !force && this.events.triggerEvent("beforedraw") === false) { + draw = null; + } + return draw; + }, + + /** + * Method: shouldDraw + * Return whether or not the tile should actually be (re-)drawn. The only + * case where we *wouldn't* want to draw the tile is if the tile is outside + * its layer's maxExtent + * + * Returns: + * {Boolean} Whether or not the tile should actually be drawn. + */ + shouldDraw: function() { + var withinMaxExtent = false, + maxExtent = this.layer.maxExtent; + if (maxExtent) { + var map = this.layer.map; + var worldBounds = map.baseLayer.wrapDateLine && map.getMaxExtent(); + if (this.bounds.intersectsBounds(maxExtent, {inclusive: false, worldBounds: worldBounds})) { + withinMaxExtent = true; + } + } + + return withinMaxExtent || this.layer.displayOutsideMaxExtent; + }, + + /** + * Method: setBounds + * Sets the bounds on this instance + * + * Parameters: + * bounds {<OpenLayers.Bounds>} + */ + setBounds: function(bounds) { + bounds = bounds.clone(); + if (this.layer.map.baseLayer.wrapDateLine) { + var worldExtent = this.layer.map.getMaxExtent(), + tolerance = this.layer.map.getResolution(); + bounds = bounds.wrapDateLine(worldExtent, { + leftTolerance: tolerance, + rightTolerance: tolerance + }); + } + this.bounds = bounds; + }, + + /** + * Method: moveTo + * Reposition the tile. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * position - {<OpenLayers.Pixel>} + * redraw - {Boolean} Call draw method on tile after moving. + * Default is true + */ + moveTo: function (bounds, position, redraw) { + if (redraw == null) { + redraw = true; + } + + this.setBounds(bounds); + this.position = position.clone(); + if (redraw) { + this.draw(); + } + }, + + /** + * Method: clear + * Clear the tile of any bounds/position-related data so that it can + * be reused in a new location. + */ + clear: function(draw) { + // to be extended by subclasses + }, + + CLASS_NAME: "OpenLayers.Tile" +}); +/* ====================================================================== + OpenLayers/Tile/Image.js + ====================================================================== */ + +/* 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/Tile.js + * @requires OpenLayers/Animation.js + * @requires OpenLayers/Util.js + */ + +/** + * Class: OpenLayers.Tile.Image + * Instances of OpenLayers.Tile.Image are used to manage the image tiles + * used by various layers. Create a new image tile with the + * <OpenLayers.Tile.Image> constructor. + * + * Inherits from: + * - <OpenLayers.Tile> + */ +OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, { + + /** + * APIProperty: events + * {<OpenLayers.Events>} An events object that handles all + * events on the tile. + * + * Register a listener for a particular event with the following syntax: + * (code) + * tile.events.register(type, obj, listener); + * (end) + * + * Supported event types (in addition to the <OpenLayers.Tile> events): + * beforeload - Triggered before an image is prepared for loading, when the + * url for the image is known already. Listeners may call <setImage> on + * the tile instance. If they do so, that image will be used and no new + * one will be created. + */ + + /** + * APIProperty: url + * {String} The URL of the image being requested. No default. Filled in by + * layer.getURL() function. May be modified by loadstart listeners. + */ + url: null, + + /** + * Property: imgDiv + * {HTMLImageElement} The image for this tile. + */ + imgDiv: null, + + /** + * Property: frame + * {DOMElement} The image element is appended to the frame. Any gutter on + * the image will be hidden behind the frame. If no gutter is set, + * this will be null. + */ + frame: null, + + /** + * Property: imageReloadAttempts + * {Integer} Attempts to load the image. + */ + imageReloadAttempts: null, + + /** + * Property: layerAlphaHack + * {Boolean} True if the png alpha hack needs to be applied on the layer's div. + */ + layerAlphaHack: null, + + /** + * Property: asyncRequestId + * {Integer} ID of an request to see if request is still valid. This is a + * number which increments by 1 for each asynchronous request. + */ + asyncRequestId: null, + + /** + * APIProperty: maxGetUrlLength + * {Number} If set, requests that would result in GET urls with more + * characters than the number provided will be made using form-encoded + * HTTP POST. It is good practice to avoid urls that are longer than 2048 + * characters. + * + * Caution: + * Older versions of Gecko based browsers (e.g. Firefox < 3.5) and most + * Opera versions do not fully support this option. On all browsers, + * transition effects are not supported if POST requests are used. + */ + maxGetUrlLength: null, + + /** + * Property: canvasContext + * {CanvasRenderingContext2D} A canvas context associated with + * the tile image. + */ + canvasContext: null, + + /** + * APIProperty: crossOriginKeyword + * The value of the crossorigin keyword to use when loading images. This is + * only relevant when using <getCanvasContext> for tiles from remote + * origins and should be set to either 'anonymous' or 'use-credentials' + * for servers that send Access-Control-Allow-Origin headers with their + * tiles. + */ + crossOriginKeyword: null, + + /** TBD 3.0 - reorder the parameters to the init function to remove + * URL. the getUrl() function on the layer gets called on + * each draw(), so no need to specify it here. + */ + + /** + * Constructor: OpenLayers.Tile.Image + * Constructor for a new <OpenLayers.Tile.Image> instance. + * + * Parameters: + * layer - {<OpenLayers.Layer>} layer that the tile will go in. + * position - {<OpenLayers.Pixel>} + * bounds - {<OpenLayers.Bounds>} + * url - {<String>} Deprecated. Remove me in 3.0. + * size - {<OpenLayers.Size>} + * options - {Object} + */ + initialize: function(layer, position, bounds, url, size, options) { + OpenLayers.Tile.prototype.initialize.apply(this, arguments); + + this.url = url; //deprecated remove me + + this.layerAlphaHack = this.layer.alpha && OpenLayers.Util.alphaHack(); + + if (this.maxGetUrlLength != null || this.layer.gutter || this.layerAlphaHack) { + // only create frame if it's needed + this.frame = document.createElement("div"); + this.frame.style.position = "absolute"; + this.frame.style.overflow = "hidden"; + } + if (this.maxGetUrlLength != null) { + OpenLayers.Util.extend(this, OpenLayers.Tile.Image.IFrame); + } + }, + + /** + * APIMethod: destroy + * nullify references to prevent circular references and memory leaks + */ + destroy: function() { + if (this.imgDiv) { + this.clear(); + this.imgDiv = null; + this.frame = null; + } + // don't handle async requests any more + this.asyncRequestId = null; + OpenLayers.Tile.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: draw + * Check that a tile should be drawn, and draw it. + * + * Returns: + * {Boolean} Was a tile drawn? Or null if a beforedraw listener returned + * false. + */ + draw: function() { + var shouldDraw = OpenLayers.Tile.prototype.draw.apply(this, arguments); + if (shouldDraw) { + // The layer's reproject option is deprecated. + if (this.layer != this.layer.map.baseLayer && this.layer.reproject) { + // getBoundsFromBaseLayer is defined in deprecated.js. + this.bounds = this.getBoundsFromBaseLayer(this.position); + } + if (this.isLoading) { + //if we're already loading, send 'reload' instead of 'loadstart'. + this._loadEvent = "reload"; + } else { + this.isLoading = true; + this._loadEvent = "loadstart"; + } + this.renderTile(); + this.positionTile(); + } else if (shouldDraw === false) { + this.unload(); + } + return shouldDraw; + }, + + /** + * Method: renderTile + * Internal function to actually initialize the image tile, + * position it correctly, and set its url. + */ + renderTile: function() { + if (this.layer.async) { + // Asynchronous image requests call the asynchronous getURL method + // on the layer to fetch an image that covers 'this.bounds'. + var id = this.asyncRequestId = (this.asyncRequestId || 0) + 1; + this.layer.getURLasync(this.bounds, function(url) { + if (id == this.asyncRequestId) { + this.url = url; + this.initImage(); + } + }, this); + } else { + // synchronous image requests get the url immediately. + this.url = this.layer.getURL(this.bounds); + this.initImage(); + } + }, + + /** + * Method: positionTile + * Using the properties currenty set on the layer, position the tile correctly. + * This method is used both by the async and non-async versions of the Tile.Image + * code. + */ + positionTile: function() { + var style = this.getTile().style, + size = this.frame ? this.size : + this.layer.getImageSize(this.bounds), + ratio = 1; + if (this.layer instanceof OpenLayers.Layer.Grid) { + ratio = this.layer.getServerResolution() / this.layer.map.getResolution(); + } + style.left = this.position.x + "px"; + style.top = this.position.y + "px"; + style.width = Math.round(ratio * size.w) + "px"; + style.height = Math.round(ratio * size.h) + "px"; + }, + + /** + * Method: clear + * Remove the tile from the DOM, clear it of any image related data so that + * it can be reused in a new location. + */ + clear: function() { + OpenLayers.Tile.prototype.clear.apply(this, arguments); + var img = this.imgDiv; + if (img) { + var tile = this.getTile(); + if (tile.parentNode === this.layer.div) { + this.layer.div.removeChild(tile); + } + this.setImgSrc(); + if (this.layerAlphaHack === true) { + img.style.filter = ""; + } + OpenLayers.Element.removeClass(img, "olImageLoadError"); + } + this.canvasContext = null; + }, + + /** + * Method: getImage + * Returns or creates and returns the tile image. + */ + getImage: function() { + if (!this.imgDiv) { + this.imgDiv = OpenLayers.Tile.Image.IMAGE.cloneNode(false); + + var style = this.imgDiv.style; + if (this.frame) { + var left = 0, top = 0; + if (this.layer.gutter) { + left = this.layer.gutter / this.layer.tileSize.w * 100; + top = this.layer.gutter / this.layer.tileSize.h * 100; + } + style.left = -left + "%"; + style.top = -top + "%"; + style.width = (2 * left + 100) + "%"; + style.height = (2 * top + 100) + "%"; + } + style.visibility = "hidden"; + style.opacity = 0; + if (this.layer.opacity < 1) { + style.filter = 'alpha(opacity=' + + (this.layer.opacity * 100) + + ')'; + } + style.position = "absolute"; + if (this.layerAlphaHack) { + // move the image out of sight + style.paddingTop = style.height; + style.height = "0"; + style.width = "100%"; + } + if (this.frame) { + this.frame.appendChild(this.imgDiv); + } + } + + return this.imgDiv; + }, + + /** + * APIMethod: setImage + * Sets the image element for this tile. This method should only be called + * from beforeload listeners. + * + * Parameters + * img - {HTMLImageElement} The image to use for this tile. + */ + setImage: function(img) { + this.imgDiv = img; + }, + + /** + * Method: initImage + * Creates the content for the frame on the tile. + */ + initImage: function() { + if (!this.url && !this.imgDiv) { + // fast path out - if there is no tile url and no previous image + this.isLoading = false; + return; + } + this.events.triggerEvent('beforeload'); + this.layer.div.appendChild(this.getTile()); + this.events.triggerEvent(this._loadEvent); + var img = this.getImage(); + var src = img.getAttribute('src') || ''; + if (this.url && OpenLayers.Util.isEquivalentUrl(src, this.url)) { + this._loadTimeout = window.setTimeout( + OpenLayers.Function.bind(this.onImageLoad, this), 0 + ); + } else { + this.stopLoading(); + if (this.crossOriginKeyword) { + img.removeAttribute("crossorigin"); + } + OpenLayers.Event.observe(img, "load", + OpenLayers.Function.bind(this.onImageLoad, this) + ); + OpenLayers.Event.observe(img, "error", + OpenLayers.Function.bind(this.onImageError, this) + ); + this.imageReloadAttempts = 0; + this.setImgSrc(this.url); + } + }, + + /** + * Method: setImgSrc + * Sets the source for the tile image + * + * Parameters: + * url - {String} or undefined to hide the image + */ + setImgSrc: function(url) { + var img = this.imgDiv; + if (url) { + img.style.visibility = 'hidden'; + img.style.opacity = 0; + // don't set crossOrigin if the url is a data URL + if (this.crossOriginKeyword) { + if (url.substr(0, 5) !== 'data:') { + img.setAttribute("crossorigin", this.crossOriginKeyword); + } else { + img.removeAttribute("crossorigin"); + } + } + img.src = url; + } else { + // Remove reference to the image, and leave it to the browser's + // caching and garbage collection. + this.stopLoading(); + this.imgDiv = null; + if (img.parentNode) { + img.parentNode.removeChild(img); + } + } + }, + + /** + * Method: getTile + * Get the tile's markup. + * + * Returns: + * {DOMElement} The tile's markup + */ + getTile: function() { + return this.frame ? this.frame : this.getImage(); + }, + + /** + * Method: createBackBuffer + * Create a backbuffer for this tile. A backbuffer isn't exactly a clone + * of the tile's markup, because we want to avoid the reloading of the + * image. So we clone the frame, and steal the image from the tile. + * + * Returns: + * {DOMElement} The markup, or undefined if the tile has no image + * or if it's currently loading. + */ + createBackBuffer: function() { + if (!this.imgDiv || this.isLoading) { + return; + } + var backBuffer; + if (this.frame) { + backBuffer = this.frame.cloneNode(false); + backBuffer.appendChild(this.imgDiv); + } else { + backBuffer = this.imgDiv; + } + this.imgDiv = null; + return backBuffer; + }, + + /** + * Method: onImageLoad + * Handler for the image onload event + */ + onImageLoad: function() { + var img = this.imgDiv; + this.stopLoading(); + img.style.visibility = 'inherit'; + img.style.opacity = this.layer.opacity; + this.isLoading = false; + this.canvasContext = null; + this.events.triggerEvent("loadend"); + + if (this.layerAlphaHack === true) { + img.style.filter = + "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + + img.src + "', sizingMethod='scale')"; + } + }, + + /** + * Method: onImageError + * Handler for the image onerror event + */ + onImageError: function() { + var img = this.imgDiv; + if (img.src != null) { + this.imageReloadAttempts++; + if (this.imageReloadAttempts <= OpenLayers.IMAGE_RELOAD_ATTEMPTS) { + this.setImgSrc(this.layer.getURL(this.bounds)); + } else { + OpenLayers.Element.addClass(img, "olImageLoadError"); + this.events.triggerEvent("loaderror"); + this.onImageLoad(); + } + } + }, + + /** + * Method: stopLoading + * Stops a loading sequence so <onImageLoad> won't be executed. + */ + stopLoading: function() { + OpenLayers.Event.stopObservingElement(this.imgDiv); + window.clearTimeout(this._loadTimeout); + delete this._loadTimeout; + }, + + /** + * APIMethod: getCanvasContext + * Returns a canvas context associated with the tile image (with + * the image drawn on it). + * Returns undefined if the browser does not support canvas, if + * the tile has no image or if it's currently loading. + * + * The function returns a canvas context instance but the + * underlying canvas is still available in the 'canvas' property: + * (code) + * var context = tile.getCanvasContext(); + * if (context) { + * var data = context.canvas.toDataURL('image/jpeg'); + * } + * (end) + * + * Returns: + * {Boolean} + */ + getCanvasContext: function() { + if (OpenLayers.CANVAS_SUPPORTED && this.imgDiv && !this.isLoading) { + if (!this.canvasContext) { + var canvas = document.createElement("canvas"); + canvas.width = this.size.w; + canvas.height = this.size.h; + this.canvasContext = canvas.getContext("2d"); + this.canvasContext.drawImage(this.imgDiv, 0, 0); + } + return this.canvasContext; + } + }, + + CLASS_NAME: "OpenLayers.Tile.Image" + +}); + +/** + * Constant: OpenLayers.Tile.Image.IMAGE + * {HTMLImageElement} The image for a tile. + */ +OpenLayers.Tile.Image.IMAGE = (function() { + var img = new Image(); + img.className = "olTileImage"; + // avoid image gallery menu in IE6 + img.galleryImg = "no"; + return img; +}()); + +/* ====================================================================== + OpenLayers/Layer/Grid.js + ====================================================================== */ + +/* 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/Layer/HTTPRequest.js + * @requires OpenLayers/Tile/Image.js + */ + +/** + * Class: OpenLayers.Layer.Grid + * Base class for layers that use a lattice of tiles. Create a new grid + * layer with the <OpenLayers.Layer.Grid> constructor. + * + * Inherits from: + * - <OpenLayers.Layer.HTTPRequest> + */ +OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { + + /** + * APIProperty: tileSize + * {<OpenLayers.Size>} + */ + tileSize: null, + + /** + * Property: tileOriginCorner + * {String} If the <tileOrigin> property is not provided, the tile origin + * will be derived from the layer's <maxExtent>. The corner of the + * <maxExtent> used is determined by this property. Acceptable values + * are "tl" (top left), "tr" (top right), "bl" (bottom left), and "br" + * (bottom right). Default is "bl". + */ + tileOriginCorner: "bl", + + /** + * APIProperty: tileOrigin + * {<OpenLayers.LonLat>} Optional origin for aligning the grid of tiles. + * If provided, requests for tiles at all resolutions will be aligned + * with this location (no tiles shall overlap this location). If + * not provided, the grid of tiles will be aligned with the layer's + * <maxExtent>. Default is ``null``. + */ + tileOrigin: null, + + /** APIProperty: tileOptions + * {Object} optional configuration options for <OpenLayers.Tile> instances + * created by this Layer, if supported by the tile class. + */ + tileOptions: null, + + /** + * APIProperty: tileClass + * {<OpenLayers.Tile>} The tile class to use for this layer. + * Defaults is OpenLayers.Tile.Image. + */ + tileClass: OpenLayers.Tile.Image, + + /** + * Property: grid + * {Array(Array(<OpenLayers.Tile>))} This is an array of rows, each row is + * an array of tiles. + */ + grid: null, + + /** + * APIProperty: singleTile + * {Boolean} Moves the layer into single-tile mode, meaning that one tile + * will be loaded. The tile's size will be determined by the 'ratio' + * property. When the tile is dragged such that it does not cover the + * entire viewport, it is reloaded. + */ + singleTile: false, + + /** APIProperty: ratio + * {Float} Used only when in single-tile mode, this specifies the + * ratio of the size of the single tile to the size of the map. + * Default value is 1.5. + */ + ratio: 1.5, + + /** + * APIProperty: buffer + * {Integer} Used only when in gridded mode, this specifies the number of + * extra rows and colums of tiles on each side which will + * surround the minimum grid tiles to cover the map. + * For very slow loading layers, a larger value may increase + * performance somewhat when dragging, but will increase bandwidth + * use significantly. + */ + buffer: 0, + + /** + * APIProperty: transitionEffect + * {String} The transition effect to use when the map is zoomed. + * Two posible values: + * + * "resize" - Existing tiles are resized on zoom to provide a visual + * effect of the zoom having taken place immediately. As the + * new tiles become available, they are drawn on top of the + * resized tiles (this is the default setting). + * "map-resize" - Existing tiles are resized on zoom and placed below the + * base layer. New tiles for the base layer will cover existing tiles. + * This setting is recommended when having an overlay duplicated during + * the transition is undesirable (e.g. street labels or big transparent + * fills). + * null - No transition effect. + * + * Using "resize" on non-opaque layers can cause undesired visual + * effects. Set transitionEffect to null in this case. + */ + transitionEffect: "resize", + + /** + * APIProperty: numLoadingTiles + * {Integer} How many tiles are still loading? + */ + numLoadingTiles: 0, + + /** + * Property: serverResolutions + * {Array(Number}} This property is documented in subclasses as + * an API property. + */ + serverResolutions: null, + + /** + * Property: loading + * {Boolean} Indicates if tiles are being loaded. + */ + loading: false, + + /** + * Property: backBuffer + * {DOMElement} The back buffer. + */ + backBuffer: null, + + /** + * Property: gridResolution + * {Number} The resolution of the current grid. Used for backbuffer and + * client zoom. This property is updated every time the grid is + * initialized. + */ + gridResolution: null, + + /** + * Property: backBufferResolution + * {Number} The resolution of the current back buffer. This property is + * updated each time a back buffer is created. + */ + backBufferResolution: null, + + /** + * Property: backBufferLonLat + * {Object} The top-left corner of the current back buffer. Includes lon + * and lat properties. This object is updated each time a back buffer + * is created. + */ + backBufferLonLat: null, + + /** + * Property: backBufferTimerId + * {Number} The id of the back buffer timer. This timer is used to + * delay the removal of the back buffer, thereby preventing + * flash effects caused by tile animation. + */ + backBufferTimerId: null, + + /** + * APIProperty: removeBackBufferDelay + * {Number} Delay for removing the backbuffer when all tiles have finished + * loading. Can be set to 0 when no css opacity transitions for the + * olTileImage class are used. Default is 0 for <singleTile> layers, + * 2500 for tiled layers. See <className> for more information on + * tile animation. + */ + removeBackBufferDelay: null, + + /** + * APIProperty: className + * {String} Name of the class added to the layer div. If not set in the + * options passed to the constructor then className defaults to + * "olLayerGridSingleTile" for single tile layers (see <singleTile>), + * and "olLayerGrid" for non single tile layers. + * + * Note: + * + * The displaying of tiles is not animated by default for single tile + * layers - OpenLayers' default theme (style.css) includes this: + * (code) + * .olLayerGrid .olTileImage { + * -webkit-transition: opacity 0.2s linear; + * -moz-transition: opacity 0.2s linear; + * -o-transition: opacity 0.2s linear; + * transition: opacity 0.2s linear; + * } + * (end) + * To animate tile displaying for any grid layer the following + * CSS rule can be used: + * (code) + * .olTileImage { + * -webkit-transition: opacity 0.2s linear; + * -moz-transition: opacity 0.2s linear; + * -o-transition: opacity 0.2s linear; + * transition: opacity 0.2s linear; + * } + * (end) + * In that case, to avoid flash effects, <removeBackBufferDelay> + * should not be zero. + */ + className: null, + + /** + * Register a listener for a particular event with the following syntax: + * (code) + * layer.events.register(type, obj, listener); + * (end) + * + * Listeners will be called with a reference to an event object. The + * properties of this event depends on exactly what happened. + * + * All event objects have at least the following properties: + * object - {Object} A reference to layer.events.object. + * element - {DOMElement} A reference to layer.events.element. + * + * Supported event types: + * addtile - Triggered when a tile is added to this layer. Listeners receive + * an object as first argument, which has a tile property that + * references the tile that has been added. + * tileloadstart - Triggered when a tile starts loading. Listeners receive + * an object as first argument, which has a tile property that + * references the tile that starts loading. + * tileloaded - Triggered when each new tile is + * loaded, as a means of progress update to listeners. + * listeners can access 'numLoadingTiles' if they wish to keep + * track of the loading progress. Listeners are called with an object + * with a 'tile' property as first argument, making the loaded tile + * available to the listener, and an 'aborted' property, which will be + * true when loading was aborted and no tile data is available. + * tileerror - Triggered before the tileloaded event (i.e. when the tile is + * still hidden) if a tile failed to load. Listeners receive an object + * as first argument, which has a tile property that references the + * tile that could not be loaded. + * retile - Triggered when the layer recreates its tile grid. + */ + + /** + * Property: gridLayout + * {Object} Object containing properties tilelon, tilelat, startcol, + * startrow + */ + gridLayout: null, + + /** + * Property: rowSign + * {Number} 1 for grids starting at the top, -1 for grids starting at the + * bottom. This is used for several grid index and offset calculations. + */ + rowSign: null, + + /** + * Property: transitionendEvents + * {Array} Event names for transitionend + */ + transitionendEvents: [ + 'transitionend', 'webkitTransitionEnd', 'otransitionend', + 'oTransitionEnd' + ], + + /** + * Constructor: OpenLayers.Layer.Grid + * Create a new grid layer + * + * Parameters: + * name - {String} + * url - {String} + * params - {Object} + * options - {Object} Hashtable of extra options to tag onto the layer + */ + initialize: function(name, url, params, options) { + OpenLayers.Layer.HTTPRequest.prototype.initialize.apply(this, + arguments); + this.grid = []; + this._removeBackBuffer = OpenLayers.Function.bind(this.removeBackBuffer, this); + + this.initProperties(); + + this.rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + }, + + /** + * Method: initProperties + * Set any properties that depend on the value of singleTile. + * Currently sets removeBackBufferDelay and className + */ + initProperties: function() { + if (this.options.removeBackBufferDelay === undefined) { + this.removeBackBufferDelay = this.singleTile ? 0 : 2500; + } + + if (this.options.className === undefined) { + this.className = this.singleTile ? 'olLayerGridSingleTile' : + 'olLayerGrid'; + } + }, + + /** + * Method: setMap + * + * Parameters: + * map - {<OpenLayers.Map>} The map. + */ + setMap: function(map) { + OpenLayers.Layer.HTTPRequest.prototype.setMap.call(this, map); + OpenLayers.Element.addClass(this.div, this.className); + }, + + /** + * Method: removeMap + * Called when the layer is removed from the map. + * + * Parameters: + * map - {<OpenLayers.Map>} The map. + */ + removeMap: function(map) { + this.removeBackBuffer(); + }, + + /** + * APIMethod: destroy + * Deconstruct the layer and clear the grid. + */ + destroy: function() { + this.removeBackBuffer(); + this.clearGrid(); + + this.grid = null; + this.tileSize = null; + OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this, arguments); + }, + + /** + * APIMethod: mergeNewParams + * Refetches tiles with new params merged, keeping a backbuffer. Each + * loading new tile will have a css class of '.olTileReplacing'. If a + * stylesheet applies a 'display: none' style to that class, any fade-in + * transition will not apply, and backbuffers for each tile will be removed + * as soon as the tile is loaded. + * + * Parameters: + * newParams - {Object} + * + * Returns: + * redrawn: {Boolean} whether the layer was actually redrawn. + */ + + /** + * Method: clearGrid + * Go through and remove all tiles from the grid, calling + * destroy() on each of them to kill circular references + */ + clearGrid:function() { + if (this.grid) { + for(var iRow=0, len=this.grid.length; iRow<len; iRow++) { + var row = this.grid[iRow]; + for(var iCol=0, clen=row.length; iCol<clen; iCol++) { + var tile = row[iCol]; + this.destroyTile(tile); + } + } + this.grid = []; + this.gridResolution = null; + this.gridLayout = null; + } + }, + + /** + * APIMethod: addOptions + * + * Parameters: + * newOptions - {Object} + * reinitialize - {Boolean} If set to true, and if resolution options of the + * current baseLayer were changed, the map will be recentered to make + * sure that it is displayed with a valid resolution, and a + * changebaselayer event will be triggered. + */ + addOptions: function (newOptions, reinitialize) { + var singleTileChanged = newOptions.singleTile !== undefined && + newOptions.singleTile !== this.singleTile; + OpenLayers.Layer.HTTPRequest.prototype.addOptions.apply(this, arguments); + if (this.map && singleTileChanged) { + this.initProperties(); + this.clearGrid(); + this.tileSize = this.options.tileSize; + this.setTileSize(); + this.moveTo(null, true); + } + }, + + /** + * APIMethod: clone + * Create a clone of this layer + * + * Parameters: + * obj - {Object} Is this ever used? + * + * Returns: + * {<OpenLayers.Layer.Grid>} An exact clone of this OpenLayers.Layer.Grid + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer.Grid(this.name, + this.url, + this.params, + this.getOptions()); + } + + //get all additions from superclasses + obj = OpenLayers.Layer.HTTPRequest.prototype.clone.apply(this, [obj]); + + // copy/set any non-init, non-simple values here + if (this.tileSize != null) { + obj.tileSize = this.tileSize.clone(); + } + + // we do not want to copy reference to grid, so we make a new array + obj.grid = []; + obj.gridResolution = null; + // same for backbuffer + obj.backBuffer = null; + obj.backBufferTimerId = null; + obj.loading = false; + obj.numLoadingTiles = 0; + + return obj; + }, + + /** + * Method: moveTo + * This function is called whenever the map is moved. All the moving + * of actual 'tiles' is done by the map, but moveTo's role is to accept + * a bounds and make sure the data that that bounds requires is pre-loaded. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * zoomChanged - {Boolean} + * dragging - {Boolean} + */ + moveTo:function(bounds, zoomChanged, dragging) { + + OpenLayers.Layer.HTTPRequest.prototype.moveTo.apply(this, arguments); + + bounds = bounds || this.map.getExtent(); + + if (bounds != null) { + + // if grid is empty or zoom has changed, we *must* re-tile + var forceReTile = !this.grid.length || zoomChanged; + + // total bounds of the tiles + var tilesBounds = this.getTilesBounds(); + + // the new map resolution + var resolution = this.map.getResolution(); + + // the server-supported resolution for the new map resolution + var serverResolution = this.getServerResolution(resolution); + + if (this.singleTile) { + + // We want to redraw whenever even the slightest part of the + // current bounds is not contained by our tile. + // (thus, we do not specify partial -- its default is false) + + if ( forceReTile || + (!dragging && !tilesBounds.containsBounds(bounds))) { + + // In single tile mode with no transition effect, we insert + // a non-scaled backbuffer when the layer is moved. But if + // a zoom occurs right after a move, i.e. before the new + // image is received, we need to remove the backbuffer, or + // an ill-positioned image will be visible during the zoom + // transition. + + if(zoomChanged && this.transitionEffect !== 'resize') { + this.removeBackBuffer(); + } + + if(!zoomChanged || this.transitionEffect === 'resize') { + this.applyBackBuffer(resolution); + } + + this.initSingleTile(bounds); + } + } else { + + // if the bounds have changed such that they are not even + // *partially* contained by our tiles (e.g. when user has + // programmatically panned to the other side of the earth on + // zoom level 18), then moveGriddedTiles could potentially have + // to run through thousands of cycles, so we want to reTile + // instead (thus, partial true). + forceReTile = forceReTile || + !tilesBounds.intersectsBounds(bounds, { + worldBounds: this.map.baseLayer.wrapDateLine && + this.map.getMaxExtent() + }); + + if(forceReTile) { + if(zoomChanged && (this.transitionEffect === 'resize' || + this.gridResolution === resolution)) { + this.applyBackBuffer(resolution); + } + this.initGriddedTiles(bounds); + } else { + this.moveGriddedTiles(); + } + } + } + }, + + /** + * Method: getTileData + * Given a map location, retrieve a tile and the pixel offset within that + * tile corresponding to the location. If there is not an existing + * tile in the grid that covers the given location, null will be + * returned. + * + * Parameters: + * loc - {<OpenLayers.LonLat>} map location + * + * Returns: + * {Object} Object with the following properties: tile ({<OpenLayers.Tile>}), + * i ({Number} x-pixel offset from top left), and j ({Integer} y-pixel + * offset from top left). + */ + getTileData: function(loc) { + var data = null, + x = loc.lon, + y = loc.lat, + numRows = this.grid.length; + + if (this.map && numRows) { + var res = this.map.getResolution(), + tileWidth = this.tileSize.w, + tileHeight = this.tileSize.h, + bounds = this.grid[0][0].bounds, + left = bounds.left, + top = bounds.top; + + if (x < left) { + // deal with multiple worlds + if (this.map.baseLayer.wrapDateLine) { + var worldWidth = this.map.getMaxExtent().getWidth(); + var worldsAway = Math.ceil((left - x) / worldWidth); + x += worldWidth * worldsAway; + } + } + // tile distance to location (fractional number of tiles); + var dtx = (x - left) / (res * tileWidth); + var dty = (top - y) / (res * tileHeight); + // index of tile in grid + var col = Math.floor(dtx); + var row = Math.floor(dty); + if (row >= 0 && row < numRows) { + var tile = this.grid[row][col]; + if (tile) { + data = { + tile: tile, + // pixel index within tile + i: Math.floor((dtx - col) * tileWidth), + j: Math.floor((dty - row) * tileHeight) + }; + } + } + } + return data; + }, + + /** + * Method: destroyTile + * + * Parameters: + * tile - {<OpenLayers.Tile>} + */ + destroyTile: function(tile) { + this.removeTileMonitoringHooks(tile); + tile.destroy(); + }, + + /** + * Method: getServerResolution + * Return the closest server-supported resolution. + * + * Parameters: + * resolution - {Number} The base resolution. If undefined the + * map resolution is used. + * + * Returns: + * {Number} The closest server resolution value. + */ + getServerResolution: function(resolution) { + var distance = Number.POSITIVE_INFINITY; + resolution = resolution || this.map.getResolution(); + if(this.serverResolutions && + OpenLayers.Util.indexOf(this.serverResolutions, resolution) === -1) { + var i, newDistance, newResolution, serverResolution; + for(i=this.serverResolutions.length-1; i>= 0; i--) { + newResolution = this.serverResolutions[i]; + newDistance = Math.abs(newResolution - resolution); + if (newDistance > distance) { + break; + } + distance = newDistance; + serverResolution = newResolution; + } + resolution = serverResolution; + } + return resolution; + }, + + /** + * Method: getServerZoom + * Return the zoom value corresponding to the best matching server + * resolution, taking into account <serverResolutions> and <zoomOffset>. + * + * Returns: + * {Number} The closest server supported zoom. This is not the map zoom + * level, but an index of the server's resolutions array. + */ + getServerZoom: function() { + var resolution = this.getServerResolution(); + return this.serverResolutions ? + OpenLayers.Util.indexOf(this.serverResolutions, resolution) : + this.map.getZoomForResolution(resolution) + (this.zoomOffset || 0); + }, + + /** + * Method: applyBackBuffer + * Create, insert, scale and position a back buffer for the layer. + * + * Parameters: + * resolution - {Number} The resolution to transition to. + */ + applyBackBuffer: function(resolution) { + if(this.backBufferTimerId !== null) { + this.removeBackBuffer(); + } + var backBuffer = this.backBuffer; + if(!backBuffer) { + backBuffer = this.createBackBuffer(); + if(!backBuffer) { + return; + } + if (resolution === this.gridResolution) { + this.div.insertBefore(backBuffer, this.div.firstChild); + } else { + this.map.baseLayer.div.parentNode.insertBefore(backBuffer, this.map.baseLayer.div); + } + this.backBuffer = backBuffer; + + // set some information in the instance for subsequent + // calls to applyBackBuffer where the same back buffer + // is reused + var topLeftTileBounds = this.grid[0][0].bounds; + this.backBufferLonLat = { + lon: topLeftTileBounds.left, + lat: topLeftTileBounds.top + }; + this.backBufferResolution = this.gridResolution; + } + + var ratio = this.backBufferResolution / resolution; + + // scale the tiles inside the back buffer + var tiles = backBuffer.childNodes, tile; + for (var i=tiles.length-1; i>=0; --i) { + tile = tiles[i]; + tile.style.top = ((ratio * tile._i * tile._h) | 0) + 'px'; + tile.style.left = ((ratio * tile._j * tile._w) | 0) + 'px'; + tile.style.width = Math.round(ratio * tile._w) + 'px'; + tile.style.height = Math.round(ratio * tile._h) + 'px'; + } + + // and position it (based on the grid's top-left corner) + var position = this.getViewPortPxFromLonLat( + this.backBufferLonLat, resolution); + var leftOffset = this.map.layerContainerOriginPx.x; + var topOffset = this.map.layerContainerOriginPx.y; + backBuffer.style.left = Math.round(position.x - leftOffset) + 'px'; + backBuffer.style.top = Math.round(position.y - topOffset) + 'px'; + }, + + /** + * Method: createBackBuffer + * Create a back buffer. + * + * Returns: + * {DOMElement} The DOM element for the back buffer, undefined if the + * grid isn't initialized yet. + */ + createBackBuffer: function() { + var backBuffer; + if(this.grid.length > 0) { + backBuffer = document.createElement('div'); + backBuffer.id = this.div.id + '_bb'; + backBuffer.className = 'olBackBuffer'; + backBuffer.style.position = 'absolute'; + var map = this.map; + backBuffer.style.zIndex = this.transitionEffect === 'resize' ? + this.getZIndex() - 1 : + // 'map-resize': + map.Z_INDEX_BASE.BaseLayer - + (map.getNumLayers() - map.getLayerIndex(this)); + for(var i=0, lenI=this.grid.length; i<lenI; i++) { + for(var j=0, lenJ=this.grid[i].length; j<lenJ; j++) { + var tile = this.grid[i][j], + markup = this.grid[i][j].createBackBuffer(); + if (markup) { + markup._i = i; + markup._j = j; + markup._w = tile.size.w; + markup._h = tile.size.h; + markup.id = tile.id + '_bb'; + backBuffer.appendChild(markup); + } + } + } + } + return backBuffer; + }, + + /** + * Method: removeBackBuffer + * Remove back buffer from DOM. + */ + removeBackBuffer: function() { + if (this._transitionElement) { + for (var i=this.transitionendEvents.length-1; i>=0; --i) { + OpenLayers.Event.stopObserving(this._transitionElement, + this.transitionendEvents[i], this._removeBackBuffer); + } + delete this._transitionElement; + } + if(this.backBuffer) { + if (this.backBuffer.parentNode) { + this.backBuffer.parentNode.removeChild(this.backBuffer); + } + this.backBuffer = null; + this.backBufferResolution = null; + if(this.backBufferTimerId !== null) { + window.clearTimeout(this.backBufferTimerId); + this.backBufferTimerId = null; + } + } + }, + + /** + * Method: moveByPx + * Move the layer based on pixel vector. + * + * Parameters: + * dx - {Number} + * dy - {Number} + */ + moveByPx: function(dx, dy) { + if (!this.singleTile) { + this.moveGriddedTiles(); + } + }, + + /** + * APIMethod: setTileSize + * Check if we are in singleTile mode and if so, set the size as a ratio + * of the map size (as specified by the layer's 'ratio' property). + * + * Parameters: + * size - {<OpenLayers.Size>} + */ + setTileSize: function(size) { + if (this.singleTile) { + size = this.map.getSize(); + size.h = parseInt(size.h * this.ratio, 10); + size.w = parseInt(size.w * this.ratio, 10); + } + OpenLayers.Layer.HTTPRequest.prototype.setTileSize.apply(this, [size]); + }, + + /** + * APIMethod: getTilesBounds + * Return the bounds of the tile grid. + * + * Returns: + * {<OpenLayers.Bounds>} A Bounds object representing the bounds of all the + * currently loaded tiles (including those partially or not at all seen + * onscreen). + */ + getTilesBounds: function() { + var bounds = null; + + var length = this.grid.length; + if (length) { + var bottomLeftTileBounds = this.grid[length - 1][0].bounds, + width = this.grid[0].length * bottomLeftTileBounds.getWidth(), + height = this.grid.length * bottomLeftTileBounds.getHeight(); + + bounds = new OpenLayers.Bounds(bottomLeftTileBounds.left, + bottomLeftTileBounds.bottom, + bottomLeftTileBounds.left + width, + bottomLeftTileBounds.bottom + height); + } + return bounds; + }, + + /** + * Method: initSingleTile + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + */ + initSingleTile: function(bounds) { + this.events.triggerEvent("retile"); + + //determine new tile bounds + var center = bounds.getCenterLonLat(); + var tileWidth = bounds.getWidth() * this.ratio; + var tileHeight = bounds.getHeight() * this.ratio; + + var tileBounds = + new OpenLayers.Bounds(center.lon - (tileWidth/2), + center.lat - (tileHeight/2), + center.lon + (tileWidth/2), + center.lat + (tileHeight/2)); + + var px = this.map.getLayerPxFromLonLat({ + lon: tileBounds.left, + lat: tileBounds.top + }); + + if (!this.grid.length) { + this.grid[0] = []; + } + + var tile = this.grid[0][0]; + if (!tile) { + tile = this.addTile(tileBounds, px); + + this.addTileMonitoringHooks(tile); + tile.draw(); + this.grid[0][0] = tile; + } else { + tile.moveTo(tileBounds, px); + } + + //remove all but our single tile + this.removeExcessTiles(1,1); + + // store the resolution of the grid + this.gridResolution = this.getServerResolution(); + }, + + /** + * Method: calculateGridLayout + * Generate parameters for the grid layout. + * + * Parameters: + * bounds - {<OpenLayers.Bound>|Object} OpenLayers.Bounds or an + * object with a 'left' and 'top' properties. + * origin - {<OpenLayers.LonLat>|Object} OpenLayers.LonLat or an + * object with a 'lon' and 'lat' properties. + * resolution - {Number} + * + * Returns: + * {Object} Object containing properties tilelon, tilelat, startcol, + * startrow + */ + calculateGridLayout: function(bounds, origin, resolution) { + var tilelon = resolution * this.tileSize.w; + var tilelat = resolution * this.tileSize.h; + + var offsetlon = bounds.left - origin.lon; + var tilecol = Math.floor(offsetlon/tilelon) - this.buffer; + + var rowSign = this.rowSign; + + var offsetlat = rowSign * (origin.lat - bounds.top + tilelat); + var tilerow = Math[~rowSign ? 'floor' : 'ceil'](offsetlat/tilelat) - this.buffer * rowSign; + + return { + tilelon: tilelon, tilelat: tilelat, + startcol: tilecol, startrow: tilerow + }; + + }, + + /** + * Method: getTileOrigin + * Determine the origin for aligning the grid of tiles. If a <tileOrigin> + * property is supplied, that will be returned. Otherwise, the origin + * will be derived from the layer's <maxExtent> property. In this case, + * the tile origin will be the corner of the <maxExtent> given by the + * <tileOriginCorner> property. + * + * Returns: + * {<OpenLayers.LonLat>} The tile origin. + */ + getTileOrigin: function() { + var origin = this.tileOrigin; + if (!origin) { + var extent = this.getMaxExtent(); + var edges = ({ + "tl": ["left", "top"], + "tr": ["right", "top"], + "bl": ["left", "bottom"], + "br": ["right", "bottom"] + })[this.tileOriginCorner]; + origin = new OpenLayers.LonLat(extent[edges[0]], extent[edges[1]]); + } + return origin; + }, + + /** + * Method: getTileBoundsForGridIndex + * + * Parameters: + * row - {Number} The row of the grid + * col - {Number} The column of the grid + * + * Returns: + * {<OpenLayers.Bounds>} The bounds for the tile at (row, col) + */ + getTileBoundsForGridIndex: function(row, col) { + var origin = this.getTileOrigin(); + var tileLayout = this.gridLayout; + var tilelon = tileLayout.tilelon; + var tilelat = tileLayout.tilelat; + var startcol = tileLayout.startcol; + var startrow = tileLayout.startrow; + var rowSign = this.rowSign; + return new OpenLayers.Bounds( + origin.lon + (startcol + col) * tilelon, + origin.lat - (startrow + row * rowSign) * tilelat * rowSign, + origin.lon + (startcol + col + 1) * tilelon, + origin.lat - (startrow + (row - 1) * rowSign) * tilelat * rowSign + ); + }, + + /** + * Method: initGriddedTiles + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + */ + initGriddedTiles:function(bounds) { + this.events.triggerEvent("retile"); + + // work out mininum number of rows and columns; this is the number of + // tiles required to cover the viewport plus at least one for panning + + var viewSize = this.map.getSize(); + + var origin = this.getTileOrigin(); + var resolution = this.map.getResolution(), + serverResolution = this.getServerResolution(), + ratio = resolution / serverResolution, + tileSize = { + w: this.tileSize.w / ratio, + h: this.tileSize.h / ratio + }; + + var minRows = Math.ceil(viewSize.h/tileSize.h) + + 2 * this.buffer + 1; + var minCols = Math.ceil(viewSize.w/tileSize.w) + + 2 * this.buffer + 1; + + var tileLayout = this.calculateGridLayout(bounds, origin, serverResolution); + this.gridLayout = tileLayout; + + var tilelon = tileLayout.tilelon; + var tilelat = tileLayout.tilelat; + + var layerContainerDivLeft = this.map.layerContainerOriginPx.x; + var layerContainerDivTop = this.map.layerContainerOriginPx.y; + + var tileBounds = this.getTileBoundsForGridIndex(0, 0); + var startPx = this.map.getViewPortPxFromLonLat( + new OpenLayers.LonLat(tileBounds.left, tileBounds.top) + ); + startPx.x = Math.round(startPx.x) - layerContainerDivLeft; + startPx.y = Math.round(startPx.y) - layerContainerDivTop; + + var tileData = [], center = this.map.getCenter(); + + var rowidx = 0; + do { + var row = this.grid[rowidx]; + if (!row) { + row = []; + this.grid.push(row); + } + + var colidx = 0; + do { + tileBounds = this.getTileBoundsForGridIndex(rowidx, colidx); + var px = startPx.clone(); + px.x = px.x + colidx * Math.round(tileSize.w); + px.y = px.y + rowidx * Math.round(tileSize.h); + var tile = row[colidx]; + if (!tile) { + tile = this.addTile(tileBounds, px); + this.addTileMonitoringHooks(tile); + row.push(tile); + } else { + tile.moveTo(tileBounds, px, false); + } + var tileCenter = tileBounds.getCenterLonLat(); + tileData.push({ + tile: tile, + distance: Math.pow(tileCenter.lon - center.lon, 2) + + Math.pow(tileCenter.lat - center.lat, 2) + }); + + colidx += 1; + } while ((tileBounds.right <= bounds.right + tilelon * this.buffer) + || colidx < minCols); + + rowidx += 1; + } while((tileBounds.bottom >= bounds.bottom - tilelat * this.buffer) + || rowidx < minRows); + + //shave off exceess rows and colums + this.removeExcessTiles(rowidx, colidx); + + var resolution = this.getServerResolution(); + // store the resolution of the grid + this.gridResolution = resolution; + + //now actually draw the tiles + tileData.sort(function(a, b) { + return a.distance - b.distance; + }); + for (var i=0, ii=tileData.length; i<ii; ++i) { + tileData[i].tile.draw(); + } + }, + + /** + * Method: getMaxExtent + * Get this layer's maximum extent. (Implemented as a getter for + * potential specific implementations in sub-classes.) + * + * Returns: + * {<OpenLayers.Bounds>} + */ + getMaxExtent: function() { + return this.maxExtent; + }, + + /** + * APIMethod: addTile + * Create a tile, initialize it, and add it to the layer div. + * + * Parameters + * bounds - {<OpenLayers.Bounds>} + * position - {<OpenLayers.Pixel>} + * + * Returns: + * {<OpenLayers.Tile>} The added OpenLayers.Tile + */ + addTile: function(bounds, position) { + var tile = new this.tileClass( + this, position, bounds, null, this.tileSize, this.tileOptions + ); + this.events.triggerEvent("addtile", {tile: tile}); + return tile; + }, + + /** + * Method: addTileMonitoringHooks + * This function takes a tile as input and adds the appropriate hooks to + * the tile so that the layer can keep track of the loading tiles. + * + * Parameters: + * tile - {<OpenLayers.Tile>} + */ + addTileMonitoringHooks: function(tile) { + + var replacingCls = 'olTileReplacing'; + + tile.onLoadStart = function() { + //if that was first tile then trigger a 'loadstart' on the layer + if (this.loading === false) { + this.loading = true; + this.events.triggerEvent("loadstart"); + } + this.events.triggerEvent("tileloadstart", {tile: tile}); + this.numLoadingTiles++; + if (!this.singleTile && this.backBuffer && this.gridResolution === this.backBufferResolution) { + OpenLayers.Element.addClass(tile.getTile(), replacingCls); + } + }; + + tile.onLoadEnd = function(evt) { + this.numLoadingTiles--; + var aborted = evt.type === 'unload'; + this.events.triggerEvent("tileloaded", { + tile: tile, + aborted: aborted + }); + if (!this.singleTile && !aborted && this.backBuffer && this.gridResolution === this.backBufferResolution) { + var tileDiv = tile.getTile(); + if (OpenLayers.Element.getStyle(tileDiv, 'display') === 'none') { + var bufferTile = document.getElementById(tile.id + '_bb'); + if (bufferTile) { + bufferTile.parentNode.removeChild(bufferTile); + } + } + OpenLayers.Element.removeClass(tileDiv, replacingCls); + } + //if that was the last tile, then trigger a 'loadend' on the layer + if (this.numLoadingTiles === 0) { + if (this.backBuffer) { + if (this.backBuffer.childNodes.length === 0) { + // no tiles transitioning, remove immediately + this.removeBackBuffer(); + } else { + // wait until transition has ended or delay has passed + this._transitionElement = aborted ? + this.div.lastChild : tile.imgDiv; + var transitionendEvents = this.transitionendEvents; + for (var i=transitionendEvents.length-1; i>=0; --i) { + OpenLayers.Event.observe(this._transitionElement, + transitionendEvents[i], + this._removeBackBuffer); + } + // the removal of the back buffer is delayed to prevent + // flash effects due to the animation of tile displaying + this.backBufferTimerId = window.setTimeout( + this._removeBackBuffer, this.removeBackBufferDelay + ); + } + } + this.loading = false; + this.events.triggerEvent("loadend"); + } + }; + + tile.onLoadError = function() { + this.events.triggerEvent("tileerror", {tile: tile}); + }; + + tile.events.on({ + "loadstart": tile.onLoadStart, + "loadend": tile.onLoadEnd, + "unload": tile.onLoadEnd, + "loaderror": tile.onLoadError, + scope: this + }); + }, + + /** + * Method: removeTileMonitoringHooks + * This function takes a tile as input and removes the tile hooks + * that were added in addTileMonitoringHooks() + * + * Parameters: + * tile - {<OpenLayers.Tile>} + */ + removeTileMonitoringHooks: function(tile) { + tile.unload(); + tile.events.un({ + "loadstart": tile.onLoadStart, + "loadend": tile.onLoadEnd, + "unload": tile.onLoadEnd, + "loaderror": tile.onLoadError, + scope: this + }); + }, + + /** + * Method: moveGriddedTiles + */ + moveGriddedTiles: function() { + var buffer = this.buffer + 1; + while(true) { + var tlTile = this.grid[0][0]; + var tlViewPort = { + x: tlTile.position.x + + this.map.layerContainerOriginPx.x, + y: tlTile.position.y + + this.map.layerContainerOriginPx.y + }; + var ratio = this.getServerResolution() / this.map.getResolution(); + var tileSize = { + w: Math.round(this.tileSize.w * ratio), + h: Math.round(this.tileSize.h * ratio) + }; + if (tlViewPort.x > -tileSize.w * (buffer - 1)) { + this.shiftColumn(true, tileSize); + } else if (tlViewPort.x < -tileSize.w * buffer) { + this.shiftColumn(false, tileSize); + } else if (tlViewPort.y > -tileSize.h * (buffer - 1)) { + this.shiftRow(true, tileSize); + } else if (tlViewPort.y < -tileSize.h * buffer) { + this.shiftRow(false, tileSize); + } else { + break; + } + } + }, + + /** + * Method: shiftRow + * Shifty grid work + * + * Parameters: + * prepend - {Boolean} if true, prepend to beginning. + * if false, then append to end + * tileSize - {Object} rendered tile size; object with w and h properties + */ + shiftRow: function(prepend, tileSize) { + var grid = this.grid; + var rowIndex = prepend ? 0 : (grid.length - 1); + var sign = prepend ? -1 : 1; + var rowSign = this.rowSign; + var tileLayout = this.gridLayout; + tileLayout.startrow += sign * rowSign; + + var modelRow = grid[rowIndex]; + var row = grid[prepend ? 'pop' : 'shift'](); + for (var i=0, len=row.length; i<len; i++) { + var tile = row[i]; + var position = modelRow[i].position.clone(); + position.y += tileSize.h * sign; + tile.moveTo(this.getTileBoundsForGridIndex(rowIndex, i), position); + } + grid[prepend ? 'unshift' : 'push'](row); + }, + + /** + * Method: shiftColumn + * Shift grid work in the other dimension + * + * Parameters: + * prepend - {Boolean} if true, prepend to beginning. + * if false, then append to end + * tileSize - {Object} rendered tile size; object with w and h properties + */ + shiftColumn: function(prepend, tileSize) { + var grid = this.grid; + var colIndex = prepend ? 0 : (grid[0].length - 1); + var sign = prepend ? -1 : 1; + var tileLayout = this.gridLayout; + tileLayout.startcol += sign; + + for (var i=0, len=grid.length; i<len; i++) { + var row = grid[i]; + var position = row[colIndex].position.clone(); + var tile = row[prepend ? 'pop' : 'shift'](); + position.x += tileSize.w * sign; + tile.moveTo(this.getTileBoundsForGridIndex(i, colIndex), position); + row[prepend ? 'unshift' : 'push'](tile); + } + }, + + /** + * Method: removeExcessTiles + * When the size of the map or the buffer changes, we may need to + * remove some excess rows and columns. + * + * Parameters: + * rows - {Integer} Maximum number of rows we want our grid to have. + * columns - {Integer} Maximum number of columns we want our grid to have. + */ + removeExcessTiles: function(rows, columns) { + var i, l; + + // remove extra rows + while (this.grid.length > rows) { + var row = this.grid.pop(); + for (i=0, l=row.length; i<l; i++) { + var tile = row[i]; + this.destroyTile(tile); + } + } + + // remove extra columns + for (i=0, l=this.grid.length; i<l; i++) { + while (this.grid[i].length > columns) { + var row = this.grid[i]; + var tile = row.pop(); + this.destroyTile(tile); + } + } + }, + + /** + * Method: onMapResize + * For singleTile layers, this will set a new tile size according to the + * dimensions of the map pane. + */ + onMapResize: function() { + if (this.singleTile) { + this.clearGrid(); + this.setTileSize(); + } + }, + + /** + * APIMethod: getTileBounds + * Returns The tile bounds for a layer given a pixel location. + * + * Parameters: + * viewPortPx - {<OpenLayers.Pixel>} The location in the viewport. + * + * Returns: + * {<OpenLayers.Bounds>} Bounds of the tile at the given pixel location. + */ + getTileBounds: function(viewPortPx) { + var maxExtent = this.maxExtent; + var resolution = this.getResolution(); + var tileMapWidth = resolution * this.tileSize.w; + var tileMapHeight = resolution * this.tileSize.h; + var mapPoint = this.getLonLatFromViewPortPx(viewPortPx); + var tileLeft = maxExtent.left + (tileMapWidth * + Math.floor((mapPoint.lon - + maxExtent.left) / + tileMapWidth)); + var tileBottom = maxExtent.bottom + (tileMapHeight * + Math.floor((mapPoint.lat - + maxExtent.bottom) / + tileMapHeight)); + return new OpenLayers.Bounds(tileLeft, tileBottom, + tileLeft + tileMapWidth, + tileBottom + tileMapHeight); + }, + + CLASS_NAME: "OpenLayers.Layer.Grid" +}); +/* ====================================================================== + OpenLayers/Layer/XYZ.js + ====================================================================== */ + +/* 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/Layer/Grid.js + */ + +/** + * Class: OpenLayers.Layer.XYZ + * The XYZ class is designed to make it easier for people who have tiles + * arranged by a standard XYZ grid. + * + * Inherits from: + * - <OpenLayers.Layer.Grid> + */ +OpenLayers.Layer.XYZ = OpenLayers.Class(OpenLayers.Layer.Grid, { + + /** + * APIProperty: isBaseLayer + * Default is true, as this is designed to be a base tile source. + */ + isBaseLayer: true, + + /** + * APIProperty: sphericalMercator + * Whether the tile extents should be set to the defaults for + * spherical mercator. Useful for things like OpenStreetMap. + * Default is false, except for the OSM subclass. + */ + sphericalMercator: false, + + /** + * APIProperty: zoomOffset + * {Number} If your cache has more zoom levels than you want to provide + * access to with this layer, supply a zoomOffset. This zoom offset + * is added to the current map zoom level to determine the level + * for a requested tile. For example, if you supply a zoomOffset + * of 3, when the map is at the zoom 0, tiles will be requested from + * level 3 of your cache. Default is 0 (assumes cache level and map + * zoom are equivalent). Using <zoomOffset> is an alternative to + * setting <serverResolutions> if you only want to expose a subset + * of the server resolutions. + */ + zoomOffset: 0, + + /** + * APIProperty: serverResolutions + * {Array} A list of all resolutions available on the server. Only set this + * property if the map resolutions differ from the server. This + * property serves two purposes. (a) <serverResolutions> can include + * resolutions that the server supports and that you don't want to + * provide with this layer; you can also look at <zoomOffset>, which is + * an alternative to <serverResolutions> for that specific purpose. + * (b) The map can work with resolutions that aren't supported by + * the server, i.e. that aren't in <serverResolutions>. When the + * map is displayed in such a resolution data for the closest + * server-supported resolution is loaded and the layer div is + * stretched as necessary. + */ + serverResolutions: null, + + /** + * Constructor: OpenLayers.Layer.XYZ + * + * Parameters: + * name - {String} + * url - {String} + * options - {Object} Hashtable of extra options to tag onto the layer + */ + initialize: function(name, url, options) { + if (options && options.sphericalMercator || this.sphericalMercator) { + options = OpenLayers.Util.extend({ + projection: "EPSG:900913", + numZoomLevels: 19 + }, options); + } + OpenLayers.Layer.Grid.prototype.initialize.apply(this, [ + name || this.name, url || this.url, {}, options + ]); + }, + + /** + * APIMethod: clone + * Create a clone of this layer + * + * Parameters: + * obj - {Object} Is this ever used? + * + * Returns: + * {<OpenLayers.Layer.XYZ>} An exact clone of this OpenLayers.Layer.XYZ + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer.XYZ(this.name, + this.url, + this.getOptions()); + } + + //get all additions from superclasses + obj = OpenLayers.Layer.Grid.prototype.clone.apply(this, [obj]); + + return obj; + }, + + /** + * Method: getURL + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * + * Returns: + * {String} A string with the layer's url and parameters and also the + * passed-in bounds and appropriate tile size specified as + * parameters + */ + getURL: function (bounds) { + var xyz = this.getXYZ(bounds); + var url = this.url; + if (OpenLayers.Util.isArray(url)) { + var s = '' + xyz.x + xyz.y + xyz.z; + url = this.selectUrl(s, url); + } + + return OpenLayers.String.format(url, xyz); + }, + + /** + * Method: getXYZ + * Calculates x, y and z for the given bounds. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * + * Returns: + * {Object} - an object with x, y and z properties. + */ + getXYZ: function(bounds) { + var res = this.getServerResolution(); + var x = Math.round((bounds.left - this.maxExtent.left) / + (res * this.tileSize.w)); + var y = Math.round((this.maxExtent.top - bounds.top) / + (res * this.tileSize.h)); + var z = this.getServerZoom(); + + if (this.wrapDateLine) { + var limit = Math.pow(2, z); + x = ((x % limit) + limit) % limit; + } + + return {'x': x, 'y': y, 'z': z}; + }, + + /* APIMethod: setMap + * When the layer is added to a map, then we can fetch our origin + * (if we don't have one.) + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + OpenLayers.Layer.Grid.prototype.setMap.apply(this, arguments); + if (!this.tileOrigin) { + this.tileOrigin = new OpenLayers.LonLat(this.maxExtent.left, + this.maxExtent.bottom); + } + }, + + CLASS_NAME: "OpenLayers.Layer.XYZ" +}); +/* ====================================================================== + OpenLayers/Layer/OSM.js + ====================================================================== */ + +/* 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/Layer/XYZ.js + */ + +/** + * Class: OpenLayers.Layer.OSM + * This layer allows accessing OpenStreetMap tiles. By default the OpenStreetMap + * hosted tile.openstreetmap.org Mapnik tileset is used. If you wish to use + * a different layer instead, you need to provide a different + * URL to the constructor. Here's an example for using OpenCycleMap: + * + * (code) + * new OpenLayers.Layer.OSM("OpenCycleMap", + * ["http://a.tile.opencyclemap.org/cycle/${z}/${x}/${y}.png", + * "http://b.tile.opencyclemap.org/cycle/${z}/${x}/${y}.png", + * "http://c.tile.opencyclemap.org/cycle/${z}/${x}/${y}.png"]); + * (end) + * + * Inherits from: + * - <OpenLayers.Layer.XYZ> + */ +OpenLayers.Layer.OSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { + + /** + * APIProperty: name + * {String} The layer name. Defaults to "OpenStreetMap" if the first + * argument to the constructor is null or undefined. + */ + name: "OpenStreetMap", + + /** + * APIProperty: url + * {String} The tileset URL scheme. Defaults to + * : http://[a|b|c].tile.openstreetmap.org/${z}/${x}/${y}.png + * (the official OSM tileset) if the second argument to the constructor + * is null or undefined. To use another tileset you can have something + * like this: + * (code) + * new OpenLayers.Layer.OSM("OpenCycleMap", + * ["http://a.tile.opencyclemap.org/cycle/${z}/${x}/${y}.png", + * "http://b.tile.opencyclemap.org/cycle/${z}/${x}/${y}.png", + * "http://c.tile.opencyclemap.org/cycle/${z}/${x}/${y}.png"]); + * (end) + */ + url: [ + 'http://a.tile.openstreetmap.org/${z}/${x}/${y}.png', + 'http://b.tile.openstreetmap.org/${z}/${x}/${y}.png', + 'http://c.tile.openstreetmap.org/${z}/${x}/${y}.png' + ], + + /** + * Property: attribution + * {String} The layer attribution. + */ + attribution: "© <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors", + + /** + * Property: sphericalMercator + * {Boolean} + */ + sphericalMercator: true, + + /** + * Property: wrapDateLine + * {Boolean} + */ + wrapDateLine: true, + + /** APIProperty: tileOptions + * {Object} optional configuration options for <OpenLayers.Tile> instances + * created by this Layer. Default is + * + * (code) + * {crossOriginKeyword: 'anonymous'} + * (end) + * + * When using OSM tilesets other than the default ones, it may be + * necessary to set this to + * + * (code) + * {crossOriginKeyword: null} + * (end) + * + * if the server does not send Access-Control-Allow-Origin headers. + */ + tileOptions: null, + + /** + * Constructor: OpenLayers.Layer.OSM + * + * Parameters: + * name - {String} The layer name. + * url - {String} The tileset URL scheme. + * options - {Object} Configuration options for the layer. Any inherited + * layer option can be set in this object (e.g. + * <OpenLayers.Layer.Grid.buffer>). + */ + initialize: function(name, url, options) { + OpenLayers.Layer.XYZ.prototype.initialize.apply(this, arguments); + this.tileOptions = OpenLayers.Util.extend({ + crossOriginKeyword: 'anonymous' + }, this.options && this.options.tileOptions); + }, + + /** + * Method: clone + */ + clone: function(obj) { + if (obj == null) { + obj = new OpenLayers.Layer.OSM( + this.name, this.url, this.getOptions()); + } + obj = OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); + return obj; + }, + + CLASS_NAME: "OpenLayers.Layer.OSM" +}); +/* ====================================================================== + OpenLayers/Renderer.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Renderer + * This is the base class for all renderers. + * + * This is based on a merger code written by Paul Spencer and Bertil Chapuis. + * It is largely composed of virtual functions that are to be implemented + * in technology-specific subclasses, but there is some generic code too. + * + * The functions that *are* implemented here merely deal with the maintenance + * of the size and extent variables, as well as the cached 'resolution' + * value. + * + * A note to the user that all subclasses should use getResolution() instead + * of directly accessing this.resolution in order to correctly use the + * cacheing system. + * + */ +OpenLayers.Renderer = OpenLayers.Class({ + + /** + * Property: container + * {DOMElement} + */ + container: null, + + /** + * Property: root + * {DOMElement} + */ + root: null, + + /** + * Property: extent + * {<OpenLayers.Bounds>} + */ + extent: null, + + /** + * Property: locked + * {Boolean} If the renderer is currently in a state where many things + * are changing, the 'locked' property is set to true. This means + * that renderers can expect at least one more drawFeature event to be + * called with the 'locked' property set to 'true': In some renderers, + * this might make sense to use as a 'only update local information' + * flag. + */ + locked: false, + + /** + * Property: size + * {<OpenLayers.Size>} + */ + size: null, + + /** + * Property: resolution + * {Float} cache of current map resolution + */ + resolution: null, + + /** + * Property: map + * {<OpenLayers.Map>} Reference to the map -- this is set in Vector's setMap() + */ + map: null, + + /** + * Property: featureDx + * {Number} Feature offset in x direction. Will be calculated for and + * applied to the current feature while rendering (see + * <calculateFeatureDx>). + */ + featureDx: 0, + + /** + * Constructor: OpenLayers.Renderer + * + * Parameters: + * containerID - {<String>} + * options - {Object} options for this renderer. See sublcasses for + * supported options. + */ + initialize: function(containerID, options) { + this.container = OpenLayers.Util.getElement(containerID); + OpenLayers.Util.extend(this, options); + }, + + /** + * APIMethod: destroy + */ + destroy: function() { + this.container = null; + this.extent = null; + this.size = null; + this.resolution = null; + this.map = null; + }, + + /** + * APIMethod: supported + * This should be overridden by specific subclasses + * + * Returns: + * {Boolean} Whether or not the browser supports the renderer class + */ + supported: function() { + return false; + }, + + /** + * Method: setExtent + * Set the visible part of the layer. + * + * Resolution has probably changed, so we nullify the resolution + * cache (this.resolution) -- this way it will be re-computed when + * next it is needed. + * We nullify the resolution cache (this.resolution) if resolutionChanged + * is set to true - this way it will be re-computed on the next + * getResolution() request. + * + * Parameters: + * extent - {<OpenLayers.Bounds>} + * resolutionChanged - {Boolean} + * + * Returns: + * {Boolean} true to notify the layer that the new extent does not exceed + * the coordinate range, and the features will not need to be redrawn. + * False otherwise. + */ + setExtent: function(extent, resolutionChanged) { + this.extent = extent.clone(); + if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) { + var ratio = extent.getWidth() / this.map.getExtent().getWidth(), + extent = extent.scale(1 / ratio); + this.extent = extent.wrapDateLine(this.map.getMaxExtent()).scale(ratio); + } + if (resolutionChanged) { + this.resolution = null; + } + return true; + }, + + /** + * Method: setSize + * Sets the size of the drawing surface. + * + * Resolution has probably changed, so we nullify the resolution + * cache (this.resolution) -- this way it will be re-computed when + * next it is needed. + * + * Parameters: + * size - {<OpenLayers.Size>} + */ + setSize: function(size) { + this.size = size.clone(); + this.resolution = null; + }, + + /** + * Method: getResolution + * Uses cached copy of resolution if available to minimize computing + * + * Returns: + * {Float} The current map's resolution + */ + getResolution: function() { + this.resolution = this.resolution || this.map.getResolution(); + return this.resolution; + }, + + /** + * Method: drawFeature + * Draw the feature. The optional style argument can be used + * to override the feature's own style. This method should only + * be called from layer.drawFeature(). + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * style - {<Object>} + * + * Returns: + * {Boolean} true if the feature has been drawn completely, false if not, + * undefined if the feature had no geometry + */ + drawFeature: function(feature, style) { + if(style == null) { + style = feature.style; + } + if (feature.geometry) { + var bounds = feature.geometry.getBounds(); + if(bounds) { + var worldBounds; + if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) { + worldBounds = this.map.getMaxExtent(); + } + if (!bounds.intersectsBounds(this.extent, {worldBounds: worldBounds})) { + style = {display: "none"}; + } else { + this.calculateFeatureDx(bounds, worldBounds); + } + var rendered = this.drawGeometry(feature.geometry, style, feature.id); + if(style.display != "none" && style.label && rendered !== false) { + + var location = feature.geometry.getCentroid(); + if(style.labelXOffset || style.labelYOffset) { + var xOffset = isNaN(style.labelXOffset) ? 0 : style.labelXOffset; + var yOffset = isNaN(style.labelYOffset) ? 0 : style.labelYOffset; + var res = this.getResolution(); + location.move(xOffset*res, yOffset*res); + } + this.drawText(feature.id, style, location); + } else { + this.removeText(feature.id); + } + return rendered; + } + } + }, + + /** + * Method: calculateFeatureDx + * {Number} Calculates the feature offset in x direction. Looking at the + * center of the feature bounds and the renderer extent, we calculate how + * many world widths the two are away from each other. This distance is + * used to shift the feature as close as possible to the center of the + * current enderer extent, which ensures that the feature is visible in the + * current viewport. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} Bounds of the feature + * worldBounds - {<OpenLayers.Bounds>} Bounds of the world + */ + calculateFeatureDx: function(bounds, worldBounds) { + this.featureDx = 0; + if (worldBounds) { + var worldWidth = worldBounds.getWidth(), + rendererCenterX = (this.extent.left + this.extent.right) / 2, + featureCenterX = (bounds.left + bounds.right) / 2, + worldsAway = Math.round((featureCenterX - rendererCenterX) / worldWidth); + this.featureDx = worldsAway * worldWidth; + } + }, + + /** + * Method: drawGeometry + * + * Draw a geometry. This should only be called from the renderer itself. + * Use layer.drawFeature() from outside the renderer. + * virtual function + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {<String>} + */ + drawGeometry: function(geometry, style, featureId) {}, + + /** + * Method: drawText + * Function for drawing text labels. + * This method is only called by the renderer itself. + * + * Parameters: + * featureId - {String} + * style - + * location - {<OpenLayers.Geometry.Point>} + */ + drawText: function(featureId, style, location) {}, + + /** + * Method: removeText + * Function for removing text labels. + * This method is only called by the renderer itself. + * + * Parameters: + * featureId - {String} + */ + removeText: function(featureId) {}, + + /** + * Method: clear + * Clear all vectors from the renderer. + * virtual function. + */ + clear: function() {}, + + /** + * Method: getFeatureIdFromEvent + * Returns a feature id from an event on the renderer. + * How this happens is specific to the renderer. This should be + * called from layer.getFeatureFromEvent(). + * Virtual function. + * + * Parameters: + * evt - {<OpenLayers.Event>} + * + * Returns: + * {String} A feature id or undefined. + */ + getFeatureIdFromEvent: function(evt) {}, + + /** + * Method: eraseFeatures + * This is called by the layer to erase features + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} + */ + eraseFeatures: function(features) { + if(!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + for(var i=0, len=features.length; i<len; ++i) { + var feature = features[i]; + this.eraseGeometry(feature.geometry, feature.id); + this.removeText(feature.id); + } + }, + + /** + * Method: eraseGeometry + * Remove a geometry from the renderer (by id). + * virtual function. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * featureId - {String} + */ + eraseGeometry: function(geometry, featureId) {}, + + /** + * Method: moveRoot + * moves this renderer's root to a (different) renderer. + * To be implemented by subclasses that require a common renderer root for + * feature selection. + * + * Parameters: + * renderer - {<OpenLayers.Renderer>} target renderer for the moved root + */ + moveRoot: function(renderer) {}, + + /** + * Method: getRenderLayerId + * Gets the layer that this renderer's output appears on. If moveRoot was + * used, this will be different from the id of the layer containing the + * features rendered by this renderer. + * + * Returns: + * {String} the id of the output layer. + */ + getRenderLayerId: function() { + return this.container.id; + }, + + /** + * Method: applyDefaultSymbolizer + * + * Parameters: + * symbolizer - {Object} + * + * Returns: + * {Object} + */ + applyDefaultSymbolizer: function(symbolizer) { + var result = OpenLayers.Util.extend({}, + OpenLayers.Renderer.defaultSymbolizer); + if(symbolizer.stroke === false) { + delete result.strokeWidth; + delete result.strokeColor; + } + if(symbolizer.fill === false) { + delete result.fillColor; + } + OpenLayers.Util.extend(result, symbolizer); + return result; + }, + + CLASS_NAME: "OpenLayers.Renderer" +}); + +/** + * Constant: OpenLayers.Renderer.defaultSymbolizer + * {Object} Properties from this symbolizer will be applied to symbolizers + * with missing properties. This can also be used to set a global + * symbolizer default in OpenLayers. To be SLD 1.x compliant, add the + * following code before rendering any vector features: + * (code) + * OpenLayers.Renderer.defaultSymbolizer = { + * fillColor: "#808080", + * fillOpacity: 1, + * strokeColor: "#000000", + * strokeOpacity: 1, + * strokeWidth: 1, + * pointRadius: 3, + * graphicName: "square" + * }; + * (end) + */ +OpenLayers.Renderer.defaultSymbolizer = { + fillColor: "#000000", + strokeColor: "#000000", + strokeWidth: 2, + fillOpacity: 1, + strokeOpacity: 1, + pointRadius: 0, + labelAlign: 'cm' +}; + + + +/** + * Constant: OpenLayers.Renderer.symbol + * Coordinate arrays for well known (named) symbols. + */ +OpenLayers.Renderer.symbol = { + "star": [350,75, 379,161, 469,161, 397,215, 423,301, 350,250, 277,301, + 303,215, 231,161, 321,161, 350,75], + "cross": [4,0, 6,0, 6,4, 10,4, 10,6, 6,6, 6,10, 4,10, 4,6, 0,6, 0,4, 4,4, + 4,0], + "x": [0,0, 25,0, 50,35, 75,0, 100,0, 65,50, 100,100, 75,100, 50,65, 25,100, 0,100, 35,50, 0,0], + "square": [0,0, 0,1, 1,1, 1,0, 0,0], + "triangle": [0,10, 10,10, 5,0, 0,10] +}; +/* ====================================================================== + OpenLayers/Renderer/Canvas.js + ====================================================================== */ + +/* 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/Renderer.js + */ + +/** + * Class: OpenLayers.Renderer.Canvas + * A renderer based on the 2D 'canvas' drawing element. + * + * Inherits: + * - <OpenLayers.Renderer> + */ +OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { + + /** + * APIProperty: hitDetection + * {Boolean} Allow for hit detection of features. Default is true. + */ + hitDetection: true, + + /** + * Property: hitOverflow + * {Number} The method for converting feature identifiers to color values + * supports 16777215 sequential values. Two features cannot be + * predictably detected if their identifiers differ by more than this + * value. The hitOverflow allows for bigger numbers (but the + * difference in values is still limited). + */ + hitOverflow: 0, + + /** + * Property: canvas + * {Canvas} The canvas context object. + */ + canvas: null, + + /** + * Property: features + * {Object} Internal object of feature/style pairs for use in redrawing the layer. + */ + features: null, + + /** + * Property: pendingRedraw + * {Boolean} The renderer needs a redraw call to render features added while + * the renderer was locked. + */ + pendingRedraw: false, + + /** + * Property: cachedSymbolBounds + * {Object} Internal cache of calculated symbol extents. + */ + cachedSymbolBounds: {}, + + /** + * Constructor: OpenLayers.Renderer.Canvas + * + * Parameters: + * containerID - {<String>} + * options - {Object} Optional properties to be set on the renderer. + */ + initialize: function(containerID, options) { + OpenLayers.Renderer.prototype.initialize.apply(this, arguments); + this.root = document.createElement("canvas"); + this.container.appendChild(this.root); + this.canvas = this.root.getContext("2d"); + this.features = {}; + if (this.hitDetection) { + this.hitCanvas = document.createElement("canvas"); + this.hitContext = this.hitCanvas.getContext("2d"); + } + }, + + /** + * Method: setExtent + * Set the visible part of the layer. + * + * Parameters: + * extent - {<OpenLayers.Bounds>} + * resolutionChanged - {Boolean} + * + * Returns: + * {Boolean} true to notify the layer that the new extent does not exceed + * the coordinate range, and the features will not need to be redrawn. + * False otherwise. + */ + setExtent: function() { + OpenLayers.Renderer.prototype.setExtent.apply(this, arguments); + // always redraw features + return false; + }, + + /** + * Method: eraseGeometry + * Erase a geometry from the renderer. Because the Canvas renderer has + * 'memory' of the features that it has drawn, we have to remove the + * feature so it doesn't redraw. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * featureId - {String} + */ + eraseGeometry: function(geometry, featureId) { + this.eraseFeatures(this.features[featureId][0]); + }, + + /** + * APIMethod: supported + * + * Returns: + * {Boolean} Whether or not the browser supports the renderer class + */ + supported: function() { + return OpenLayers.CANVAS_SUPPORTED; + }, + + /** + * Method: setSize + * Sets the size of the drawing surface. + * + * Once the size is updated, redraw the canvas. + * + * Parameters: + * size - {<OpenLayers.Size>} + */ + setSize: function(size) { + this.size = size.clone(); + var root = this.root; + root.style.width = size.w + "px"; + root.style.height = size.h + "px"; + root.width = size.w; + root.height = size.h; + this.resolution = null; + if (this.hitDetection) { + var hitCanvas = this.hitCanvas; + hitCanvas.style.width = size.w + "px"; + hitCanvas.style.height = size.h + "px"; + hitCanvas.width = size.w; + hitCanvas.height = size.h; + } + }, + + /** + * Method: drawFeature + * Draw the feature. Stores the feature in the features list, + * then redraws the layer. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * style - {<Object>} + * + * Returns: + * {Boolean} The feature has been drawn completely. If the feature has no + * geometry, undefined will be returned. If the feature is not rendered + * for other reasons, false will be returned. + */ + drawFeature: function(feature, style) { + var rendered; + if (feature.geometry) { + style = this.applyDefaultSymbolizer(style || feature.style); + // don't render if display none or feature outside extent + var bounds = feature.geometry.getBounds(); + + var worldBounds; + if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) { + worldBounds = this.map.getMaxExtent(); + } + + var intersects = bounds && bounds.intersectsBounds(this.extent, {worldBounds: worldBounds}); + + rendered = (style.display !== "none") && !!bounds && intersects; + if (rendered) { + // keep track of what we have rendered for redraw + this.features[feature.id] = [feature, style]; + } + else { + // remove from features tracked for redraw + delete(this.features[feature.id]); + } + this.pendingRedraw = true; + } + if (this.pendingRedraw && !this.locked) { + this.redraw(); + this.pendingRedraw = false; + } + return rendered; + }, + + /** + * Method: drawGeometry + * Used when looping (in redraw) over the features; draws + * the canvas. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + */ + drawGeometry: function(geometry, style, featureId) { + var className = geometry.CLASS_NAME; + if ((className == "OpenLayers.Geometry.Collection") || + (className == "OpenLayers.Geometry.MultiPoint") || + (className == "OpenLayers.Geometry.MultiLineString") || + (className == "OpenLayers.Geometry.MultiPolygon")) { + for (var i = 0; i < geometry.components.length; i++) { + this.drawGeometry(geometry.components[i], style, featureId); + } + return; + } + switch (geometry.CLASS_NAME) { + case "OpenLayers.Geometry.Point": + this.drawPoint(geometry, style, featureId); + break; + case "OpenLayers.Geometry.LineString": + this.drawLineString(geometry, style, featureId); + break; + case "OpenLayers.Geometry.LinearRing": + this.drawLinearRing(geometry, style, featureId); + break; + case "OpenLayers.Geometry.Polygon": + this.drawPolygon(geometry, style, featureId); + break; + default: + break; + } + }, + + /** + * Method: drawExternalGraphic + * Called to draw External graphics. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawExternalGraphic: function(geometry, style, featureId) { + var img = new Image(); + + var title = style.title || style.graphicTitle; + if (title) { + img.title = title; + } + + var width = style.graphicWidth || style.graphicHeight; + var height = style.graphicHeight || style.graphicWidth; + width = width ? width : style.pointRadius * 2; + height = height ? height : style.pointRadius * 2; + var xOffset = (style.graphicXOffset != undefined) ? + style.graphicXOffset : -(0.5 * width); + var yOffset = (style.graphicYOffset != undefined) ? + style.graphicYOffset : -(0.5 * height); + + var opacity = style.graphicOpacity || style.fillOpacity; + + var onLoad = function() { + if(!this.features[featureId]) { + return; + } + var pt = this.getLocalXY(geometry); + var p0 = pt[0]; + var p1 = pt[1]; + if(!isNaN(p0) && !isNaN(p1)) { + var x = (p0 + xOffset) | 0; + var y = (p1 + yOffset) | 0; + var canvas = this.canvas; + canvas.globalAlpha = opacity; + var factor = OpenLayers.Renderer.Canvas.drawImageScaleFactor || + (OpenLayers.Renderer.Canvas.drawImageScaleFactor = + /android 2.1/.test(navigator.userAgent.toLowerCase()) ? + // 320 is the screen width of the G1 phone, for + // which drawImage works out of the box. + 320 / window.screen.width : 1 + ); + canvas.drawImage( + img, x*factor, y*factor, width*factor, height*factor + ); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId); + this.hitContext.fillRect(x, y, width, height); + } + } + }; + + img.onload = OpenLayers.Function.bind(onLoad, this); + img.src = style.externalGraphic; + }, + + /** + * Method: drawNamedSymbol + * Called to draw Well Known Graphic Symbol Name. + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawNamedSymbol: function(geometry, style, featureId) { + var x, y, cx, cy, i, symbolBounds, scaling, angle; + var unscaledStrokeWidth; + var deg2rad = Math.PI / 180.0; + + var symbol = OpenLayers.Renderer.symbol[style.graphicName]; + + if (!symbol) { + throw new Error(style.graphicName + ' is not a valid symbol name'); + } + + if (!symbol.length || symbol.length < 2) return; + + var pt = this.getLocalXY(geometry); + var p0 = pt[0]; + var p1 = pt[1]; + + if (isNaN(p0) || isNaN(p1)) return; + + // Use rounded line caps + this.canvas.lineCap = "round"; + this.canvas.lineJoin = "round"; + + if (this.hitDetection) { + this.hitContext.lineCap = "round"; + this.hitContext.lineJoin = "round"; + } + + // Scale and rotate symbols, using precalculated bounds whenever possible. + if (style.graphicName in this.cachedSymbolBounds) { + symbolBounds = this.cachedSymbolBounds[style.graphicName]; + } else { + symbolBounds = new OpenLayers.Bounds(); + for(i = 0; i < symbol.length; i+=2) { + symbolBounds.extend(new OpenLayers.LonLat(symbol[i], symbol[i+1])); + } + this.cachedSymbolBounds[style.graphicName] = symbolBounds; + } + + // Push symbol scaling, translation and rotation onto the transformation stack in reverse order. + // Don't forget to apply all canvas transformations to the hitContext canvas as well(!) + this.canvas.save(); + if (this.hitDetection) { this.hitContext.save(); } + + // Step 3: place symbol at the desired location + this.canvas.translate(p0,p1); + if (this.hitDetection) { this.hitContext.translate(p0,p1); } + + // Step 2a. rotate the symbol if necessary + angle = deg2rad * style.rotation; // will be NaN when style.rotation is undefined. + if (!isNaN(angle)) { + this.canvas.rotate(angle); + if (this.hitDetection) { this.hitContext.rotate(angle); } + } + + // // Step 2: scale symbol such that pointRadius equals half the maximum symbol dimension. + scaling = 2.0 * style.pointRadius / Math.max(symbolBounds.getWidth(), symbolBounds.getHeight()); + this.canvas.scale(scaling,scaling); + if (this.hitDetection) { this.hitContext.scale(scaling,scaling); } + + // Step 1: center the symbol at the origin + cx = symbolBounds.getCenterLonLat().lon; + cy = symbolBounds.getCenterLonLat().lat; + this.canvas.translate(-cx,-cy); + if (this.hitDetection) { this.hitContext.translate(-cx,-cy); } + + // Don't forget to scale stroke widths, because they are affected by canvas scale transformations as well(!) + // Alternative: scale symbol coordinates manually, so stroke width scaling is not needed anymore. + unscaledStrokeWidth = style.strokeWidth; + style.strokeWidth = unscaledStrokeWidth / scaling; + + if (style.fill !== false) { + this.setCanvasStyle("fill", style); + this.canvas.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.canvas.moveTo(x,y); + this.canvas.lineTo(x,y); + } + this.canvas.closePath(); + this.canvas.fill(); + + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.hitContext.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.canvas.moveTo(x,y); + this.hitContext.lineTo(x,y); + } + this.hitContext.closePath(); + this.hitContext.fill(); + } + } + + if (style.stroke !== false) { + this.setCanvasStyle("stroke", style); + this.canvas.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.canvas.moveTo(x,y); + this.canvas.lineTo(x,y); + } + this.canvas.closePath(); + this.canvas.stroke(); + + + if (this.hitDetection) { + this.setHitContextStyle("stroke", featureId, style, scaling); + this.hitContext.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.hitContext.moveTo(x,y); + this.hitContext.lineTo(x,y); + } + this.hitContext.closePath(); + this.hitContext.stroke(); + } + + } + + style.strokeWidth = unscaledStrokeWidth; + this.canvas.restore(); + if (this.hitDetection) { this.hitContext.restore(); } + this.setCanvasStyle("reset"); + }, + + /** + * Method: setCanvasStyle + * Prepare the canvas for drawing by setting various global settings. + * + * Parameters: + * type - {String} one of 'stroke', 'fill', or 'reset' + * style - {Object} Symbolizer hash + */ + setCanvasStyle: function(type, style) { + if (type === "fill") { + this.canvas.globalAlpha = style['fillOpacity']; + this.canvas.fillStyle = style['fillColor']; + } else if (type === "stroke") { + this.canvas.globalAlpha = style['strokeOpacity']; + this.canvas.strokeStyle = style['strokeColor']; + this.canvas.lineWidth = style['strokeWidth']; + } else { + this.canvas.globalAlpha = 0; + this.canvas.lineWidth = 1; + } + }, + + /** + * Method: featureIdToHex + * Convert a feature ID string into an RGB hex string. + * + * Parameters: + * featureId - {String} Feature id + * + * Returns: + * {String} RGB hex string. + */ + featureIdToHex: function(featureId) { + var id = Number(featureId.split("_").pop()) + 1; // zero for no feature + if (id >= 16777216) { + this.hitOverflow = id - 16777215; + id = id % 16777216 + 1; + } + var hex = "000000" + id.toString(16); + var len = hex.length; + hex = "#" + hex.substring(len-6, len); + return hex; + }, + + /** + * Method: setHitContextStyle + * Prepare the hit canvas for drawing by setting various global settings. + * + * Parameters: + * type - {String} one of 'stroke', 'fill', or 'reset' + * featureId - {String} The feature id. + * symbolizer - {<OpenLayers.Symbolizer>} The symbolizer. + */ + setHitContextStyle: function(type, featureId, symbolizer, strokeScaling) { + var hex = this.featureIdToHex(featureId); + if (type == "fill") { + this.hitContext.globalAlpha = 1.0; + this.hitContext.fillStyle = hex; + } else if (type == "stroke") { + this.hitContext.globalAlpha = 1.0; + this.hitContext.strokeStyle = hex; + // bump up stroke width to deal with antialiasing. If strokeScaling is defined, we're rendering a symbol + // on a transformed canvas, so the antialias width bump has to scale as well. + if (typeof strokeScaling === "undefined") { + this.hitContext.lineWidth = symbolizer.strokeWidth + 2; + } else { + if (!isNaN(strokeScaling)) { this.hitContext.lineWidth = symbolizer.strokeWidth + 2.0 / strokeScaling; } + } + } else { + this.hitContext.globalAlpha = 0; + this.hitContext.lineWidth = 1; + } + }, + + /** + * Method: drawPoint + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawPoint: function(geometry, style, featureId) { + if(style.graphic !== false) { + if(style.externalGraphic) { + this.drawExternalGraphic(geometry, style, featureId); + } else if (style.graphicName && (style.graphicName != "circle")) { + this.drawNamedSymbol(geometry, style, featureId); + } else { + var pt = this.getLocalXY(geometry); + var p0 = pt[0]; + var p1 = pt[1]; + if(!isNaN(p0) && !isNaN(p1)) { + var twoPi = Math.PI*2; + var radius = style.pointRadius; + if(style.fill !== false) { + this.setCanvasStyle("fill", style); + this.canvas.beginPath(); + this.canvas.arc(p0, p1, radius, 0, twoPi, true); + this.canvas.fill(); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.hitContext.beginPath(); + this.hitContext.arc(p0, p1, radius, 0, twoPi, true); + this.hitContext.fill(); + } + } + + if(style.stroke !== false) { + this.setCanvasStyle("stroke", style); + this.canvas.beginPath(); + this.canvas.arc(p0, p1, radius, 0, twoPi, true); + this.canvas.stroke(); + if (this.hitDetection) { + this.setHitContextStyle("stroke", featureId, style); + this.hitContext.beginPath(); + this.hitContext.arc(p0, p1, radius, 0, twoPi, true); + this.hitContext.stroke(); + } + this.setCanvasStyle("reset"); + } + } + } + } + }, + + /** + * Method: drawLineString + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawLineString: function(geometry, style, featureId) { + style = OpenLayers.Util.applyDefaults({fill: false}, style); + this.drawLinearRing(geometry, style, featureId); + }, + + /** + * Method: drawLinearRing + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawLinearRing: function(geometry, style, featureId) { + if (style.fill !== false) { + this.setCanvasStyle("fill", style); + this.renderPath(this.canvas, geometry, style, featureId, "fill"); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.renderPath(this.hitContext, geometry, style, featureId, "fill"); + } + } + if (style.stroke !== false) { + this.setCanvasStyle("stroke", style); + this.renderPath(this.canvas, geometry, style, featureId, "stroke"); + if (this.hitDetection) { + this.setHitContextStyle("stroke", featureId, style); + this.renderPath(this.hitContext, geometry, style, featureId, "stroke"); + } + } + this.setCanvasStyle("reset"); + }, + + /** + * Method: renderPath + * Render a path with stroke and optional fill. + */ + renderPath: function(context, geometry, style, featureId, type) { + var components = geometry.components; + var len = components.length; + context.beginPath(); + var start = this.getLocalXY(components[0]); + var x = start[0]; + var y = start[1]; + if (!isNaN(x) && !isNaN(y)) { + context.moveTo(start[0], start[1]); + for (var i=1; i<len; ++i) { + var pt = this.getLocalXY(components[i]); + context.lineTo(pt[0], pt[1]); + } + if (type === "fill") { + context.fill(); + } else { + context.stroke(); + } + } + }, + + /** + * Method: drawPolygon + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawPolygon: function(geometry, style, featureId) { + var components = geometry.components; + var len = components.length; + this.drawLinearRing(components[0], style, featureId); + // erase inner rings + for (var i=1; i<len; ++i) { + /** + * Note that this is overly agressive. Here we punch holes through + * all previously rendered features on the same canvas. A better + * solution for polygons with interior rings would be to draw the + * polygon on a sketch canvas first. We could erase all holes + * there and then copy the drawing to the layer canvas. + * TODO: http://trac.osgeo.org/openlayers/ticket/3130 + */ + this.canvas.globalCompositeOperation = "destination-out"; + if (this.hitDetection) { + this.hitContext.globalCompositeOperation = "destination-out"; + } + this.drawLinearRing( + components[i], + OpenLayers.Util.applyDefaults({stroke: false, fillOpacity: 1.0}, style), + featureId + ); + this.canvas.globalCompositeOperation = "source-over"; + if (this.hitDetection) { + this.hitContext.globalCompositeOperation = "source-over"; + } + this.drawLinearRing( + components[i], + OpenLayers.Util.applyDefaults({fill: false}, style), + featureId + ); + } + }, + + /** + * Method: drawText + * This method is only called by the renderer itself. + * + * Parameters: + * location - {<OpenLayers.Point>} + * style - {Object} + */ + drawText: function(location, style) { + var pt = this.getLocalXY(location); + + this.setCanvasStyle("reset"); + this.canvas.fillStyle = style.fontColor; + this.canvas.globalAlpha = style.fontOpacity || 1.0; + var fontStyle = [style.fontStyle ? style.fontStyle : "normal", + "normal", // "font-variant" not supported + style.fontWeight ? style.fontWeight : "normal", + style.fontSize ? style.fontSize : "1em", + style.fontFamily ? style.fontFamily : "sans-serif"].join(" "); + var labelRows = style.label.split('\n'); + var numRows = labelRows.length; + if (this.canvas.fillText) { + // HTML5 + this.canvas.font = fontStyle; + this.canvas.textAlign = + OpenLayers.Renderer.Canvas.LABEL_ALIGN[style.labelAlign[0]] || + "center"; + this.canvas.textBaseline = + OpenLayers.Renderer.Canvas.LABEL_ALIGN[style.labelAlign[1]] || + "middle"; + var vfactor = + OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[1]]; + if (vfactor == null) { + vfactor = -.5; + } + var lineHeight = + this.canvas.measureText('Mg').height || + this.canvas.measureText('xx').width; + pt[1] += lineHeight*vfactor*(numRows-1); + for (var i = 0; i < numRows; i++) { + if (style.labelOutlineWidth) { + this.canvas.save(); + this.canvas.globalAlpha = style.labelOutlineOpacity || style.fontOpacity || 1.0; + this.canvas.strokeStyle = style.labelOutlineColor; + this.canvas.lineWidth = style.labelOutlineWidth; + this.canvas.strokeText(labelRows[i], pt[0], pt[1] + (lineHeight*i) + 1); + this.canvas.restore(); + } + this.canvas.fillText(labelRows[i], pt[0], pt[1] + (lineHeight*i)); + } + } else if (this.canvas.mozDrawText) { + // Mozilla pre-Gecko1.9.1 (<FF3.1) + this.canvas.mozTextStyle = fontStyle; + // No built-in text alignment, so we measure and adjust the position + var hfactor = + OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[0]]; + if (hfactor == null) { + hfactor = -.5; + } + var vfactor = + OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[1]]; + if (vfactor == null) { + vfactor = -.5; + } + var lineHeight = this.canvas.mozMeasureText('xx'); + pt[1] += lineHeight*(1 + (vfactor*numRows)); + for (var i = 0; i < numRows; i++) { + var x = pt[0] + (hfactor*this.canvas.mozMeasureText(labelRows[i])); + var y = pt[1] + (i*lineHeight); + this.canvas.translate(x, y); + this.canvas.mozDrawText(labelRows[i]); + this.canvas.translate(-x, -y); + } + } + this.setCanvasStyle("reset"); + }, + + /** + * Method: getLocalXY + * transform geographic xy into pixel xy + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + */ + getLocalXY: function(point) { + var resolution = this.getResolution(); + var extent = this.extent; + var x = ((point.x - this.featureDx) / resolution + (-extent.left / resolution)); + var y = ((extent.top / resolution) - point.y / resolution); + return [x, y]; + }, + + /** + * Method: clear + * Clear all vectors from the renderer. + */ + clear: function() { + var height = this.root.height; + var width = this.root.width; + this.canvas.clearRect(0, 0, width, height); + this.features = {}; + if (this.hitDetection) { + this.hitContext.clearRect(0, 0, width, height); + } + }, + + /** + * Method: getFeatureIdFromEvent + * Returns a feature id from an event on the renderer. + * + * Parameters: + * evt - {<OpenLayers.Event>} + * + * Returns: + * {<OpenLayers.Feature.Vector} A feature or undefined. This method returns a + * feature instead of a feature id to avoid an unnecessary lookup on the + * layer. + */ + getFeatureIdFromEvent: function(evt) { + var featureId, feature; + + if (this.hitDetection && this.root.style.display !== "none") { + // this dragging check should go in the feature handler + if (!this.map.dragging) { + var xy = evt.xy; + var x = xy.x | 0; + var y = xy.y | 0; + var data = this.hitContext.getImageData(x, y, 1, 1).data; + if (data[3] === 255) { // antialiased + var id = data[2] + (256 * (data[1] + (256 * data[0]))); + if (id) { + featureId = "OpenLayers_Feature_Vector_" + (id - 1 + this.hitOverflow); + try { + feature = this.features[featureId][0]; + } catch(err) { + // Because of antialiasing on the canvas, when the hit location is at a point where the edge of + // one symbol intersects the interior of another symbol, a wrong hit color (and therefore id) results. + // todo: set Antialiasing = 'off' on the hitContext as soon as browsers allow it. + } + } + } + } + } + return feature; + }, + + /** + * Method: eraseFeatures + * This is called by the layer to erase features; removes the feature from + * the list, then redraws the layer. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} + */ + eraseFeatures: function(features) { + if(!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + for(var i=0; i<features.length; ++i) { + delete this.features[features[i].id]; + } + this.redraw(); + }, + + /** + * Method: redraw + * The real 'meat' of the function: any time things have changed, + * redraw() can be called to loop over all the data and (you guessed + * it) redraw it. Unlike Elements-based Renderers, we can't interact + * with things once they're drawn, to remove them, for example, so + * instead we have to just clear everything and draw from scratch. + */ + redraw: function() { + if (!this.locked) { + var height = this.root.height; + var width = this.root.width; + this.canvas.clearRect(0, 0, width, height); + if (this.hitDetection) { + this.hitContext.clearRect(0, 0, width, height); + } + var labelMap = []; + var feature, geometry, style; + var worldBounds = (this.map.baseLayer && this.map.baseLayer.wrapDateLine) && this.map.getMaxExtent(); + for (var id in this.features) { + if (!this.features.hasOwnProperty(id)) { continue; } + feature = this.features[id][0]; + geometry = feature.geometry; + this.calculateFeatureDx(geometry.getBounds(), worldBounds); + style = this.features[id][1]; + this.drawGeometry(geometry, style, feature.id); + if(style.label) { + labelMap.push([feature, style]); + } + } + var item; + for (var i=0, len=labelMap.length; i<len; ++i) { + item = labelMap[i]; + this.drawText(item[0].geometry.getCentroid(), item[1]); + } + } + }, + + CLASS_NAME: "OpenLayers.Renderer.Canvas" +}); + +/** + * Constant: OpenLayers.Renderer.Canvas.LABEL_ALIGN + * {Object} + */ +OpenLayers.Renderer.Canvas.LABEL_ALIGN = { + "l": "left", + "r": "right", + "t": "top", + "b": "bottom" +}; + +/** + * Constant: OpenLayers.Renderer.Canvas.LABEL_FACTOR + * {Object} + */ +OpenLayers.Renderer.Canvas.LABEL_FACTOR = { + "l": 0, + "r": -1, + "t": 0, + "b": -1 +}; + +/** + * Constant: OpenLayers.Renderer.Canvas.drawImageScaleFactor + * {Number} Scale factor to apply to the canvas drawImage arguments. This + * is always 1 except for Android 2.1 devices, to work around + * http://code.google.com/p/android/issues/detail?id=5141. + */ +OpenLayers.Renderer.Canvas.drawImageScaleFactor = null; +/* ====================================================================== + OpenLayers/Handler.js + ====================================================================== */ + +/* 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/Events.js + */ + +/** + * Class: OpenLayers.Handler + * Base class to construct a higher-level handler for event sequences. All + * handlers have activate and deactivate methods. In addition, they have + * methods named like browser events. When a handler is activated, any + * additional methods named like a browser event is registered as a + * listener for the corresponding event. When a handler is deactivated, + * those same methods are unregistered as event listeners. + * + * Handlers also typically have a callbacks object with keys named like + * the abstracted events or event sequences that they are in charge of + * handling. The controls that wrap handlers define the methods that + * correspond to these abstract events - so instead of listening for + * individual browser events, they only listen for the abstract events + * defined by the handler. + * + * Handlers are created by controls, which ultimately have the responsibility + * of making changes to the the state of the application. Handlers + * themselves may make temporary changes, but in general are expected to + * return the application in the same state that they found it. + */ +OpenLayers.Handler = OpenLayers.Class({ + + /** + * Property: id + * {String} + */ + id: null, + + /** + * APIProperty: control + * {<OpenLayers.Control>}. The control that initialized this handler. The + * control is assumed to have a valid map property - that map is used + * in the handler's own setMap method. + */ + control: null, + + /** + * Property: map + * {<OpenLayers.Map>} + */ + map: null, + + /** + * APIProperty: keyMask + * {Integer} Use bitwise operators and one or more of the OpenLayers.Handler + * constants to construct a keyMask. The keyMask is used by + * <checkModifiers>. If the keyMask matches the combination of keys + * down on an event, checkModifiers returns true. + * + * Example: + * (code) + * // handler only responds if the Shift key is down + * handler.keyMask = OpenLayers.Handler.MOD_SHIFT; + * + * // handler only responds if Ctrl-Shift is down + * handler.keyMask = OpenLayers.Handler.MOD_SHIFT | + * OpenLayers.Handler.MOD_CTRL; + * (end) + */ + keyMask: null, + + /** + * Property: active + * {Boolean} + */ + active: false, + + /** + * Property: evt + * {Event} This property references the last event handled by the handler. + * Note that this property is not part of the stable API. Use of the + * evt property should be restricted to controls in the library + * or other applications that are willing to update with changes to + * the OpenLayers code. + */ + evt: null, + + /** + * Property: touch + * {Boolean} Indicates the support of touch events. When touch events are + * started touch will be true and all mouse related listeners will do + * nothing. + */ + touch: false, + + /** + * Constructor: OpenLayers.Handler + * Construct a handler. + * + * Parameters: + * control - {<OpenLayers.Control>} The control that initialized this + * handler. The control is assumed to have a valid map property; that + * map is used in the handler's own setMap method. If a map property + * is present in the options argument it will be used instead. + * callbacks - {Object} An object whose properties correspond to abstracted + * events or sequences of browser events. The values for these + * properties are functions defined by the control that get called by + * the handler. + * options - {Object} An optional object whose properties will be set on + * the handler. + */ + initialize: function(control, callbacks, options) { + OpenLayers.Util.extend(this, options); + this.control = control; + this.callbacks = callbacks; + + var map = this.map || control.map; + if (map) { + this.setMap(map); + } + + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + }, + + /** + * Method: setMap + */ + setMap: function (map) { + this.map = map; + }, + + /** + * Method: checkModifiers + * Check the keyMask on the handler. If no <keyMask> is set, this always + * returns true. If a <keyMask> is set and it matches the combination + * of keys down on an event, this returns true. + * + * Returns: + * {Boolean} The keyMask matches the keys down on an event. + */ + checkModifiers: function (evt) { + if(this.keyMask == null) { + return true; + } + /* calculate the keyboard modifier mask for this event */ + var keyModifiers = + (evt.shiftKey ? OpenLayers.Handler.MOD_SHIFT : 0) | + (evt.ctrlKey ? OpenLayers.Handler.MOD_CTRL : 0) | + (evt.altKey ? OpenLayers.Handler.MOD_ALT : 0) | + (evt.metaKey ? OpenLayers.Handler.MOD_META : 0); + + /* if it differs from the handler object's key mask, + bail out of the event handler */ + return (keyModifiers == this.keyMask); + }, + + /** + * APIMethod: activate + * Turn on the handler. Returns false if the handler was already active. + * + * Returns: + * {Boolean} The handler was activated. + */ + activate: function() { + if(this.active) { + return false; + } + // register for event handlers defined on this class. + var events = OpenLayers.Events.prototype.BROWSER_EVENTS; + for (var i=0, len=events.length; i<len; i++) { + if (this[events[i]]) { + this.register(events[i], this[events[i]]); + } + } + this.active = true; + return true; + }, + + /** + * APIMethod: deactivate + * Turn off the handler. Returns false if the handler was already inactive. + * + * Returns: + * {Boolean} The handler was deactivated. + */ + deactivate: function() { + if(!this.active) { + return false; + } + // unregister event handlers defined on this class. + var events = OpenLayers.Events.prototype.BROWSER_EVENTS; + for (var i=0, len=events.length; i<len; i++) { + if (this[events[i]]) { + this.unregister(events[i], this[events[i]]); + } + } + this.touch = false; + this.active = false; + return true; + }, + + /** + * Method: startTouch + * Start touch events, this method must be called by subclasses in + * "touchstart" method. When touch events are started <touch> will be + * true and all mouse related listeners will do nothing. + */ + startTouch: function() { + if (!this.touch) { + this.touch = true; + var events = [ + "mousedown", "mouseup", "mousemove", "click", "dblclick", + "mouseout" + ]; + for (var i=0, len=events.length; i<len; i++) { + if (this[events[i]]) { + this.unregister(events[i], this[events[i]]); + } + } + } + }, + + /** + * Method: callback + * Trigger the control's named callback with the given arguments + * + * Parameters: + * name - {String} The key for the callback that is one of the properties + * of the handler's callbacks object. + * args - {Array(*)} An array of arguments (any type) with which to call + * the callback (defined by the control). + */ + callback: function (name, args) { + if (name && this.callbacks[name]) { + this.callbacks[name].apply(this.control, args); + } + }, + + /** + * Method: register + * register an event on the map + */ + register: function (name, method) { + // TODO: deal with registerPriority in 3.0 + this.map.events.registerPriority(name, this, method); + this.map.events.registerPriority(name, this, this.setEvent); + }, + + /** + * Method: unregister + * unregister an event from the map + */ + unregister: function (name, method) { + this.map.events.unregister(name, this, method); + this.map.events.unregister(name, this, this.setEvent); + }, + + /** + * Method: setEvent + * With each registered browser event, the handler sets its own evt + * property. This property can be accessed by controls if needed + * to get more information about the event that the handler is + * processing. + * + * This allows modifier keys on the event to be checked (alt, shift, ctrl, + * and meta cannot be checked with the keyboard handler). For a + * control to determine which modifier keys are associated with the + * event that a handler is currently processing, it should access + * (code)handler.evt.altKey || handler.evt.shiftKey || + * handler.evt.ctrlKey || handler.evt.metaKey(end). + * + * Parameters: + * evt - {Event} The browser event. + */ + setEvent: function(evt) { + this.evt = evt; + return true; + }, + + /** + * Method: destroy + * Deconstruct the handler. + */ + destroy: function () { + // unregister event listeners + this.deactivate(); + // eliminate circular references + this.control = this.map = null; + }, + + CLASS_NAME: "OpenLayers.Handler" +}); + +/** + * Constant: OpenLayers.Handler.MOD_NONE + * If set as the <keyMask>, <checkModifiers> returns false if any key is down. + */ +OpenLayers.Handler.MOD_NONE = 0; + +/** + * Constant: OpenLayers.Handler.MOD_SHIFT + * If set as the <keyMask>, <checkModifiers> returns false if Shift is down. + */ +OpenLayers.Handler.MOD_SHIFT = 1; + +/** + * Constant: OpenLayers.Handler.MOD_CTRL + * If set as the <keyMask>, <checkModifiers> returns false if Ctrl is down. + */ +OpenLayers.Handler.MOD_CTRL = 2; + +/** + * Constant: OpenLayers.Handler.MOD_ALT + * If set as the <keyMask>, <checkModifiers> returns false if Alt is down. + */ +OpenLayers.Handler.MOD_ALT = 4; + +/** + * Constant: OpenLayers.Handler.MOD_META + * If set as the <keyMask>, <checkModifiers> returns false if Cmd is down. + */ +OpenLayers.Handler.MOD_META = 8; + + +/* ====================================================================== + OpenLayers/Handler/Drag.js + ====================================================================== */ + +/* 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/Handler.js + */ + +/** + * Class: OpenLayers.Handler.Drag + * The drag handler is used to deal with sequences of browser events related + * to dragging. The handler is used by controls that want to know when + * a drag sequence begins, when a drag is happening, and when it has + * finished. + * + * Controls that use the drag handler typically construct it with callbacks + * for 'down', 'move', and 'done'. Callbacks for these keys are called + * when the drag begins, with each move, and when the drag is done. In + * addition, controls can have callbacks keyed to 'up' and 'out' if they + * care to differentiate between the types of events that correspond with + * the end of a drag sequence. If no drag actually occurs (no mouse move) + * the 'down' and 'up' callbacks will be called, but not the 'done' + * callback. + * + * Create a new drag handler with the <OpenLayers.Handler.Drag> constructor. + * + * Inherits from: + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Drag = OpenLayers.Class(OpenLayers.Handler, { + + /** + * Property: started + * {Boolean} When a mousedown or touchstart event is received, we want to + * record it, but not set 'dragging' until the mouse moves after starting. + */ + started: false, + + /** + * Property: stopDown + * {Boolean} Stop propagation of mousedown events from getting to listeners + * on the same element. Default is true. + */ + stopDown: true, + + /** + * Property: dragging + * {Boolean} + */ + dragging: false, + + /** + * Property: last + * {<OpenLayers.Pixel>} The last pixel location of the drag. + */ + last: null, + + /** + * Property: start + * {<OpenLayers.Pixel>} The first pixel location of the drag. + */ + start: null, + + /** + * Property: lastMoveEvt + * {Object} The last mousemove event that occurred. Used to + * position the map correctly when our "delay drag" + * timeout expired. + */ + lastMoveEvt: null, + + /** + * Property: oldOnselectstart + * {Function} + */ + oldOnselectstart: null, + + /** + * Property: interval + * {Integer} In order to increase performance, an interval (in + * milliseconds) can be set to reduce the number of drag events + * called. If set, a new drag event will not be set until the + * interval has passed. + * Defaults to 0, meaning no interval. + */ + interval: 0, + + /** + * Property: timeoutId + * {String} The id of the timeout used for the mousedown interval. + * This is "private", and should be left alone. + */ + timeoutId: null, + + /** + * APIProperty: documentDrag + * {Boolean} If set to true, the handler will also handle mouse moves when + * the cursor has moved out of the map viewport. Default is false. + */ + documentDrag: false, + + /** + * Property: documentEvents + * {Boolean} Are we currently observing document events? + */ + documentEvents: null, + + /** + * Constructor: OpenLayers.Handler.Drag + * Returns OpenLayers.Handler.Drag + * + * Parameters: + * control - {<OpenLayers.Control>} The control that is making use of + * this handler. If a handler is being used without a control, the + * handlers setMap method must be overridden to deal properly with + * the map. + * callbacks - {Object} An object containing a single function to be + * called when the drag operation is finished. The callback should + * expect to recieve a single argument, the pixel location of the event. + * Callbacks for 'move' and 'done' are supported. You can also speficy + * callbacks for 'down', 'up', and 'out' to respond to those events. + * options - {Object} + */ + initialize: function(control, callbacks, options) { + OpenLayers.Handler.prototype.initialize.apply(this, arguments); + + if (this.documentDrag === true) { + var me = this; + this._docMove = function(evt) { + me.mousemove({ + xy: {x: evt.clientX, y: evt.clientY}, + element: document + }); + }; + this._docUp = function(evt) { + me.mouseup({xy: {x: evt.clientX, y: evt.clientY}}); + }; + } + }, + + + /** + * Method: dragstart + * This private method is factorized from mousedown and touchstart methods + * + * Parameters: + * evt - {Event} The event + * + * Returns: + * {Boolean} Let the event propagate. + */ + dragstart: function (evt) { + var propagate = true; + this.dragging = false; + if (this.checkModifiers(evt) && + (OpenLayers.Event.isLeftClick(evt) || + OpenLayers.Event.isSingleTouch(evt))) { + this.started = true; + this.start = evt.xy; + this.last = evt.xy; + OpenLayers.Element.addClass( + this.map.viewPortDiv, "olDragDown" + ); + this.down(evt); + this.callback("down", [evt.xy]); + + // prevent document dragging + OpenLayers.Event.preventDefault(evt); + + if(!this.oldOnselectstart) { + this.oldOnselectstart = document.onselectstart ? + document.onselectstart : OpenLayers.Function.True; + } + document.onselectstart = OpenLayers.Function.False; + + propagate = !this.stopDown; + } else { + this.started = false; + this.start = null; + this.last = null; + } + return propagate; + }, + + /** + * Method: dragmove + * This private method is factorized from mousemove and touchmove methods + * + * Parameters: + * evt - {Event} The event + * + * Returns: + * {Boolean} Let the event propagate. + */ + dragmove: function (evt) { + this.lastMoveEvt = evt; + if (this.started && !this.timeoutId && (evt.xy.x != this.last.x || + evt.xy.y != this.last.y)) { + if(this.documentDrag === true && this.documentEvents) { + if(evt.element === document) { + this.adjustXY(evt); + // do setEvent manually because the documentEvents are not + // registered with the map + this.setEvent(evt); + } else { + this.removeDocumentEvents(); + } + } + if (this.interval > 0) { + this.timeoutId = setTimeout( + OpenLayers.Function.bind(this.removeTimeout, this), + this.interval); + } + this.dragging = true; + + this.move(evt); + this.callback("move", [evt.xy]); + if(!this.oldOnselectstart) { + this.oldOnselectstart = document.onselectstart; + document.onselectstart = OpenLayers.Function.False; + } + this.last = evt.xy; + } + return true; + }, + + /** + * Method: dragend + * This private method is factorized from mouseup and touchend methods + * + * Parameters: + * evt - {Event} The event + * + * Returns: + * {Boolean} Let the event propagate. + */ + dragend: function (evt) { + if (this.started) { + if(this.documentDrag === true && this.documentEvents) { + this.adjustXY(evt); + this.removeDocumentEvents(); + } + var dragged = (this.start != this.last); + this.started = false; + this.dragging = false; + OpenLayers.Element.removeClass( + this.map.viewPortDiv, "olDragDown" + ); + this.up(evt); + this.callback("up", [evt.xy]); + if(dragged) { + this.callback("done", [evt.xy]); + } + document.onselectstart = this.oldOnselectstart; + } + return true; + }, + + /** + * The four methods below (down, move, up, and out) are used by subclasses + * to do their own processing related to these mouse events. + */ + + /** + * Method: down + * This method is called during the handling of the mouse down event. + * Subclasses can do their own processing here. + * + * Parameters: + * evt - {Event} The mouse down event + */ + down: function(evt) { + }, + + /** + * Method: move + * This method is called during the handling of the mouse move event. + * Subclasses can do their own processing here. + * + * Parameters: + * evt - {Event} The mouse move event + * + */ + move: function(evt) { + }, + + /** + * Method: up + * This method is called during the handling of the mouse up event. + * Subclasses can do their own processing here. + * + * Parameters: + * evt - {Event} The mouse up event + */ + up: function(evt) { + }, + + /** + * Method: out + * This method is called during the handling of the mouse out event. + * Subclasses can do their own processing here. + * + * Parameters: + * evt - {Event} The mouse out event + */ + out: function(evt) { + }, + + /** + * The methods below are part of the magic of event handling. Because + * they are named like browser events, they are registered as listeners + * for the events they represent. + */ + + /** + * Method: mousedown + * Handle mousedown events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + mousedown: function(evt) { + return this.dragstart(evt); + }, + + /** + * Method: touchstart + * Handle touchstart events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchstart: function(evt) { + this.startTouch(); + return this.dragstart(evt); + }, + + /** + * Method: mousemove + * Handle mousemove events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + mousemove: function(evt) { + return this.dragmove(evt); + }, + + /** + * Method: touchmove + * Handle touchmove events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchmove: function(evt) { + return this.dragmove(evt); + }, + + /** + * Method: removeTimeout + * Private. Called by mousemove() to remove the drag timeout. + */ + removeTimeout: function() { + this.timeoutId = null; + // if timeout expires while we're still dragging (mouseup + // hasn't occurred) then call mousemove to move to the + // correct position + if(this.dragging) { + this.mousemove(this.lastMoveEvt); + } + }, + + /** + * Method: mouseup + * Handle mouseup events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + mouseup: function(evt) { + return this.dragend(evt); + }, + + /** + * Method: touchend + * Handle touchend events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchend: function(evt) { + // override evt.xy with last position since touchend does not have + // any touch position + evt.xy = this.last; + return this.dragend(evt); + }, + + /** + * Method: mouseout + * Handle mouseout events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + mouseout: function (evt) { + if (this.started && OpenLayers.Util.mouseLeft(evt, this.map.viewPortDiv)) { + if(this.documentDrag === true) { + this.addDocumentEvents(); + } else { + var dragged = (this.start != this.last); + this.started = false; + this.dragging = false; + OpenLayers.Element.removeClass( + this.map.viewPortDiv, "olDragDown" + ); + this.out(evt); + this.callback("out", []); + if(dragged) { + this.callback("done", [evt.xy]); + } + if(document.onselectstart) { + document.onselectstart = this.oldOnselectstart; + } + } + } + return true; + }, + + /** + * Method: click + * The drag handler captures the click event. If something else registers + * for clicks on the same element, its listener will not be called + * after a drag. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + click: function (evt) { + // let the click event propagate only if the mouse moved + return (this.start == this.last); + }, + + /** + * Method: activate + * Activate the handler. + * + * Returns: + * {Boolean} The handler was successfully activated. + */ + activate: function() { + var activated = false; + if(OpenLayers.Handler.prototype.activate.apply(this, arguments)) { + this.dragging = false; + activated = true; + } + return activated; + }, + + /** + * Method: deactivate + * Deactivate the handler. + * + * Returns: + * {Boolean} The handler was successfully deactivated. + */ + deactivate: function() { + var deactivated = false; + if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { + this.started = false; + this.dragging = false; + this.start = null; + this.last = null; + deactivated = true; + OpenLayers.Element.removeClass( + this.map.viewPortDiv, "olDragDown" + ); + } + return deactivated; + }, + + /** + * Method: adjustXY + * Converts event coordinates that are relative to the document body to + * ones that are relative to the map viewport. The latter is the default in + * OpenLayers. + * + * Parameters: + * evt - {Object} + */ + adjustXY: function(evt) { + var pos = OpenLayers.Util.pagePosition(this.map.viewPortDiv); + evt.xy.x -= pos[0]; + evt.xy.y -= pos[1]; + }, + + /** + * Method: addDocumentEvents + * Start observing document events when documentDrag is true and the mouse + * cursor leaves the map viewport while dragging. + */ + addDocumentEvents: function() { + OpenLayers.Element.addClass(document.body, "olDragDown"); + this.documentEvents = true; + OpenLayers.Event.observe(document, "mousemove", this._docMove); + OpenLayers.Event.observe(document, "mouseup", this._docUp); + }, + + /** + * Method: removeDocumentEvents + * Stops observing document events when documentDrag is true and the mouse + * cursor re-enters the map viewport while dragging. + */ + removeDocumentEvents: function() { + OpenLayers.Element.removeClass(document.body, "olDragDown"); + this.documentEvents = false; + OpenLayers.Event.stopObserving(document, "mousemove", this._docMove); + OpenLayers.Event.stopObserving(document, "mouseup", this._docUp); + }, + + CLASS_NAME: "OpenLayers.Handler.Drag" +}); +/* ====================================================================== + OpenLayers/Handler/Keyboard.js + ====================================================================== */ + +/* 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/Handler.js + * @requires OpenLayers/Events.js + */ + +/** + * Class: OpenLayers.handler.Keyboard + * A handler for keyboard events. Create a new instance with the + * <OpenLayers.Handler.Keyboard> constructor. + * + * Inherits from: + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Keyboard = OpenLayers.Class(OpenLayers.Handler, { + + /* http://www.quirksmode.org/js/keys.html explains key x-browser + key handling quirks in pretty nice detail */ + + /** + * Constant: KEY_EVENTS + * keydown, keypress, keyup + */ + KEY_EVENTS: ["keydown", "keyup"], + + /** + * Property: eventListener + * {Function} + */ + eventListener: null, + + /** + * Property: observeElement + * {DOMElement|String} The DOM element on which we listen for + * key events. Default to the document. + */ + observeElement: null, + + /** + * Constructor: OpenLayers.Handler.Keyboard + * Returns a new keyboard handler. + * + * Parameters: + * control - {<OpenLayers.Control>} The control that is making use of + * this handler. If a handler is being used without a control, the + * handlers setMap method must be overridden to deal properly with + * the map. + * callbacks - {Object} An object containing a single function to be + * called when the drag operation is finished. The callback should + * expect to recieve a single argument, the pixel location of the event. + * Callbacks for 'keydown', 'keypress', and 'keyup' are supported. + * options - {Object} Optional object whose properties will be set on the + * handler. + */ + initialize: function(control, callbacks, options) { + OpenLayers.Handler.prototype.initialize.apply(this, arguments); + // cache the bound event listener method so it can be unobserved later + this.eventListener = OpenLayers.Function.bindAsEventListener( + this.handleKeyEvent, this + ); + }, + + /** + * Method: destroy + */ + destroy: function() { + this.deactivate(); + this.eventListener = null; + OpenLayers.Handler.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: activate + */ + activate: function() { + if (OpenLayers.Handler.prototype.activate.apply(this, arguments)) { + this.observeElement = this.observeElement || document; + for (var i=0, len=this.KEY_EVENTS.length; i<len; i++) { + OpenLayers.Event.observe( + this.observeElement, this.KEY_EVENTS[i], this.eventListener); + } + return true; + } else { + return false; + } + }, + + /** + * Method: deactivate + */ + deactivate: function() { + var deactivated = false; + if (OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { + for (var i=0, len=this.KEY_EVENTS.length; i<len; i++) { + OpenLayers.Event.stopObserving( + this.observeElement, this.KEY_EVENTS[i], this.eventListener); + } + deactivated = true; + } + return deactivated; + }, + + /** + * Method: handleKeyEvent + */ + handleKeyEvent: function (evt) { + if (this.checkModifiers(evt)) { + this.callback(evt.type, [evt]); + } + }, + + CLASS_NAME: "OpenLayers.Handler.Keyboard" +}); +/* ====================================================================== + OpenLayers/Control/ModifyFeature.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Handler/Drag.js + * @requires OpenLayers/Handler/Keyboard.js + */ + +/** + * Class: OpenLayers.Control.ModifyFeature + * Control to modify features. When activated, a click renders the vertices + * of a feature - these vertices can then be dragged. By default, the + * delete key will delete the vertex under the mouse. New features are + * added by dragging "virtual vertices" between vertices. Create a new + * control with the <OpenLayers.Control.ModifyFeature> constructor. + * + * Inherits From: + * - <OpenLayers.Control> + */ +OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: documentDrag + * {Boolean} If set to true, dragging vertices will continue even if the + * mouse cursor leaves the map viewport. Default is false. + */ + documentDrag: false, + + /** + * APIProperty: geometryTypes + * {Array(String)} To restrict modification to a limited set of geometry + * types, send a list of strings corresponding to the geometry class + * names. + */ + geometryTypes: null, + + /** + * APIProperty: clickout + * {Boolean} Unselect features when clicking outside any feature. + * Default is true. + */ + clickout: true, + + /** + * APIProperty: toggle + * {Boolean} Unselect a selected feature on click. + * Default is true. + */ + toggle: true, + + /** + * APIProperty: standalone + * {Boolean} Set to true to create a control without SelectFeature + * capabilities. Default is false. If standalone is true, to modify + * a feature, call the <selectFeature> method with the target feature. + * Note that you must call the <unselectFeature> method to finish + * feature modification in standalone mode (before starting to modify + * another feature). + */ + standalone: false, + + /** + * Property: layer + * {<OpenLayers.Layer.Vector>} + */ + layer: null, + + /** + * Property: feature + * {<OpenLayers.Feature.Vector>} Feature currently available for modification. + */ + feature: null, + + /** + * Property: vertex + * {<OpenLayers.Feature.Vector>} Vertex currently being modified. + */ + vertex: null, + + /** + * Property: vertices + * {Array(<OpenLayers.Feature.Vector>)} Verticies currently available + * for dragging. + */ + vertices: null, + + /** + * Property: virtualVertices + * {Array(<OpenLayers.Feature.Vector>)} Virtual vertices in the middle + * of each edge. + */ + virtualVertices: null, + + /** + * Property: handlers + * {Object} + */ + handlers: null, + + /** + * APIProperty: deleteCodes + * {Array(Integer)} Keycodes for deleting verticies. Set to null to disable + * vertex deltion by keypress. If non-null, keypresses with codes + * in this array will delete vertices under the mouse. Default + * is 46 and 68, the 'delete' and lowercase 'd' keys. + */ + deleteCodes: null, + + /** + * APIProperty: virtualStyle + * {Object} A symbolizer to be used for virtual vertices. + */ + virtualStyle: null, + + /** + * APIProperty: vertexRenderIntent + * {String} The renderIntent to use for vertices. If no <virtualStyle> is + * provided, this renderIntent will also be used for virtual vertices, with + * a fillOpacity and strokeOpacity of 0.3. Default is null, which means + * that the layer's default style will be used for vertices. + */ + vertexRenderIntent: null, + + /** + * APIProperty: mode + * {Integer} Bitfields specifying the modification mode. Defaults to + * OpenLayers.Control.ModifyFeature.RESHAPE. To set the mode to a + * combination of options, use the | operator. For example, to allow + * the control to both resize and rotate features, use the following + * syntax + * (code) + * control.mode = OpenLayers.Control.ModifyFeature.RESIZE | + * OpenLayers.Control.ModifyFeature.ROTATE; + * (end) + */ + mode: null, + + /** + * APIProperty: createVertices + * {Boolean} Create new vertices by dragging the virtual vertices + * in the middle of each edge. Default is true. + */ + createVertices: true, + + /** + * Property: modified + * {Boolean} The currently selected feature has been modified. + */ + modified: false, + + /** + * Property: radiusHandle + * {<OpenLayers.Feature.Vector>} A handle for rotating/resizing a feature. + */ + radiusHandle: null, + + /** + * Property: dragHandle + * {<OpenLayers.Feature.Vector>} A handle for dragging a feature. + */ + dragHandle: null, + + /** + * APIProperty: onModificationStart + * {Function} *Deprecated*. Register for "beforefeaturemodified" instead. + * The "beforefeaturemodified" event is triggered on the layer before + * any modification begins. + * + * Optional function to be called when a feature is selected + * to be modified. The function should expect to be called with a + * feature. This could be used for example to allow to lock the + * feature on server-side. + */ + onModificationStart: function() {}, + + /** + * APIProperty: onModification + * {Function} *Deprecated*. Register for "featuremodified" instead. + * The "featuremodified" event is triggered on the layer with each + * feature modification. + * + * Optional function to be called when a feature has been + * modified. The function should expect to be called with a feature. + */ + onModification: function() {}, + + /** + * APIProperty: onModificationEnd + * {Function} *Deprecated*. Register for "afterfeaturemodified" instead. + * The "afterfeaturemodified" event is triggered on the layer after + * a feature has been modified. + * + * Optional function to be called when a feature is finished + * being modified. The function should expect to be called with a + * feature. + */ + onModificationEnd: function() {}, + + /** + * Constructor: OpenLayers.Control.ModifyFeature + * Create a new modify feature control. + * + * Parameters: + * layer - {<OpenLayers.Layer.Vector>} Layer that contains features that + * will be modified. + * options - {Object} Optional object whose properties will be set on the + * control. + */ + initialize: function(layer, options) { + options = options || {}; + this.layer = layer; + this.vertices = []; + this.virtualVertices = []; + this.virtualStyle = OpenLayers.Util.extend({}, + this.layer.style || + this.layer.styleMap.createSymbolizer(null, options.vertexRenderIntent) + ); + this.virtualStyle.fillOpacity = 0.3; + this.virtualStyle.strokeOpacity = 0.3; + this.deleteCodes = [46, 68]; + this.mode = OpenLayers.Control.ModifyFeature.RESHAPE; + OpenLayers.Control.prototype.initialize.apply(this, [options]); + if(!(OpenLayers.Util.isArray(this.deleteCodes))) { + this.deleteCodes = [this.deleteCodes]; + } + + // configure the drag handler + var dragCallbacks = { + down: function(pixel) { + this.vertex = null; + var feature = this.layer.getFeatureFromEvent( + this.handlers.drag.evt); + if (feature) { + this.dragStart(feature); + } else if (this.clickout) { + this._unselect = this.feature; + } + }, + move: function(pixel) { + delete this._unselect; + if (this.vertex) { + this.dragVertex(this.vertex, pixel); + } + }, + up: function() { + this.handlers.drag.stopDown = false; + if (this._unselect) { + this.unselectFeature(this._unselect); + delete this._unselect; + } + }, + done: function(pixel) { + if (this.vertex) { + this.dragComplete(this.vertex); + } + } + }; + var dragOptions = { + documentDrag: this.documentDrag, + stopDown: false + }; + + // configure the keyboard handler + var keyboardOptions = { + keydown: this.handleKeypress + }; + this.handlers = { + keyboard: new OpenLayers.Handler.Keyboard(this, keyboardOptions), + drag: new OpenLayers.Handler.Drag(this, dragCallbacks, dragOptions) + }; + }, + + /** + * APIMethod: destroy + * Take care of things that are not handled in superclass. + */ + destroy: function() { + if (this.map) { + this.map.events.un({ + "removelayer": this.handleMapEvents, + "changelayer": this.handleMapEvents, + scope: this + }); + } + this.layer = null; + OpenLayers.Control.prototype.destroy.apply(this, []); + }, + + /** + * APIMethod: activate + * Activate the control. + * + * Returns: + * {Boolean} Successfully activated the control. + */ + activate: function() { + this.moveLayerToTop(); + this.map.events.on({ + "removelayer": this.handleMapEvents, + "changelayer": this.handleMapEvents, + scope: this + }); + return (this.handlers.keyboard.activate() && + this.handlers.drag.activate() && + OpenLayers.Control.prototype.activate.apply(this, arguments)); + }, + + /** + * APIMethod: deactivate + * Deactivate the control. + * + * Returns: + * {Boolean} Successfully deactivated the control. + */ + deactivate: function() { + var deactivated = false; + // the return from the controls is unimportant in this case + if(OpenLayers.Control.prototype.deactivate.apply(this, arguments)) { + this.moveLayerBack(); + this.map.events.un({ + "removelayer": this.handleMapEvents, + "changelayer": this.handleMapEvents, + scope: this + }); + this.layer.removeFeatures(this.vertices, {silent: true}); + this.layer.removeFeatures(this.virtualVertices, {silent: true}); + this.vertices = []; + this.handlers.drag.deactivate(); + this.handlers.keyboard.deactivate(); + var feature = this.feature; + if (feature && feature.geometry && feature.layer) { + this.unselectFeature(feature); + } + deactivated = true; + } + return deactivated; + }, + + /** + * Method: beforeSelectFeature + * Called before a feature is selected. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} The feature about to be selected. + */ + beforeSelectFeature: function(feature) { + return this.layer.events.triggerEvent( + "beforefeaturemodified", {feature: feature} + ); + }, + + /** + * APIMethod: selectFeature + * Select a feature for modification in standalone mode. In non-standalone + * mode, this method is called when a feature is selected by clicking. + * Register a listener to the beforefeaturemodified event and return false + * to prevent feature modification. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} the selected feature. + */ + selectFeature: function(feature) { + if (this.feature === feature || + (this.geometryTypes && OpenLayers.Util.indexOf(this.geometryTypes, + feature.geometry.CLASS_NAME) == -1)) { + return; + } + if (this.beforeSelectFeature(feature) !== false) { + if (this.feature) { + this.unselectFeature(this.feature); + } + this.feature = feature; + this.layer.selectedFeatures.push(feature); + this.layer.drawFeature(feature, 'select'); + this.modified = false; + this.resetVertices(); + this.onModificationStart(this.feature); + } + // keep track of geometry modifications + var modified = feature.modified; + if (feature.geometry && !(modified && modified.geometry)) { + this._originalGeometry = feature.geometry.clone(); + } + }, + + /** + * APIMethod: unselectFeature + * Called when the select feature control unselects a feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} The unselected feature. + */ + unselectFeature: function(feature) { + this.layer.removeFeatures(this.vertices, {silent: true}); + this.vertices = []; + this.layer.destroyFeatures(this.virtualVertices, {silent: true}); + this.virtualVertices = []; + if(this.dragHandle) { + this.layer.destroyFeatures([this.dragHandle], {silent: true}); + delete this.dragHandle; + } + if(this.radiusHandle) { + this.layer.destroyFeatures([this.radiusHandle], {silent: true}); + delete this.radiusHandle; + } + this.layer.drawFeature(this.feature, 'default'); + this.feature = null; + OpenLayers.Util.removeItem(this.layer.selectedFeatures, feature); + this.onModificationEnd(feature); + this.layer.events.triggerEvent("afterfeaturemodified", { + feature: feature, + modified: this.modified + }); + this.modified = false; + }, + + + /** + * Method: dragStart + * Called by the drag handler before a feature is dragged. This method is + * used to differentiate between points and vertices + * of higher order geometries. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} The point or vertex about to be + * dragged. + */ + dragStart: function(feature) { + var isPoint = feature.geometry.CLASS_NAME == + 'OpenLayers.Geometry.Point'; + if (!this.standalone && + ((!feature._sketch && isPoint) || !feature._sketch)) { + if (this.toggle && this.feature === feature) { + // mark feature for unselection + this._unselect = feature; + } + this.selectFeature(feature); + } + if (feature._sketch || isPoint) { + // feature is a drag or virtual handle or point + this.vertex = feature; + this.handlers.drag.stopDown = true; + } + }, + + /** + * Method: dragVertex + * Called by the drag handler with each drag move of a vertex. + * + * Parameters: + * vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged. + * pixel - {<OpenLayers.Pixel>} Pixel location of the mouse event. + */ + dragVertex: function(vertex, pixel) { + var pos = this.map.getLonLatFromViewPortPx(pixel); + var geom = vertex.geometry; + geom.move(pos.lon - geom.x, pos.lat - geom.y); + this.modified = true; + /** + * Five cases: + * 1) dragging a simple point + * 2) dragging a virtual vertex + * 3) dragging a drag handle + * 4) dragging a real vertex + * 5) dragging a radius handle + */ + if(this.feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + // dragging a simple point + this.layer.events.triggerEvent("vertexmodified", { + vertex: vertex.geometry, + feature: this.feature, + pixel: pixel + }); + } else { + if(vertex._index) { + // dragging a virtual vertex + vertex.geometry.parent.addComponent(vertex.geometry, + vertex._index); + // move from virtual to real vertex + delete vertex._index; + OpenLayers.Util.removeItem(this.virtualVertices, vertex); + this.vertices.push(vertex); + } else if(vertex == this.dragHandle) { + // dragging a drag handle + this.layer.removeFeatures(this.vertices, {silent: true}); + this.vertices = []; + if(this.radiusHandle) { + this.layer.destroyFeatures([this.radiusHandle], {silent: true}); + this.radiusHandle = null; + } + } else if(vertex !== this.radiusHandle) { + // dragging a real vertex + this.layer.events.triggerEvent("vertexmodified", { + vertex: vertex.geometry, + feature: this.feature, + pixel: pixel + }); + } + // dragging a radius handle - no special treatment + if(this.virtualVertices.length > 0) { + this.layer.destroyFeatures(this.virtualVertices, {silent: true}); + this.virtualVertices = []; + } + this.layer.drawFeature(this.feature, this.standalone ? undefined : + 'select'); + } + // keep the vertex on top so it gets the mouseout after dragging + // this should be removed in favor of an option to draw under or + // maintain node z-index + this.layer.drawFeature(vertex); + }, + + /** + * Method: dragComplete + * Called by the drag handler when the feature dragging is complete. + * + * Parameters: + * vertex - {<OpenLayers.Feature.Vector>} The vertex being dragged. + */ + dragComplete: function(vertex) { + this.resetVertices(); + this.setFeatureState(); + this.onModification(this.feature); + this.layer.events.triggerEvent("featuremodified", + {feature: this.feature}); + }, + + /** + * Method: setFeatureState + * Called when the feature is modified. If the current state is not + * INSERT or DELETE, the state is set to UPDATE. + */ + setFeatureState: function() { + if(this.feature.state != OpenLayers.State.INSERT && + this.feature.state != OpenLayers.State.DELETE) { + this.feature.state = OpenLayers.State.UPDATE; + if (this.modified && this._originalGeometry) { + var feature = this.feature; + feature.modified = OpenLayers.Util.extend(feature.modified, { + geometry: this._originalGeometry + }); + delete this._originalGeometry; + } + } + }, + + /** + * Method: resetVertices + */ + resetVertices: function() { + if(this.vertices.length > 0) { + this.layer.removeFeatures(this.vertices, {silent: true}); + this.vertices = []; + } + if(this.virtualVertices.length > 0) { + this.layer.removeFeatures(this.virtualVertices, {silent: true}); + this.virtualVertices = []; + } + if(this.dragHandle) { + this.layer.destroyFeatures([this.dragHandle], {silent: true}); + this.dragHandle = null; + } + if(this.radiusHandle) { + this.layer.destroyFeatures([this.radiusHandle], {silent: true}); + this.radiusHandle = null; + } + if(this.feature && + this.feature.geometry.CLASS_NAME != "OpenLayers.Geometry.Point") { + if((this.mode & OpenLayers.Control.ModifyFeature.DRAG)) { + this.collectDragHandle(); + } + if((this.mode & (OpenLayers.Control.ModifyFeature.ROTATE | + OpenLayers.Control.ModifyFeature.RESIZE))) { + this.collectRadiusHandle(); + } + if(this.mode & OpenLayers.Control.ModifyFeature.RESHAPE){ + // Don't collect vertices when we're resizing + if (!(this.mode & OpenLayers.Control.ModifyFeature.RESIZE)){ + this.collectVertices(); + } + } + } + }, + + /** + * Method: handleKeypress + * Called by the feature handler on keypress. This is used to delete + * vertices. If the <deleteCode> property is set, vertices will + * be deleted when a feature is selected for modification and + * the mouse is over a vertex. + * + * Parameters: + * evt - {Event} Keypress event. + */ + handleKeypress: function(evt) { + var code = evt.keyCode; + + // check for delete key + if(this.feature && + OpenLayers.Util.indexOf(this.deleteCodes, code) != -1) { + var vertex = this.layer.getFeatureFromEvent(this.handlers.drag.evt); + if (vertex && + OpenLayers.Util.indexOf(this.vertices, vertex) != -1 && + !this.handlers.drag.dragging && vertex.geometry.parent) { + // remove the vertex + vertex.geometry.parent.removeComponent(vertex.geometry); + this.layer.events.triggerEvent("vertexremoved", { + vertex: vertex.geometry, + feature: this.feature, + pixel: evt.xy + }); + this.layer.drawFeature(this.feature, this.standalone ? + undefined : 'select'); + this.modified = true; + this.resetVertices(); + this.setFeatureState(); + this.onModification(this.feature); + this.layer.events.triggerEvent("featuremodified", + {feature: this.feature}); + } + } + }, + + /** + * Method: collectVertices + * Collect the vertices from the modifiable feature's geometry and push + * them on to the control's vertices array. + */ + collectVertices: function() { + this.vertices = []; + this.virtualVertices = []; + var control = this; + function collectComponentVertices(geometry) { + var i, vertex, component, len; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + vertex = new OpenLayers.Feature.Vector(geometry); + vertex._sketch = true; + vertex.renderIntent = control.vertexRenderIntent; + control.vertices.push(vertex); + } else { + var numVert = geometry.components.length; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.LinearRing") { + numVert -= 1; + } + for(i=0; i<numVert; ++i) { + component = geometry.components[i]; + if(component.CLASS_NAME == "OpenLayers.Geometry.Point") { + vertex = new OpenLayers.Feature.Vector(component); + vertex._sketch = true; + vertex.renderIntent = control.vertexRenderIntent; + control.vertices.push(vertex); + } else { + collectComponentVertices(component); + } + } + + // add virtual vertices in the middle of each edge + if (control.createVertices && geometry.CLASS_NAME != "OpenLayers.Geometry.MultiPoint") { + for(i=0, len=geometry.components.length; i<len-1; ++i) { + var prevVertex = geometry.components[i]; + var nextVertex = geometry.components[i + 1]; + if(prevVertex.CLASS_NAME == "OpenLayers.Geometry.Point" && + nextVertex.CLASS_NAME == "OpenLayers.Geometry.Point") { + var x = (prevVertex.x + nextVertex.x) / 2; + var y = (prevVertex.y + nextVertex.y) / 2; + var point = new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(x, y), + null, control.virtualStyle + ); + // set the virtual parent and intended index + point.geometry.parent = geometry; + point._index = i + 1; + point._sketch = true; + control.virtualVertices.push(point); + } + } + } + } + } + collectComponentVertices.call(this, this.feature.geometry); + this.layer.addFeatures(this.virtualVertices, {silent: true}); + this.layer.addFeatures(this.vertices, {silent: true}); + }, + + /** + * Method: collectDragHandle + * Collect the drag handle for the selected geometry. + */ + collectDragHandle: function() { + var geometry = this.feature.geometry; + var center = geometry.getBounds().getCenterLonLat(); + var originGeometry = new OpenLayers.Geometry.Point( + center.lon, center.lat + ); + var origin = new OpenLayers.Feature.Vector(originGeometry); + originGeometry.move = function(x, y) { + OpenLayers.Geometry.Point.prototype.move.call(this, x, y); + geometry.move(x, y); + }; + origin._sketch = true; + this.dragHandle = origin; + this.dragHandle.renderIntent = this.vertexRenderIntent; + this.layer.addFeatures([this.dragHandle], {silent: true}); + }, + + /** + * Method: collectRadiusHandle + * Collect the radius handle for the selected geometry. + */ + collectRadiusHandle: function() { + var geometry = this.feature.geometry; + var bounds = geometry.getBounds(); + var center = bounds.getCenterLonLat(); + var originGeometry = new OpenLayers.Geometry.Point( + center.lon, center.lat + ); + var radiusGeometry = new OpenLayers.Geometry.Point( + bounds.right, bounds.bottom + ); + var radius = new OpenLayers.Feature.Vector(radiusGeometry); + var resize = (this.mode & OpenLayers.Control.ModifyFeature.RESIZE); + var reshape = (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE); + var rotate = (this.mode & OpenLayers.Control.ModifyFeature.ROTATE); + + radiusGeometry.move = function(x, y) { + OpenLayers.Geometry.Point.prototype.move.call(this, x, y); + var dx1 = this.x - originGeometry.x; + var dy1 = this.y - originGeometry.y; + var dx0 = dx1 - x; + var dy0 = dy1 - y; + if(rotate) { + var a0 = Math.atan2(dy0, dx0); + var a1 = Math.atan2(dy1, dx1); + var angle = a1 - a0; + angle *= 180 / Math.PI; + geometry.rotate(angle, originGeometry); + } + if(resize) { + var scale, ratio; + // 'resize' together with 'reshape' implies that the aspect + // ratio of the geometry will not be preserved whilst resizing + if (reshape) { + scale = dy1 / dy0; + ratio = (dx1 / dx0) / scale; + } else { + var l0 = Math.sqrt((dx0 * dx0) + (dy0 * dy0)); + var l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1)); + scale = l1 / l0; + } + geometry.resize(scale, originGeometry, ratio); + } + }; + radius._sketch = true; + this.radiusHandle = radius; + this.radiusHandle.renderIntent = this.vertexRenderIntent; + this.layer.addFeatures([this.radiusHandle], {silent: true}); + }, + + /** + * Method: setMap + * Set the map property for the control and all handlers. + * + * Parameters: + * map - {<OpenLayers.Map>} The control's map. + */ + setMap: function(map) { + this.handlers.drag.setMap(map); + OpenLayers.Control.prototype.setMap.apply(this, arguments); + }, + + /** + * Method: handleMapEvents + * + * Parameters: + * evt - {Object} + */ + handleMapEvents: function(evt) { + if (evt.type == "removelayer" || evt.property == "order") { + this.moveLayerToTop(); + } + }, + + /** + * Method: moveLayerToTop + * Moves the layer for this handler to the top, so mouse events can reach + * it. + */ + moveLayerToTop: function() { + var index = Math.max(this.map.Z_INDEX_BASE['Feature'] - 1, + this.layer.getZIndex()) + 1; + this.layer.setZIndex(index); + + }, + + /** + * Method: moveLayerBack + * Moves the layer back to the position determined by the map's layers + * array. + */ + moveLayerBack: function() { + var index = this.layer.getZIndex() - 1; + if (index >= this.map.Z_INDEX_BASE['Feature']) { + this.layer.setZIndex(index); + } else { + this.map.setLayerZIndex(this.layer, + this.map.getLayerIndex(this.layer)); + } + }, + + CLASS_NAME: "OpenLayers.Control.ModifyFeature" +}); + +/** + * Constant: RESHAPE + * {Integer} Constant used to make the control work in reshape mode + */ +OpenLayers.Control.ModifyFeature.RESHAPE = 1; +/** + * Constant: RESIZE + * {Integer} Constant used to make the control work in resize mode + */ +OpenLayers.Control.ModifyFeature.RESIZE = 2; +/** + * Constant: ROTATE + * {Integer} Constant used to make the control work in rotate mode + */ +OpenLayers.Control.ModifyFeature.ROTATE = 4; +/** + * Constant: DRAG + * {Integer} Constant used to make the control work in drag mode + */ +OpenLayers.Control.ModifyFeature.DRAG = 8; +/* ====================================================================== + OpenLayers/Layer/Bing.js + ====================================================================== */ + +/* 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/Layer/XYZ.js + */ + +/** + * Class: OpenLayers.Layer.Bing + * Bing layer using direct tile access as provided by Bing Maps REST Services. + * See http://msdn.microsoft.com/en-us/library/ff701713.aspx for more + * information. Note: Terms of Service compliant use requires the map to be + * configured with an <OpenLayers.Control.Attribution> control and the + * attribution placed on or near the map. + * + * Inherits from: + * - <OpenLayers.Layer.XYZ> + */ +OpenLayers.Layer.Bing = OpenLayers.Class(OpenLayers.Layer.XYZ, { + + /** + * Property: key + * {String} API key for Bing maps, get your own key + * at http://bingmapsportal.com/ . + */ + key: null, + + /** + * Property: serverResolutions + * {Array} the resolutions provided by the Bing servers. + */ + serverResolutions: [ + 156543.03390625, 78271.516953125, 39135.7584765625, + 19567.87923828125, 9783.939619140625, 4891.9698095703125, + 2445.9849047851562, 1222.9924523925781, 611.4962261962891, + 305.74811309814453, 152.87405654907226, 76.43702827453613, + 38.218514137268066, 19.109257068634033, 9.554628534317017, + 4.777314267158508, 2.388657133579254, 1.194328566789627, + 0.5971642833948135, 0.29858214169740677, 0.14929107084870338, + 0.07464553542435169 + ], + + /** + * Property: attributionTemplate + * {String} + */ + attributionTemplate: '<span class="olBingAttribution ${type}">' + + '<div><a target="_blank" href="http://www.bing.com/maps/">' + + '<img src="${logo}" /></a></div>${copyrights}' + + '<a style="white-space: nowrap" target="_blank" '+ + 'href="http://www.microsoft.com/maps/product/terms.html">' + + 'Terms of Use</a></span>', + + /** + * Property: metadata + * {Object} Metadata for this layer, as returned by the callback script + */ + metadata: null, + + /** + * Property: protocolRegex + * {RegExp} Regular expression to match and replace http: in bing urls + */ + protocolRegex: /^http:/i, + + /** + * APIProperty: type + * {String} The layer identifier. Any non-birdseye imageryType + * from http://msdn.microsoft.com/en-us/library/ff701716.aspx can be + * used. Default is "Road". + */ + type: "Road", + + /** + * APIProperty: culture + * {String} The culture identifier. See http://msdn.microsoft.com/en-us/library/ff701709.aspx + * for the definition and the possible values. Default is "en-US". + */ + culture: "en-US", + + /** + * APIProperty: metadataParams + * {Object} Optional url parameters for the Get Imagery Metadata request + * as described here: http://msdn.microsoft.com/en-us/library/ff701716.aspx + */ + metadataParams: null, + + /** APIProperty: tileOptions + * {Object} optional configuration options for <OpenLayers.Tile> instances + * created by this Layer. Default is + * + * (code) + * {crossOriginKeyword: 'anonymous'} + * (end) + */ + tileOptions: null, + + /** APIProperty: protocol + * {String} Protocol to use to fetch Imagery Metadata, tiles and bing logo + * Can be 'http:' 'https:' or '' + * + * Warning: tiles may not be available under both HTTP and HTTPS protocols. + * Microsoft approved use of both HTTP and HTTPS urls for tiles. However + * this is undocumented and the Imagery Metadata API always returns HTTP + * urls. + * + * Default is '', unless when executed from a file:/// uri, in which case + * it is 'http:'. + */ + protocol: ~window.location.href.indexOf('http') ? '' : 'http:', + + /** + * Constructor: OpenLayers.Layer.Bing + * Create a new Bing layer. + * + * Example: + * (code) + * var road = new OpenLayers.Layer.Bing({ + * name: "My Bing Aerial Layer", + * type: "Aerial", + * key: "my-api-key-here", + * }); + * (end) + * + * Parameters: + * options - {Object} Configuration properties for the layer. + * + * Required configuration properties: + * key - {String} Bing Maps API key for your application. Get one at + * http://bingmapsportal.com/. + * type - {String} The layer identifier. Any non-birdseye imageryType + * from http://msdn.microsoft.com/en-us/library/ff701716.aspx can be + * used. + * + * Any other documented layer properties can be provided in the config object. + */ + initialize: function(options) { + options = OpenLayers.Util.applyDefaults({ + sphericalMercator: true + }, options); + var name = options.name || "Bing " + (options.type || this.type); + + var newArgs = [name, null, options]; + OpenLayers.Layer.XYZ.prototype.initialize.apply(this, newArgs); + this.tileOptions = OpenLayers.Util.extend({ + crossOriginKeyword: 'anonymous' + }, this.options.tileOptions); + this.loadMetadata(); + }, + + /** + * Method: loadMetadata + */ + loadMetadata: function() { + this._callbackId = "_callback_" + this.id.replace(/\./g, "_"); + // link the processMetadata method to the global scope and bind it + // to this instance + window[this._callbackId] = OpenLayers.Function.bind( + OpenLayers.Layer.Bing.processMetadata, this + ); + var params = OpenLayers.Util.applyDefaults({ + key: this.key, + jsonp: this._callbackId, + include: "ImageryProviders" + }, this.metadataParams); + var url = this.protocol + "//dev.virtualearth.net/REST/v1/Imagery/Metadata/" + + this.type + "?" + OpenLayers.Util.getParameterString(params); + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = url; + script.id = this._callbackId; + document.getElementsByTagName("head")[0].appendChild(script); + }, + + /** + * Method: initLayer + * + * Sets layer properties according to the metadata provided by the API + */ + initLayer: function() { + var res = this.metadata.resourceSets[0].resources[0]; + var url = res.imageUrl.replace("{quadkey}", "${quadkey}"); + url = url.replace("{culture}", this.culture); + url = url.replace(this.protocolRegex, this.protocol); + this.url = []; + for (var i=0; i<res.imageUrlSubdomains.length; ++i) { + this.url.push(url.replace("{subdomain}", res.imageUrlSubdomains[i])); + } + this.addOptions({ + maxResolution: Math.min( + this.serverResolutions[res.zoomMin], + this.maxResolution || Number.POSITIVE_INFINITY + ), + numZoomLevels: Math.min( + res.zoomMax + 1 - res.zoomMin, this.numZoomLevels + ) + }, true); + if (!this.isBaseLayer) { + this.redraw(); + } + this.updateAttribution(); + }, + + /** + * Method: getURL + * + * Paramters: + * bounds - {<OpenLayers.Bounds>} + */ + getURL: function(bounds) { + if (!this.url) { + return; + } + var xyz = this.getXYZ(bounds), x = xyz.x, y = xyz.y, z = xyz.z; + var quadDigits = []; + for (var i = z; i > 0; --i) { + var digit = '0'; + var mask = 1 << (i - 1); + if ((x & mask) != 0) { + digit++; + } + if ((y & mask) != 0) { + digit++; + digit++; + } + quadDigits.push(digit); + } + var quadKey = quadDigits.join(""); + var url = this.selectUrl('' + x + y + z, this.url); + + return OpenLayers.String.format(url, {'quadkey': quadKey}); + }, + + /** + * Method: updateAttribution + * Updates the attribution according to the requirements outlined in + * http://gis.638310.n2.nabble.com/Bing-imagery-td5789168.html + */ + updateAttribution: function() { + var metadata = this.metadata; + if (!metadata.resourceSets || !this.map || !this.map.center) { + return; + } + var res = metadata.resourceSets[0].resources[0]; + var extent = this.map.getExtent().transform( + this.map.getProjectionObject(), + new OpenLayers.Projection("EPSG:4326") + ); + var providers = res.imageryProviders || [], + zoom = OpenLayers.Util.indexOf(this.serverResolutions, + this.getServerResolution()), + copyrights = "", provider, i, ii, j, jj, bbox, coverage; + for (i=0,ii=providers.length; i<ii; ++i) { + provider = providers[i]; + for (j=0,jj=provider.coverageAreas.length; j<jj; ++j) { + coverage = provider.coverageAreas[j]; + // axis order provided is Y,X + bbox = OpenLayers.Bounds.fromArray(coverage.bbox, true); + if (extent.intersectsBounds(bbox) && + zoom <= coverage.zoomMax && zoom >= coverage.zoomMin) { + copyrights += provider.attribution + " "; + } + } + } + var logo = metadata.brandLogoUri.replace(this.protocolRegex, this.protocol); + this.attribution = OpenLayers.String.format(this.attributionTemplate, { + type: this.type.toLowerCase(), + logo: logo, + copyrights: copyrights + }); + this.map && this.map.events.triggerEvent("changelayer", { + layer: this, + property: "attribution" + }); + }, + + /** + * Method: setMap + */ + setMap: function() { + OpenLayers.Layer.XYZ.prototype.setMap.apply(this, arguments); + this.map.events.register("moveend", this, this.updateAttribution); + }, + + /** + * APIMethod: clone + * + * Parameters: + * obj - {Object} + * + * Returns: + * {<OpenLayers.Layer.Bing>} An exact clone of this <OpenLayers.Layer.Bing> + */ + clone: function(obj) { + if (obj == null) { + obj = new OpenLayers.Layer.Bing(this.options); + } + //get all additions from superclasses + obj = OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); + // copy/set any non-init, non-simple values here + return obj; + }, + + /** + * Method: destroy + */ + destroy: function() { + this.map && + this.map.events.unregister("moveend", this, this.updateAttribution); + OpenLayers.Layer.XYZ.prototype.destroy.apply(this, arguments); + }, + + CLASS_NAME: "OpenLayers.Layer.Bing" +}); + +/** + * Function: OpenLayers.Layer.Bing.processMetadata + * This function will be bound to an instance, linked to the global scope with + * an id, and called by the JSONP script returned by the API. + * + * Parameters: + * metadata - {Object} metadata as returned by the API + */ +OpenLayers.Layer.Bing.processMetadata = function(metadata) { + this.metadata = metadata; + this.initLayer(); + var script = document.getElementById(this._callbackId); + script.parentNode.removeChild(script); + window[this._callbackId] = undefined; // cannot delete from window in IE + delete this._callbackId; +}; +/* ====================================================================== + OpenLayers/Geometry/MultiLineString.js + ====================================================================== */ + +/* 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/Geometry/Collection.js + * @requires OpenLayers/Geometry/LineString.js + */ + +/** + * Class: OpenLayers.Geometry.MultiLineString + * A MultiLineString is a geometry with multiple <OpenLayers.Geometry.LineString> + * components. + * + * Inherits from: + * - <OpenLayers.Geometry.Collection> + * - <OpenLayers.Geometry> + */ +OpenLayers.Geometry.MultiLineString = OpenLayers.Class( + OpenLayers.Geometry.Collection, { + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null value means the + * component types are not restricted. + */ + componentTypes: ["OpenLayers.Geometry.LineString"], + + /** + * Constructor: OpenLayers.Geometry.MultiLineString + * Constructor for a MultiLineString Geometry. + * + * Parameters: + * components - {Array(<OpenLayers.Geometry.LineString>)} + * + */ + + /** + * Method: split + * Use this geometry (the source) to attempt to split a target geometry. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Properties of this object will be used to determine + * how the split is conducted. + * + * Valid options: + * mutual - {Boolean} Split the source geometry in addition to the target + * geometry. Default is false. + * edge - {Boolean} Allow splitting when only edges intersect. Default is + * true. If false, a vertex on the source must be within the tolerance + * distance of the intersection to be considered a split. + * tolerance - {Number} If a non-null value is provided, intersections + * within the tolerance distance of an existing vertex on the source + * will be assumed to occur at the vertex. + * + * Returns: + * {Array} A list of geometries (of this same type as the target) that + * result from splitting the target with the source geometry. The + * source and target geometry will remain unmodified. If no split + * results, null will be returned. If mutual is true and a split + * results, return will be an array of two arrays - the first will be + * all geometries that result from splitting the source geometry and + * the second will be all geometries that result from splitting the + * target geometry. + */ + split: function(geometry, options) { + var results = null; + var mutual = options && options.mutual; + var splits, sourceLine, sourceLines, sourceSplit, targetSplit; + var sourceParts = []; + var targetParts = [geometry]; + for(var i=0, len=this.components.length; i<len; ++i) { + sourceLine = this.components[i]; + sourceSplit = false; + for(var j=0; j < targetParts.length; ++j) { + splits = sourceLine.split(targetParts[j], options); + if(splits) { + if(mutual) { + sourceLines = splits[0]; + for(var k=0, klen=sourceLines.length; k<klen; ++k) { + if(k===0 && sourceParts.length) { + sourceParts[sourceParts.length-1].addComponent( + sourceLines[k] + ); + } else { + sourceParts.push( + new OpenLayers.Geometry.MultiLineString([ + sourceLines[k] + ]) + ); + } + } + sourceSplit = true; + splits = splits[1]; + } + if(splits.length) { + // splice in new target parts + splits.unshift(j, 1); + Array.prototype.splice.apply(targetParts, splits); + break; + } + } + } + if(!sourceSplit) { + // source line was not hit + if(sourceParts.length) { + // add line to existing multi + sourceParts[sourceParts.length-1].addComponent( + sourceLine.clone() + ); + } else { + // create a fresh multi + sourceParts = [ + new OpenLayers.Geometry.MultiLineString( + sourceLine.clone() + ) + ]; + } + } + } + if(sourceParts && sourceParts.length > 1) { + sourceSplit = true; + } else { + sourceParts = []; + } + if(targetParts && targetParts.length > 1) { + targetSplit = true; + } else { + targetParts = []; + } + if(sourceSplit || targetSplit) { + if(mutual) { + results = [sourceParts, targetParts]; + } else { + results = targetParts; + } + } + return results; + }, + + /** + * Method: splitWith + * Split this geometry (the target) with the given geometry (the source). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} A geometry used to split this + * geometry (the source). + * options - {Object} Properties of this object will be used to determine + * how the split is conducted. + * + * Valid options: + * mutual - {Boolean} Split the source geometry in addition to the target + * geometry. Default is false. + * edge - {Boolean} Allow splitting when only edges intersect. Default is + * true. If false, a vertex on the source must be within the tolerance + * distance of the intersection to be considered a split. + * tolerance - {Number} If a non-null value is provided, intersections + * within the tolerance distance of an existing vertex on the source + * will be assumed to occur at the vertex. + * + * Returns: + * {Array} A list of geometries (of this same type as the target) that + * result from splitting the target with the source geometry. The + * source and target geometry will remain unmodified. If no split + * results, null will be returned. If mutual is true and a split + * results, return will be an array of two arrays - the first will be + * all geometries that result from splitting the source geometry and + * the second will be all geometries that result from splitting the + * target geometry. + */ + splitWith: function(geometry, options) { + var results = null; + var mutual = options && options.mutual; + var splits, targetLine, sourceLines, sourceSplit, targetSplit, sourceParts, targetParts; + if(geometry instanceof OpenLayers.Geometry.LineString) { + targetParts = []; + sourceParts = [geometry]; + for(var i=0, len=this.components.length; i<len; ++i) { + targetSplit = false; + targetLine = this.components[i]; + for(var j=0; j<sourceParts.length; ++j) { + splits = sourceParts[j].split(targetLine, options); + if(splits) { + if(mutual) { + sourceLines = splits[0]; + if(sourceLines.length) { + // splice in new source parts + sourceLines.unshift(j, 1); + Array.prototype.splice.apply(sourceParts, sourceLines); + j += sourceLines.length - 2; + } + splits = splits[1]; + if(splits.length === 0) { + splits = [targetLine.clone()]; + } + } + for(var k=0, klen=splits.length; k<klen; ++k) { + if(k===0 && targetParts.length) { + targetParts[targetParts.length-1].addComponent( + splits[k] + ); + } else { + targetParts.push( + new OpenLayers.Geometry.MultiLineString([ + splits[k] + ]) + ); + } + } + targetSplit = true; + } + } + if(!targetSplit) { + // target component was not hit + if(targetParts.length) { + // add it to any existing multi-line + targetParts[targetParts.length-1].addComponent( + targetLine.clone() + ); + } else { + // or start with a fresh multi-line + targetParts = [ + new OpenLayers.Geometry.MultiLineString([ + targetLine.clone() + ]) + ]; + } + + } + } + } else { + results = geometry.split(this); + } + if(sourceParts && sourceParts.length > 1) { + sourceSplit = true; + } else { + sourceParts = []; + } + if(targetParts && targetParts.length > 1) { + targetSplit = true; + } else { + targetParts = []; + } + if(sourceSplit || targetSplit) { + if(mutual) { + results = [sourceParts, targetParts]; + } else { + results = targetParts; + } + } + return results; + }, + + CLASS_NAME: "OpenLayers.Geometry.MultiLineString" +}); +/* ====================================================================== + OpenLayers/Format.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Format + * Base class for format reading/writing a variety of formats. Subclasses + * of OpenLayers.Format are expected to have read and write methods. + */ +OpenLayers.Format = OpenLayers.Class({ + + /** + * Property: options + * {Object} A reference to options passed to the constructor. + */ + options: null, + + /** + * APIProperty: externalProjection + * {<OpenLayers.Projection>} When passed a externalProjection and + * internalProjection, the format will reproject the geometries it + * reads or writes. The externalProjection is the projection used by + * the content which is passed into read or which comes out of write. + * In order to reproject, a projection transformation function for the + * specified projections must be available. This support may be + * provided via proj4js or via a custom transformation function. See + * {<OpenLayers.Projection.addTransform>} for more information on + * custom transformations. + */ + externalProjection: null, + + /** + * APIProperty: internalProjection + * {<OpenLayers.Projection>} When passed a externalProjection and + * internalProjection, the format will reproject the geometries it + * reads or writes. The internalProjection is the projection used by + * the geometries which are returned by read or which are passed into + * write. In order to reproject, a projection transformation function + * for the specified projections must be available. This support may be + * provided via proj4js or via a custom transformation function. See + * {<OpenLayers.Projection.addTransform>} for more information on + * custom transformations. + */ + internalProjection: null, + + /** + * APIProperty: data + * {Object} When <keepData> is true, this is the parsed string sent to + * <read>. + */ + data: null, + + /** + * APIProperty: keepData + * {Object} Maintain a reference (<data>) to the most recently read data. + * Default is false. + */ + keepData: false, + + /** + * Constructor: OpenLayers.Format + * Instances of this class are not useful. See one of the subclasses. + * + * Parameters: + * options - {Object} An optional object with properties to set on the + * format + * + * Valid options: + * keepData - {Boolean} If true, upon <read>, the data property will be + * set to the parsed object (e.g. the json or xml object). + * + * Returns: + * An instance of OpenLayers.Format + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + this.options = options; + }, + + /** + * APIMethod: destroy + * Clean up. + */ + destroy: function() { + }, + + /** + * Method: read + * Read data from a string, and return an object whose type depends on the + * subclass. + * + * Parameters: + * data - {string} Data to read/parse. + * + * Returns: + * Depends on the subclass + */ + read: function(data) { + throw new Error('Read not implemented.'); + }, + + /** + * Method: write + * Accept an object, and return a string. + * + * Parameters: + * object - {Object} Object to be serialized + * + * Returns: + * {String} A string representation of the object. + */ + write: function(object) { + throw new Error('Write not implemented.'); + }, + + CLASS_NAME: "OpenLayers.Format" +}); +/* ====================================================================== + OpenLayers/Format/XML.js + ====================================================================== */ + +/* 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/Format.js + */ + +/** + * Class: OpenLayers.Format.XML + * Read and write XML. For cross-browser XML generation, use methods on an + * instance of the XML format class instead of on <code>document<end>. + * The DOM creation and traversing methods exposed here all mimic the + * W3C XML DOM methods. Create a new parser with the + * <OpenLayers.Format.XML> constructor. + * + * Inherits from: + * - <OpenLayers.Format> + */ +OpenLayers.Format.XML = OpenLayers.Class(OpenLayers.Format, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. Properties + * of this object should not be set individually. Read-only. All + * XML subclasses should have their own namespaces object. Use + * <setNamespace> to add or set a namespace alias after construction. + */ + namespaces: null, + + /** + * Property: namespaceAlias + * {Object} Mapping of namespace URI to namespace alias. This object + * is read-only. Use <setNamespace> to add or set a namespace alias. + */ + namespaceAlias: null, + + /** + * Property: defaultPrefix + * {String} The default namespace alias for creating element nodes. + */ + defaultPrefix: null, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: {}, + + /** + * Property: writers + * As a compliment to the <readers> property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: {}, + + /** + * Property: xmldom + * {XMLDom} If this browser uses ActiveX, this will be set to a XMLDOM + * object. It is not intended to be a browser sniffing property. + * Instead, the xmldom property is used instead of <code>document<end> + * where namespaced node creation methods are not supported. In all + * other browsers, this remains null. + */ + xmldom: null, + + /** + * Constructor: OpenLayers.Format.XML + * Construct an XML parser. The parser is used to read and write XML. + * Reading XML from a string returns a DOM element. Writing XML from + * a DOM element returns a string. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on + * the object. + */ + initialize: function(options) { + if(window.ActiveXObject) { + this.xmldom = new ActiveXObject("Microsoft.XMLDOM"); + } + OpenLayers.Format.prototype.initialize.apply(this, [options]); + // clone the namespace object and set all namespace aliases + this.namespaces = OpenLayers.Util.extend({}, this.namespaces); + this.namespaceAlias = {}; + for(var alias in this.namespaces) { + this.namespaceAlias[this.namespaces[alias]] = alias; + } + }, + + /** + * APIMethod: destroy + * Clean up. + */ + destroy: function() { + this.xmldom = null; + OpenLayers.Format.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: setNamespace + * Set a namespace alias and URI for the format. + * + * Parameters: + * alias - {String} The namespace alias (prefix). + * uri - {String} The namespace URI. + */ + setNamespace: function(alias, uri) { + this.namespaces[alias] = uri; + this.namespaceAlias[uri] = alias; + }, + + /** + * APIMethod: read + * Deserialize a XML string and return a DOM node. + * + * Parameters: + * text - {String} A XML string + + * Returns: + * {DOMElement} A DOM node + */ + read: function(text) { + var index = text.indexOf('<'); + if(index > 0) { + text = text.substring(index); + } + var node = OpenLayers.Util.Try( + OpenLayers.Function.bind(( + function() { + var xmldom; + /** + * Since we want to be able to call this method on the prototype + * itself, this.xmldom may not exist even if in IE. + */ + if(window.ActiveXObject && !this.xmldom) { + xmldom = new ActiveXObject("Microsoft.XMLDOM"); + } else { + xmldom = this.xmldom; + + } + xmldom.loadXML(text); + return xmldom; + } + ), this), + function() { + return new DOMParser().parseFromString(text, 'text/xml'); + }, + function() { + var req = new XMLHttpRequest(); + req.open("GET", "data:" + "text/xml" + + ";charset=utf-8," + encodeURIComponent(text), false); + if(req.overrideMimeType) { + req.overrideMimeType("text/xml"); + } + req.send(null); + return req.responseXML; + } + ); + + if(this.keepData) { + this.data = node; + } + + return node; + }, + + /** + * APIMethod: write + * Serialize a DOM node into a XML string. + * + * Parameters: + * node - {DOMElement} A DOM node. + * + * Returns: + * {String} The XML string representation of the input node. + */ + write: function(node) { + var data; + if(this.xmldom) { + data = node.xml; + } else { + var serializer = new XMLSerializer(); + if (node.nodeType == 1) { + // Add nodes to a document before serializing. Everything else + // is serialized as is. This may need more work. See #1218 . + var doc = document.implementation.createDocument("", "", null); + if (doc.importNode) { + node = doc.importNode(node, true); + } + doc.appendChild(node); + data = serializer.serializeToString(doc); + } else { + data = serializer.serializeToString(node); + } + } + return data; + }, + + /** + * APIMethod: createElementNS + * Create a new element with namespace. This node can be appended to + * another node with the standard node.appendChild method. For + * cross-browser support, this method must be used instead of + * document.createElementNS. + * + * Parameters: + * uri - {String} Namespace URI for the element. + * name - {String} The qualified name of the element (prefix:localname). + * + * Returns: + * {Element} A DOM element with namespace. + */ + createElementNS: function(uri, name) { + var element; + if(this.xmldom) { + if(typeof uri == "string") { + element = this.xmldom.createNode(1, name, uri); + } else { + element = this.xmldom.createNode(1, name, ""); + } + } else { + element = document.createElementNS(uri, name); + } + return element; + }, + + /** + * APIMethod: createDocumentFragment + * Create a document fragment node that can be appended to another node + * created by createElementNS. This will call + * document.createDocumentFragment outside of IE. In IE, the ActiveX + * object's createDocumentFragment method is used. + * + * Returns: + * {Element} A document fragment. + */ + createDocumentFragment: function() { + var element; + if (this.xmldom) { + element = this.xmldom.createDocumentFragment(); + } else { + element = document.createDocumentFragment(); + } + return element; + }, + + /** + * APIMethod: createTextNode + * Create a text node. This node can be appended to another node with + * the standard node.appendChild method. For cross-browser support, + * this method must be used instead of document.createTextNode. + * + * Parameters: + * text - {String} The text of the node. + * + * Returns: + * {DOMElement} A DOM text node. + */ + createTextNode: function(text) { + var node; + if (typeof text !== "string") { + text = String(text); + } + if(this.xmldom) { + node = this.xmldom.createTextNode(text); + } else { + node = document.createTextNode(text); + } + return node; + }, + + /** + * APIMethod: getElementsByTagNameNS + * Get a list of elements on a node given the namespace URI and local name. + * To return all nodes in a given namespace, use '*' for the name + * argument. To return all nodes of a given (local) name, regardless + * of namespace, use '*' for the uri argument. + * + * Parameters: + * node - {Element} Node on which to search for other nodes. + * uri - {String} Namespace URI. + * name - {String} Local name of the tag (without the prefix). + * + * Returns: + * {NodeList} A node list or array of elements. + */ + getElementsByTagNameNS: function(node, uri, name) { + var elements = []; + if(node.getElementsByTagNameNS) { + elements = node.getElementsByTagNameNS(uri, name); + } else { + // brute force method + var allNodes = node.getElementsByTagName("*"); + var potentialNode, fullName; + for(var i=0, len=allNodes.length; i<len; ++i) { + potentialNode = allNodes[i]; + fullName = (potentialNode.prefix) ? + (potentialNode.prefix + ":" + name) : name; + if((name == "*") || (fullName == potentialNode.nodeName)) { + if((uri == "*") || (uri == potentialNode.namespaceURI)) { + elements.push(potentialNode); + } + } + } + } + return elements; + }, + + /** + * APIMethod: getAttributeNodeNS + * Get an attribute node given the namespace URI and local name. + * + * Parameters: + * node - {Element} Node on which to search for attribute nodes. + * uri - {String} Namespace URI. + * name - {String} Local name of the attribute (without the prefix). + * + * Returns: + * {DOMElement} An attribute node or null if none found. + */ + getAttributeNodeNS: function(node, uri, name) { + var attributeNode = null; + if(node.getAttributeNodeNS) { + attributeNode = node.getAttributeNodeNS(uri, name); + } else { + var attributes = node.attributes; + var potentialNode, fullName; + for(var i=0, len=attributes.length; i<len; ++i) { + potentialNode = attributes[i]; + if(potentialNode.namespaceURI == uri) { + fullName = (potentialNode.prefix) ? + (potentialNode.prefix + ":" + name) : name; + if(fullName == potentialNode.nodeName) { + attributeNode = potentialNode; + break; + } + } + } + } + return attributeNode; + }, + + /** + * APIMethod: getAttributeNS + * Get an attribute value given the namespace URI and local name. + * + * Parameters: + * node - {Element} Node on which to search for an attribute. + * uri - {String} Namespace URI. + * name - {String} Local name of the attribute (without the prefix). + * + * Returns: + * {String} An attribute value or and empty string if none found. + */ + getAttributeNS: function(node, uri, name) { + var attributeValue = ""; + if(node.getAttributeNS) { + attributeValue = node.getAttributeNS(uri, name) || ""; + } else { + var attributeNode = this.getAttributeNodeNS(node, uri, name); + if(attributeNode) { + attributeValue = attributeNode.nodeValue; + } + } + return attributeValue; + }, + + /** + * APIMethod: getChildValue + * Get the textual value of the node if it exists, or return an + * optional default string. Returns an empty string if no first child + * exists and no default value is supplied. + * + * Parameters: + * node - {DOMElement} The element used to look for a first child value. + * def - {String} Optional string to return in the event that no + * first child value exists. + * + * Returns: + * {String} The value of the first child of the given node. + */ + getChildValue: function(node, def) { + var value = def || ""; + if(node) { + for(var child=node.firstChild; child; child=child.nextSibling) { + switch(child.nodeType) { + case 3: // text node + case 4: // cdata section + value += child.nodeValue; + } + } + } + return value; + }, + + /** + * APIMethod: isSimpleContent + * Test if the given node has only simple content (i.e. no child element + * nodes). + * + * Parameters: + * node - {DOMElement} An element node. + * + * Returns: + * {Boolean} The node has no child element nodes (nodes of type 1). + */ + isSimpleContent: function(node) { + var simple = true; + for(var child=node.firstChild; child; child=child.nextSibling) { + if(child.nodeType === 1) { + simple = false; + break; + } + } + return simple; + }, + + /** + * APIMethod: contentType + * Determine the content type for a given node. + * + * Parameters: + * node - {DOMElement} + * + * Returns: + * {Integer} One of OpenLayers.Format.XML.CONTENT_TYPE.{EMPTY,SIMPLE,COMPLEX,MIXED} + * if the node has no, simple, complex, or mixed content. + */ + contentType: function(node) { + var simple = false, + complex = false; + + var type = OpenLayers.Format.XML.CONTENT_TYPE.EMPTY; + + for(var child=node.firstChild; child; child=child.nextSibling) { + switch(child.nodeType) { + case 1: // element + complex = true; + break; + case 8: // comment + break; + default: + simple = true; + } + if(complex && simple) { + break; + } + } + + if(complex && simple) { + type = OpenLayers.Format.XML.CONTENT_TYPE.MIXED; + } else if(complex) { + return OpenLayers.Format.XML.CONTENT_TYPE.COMPLEX; + } else if(simple) { + return OpenLayers.Format.XML.CONTENT_TYPE.SIMPLE; + } + return type; + }, + + /** + * APIMethod: hasAttributeNS + * Determine whether a node has a particular attribute matching the given + * name and namespace. + * + * Parameters: + * node - {Element} Node on which to search for an attribute. + * uri - {String} Namespace URI. + * name - {String} Local name of the attribute (without the prefix). + * + * Returns: + * {Boolean} The node has an attribute matching the name and namespace. + */ + hasAttributeNS: function(node, uri, name) { + var found = false; + if(node.hasAttributeNS) { + found = node.hasAttributeNS(uri, name); + } else { + found = !!this.getAttributeNodeNS(node, uri, name); + } + return found; + }, + + /** + * APIMethod: setAttributeNS + * Adds a new attribute or changes the value of an attribute with the given + * namespace and name. + * + * Parameters: + * node - {Element} Element node on which to set the attribute. + * uri - {String} Namespace URI for the attribute. + * name - {String} Qualified name (prefix:localname) for the attribute. + * value - {String} Attribute value. + */ + setAttributeNS: function(node, uri, name, value) { + if(node.setAttributeNS) { + node.setAttributeNS(uri, name, value); + } else { + if(this.xmldom) { + if(uri) { + var attribute = node.ownerDocument.createNode( + 2, name, uri + ); + attribute.nodeValue = value; + node.setAttributeNode(attribute); + } else { + node.setAttribute(name, value); + } + } else { + throw "setAttributeNS not implemented"; + } + } + }, + + /** + * Method: createElementNSPlus + * Shorthand for creating namespaced elements with optional attributes and + * child text nodes. + * + * Parameters: + * name - {String} The qualified node name. + * options - {Object} Optional object for node configuration. + * + * Valid options: + * uri - {String} Optional namespace uri for the element - supply a prefix + * instead if the namespace uri is a property of the format's namespace + * object. + * attributes - {Object} Optional attributes to be set using the + * <setAttributes> method. + * value - {String} Optional text to be appended as a text node. + * + * Returns: + * {Element} An element node. + */ + createElementNSPlus: function(name, options) { + options = options || {}; + // order of prefix preference + // 1. in the uri option + // 2. in the prefix option + // 3. in the qualified name + // 4. from the defaultPrefix + var uri = options.uri || this.namespaces[options.prefix]; + if(!uri) { + var loc = name.indexOf(":"); + uri = this.namespaces[name.substring(0, loc)]; + } + if(!uri) { + uri = this.namespaces[this.defaultPrefix]; + } + var node = this.createElementNS(uri, name); + if(options.attributes) { + this.setAttributes(node, options.attributes); + } + var value = options.value; + if(value != null) { + node.appendChild(this.createTextNode(value)); + } + return node; + }, + + /** + * Method: setAttributes + * Set multiple attributes given key value pairs from an object. + * + * Parameters: + * node - {Element} An element node. + * obj - {Object || Array} An object whose properties represent attribute + * names and values represent attribute values. If an attribute name + * is a qualified name ("prefix:local"), the prefix will be looked up + * in the parsers {namespaces} object. If the prefix is found, + * setAttributeNS will be used instead of setAttribute. + */ + setAttributes: function(node, obj) { + var value, uri; + for(var name in obj) { + if(obj[name] != null && obj[name].toString) { + value = obj[name].toString(); + // check for qualified attribute name ("prefix:local") + uri = this.namespaces[name.substring(0, name.indexOf(":"))] || null; + this.setAttributeNS(node, uri, name, value); + } + } + }, + + /** + * Method: readNode + * Shorthand for applying one of the named readers given the node + * namespace and local name. Readers take two args (node, obj) and + * generally extend or modify the second. + * + * Parameters: + * node - {DOMElement} The node to be read (required). + * obj - {Object} The object to be modified (optional). + * + * Returns: + * {Object} The input object, modified (or a new one if none was provided). + */ + readNode: function(node, obj) { + if(!obj) { + obj = {}; + } + var group = this.readers[node.namespaceURI ? this.namespaceAlias[node.namespaceURI]: this.defaultPrefix]; + if(group) { + var local = node.localName || node.nodeName.split(":").pop(); + var reader = group[local] || group["*"]; + if(reader) { + reader.apply(this, [node, obj]); + } + } + return obj; + }, + + /** + * Method: readChildNodes + * Shorthand for applying the named readers to all children of a node. + * For each child of type 1 (element), <readSelf> is called. + * + * Parameters: + * node - {DOMElement} The node to be read (required). + * obj - {Object} The object to be modified (optional). + * + * Returns: + * {Object} The input object, modified. + */ + readChildNodes: function(node, obj) { + if(!obj) { + obj = {}; + } + var children = node.childNodes; + var child; + for(var i=0, len=children.length; i<len; ++i) { + child = children[i]; + if(child.nodeType == 1) { + this.readNode(child, obj); + } + } + return obj; + }, + + /** + * Method: writeNode + * Shorthand for applying one of the named writers and appending the + * results to a node. If a qualified name is not provided for the + * second argument (and a local name is used instead), the namespace + * of the parent node will be assumed. + * + * Parameters: + * name - {String} The name of a node to generate. If a qualified name + * (e.g. "pre:Name") is used, the namespace prefix is assumed to be + * in the <writers> group. If a local name is used (e.g. "Name") then + * the namespace of the parent is assumed. If a local name is used + * and no parent is supplied, then the default namespace is assumed. + * obj - {Object} Structure containing data for the writer. + * parent - {DOMElement} Result will be appended to this node. If no parent + * is supplied, the node will not be appended to anything. + * + * Returns: + * {DOMElement} The child node. + */ + writeNode: function(name, obj, parent) { + var prefix, local; + var split = name.indexOf(":"); + if(split > 0) { + prefix = name.substring(0, split); + local = name.substring(split + 1); + } else { + if(parent) { + prefix = this.namespaceAlias[parent.namespaceURI]; + } else { + prefix = this.defaultPrefix; + } + local = name; + } + var child = this.writers[prefix][local].apply(this, [obj]); + if(parent) { + parent.appendChild(child); + } + return child; + }, + + /** + * APIMethod: getChildEl + * Get the first child element. Optionally only return the first child + * if it matches the given name and namespace URI. + * + * Parameters: + * node - {DOMElement} The parent node. + * name - {String} Optional node name (local) to search for. + * uri - {String} Optional namespace URI to search for. + * + * Returns: + * {DOMElement} The first child. Returns null if no element is found, if + * something significant besides an element is found, or if the element + * found does not match the optional name and uri. + */ + getChildEl: function(node, name, uri) { + return node && this.getThisOrNextEl(node.firstChild, name, uri); + }, + + /** + * APIMethod: getNextEl + * Get the next sibling element. Optionally get the first sibling only + * if it matches the given local name and namespace URI. + * + * Parameters: + * node - {DOMElement} The node. + * name - {String} Optional local name of the sibling to search for. + * uri - {String} Optional namespace URI of the sibling to search for. + * + * Returns: + * {DOMElement} The next sibling element. Returns null if no element is + * found, something significant besides an element is found, or the + * found element does not match the optional name and uri. + */ + getNextEl: function(node, name, uri) { + return node && this.getThisOrNextEl(node.nextSibling, name, uri); + }, + + /** + * Method: getThisOrNextEl + * Return this node or the next element node. Optionally get the first + * sibling with the given local name or namespace URI. + * + * Parameters: + * node - {DOMElement} The node. + * name - {String} Optional local name of the sibling to search for. + * uri - {String} Optional namespace URI of the sibling to search for. + * + * Returns: + * {DOMElement} The next sibling element. Returns null if no element is + * found, something significant besides an element is found, or the + * found element does not match the query. + */ + getThisOrNextEl: function(node, name, uri) { + outer: for(var sibling=node; sibling; sibling=sibling.nextSibling) { + switch(sibling.nodeType) { + case 1: // Element + if((!name || name === (sibling.localName || sibling.nodeName.split(":").pop())) && + (!uri || uri === sibling.namespaceURI)) { + // matches + break outer; + } + sibling = null; + break outer; + case 3: // Text + if(/^\s*$/.test(sibling.nodeValue)) { + break; + } + case 4: // CDATA + case 6: // ENTITY_NODE + case 12: // NOTATION_NODE + case 10: // DOCUMENT_TYPE_NODE + case 11: // DOCUMENT_FRAGMENT_NODE + sibling = null; + break outer; + } // ignore comments and processing instructions + } + return sibling || null; + }, + + /** + * APIMethod: lookupNamespaceURI + * Takes a prefix and returns the namespace URI associated with it on the given + * node if found (and null if not). Supplying null for the prefix will + * return the default namespace. + * + * For browsers that support it, this calls the native lookupNamesapceURI + * function. In other browsers, this is an implementation of + * http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespaceURI. + * + * For browsers that don't support the attribute.ownerElement property, this + * method cannot be called on attribute nodes. + * + * Parameters: + * node - {DOMElement} The node from which to start looking. + * prefix - {String} The prefix to lookup or null to lookup the default namespace. + * + * Returns: + * {String} The namespace URI for the given prefix. Returns null if the prefix + * cannot be found or the node is the wrong type. + */ + lookupNamespaceURI: function(node, prefix) { + var uri = null; + if(node) { + if(node.lookupNamespaceURI) { + uri = node.lookupNamespaceURI(prefix); + } else { + outer: switch(node.nodeType) { + case 1: // ELEMENT_NODE + if(node.namespaceURI !== null && node.prefix === prefix) { + uri = node.namespaceURI; + break outer; + } + var len = node.attributes.length; + if(len) { + var attr; + for(var i=0; i<len; ++i) { + attr = node.attributes[i]; + if(attr.prefix === "xmlns" && attr.name === "xmlns:" + prefix) { + uri = attr.value || null; + break outer; + } else if(attr.name === "xmlns" && prefix === null) { + uri = attr.value || null; + break outer; + } + } + } + uri = this.lookupNamespaceURI(node.parentNode, prefix); + break outer; + case 2: // ATTRIBUTE_NODE + uri = this.lookupNamespaceURI(node.ownerElement, prefix); + break outer; + case 9: // DOCUMENT_NODE + uri = this.lookupNamespaceURI(node.documentElement, prefix); + break outer; + case 6: // ENTITY_NODE + case 12: // NOTATION_NODE + case 10: // DOCUMENT_TYPE_NODE + case 11: // DOCUMENT_FRAGMENT_NODE + break outer; + default: + // TEXT_NODE (3), CDATA_SECTION_NODE (4), ENTITY_REFERENCE_NODE (5), + // PROCESSING_INSTRUCTION_NODE (7), COMMENT_NODE (8) + uri = this.lookupNamespaceURI(node.parentNode, prefix); + break outer; + } + } + } + return uri; + }, + + /** + * Method: getXMLDoc + * Get an XML document for nodes that are not supported in HTML (e.g. + * createCDATASection). On IE, this will either return an existing or + * create a new <xmldom> on the instance. On other browsers, this will + * either return an existing or create a new shared document (see + * <OpenLayers.Format.XML.document>). + * + * Returns: + * {XMLDocument} + */ + getXMLDoc: function() { + if (!OpenLayers.Format.XML.document && !this.xmldom) { + if (document.implementation && document.implementation.createDocument) { + OpenLayers.Format.XML.document = + document.implementation.createDocument("", "", null); + } else if (!this.xmldom && window.ActiveXObject) { + this.xmldom = new ActiveXObject("Microsoft.XMLDOM"); + } + } + return OpenLayers.Format.XML.document || this.xmldom; + }, + + CLASS_NAME: "OpenLayers.Format.XML" + +}); + +OpenLayers.Format.XML.CONTENT_TYPE = {EMPTY: 0, SIMPLE: 1, COMPLEX: 2, MIXED: 3}; + +/** + * APIFunction: OpenLayers.Format.XML.lookupNamespaceURI + * Takes a prefix and returns the namespace URI associated with it on the given + * node if found (and null if not). Supplying null for the prefix will + * return the default namespace. + * + * For browsers that support it, this calls the native lookupNamesapceURI + * function. In other browsers, this is an implementation of + * http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespaceURI. + * + * For browsers that don't support the attribute.ownerElement property, this + * method cannot be called on attribute nodes. + * + * Parameters: + * node - {DOMElement} The node from which to start looking. + * prefix - {String} The prefix to lookup or null to lookup the default namespace. + * + * Returns: + * {String} The namespace URI for the given prefix. Returns null if the prefix + * cannot be found or the node is the wrong type. + */ +OpenLayers.Format.XML.lookupNamespaceURI = OpenLayers.Function.bind( + OpenLayers.Format.XML.prototype.lookupNamespaceURI, + OpenLayers.Format.XML.prototype +); + +/** + * Property: OpenLayers.Format.XML.document + * {XMLDocument} XML document to reuse for creating non-HTML compliant nodes, + * like document.createCDATASection. + */ +OpenLayers.Format.XML.document = null; +/* ====================================================================== + OpenLayers/Format/OGCExceptionReport.js + ====================================================================== */ + +/* 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/Format/XML.js + */ + +/** + * Class: OpenLayers.Format.OGCExceptionReport + * Class to read exception reports for various OGC services and versions. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.OGCExceptionReport = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. + */ + namespaces: { + ogc: "http://www.opengis.net/ogc" + }, + + /** + * Property: regExes + * Compiled regular expressions for manipulating strings. + */ + regExes: { + trimSpace: (/^\s*|\s*$/g), + removeSpace: (/\s*/g), + splitSpace: (/\s+/), + trimComma: (/\s*,\s*/g) + }, + + /** + * Property: defaultPrefix + */ + defaultPrefix: "ogc", + + /** + * Constructor: OpenLayers.Format.OGCExceptionReport + * Create a new parser for OGC exception reports. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + + /** + * APIMethod: read + * Read OGC exception report data from a string, and return an object with + * information about the exceptions. + * + * Parameters: + * data - {String} or {DOMElement} data to read/parse. + * + * Returns: + * {Object} Information about the exceptions that occurred. + */ + read: function(data) { + var result; + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + var root = data.documentElement; + var exceptionInfo = {exceptionReport: null}; + if (root) { + this.readChildNodes(data, exceptionInfo); + if (exceptionInfo.exceptionReport === null) { + // fall-back to OWSCommon since this is a common output format for exceptions + // we cannot easily use the ows readers directly since they differ for 1.0 and 1.1 + exceptionInfo = new OpenLayers.Format.OWSCommon().read(data); + } + } + return exceptionInfo; + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "ogc": { + "ServiceExceptionReport": function(node, obj) { + obj.exceptionReport = {exceptions: []}; + this.readChildNodes(node, obj.exceptionReport); + }, + "ServiceException": function(node, exceptionReport) { + var exception = { + code: node.getAttribute("code"), + locator: node.getAttribute("locator"), + text: this.getChildValue(node) + }; + exceptionReport.exceptions.push(exception); + } + } + }, + + CLASS_NAME: "OpenLayers.Format.OGCExceptionReport" + +}); +/* ====================================================================== + OpenLayers/Format/XML/VersionedOGC.js + ====================================================================== */ + +/* 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/Format/XML.js + * @requires OpenLayers/Format/OGCExceptionReport.js + */ + +/** + * Class: OpenLayers.Format.XML.VersionedOGC + * Base class for versioned formats, i.e. a format which supports multiple + * versions. + * + * To enable checking if parsing succeeded, you will need to define a property + * called errorProperty on the parser you want to check. The parser will then + * check the returned object to see if that property is present. If it is, it + * assumes the parsing was successful. If it is not present (or is null), it will + * pass the document through an OGCExceptionReport parser. + * + * If errorProperty is undefined for the parser, this error checking mechanism + * will be disabled. + * + * + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.XML.VersionedOGC = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * APIProperty: defaultVersion + * {String} Version number to assume if none found. + */ + defaultVersion: null, + + /** + * APIProperty: version + * {String} Specify a version string if one is known. + */ + version: null, + + /** + * APIProperty: profile + * {String} If provided, use a custom profile. + */ + profile: null, + + /** + * APIProperty: allowFallback + * {Boolean} If a profiled parser cannot be found for the returned version, + * use a non-profiled parser as the fallback. Application code using this + * should take into account that the return object structure might be + * missing the specifics of the profile. Defaults to false. + */ + allowFallback: false, + + /** + * Property: name + * {String} The name of this parser, this is the part of the CLASS_NAME + * except for "OpenLayers.Format." + */ + name: null, + + /** + * APIProperty: stringifyOutput + * {Boolean} If true, write will return a string otherwise a DOMElement. + * Default is false. + */ + stringifyOutput: false, + + /** + * Property: parser + * {Object} Instance of the versioned parser. Cached for multiple read and + * write calls of the same version. + */ + parser: null, + + /** + * Constructor: OpenLayers.Format.XML.VersionedOGC. + * Constructor. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on + * the object. + */ + initialize: function(options) { + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + var className = this.CLASS_NAME; + this.name = className.substring(className.lastIndexOf(".")+1); + }, + + /** + * Method: getVersion + * Returns the version to use. Subclasses can override this function + * if a different version detection is needed. + * + * Parameters: + * root - {DOMElement} + * options - {Object} Optional configuration object. + * + * Returns: + * {String} The version to use. + */ + getVersion: function(root, options) { + var version; + // read + if (root) { + version = this.version; + if(!version) { + version = root.getAttribute("version"); + if(!version) { + version = this.defaultVersion; + } + } + } else { // write + version = (options && options.version) || + this.version || this.defaultVersion; + } + return version; + }, + + /** + * Method: getParser + * Get an instance of the cached parser if available, otherwise create one. + * + * Parameters: + * version - {String} + * + * Returns: + * {<OpenLayers.Format>} + */ + getParser: function(version) { + version = version || this.defaultVersion; + var profile = this.profile ? "_" + this.profile : ""; + if(!this.parser || this.parser.VERSION != version) { + var format = OpenLayers.Format[this.name][ + "v" + version.replace(/\./g, "_") + profile + ]; + if(!format) { + if (profile !== "" && this.allowFallback) { + // fallback to the non-profiled version of the parser + profile = ""; + format = OpenLayers.Format[this.name][ + "v" + version.replace(/\./g, "_") + ]; + } + if (!format) { + throw "Can't find a " + this.name + " parser for version " + + version + profile; + } + } + this.parser = new format(this.options); + } + return this.parser; + }, + + /** + * APIMethod: write + * Write a document. + * + * Parameters: + * obj - {Object} An object representing the document. + * options - {Object} Optional configuration object. + * + * Returns: + * {String} The document as a string + */ + write: function(obj, options) { + var version = this.getVersion(null, options); + this.parser = this.getParser(version); + var root = this.parser.write(obj, options); + if (this.stringifyOutput === false) { + return root; + } else { + return OpenLayers.Format.XML.prototype.write.apply(this, [root]); + } + }, + + /** + * APIMethod: read + * Read a doc and return an object representing the document. + * + * Parameters: + * data - {String | DOMElement} Data to read. + * options - {Object} Options for the reader. + * + * Returns: + * {Object} An object representing the document. + */ + read: function(data, options) { + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + var root = data.documentElement; + var version = this.getVersion(root); + this.parser = this.getParser(version); // Select the parser + var obj = this.parser.read(data, options); // Parse the data + + var errorProperty = this.parser.errorProperty || null; + if (errorProperty !== null && obj[errorProperty] === undefined) { + // an error must have happened, so parse it and report back + var format = new OpenLayers.Format.OGCExceptionReport(); + obj.error = format.read(data); + } + obj.version = version; + return obj; + }, + + CLASS_NAME: "OpenLayers.Format.XML.VersionedOGC" +}); +/* ====================================================================== + OpenLayers/Feature.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Feature + * Features are combinations of geography and attributes. The OpenLayers.Feature + * class specifically combines a marker and a lonlat. + */ +OpenLayers.Feature = OpenLayers.Class({ + + /** + * Property: layer + * {<OpenLayers.Layer>} + */ + layer: null, + + /** + * Property: id + * {String} + */ + id: null, + + /** + * Property: lonlat + * {<OpenLayers.LonLat>} + */ + lonlat: null, + + /** + * Property: data + * {Object} + */ + data: null, + + /** + * Property: marker + * {<OpenLayers.Marker>} + */ + marker: null, + + /** + * APIProperty: popupClass + * {<OpenLayers.Class>} The class which will be used to instantiate + * a new Popup. Default is <OpenLayers.Popup.Anchored>. + */ + popupClass: null, + + /** + * Property: popup + * {<OpenLayers.Popup>} + */ + popup: null, + + /** + * Constructor: OpenLayers.Feature + * Constructor for features. + * + * Parameters: + * layer - {<OpenLayers.Layer>} + * lonlat - {<OpenLayers.LonLat>} + * data - {Object} + * + * Returns: + * {<OpenLayers.Feature>} + */ + initialize: function(layer, lonlat, data) { + this.layer = layer; + this.lonlat = lonlat; + this.data = (data != null) ? data : {}; + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + }, + + /** + * Method: destroy + * nullify references to prevent circular references and memory leaks + */ + destroy: function() { + + //remove the popup from the map + if ((this.layer != null) && (this.layer.map != null)) { + if (this.popup != null) { + this.layer.map.removePopup(this.popup); + } + } + // remove the marker from the layer + if (this.layer != null && this.marker != null) { + this.layer.removeMarker(this.marker); + } + + this.layer = null; + this.id = null; + this.lonlat = null; + this.data = null; + if (this.marker != null) { + this.destroyMarker(this.marker); + this.marker = null; + } + if (this.popup != null) { + this.destroyPopup(this.popup); + this.popup = null; + } + }, + + /** + * Method: onScreen + * + * Returns: + * {Boolean} Whether or not the feature is currently visible on screen + * (based on its 'lonlat' property) + */ + onScreen:function() { + + var onScreen = false; + if ((this.layer != null) && (this.layer.map != null)) { + var screenBounds = this.layer.map.getExtent(); + onScreen = screenBounds.containsLonLat(this.lonlat); + } + return onScreen; + }, + + + /** + * Method: createMarker + * Based on the data associated with the Feature, create and return a marker object. + * + * Returns: + * {<OpenLayers.Marker>} A Marker Object created from the 'lonlat' and 'icon' properties + * set in this.data. If no 'lonlat' is set, returns null. If no + * 'icon' is set, OpenLayers.Marker() will load the default image. + * + * Note - this.marker is set to return value + * + */ + createMarker: function() { + + if (this.lonlat != null) { + this.marker = new OpenLayers.Marker(this.lonlat, this.data.icon); + } + return this.marker; + }, + + /** + * Method: destroyMarker + * Destroys marker. + * If user overrides the createMarker() function, s/he should be able + * to also specify an alternative function for destroying it + */ + destroyMarker: function() { + this.marker.destroy(); + }, + + /** + * Method: createPopup + * Creates a popup object created from the 'lonlat', 'popupSize', + * and 'popupContentHTML' properties set in this.data. It uses + * this.marker.icon as default anchor. + * + * If no 'lonlat' is set, returns null. + * If no this.marker has been created, no anchor is sent. + * + * Note - the returned popup object is 'owned' by the feature, so you + * cannot use the popup's destroy method to discard the popup. + * Instead, you must use the feature's destroyPopup + * + * Note - this.popup is set to return value + * + * Parameters: + * closeBox - {Boolean} create popup with closebox or not + * + * Returns: + * {<OpenLayers.Popup>} Returns the created popup, which is also set + * as 'popup' property of this feature. Will be of whatever type + * specified by this feature's 'popupClass' property, but must be + * of type <OpenLayers.Popup>. + * + */ + createPopup: function(closeBox) { + + if (this.lonlat != null) { + if (!this.popup) { + var anchor = (this.marker) ? this.marker.icon : null; + var popupClass = this.popupClass ? + this.popupClass : OpenLayers.Popup.Anchored; + this.popup = new popupClass(this.id + "_popup", + this.lonlat, + this.data.popupSize, + this.data.popupContentHTML, + anchor, + closeBox); + } + if (this.data.overflow != null) { + this.popup.contentDiv.style.overflow = this.data.overflow; + } + + this.popup.feature = this; + } + return this.popup; + }, + + + /** + * Method: destroyPopup + * Destroys the popup created via createPopup. + * + * As with the marker, if user overrides the createPopup() function, s/he + * should also be able to override the destruction + */ + destroyPopup: function() { + if (this.popup) { + this.popup.feature = null; + this.popup.destroy(); + this.popup = null; + } + }, + + CLASS_NAME: "OpenLayers.Feature" +}); +/* ====================================================================== + OpenLayers/Feature/Vector.js + ====================================================================== */ + +/* 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. */ + +// TRASH THIS +OpenLayers.State = { + /** states */ + UNKNOWN: 'Unknown', + INSERT: 'Insert', + UPDATE: 'Update', + DELETE: 'Delete' +}; + +/** + * @requires OpenLayers/Feature.js + * @requires OpenLayers/Util.js + */ + +/** + * Class: OpenLayers.Feature.Vector + * Vector features use the OpenLayers.Geometry classes as geometry description. + * They have an 'attributes' property, which is the data object, and a 'style' + * property, the default values of which are defined in the + * <OpenLayers.Feature.Vector.style> objects. + * + * Inherits from: + * - <OpenLayers.Feature> + */ +OpenLayers.Feature.Vector = OpenLayers.Class(OpenLayers.Feature, { + + /** + * Property: fid + * {String} + */ + fid: null, + + /** + * APIProperty: geometry + * {<OpenLayers.Geometry>} + */ + geometry: null, + + /** + * APIProperty: attributes + * {Object} This object holds arbitrary, serializable properties that + * describe the feature. + */ + attributes: null, + + /** + * Property: bounds + * {<OpenLayers.Bounds>} The box bounding that feature's geometry, that + * property can be set by an <OpenLayers.Format> object when + * deserializing the feature, so in most cases it represents an + * information set by the server. + */ + bounds: null, + + /** + * Property: state + * {String} + */ + state: null, + + /** + * APIProperty: style + * {Object} + */ + style: null, + + /** + * APIProperty: url + * {String} If this property is set it will be taken into account by + * {<OpenLayers.HTTP>} when upadting or deleting the feature. + */ + url: null, + + /** + * Property: renderIntent + * {String} rendering intent currently being used + */ + renderIntent: "default", + + /** + * APIProperty: modified + * {Object} An object with the originals of the geometry and attributes of + * the feature, if they were changed. Currently this property is only read + * by <OpenLayers.Format.WFST.v1>, and written by + * <OpenLayers.Control.ModifyFeature>, which sets the geometry property. + * Applications can set the originals of modified attributes in the + * attributes property. Note that applications have to check if this + * object and the attributes property is already created before using it. + * After a change made with ModifyFeature, this object could look like + * + * (code) + * { + * geometry: >Object + * } + * (end) + * + * When an application has made changes to feature attributes, it could + * have set the attributes to something like this: + * + * (code) + * { + * attributes: { + * myAttribute: "original" + * } + * } + * (end) + * + * Note that <OpenLayers.Format.WFST.v1> only checks for truthy values in + * *modified.geometry* and the attribute names in *modified.attributes*, + * but it is recommended to set the original values (and not just true) as + * attribute value, so applications could use this information to undo + * changes. + */ + modified: null, + + /** + * Constructor: OpenLayers.Feature.Vector + * Create a vector feature. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The geometry that this feature + * represents. + * attributes - {Object} An optional object that will be mapped to the + * <attributes> property. + * style - {Object} An optional style object. + */ + initialize: function(geometry, attributes, style) { + OpenLayers.Feature.prototype.initialize.apply(this, + [null, null, attributes]); + this.lonlat = null; + this.geometry = geometry ? geometry : null; + this.state = null; + this.attributes = {}; + if (attributes) { + this.attributes = OpenLayers.Util.extend(this.attributes, + attributes); + } + this.style = style ? style : null; + }, + + /** + * Method: destroy + * nullify references to prevent circular references and memory leaks + */ + destroy: function() { + if (this.layer) { + this.layer.removeFeatures(this); + this.layer = null; + } + + this.geometry = null; + this.modified = null; + OpenLayers.Feature.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: clone + * Create a clone of this vector feature. Does not set any non-standard + * properties. + * + * Returns: + * {<OpenLayers.Feature.Vector>} An exact clone of this vector feature. + */ + clone: function () { + return new OpenLayers.Feature.Vector( + this.geometry ? this.geometry.clone() : null, + this.attributes, + this.style); + }, + + /** + * Method: onScreen + * Determine whether the feature is within the map viewport. This method + * tests for an intersection between the geometry and the viewport + * bounds. If a more effecient but less precise geometry bounds + * intersection is desired, call the method with the boundsOnly + * parameter true. + * + * Parameters: + * boundsOnly - {Boolean} Only test whether a feature's bounds intersects + * the viewport bounds. Default is false. If false, the feature's + * geometry must intersect the viewport for onScreen to return true. + * + * Returns: + * {Boolean} The feature is currently visible on screen (optionally + * based on its bounds if boundsOnly is true). + */ + onScreen:function(boundsOnly) { + var onScreen = false; + if(this.layer && this.layer.map) { + var screenBounds = this.layer.map.getExtent(); + if(boundsOnly) { + var featureBounds = this.geometry.getBounds(); + onScreen = screenBounds.intersectsBounds(featureBounds); + } else { + var screenPoly = screenBounds.toGeometry(); + onScreen = screenPoly.intersects(this.geometry); + } + } + return onScreen; + }, + + /** + * Method: getVisibility + * Determine whether the feature is displayed or not. It may not displayed + * because: + * - its style display property is set to 'none', + * - it doesn't belong to any layer, + * - the styleMap creates a symbolizer with display property set to 'none' + * for it, + * - the layer which it belongs to is not visible. + * + * Returns: + * {Boolean} The feature is currently displayed. + */ + getVisibility: function() { + return !(this.style && this.style.display == 'none' || + !this.layer || + this.layer && this.layer.styleMap && + this.layer.styleMap.createSymbolizer(this, this.renderIntent).display == 'none' || + this.layer && !this.layer.getVisibility()); + }, + + /** + * Method: createMarker + * HACK - we need to decide if all vector features should be able to + * create markers + * + * Returns: + * {<OpenLayers.Marker>} For now just returns null + */ + createMarker: function() { + return null; + }, + + /** + * Method: destroyMarker + * HACK - we need to decide if all vector features should be able to + * delete markers + * + * If user overrides the createMarker() function, s/he should be able + * to also specify an alternative function for destroying it + */ + destroyMarker: function() { + // pass + }, + + /** + * Method: createPopup + * HACK - we need to decide if all vector features should be able to + * create popups + * + * Returns: + * {<OpenLayers.Popup>} For now just returns null + */ + createPopup: function() { + return null; + }, + + /** + * Method: atPoint + * Determins whether the feature intersects with the specified location. + * + * Parameters: + * lonlat - {<OpenLayers.LonLat>|Object} OpenLayers.LonLat or an + * object with a 'lon' and 'lat' properties. + * toleranceLon - {float} Optional tolerance in Geometric Coords + * toleranceLat - {float} Optional tolerance in Geographic Coords + * + * Returns: + * {Boolean} Whether or not the feature is at the specified location + */ + atPoint: function(lonlat, toleranceLon, toleranceLat) { + var atPoint = false; + if(this.geometry) { + atPoint = this.geometry.atPoint(lonlat, toleranceLon, + toleranceLat); + } + return atPoint; + }, + + /** + * Method: destroyPopup + * HACK - we need to decide if all vector features should be able to + * delete popups + */ + destroyPopup: function() { + // pass + }, + + /** + * Method: move + * Moves the feature and redraws it at its new location + * + * Parameters: + * location - {<OpenLayers.LonLat> or <OpenLayers.Pixel>} the + * location to which to move the feature. + */ + move: function(location) { + + if(!this.layer || !this.geometry.move){ + //do nothing if no layer or immoveable geometry + return undefined; + } + + var pixel; + if (location.CLASS_NAME == "OpenLayers.LonLat") { + pixel = this.layer.getViewPortPxFromLonLat(location); + } else { + pixel = location; + } + + var lastPixel = this.layer.getViewPortPxFromLonLat(this.geometry.getBounds().getCenterLonLat()); + var res = this.layer.map.getResolution(); + this.geometry.move(res * (pixel.x - lastPixel.x), + res * (lastPixel.y - pixel.y)); + this.layer.drawFeature(this); + return lastPixel; + }, + + /** + * Method: toState + * Sets the new state + * + * Parameters: + * state - {String} + */ + toState: function(state) { + if (state == OpenLayers.State.UPDATE) { + switch (this.state) { + case OpenLayers.State.UNKNOWN: + case OpenLayers.State.DELETE: + this.state = state; + break; + case OpenLayers.State.UPDATE: + case OpenLayers.State.INSERT: + break; + } + } else if (state == OpenLayers.State.INSERT) { + switch (this.state) { + case OpenLayers.State.UNKNOWN: + break; + default: + this.state = state; + break; + } + } else if (state == OpenLayers.State.DELETE) { + switch (this.state) { + case OpenLayers.State.INSERT: + // the feature should be destroyed + break; + case OpenLayers.State.DELETE: + break; + case OpenLayers.State.UNKNOWN: + case OpenLayers.State.UPDATE: + this.state = state; + break; + } + } else if (state == OpenLayers.State.UNKNOWN) { + this.state = state; + } + }, + + CLASS_NAME: "OpenLayers.Feature.Vector" +}); + + +/** + * Constant: OpenLayers.Feature.Vector.style + * OpenLayers features can have a number of style attributes. The 'default' + * style will typically be used if no other style is specified. These + * styles correspond for the most part, to the styling properties defined + * by the SVG standard. + * Information on fill properties: http://www.w3.org/TR/SVG/painting.html#FillProperties + * Information on stroke properties: http://www.w3.org/TR/SVG/painting.html#StrokeProperties + * + * Symbolizer properties: + * fill - {Boolean} Set to false if no fill is desired. + * fillColor - {String} Hex fill color. Default is "#ee9900". + * fillOpacity - {Number} Fill opacity (0-1). Default is 0.4 + * stroke - {Boolean} Set to false if no stroke is desired. + * strokeColor - {String} Hex stroke color. Default is "#ee9900". + * strokeOpacity - {Number} Stroke opacity (0-1). Default is 1. + * strokeWidth - {Number} Pixel stroke width. Default is 1. + * strokeLinecap - {String} Stroke cap type. Default is "round". [butt | round | square] + * strokeDashstyle - {String} Stroke dash style. Default is "solid". [dot | dash | dashdot | longdash | longdashdot | solid] + * graphic - {Boolean} Set to false if no graphic is desired. + * pointRadius - {Number} Pixel point radius. Default is 6. + * pointerEvents - {String} Default is "visiblePainted". + * cursor - {String} Default is "". + * externalGraphic - {String} Url to an external graphic that will be used for rendering points. + * graphicWidth - {Number} Pixel width for sizing an external graphic. + * graphicHeight - {Number} Pixel height for sizing an external graphic. + * graphicOpacity - {Number} Opacity (0-1) for an external graphic. + * graphicXOffset - {Number} Pixel offset along the positive x axis for displacing an external graphic. + * graphicYOffset - {Number} Pixel offset along the positive y axis for displacing an external graphic. + * rotation - {Number} For point symbolizers, this is the rotation of a graphic in the clockwise direction about its center point (or any point off center as specified by graphicXOffset and graphicYOffset). + * graphicZIndex - {Number} The integer z-index value to use in rendering. + * graphicName - {String} Named graphic to use when rendering points. Supported values include "circle" (default), + * "square", "star", "x", "cross", "triangle". + * graphicTitle - {String} Tooltip when hovering over a feature. *deprecated*, use title instead + * title - {String} Tooltip when hovering over a feature. Not supported by the canvas renderer. + * backgroundGraphic - {String} Url to a graphic to be used as the background under an externalGraphic. + * backgroundGraphicZIndex - {Number} The integer z-index value to use in rendering the background graphic. + * backgroundXOffset - {Number} The x offset (in pixels) for the background graphic. + * backgroundYOffset - {Number} The y offset (in pixels) for the background graphic. + * backgroundHeight - {Number} The height of the background graphic. If not provided, the graphicHeight will be used. + * backgroundWidth - {Number} The width of the background width. If not provided, the graphicWidth will be used. + * label - {String} The text for an optional label. For browsers that use the canvas renderer, this requires either + * fillText or mozDrawText to be available. + * labelAlign - {String} Label alignment. This specifies the insertion point relative to the text. It is a string + * composed of two characters. The first character is for the horizontal alignment, the second for the vertical + * alignment. Valid values for horizontal alignment: "l"=left, "c"=center, "r"=right. Valid values for vertical + * alignment: "t"=top, "m"=middle, "b"=bottom. Example values: "lt", "cm", "rb". Default is "cm". + * labelXOffset - {Number} Pixel offset along the positive x axis for displacing the label. Not supported by the canvas renderer. + * labelYOffset - {Number} Pixel offset along the positive y axis for displacing the label. Not supported by the canvas renderer. + * labelSelect - {Boolean} If set to true, labels will be selectable using SelectFeature or similar controls. + * Default is false. + * labelOutlineColor - {String} The color of the label outline. Default is 'white'. Only supported by the canvas & SVG renderers. + * labelOutlineWidth - {Number} The width of the label outline. Default is 3, set to 0 or null to disable. Only supported by the SVG renderers. + * labelOutlineOpacity - {Number} The opacity (0-1) of the label outline. Default is fontOpacity. Only supported by the canvas & SVG renderers. + * fontColor - {String} The font color for the label, to be provided like CSS. + * fontOpacity - {Number} Opacity (0-1) for the label + * fontFamily - {String} The font family for the label, to be provided like in CSS. + * fontSize - {String} The font size for the label, to be provided like in CSS. + * fontStyle - {String} The font style for the label, to be provided like in CSS. + * fontWeight - {String} The font weight for the label, to be provided like in CSS. + * display - {String} Symbolizers will have no effect if display is set to "none". All other values have no effect. + */ +OpenLayers.Feature.Vector.style = { + 'default': { + fillColor: "#ee9900", + fillOpacity: 0.4, + hoverFillColor: "white", + hoverFillOpacity: 0.8, + strokeColor: "#ee9900", + strokeOpacity: 1, + strokeWidth: 1, + strokeLinecap: "round", + strokeDashstyle: "solid", + hoverStrokeColor: "red", + hoverStrokeOpacity: 1, + hoverStrokeWidth: 0.2, + pointRadius: 6, + hoverPointRadius: 1, + hoverPointUnit: "%", + pointerEvents: "visiblePainted", + cursor: "inherit", + fontColor: "#000000", + labelAlign: "cm", + labelOutlineColor: "white", + labelOutlineWidth: 3 + }, + 'select': { + fillColor: "blue", + fillOpacity: 0.4, + hoverFillColor: "white", + hoverFillOpacity: 0.8, + strokeColor: "blue", + strokeOpacity: 1, + strokeWidth: 2, + strokeLinecap: "round", + strokeDashstyle: "solid", + hoverStrokeColor: "red", + hoverStrokeOpacity: 1, + hoverStrokeWidth: 0.2, + pointRadius: 6, + hoverPointRadius: 1, + hoverPointUnit: "%", + pointerEvents: "visiblePainted", + cursor: "pointer", + fontColor: "#000000", + labelAlign: "cm", + labelOutlineColor: "white", + labelOutlineWidth: 3 + + }, + 'temporary': { + fillColor: "#66cccc", + fillOpacity: 0.2, + hoverFillColor: "white", + hoverFillOpacity: 0.8, + strokeColor: "#66cccc", + strokeOpacity: 1, + strokeLinecap: "round", + strokeWidth: 2, + strokeDashstyle: "solid", + hoverStrokeColor: "red", + hoverStrokeOpacity: 1, + hoverStrokeWidth: 0.2, + pointRadius: 6, + hoverPointRadius: 1, + hoverPointUnit: "%", + pointerEvents: "visiblePainted", + cursor: "inherit", + fontColor: "#000000", + labelAlign: "cm", + labelOutlineColor: "white", + labelOutlineWidth: 3 + + }, + 'delete': { + display: "none" + } +}; +/* ====================================================================== + OpenLayers/Style.js + ====================================================================== */ + +/* 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/Feature/Vector.js + */ + +/** + * Class: OpenLayers.Style + * This class represents a UserStyle obtained + * from a SLD, containing styling rules. + */ +OpenLayers.Style = OpenLayers.Class({ + + /** + * Property: id + * {String} A unique id for this session. + */ + id: null, + + /** + * APIProperty: name + * {String} + */ + name: null, + + /** + * Property: title + * {String} Title of this style (set if included in SLD) + */ + title: null, + + /** + * Property: description + * {String} Description of this style (set if abstract is included in SLD) + */ + description: null, + + /** + * APIProperty: layerName + * {<String>} name of the layer that this style belongs to, usually + * according to the NamedLayer attribute of an SLD document. + */ + layerName: null, + + /** + * APIProperty: isDefault + * {Boolean} + */ + isDefault: false, + + /** + * Property: rules + * {Array(<OpenLayers.Rule>)} + */ + rules: null, + + /** + * APIProperty: context + * {Object} An optional object with properties that symbolizers' property + * values should be evaluated against. If no context is specified, + * feature.attributes will be used + */ + context: null, + + /** + * Property: defaultStyle + * {Object} hash of style properties to use as default for merging + * rule-based style symbolizers onto. If no rules are defined, + * createSymbolizer will return this style. If <defaultsPerSymbolizer> is set to + * true, the defaultStyle will only be taken into account if there are + * rules defined. + */ + defaultStyle: null, + + /** + * Property: defaultsPerSymbolizer + * {Boolean} If set to true, the <defaultStyle> will extend the symbolizer + * of every rule. Properties of the <defaultStyle> will also be used to set + * missing symbolizer properties if the symbolizer has stroke, fill or + * graphic set to true. Default is false. + */ + defaultsPerSymbolizer: false, + + /** + * Property: propertyStyles + * {Hash of Boolean} cache of style properties that need to be parsed for + * propertyNames. Property names are keys, values won't be used. + */ + propertyStyles: null, + + + /** + * Constructor: OpenLayers.Style + * Creates a UserStyle. + * + * Parameters: + * style - {Object} Optional hash of style properties that will be + * used as default style for this style object. This style + * applies if no rules are specified. Symbolizers defined in + * rules will extend this default style. + * options - {Object} An optional object with properties to set on the + * style. + * + * Valid options: + * rules - {Array(<OpenLayers.Rule>)} List of rules to be added to the + * style. + * + * Returns: + * {<OpenLayers.Style>} + */ + initialize: function(style, options) { + + OpenLayers.Util.extend(this, options); + this.rules = []; + if(options && options.rules) { + this.addRules(options.rules); + } + + // use the default style from OpenLayers.Feature.Vector if no style + // was given in the constructor + this.setDefaultStyle(style || + OpenLayers.Feature.Vector.style["default"]); + + this.id = OpenLayers.Util.createUniqueID(this.CLASS_NAME + "_"); + }, + + /** + * APIMethod: destroy + * nullify references to prevent circular references and memory leaks + */ + destroy: function() { + for (var i=0, len=this.rules.length; i<len; i++) { + this.rules[i].destroy(); + this.rules[i] = null; + } + this.rules = null; + this.defaultStyle = null; + }, + + /** + * Method: createSymbolizer + * creates a style by applying all feature-dependent rules to the base + * style. + * + * Parameters: + * feature - {<OpenLayers.Feature>} feature to evaluate rules for + * + * Returns: + * {Object} symbolizer hash + */ + createSymbolizer: function(feature) { + var style = this.defaultsPerSymbolizer ? {} : this.createLiterals( + OpenLayers.Util.extend({}, this.defaultStyle), feature); + + var rules = this.rules; + + var rule, context; + var elseRules = []; + var appliedRules = false; + for(var i=0, len=rules.length; i<len; i++) { + rule = rules[i]; + // does the rule apply? + var applies = rule.evaluate(feature); + + if(applies) { + if(rule instanceof OpenLayers.Rule && rule.elseFilter) { + elseRules.push(rule); + } else { + appliedRules = true; + this.applySymbolizer(rule, style, feature); + } + } + } + + // if no other rules apply, apply the rules with else filters + if(appliedRules == false && elseRules.length > 0) { + appliedRules = true; + for(var i=0, len=elseRules.length; i<len; i++) { + this.applySymbolizer(elseRules[i], style, feature); + } + } + + // don't display if there were rules but none applied + if(rules.length > 0 && appliedRules == false) { + style.display = "none"; + } + + if (style.label != null && typeof style.label !== "string") { + style.label = String(style.label); + } + + return style; + }, + + /** + * Method: applySymbolizer + * + * Parameters: + * rule - {<OpenLayers.Rule>} + * style - {Object} + * feature - {<OpenLayer.Feature.Vector>} + * + * Returns: + * {Object} A style with new symbolizer applied. + */ + applySymbolizer: function(rule, style, feature) { + var symbolizerPrefix = feature.geometry ? + this.getSymbolizerPrefix(feature.geometry) : + OpenLayers.Style.SYMBOLIZER_PREFIXES[0]; + + var symbolizer = rule.symbolizer[symbolizerPrefix] || rule.symbolizer; + + if(this.defaultsPerSymbolizer === true) { + var defaults = this.defaultStyle; + OpenLayers.Util.applyDefaults(symbolizer, { + pointRadius: defaults.pointRadius + }); + if(symbolizer.stroke === true || symbolizer.graphic === true) { + OpenLayers.Util.applyDefaults(symbolizer, { + strokeWidth: defaults.strokeWidth, + strokeColor: defaults.strokeColor, + strokeOpacity: defaults.strokeOpacity, + strokeDashstyle: defaults.strokeDashstyle, + strokeLinecap: defaults.strokeLinecap + }); + } + if(symbolizer.fill === true || symbolizer.graphic === true) { + OpenLayers.Util.applyDefaults(symbolizer, { + fillColor: defaults.fillColor, + fillOpacity: defaults.fillOpacity + }); + } + if(symbolizer.graphic === true) { + OpenLayers.Util.applyDefaults(symbolizer, { + pointRadius: this.defaultStyle.pointRadius, + externalGraphic: this.defaultStyle.externalGraphic, + graphicName: this.defaultStyle.graphicName, + graphicOpacity: this.defaultStyle.graphicOpacity, + graphicWidth: this.defaultStyle.graphicWidth, + graphicHeight: this.defaultStyle.graphicHeight, + graphicXOffset: this.defaultStyle.graphicXOffset, + graphicYOffset: this.defaultStyle.graphicYOffset + }); + } + } + + // merge the style with the current style + return this.createLiterals( + OpenLayers.Util.extend(style, symbolizer), feature); + }, + + /** + * Method: createLiterals + * creates literals for all style properties that have an entry in + * <this.propertyStyles>. + * + * Parameters: + * style - {Object} style to create literals for. Will be modified + * inline. + * feature - {Object} + * + * Returns: + * {Object} the modified style + */ + createLiterals: function(style, feature) { + var context = OpenLayers.Util.extend({}, feature.attributes || feature.data); + OpenLayers.Util.extend(context, this.context); + + for (var i in this.propertyStyles) { + style[i] = OpenLayers.Style.createLiteral(style[i], context, feature, i); + } + return style; + }, + + /** + * Method: findPropertyStyles + * Looks into all rules for this style and the defaultStyle to collect + * all the style hash property names containing ${...} strings that have + * to be replaced using the createLiteral method before returning them. + * + * Returns: + * {Object} hash of property names that need createLiteral parsing. The + * name of the property is the key, and the value is true; + */ + findPropertyStyles: function() { + var propertyStyles = {}; + + // check the default style + var style = this.defaultStyle; + this.addPropertyStyles(propertyStyles, style); + + // walk through all rules to check for properties in their symbolizer + var rules = this.rules; + var symbolizer, value; + for (var i=0, len=rules.length; i<len; i++) { + symbolizer = rules[i].symbolizer; + for (var key in symbolizer) { + value = symbolizer[key]; + if (typeof value == "object") { + // symbolizer key is "Point", "Line" or "Polygon" + this.addPropertyStyles(propertyStyles, value); + } else { + // symbolizer is a hash of style properties + this.addPropertyStyles(propertyStyles, symbolizer); + break; + } + } + } + return propertyStyles; + }, + + /** + * Method: addPropertyStyles + * + * Parameters: + * propertyStyles - {Object} hash to add new property styles to. Will be + * modified inline + * symbolizer - {Object} search this symbolizer for property styles + * + * Returns: + * {Object} propertyStyles hash + */ + addPropertyStyles: function(propertyStyles, symbolizer) { + var property; + for (var key in symbolizer) { + property = symbolizer[key]; + if (typeof property == "string" && + property.match(/\$\{\w+\}/)) { + propertyStyles[key] = true; + } + } + return propertyStyles; + }, + + /** + * APIMethod: addRules + * Adds rules to this style. + * + * Parameters: + * rules - {Array(<OpenLayers.Rule>)} + */ + addRules: function(rules) { + Array.prototype.push.apply(this.rules, rules); + this.propertyStyles = this.findPropertyStyles(); + }, + + /** + * APIMethod: setDefaultStyle + * Sets the default style for this style object. + * + * Parameters: + * style - {Object} Hash of style properties + */ + setDefaultStyle: function(style) { + this.defaultStyle = style; + this.propertyStyles = this.findPropertyStyles(); + }, + + /** + * Method: getSymbolizerPrefix + * Returns the correct symbolizer prefix according to the + * geometry type of the passed geometry + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {String} key of the according symbolizer + */ + getSymbolizerPrefix: function(geometry) { + var prefixes = OpenLayers.Style.SYMBOLIZER_PREFIXES; + for (var i=0, len=prefixes.length; i<len; i++) { + if (geometry.CLASS_NAME.indexOf(prefixes[i]) != -1) { + return prefixes[i]; + } + } + }, + + /** + * APIMethod: clone + * Clones this style. + * + * Returns: + * {<OpenLayers.Style>} Clone of this style. + */ + clone: function() { + var options = OpenLayers.Util.extend({}, this); + // clone rules + if(this.rules) { + options.rules = []; + for(var i=0, len=this.rules.length; i<len; ++i) { + options.rules.push(this.rules[i].clone()); + } + } + // clone context + options.context = this.context && OpenLayers.Util.extend({}, this.context); + //clone default style + var defaultStyle = OpenLayers.Util.extend({}, this.defaultStyle); + return new OpenLayers.Style(defaultStyle, options); + }, + + CLASS_NAME: "OpenLayers.Style" +}); + + +/** + * Function: createLiteral + * converts a style value holding a combination of PropertyName and Literal + * into a Literal, taking the property values from the passed features. + * + * Parameters: + * value - {String} value to parse. If this string contains a construct like + * "foo ${bar}", then "foo " will be taken as literal, and "${bar}" + * will be replaced by the value of the "bar" attribute of the passed + * feature. + * context - {Object} context to take attribute values from + * feature - {<OpenLayers.Feature.Vector>} optional feature to pass to + * <OpenLayers.String.format> for evaluating functions in the + * context. + * property - {String} optional, name of the property for which the literal is + * being created for evaluating functions in the context. + * + * Returns: + * {String} the parsed value. In the example of the value parameter above, the + * result would be "foo valueOfBar", assuming that the passed feature has an + * attribute named "bar" with the value "valueOfBar". + */ +OpenLayers.Style.createLiteral = function(value, context, feature, property) { + if (typeof value == "string" && value.indexOf("${") != -1) { + value = OpenLayers.String.format(value, context, [feature, property]); + value = (isNaN(value) || !value) ? value : parseFloat(value); + } + return value; +}; + +/** + * Constant: OpenLayers.Style.SYMBOLIZER_PREFIXES + * {Array} prefixes of the sld symbolizers. These are the + * same as the main geometry types + */ +OpenLayers.Style.SYMBOLIZER_PREFIXES = ['Point', 'Line', 'Polygon', 'Text', + 'Raster']; +/* ====================================================================== + OpenLayers/Filter.js + ====================================================================== */ + +/* 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/Style.js + */ + +/** + * Class: OpenLayers.Filter + * This class represents an OGC Filter. + */ +OpenLayers.Filter = OpenLayers.Class({ + + /** + * Constructor: OpenLayers.Filter + * This class represents a generic filter. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + * + * Returns: + * {<OpenLayers.Filter>} + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + }, + + /** + * APIMethod: destroy + * Remove reference to anything added. + */ + destroy: function() { + }, + + /** + * APIMethod: evaluate + * Evaluates this filter in a specific context. Instances or subclasses + * are supposed to override this method. + * + * Parameters: + * context - {Object} Context to use in evaluating the filter. If a vector + * feature is provided, the feature.attributes will be used as context. + * + * Returns: + * {Boolean} The filter applies. + */ + evaluate: function(context) { + return true; + }, + + /** + * APIMethod: clone + * Clones this filter. Should be implemented by subclasses. + * + * Returns: + * {<OpenLayers.Filter>} Clone of this filter. + */ + clone: function() { + return null; + }, + + /** + * APIMethod: toString + * + * Returns: + * {String} Include <OpenLayers.Format.CQL> in your build to get a CQL + * representation of the filter returned. Otherwise "[Object object]" + * will be returned. + */ + toString: function() { + var string; + if (OpenLayers.Format && OpenLayers.Format.CQL) { + string = OpenLayers.Format.CQL.prototype.write(this); + } else { + string = Object.prototype.toString.call(this); + } + return string; + }, + + CLASS_NAME: "OpenLayers.Filter" +}); +/* ====================================================================== + OpenLayers/Filter/FeatureId.js + ====================================================================== */ + +/* 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/Filter.js + */ + +/** + * Class: OpenLayers.Filter.FeatureId + * This class represents a ogc:FeatureId Filter, as being used for rule-based SLD + * styling + * + * Inherits from: + * - <OpenLayers.Filter> + */ +OpenLayers.Filter.FeatureId = OpenLayers.Class(OpenLayers.Filter, { + + /** + * APIProperty: fids + * {Array(String)} Feature Ids to evaluate this rule against. + * To be passed inside the params object. + */ + fids: null, + + /** + * Property: type + * {String} Type to identify this filter. + */ + type: "FID", + + /** + * Constructor: OpenLayers.Filter.FeatureId + * Creates an ogc:FeatureId rule. + * + * Parameters: + * options - {Object} An optional object with properties to set on the + * rule + * + * Returns: + * {<OpenLayers.Filter.FeatureId>} + */ + initialize: function(options) { + this.fids = []; + OpenLayers.Filter.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: evaluate + * evaluates this rule for a specific feature + * + * Parameters: + * feature - {<OpenLayers.Feature>} feature to apply the rule to. + * For vector features, the check is run against the fid, + * for plain features against the id. + * + * Returns: + * {Boolean} true if the rule applies, false if it does not + */ + evaluate: function(feature) { + for (var i=0, len=this.fids.length; i<len; i++) { + var fid = feature.fid || feature.id; + if (fid == this.fids[i]) { + return true; + } + } + return false; + }, + + /** + * APIMethod: clone + * Clones this filter. + * + * Returns: + * {<OpenLayers.Filter.FeatureId>} Clone of this filter. + */ + clone: function() { + var filter = new OpenLayers.Filter.FeatureId(); + OpenLayers.Util.extend(filter, this); + filter.fids = this.fids.slice(); + return filter; + }, + + CLASS_NAME: "OpenLayers.Filter.FeatureId" +}); +/* ====================================================================== + OpenLayers/Filter/Logical.js + ====================================================================== */ + +/* 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/Filter.js + */ + +/** + * Class: OpenLayers.Filter.Logical + * This class represents ogc:And, ogc:Or and ogc:Not rules. + * + * Inherits from: + * - <OpenLayers.Filter> + */ +OpenLayers.Filter.Logical = OpenLayers.Class(OpenLayers.Filter, { + + /** + * APIProperty: filters + * {Array(<OpenLayers.Filter>)} Child filters for this filter. + */ + filters: null, + + /** + * APIProperty: type + * {String} type of logical operator. Available types are: + * - OpenLayers.Filter.Logical.AND = "&&"; + * - OpenLayers.Filter.Logical.OR = "||"; + * - OpenLayers.Filter.Logical.NOT = "!"; + */ + type: null, + + /** + * Constructor: OpenLayers.Filter.Logical + * Creates a logical filter (And, Or, Not). + * + * Parameters: + * options - {Object} An optional object with properties to set on the + * filter. + * + * Returns: + * {<OpenLayers.Filter.Logical>} + */ + initialize: function(options) { + this.filters = []; + OpenLayers.Filter.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: destroy + * Remove reference to child filters. + */ + destroy: function() { + this.filters = null; + OpenLayers.Filter.prototype.destroy.apply(this); + }, + + /** + * APIMethod: evaluate + * Evaluates this filter in a specific context. + * + * Parameters: + * context - {Object} Context to use in evaluating the filter. A vector + * feature may also be provided to evaluate feature attributes in + * comparison filters or geometries in spatial filters. + * + * Returns: + * {Boolean} The filter applies. + */ + evaluate: function(context) { + var i, len; + switch(this.type) { + case OpenLayers.Filter.Logical.AND: + for (i=0, len=this.filters.length; i<len; i++) { + if (this.filters[i].evaluate(context) == false) { + return false; + } + } + return true; + + case OpenLayers.Filter.Logical.OR: + for (i=0, len=this.filters.length; i<len; i++) { + if (this.filters[i].evaluate(context) == true) { + return true; + } + } + return false; + + case OpenLayers.Filter.Logical.NOT: + return (!this.filters[0].evaluate(context)); + } + return undefined; + }, + + /** + * APIMethod: clone + * Clones this filter. + * + * Returns: + * {<OpenLayers.Filter.Logical>} Clone of this filter. + */ + clone: function() { + var filters = []; + for(var i=0, len=this.filters.length; i<len; ++i) { + filters.push(this.filters[i].clone()); + } + return new OpenLayers.Filter.Logical({ + type: this.type, + filters: filters + }); + }, + + CLASS_NAME: "OpenLayers.Filter.Logical" +}); + + +OpenLayers.Filter.Logical.AND = "&&"; +OpenLayers.Filter.Logical.OR = "||"; +OpenLayers.Filter.Logical.NOT = "!"; +/* ====================================================================== + OpenLayers/Filter/Comparison.js + ====================================================================== */ + +/* 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/Filter.js + */ + +/** + * Class: OpenLayers.Filter.Comparison + * This class represents a comparison filter. + * + * Inherits from: + * - <OpenLayers.Filter> + */ +OpenLayers.Filter.Comparison = OpenLayers.Class(OpenLayers.Filter, { + + /** + * APIProperty: type + * {String} type: type of the comparison. This is one of + * - OpenLayers.Filter.Comparison.EQUAL_TO = "=="; + * - OpenLayers.Filter.Comparison.NOT_EQUAL_TO = "!="; + * - OpenLayers.Filter.Comparison.LESS_THAN = "<"; + * - OpenLayers.Filter.Comparison.GREATER_THAN = ">"; + * - OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO = "<="; + * - OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO = ">="; + * - OpenLayers.Filter.Comparison.BETWEEN = ".."; + * - OpenLayers.Filter.Comparison.LIKE = "~"; + * - OpenLayers.Filter.Comparison.IS_NULL = "NULL"; + */ + type: null, + + /** + * APIProperty: property + * {String} + * name of the context property to compare + */ + property: null, + + /** + * APIProperty: value + * {Number} or {String} + * comparison value for binary comparisons. In the case of a String, this + * can be a combination of text and propertyNames in the form + * "literal ${propertyName}" + */ + value: null, + + /** + * Property: matchCase + * {Boolean} Force case sensitive searches for EQUAL_TO and NOT_EQUAL_TO + * comparisons. The Filter Encoding 1.1 specification added a matchCase + * attribute to ogc:PropertyIsEqualTo and ogc:PropertyIsNotEqualTo + * elements. This property will be serialized with those elements only + * if using the v1.1.0 filter format. However, when evaluating filters + * here, the matchCase property will always be respected (for EQUAL_TO + * and NOT_EQUAL_TO). Default is true. + */ + matchCase: true, + + /** + * APIProperty: lowerBoundary + * {Number} or {String} + * lower boundary for between comparisons. In the case of a String, this + * can be a combination of text and propertyNames in the form + * "literal ${propertyName}" + */ + lowerBoundary: null, + + /** + * APIProperty: upperBoundary + * {Number} or {String} + * upper boundary for between comparisons. In the case of a String, this + * can be a combination of text and propertyNames in the form + * "literal ${propertyName}" + */ + upperBoundary: null, + + /** + * Constructor: OpenLayers.Filter.Comparison + * Creates a comparison rule. + * + * Parameters: + * options - {Object} An optional object with properties to set on the + * rule + * + * Returns: + * {<OpenLayers.Filter.Comparison>} + */ + initialize: function(options) { + OpenLayers.Filter.prototype.initialize.apply(this, [options]); + // since matchCase on PropertyIsLike is not schema compliant, we only + // want to use this if explicitly asked for + if (this.type === OpenLayers.Filter.Comparison.LIKE + && options.matchCase === undefined) { + this.matchCase = null; + } + }, + + /** + * APIMethod: evaluate + * Evaluates this filter in a specific context. + * + * Parameters: + * context - {Object} Context to use in evaluating the filter. If a vector + * feature is provided, the feature.attributes will be used as context. + * + * Returns: + * {Boolean} The filter applies. + */ + evaluate: function(context) { + if (context instanceof OpenLayers.Feature.Vector) { + context = context.attributes; + } + var result = false; + var got = context[this.property]; + var exp; + switch(this.type) { + case OpenLayers.Filter.Comparison.EQUAL_TO: + exp = this.value; + if(!this.matchCase && + typeof got == "string" && typeof exp == "string") { + result = (got.toUpperCase() == exp.toUpperCase()); + } else { + result = (got == exp); + } + break; + case OpenLayers.Filter.Comparison.NOT_EQUAL_TO: + exp = this.value; + if(!this.matchCase && + typeof got == "string" && typeof exp == "string") { + result = (got.toUpperCase() != exp.toUpperCase()); + } else { + result = (got != exp); + } + break; + case OpenLayers.Filter.Comparison.LESS_THAN: + result = got < this.value; + break; + case OpenLayers.Filter.Comparison.GREATER_THAN: + result = got > this.value; + break; + case OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO: + result = got <= this.value; + break; + case OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO: + result = got >= this.value; + break; + case OpenLayers.Filter.Comparison.BETWEEN: + result = (got >= this.lowerBoundary) && + (got <= this.upperBoundary); + break; + case OpenLayers.Filter.Comparison.LIKE: + var regexp = new RegExp(this.value, "gi"); + result = regexp.test(got); + break; + case OpenLayers.Filter.Comparison.IS_NULL: + result = (got === null); + break; + } + return result; + }, + + /** + * APIMethod: value2regex + * Converts the value of this rule into a regular expression string, + * according to the wildcard characters specified. This method has to + * be called after instantiation of this class, if the value is not a + * regular expression already. + * + * Parameters: + * wildCard - {Char} wildcard character in the above value, default + * is "*" + * singleChar - {Char} single-character wildcard in the above value + * default is "." + * escapeChar - {Char} escape character in the above value, default is + * "!" + * + * Returns: + * {String} regular expression string + */ + value2regex: function(wildCard, singleChar, escapeChar) { + if (wildCard == ".") { + throw new Error("'.' is an unsupported wildCard character for " + + "OpenLayers.Filter.Comparison"); + } + + + // set UMN MapServer defaults for unspecified parameters + wildCard = wildCard ? wildCard : "*"; + singleChar = singleChar ? singleChar : "."; + escapeChar = escapeChar ? escapeChar : "!"; + + this.value = this.value.replace( + new RegExp("\\"+escapeChar+"(.|$)", "g"), "\\$1"); + this.value = this.value.replace( + new RegExp("\\"+singleChar, "g"), "."); + this.value = this.value.replace( + new RegExp("\\"+wildCard, "g"), ".*"); + this.value = this.value.replace( + new RegExp("\\\\.\\*", "g"), "\\"+wildCard); + this.value = this.value.replace( + new RegExp("\\\\\\.", "g"), "\\"+singleChar); + + return this.value; + }, + + /** + * Method: regex2value + * Convert the value of this rule from a regular expression string into an + * ogc literal string using a wildCard of *, a singleChar of ., and an + * escape of !. Leaves the <value> property unmodified. + * + * Returns: + * {String} A string value. + */ + regex2value: function() { + + var value = this.value; + + // replace ! with !! + value = value.replace(/!/g, "!!"); + + // replace \. with !. (watching out for \\.) + value = value.replace(/(\\)?\\\./g, function($0, $1) { + return $1 ? $0 : "!."; + }); + + // replace \* with #* (watching out for \\*) + value = value.replace(/(\\)?\\\*/g, function($0, $1) { + return $1 ? $0 : "!*"; + }); + + // replace \\ with \ + value = value.replace(/\\\\/g, "\\"); + + // convert .* to * (the sequence #.* is not allowed) + value = value.replace(/\.\*/g, "*"); + + return value; + }, + + /** + * APIMethod: clone + * Clones this filter. + * + * Returns: + * {<OpenLayers.Filter.Comparison>} Clone of this filter. + */ + clone: function() { + return OpenLayers.Util.extend(new OpenLayers.Filter.Comparison(), this); + }, + + CLASS_NAME: "OpenLayers.Filter.Comparison" +}); + + +OpenLayers.Filter.Comparison.EQUAL_TO = "=="; +OpenLayers.Filter.Comparison.NOT_EQUAL_TO = "!="; +OpenLayers.Filter.Comparison.LESS_THAN = "<"; +OpenLayers.Filter.Comparison.GREATER_THAN = ">"; +OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO = "<="; +OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO = ">="; +OpenLayers.Filter.Comparison.BETWEEN = ".."; +OpenLayers.Filter.Comparison.LIKE = "~"; +OpenLayers.Filter.Comparison.IS_NULL = "NULL"; +/* ====================================================================== + OpenLayers/Format/Filter.js + ====================================================================== */ + +/* 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/Format/XML/VersionedOGC.js + * @requires OpenLayers/Filter/FeatureId.js + * @requires OpenLayers/Filter/Logical.js + * @requires OpenLayers/Filter/Comparison.js + */ + +/** + * Class: OpenLayers.Format.Filter + * Read/Write ogc:Filter. Create a new instance with the <OpenLayers.Format.Filter> + * constructor. + * + * Inherits from: + * - <OpenLayers.Format.XML.VersionedOGC> + */ +OpenLayers.Format.Filter = OpenLayers.Class(OpenLayers.Format.XML.VersionedOGC, { + + /** + * APIProperty: defaultVersion + * {String} Version number to assume if none found. Default is "1.0.0". + */ + defaultVersion: "1.0.0", + + /** + * APIMethod: write + * Write an ogc:Filter given a filter object. + * + * Parameters: + * filter - {<OpenLayers.Filter>} An filter. + * options - {Object} Optional configuration object. + * + * Returns: + * {Elment} An ogc:Filter element node. + */ + + /** + * APIMethod: read + * Read and Filter doc and return an object representing the Filter. + * + * Parameters: + * data - {String | DOMElement} Data to read. + * + * Returns: + * {<OpenLayers.Filter>} A filter object. + */ + + CLASS_NAME: "OpenLayers.Format.Filter" +}); +/* ====================================================================== + OpenLayers/Format/WFST.js + ====================================================================== */ + +/* 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/Format.js + */ + +/** + * Function: OpenLayers.Format.WFST + * Used to create a versioned WFS protocol. Default version is 1.0.0. + * + * Returns: + * {<OpenLayers.Format>} A WFST format of the given version. + */ +OpenLayers.Format.WFST = function(options) { + options = OpenLayers.Util.applyDefaults( + options, OpenLayers.Format.WFST.DEFAULTS + ); + var cls = OpenLayers.Format.WFST["v"+options.version.replace(/\./g, "_")]; + if(!cls) { + throw "Unsupported WFST version: " + options.version; + } + return new cls(options); +}; + +/** + * Constant: OpenLayers.Format.WFST.DEFAULTS + * {Object} Default properties for the WFST format. + */ +OpenLayers.Format.WFST.DEFAULTS = { + "version": "1.0.0" +}; +/* ====================================================================== + OpenLayers/Filter/Spatial.js + ====================================================================== */ + +/* 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/Filter.js + */ + +/** + * Class: OpenLayers.Filter.Spatial + * This class represents a spatial filter. + * Currently implemented: BBOX, DWithin and Intersects + * + * Inherits from: + * - <OpenLayers.Filter> + */ +OpenLayers.Filter.Spatial = OpenLayers.Class(OpenLayers.Filter, { + + /** + * APIProperty: type + * {String} Type of spatial filter. + * + * The type should be one of: + * - OpenLayers.Filter.Spatial.BBOX + * - OpenLayers.Filter.Spatial.INTERSECTS + * - OpenLayers.Filter.Spatial.DWITHIN + * - OpenLayers.Filter.Spatial.WITHIN + * - OpenLayers.Filter.Spatial.CONTAINS + */ + type: null, + + /** + * APIProperty: property + * {String} Name of the context property to compare. + */ + property: null, + + /** + * APIProperty: value + * {<OpenLayers.Bounds> || <OpenLayers.Geometry>} The bounds or geometry + * to be used by the filter. Use bounds for BBOX filters and geometry + * for INTERSECTS or DWITHIN filters. + */ + value: null, + + /** + * APIProperty: distance + * {Number} The distance to use in a DWithin spatial filter. + */ + distance: null, + + /** + * APIProperty: distanceUnits + * {String} The units to use for the distance, e.g. 'm'. + */ + distanceUnits: null, + + /** + * Constructor: OpenLayers.Filter.Spatial + * Creates a spatial filter. + * + * Parameters: + * options - {Object} An optional object with properties to set on the + * filter. + * + * Returns: + * {<OpenLayers.Filter.Spatial>} + */ + + /** + * Method: evaluate + * Evaluates this filter for a specific feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} feature to apply the filter to. + * + * Returns: + * {Boolean} The feature meets filter criteria. + */ + evaluate: function(feature) { + var intersect = false; + switch(this.type) { + case OpenLayers.Filter.Spatial.BBOX: + case OpenLayers.Filter.Spatial.INTERSECTS: + if(feature.geometry) { + var geom = this.value; + if(this.value.CLASS_NAME == "OpenLayers.Bounds") { + geom = this.value.toGeometry(); + } + if(feature.geometry.intersects(geom)) { + intersect = true; + } + } + break; + default: + throw new Error('evaluate is not implemented for this filter type.'); + } + return intersect; + }, + + /** + * APIMethod: clone + * Clones this filter. + * + * Returns: + * {<OpenLayers.Filter.Spatial>} Clone of this filter. + */ + clone: function() { + var options = OpenLayers.Util.applyDefaults({ + value: this.value && this.value.clone && this.value.clone() + }, this); + return new OpenLayers.Filter.Spatial(options); + }, + CLASS_NAME: "OpenLayers.Filter.Spatial" +}); + +OpenLayers.Filter.Spatial.BBOX = "BBOX"; +OpenLayers.Filter.Spatial.INTERSECTS = "INTERSECTS"; +OpenLayers.Filter.Spatial.DWITHIN = "DWITHIN"; +OpenLayers.Filter.Spatial.WITHIN = "WITHIN"; +OpenLayers.Filter.Spatial.CONTAINS = "CONTAINS"; +/* ====================================================================== + OpenLayers/Format/WFST/v1.js + ====================================================================== */ + +/* 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/Format/XML.js + * @requires OpenLayers/Format/WFST.js + * @requires OpenLayers/Filter/Spatial.js + * @requires OpenLayers/Filter/FeatureId.js + */ + +/** + * Class: OpenLayers.Format.WFST.v1 + * Superclass for WFST parsers. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.WFST.v1 = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. + */ + namespaces: { + xlink: "http://www.w3.org/1999/xlink", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + wfs: "http://www.opengis.net/wfs", + gml: "http://www.opengis.net/gml", + ogc: "http://www.opengis.net/ogc", + ows: "http://www.opengis.net/ows" + }, + + /** + * Property: defaultPrefix + */ + defaultPrefix: "wfs", + + /** + * Property: version + * {String} WFS version number. + */ + version: null, + + /** + * Property: schemaLocation + * {String} Schema location for a particular minor version. + */ + schemaLocations: null, + + /** + * APIProperty: srsName + * {String} URI for spatial reference system. + */ + srsName: null, + + /** + * APIProperty: extractAttributes + * {Boolean} Extract attributes from GML. Default is true. + */ + extractAttributes: true, + + /** + * APIProperty: xy + * {Boolean} Order of the GML coordinate true:(x,y) or false:(y,x) + * Changing is not recommended, a new Format should be instantiated. + */ + xy: true, + + /** + * Property: stateName + * {Object} Maps feature states to node names. + */ + stateName: null, + + /** + * Constructor: OpenLayers.Format.WFST.v1 + * Instances of this class are not created directly. Use the + * <OpenLayers.Format.WFST.v1_0_0> or <OpenLayers.Format.WFST.v1_1_0> + * constructor instead. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + // set state name mapping + this.stateName = {}; + this.stateName[OpenLayers.State.INSERT] = "wfs:Insert"; + this.stateName[OpenLayers.State.UPDATE] = "wfs:Update"; + this.stateName[OpenLayers.State.DELETE] = "wfs:Delete"; + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + }, + + /** + * Method: getSrsName + */ + getSrsName: function(feature, options) { + var srsName = options && options.srsName; + if(!srsName) { + if(feature && feature.layer) { + srsName = feature.layer.projection.getCode(); + } else { + srsName = this.srsName; + } + } + return srsName; + }, + + /** + * APIMethod: read + * Parse the response from a transaction. Because WFS is split into + * Transaction requests (create, update, and delete) and GetFeature + * requests (read), this method handles parsing of both types of + * responses. + * + * Parameters: + * data - {String | Document} The WFST document to read + * options - {Object} Options for the reader + * + * Valid options properties: + * output - {String} either "features" or "object". The default is + * "features", which means that the method will return an array of + * features. If set to "object", an object with a "features" property + * and other properties read by the parser will be returned. + * + * Returns: + * {Array | Object} Output depending on the output option. + */ + read: function(data, options) { + options = options || {}; + OpenLayers.Util.applyDefaults(options, { + output: "features" + }); + + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + if(data && data.nodeType == 9) { + data = data.documentElement; + } + var obj = {}; + if(data) { + this.readNode(data, obj, true); + } + if(obj.features && options.output === "features") { + obj = obj.features; + } + return obj; + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "wfs": { + "FeatureCollection": function(node, obj) { + obj.features = []; + this.readChildNodes(node, obj); + } + } + }, + + /** + * Method: write + * Given an array of features, write a WFS transaction. This assumes + * the features have a state property that determines the operation + * type - insert, update, or delete. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} A list of features. See + * below for a more detailed description of the influence of the + * feature's *modified* property. + * options - {Object} + * + * feature.modified rules: + * If a feature has a modified property set, the following checks will be + * made before a feature's geometry or attribute is included in an Update + * transaction: + * - *modified* is not set at all: The geometry and all attributes will be + * included. + * - *modified.geometry* is set (null or a geometry): The geometry will be + * included. If *modified.attributes* is not set, all attributes will + * be included. + * - *modified.attributes* is set: Only the attributes set (i.e. to null or + * a value) in *modified.attributes* will be included. + * If *modified.geometry* is not set, the geometry will not be included. + * + * Valid options include: + * - *multi* {Boolean} If set to true, geometries will be casted to + * Multi geometries before writing. + * + * Returns: + * {String} A serialized WFS transaction. + */ + write: function(features, options) { + var node = this.writeNode("wfs:Transaction", { + features:features, + options: options + }); + var value = this.schemaLocationAttr(); + if(value) { + this.setAttributeNS( + node, this.namespaces["xsi"], "xsi:schemaLocation", value + ); + } + return OpenLayers.Format.XML.prototype.write.apply(this, [node]); + }, + + /** + * Property: writers + * As a compliment to the readers property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: { + "wfs": { + "GetFeature": function(options) { + var node = this.createElementNSPlus("wfs:GetFeature", { + attributes: { + service: "WFS", + version: this.version, + handle: options && options.handle, + outputFormat: options && options.outputFormat, + maxFeatures: options && options.maxFeatures, + "xsi:schemaLocation": this.schemaLocationAttr(options) + } + }); + if (typeof this.featureType == "string") { + this.writeNode("Query", options, node); + } else { + for (var i=0,len = this.featureType.length; i<len; i++) { + options.featureType = this.featureType[i]; + this.writeNode("Query", options, node); + } + } + return node; + }, + "Transaction": function(obj) { + obj = obj || {}; + var options = obj.options || {}; + var node = this.createElementNSPlus("wfs:Transaction", { + attributes: { + service: "WFS", + version: this.version, + handle: options.handle + } + }); + var i, len; + var features = obj.features; + if(features) { + // temporarily re-assigning geometry types + if (options.multi === true) { + OpenLayers.Util.extend(this.geometryTypes, { + "OpenLayers.Geometry.Point": "MultiPoint", + "OpenLayers.Geometry.LineString": (this.multiCurve === true) ? "MultiCurve": "MultiLineString", + "OpenLayers.Geometry.Polygon": (this.multiSurface === true) ? "MultiSurface" : "MultiPolygon" + }); + } + var name, feature; + for(i=0, len=features.length; i<len; ++i) { + feature = features[i]; + name = this.stateName[feature.state]; + if(name) { + this.writeNode(name, { + feature: feature, + options: options + }, node); + } + } + // switch back to original geometry types assignment + if (options.multi === true) { + this.setGeometryTypes(); + } + } + if (options.nativeElements) { + for (i=0, len=options.nativeElements.length; i<len; ++i) { + this.writeNode("wfs:Native", + options.nativeElements[i], node); + } + } + return node; + }, + "Native": function(nativeElement) { + var node = this.createElementNSPlus("wfs:Native", { + attributes: { + vendorId: nativeElement.vendorId, + safeToIgnore: nativeElement.safeToIgnore + }, + value: nativeElement.value + }); + return node; + }, + "Insert": function(obj) { + var feature = obj.feature; + var options = obj.options; + var node = this.createElementNSPlus("wfs:Insert", { + attributes: { + handle: options && options.handle + } + }); + this.srsName = this.getSrsName(feature); + this.writeNode("feature:_typeName", feature, node); + return node; + }, + "Update": function(obj) { + var feature = obj.feature; + var options = obj.options; + var node = this.createElementNSPlus("wfs:Update", { + attributes: { + handle: options && options.handle, + typeName: (this.featureNS ? this.featurePrefix + ":" : "") + + this.featureType + } + }); + if(this.featureNS) { + node.setAttribute("xmlns:" + this.featurePrefix, this.featureNS); + } + + // add in geometry + var modified = feature.modified; + if (this.geometryName !== null && (!modified || modified.geometry !== undefined)) { + this.srsName = this.getSrsName(feature); + this.writeNode( + "Property", {name: this.geometryName, value: feature.geometry}, node + ); + } + + // add in attributes + for(var key in feature.attributes) { + if(feature.attributes[key] !== undefined && + (!modified || !modified.attributes || + (modified.attributes && modified.attributes[key] !== undefined))) { + this.writeNode( + "Property", {name: key, value: feature.attributes[key]}, node + ); + } + } + + // add feature id filter + this.writeNode("ogc:Filter", new OpenLayers.Filter.FeatureId({ + fids: [feature.fid] + }), node); + + return node; + }, + "Property": function(obj) { + var node = this.createElementNSPlus("wfs:Property"); + this.writeNode("Name", obj.name, node); + if(obj.value !== null) { + this.writeNode("Value", obj.value, node); + } + return node; + }, + "Name": function(name) { + return this.createElementNSPlus("wfs:Name", {value: name}); + }, + "Value": function(obj) { + var node; + if(obj instanceof OpenLayers.Geometry) { + node = this.createElementNSPlus("wfs:Value"); + var geom = this.writeNode("feature:_geometry", obj).firstChild; + node.appendChild(geom); + } else { + node = this.createElementNSPlus("wfs:Value", {value: obj}); + } + return node; + }, + "Delete": function(obj) { + var feature = obj.feature; + var options = obj.options; + var node = this.createElementNSPlus("wfs:Delete", { + attributes: { + handle: options && options.handle, + typeName: (this.featureNS ? this.featurePrefix + ":" : "") + + this.featureType + } + }); + if(this.featureNS) { + node.setAttribute("xmlns:" + this.featurePrefix, this.featureNS); + } + this.writeNode("ogc:Filter", new OpenLayers.Filter.FeatureId({ + fids: [feature.fid] + }), node); + return node; + } + } + }, + + /** + * Method: schemaLocationAttr + * Generate the xsi:schemaLocation attribute value. + * + * Returns: + * {String} The xsi:schemaLocation attribute or undefined if none. + */ + schemaLocationAttr: function(options) { + options = OpenLayers.Util.extend({ + featurePrefix: this.featurePrefix, + schema: this.schema + }, options); + var schemaLocations = OpenLayers.Util.extend({}, this.schemaLocations); + if(options.schema) { + schemaLocations[options.featurePrefix] = options.schema; + } + var parts = []; + var uri; + for(var key in schemaLocations) { + uri = this.namespaces[key]; + if(uri) { + parts.push(uri + " " + schemaLocations[key]); + } + } + var value = parts.join(" ") || undefined; + return value; + }, + + /** + * Method: setFilterProperty + * Set the property of each spatial filter. + * + * Parameters: + * filter - {<OpenLayers.Filter>} + */ + setFilterProperty: function(filter) { + if(filter.filters) { + for(var i=0, len=filter.filters.length; i<len; ++i) { + OpenLayers.Format.WFST.v1.prototype.setFilterProperty.call(this, filter.filters[i]); + } + } else { + if(filter instanceof OpenLayers.Filter.Spatial && !filter.property) { + // got a spatial filter without property, so set it + filter.property = this.geometryName; + } + } + }, + + CLASS_NAME: "OpenLayers.Format.WFST.v1" + +}); +/* ====================================================================== + OpenLayers/Geometry/Polygon.js + ====================================================================== */ + +/* 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/Geometry/Collection.js + * @requires OpenLayers/Geometry/LinearRing.js + */ + +/** + * Class: OpenLayers.Geometry.Polygon + * Polygon is a collection of Geometry.LinearRings. + * + * Inherits from: + * - <OpenLayers.Geometry.Collection> + * - <OpenLayers.Geometry> + */ +OpenLayers.Geometry.Polygon = OpenLayers.Class( + OpenLayers.Geometry.Collection, { + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null value means the + * component types are not restricted. + */ + componentTypes: ["OpenLayers.Geometry.LinearRing"], + + /** + * Constructor: OpenLayers.Geometry.Polygon + * Constructor for a Polygon geometry. + * The first ring (this.component[0])is the outer bounds of the polygon and + * all subsequent rings (this.component[1-n]) are internal holes. + * + * + * Parameters: + * components - {Array(<OpenLayers.Geometry.LinearRing>)} + */ + + /** + * APIMethod: getArea + * Calculated by subtracting the areas of the internal holes from the + * area of the outer hole. + * + * Returns: + * {float} The area of the geometry + */ + getArea: function() { + var area = 0.0; + if ( this.components && (this.components.length > 0)) { + area += Math.abs(this.components[0].getArea()); + for (var i=1, len=this.components.length; i<len; i++) { + area -= Math.abs(this.components[i].getArea()); + } + } + return area; + }, + + /** + * APIMethod: getGeodesicArea + * Calculate the approximate area of the polygon were it projected onto + * the earth. + * + * Parameters: + * projection - {<OpenLayers.Projection>} The spatial reference system + * for the geometry coordinates. If not provided, Geographic/WGS84 is + * assumed. + * + * Reference: + * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for + * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion + * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409 + * + * Returns: + * {float} The approximate geodesic area of the polygon in square meters. + */ + getGeodesicArea: function(projection) { + var area = 0.0; + if(this.components && (this.components.length > 0)) { + area += Math.abs(this.components[0].getGeodesicArea(projection)); + for(var i=1, len=this.components.length; i<len; i++) { + area -= Math.abs(this.components[i].getGeodesicArea(projection)); + } + } + return area; + }, + + /** + * Method: containsPoint + * Test if a point is inside a polygon. Points on a polygon edge are + * considered inside. + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns: + * {Boolean | Number} The point is inside the polygon. Returns 1 if the + * point is on an edge. Returns boolean otherwise. + */ + containsPoint: function(point) { + var numRings = this.components.length; + var contained = false; + if(numRings > 0) { + // check exterior ring - 1 means on edge, boolean otherwise + contained = this.components[0].containsPoint(point); + if(contained !== 1) { + if(contained && numRings > 1) { + // check interior rings + var hole; + for(var i=1; i<numRings; ++i) { + hole = this.components[i].containsPoint(point); + if(hole) { + if(hole === 1) { + // on edge + contained = 1; + } else { + // in hole + contained = false; + } + break; + } + } + } + } + } + return contained; + }, + + /** + * APIMethod: intersects + * Determine if the input geometry intersects this one. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} Any type of geometry. + * + * Returns: + * {Boolean} The input geometry intersects this one. + */ + intersects: function(geometry) { + var intersect = false; + var i, len; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + intersect = this.containsPoint(geometry); + } else if(geometry.CLASS_NAME == "OpenLayers.Geometry.LineString" || + geometry.CLASS_NAME == "OpenLayers.Geometry.LinearRing") { + // check if rings/linestrings intersect + for(i=0, len=this.components.length; i<len; ++i) { + intersect = geometry.intersects(this.components[i]); + if(intersect) { + break; + } + } + if(!intersect) { + // check if this poly contains points of the ring/linestring + for(i=0, len=geometry.components.length; i<len; ++i) { + intersect = this.containsPoint(geometry.components[i]); + if(intersect) { + break; + } + } + } + } else { + for(i=0, len=geometry.components.length; i<len; ++ i) { + intersect = this.intersects(geometry.components[i]); + if(intersect) { + break; + } + } + } + // check case where this poly is wholly contained by another + if(!intersect && geometry.CLASS_NAME == "OpenLayers.Geometry.Polygon") { + // exterior ring points will be contained in the other geometry + var ring = this.components[0]; + for(i=0, len=ring.components.length; i<len; ++i) { + intersect = geometry.containsPoint(ring.components[i]); + if(intersect) { + break; + } + } + } + return intersect; + }, + + /** + * APIMethod: distanceTo + * Calculate the closest distance between two geometries (on the x-y plane). + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} The target geometry. + * options - {Object} Optional properties for configuring the distance + * calculation. + * + * Valid options: + * details - {Boolean} Return details from the distance calculation. + * Default is false. + * edge - {Boolean} Calculate the distance from this geometry to the + * nearest edge of the target geometry. Default is true. If true, + * calling distanceTo from a geometry that is wholly contained within + * the target will result in a non-zero distance. If false, whenever + * geometries intersect, calling distanceTo will return 0. If false, + * details cannot be returned. + * + * Returns: + * {Number | Object} The distance between this geometry and the target. + * If details is true, the return will be an object with distance, + * x0, y0, x1, and y1 properties. The x0 and y0 properties represent + * the coordinates of the closest point on this geometry. The x1 and y1 + * properties represent the coordinates of the closest point on the + * target geometry. + */ + distanceTo: function(geometry, options) { + var edge = !(options && options.edge === false); + var result; + // this is the case where we might not be looking for distance to edge + if(!edge && this.intersects(geometry)) { + result = 0; + } else { + result = OpenLayers.Geometry.Collection.prototype.distanceTo.apply( + this, [geometry, options] + ); + } + return result; + }, + + CLASS_NAME: "OpenLayers.Geometry.Polygon" +}); + +/** + * APIMethod: createRegularPolygon + * Create a regular polygon around a radius. Useful for creating circles + * and the like. + * + * Parameters: + * origin - {<OpenLayers.Geometry.Point>} center of polygon. + * radius - {Float} distance to vertex, in map units. + * sides - {Integer} Number of sides. 20 approximates a circle. + * rotation - {Float} original angle of rotation, in degrees. + */ +OpenLayers.Geometry.Polygon.createRegularPolygon = function(origin, radius, sides, rotation) { + var angle = Math.PI * ((1/sides) - (1/2)); + if(rotation) { + angle += (rotation / 180) * Math.PI; + } + var rotatedAngle, x, y; + var points = []; + for(var i=0; i<sides; ++i) { + rotatedAngle = angle + (i * 2 * Math.PI / sides); + x = origin.x + (radius * Math.cos(rotatedAngle)); + y = origin.y + (radius * Math.sin(rotatedAngle)); + points.push(new OpenLayers.Geometry.Point(x, y)); + } + var ring = new OpenLayers.Geometry.LinearRing(points); + return new OpenLayers.Geometry.Polygon([ring]); +}; +/* ====================================================================== + OpenLayers/Geometry/MultiPolygon.js + ====================================================================== */ + +/* 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/Geometry/Collection.js + * @requires OpenLayers/Geometry/Polygon.js + */ + +/** + * Class: OpenLayers.Geometry.MultiPolygon + * MultiPolygon is a geometry with multiple <OpenLayers.Geometry.Polygon> + * components. Create a new instance with the <OpenLayers.Geometry.MultiPolygon> + * constructor. + * + * Inherits from: + * - <OpenLayers.Geometry.Collection> + */ +OpenLayers.Geometry.MultiPolygon = OpenLayers.Class( + OpenLayers.Geometry.Collection, { + + /** + * Property: componentTypes + * {Array(String)} An array of class names representing the types of + * components that the collection can include. A null value means the + * component types are not restricted. + */ + componentTypes: ["OpenLayers.Geometry.Polygon"], + + /** + * Constructor: OpenLayers.Geometry.MultiPolygon + * Create a new MultiPolygon geometry + * + * Parameters: + * components - {Array(<OpenLayers.Geometry.Polygon>)} An array of polygons + * used to generate the MultiPolygon + * + */ + + CLASS_NAME: "OpenLayers.Geometry.MultiPolygon" +}); +/* ====================================================================== + OpenLayers/Format/GML.js + ====================================================================== */ + +/* 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/Format/XML.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Geometry/Point.js + * @requires OpenLayers/Geometry/MultiPoint.js + * @requires OpenLayers/Geometry/LineString.js + * @requires OpenLayers/Geometry/MultiLineString.js + * @requires OpenLayers/Geometry/Polygon.js + * @requires OpenLayers/Geometry/MultiPolygon.js + */ + +/** + * Class: OpenLayers.Format.GML + * Read/Write GML. Create a new instance with the <OpenLayers.Format.GML> + * constructor. Supports the GML simple features profile. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.GML = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * APIProperty: featureNS + * {String} Namespace used for feature attributes. Default is + * "http://mapserver.gis.umn.edu/mapserver". + */ + featureNS: "http://mapserver.gis.umn.edu/mapserver", + + /** + * APIProperty: featurePrefix + * {String} Namespace alias (or prefix) for feature nodes. Default is + * "feature". + */ + featurePrefix: "feature", + + /** + * APIProperty: featureName + * {String} Element name for features. Default is "featureMember". + */ + featureName: "featureMember", + + /** + * APIProperty: layerName + * {String} Name of data layer. Default is "features". + */ + layerName: "features", + + /** + * APIProperty: geometryName + * {String} Name of geometry element. Defaults to "geometry". + */ + geometryName: "geometry", + + /** + * APIProperty: collectionName + * {String} Name of featureCollection element. + */ + collectionName: "FeatureCollection", + + /** + * APIProperty: gmlns + * {String} GML Namespace. + */ + gmlns: "http://www.opengis.net/gml", + + /** + * APIProperty: extractAttributes + * {Boolean} Extract attributes from GML. + */ + extractAttributes: true, + + /** + * APIProperty: xy + * {Boolean} Order of the GML coordinate true:(x,y) or false:(y,x) + * Changing is not recommended, a new Format should be instantiated. + */ + xy: true, + + /** + * Constructor: OpenLayers.Format.GML + * Create a new parser for GML. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + // compile regular expressions once instead of every time they are used + this.regExes = { + trimSpace: (/^\s*|\s*$/g), + removeSpace: (/\s*/g), + splitSpace: (/\s+/), + trimComma: (/\s*,\s*/g) + }; + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: read + * Read data from a string, and return a list of features. + * + * Parameters: + * data - {String} or {DOMElement} data to read/parse. + * + * Returns: + * {Array(<OpenLayers.Feature.Vector>)} An array of features. + */ + read: function(data) { + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + var featureNodes = this.getElementsByTagNameNS(data.documentElement, + this.gmlns, + this.featureName); + var features = []; + for(var i=0; i<featureNodes.length; i++) { + var feature = this.parseFeature(featureNodes[i]); + if(feature) { + features.push(feature); + } + } + return features; + }, + + /** + * Method: parseFeature + * This function is the core of the GML parsing code in OpenLayers. + * It creates the geometries that are then attached to the returned + * feature, and calls parseAttributes() to get attribute data out. + * + * Parameters: + * node - {DOMElement} A GML feature node. + */ + parseFeature: function(node) { + // only accept one geometry per feature - look for highest "order" + var order = ["MultiPolygon", "Polygon", + "MultiLineString", "LineString", + "MultiPoint", "Point", "Envelope"]; + // FIXME: In case we parse a feature with no geometry, but boundedBy an Envelope, + // this code creates a geometry derived from the Envelope. This is not correct. + var type, nodeList, geometry, parser; + for(var i=0; i<order.length; ++i) { + type = order[i]; + nodeList = this.getElementsByTagNameNS(node, this.gmlns, type); + if(nodeList.length > 0) { + // only deal with first geometry of this type + parser = this.parseGeometry[type.toLowerCase()]; + if(parser) { + geometry = parser.apply(this, [nodeList[0]]); + if (this.internalProjection && this.externalProjection) { + geometry.transform(this.externalProjection, + this.internalProjection); + } + } else { + throw new TypeError("Unsupported geometry type: " + type); + } + // stop looking for different geometry types + break; + } + } + + var bounds; + var boxNodes = this.getElementsByTagNameNS(node, this.gmlns, "Box"); + for(i=0; i<boxNodes.length; ++i) { + var boxNode = boxNodes[i]; + var box = this.parseGeometry["box"].apply(this, [boxNode]); + var parentNode = boxNode.parentNode; + var parentName = parentNode.localName || + parentNode.nodeName.split(":").pop(); + if(parentName === "boundedBy") { + bounds = box; + } else { + geometry = box.toGeometry(); + } + } + + // construct feature (optionally with attributes) + var attributes; + if(this.extractAttributes) { + attributes = this.parseAttributes(node); + } + var feature = new OpenLayers.Feature.Vector(geometry, attributes); + feature.bounds = bounds; + + feature.gml = { + featureType: node.firstChild.nodeName.split(":")[1], + featureNS: node.firstChild.namespaceURI, + featureNSPrefix: node.firstChild.prefix + }; + + // assign fid - this can come from a "fid" or "id" attribute + var childNode = node.firstChild; + var fid; + while(childNode) { + if(childNode.nodeType == 1) { + fid = childNode.getAttribute("fid") || + childNode.getAttribute("id"); + if(fid) { + break; + } + } + childNode = childNode.nextSibling; + } + feature.fid = fid; + return feature; + }, + + /** + * Property: parseGeometry + * Properties of this object are the functions that parse geometries based + * on their type. + */ + parseGeometry: { + + /** + * Method: parseGeometry.point + * Given a GML node representing a point geometry, create an OpenLayers + * point geometry. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Geometry.Point>} A point geometry. + */ + point: function(node) { + /** + * Three coordinate variations to consider: + * 1) <gml:pos>x y z</gml:pos> + * 2) <gml:coordinates>x, y, z</gml:coordinates> + * 3) <gml:coord><gml:X>x</gml:X><gml:Y>y</gml:Y></gml:coord> + */ + var nodeList, coordString; + var coords = []; + + // look for <gml:pos> + var nodeList = this.getElementsByTagNameNS(node, this.gmlns, "pos"); + if(nodeList.length > 0) { + coordString = nodeList[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.trimSpace, ""); + coords = coordString.split(this.regExes.splitSpace); + } + + // look for <gml:coordinates> + if(coords.length == 0) { + nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "coordinates"); + if(nodeList.length > 0) { + coordString = nodeList[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.removeSpace, + ""); + coords = coordString.split(","); + } + } + + // look for <gml:coord> + if(coords.length == 0) { + nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "coord"); + if(nodeList.length > 0) { + var xList = this.getElementsByTagNameNS(nodeList[0], + this.gmlns, "X"); + var yList = this.getElementsByTagNameNS(nodeList[0], + this.gmlns, "Y"); + if(xList.length > 0 && yList.length > 0) { + coords = [xList[0].firstChild.nodeValue, + yList[0].firstChild.nodeValue]; + } + } + } + + // preserve third dimension + if(coords.length == 2) { + coords[2] = null; + } + + if (this.xy) { + return new OpenLayers.Geometry.Point(coords[0], coords[1], + coords[2]); + } + else{ + return new OpenLayers.Geometry.Point(coords[1], coords[0], + coords[2]); + } + }, + + /** + * Method: parseGeometry.multipoint + * Given a GML node representing a multipoint geometry, create an + * OpenLayers multipoint geometry. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Geometry.MultiPoint>} A multipoint geometry. + */ + multipoint: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "Point"); + var components = []; + if(nodeList.length > 0) { + var point; + for(var i=0; i<nodeList.length; ++i) { + point = this.parseGeometry.point.apply(this, [nodeList[i]]); + if(point) { + components.push(point); + } + } + } + return new OpenLayers.Geometry.MultiPoint(components); + }, + + /** + * Method: parseGeometry.linestring + * Given a GML node representing a linestring geometry, create an + * OpenLayers linestring geometry. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Geometry.LineString>} A linestring geometry. + */ + linestring: function(node, ring) { + /** + * Two coordinate variations to consider: + * 1) <gml:posList dimension="d">x0 y0 z0 x1 y1 z1</gml:posList> + * 2) <gml:coordinates>x0, y0, z0 x1, y1, z1</gml:coordinates> + */ + var nodeList, coordString; + var coords = []; + var points = []; + + // look for <gml:posList> + nodeList = this.getElementsByTagNameNS(node, this.gmlns, "posList"); + if(nodeList.length > 0) { + coordString = this.getChildValue(nodeList[0]); + coordString = coordString.replace(this.regExes.trimSpace, ""); + coords = coordString.split(this.regExes.splitSpace); + var dim = parseInt(nodeList[0].getAttribute("dimension")); + var j, x, y, z; + for(var i=0; i<coords.length/dim; ++i) { + j = i * dim; + x = coords[j]; + y = coords[j+1]; + z = (dim == 2) ? null : coords[j+2]; + if (this.xy) { + points.push(new OpenLayers.Geometry.Point(x, y, z)); + } else { + points.push(new OpenLayers.Geometry.Point(y, x, z)); + } + } + } + + // look for <gml:coordinates> + if(coords.length == 0) { + nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "coordinates"); + if(nodeList.length > 0) { + coordString = this.getChildValue(nodeList[0]); + coordString = coordString.replace(this.regExes.trimSpace, + ""); + coordString = coordString.replace(this.regExes.trimComma, + ","); + var pointList = coordString.split(this.regExes.splitSpace); + for(var i=0; i<pointList.length; ++i) { + coords = pointList[i].split(","); + if(coords.length == 2) { + coords[2] = null; + } + if (this.xy) { + points.push(new OpenLayers.Geometry.Point(coords[0], + coords[1], + coords[2])); + } else { + points.push(new OpenLayers.Geometry.Point(coords[1], + coords[0], + coords[2])); + } + } + } + } + + var line = null; + if(points.length != 0) { + if(ring) { + line = new OpenLayers.Geometry.LinearRing(points); + } else { + line = new OpenLayers.Geometry.LineString(points); + } + } + return line; + }, + + /** + * Method: parseGeometry.multilinestring + * Given a GML node representing a multilinestring geometry, create an + * OpenLayers multilinestring geometry. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Geometry.MultiLineString>} A multilinestring geometry. + */ + multilinestring: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "LineString"); + var components = []; + if(nodeList.length > 0) { + var line; + for(var i=0; i<nodeList.length; ++i) { + line = this.parseGeometry.linestring.apply(this, + [nodeList[i]]); + if(line) { + components.push(line); + } + } + } + return new OpenLayers.Geometry.MultiLineString(components); + }, + + /** + * Method: parseGeometry.polygon + * Given a GML node representing a polygon geometry, create an + * OpenLayers polygon geometry. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Geometry.Polygon>} A polygon geometry. + */ + polygon: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "LinearRing"); + var components = []; + if(nodeList.length > 0) { + // this assumes exterior ring first, inner rings after + var ring; + for(var i=0; i<nodeList.length; ++i) { + ring = this.parseGeometry.linestring.apply(this, + [nodeList[i], true]); + if(ring) { + components.push(ring); + } + } + } + return new OpenLayers.Geometry.Polygon(components); + }, + + /** + * Method: parseGeometry.multipolygon + * Given a GML node representing a multipolygon geometry, create an + * OpenLayers multipolygon geometry. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Geometry.MultiPolygon>} A multipolygon geometry. + */ + multipolygon: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "Polygon"); + var components = []; + if(nodeList.length > 0) { + var polygon; + for(var i=0; i<nodeList.length; ++i) { + polygon = this.parseGeometry.polygon.apply(this, + [nodeList[i]]); + if(polygon) { + components.push(polygon); + } + } + } + return new OpenLayers.Geometry.MultiPolygon(components); + }, + + envelope: function(node) { + var components = []; + var coordString; + var envelope; + + var lpoint = this.getElementsByTagNameNS(node, this.gmlns, "lowerCorner"); + if (lpoint.length > 0) { + var coords = []; + + if(lpoint.length > 0) { + coordString = lpoint[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.trimSpace, ""); + coords = coordString.split(this.regExes.splitSpace); + } + + if(coords.length == 2) { + coords[2] = null; + } + if (this.xy) { + var lowerPoint = new OpenLayers.Geometry.Point(coords[0], coords[1],coords[2]); + } else { + var lowerPoint = new OpenLayers.Geometry.Point(coords[1], coords[0],coords[2]); + } + } + + var upoint = this.getElementsByTagNameNS(node, this.gmlns, "upperCorner"); + if (upoint.length > 0) { + var coords = []; + + if(upoint.length > 0) { + coordString = upoint[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.trimSpace, ""); + coords = coordString.split(this.regExes.splitSpace); + } + + if(coords.length == 2) { + coords[2] = null; + } + if (this.xy) { + var upperPoint = new OpenLayers.Geometry.Point(coords[0], coords[1],coords[2]); + } else { + var upperPoint = new OpenLayers.Geometry.Point(coords[1], coords[0],coords[2]); + } + } + + if (lowerPoint && upperPoint) { + components.push(new OpenLayers.Geometry.Point(lowerPoint.x, lowerPoint.y)); + components.push(new OpenLayers.Geometry.Point(upperPoint.x, lowerPoint.y)); + components.push(new OpenLayers.Geometry.Point(upperPoint.x, upperPoint.y)); + components.push(new OpenLayers.Geometry.Point(lowerPoint.x, upperPoint.y)); + components.push(new OpenLayers.Geometry.Point(lowerPoint.x, lowerPoint.y)); + + var ring = new OpenLayers.Geometry.LinearRing(components); + envelope = new OpenLayers.Geometry.Polygon([ring]); + } + return envelope; + }, + + /** + * Method: parseGeometry.box + * Given a GML node representing a box geometry, create an + * OpenLayers.Bounds. + * + * Parameters: + * node - {DOMElement} A GML node. + * + * Returns: + * {<OpenLayers.Bounds>} A bounds representing the box. + */ + box: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.gmlns, + "coordinates"); + var coordString; + var coords, beginPoint = null, endPoint = null; + if (nodeList.length > 0) { + coordString = nodeList[0].firstChild.nodeValue; + coords = coordString.split(" "); + if (coords.length == 2) { + beginPoint = coords[0].split(","); + endPoint = coords[1].split(","); + } + } + if (beginPoint !== null && endPoint !== null) { + return new OpenLayers.Bounds(parseFloat(beginPoint[0]), + parseFloat(beginPoint[1]), + parseFloat(endPoint[0]), + parseFloat(endPoint[1]) ); + } + } + + }, + + /** + * Method: parseAttributes + * + * Parameters: + * node - {DOMElement} + * + * Returns: + * {Object} An attributes object. + */ + parseAttributes: function(node) { + var attributes = {}; + // assume attributes are children of the first type 1 child + var childNode = node.firstChild; + var children, i, child, grandchildren, grandchild, name, value; + while(childNode) { + if(childNode.nodeType == 1) { + // attributes are type 1 children with one type 3 child + children = childNode.childNodes; + for(i=0; i<children.length; ++i) { + child = children[i]; + if(child.nodeType == 1) { + grandchildren = child.childNodes; + if(grandchildren.length == 1) { + grandchild = grandchildren[0]; + if(grandchild.nodeType == 3 || + grandchild.nodeType == 4) { + name = (child.prefix) ? + child.nodeName.split(":")[1] : + child.nodeName; + value = grandchild.nodeValue.replace( + this.regExes.trimSpace, ""); + attributes[name] = value; + } + } else { + // If child has no childNodes (grandchildren), + // set an attribute with null value. + // e.g. <prefix:fieldname/> becomes + // {fieldname: null} + attributes[child.nodeName.split(":").pop()] = null; + } + } + } + break; + } + childNode = childNode.nextSibling; + } + return attributes; + }, + + /** + * APIMethod: write + * Generate a GML document string given a list of features. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} List of features to + * serialize into a string. + * + * Returns: + * {String} A string representing the GML document. + */ + write: function(features) { + if(!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + var gml = this.createElementNS("http://www.opengis.net/wfs", + "wfs:" + this.collectionName); + for(var i=0; i<features.length; i++) { + gml.appendChild(this.createFeatureXML(features[i])); + } + return OpenLayers.Format.XML.prototype.write.apply(this, [gml]); + }, + + /** + * Method: createFeatureXML + * Accept an OpenLayers.Feature.Vector, and build a GML node for it. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} The feature to be built as GML. + * + * Returns: + * {DOMElement} A node reprensting the feature in GML. + */ + createFeatureXML: function(feature) { + var geometry = feature.geometry; + var geometryNode = this.buildGeometryNode(geometry); + var geomContainer = this.createElementNS(this.featureNS, + this.featurePrefix + ":" + + this.geometryName); + geomContainer.appendChild(geometryNode); + var featureNode = this.createElementNS(this.gmlns, + "gml:" + this.featureName); + var featureContainer = this.createElementNS(this.featureNS, + this.featurePrefix + ":" + + this.layerName); + var fid = feature.fid || feature.id; + featureContainer.setAttribute("fid", fid); + featureContainer.appendChild(geomContainer); + for(var attr in feature.attributes) { + var attrText = this.createTextNode(feature.attributes[attr]); + var nodename = attr.substring(attr.lastIndexOf(":") + 1); + var attrContainer = this.createElementNS(this.featureNS, + this.featurePrefix + ":" + + nodename); + attrContainer.appendChild(attrText); + featureContainer.appendChild(attrContainer); + } + featureNode.appendChild(featureContainer); + return featureNode; + }, + + /** + * APIMethod: buildGeometryNode + */ + buildGeometryNode: function(geometry) { + if (this.externalProjection && this.internalProjection) { + geometry = geometry.clone(); + geometry.transform(this.internalProjection, + this.externalProjection); + } + var className = geometry.CLASS_NAME; + var type = className.substring(className.lastIndexOf(".") + 1); + var builder = this.buildGeometry[type.toLowerCase()]; + return builder.apply(this, [geometry]); + }, + + /** + * Property: buildGeometry + * Object containing methods to do the actual geometry node building + * based on geometry type. + */ + buildGeometry: { + // TBD retrieve the srs from layer + // srsName is non-standard, so not including it until it's right. + // gml.setAttribute("srsName", + // "http://www.opengis.net/gml/srs/epsg.xml#4326"); + + /** + * Method: buildGeometry.point + * Given an OpenLayers point geometry, create a GML point. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Point>} A point geometry. + * + * Returns: + * {DOMElement} A GML point node. + */ + point: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:Point"); + gml.appendChild(this.buildCoordinatesNode(geometry)); + return gml; + }, + + /** + * Method: buildGeometry.multipoint + * Given an OpenLayers multipoint geometry, create a GML multipoint. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.MultiPoint>} A multipoint geometry. + * + * Returns: + * {DOMElement} A GML multipoint node. + */ + multipoint: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:MultiPoint"); + var points = geometry.components; + var pointMember, pointGeom; + for(var i=0; i<points.length; i++) { + pointMember = this.createElementNS(this.gmlns, + "gml:pointMember"); + pointGeom = this.buildGeometry.point.apply(this, + [points[i]]); + pointMember.appendChild(pointGeom); + gml.appendChild(pointMember); + } + return gml; + }, + + /** + * Method: buildGeometry.linestring + * Given an OpenLayers linestring geometry, create a GML linestring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.LineString>} A linestring geometry. + * + * Returns: + * {DOMElement} A GML linestring node. + */ + linestring: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:LineString"); + gml.appendChild(this.buildCoordinatesNode(geometry)); + return gml; + }, + + /** + * Method: buildGeometry.multilinestring + * Given an OpenLayers multilinestring geometry, create a GML + * multilinestring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.MultiLineString>} A multilinestring + * geometry. + * + * Returns: + * {DOMElement} A GML multilinestring node. + */ + multilinestring: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:MultiLineString"); + var lines = geometry.components; + var lineMember, lineGeom; + for(var i=0; i<lines.length; ++i) { + lineMember = this.createElementNS(this.gmlns, + "gml:lineStringMember"); + lineGeom = this.buildGeometry.linestring.apply(this, + [lines[i]]); + lineMember.appendChild(lineGeom); + gml.appendChild(lineMember); + } + return gml; + }, + + /** + * Method: buildGeometry.linearring + * Given an OpenLayers linearring geometry, create a GML linearring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.LinearRing>} A linearring geometry. + * + * Returns: + * {DOMElement} A GML linearring node. + */ + linearring: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:LinearRing"); + gml.appendChild(this.buildCoordinatesNode(geometry)); + return gml; + }, + + /** + * Method: buildGeometry.polygon + * Given an OpenLayers polygon geometry, create a GML polygon. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Polygon>} A polygon geometry. + * + * Returns: + * {DOMElement} A GML polygon node. + */ + polygon: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:Polygon"); + var rings = geometry.components; + var ringMember, ringGeom, type; + for(var i=0; i<rings.length; ++i) { + type = (i==0) ? "outerBoundaryIs" : "innerBoundaryIs"; + ringMember = this.createElementNS(this.gmlns, + "gml:" + type); + ringGeom = this.buildGeometry.linearring.apply(this, + [rings[i]]); + ringMember.appendChild(ringGeom); + gml.appendChild(ringMember); + } + return gml; + }, + + /** + * Method: buildGeometry.multipolygon + * Given an OpenLayers multipolygon geometry, create a GML multipolygon. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.MultiPolygon>} A multipolygon + * geometry. + * + * Returns: + * {DOMElement} A GML multipolygon node. + */ + multipolygon: function(geometry) { + var gml = this.createElementNS(this.gmlns, "gml:MultiPolygon"); + var polys = geometry.components; + var polyMember, polyGeom; + for(var i=0; i<polys.length; ++i) { + polyMember = this.createElementNS(this.gmlns, + "gml:polygonMember"); + polyGeom = this.buildGeometry.polygon.apply(this, + [polys[i]]); + polyMember.appendChild(polyGeom); + gml.appendChild(polyMember); + } + return gml; + + }, + + /** + * Method: buildGeometry.bounds + * Given an OpenLayers bounds, create a GML box. + * + * Parameters: + * bounds - {<OpenLayers.Geometry.Bounds>} A bounds object. + * + * Returns: + * {DOMElement} A GML box node. + */ + bounds: function(bounds) { + var gml = this.createElementNS(this.gmlns, "gml:Box"); + gml.appendChild(this.buildCoordinatesNode(bounds)); + return gml; + } + }, + + /** + * Method: buildCoordinates + * builds the coordinates XmlNode + * (code) + * <gml:coordinates decimal="." cs="," ts=" ">...</gml:coordinates> + * (end) + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {XmlNode} created xmlNode + */ + buildCoordinatesNode: function(geometry) { + var coordinatesNode = this.createElementNS(this.gmlns, + "gml:coordinates"); + coordinatesNode.setAttribute("decimal", "."); + coordinatesNode.setAttribute("cs", ","); + coordinatesNode.setAttribute("ts", " "); + + var parts = []; + + if(geometry instanceof OpenLayers.Bounds){ + parts.push(geometry.left + "," + geometry.bottom); + parts.push(geometry.right + "," + geometry.top); + } else { + var points = (geometry.components) ? geometry.components : [geometry]; + for(var i=0; i<points.length; i++) { + parts.push(points[i].x + "," + points[i].y); + } + } + + var txtNode = this.createTextNode(parts.join(" ")); + coordinatesNode.appendChild(txtNode); + + return coordinatesNode; + }, + + CLASS_NAME: "OpenLayers.Format.GML" +}); +/* ====================================================================== + OpenLayers/Format/GML/Base.js + ====================================================================== */ + +/* 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/Format/XML.js + * @requires OpenLayers/Format/GML.js + */ + +/** + * Though required in the full build, if the GML format is excluded, we set + * the namespace here. + */ +if(!OpenLayers.Format.GML) { + OpenLayers.Format.GML = {}; +} + +/** + * Class: OpenLayers.Format.GML.Base + * Superclass for GML parsers. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.GML.Base = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. + */ + namespaces: { + gml: "http://www.opengis.net/gml", + xlink: "http://www.w3.org/1999/xlink", + xsi: "http://www.w3.org/2001/XMLSchema-instance", + wfs: "http://www.opengis.net/wfs" // this is a convenience for reading wfs:FeatureCollection + }, + + /** + * Property: defaultPrefix + */ + defaultPrefix: "gml", + + /** + * Property: schemaLocation + * {String} Schema location for a particular minor version. + */ + schemaLocation: null, + + /** + * APIProperty: featureType + * {Array(String) or String} The local (without prefix) feature typeName(s). + */ + featureType: null, + + /** + * APIProperty: featureNS + * {String} The feature namespace. Must be set in the options at + * construction. + */ + featureNS: null, + + /** + * APIProperty: geometry + * {String} Name of geometry element. Defaults to "geometry". If null, it + * will be set on <read> when the first geometry is parsed. + */ + geometryName: "geometry", + + /** + * APIProperty: extractAttributes + * {Boolean} Extract attributes from GML. Default is true. + */ + extractAttributes: true, + + /** + * APIProperty: srsName + * {String} URI for spatial reference system. This is optional for + * single part geometries and mandatory for collections and multis. + * If set, the srsName attribute will be written for all geometries. + * Default is null. + */ + srsName: null, + + /** + * APIProperty: xy + * {Boolean} Order of the GML coordinate true:(x,y) or false:(y,x) + * Changing is not recommended, a new Format should be instantiated. + */ + xy: true, + + /** + * Property: geometryTypes + * {Object} Maps OpenLayers geometry class names to GML element names. + * Use <setGeometryTypes> before accessing this property. + */ + geometryTypes: null, + + /** + * Property: singleFeatureType + * {Boolean} True if there is only 1 featureType, and not an array + * of featuretypes. + */ + singleFeatureType: null, + + /** + * Property: autoConfig + * {Boolean} Indicates if the format was configured without a <featureNS>, + * but auto-configured <featureNS> and <featureType> during read. + * Subclasses making use of <featureType> auto-configuration should make + * the first call to the <readNode> method (usually in the read method) + * with true as 3rd argument, so the auto-configured featureType can be + * reset and the format can be reused for subsequent reads with data from + * different featureTypes. Set to false after read if you want to keep the + * auto-configured values. + */ + + /** + * Property: regExes + * Compiled regular expressions for manipulating strings. + */ + regExes: { + trimSpace: (/^\s*|\s*$/g), + removeSpace: (/\s*/g), + splitSpace: (/\s+/), + trimComma: (/\s*,\s*/g), + featureMember: (/^(.*:)?featureMembers?$/) + }, + + /** + * Constructor: OpenLayers.Format.GML.Base + * Instances of this class are not created directly. Use the + * <OpenLayers.Format.GML.v2> or <OpenLayers.Format.GML.v3> constructor + * instead. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + * + * Valid options properties: + * featureType - {Array(String) or String} Local (without prefix) feature + * typeName(s) (required for write). + * featureNS - {String} Feature namespace (required for write). + * geometryName - {String} Geometry element name (required for write). + */ + initialize: function(options) { + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + this.setGeometryTypes(); + if(options && options.featureNS) { + this.setNamespace("feature", options.featureNS); + } + this.singleFeatureType = !options || (typeof options.featureType === "string"); + }, + + /** + * Method: read + * + * Parameters: + * data - {DOMElement} A gml:featureMember element, a gml:featureMembers + * element, or an element containing either of the above at any level. + * + * Returns: + * {Array(<OpenLayers.Feature.Vector>)} An array of features. + */ + read: function(data) { + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + if(data && data.nodeType == 9) { + data = data.documentElement; + } + var features = []; + this.readNode(data, {features: features}, true); + if(features.length == 0) { + // look for gml:featureMember elements + var elements = this.getElementsByTagNameNS( + data, this.namespaces.gml, "featureMember" + ); + if(elements.length) { + for(var i=0, len=elements.length; i<len; ++i) { + this.readNode(elements[i], {features: features}, true); + } + } else { + // look for gml:featureMembers elements (this is v3, but does no harm here) + var elements = this.getElementsByTagNameNS( + data, this.namespaces.gml, "featureMembers" + ); + if(elements.length) { + // there can be only one + this.readNode(elements[0], {features: features}, true); + } + } + } + return features; + }, + + /** + * Method: readNode + * Shorthand for applying one of the named readers given the node + * namespace and local name. Readers take two args (node, obj) and + * generally extend or modify the second. + * + * Parameters: + * node - {DOMElement} The node to be read (required). + * obj - {Object} The object to be modified (optional). + * first - {Boolean} Should be set to true for the first node read. This + * is usually the readNode call in the read method. Without this being + * set, auto-configured properties will stick on subsequent reads. + * + * Returns: + * {Object} The input object, modified (or a new one if none was provided). + */ + readNode: function(node, obj, first) { + // on subsequent calls of format.read(), we want to reset auto- + // configured properties and auto-configure again. + if (first === true && this.autoConfig === true) { + this.featureType = null; + delete this.namespaceAlias[this.featureNS]; + delete this.namespaces["feature"]; + this.featureNS = null; + } + // featureType auto-configuration + if (!this.featureNS && (!(node.prefix in this.namespaces) && + node.parentNode.namespaceURI == this.namespaces["gml"] && + this.regExes.featureMember.test(node.parentNode.nodeName))) { + this.featureType = node.nodeName.split(":").pop(); + this.setNamespace("feature", node.namespaceURI); + this.featureNS = node.namespaceURI; + this.autoConfig = true; + } + return OpenLayers.Format.XML.prototype.readNode.apply(this, [node, obj]); + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "gml": { + "_inherit": function(node, obj, container) { + // To be implemented by version specific parsers + }, + "featureMember": function(node, obj) { + this.readChildNodes(node, obj); + }, + "featureMembers": function(node, obj) { + this.readChildNodes(node, obj); + }, + "name": function(node, obj) { + obj.name = this.getChildValue(node); + }, + "boundedBy": function(node, obj) { + var container = {}; + this.readChildNodes(node, container); + if(container.components && container.components.length > 0) { + obj.bounds = container.components[0]; + } + }, + "Point": function(node, container) { + var obj = {points: []}; + this.readChildNodes(node, obj); + if(!container.components) { + container.components = []; + } + container.components.push(obj.points[0]); + }, + "coordinates": function(node, obj) { + var str = this.getChildValue(node).replace( + this.regExes.trimSpace, "" + ); + str = str.replace(this.regExes.trimComma, ","); + var pointList = str.split(this.regExes.splitSpace); + var coords; + var numPoints = pointList.length; + var points = new Array(numPoints); + for(var i=0; i<numPoints; ++i) { + coords = pointList[i].split(","); + if (this.xy) { + points[i] = new OpenLayers.Geometry.Point( + coords[0], coords[1], coords[2] + ); + } else { + points[i] = new OpenLayers.Geometry.Point( + coords[1], coords[0], coords[2] + ); + } + } + obj.points = points; + }, + "coord": function(node, obj) { + var coord = {}; + this.readChildNodes(node, coord); + if(!obj.points) { + obj.points = []; + } + obj.points.push(new OpenLayers.Geometry.Point( + coord.x, coord.y, coord.z + )); + }, + "X": function(node, coord) { + coord.x = this.getChildValue(node); + }, + "Y": function(node, coord) { + coord.y = this.getChildValue(node); + }, + "Z": function(node, coord) { + coord.z = this.getChildValue(node); + }, + "MultiPoint": function(node, container) { + var obj = {components: []}; + this.readers.gml._inherit.apply(this, [node, obj, container]); + this.readChildNodes(node, obj); + container.components = [ + new OpenLayers.Geometry.MultiPoint(obj.components) + ]; + }, + "pointMember": function(node, obj) { + this.readChildNodes(node, obj); + }, + "LineString": function(node, container) { + var obj = {}; + this.readers.gml._inherit.apply(this, [node, obj, container]); + this.readChildNodes(node, obj); + if(!container.components) { + container.components = []; + } + container.components.push( + new OpenLayers.Geometry.LineString(obj.points) + ); + }, + "MultiLineString": function(node, container) { + var obj = {components: []}; + this.readers.gml._inherit.apply(this, [node, obj, container]); + this.readChildNodes(node, obj); + container.components = [ + new OpenLayers.Geometry.MultiLineString(obj.components) + ]; + }, + "lineStringMember": function(node, obj) { + this.readChildNodes(node, obj); + }, + "Polygon": function(node, container) { + var obj = {outer: null, inner: []}; + this.readers.gml._inherit.apply(this, [node, obj, container]); + this.readChildNodes(node, obj); + obj.inner.unshift(obj.outer); + if(!container.components) { + container.components = []; + } + container.components.push( + new OpenLayers.Geometry.Polygon(obj.inner) + ); + }, + "LinearRing": function(node, obj) { + var container = {}; + this.readers.gml._inherit.apply(this, [node, container]); + this.readChildNodes(node, container); + obj.components = [new OpenLayers.Geometry.LinearRing( + container.points + )]; + }, + "MultiPolygon": function(node, container) { + var obj = {components: []}; + this.readers.gml._inherit.apply(this, [node, obj, container]); + this.readChildNodes(node, obj); + container.components = [ + new OpenLayers.Geometry.MultiPolygon(obj.components) + ]; + }, + "polygonMember": function(node, obj) { + this.readChildNodes(node, obj); + }, + "GeometryCollection": function(node, container) { + var obj = {components: []}; + this.readers.gml._inherit.apply(this, [node, obj, container]); + this.readChildNodes(node, obj); + container.components = [ + new OpenLayers.Geometry.Collection(obj.components) + ]; + }, + "geometryMember": function(node, obj) { + this.readChildNodes(node, obj); + } + }, + "feature": { + "*": function(node, obj) { + // The node can either be named like the featureType, or it + // can be a child of the feature:featureType. Children can be + // geometry or attributes. + var name; + var local = node.localName || node.nodeName.split(":").pop(); + // Since an attribute can have the same name as the feature type + // we only want to read the node as a feature if the parent + // node can have feature nodes as children. In this case, the + // obj.features property is set. + if (obj.features) { + if (!this.singleFeatureType && + (OpenLayers.Util.indexOf(this.featureType, local) !== -1)) { + name = "_typeName"; + } else if(local === this.featureType) { + name = "_typeName"; + } + } else { + // Assume attribute elements have one child node and that the child + // is a text node. Otherwise assume it is a geometry node. + if(node.childNodes.length == 0 || + (node.childNodes.length == 1 && node.firstChild.nodeType == 3)) { + if(this.extractAttributes) { + name = "_attribute"; + } + } else { + name = "_geometry"; + } + } + if(name) { + this.readers.feature[name].apply(this, [node, obj]); + } + }, + "_typeName": function(node, obj) { + var container = {components: [], attributes: {}}; + this.readChildNodes(node, container); + // look for common gml namespaced elements + if(container.name) { + container.attributes.name = container.name; + } + var feature = new OpenLayers.Feature.Vector( + container.components[0], container.attributes + ); + if (!this.singleFeatureType) { + feature.type = node.nodeName.split(":").pop(); + feature.namespace = node.namespaceURI; + } + var fid = node.getAttribute("fid") || + this.getAttributeNS(node, this.namespaces["gml"], "id"); + if(fid) { + feature.fid = fid; + } + if(this.internalProjection && this.externalProjection && + feature.geometry) { + feature.geometry.transform( + this.externalProjection, this.internalProjection + ); + } + if(container.bounds) { + feature.bounds = container.bounds; + } + obj.features.push(feature); + }, + "_geometry": function(node, obj) { + if (!this.geometryName) { + this.geometryName = node.nodeName.split(":").pop(); + } + this.readChildNodes(node, obj); + }, + "_attribute": function(node, obj) { + var local = node.localName || node.nodeName.split(":").pop(); + var value = this.getChildValue(node); + obj.attributes[local] = value; + } + }, + "wfs": { + "FeatureCollection": function(node, obj) { + this.readChildNodes(node, obj); + } + } + }, + + /** + * Method: write + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>) | OpenLayers.Feature.Vector} + * An array of features or a single feature. + * + * Returns: + * {String} Given an array of features, a doc with a gml:featureMembers + * element will be returned. Given a single feature, a doc with a + * gml:featureMember element will be returned. + */ + write: function(features) { + var name; + if(OpenLayers.Util.isArray(features)) { + name = "featureMembers"; + } else { + name = "featureMember"; + } + var root = this.writeNode("gml:" + name, features); + this.setAttributeNS( + root, this.namespaces["xsi"], + "xsi:schemaLocation", this.schemaLocation + ); + + return OpenLayers.Format.XML.prototype.write.apply(this, [root]); + }, + + /** + * Property: writers + * As a compliment to the readers property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: { + "gml": { + "featureMember": function(feature) { + var node = this.createElementNSPlus("gml:featureMember"); + this.writeNode("feature:_typeName", feature, node); + return node; + }, + "MultiPoint": function(geometry) { + var node = this.createElementNSPlus("gml:MultiPoint"); + var components = geometry.components || [geometry]; + for(var i=0, ii=components.length; i<ii; ++i) { + this.writeNode("pointMember", components[i], node); + } + return node; + }, + "pointMember": function(geometry) { + var node = this.createElementNSPlus("gml:pointMember"); + this.writeNode("Point", geometry, node); + return node; + }, + "MultiLineString": function(geometry) { + var node = this.createElementNSPlus("gml:MultiLineString"); + var components = geometry.components || [geometry]; + for(var i=0, ii=components.length; i<ii; ++i) { + this.writeNode("lineStringMember", components[i], node); + } + return node; + }, + "lineStringMember": function(geometry) { + var node = this.createElementNSPlus("gml:lineStringMember"); + this.writeNode("LineString", geometry, node); + return node; + }, + "MultiPolygon": function(geometry) { + var node = this.createElementNSPlus("gml:MultiPolygon"); + var components = geometry.components || [geometry]; + for(var i=0, ii=components.length; i<ii; ++i) { + this.writeNode( + "polygonMember", components[i], node + ); + } + return node; + }, + "polygonMember": function(geometry) { + var node = this.createElementNSPlus("gml:polygonMember"); + this.writeNode("Polygon", geometry, node); + return node; + }, + "GeometryCollection": function(geometry) { + var node = this.createElementNSPlus("gml:GeometryCollection"); + for(var i=0, len=geometry.components.length; i<len; ++i) { + this.writeNode("geometryMember", geometry.components[i], node); + } + return node; + }, + "geometryMember": function(geometry) { + var node = this.createElementNSPlus("gml:geometryMember"); + var child = this.writeNode("feature:_geometry", geometry); + node.appendChild(child.firstChild); + return node; + } + }, + "feature": { + "_typeName": function(feature) { + var node = this.createElementNSPlus("feature:" + this.featureType, { + attributes: {fid: feature.fid} + }); + if(feature.geometry) { + this.writeNode("feature:_geometry", feature.geometry, node); + } + for(var name in feature.attributes) { + var value = feature.attributes[name]; + if(value != null) { + this.writeNode( + "feature:_attribute", + {name: name, value: value}, node + ); + } + } + return node; + }, + "_geometry": function(geometry) { + if(this.externalProjection && this.internalProjection) { + geometry = geometry.clone().transform( + this.internalProjection, this.externalProjection + ); + } + var node = this.createElementNSPlus( + "feature:" + this.geometryName + ); + var type = this.geometryTypes[geometry.CLASS_NAME]; + var child = this.writeNode("gml:" + type, geometry, node); + if(this.srsName) { + child.setAttribute("srsName", this.srsName); + } + return node; + }, + "_attribute": function(obj) { + return this.createElementNSPlus("feature:" + obj.name, { + value: obj.value + }); + } + }, + "wfs": { + "FeatureCollection": function(features) { + /** + * This is only here because GML2 only describes abstract + * feature collections. Typically, you would not be using + * the GML format to write wfs elements. This just provides + * some way to write out lists of features. GML3 defines the + * featureMembers element, so that is used by default instead. + */ + var node = this.createElementNSPlus("wfs:FeatureCollection"); + for(var i=0, len=features.length; i<len; ++i) { + this.writeNode("gml:featureMember", features[i], node); + } + return node; + } + } + }, + + /** + * Method: setGeometryTypes + * Sets the <geometryTypes> mapping. + */ + setGeometryTypes: function() { + this.geometryTypes = { + "OpenLayers.Geometry.Point": "Point", + "OpenLayers.Geometry.MultiPoint": "MultiPoint", + "OpenLayers.Geometry.LineString": "LineString", + "OpenLayers.Geometry.MultiLineString": "MultiLineString", + "OpenLayers.Geometry.Polygon": "Polygon", + "OpenLayers.Geometry.MultiPolygon": "MultiPolygon", + "OpenLayers.Geometry.Collection": "GeometryCollection" + }; + }, + + CLASS_NAME: "OpenLayers.Format.GML.Base" + +}); +/* ====================================================================== + OpenLayers/Format/GML/v2.js + ====================================================================== */ + +/* 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/Format/GML/Base.js + */ + +/** + * Class: OpenLayers.Format.GML.v2 + * Parses GML version 2. + * + * Inherits from: + * - <OpenLayers.Format.GML.Base> + */ +OpenLayers.Format.GML.v2 = OpenLayers.Class(OpenLayers.Format.GML.Base, { + + /** + * Property: schemaLocation + * {String} Schema location for a particular minor version. + */ + schemaLocation: "http://www.opengis.net/gml http://schemas.opengis.net/gml/2.1.2/feature.xsd", + + /** + * Constructor: OpenLayers.Format.GML.v2 + * Create a parser for GML v2. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + * + * Valid options properties: + * featureType - {String} Local (without prefix) feature typeName (required). + * featureNS - {String} Feature namespace (required). + * geometryName - {String} Geometry element name. + */ + initialize: function(options) { + OpenLayers.Format.GML.Base.prototype.initialize.apply(this, [options]); + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "gml": OpenLayers.Util.applyDefaults({ + "outerBoundaryIs": function(node, container) { + var obj = {}; + this.readChildNodes(node, obj); + container.outer = obj.components[0]; + }, + "innerBoundaryIs": function(node, container) { + var obj = {}; + this.readChildNodes(node, obj); + container.inner.push(obj.components[0]); + }, + "Box": function(node, container) { + var obj = {}; + this.readChildNodes(node, obj); + if(!container.components) { + container.components = []; + } + var min = obj.points[0]; + var max = obj.points[1]; + container.components.push( + new OpenLayers.Bounds(min.x, min.y, max.x, max.y) + ); + } + }, OpenLayers.Format.GML.Base.prototype.readers["gml"]), + "feature": OpenLayers.Format.GML.Base.prototype.readers["feature"], + "wfs": OpenLayers.Format.GML.Base.prototype.readers["wfs"] + }, + + /** + * Method: write + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>) | OpenLayers.Feature.Vector} + * An array of features or a single feature. + * + * Returns: + * {String} Given an array of features, a doc with a gml:featureMembers + * element will be returned. Given a single feature, a doc with a + * gml:featureMember element will be returned. + */ + write: function(features) { + var name; + if(OpenLayers.Util.isArray(features)) { + // GML2 only has abstract feature collections + // wfs provides a feature collection from a well-known schema + name = "wfs:FeatureCollection"; + } else { + name = "gml:featureMember"; + } + var root = this.writeNode(name, features); + this.setAttributeNS( + root, this.namespaces["xsi"], + "xsi:schemaLocation", this.schemaLocation + ); + + return OpenLayers.Format.XML.prototype.write.apply(this, [root]); + }, + + /** + * Property: writers + * As a compliment to the readers property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: { + "gml": OpenLayers.Util.applyDefaults({ + "Point": function(geometry) { + var node = this.createElementNSPlus("gml:Point"); + this.writeNode("coordinates", [geometry], node); + return node; + }, + "coordinates": function(points) { + var numPoints = points.length; + var parts = new Array(numPoints); + var point; + for(var i=0; i<numPoints; ++i) { + point = points[i]; + if(this.xy) { + parts[i] = point.x + "," + point.y; + } else { + parts[i] = point.y + "," + point.x; + } + if(point.z != undefined) { // allow null or undefined + parts[i] += "," + point.z; + } + } + return this.createElementNSPlus("gml:coordinates", { + attributes: { + decimal: ".", cs: ",", ts: " " + }, + value: (numPoints == 1) ? parts[0] : parts.join(" ") + }); + }, + "LineString": function(geometry) { + var node = this.createElementNSPlus("gml:LineString"); + this.writeNode("coordinates", geometry.components, node); + return node; + }, + "Polygon": function(geometry) { + var node = this.createElementNSPlus("gml:Polygon"); + this.writeNode("outerBoundaryIs", geometry.components[0], node); + for(var i=1; i<geometry.components.length; ++i) { + this.writeNode( + "innerBoundaryIs", geometry.components[i], node + ); + } + return node; + }, + "outerBoundaryIs": function(ring) { + var node = this.createElementNSPlus("gml:outerBoundaryIs"); + this.writeNode("LinearRing", ring, node); + return node; + }, + "innerBoundaryIs": function(ring) { + var node = this.createElementNSPlus("gml:innerBoundaryIs"); + this.writeNode("LinearRing", ring, node); + return node; + }, + "LinearRing": function(ring) { + var node = this.createElementNSPlus("gml:LinearRing"); + this.writeNode("coordinates", ring.components, node); + return node; + }, + "Box": function(bounds) { + var node = this.createElementNSPlus("gml:Box"); + this.writeNode("coordinates", [ + {x: bounds.left, y: bounds.bottom}, + {x: bounds.right, y: bounds.top} + ], node); + // srsName attribute is optional for gml:Box + if(this.srsName) { + node.setAttribute("srsName", this.srsName); + } + return node; + } + }, OpenLayers.Format.GML.Base.prototype.writers["gml"]), + "feature": OpenLayers.Format.GML.Base.prototype.writers["feature"], + "wfs": OpenLayers.Format.GML.Base.prototype.writers["wfs"] + }, + + CLASS_NAME: "OpenLayers.Format.GML.v2" + +}); +/* ====================================================================== + OpenLayers/Filter/Function.js + ====================================================================== */ + +/* 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/Filter.js + */ + +/** + * Class: OpenLayers.Filter.Function + * This class represents a filter function. + * We are using this class for creation of complex + * filters that can contain filter functions as values. + * Nesting function as other functions parameter is supported. + * + * Inherits from: + * - <OpenLayers.Filter> + */ +OpenLayers.Filter.Function = OpenLayers.Class(OpenLayers.Filter, { + + /** + * APIProperty: name + * {String} Name of the function. + */ + name: null, + + /** + * APIProperty: params + * {Array(<OpenLayers.Filter.Function> || String || Number)} Function parameters + * For now support only other Functions, String or Number + */ + params: null, + + /** + * Constructor: OpenLayers.Filter.Function + * Creates a filter function. + * + * Parameters: + * options - {Object} An optional object with properties to set on the + * function. + * + * Returns: + * {<OpenLayers.Filter.Function>} + */ + + CLASS_NAME: "OpenLayers.Filter.Function" +}); + +/* ====================================================================== + OpenLayers/BaseTypes/Date.js + ====================================================================== */ + +/* 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/SingleFile.js + */ + +/** + * Namespace: OpenLayers.Date + * Contains implementations of Date.parse and date.toISOString that match the + * ECMAScript 5 specification for parsing RFC 3339 dates. + * http://tools.ietf.org/html/rfc3339 + */ +OpenLayers.Date = { + + /** + * APIProperty: dateRegEx + * The regex to be used for validating dates. You can provide your own + * regex for instance for adding support for years before BC. Default + * value is: /^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:(?:T(\d{1,2}):(\d{2}):(\d{2}(?:\.\d+)?)(Z|(?:[+-]\d{1,2}(?::(\d{2}))?)))|Z)?$/ + */ + dateRegEx: /^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:(?:T(\d{1,2}):(\d{2}):(\d{2}(?:\.\d+)?)(Z|(?:[+-]\d{1,2}(?::(\d{2}))?)))|Z)?$/, + + /** + * APIMethod: toISOString + * Generates a string representing a date. The format of the string follows + * the profile of ISO 8601 for date and time on the Internet (see + * http://tools.ietf.org/html/rfc3339). If the toISOString method is + * available on the Date prototype, that is used. The toISOString + * method for Date instances is defined in ECMA-262. + * + * Parameters: + * date - {Date} A date object. + * + * Returns: + * {String} A string representing the date (e.g. + * "2010-08-07T16:58:23.123Z"). If the date does not have a valid time + * (i.e. isNaN(date.getTime())) this method returns the string "Invalid + * Date". The ECMA standard says the toISOString method should throw + * RangeError in this case, but Firefox returns a string instead. For + * best results, use isNaN(date.getTime()) to determine date validity + * before generating date strings. + */ + toISOString: (function() { + if ("toISOString" in Date.prototype) { + return function(date) { + return date.toISOString(); + }; + } else { + return function(date) { + var str; + if (isNaN(date.getTime())) { + // ECMA-262 says throw RangeError, Firefox returns + // "Invalid Date" + str = "Invalid Date"; + } else { + str = + date.getUTCFullYear() + "-" + + OpenLayers.Number.zeroPad(date.getUTCMonth() + 1, 2) + "-" + + OpenLayers.Number.zeroPad(date.getUTCDate(), 2) + "T" + + OpenLayers.Number.zeroPad(date.getUTCHours(), 2) + ":" + + OpenLayers.Number.zeroPad(date.getUTCMinutes(), 2) + ":" + + OpenLayers.Number.zeroPad(date.getUTCSeconds(), 2) + "." + + OpenLayers.Number.zeroPad(date.getUTCMilliseconds(), 3) + "Z"; + } + return str; + }; + } + + })(), + + /** + * APIMethod: parse + * Generate a date object from a string. The format for the string follows + * the profile of ISO 8601 for date and time on the Internet (see + * http://tools.ietf.org/html/rfc3339). We don't call the native + * Date.parse because of inconsistency between implmentations. In + * Chrome, calling Date.parse with a string that doesn't contain any + * indication of the timezone (e.g. "2011"), the date is interpreted + * in local time. On Firefox, the assumption is UTC. + * + * Parameters: + * str - {String} A string representing the date (e.g. + * "2010", "2010-08", "2010-08-07", "2010-08-07T16:58:23.123Z", + * "2010-08-07T11:58:23.123-06"). + * + * Returns: + * {Date} A date object. If the string could not be parsed, an invalid + * date is returned (i.e. isNaN(date.getTime())). + */ + parse: function(str) { + var date; + var match = str.match(this.dateRegEx); + if (match && (match[1] || match[7])) { // must have at least year or time + var year = parseInt(match[1], 10) || 0; + var month = (parseInt(match[2], 10) - 1) || 0; + var day = parseInt(match[3], 10) || 1; + date = new Date(Date.UTC(year, month, day)); + // optional time + var type = match[7]; + if (type) { + var hours = parseInt(match[4], 10); + var minutes = parseInt(match[5], 10); + var secFrac = parseFloat(match[6]); + var seconds = secFrac | 0; + var milliseconds = Math.round(1000 * (secFrac - seconds)); + date.setUTCHours(hours, minutes, seconds, milliseconds); + // check offset + if (type !== "Z") { + var hoursOffset = parseInt(type, 10); + var minutesOffset = parseInt(match[8], 10) || 0; + var offset = -1000 * (60 * (hoursOffset * 60) + minutesOffset * 60); + date = new Date(date.getTime() + offset); + } + } + } else { + date = new Date("invalid"); + } + return date; + } +}; +/* ====================================================================== + OpenLayers/Format/Filter/v1.js + ====================================================================== */ + +/* 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/Format/Filter.js + * @requires OpenLayers/Format/XML.js + * @requires OpenLayers/Filter/Function.js + * @requires OpenLayers/BaseTypes/Date.js + */ + +/** + * Class: OpenLayers.Format.Filter.v1 + * Superclass for Filter version 1 parsers. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.Filter.v1 = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. + */ + namespaces: { + ogc: "http://www.opengis.net/ogc", + gml: "http://www.opengis.net/gml", + xlink: "http://www.w3.org/1999/xlink", + xsi: "http://www.w3.org/2001/XMLSchema-instance" + }, + + /** + * Property: defaultPrefix + */ + defaultPrefix: "ogc", + + /** + * Property: schemaLocation + * {String} Schema location for a particular minor version. + */ + schemaLocation: null, + + /** + * Constructor: OpenLayers.Format.Filter.v1 + * Instances of this class are not created directly. Use the + * <OpenLayers.Format.Filter> constructor instead. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + }, + + /** + * Method: read + * + * Parameters: + * data - {DOMElement} A Filter document element. + * + * Returns: + * {<OpenLayers.Filter>} A filter object. + */ + read: function(data) { + var obj = {}; + this.readers.ogc["Filter"].apply(this, [data, obj]); + return obj.filter; + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "ogc": { + "_expression": function(node) { + // only the simplest of ogc:expression handled + // "some text and an <PropertyName>attribute</PropertyName>"} + var obj, value = ""; + for(var child=node.firstChild; child; child=child.nextSibling) { + switch(child.nodeType) { + case 1: + obj = this.readNode(child); + if (obj.property) { + value += "${" + obj.property + "}"; + } else if (obj.value !== undefined) { + value += obj.value; + } + break; + case 3: // text node + case 4: // cdata section + value += child.nodeValue; + } + } + return value; + }, + "Filter": function(node, parent) { + // Filters correspond to subclasses of OpenLayers.Filter. + // Since they contain information we don't persist, we + // create a temporary object and then pass on the filter + // (ogc:Filter) to the parent obj. + var obj = { + fids: [], + filters: [] + }; + this.readChildNodes(node, obj); + if(obj.fids.length > 0) { + parent.filter = new OpenLayers.Filter.FeatureId({ + fids: obj.fids + }); + } else if(obj.filters.length > 0) { + parent.filter = obj.filters[0]; + } + }, + "FeatureId": function(node, obj) { + var fid = node.getAttribute("fid"); + if(fid) { + obj.fids.push(fid); + } + }, + "And": function(node, obj) { + var filter = new OpenLayers.Filter.Logical({ + type: OpenLayers.Filter.Logical.AND + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "Or": function(node, obj) { + var filter = new OpenLayers.Filter.Logical({ + type: OpenLayers.Filter.Logical.OR + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "Not": function(node, obj) { + var filter = new OpenLayers.Filter.Logical({ + type: OpenLayers.Filter.Logical.NOT + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsLessThan": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.LESS_THAN + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsGreaterThan": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.GREATER_THAN + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsLessThanOrEqualTo": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsGreaterThanOrEqualTo": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsBetween": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.BETWEEN + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "Literal": function(node, obj) { + obj.value = OpenLayers.String.numericIf( + this.getChildValue(node), true); + }, + "PropertyName": function(node, filter) { + filter.property = this.getChildValue(node); + }, + "LowerBoundary": function(node, filter) { + filter.lowerBoundary = OpenLayers.String.numericIf( + this.readers.ogc._expression.call(this, node), true); + }, + "UpperBoundary": function(node, filter) { + filter.upperBoundary = OpenLayers.String.numericIf( + this.readers.ogc._expression.call(this, node), true); + }, + "Intersects": function(node, obj) { + this.readSpatial(node, obj, OpenLayers.Filter.Spatial.INTERSECTS); + }, + "Within": function(node, obj) { + this.readSpatial(node, obj, OpenLayers.Filter.Spatial.WITHIN); + }, + "Contains": function(node, obj) { + this.readSpatial(node, obj, OpenLayers.Filter.Spatial.CONTAINS); + }, + "DWithin": function(node, obj) { + this.readSpatial(node, obj, OpenLayers.Filter.Spatial.DWITHIN); + }, + "Distance": function(node, obj) { + obj.distance = parseInt(this.getChildValue(node)); + obj.distanceUnits = node.getAttribute("units"); + }, + "Function": function(node, obj) { + //TODO write decoder for it + return; + }, + "PropertyIsNull": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.IS_NULL + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + } + } + }, + + /** + * Method: readSpatial + * + * Read a {<OpenLayers.Filter.Spatial>} filter. + * + * Parameters: + * node - {DOMElement} A DOM element that contains an ogc:expression. + * obj - {Object} The target object. + * type - {String} One of the OpenLayers.Filter.Spatial.* constants. + * + * Returns: + * {<OpenLayers.Filter.Spatial>} The created filter. + */ + readSpatial: function(node, obj, type) { + var filter = new OpenLayers.Filter.Spatial({ + type: type + }); + this.readChildNodes(node, filter); + filter.value = filter.components[0]; + delete filter.components; + obj.filters.push(filter); + }, + + /** + * APIMethod: encodeLiteral + * Generates the string representation of a value for use in <Literal> + * elements. The default encoder writes Date values as ISO 8601 + * strings. + * + * Parameters: + * value - {Object} Literal value to encode + * + * Returns: + * {String} String representation of the provided value. + */ + encodeLiteral: function(value) { + if (value instanceof Date) { + value = OpenLayers.Date.toISOString(value); + } + return value; + }, + + /** + * Method: writeOgcExpression + * Limited support for writing OGC expressions. Currently it supports + * (<OpenLayers.Filter.Function> || String || Number) + * + * Parameters: + * value - (<OpenLayers.Filter.Function> || String || Number) + * node - {DOMElement} A parent DOM element + * + * Returns: + * {DOMElement} Updated node element. + */ + writeOgcExpression: function(value, node) { + if (value instanceof OpenLayers.Filter.Function){ + this.writeNode("Function", value, node); + } else { + this.writeNode("Literal", value, node); + } + return node; + }, + + /** + * Method: write + * + * Parameters: + * filter - {<OpenLayers.Filter>} A filter object. + * + * Returns: + * {DOMElement} An ogc:Filter element. + */ + write: function(filter) { + return this.writers.ogc["Filter"].apply(this, [filter]); + }, + + /** + * Property: writers + * As a compliment to the readers property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: { + "ogc": { + "Filter": function(filter) { + var node = this.createElementNSPlus("ogc:Filter"); + this.writeNode(this.getFilterType(filter), filter, node); + return node; + }, + "_featureIds": function(filter) { + var node = this.createDocumentFragment(); + for (var i=0, ii=filter.fids.length; i<ii; ++i) { + this.writeNode("ogc:FeatureId", filter.fids[i], node); + } + return node; + }, + "FeatureId": function(fid) { + return this.createElementNSPlus("ogc:FeatureId", { + attributes: {fid: fid} + }); + }, + "And": function(filter) { + var node = this.createElementNSPlus("ogc:And"); + var childFilter; + for (var i=0, ii=filter.filters.length; i<ii; ++i) { + childFilter = filter.filters[i]; + this.writeNode( + this.getFilterType(childFilter), childFilter, node + ); + } + return node; + }, + "Or": function(filter) { + var node = this.createElementNSPlus("ogc:Or"); + var childFilter; + for (var i=0, ii=filter.filters.length; i<ii; ++i) { + childFilter = filter.filters[i]; + this.writeNode( + this.getFilterType(childFilter), childFilter, node + ); + } + return node; + }, + "Not": function(filter) { + var node = this.createElementNSPlus("ogc:Not"); + var childFilter = filter.filters[0]; + this.writeNode( + this.getFilterType(childFilter), childFilter, node + ); + return node; + }, + "PropertyIsLessThan": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsLessThan"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + // handle Literals or Functions for now + this.writeOgcExpression(filter.value, node); + return node; + }, + "PropertyIsGreaterThan": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsGreaterThan"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + // handle Literals or Functions for now + this.writeOgcExpression(filter.value, node); + return node; + }, + "PropertyIsLessThanOrEqualTo": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsLessThanOrEqualTo"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + // handle Literals or Functions for now + this.writeOgcExpression(filter.value, node); + return node; + }, + "PropertyIsGreaterThanOrEqualTo": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsGreaterThanOrEqualTo"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + // handle Literals or Functions for now + this.writeOgcExpression(filter.value, node); + return node; + }, + "PropertyIsBetween": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsBetween"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + this.writeNode("LowerBoundary", filter, node); + this.writeNode("UpperBoundary", filter, node); + return node; + }, + "PropertyName": function(filter) { + // no ogc:expression handling for now + return this.createElementNSPlus("ogc:PropertyName", { + value: filter.property + }); + }, + "Literal": function(value) { + var encode = this.encodeLiteral || + OpenLayers.Format.Filter.v1.prototype.encodeLiteral; + return this.createElementNSPlus("ogc:Literal", { + value: encode(value) + }); + }, + "LowerBoundary": function(filter) { + // handle Literals or Functions for now + var node = this.createElementNSPlus("ogc:LowerBoundary"); + this.writeOgcExpression(filter.lowerBoundary, node); + return node; + }, + "UpperBoundary": function(filter) { + // handle Literals or Functions for now + var node = this.createElementNSPlus("ogc:UpperBoundary"); + this.writeNode("Literal", filter.upperBoundary, node); + return node; + }, + "INTERSECTS": function(filter) { + return this.writeSpatial(filter, "Intersects"); + }, + "WITHIN": function(filter) { + return this.writeSpatial(filter, "Within"); + }, + "CONTAINS": function(filter) { + return this.writeSpatial(filter, "Contains"); + }, + "DWITHIN": function(filter) { + var node = this.writeSpatial(filter, "DWithin"); + this.writeNode("Distance", filter, node); + return node; + }, + "Distance": function(filter) { + return this.createElementNSPlus("ogc:Distance", { + attributes: { + units: filter.distanceUnits + }, + value: filter.distance + }); + }, + "Function": function(filter) { + var node = this.createElementNSPlus("ogc:Function", { + attributes: { + name: filter.name + } + }); + var params = filter.params; + for(var i=0, len=params.length; i<len; i++){ + this.writeOgcExpression(params[i], node); + } + return node; + }, + "PropertyIsNull": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsNull"); + this.writeNode("PropertyName", filter, node); + return node; + } + } + }, + + /** + * Method: getFilterType + */ + getFilterType: function(filter) { + var filterType = this.filterMap[filter.type]; + if(!filterType) { + throw "Filter writing not supported for rule type: " + filter.type; + } + return filterType; + }, + + /** + * Property: filterMap + * {Object} Contains a member for each filter type. Values are node names + * for corresponding OGC Filter child elements. + */ + filterMap: { + "&&": "And", + "||": "Or", + "!": "Not", + "==": "PropertyIsEqualTo", + "!=": "PropertyIsNotEqualTo", + "<": "PropertyIsLessThan", + ">": "PropertyIsGreaterThan", + "<=": "PropertyIsLessThanOrEqualTo", + ">=": "PropertyIsGreaterThanOrEqualTo", + "..": "PropertyIsBetween", + "~": "PropertyIsLike", + "NULL": "PropertyIsNull", + "BBOX": "BBOX", + "DWITHIN": "DWITHIN", + "WITHIN": "WITHIN", + "CONTAINS": "CONTAINS", + "INTERSECTS": "INTERSECTS", + "FID": "_featureIds" + }, + + CLASS_NAME: "OpenLayers.Format.Filter.v1" + +}); +/* ====================================================================== + OpenLayers/Format/Filter/v1_0_0.js + ====================================================================== */ + +/* 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/Format/GML/v2.js + * @requires OpenLayers/Format/Filter/v1.js + */ + +/** + * Class: OpenLayers.Format.Filter.v1_0_0 + * Write ogc:Filter version 1.0.0. + * + * Inherits from: + * - <OpenLayers.Format.GML.v2> + * - <OpenLayers.Format.Filter.v1> + */ +OpenLayers.Format.Filter.v1_0_0 = OpenLayers.Class( + OpenLayers.Format.GML.v2, OpenLayers.Format.Filter.v1, { + + /** + * Constant: VERSION + * {String} 1.0.0 + */ + VERSION: "1.0.0", + + /** + * Property: schemaLocation + * {String} http://www.opengis.net/ogc/filter/1.0.0/filter.xsd + */ + schemaLocation: "http://www.opengis.net/ogc/filter/1.0.0/filter.xsd", + + /** + * Constructor: OpenLayers.Format.Filter.v1_0_0 + * Instances of this class are not created directly. Use the + * <OpenLayers.Format.Filter> constructor instead. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + OpenLayers.Format.GML.v2.prototype.initialize.apply( + this, [options] + ); + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "ogc": OpenLayers.Util.applyDefaults({ + "PropertyIsEqualTo": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.EQUAL_TO + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsNotEqualTo": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.NOT_EQUAL_TO + }); + this.readChildNodes(node, filter); + obj.filters.push(filter); + }, + "PropertyIsLike": function(node, obj) { + var filter = new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.LIKE + }); + this.readChildNodes(node, filter); + var wildCard = node.getAttribute("wildCard"); + var singleChar = node.getAttribute("singleChar"); + var esc = node.getAttribute("escape"); + filter.value2regex(wildCard, singleChar, esc); + obj.filters.push(filter); + } + }, OpenLayers.Format.Filter.v1.prototype.readers["ogc"]), + "gml": OpenLayers.Format.GML.v2.prototype.readers["gml"], + "feature": OpenLayers.Format.GML.v2.prototype.readers["feature"] + }, + + /** + * Property: writers + * As a compliment to the readers property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: { + "ogc": OpenLayers.Util.applyDefaults({ + "PropertyIsEqualTo": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsEqualTo"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + // handle Literals or Functions for now + this.writeOgcExpression(filter.value, node); + return node; + }, + "PropertyIsNotEqualTo": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsNotEqualTo"); + // no ogc:expression handling for PropertyName for now + this.writeNode("PropertyName", filter, node); + // handle Literals or Functions for now + this.writeOgcExpression(filter.value, node); + return node; + }, + "PropertyIsLike": function(filter) { + var node = this.createElementNSPlus("ogc:PropertyIsLike", { + attributes: { + wildCard: "*", singleChar: ".", escape: "!" + } + }); + // no ogc:expression handling for now + this.writeNode("PropertyName", filter, node); + // convert regex string to ogc string + this.writeNode("Literal", filter.regex2value(), node); + return node; + }, + "BBOX": function(filter) { + var node = this.createElementNSPlus("ogc:BBOX"); + // PropertyName is mandatory in 1.0.0, but e.g. GeoServer also + // accepts filters without it. When this is used with + // OpenLayers.Protocol.WFS, OpenLayers.Format.WFST will set a + // missing filter.property to the geometryName that is + // configured with the protocol, which defaults to "the_geom". + // So the only way to omit this mandatory property is to not + // set the property on the filter and to set the geometryName + // on the WFS protocol to null. The latter also happens when + // the protocol is configured without a geometryName and a + // featureNS. + filter.property && this.writeNode("PropertyName", filter, node); + var box = this.writeNode("gml:Box", filter.value, node); + if(filter.projection) { + box.setAttribute("srsName", filter.projection); + } + return node; + } + }, OpenLayers.Format.Filter.v1.prototype.writers["ogc"]), + "gml": OpenLayers.Format.GML.v2.prototype.writers["gml"], + "feature": OpenLayers.Format.GML.v2.prototype.writers["feature"] + }, + + /** + * Method: writeSpatial + * + * Read a {<OpenLayers.Filter.Spatial>} filter and converts it into XML. + * + * Parameters: + * filter - {<OpenLayers.Filter.Spatial>} The filter. + * name - {String} Name of the generated XML element. + * + * Returns: + * {DOMElement} The created XML element. + */ + writeSpatial: function(filter, name) { + var node = this.createElementNSPlus("ogc:"+name); + this.writeNode("PropertyName", filter, node); + if(filter.value instanceof OpenLayers.Filter.Function) { + this.writeNode("Function", filter.value, node); + } else { + var child; + if(filter.value instanceof OpenLayers.Geometry) { + child = this.writeNode("feature:_geometry", filter.value).firstChild; + } else { + child = this.writeNode("gml:Box", filter.value); + } + if(filter.projection) { + child.setAttribute("srsName", filter.projection); + } + node.appendChild(child); + } + return node; + }, + + + CLASS_NAME: "OpenLayers.Format.Filter.v1_0_0" + +}); +/* ====================================================================== + OpenLayers/Format/WFST/v1_0_0.js + ====================================================================== */ + +/* 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/Format/WFST/v1.js + * @requires OpenLayers/Format/Filter/v1_0_0.js + */ + +/** + * Class: OpenLayers.Format.WFST.v1_0_0 + * A format for creating WFS v1.0.0 transactions. Create a new instance with the + * <OpenLayers.Format.WFST.v1_0_0> constructor. + * + * Inherits from: + * - <OpenLayers.Format.Filter.v1_0_0> + * - <OpenLayers.Format.WFST.v1> + */ +OpenLayers.Format.WFST.v1_0_0 = OpenLayers.Class( + OpenLayers.Format.Filter.v1_0_0, OpenLayers.Format.WFST.v1, { + + /** + * Property: version + * {String} WFS version number. + */ + version: "1.0.0", + + /** + * APIProperty: srsNameInQuery + * {Boolean} If true the reference system is passed in Query requests + * via the "srsName" attribute to the "wfs:Query" element, this + * property defaults to false as it isn't WFS 1.0.0 compliant. + */ + srsNameInQuery: false, + + /** + * Property: schemaLocations + * {Object} Properties are namespace aliases, values are schema locations. + */ + schemaLocations: { + "wfs": "http://schemas.opengis.net/wfs/1.0.0/WFS-transaction.xsd" + }, + + /** + * Constructor: OpenLayers.Format.WFST.v1_0_0 + * A class for parsing and generating WFS v1.0.0 transactions. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + * + * Valid options properties: + * featureType - {String} Local (without prefix) feature typeName (required). + * featureNS - {String} Feature namespace (optional). + * featurePrefix - {String} Feature namespace alias (optional - only used + * if featureNS is provided). Default is 'feature'. + * geometryName - {String} Name of geometry attribute. Default is 'the_geom'. + */ + initialize: function(options) { + OpenLayers.Format.Filter.v1_0_0.prototype.initialize.apply(this, [options]); + OpenLayers.Format.WFST.v1.prototype.initialize.apply(this, [options]); + }, + + /** + * Method: readNode + * Shorthand for applying one of the named readers given the node + * namespace and local name. Readers take two args (node, obj) and + * generally extend or modify the second. + * + * Parameters: + * node - {DOMElement} The node to be read (required). + * obj - {Object} The object to be modified (optional). + * first - {Boolean} Should be set to true for the first node read. This + * is usually the readNode call in the read method. Without this being + * set, auto-configured properties will stick on subsequent reads. + * + * Returns: + * {Object} The input object, modified (or a new one if none was provided). + */ + readNode: function(node, obj, first) { + // Not the superclass, only the mixin classes inherit from + // Format.GML.v2. We need this because we don't want to get readNode + // from the superclass's superclass, which is OpenLayers.Format.XML. + return OpenLayers.Format.GML.v2.prototype.readNode.apply(this, arguments); + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "wfs": OpenLayers.Util.applyDefaults({ + "WFS_TransactionResponse": function(node, obj) { + obj.insertIds = []; + obj.success = false; + this.readChildNodes(node, obj); + }, + "InsertResult": function(node, container) { + var obj = {fids: []}; + this.readChildNodes(node, obj); + container.insertIds = container.insertIds.concat(obj.fids); + }, + "TransactionResult": function(node, obj) { + this.readChildNodes(node, obj); + }, + "Status": function(node, obj) { + this.readChildNodes(node, obj); + }, + "SUCCESS": function(node, obj) { + obj.success = true; + } + }, OpenLayers.Format.WFST.v1.prototype.readers["wfs"]), + "gml": OpenLayers.Format.GML.v2.prototype.readers["gml"], + "feature": OpenLayers.Format.GML.v2.prototype.readers["feature"], + "ogc": OpenLayers.Format.Filter.v1_0_0.prototype.readers["ogc"] + }, + + /** + * Property: writers + * As a compliment to the readers property, this structure contains public + * writing functions grouped by namespace alias and named like the + * node names they produce. + */ + writers: { + "wfs": OpenLayers.Util.applyDefaults({ + "Query": function(options) { + options = OpenLayers.Util.extend({ + featureNS: this.featureNS, + featurePrefix: this.featurePrefix, + featureType: this.featureType, + srsName: this.srsName, + srsNameInQuery: this.srsNameInQuery + }, options); + var prefix = options.featurePrefix; + var node = this.createElementNSPlus("wfs:Query", { + attributes: { + typeName: (prefix ? prefix + ":" : "") + + options.featureType + } + }); + if(options.srsNameInQuery && options.srsName) { + node.setAttribute("srsName", options.srsName); + } + if(options.featureNS) { + node.setAttribute("xmlns:" + prefix, options.featureNS); + } + if(options.propertyNames) { + for(var i=0,len = options.propertyNames.length; i<len; i++) { + this.writeNode( + "ogc:PropertyName", + {property: options.propertyNames[i]}, + node + ); + } + } + if(options.filter) { + this.setFilterProperty(options.filter); + this.writeNode("ogc:Filter", options.filter, node); + } + return node; + } + }, OpenLayers.Format.WFST.v1.prototype.writers["wfs"]), + "gml": OpenLayers.Format.GML.v2.prototype.writers["gml"], + "feature": OpenLayers.Format.GML.v2.prototype.writers["feature"], + "ogc": OpenLayers.Format.Filter.v1_0_0.prototype.writers["ogc"] + }, + + CLASS_NAME: "OpenLayers.Format.WFST.v1_0_0" +}); +/* ====================================================================== + OpenLayers/Renderer/Elements.js + ====================================================================== */ + +/* 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/Renderer.js + */ + +/** + * Class: OpenLayers.ElementsIndexer + * This class takes care of figuring out which order elements should be + * placed in the DOM based on given indexing methods. + */ +OpenLayers.ElementsIndexer = OpenLayers.Class({ + + /** + * Property: maxZIndex + * {Integer} This is the largest-most z-index value for a node + * contained within the indexer. + */ + maxZIndex: null, + + /** + * Property: order + * {Array<String>} This is an array of node id's stored in the + * order that they should show up on screen. Id's higher up in the + * array (higher array index) represent nodes with higher z-indeces. + */ + order: null, + + /** + * Property: indices + * {Object} This is a hash that maps node ids to their z-index value + * stored in the indexer. This is done to make finding a nodes z-index + * value O(1). + */ + indices: null, + + /** + * Property: compare + * {Function} This is the function used to determine placement of + * of a new node within the indexer. If null, this defaults to to + * the Z_ORDER_DRAWING_ORDER comparison method. + */ + compare: null, + + /** + * APIMethod: initialize + * Create a new indexer with + * + * Parameters: + * yOrdering - {Boolean} Whether to use y-ordering. + */ + initialize: function(yOrdering) { + + this.compare = yOrdering ? + OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER_Y_ORDER : + OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER_DRAWING_ORDER; + + this.clear(); + }, + + /** + * APIMethod: insert + * Insert a new node into the indexer. In order to find the correct + * positioning for the node to be inserted, this method uses a binary + * search. This makes inserting O(log(n)). + * + * Parameters: + * newNode - {DOMElement} The new node to be inserted. + * + * Returns + * {DOMElement} the node before which we should insert our newNode, or + * null if newNode can just be appended. + */ + insert: function(newNode) { + // If the node is known to the indexer, remove it so we can + // recalculate where it should go. + if (this.exists(newNode)) { + this.remove(newNode); + } + + var nodeId = newNode.id; + + this.determineZIndex(newNode); + + var leftIndex = -1; + var rightIndex = this.order.length; + var middle; + + while (rightIndex - leftIndex > 1) { + middle = parseInt((leftIndex + rightIndex) / 2); + + var placement = this.compare(this, newNode, + OpenLayers.Util.getElement(this.order[middle])); + + if (placement > 0) { + leftIndex = middle; + } else { + rightIndex = middle; + } + } + + this.order.splice(rightIndex, 0, nodeId); + this.indices[nodeId] = this.getZIndex(newNode); + + // If the new node should be before another in the index + // order, return the node before which we have to insert the new one; + // else, return null to indicate that the new node can be appended. + return this.getNextElement(rightIndex); + }, + + /** + * APIMethod: remove + * + * Parameters: + * node - {DOMElement} The node to be removed. + */ + remove: function(node) { + var nodeId = node.id; + var arrayIndex = OpenLayers.Util.indexOf(this.order, nodeId); + if (arrayIndex >= 0) { + // Remove it from the order array, as well as deleting the node + // from the indeces hash. + this.order.splice(arrayIndex, 1); + delete this.indices[nodeId]; + + // Reset the maxium z-index based on the last item in the + // order array. + if (this.order.length > 0) { + var lastId = this.order[this.order.length - 1]; + this.maxZIndex = this.indices[lastId]; + } else { + this.maxZIndex = 0; + } + } + }, + + /** + * APIMethod: clear + */ + clear: function() { + this.order = []; + this.indices = {}; + this.maxZIndex = 0; + }, + + /** + * APIMethod: exists + * + * Parameters: + * node - {DOMElement} The node to test for existence. + * + * Returns: + * {Boolean} Whether or not the node exists in the indexer? + */ + exists: function(node) { + return (this.indices[node.id] != null); + }, + + /** + * APIMethod: getZIndex + * Get the z-index value for the current node from the node data itself. + * + * Parameters: + * node - {DOMElement} The node whose z-index to get. + * + * Returns: + * {Integer} The z-index value for the specified node (from the node + * data itself). + */ + getZIndex: function(node) { + return node._style.graphicZIndex; + }, + + /** + * Method: determineZIndex + * Determine the z-index for the current node if there isn't one, + * and set the maximum value if we've found a new maximum. + * + * Parameters: + * node - {DOMElement} + */ + determineZIndex: function(node) { + var zIndex = node._style.graphicZIndex; + + // Everything must have a zIndex. If none is specified, + // this means the user *must* (hint: assumption) want this + // node to succomb to drawing order. To enforce drawing order + // over all indexing methods, we'll create a new z-index that's + // greater than any currently in the indexer. + if (zIndex == null) { + zIndex = this.maxZIndex; + node._style.graphicZIndex = zIndex; + } else if (zIndex > this.maxZIndex) { + this.maxZIndex = zIndex; + } + }, + + /** + * APIMethod: getNextElement + * Get the next element in the order stack. + * + * Parameters: + * index - {Integer} The index of the current node in this.order. + * + * Returns: + * {DOMElement} the node following the index passed in, or + * null. + */ + getNextElement: function(index) { + var nextIndex = index + 1; + if (nextIndex < this.order.length) { + var nextElement = OpenLayers.Util.getElement(this.order[nextIndex]); + if (nextElement == undefined) { + nextElement = this.getNextElement(nextIndex); + } + return nextElement; + } else { + return null; + } + }, + + CLASS_NAME: "OpenLayers.ElementsIndexer" +}); + +/** + * Namespace: OpenLayers.ElementsIndexer.IndexingMethods + * These are the compare methods for figuring out where a new node should be + * placed within the indexer. These methods are very similar to general + * sorting methods in that they return -1, 0, and 1 to specify the + * direction in which new nodes fall in the ordering. + */ +OpenLayers.ElementsIndexer.IndexingMethods = { + + /** + * Method: Z_ORDER + * This compare method is used by other comparison methods. + * It can be used individually for ordering, but is not recommended, + * because it doesn't subscribe to drawing order. + * + * Parameters: + * indexer - {<OpenLayers.ElementsIndexer>} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER: function(indexer, newNode, nextNode) { + var newZIndex = indexer.getZIndex(newNode); + + var returnVal = 0; + if (nextNode) { + var nextZIndex = indexer.getZIndex(nextNode); + returnVal = newZIndex - nextZIndex; + } + + return returnVal; + }, + + /** + * APIMethod: Z_ORDER_DRAWING_ORDER + * This method orders nodes by their z-index, but does so in a way + * that, if there are other nodes with the same z-index, the newest + * drawn will be the front most within that z-index. This is the + * default indexing method. + * + * Parameters: + * indexer - {<OpenLayers.ElementsIndexer>} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER_DRAWING_ORDER: function(indexer, newNode, nextNode) { + var returnVal = OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER( + indexer, + newNode, + nextNode + ); + + // Make Z_ORDER subscribe to drawing order by pushing it above + // all of the other nodes with the same z-index. + if (nextNode && returnVal == 0) { + returnVal = 1; + } + + return returnVal; + }, + + /** + * APIMethod: Z_ORDER_Y_ORDER + * This one should really be called Z_ORDER_Y_ORDER_DRAWING_ORDER, as it + * best describes which ordering methods have precedence (though, the + * name would be too long). This method orders nodes by their z-index, + * but does so in a way that, if there are other nodes with the same + * z-index, the nodes with the lower y position will be "closer" than + * those with a higher y position. If two nodes have the exact same y + * position, however, then this method will revert to using drawing + * order to decide placement. + * + * Parameters: + * indexer - {<OpenLayers.ElementsIndexer>} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER_Y_ORDER: function(indexer, newNode, nextNode) { + var returnVal = OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER( + indexer, + newNode, + nextNode + ); + + if (nextNode && returnVal === 0) { + var result = nextNode._boundsBottom - newNode._boundsBottom; + returnVal = (result === 0) ? 1 : result; + } + + return returnVal; + } +}; + +/** + * Class: OpenLayers.Renderer.Elements + * This is another virtual class in that it should never be instantiated by + * itself as a Renderer. It exists because there is *tons* of shared + * functionality between different vector libraries which use nodes/elements + * as a base for rendering vectors. + * + * The highlevel bits of code that are implemented here are the adding and + * removing of geometries, which is essentially the same for any + * element-based renderer. The details of creating each node and drawing the + * paths are of course different, but the machinery is the same. + * + * Inherits: + * - <OpenLayers.Renderer> + */ +OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { + + /** + * Property: rendererRoot + * {DOMElement} + */ + rendererRoot: null, + + /** + * Property: root + * {DOMElement} + */ + root: null, + + /** + * Property: vectorRoot + * {DOMElement} + */ + vectorRoot: null, + + /** + * Property: textRoot + * {DOMElement} + */ + textRoot: null, + + /** + * Property: xmlns + * {String} + */ + xmlns: null, + + /** + * Property: xOffset + * {Number} Offset to apply to the renderer viewport translation in x + * direction. If the renderer extent's center is on the right of the + * dateline (i.e. exceeds the world bounds), we shift the viewport to the + * left by one world width. This avoids that features disappear from the + * map viewport. Because our dateline handling logic in other places + * ensures that extents crossing the dateline always have a center + * exceeding the world bounds on the left, we need this offset to make sure + * that the same is true for the renderer extent in pixel space as well. + */ + xOffset: 0, + + /** + * Property: rightOfDateLine + * {Boolean} Keeps track of the location of the map extent relative to the + * date line. The <setExtent> method compares this value (which is the one + * from the previous <setExtent> call) with the current position of the map + * extent relative to the date line and updates the xOffset when the extent + * has moved from one side of the date line to the other. + */ + + /** + * Property: Indexer + * {<OpenLayers.ElementIndexer>} An instance of OpenLayers.ElementsIndexer + * created upon initialization if the zIndexing or yOrdering options + * passed to this renderer's constructor are set to true. + */ + indexer: null, + + /** + * Constant: BACKGROUND_ID_SUFFIX + * {String} + */ + BACKGROUND_ID_SUFFIX: "_background", + + /** + * Constant: LABEL_ID_SUFFIX + * {String} + */ + LABEL_ID_SUFFIX: "_label", + + /** + * Constant: LABEL_OUTLINE_SUFFIX + * {String} + */ + LABEL_OUTLINE_SUFFIX: "_outline", + + /** + * Constructor: OpenLayers.Renderer.Elements + * + * Parameters: + * containerID - {String} + * options - {Object} options for this renderer. + * + * Supported options are: + * yOrdering - {Boolean} Whether to use y-ordering + * zIndexing - {Boolean} Whether to use z-indexing. Will be ignored + * if yOrdering is set to true. + */ + initialize: function(containerID, options) { + OpenLayers.Renderer.prototype.initialize.apply(this, arguments); + + this.rendererRoot = this.createRenderRoot(); + this.root = this.createRoot("_root"); + this.vectorRoot = this.createRoot("_vroot"); + this.textRoot = this.createRoot("_troot"); + + this.root.appendChild(this.vectorRoot); + this.root.appendChild(this.textRoot); + + this.rendererRoot.appendChild(this.root); + this.container.appendChild(this.rendererRoot); + + if(options && (options.zIndexing || options.yOrdering)) { + this.indexer = new OpenLayers.ElementsIndexer(options.yOrdering); + } + }, + + /** + * Method: destroy + */ + destroy: function() { + + this.clear(); + + this.rendererRoot = null; + this.root = null; + this.xmlns = null; + + OpenLayers.Renderer.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: clear + * Remove all the elements from the root + */ + clear: function() { + var child; + var root = this.vectorRoot; + if (root) { + while (child = root.firstChild) { + root.removeChild(child); + } + } + root = this.textRoot; + if (root) { + while (child = root.firstChild) { + root.removeChild(child); + } + } + if (this.indexer) { + this.indexer.clear(); + } + }, + + /** + * Method: setExtent + * Set the visible part of the layer. + * + * Parameters: + * extent - {<OpenLayers.Bounds>} + * resolutionChanged - {Boolean} + * + * Returns: + * {Boolean} true to notify the layer that the new extent does not exceed + * the coordinate range, and the features will not need to be redrawn. + * False otherwise. + */ + setExtent: function(extent, resolutionChanged) { + var coordSysUnchanged = OpenLayers.Renderer.prototype.setExtent.apply(this, arguments); + var resolution = this.getResolution(); + if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) { + var rightOfDateLine, + ratio = extent.getWidth() / this.map.getExtent().getWidth(), + extent = extent.scale(1 / ratio), + world = this.map.getMaxExtent(); + if (world.right > extent.left && world.right < extent.right) { + rightOfDateLine = true; + } else if (world.left > extent.left && world.left < extent.right) { + rightOfDateLine = false; + } + if (rightOfDateLine !== this.rightOfDateLine || resolutionChanged) { + coordSysUnchanged = false; + this.xOffset = rightOfDateLine === true ? + world.getWidth() / resolution : 0; + } + this.rightOfDateLine = rightOfDateLine; + } + return coordSysUnchanged; + }, + + /** + * Method: getNodeType + * This function is in charge of asking the specific renderer which type + * of node to create for the given geometry and style. All geometries + * in an Elements-based renderer consist of one node and some + * attributes. We have the nodeFactory() function which creates a node + * for us, but it takes a 'type' as input, and that is precisely what + * this function tells us. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * + * Returns: + * {String} The corresponding node type for the specified geometry + */ + getNodeType: function(geometry, style) { }, + + /** + * Method: drawGeometry + * Draw the geometry, creating new nodes, setting paths, setting style, + * setting featureId on the node. This method should only be called + * by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + * + * Returns: + * {Boolean} true if the geometry has been drawn completely; null if + * incomplete; false otherwise + */ + drawGeometry: function(geometry, style, featureId) { + var className = geometry.CLASS_NAME; + var rendered = true; + if ((className == "OpenLayers.Geometry.Collection") || + (className == "OpenLayers.Geometry.MultiPoint") || + (className == "OpenLayers.Geometry.MultiLineString") || + (className == "OpenLayers.Geometry.MultiPolygon")) { + for (var i = 0, len=geometry.components.length; i<len; i++) { + rendered = this.drawGeometry( + geometry.components[i], style, featureId) && rendered; + } + return rendered; + } + + rendered = false; + var removeBackground = false; + if (style.display != "none") { + if (style.backgroundGraphic) { + this.redrawBackgroundNode(geometry.id, geometry, style, + featureId); + } else { + removeBackground = true; + } + rendered = this.redrawNode(geometry.id, geometry, style, + featureId); + } + if (rendered == false) { + var node = document.getElementById(geometry.id); + if (node) { + if (node._style.backgroundGraphic) { + removeBackground = true; + } + node.parentNode.removeChild(node); + } + } + if (removeBackground) { + var node = document.getElementById( + geometry.id + this.BACKGROUND_ID_SUFFIX); + if (node) { + node.parentNode.removeChild(node); + } + } + return rendered; + }, + + /** + * Method: redrawNode + * + * Parameters: + * id - {String} + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + * + * Returns: + * {Boolean} true if the complete geometry could be drawn, null if parts of + * the geometry could not be drawn, false otherwise + */ + redrawNode: function(id, geometry, style, featureId) { + style = this.applyDefaultSymbolizer(style); + // Get the node if it's already on the map. + var node = this.nodeFactory(id, this.getNodeType(geometry, style)); + + // Set the data for the node, then draw it. + node._featureId = featureId; + node._boundsBottom = geometry.getBounds().bottom; + node._geometryClass = geometry.CLASS_NAME; + node._style = style; + + var drawResult = this.drawGeometryNode(node, geometry, style); + if(drawResult === false) { + return false; + } + + node = drawResult.node; + + // Insert the node into the indexer so it can show us where to + // place it. Note that this operation is O(log(n)). If there's a + // performance problem (when dragging, for instance) this is + // likely where it would be. + if (this.indexer) { + var insert = this.indexer.insert(node); + if (insert) { + this.vectorRoot.insertBefore(node, insert); + } else { + this.vectorRoot.appendChild(node); + } + } else { + // if there's no indexer, simply append the node to root, + // but only if the node is a new one + if (node.parentNode !== this.vectorRoot){ + this.vectorRoot.appendChild(node); + } + } + + this.postDraw(node); + + return drawResult.complete; + }, + + /** + * Method: redrawBackgroundNode + * Redraws the node using special 'background' style properties. Basically + * just calls redrawNode(), but instead of directly using the + * 'externalGraphic', 'graphicXOffset', 'graphicYOffset', and + * 'graphicZIndex' properties directly from the specified 'style' + * parameter, we create a new style object and set those properties + * from the corresponding 'background'-prefixed properties from + * specified 'style' parameter. + * + * Parameters: + * id - {String} + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + * + * Returns: + * {Boolean} true if the complete geometry could be drawn, null if parts of + * the geometry could not be drawn, false otherwise + */ + redrawBackgroundNode: function(id, geometry, style, featureId) { + var backgroundStyle = OpenLayers.Util.extend({}, style); + + // Set regular style attributes to apply to the background styles. + backgroundStyle.externalGraphic = backgroundStyle.backgroundGraphic; + backgroundStyle.graphicXOffset = backgroundStyle.backgroundXOffset; + backgroundStyle.graphicYOffset = backgroundStyle.backgroundYOffset; + backgroundStyle.graphicZIndex = backgroundStyle.backgroundGraphicZIndex; + backgroundStyle.graphicWidth = backgroundStyle.backgroundWidth || backgroundStyle.graphicWidth; + backgroundStyle.graphicHeight = backgroundStyle.backgroundHeight || backgroundStyle.graphicHeight; + + // Erase background styles. + backgroundStyle.backgroundGraphic = null; + backgroundStyle.backgroundXOffset = null; + backgroundStyle.backgroundYOffset = null; + backgroundStyle.backgroundGraphicZIndex = null; + + return this.redrawNode( + id + this.BACKGROUND_ID_SUFFIX, + geometry, + backgroundStyle, + null + ); + }, + + /** + * Method: drawGeometryNode + * Given a node, draw a geometry on the specified layer. + * node and geometry are required arguments, style is optional. + * This method is only called by the render itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * + * Returns: + * {Object} a hash with properties "node" (the drawn node) and "complete" + * (null if parts of the geometry could not be drawn, false if nothing + * could be drawn) + */ + drawGeometryNode: function(node, geometry, style) { + style = style || node._style; + + var options = { + 'isFilled': style.fill === undefined ? + true : + style.fill, + 'isStroked': style.stroke === undefined ? + !!style.strokeWidth : + style.stroke + }; + var drawn; + switch (geometry.CLASS_NAME) { + case "OpenLayers.Geometry.Point": + if(style.graphic === false) { + options.isFilled = false; + options.isStroked = false; + } + drawn = this.drawPoint(node, geometry); + break; + case "OpenLayers.Geometry.LineString": + options.isFilled = false; + drawn = this.drawLineString(node, geometry); + break; + case "OpenLayers.Geometry.LinearRing": + drawn = this.drawLinearRing(node, geometry); + break; + case "OpenLayers.Geometry.Polygon": + drawn = this.drawPolygon(node, geometry); + break; + case "OpenLayers.Geometry.Rectangle": + drawn = this.drawRectangle(node, geometry); + break; + default: + break; + } + + node._options = options; + + //set style + //TBD simplify this + if (drawn != false) { + return { + node: this.setStyle(node, style, options, geometry), + complete: drawn + }; + } else { + return false; + } + }, + + /** + * Method: postDraw + * Things that have do be done after the geometry node is appended + * to its parent node. To be overridden by subclasses. + * + * Parameters: + * node - {DOMElement} + */ + postDraw: function(node) {}, + + /** + * Method: drawPoint + * Virtual function for drawing Point Geometry. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or false if the renderer could not draw the point + */ + drawPoint: function(node, geometry) {}, + + /** + * Method: drawLineString + * Virtual function for drawing LineString Geometry. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or null if the renderer could not draw all components of + * the linestring, or false if nothing could be drawn + */ + drawLineString: function(node, geometry) {}, + + /** + * Method: drawLinearRing + * Virtual function for drawing LinearRing Geometry. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or null if the renderer could not draw all components + * of the linear ring, or false if nothing could be drawn + */ + drawLinearRing: function(node, geometry) {}, + + /** + * Method: drawPolygon + * Virtual function for drawing Polygon Geometry. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or null if the renderer could not draw all components + * of the polygon, or false if nothing could be drawn + */ + drawPolygon: function(node, geometry) {}, + + /** + * Method: drawRectangle + * Virtual function for drawing Rectangle Geometry. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or false if the renderer could not draw the rectangle + */ + drawRectangle: function(node, geometry) {}, + + /** + * Method: drawCircle + * Virtual function for drawing Circle Geometry. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or false if the renderer could not draw the circle + */ + drawCircle: function(node, geometry) {}, + + /** + * Method: removeText + * Removes a label + * + * Parameters: + * featureId - {String} + */ + removeText: function(featureId) { + var label = document.getElementById(featureId + this.LABEL_ID_SUFFIX); + if (label) { + this.textRoot.removeChild(label); + } + var outline = document.getElementById(featureId + this.LABEL_OUTLINE_SUFFIX); + if (outline) { + this.textRoot.removeChild(outline); + } + }, + + /** + * Method: getFeatureIdFromEvent + * + * Parameters: + * evt - {Object} An <OpenLayers.Event> object + * + * Returns: + * {String} A feature id or undefined. + */ + getFeatureIdFromEvent: function(evt) { + var target = evt.target; + var useElement = target && target.correspondingUseElement; + var node = useElement ? useElement : (target || evt.srcElement); + return node._featureId; + }, + + /** + * Method: eraseGeometry + * Erase a geometry from the renderer. In the case of a multi-geometry, + * we cycle through and recurse on ourselves. Otherwise, we look for a + * node with the geometry.id, destroy its geometry, and remove it from + * the DOM. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * featureId - {String} + */ + eraseGeometry: function(geometry, featureId) { + if ((geometry.CLASS_NAME == "OpenLayers.Geometry.MultiPoint") || + (geometry.CLASS_NAME == "OpenLayers.Geometry.MultiLineString") || + (geometry.CLASS_NAME == "OpenLayers.Geometry.MultiPolygon") || + (geometry.CLASS_NAME == "OpenLayers.Geometry.Collection")) { + for (var i=0, len=geometry.components.length; i<len; i++) { + this.eraseGeometry(geometry.components[i], featureId); + } + } else { + var element = OpenLayers.Util.getElement(geometry.id); + if (element && element.parentNode) { + if (element.geometry) { + element.geometry.destroy(); + element.geometry = null; + } + element.parentNode.removeChild(element); + + if (this.indexer) { + this.indexer.remove(element); + } + + if (element._style.backgroundGraphic) { + var backgroundId = geometry.id + this.BACKGROUND_ID_SUFFIX; + var bElem = OpenLayers.Util.getElement(backgroundId); + if (bElem && bElem.parentNode) { + // No need to destroy the geometry since the element and the background + // node share the same geometry. + bElem.parentNode.removeChild(bElem); + } + } + } + } + }, + + /** + * Method: nodeFactory + * Create new node of the specified type, with the (optional) specified id. + * + * If node already exists with same ID and a different type, we remove it + * and then call ourselves again to recreate it. + * + * Parameters: + * id - {String} + * type - {String} type Kind of node to draw. + * + * Returns: + * {DOMElement} A new node of the given type and id. + */ + nodeFactory: function(id, type) { + var node = OpenLayers.Util.getElement(id); + if (node) { + if (!this.nodeTypeCompare(node, type)) { + node.parentNode.removeChild(node); + node = this.nodeFactory(id, type); + } + } else { + node = this.createNode(type, id); + } + return node; + }, + + /** + * Method: nodeTypeCompare + * + * Parameters: + * node - {DOMElement} + * type - {String} Kind of node + * + * Returns: + * {Boolean} Whether or not the specified node is of the specified type + * This function must be overridden by subclasses. + */ + nodeTypeCompare: function(node, type) {}, + + /** + * Method: createNode + * + * Parameters: + * type - {String} Kind of node to draw. + * id - {String} Id for node. + * + * Returns: + * {DOMElement} A new node of the given type and id. + * This function must be overridden by subclasses. + */ + createNode: function(type, id) {}, + + /** + * Method: moveRoot + * moves this renderer's root to a different renderer. + * + * Parameters: + * renderer - {<OpenLayers.Renderer>} target renderer for the moved root + */ + moveRoot: function(renderer) { + var root = this.root; + if(renderer.root.parentNode == this.rendererRoot) { + root = renderer.root; + } + root.parentNode.removeChild(root); + renderer.rendererRoot.appendChild(root); + }, + + /** + * Method: getRenderLayerId + * Gets the layer that this renderer's output appears on. If moveRoot was + * used, this will be different from the id of the layer containing the + * features rendered by this renderer. + * + * Returns: + * {String} the id of the output layer. + */ + getRenderLayerId: function() { + return this.root.parentNode.parentNode.id; + }, + + /** + * Method: isComplexSymbol + * Determines if a symbol cannot be rendered using drawCircle + * + * Parameters: + * graphicName - {String} + * + * Returns + * {Boolean} true if the symbol is complex, false if not + */ + isComplexSymbol: function(graphicName) { + return (graphicName != "circle") && !!graphicName; + }, + + CLASS_NAME: "OpenLayers.Renderer.Elements" +}); + +/* ====================================================================== + OpenLayers/Control/Panel.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Events/buttonclick.js + */ + +/** + * Class: OpenLayers.Control.Panel + * The Panel control is a container for other controls. With it toolbars + * may be composed. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.Panel = OpenLayers.Class(OpenLayers.Control, { + /** + * Property: controls + * {Array(<OpenLayers.Control>)} + */ + controls: null, + + /** + * APIProperty: autoActivate + * {Boolean} Activate the control when it is added to a map. Default is + * true. + */ + autoActivate: true, + + /** + * APIProperty: defaultControl + * {<OpenLayers.Control>} The control which is activated when the control is + * activated (turned on), which also happens at instantiation. + * If <saveState> is true, <defaultControl> will be nullified after the + * first activation of the panel. + */ + defaultControl: null, + + /** + * APIProperty: saveState + * {Boolean} If set to true, the active state of this panel's controls will + * be stored on panel deactivation, and restored on reactivation. Default + * is false. + */ + saveState: false, + + /** + * APIProperty: allowDepress + * {Boolean} If is true the <OpenLayers.Control.TYPE_TOOL> controls can + * be deactivated by clicking the icon that represents them. Default + * is false. + */ + allowDepress: false, + + /** + * Property: activeState + * {Object} stores the active state of this panel's controls. + */ + activeState: null, + + /** + * Constructor: OpenLayers.Control.Panel + * Create a new control panel. + * + * Each control in the panel is represented by an icon. When clicking + * on an icon, the <activateControl> method is called. + * + * Specific properties for controls on a panel: + * type - {Number} One of <OpenLayers.Control.TYPE_TOOL>, + * <OpenLayers.Control.TYPE_TOGGLE>, <OpenLayers.Control.TYPE_BUTTON>. + * If not provided, <OpenLayers.Control.TYPE_TOOL> is assumed. + * title - {string} Text displayed when mouse is over the icon that + * represents the control. + * + * The <OpenLayers.Control.type> of a control determines the behavior when + * clicking its icon: + * <OpenLayers.Control.TYPE_TOOL> - The control is activated and other + * controls of this type in the same panel are deactivated. This is + * the default type. + * <OpenLayers.Control.TYPE_TOGGLE> - The active state of the control is + * toggled. + * <OpenLayers.Control.TYPE_BUTTON> - The + * <OpenLayers.Control.Button.trigger> method of the control is called, + * but its active state is not changed. + * + * If a control is <OpenLayers.Control.active>, it will be drawn with the + * olControl[Name]ItemActive class, otherwise with the + * olControl[Name]ItemInactive class. + * + * Parameters: + * options - {Object} An optional object whose properties will be used + * to extend the control. + */ + initialize: function(options) { + OpenLayers.Control.prototype.initialize.apply(this, [options]); + this.controls = []; + this.activeState = {}; + }, + + /** + * APIMethod: destroy + */ + destroy: function() { + if (this.map) { + this.map.events.unregister("buttonclick", this, this.onButtonClick); + } + OpenLayers.Control.prototype.destroy.apply(this, arguments); + for (var ctl, i = this.controls.length - 1; i >= 0; i--) { + ctl = this.controls[i]; + if (ctl.events) { + ctl.events.un({ + activate: this.iconOn, + deactivate: this.iconOff + }); + } + ctl.panel_div = null; + } + this.activeState = null; + }, + + /** + * APIMethod: activate + */ + activate: function() { + if (OpenLayers.Control.prototype.activate.apply(this, arguments)) { + var control; + for (var i=0, len=this.controls.length; i<len; i++) { + control = this.controls[i]; + if (control === this.defaultControl || + (this.saveState && this.activeState[control.id])) { + control.activate(); + } + } + if (this.saveState === true) { + this.defaultControl = null; + } + this.redraw(); + return true; + } else { + return false; + } + }, + + /** + * APIMethod: deactivate + */ + deactivate: function() { + if (OpenLayers.Control.prototype.deactivate.apply(this, arguments)) { + var control; + for (var i=0, len=this.controls.length; i<len; i++) { + control = this.controls[i]; + this.activeState[control.id] = control.deactivate(); + } + this.redraw(); + return true; + } else { + return false; + } + }, + + /** + * Method: draw + * + * Returns: + * {DOMElement} + */ + draw: function() { + OpenLayers.Control.prototype.draw.apply(this, arguments); + if (this.outsideViewport) { + this.events.attachToElement(this.div); + this.events.register("buttonclick", this, this.onButtonClick); + } else { + this.map.events.register("buttonclick", this, this.onButtonClick); + } + this.addControlsToMap(this.controls); + return this.div; + }, + + /** + * Method: redraw + */ + redraw: function() { + for (var l=this.div.childNodes.length, i=l-1; i>=0; i--) { + this.div.removeChild(this.div.childNodes[i]); + } + this.div.innerHTML = ""; + if (this.active) { + for (var i=0, len=this.controls.length; i<len; i++) { + this.div.appendChild(this.controls[i].panel_div); + } + } + }, + + /** + * APIMethod: activateControl + * This method is called when the user click on the icon representing a + * control in the panel. + * + * Parameters: + * control - {<OpenLayers.Control>} + */ + activateControl: function (control) { + if (!this.active) { return false; } + if (control.type == OpenLayers.Control.TYPE_BUTTON) { + control.trigger(); + return; + } + if (control.type == OpenLayers.Control.TYPE_TOGGLE) { + if (control.active) { + control.deactivate(); + } else { + control.activate(); + } + return; + } + if (this.allowDepress && control.active) { + control.deactivate(); + } else { + var c; + for (var i=0, len=this.controls.length; i<len; i++) { + c = this.controls[i]; + if (c != control && + (c.type === OpenLayers.Control.TYPE_TOOL || c.type == null)) { + c.deactivate(); + } + } + control.activate(); + } + }, + + /** + * APIMethod: addControls + * To build a toolbar, you add a set of controls to it. addControls + * lets you add a single control or a list of controls to the + * Control Panel. + * + * Parameters: + * controls - {<OpenLayers.Control>} Controls to add in the panel. + */ + addControls: function(controls) { + if (!(OpenLayers.Util.isArray(controls))) { + controls = [controls]; + } + this.controls = this.controls.concat(controls); + + for (var i=0, len=controls.length; i<len; i++) { + var control = controls[i], + element = this.createControlMarkup(control); + OpenLayers.Element.addClass(element, + control.displayClass + "ItemInactive"); + OpenLayers.Element.addClass(element, "olButton"); + if (control.title != "" && !element.title) { + element.title = control.title; + } + control.panel_div = element; + } + + if (this.map) { // map.addControl() has already been called on the panel + this.addControlsToMap(controls); + this.redraw(); + } + }, + + /** + * APIMethod: createControlMarkup + * This function just creates a div for the control. If specific HTML + * markup is needed this function can be overridden in specific classes, + * or at panel instantiation time: + * + * Example: + * (code) + * var panel = new OpenLayers.Control.Panel({ + * defaultControl: control, + * // ovverride createControlMarkup to create actual buttons + * // including texts wrapped into span elements. + * createControlMarkup: function(control) { + * var button = document.createElement('button'), + * span = document.createElement('span'); + * if (control.text) { + * span.innerHTML = control.text; + * } + * return button; + * } + * }); + * (end) + * + * Parameters: + * control - {<OpenLayers.Control>} The control to create the HTML + * markup for. + * + * Returns: + * {DOMElement} The markup. + */ + createControlMarkup: function(control) { + return document.createElement("div"); + }, + + /** + * Method: addControlsToMap + * Only for internal use in draw() and addControls() methods. + * + * Parameters: + * controls - {Array(<OpenLayers.Control>)} Controls to add into map. + */ + addControlsToMap: function (controls) { + var control; + for (var i=0, len=controls.length; i<len; i++) { + control = controls[i]; + if (control.autoActivate === true) { + control.autoActivate = false; + this.map.addControl(control); + control.autoActivate = true; + } else { + this.map.addControl(control); + control.deactivate(); + } + control.events.on({ + activate: this.iconOn, + deactivate: this.iconOff + }); + } + }, + + /** + * Method: iconOn + * Internal use, for use only with "controls[i].events.on/un". + */ + iconOn: function() { + var d = this.panel_div; // "this" refers to a control on panel! + var re = new RegExp("\\b(" + this.displayClass + "Item)Inactive\\b"); + d.className = d.className.replace(re, "$1Active"); + }, + + /** + * Method: iconOff + * Internal use, for use only with "controls[i].events.on/un". + */ + iconOff: function() { + var d = this.panel_div; // "this" refers to a control on panel! + var re = new RegExp("\\b(" + this.displayClass + "Item)Active\\b"); + d.className = d.className.replace(re, "$1Inactive"); + }, + + /** + * Method: onButtonClick + * + * Parameters: + * evt - {Event} + */ + onButtonClick: function (evt) { + var controls = this.controls, + button = evt.buttonElement; + for (var i=controls.length-1; i>=0; --i) { + if (controls[i].panel_div === button) { + this.activateControl(controls[i]); + break; + } + } + }, + + /** + * 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(control[property]) evaluates to true, the control will be + * included in the array returned. If no controls 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) { + var test = (typeof match.test == "function"); + var found = OpenLayers.Array.filter(this.controls, function(item) { + return item[property] == match || (test && match.test(item[property])); + }); + return found; + }, + + /** + * APIMethod: getControlsByName + * Get a list of contorls with names matching the given name. + * + * Parameters: + * match - {String | Object} A control 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(control.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 name. + * An empty array is returned if no matches are found. + */ + getControlsByName: function(match) { + return this.getControlsBy("name", match); + }, + + /** + * APIMethod: getControlsByClass + * Get a list of controls of a given type (CLASS_NAME). + * + * Parameters: + * match - {String | Object} A control class name. The type 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 type. + * An empty array is returned if no matches are found. + */ + getControlsByClass: function(match) { + return this.getControlsBy("CLASS_NAME", match); + }, + + CLASS_NAME: "OpenLayers.Control.Panel" +}); + +/* ====================================================================== + OpenLayers/Strategy.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Strategy + * Abstract vector layer strategy class. Not to be instantiated directly. Use + * one of the strategy subclasses instead. + */ +OpenLayers.Strategy = OpenLayers.Class({ + + /** + * Property: layer + * {<OpenLayers.Layer.Vector>} The layer this strategy belongs to. + */ + layer: null, + + /** + * Property: options + * {Object} Any options sent to the constructor. + */ + options: null, + + /** + * Property: active + * {Boolean} The control is active. + */ + active: null, + + /** + * Property: autoActivate + * {Boolean} The creator of the strategy can set autoActivate to false + * to fully control when the protocol is activated and deactivated. + * Defaults to true. + */ + autoActivate: true, + + /** + * Property: autoDestroy + * {Boolean} The creator of the strategy can set autoDestroy to false + * to fully control when the strategy is destroyed. Defaults to + * true. + */ + autoDestroy: true, + + /** + * Constructor: OpenLayers.Strategy + * Abstract class for vector strategies. Create instances of a subclass. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + this.options = options; + // set the active property here, so that user cannot override it + this.active = false; + }, + + /** + * APIMethod: destroy + * Clean up the strategy. + */ + destroy: function() { + this.deactivate(); + this.layer = null; + this.options = null; + }, + + /** + * Method: setLayer + * Called to set the <layer> property. + * + * Parameters: + * layer - {<OpenLayers.Layer.Vector>} + */ + setLayer: function(layer) { + this.layer = layer; + }, + + /** + * Method: activate + * Activate the strategy. Register any listeners, do appropriate setup. + * + * Returns: + * {Boolean} True if the strategy was successfully activated or false if + * the strategy was already active. + */ + activate: function() { + if (!this.active) { + this.active = true; + return true; + } + return false; + }, + + /** + * Method: deactivate + * Deactivate the strategy. Unregister any listeners, do appropriate + * tear-down. + * + * Returns: + * {Boolean} True if the strategy was successfully deactivated or false if + * the strategy was already inactive. + */ + deactivate: function() { + if (this.active) { + this.active = false; + return true; + } + return false; + }, + + CLASS_NAME: "OpenLayers.Strategy" +}); +/* ====================================================================== + OpenLayers/Strategy/Fixed.js + ====================================================================== */ + +/* 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/Strategy.js + */ + +/** + * Class: OpenLayers.Strategy.Fixed + * A simple strategy that requests features once and never requests new data. + * + * Inherits from: + * - <OpenLayers.Strategy> + */ +OpenLayers.Strategy.Fixed = OpenLayers.Class(OpenLayers.Strategy, { + + /** + * APIProperty: preload + * {Boolean} Load data before layer made visible. Enabling this may result + * in considerable overhead if your application loads many data layers + * that are not visible by default. Default is false. + */ + preload: false, + + /** + * Constructor: OpenLayers.Strategy.Fixed + * Create a new Fixed strategy. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + */ + + /** + * Method: activate + * Activate the strategy: load data or add listener to load when visible + * + * Returns: + * {Boolean} True if the strategy was successfully activated or false if + * the strategy was already active. + */ + activate: function() { + var activated = OpenLayers.Strategy.prototype.activate.apply(this, arguments); + if(activated) { + this.layer.events.on({ + "refresh": this.load, + scope: this + }); + if(this.layer.visibility == true || this.preload) { + this.load(); + } else { + this.layer.events.on({ + "visibilitychanged": this.load, + scope: this + }); + } + } + return activated; + }, + + /** + * Method: deactivate + * Deactivate the strategy. Undo what is done in <activate>. + * + * Returns: + * {Boolean} The strategy was successfully deactivated. + */ + deactivate: function() { + var deactivated = OpenLayers.Strategy.prototype.deactivate.call(this); + if(deactivated) { + this.layer.events.un({ + "refresh": this.load, + "visibilitychanged": this.load, + scope: this + }); + } + return deactivated; + }, + + /** + * Method: load + * Tells protocol to load data and unhooks the visibilitychanged event + * + * Parameters: + * options - {Object} options to pass to protocol read. + */ + load: function(options) { + var layer = this.layer; + layer.events.triggerEvent("loadstart", {filter: layer.filter}); + layer.protocol.read(OpenLayers.Util.applyDefaults({ + callback: this.merge, + filter: layer.filter, + scope: this + }, options)); + layer.events.un({ + "visibilitychanged": this.load, + scope: this + }); + }, + + /** + * Method: merge + * Add all features to the layer. + * If the layer projection differs from the map projection, features + * will be transformed from the layer projection to the map projection. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} The response object passed + * by the protocol. + */ + merge: function(resp) { + var layer = this.layer; + layer.destroyFeatures(); + var features = resp.features; + if (features && features.length > 0) { + var remote = layer.projection; + var local = layer.map.getProjectionObject(); + if(!local.equals(remote)) { + var geom; + for(var i=0, len=features.length; i<len; ++i) { + geom = features[i].geometry; + if(geom) { + geom.transform(remote, local); + } + } + } + layer.addFeatures(features); + } + layer.events.triggerEvent("loadend", {response: resp}); + }, + + CLASS_NAME: "OpenLayers.Strategy.Fixed" +}); +/* ====================================================================== + OpenLayers/Control/Zoom.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Events/buttonclick.js + */ + +/** + * Class: OpenLayers.Control.Zoom + * The Zoom control is a pair of +/- links for zooming in and out. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.Zoom = OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: zoomInText + * {String} + * Text for zoom-in link. Default is "+". + */ + zoomInText: "+", + + /** + * APIProperty: zoomInId + * {String} + * Instead of having the control create a zoom in link, you can provide + * the identifier for an anchor element already added to the document. + * By default, an element with id "olZoomInLink" will be searched for + * and used if it exists. + */ + zoomInId: "olZoomInLink", + + /** + * APIProperty: zoomOutText + * {String} + * Text for zoom-out link. Default is "\u2212". + */ + zoomOutText: "\u2212", + + /** + * APIProperty: zoomOutId + * {String} + * Instead of having the control create a zoom out link, you can provide + * the identifier for an anchor element already added to the document. + * By default, an element with id "olZoomOutLink" will be searched for + * and used if it exists. + */ + zoomOutId: "olZoomOutLink", + + /** + * Method: draw + * + * Returns: + * {DOMElement} A reference to the DOMElement containing the zoom links. + */ + draw: function() { + var div = OpenLayers.Control.prototype.draw.apply(this), + links = this.getOrCreateLinks(div), + zoomIn = links.zoomIn, + zoomOut = links.zoomOut, + eventsInstance = this.map.events; + + if (zoomOut.parentNode !== div) { + eventsInstance = this.events; + eventsInstance.attachToElement(zoomOut.parentNode); + } + eventsInstance.register("buttonclick", this, this.onZoomClick); + + this.zoomInLink = zoomIn; + this.zoomOutLink = zoomOut; + return div; + }, + + /** + * Method: getOrCreateLinks + * + * Parameters: + * el - {DOMElement} + * + * Return: + * {Object} Object with zoomIn and zoomOut properties referencing links. + */ + getOrCreateLinks: function(el) { + var zoomIn = document.getElementById(this.zoomInId), + zoomOut = document.getElementById(this.zoomOutId); + if (!zoomIn) { + zoomIn = document.createElement("a"); + zoomIn.href = "#zoomIn"; + zoomIn.appendChild(document.createTextNode(this.zoomInText)); + zoomIn.className = "olControlZoomIn"; + el.appendChild(zoomIn); + } + OpenLayers.Element.addClass(zoomIn, "olButton"); + if (!zoomOut) { + zoomOut = document.createElement("a"); + zoomOut.href = "#zoomOut"; + zoomOut.appendChild(document.createTextNode(this.zoomOutText)); + zoomOut.className = "olControlZoomOut"; + el.appendChild(zoomOut); + } + OpenLayers.Element.addClass(zoomOut, "olButton"); + return { + zoomIn: zoomIn, zoomOut: zoomOut + }; + }, + + /** + * Method: onZoomClick + * Called when zoomin/out link is clicked. + */ + onZoomClick: function(evt) { + var button = evt.buttonElement; + if (button === this.zoomInLink) { + this.map.zoomIn(); + } else if (button === this.zoomOutLink) { + this.map.zoomOut(); + } + }, + + /** + * Method: destroy + * Clean up. + */ + destroy: function() { + if (this.map) { + this.map.events.unregister("buttonclick", this, this.onZoomClick); + } + delete this.zoomInLink; + delete this.zoomOutLink; + OpenLayers.Control.prototype.destroy.apply(this); + }, + + CLASS_NAME: "OpenLayers.Control.Zoom" +}); +/* ====================================================================== + OpenLayers/Protocol.js + ====================================================================== */ + +/* 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 + */ + +/** + * Class: OpenLayers.Protocol + * Abstract vector layer protocol class. Not to be instantiated directly. Use + * one of the protocol subclasses instead. + */ +OpenLayers.Protocol = OpenLayers.Class({ + + /** + * Property: format + * {<OpenLayers.Format>} The format used by this protocol. + */ + format: null, + + /** + * Property: options + * {Object} Any options sent to the constructor. + */ + options: null, + + /** + * Property: autoDestroy + * {Boolean} The creator of the protocol can set autoDestroy to false + * to fully control when the protocol is destroyed. Defaults to + * true. + */ + autoDestroy: true, + + /** + * Property: defaultFilter + * {<OpenLayers.Filter>} Optional default filter to read requests + */ + defaultFilter: null, + + /** + * Constructor: OpenLayers.Protocol + * Abstract class for vector protocols. Create instances of a subclass. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + */ + initialize: function(options) { + options = options || {}; + OpenLayers.Util.extend(this, options); + this.options = options; + }, + + /** + * Method: mergeWithDefaultFilter + * Merge filter passed to the read method with the default one + * + * Parameters: + * filter - {<OpenLayers.Filter>} + */ + mergeWithDefaultFilter: function(filter) { + var merged; + if (filter && this.defaultFilter) { + merged = new OpenLayers.Filter.Logical({ + type: OpenLayers.Filter.Logical.AND, + filters: [this.defaultFilter, filter] + }); + } else { + merged = filter || this.defaultFilter || undefined; + } + return merged; + }, + + /** + * APIMethod: destroy + * Clean up the protocol. + */ + destroy: function() { + this.options = null; + this.format = null; + }, + + /** + * APIMethod: read + * Construct a request for reading new features. + * + * Parameters: + * options - {Object} Optional object for configuring the request. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, the same object will be passed to the callback function passed + * if one exists in the options object. + */ + read: function(options) { + options = options || {}; + options.filter = this.mergeWithDefaultFilter(options.filter); + }, + + + /** + * APIMethod: create + * Construct a request for writing newly created features. + * + * Parameters: + * features - {Array({<OpenLayers.Feature.Vector>})} or + * {<OpenLayers.Feature.Vector>} + * options - {Object} Optional object for configuring the request. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, the same object will be passed to the callback function passed + * if one exists in the options object. + */ + create: function() { + }, + + /** + * APIMethod: update + * Construct a request updating modified features. + * + * Parameters: + * features - {Array({<OpenLayers.Feature.Vector>})} or + * {<OpenLayers.Feature.Vector>} + * options - {Object} Optional object for configuring the request. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, the same object will be passed to the callback function passed + * if one exists in the options object. + */ + update: function() { + }, + + /** + * APIMethod: delete + * Construct a request deleting a removed feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * options - {Object} Optional object for configuring the request. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, the same object will be passed to the callback function passed + * if one exists in the options object. + */ + "delete": function() { + }, + + /** + * APIMethod: commit + * Go over the features and for each take action + * based on the feature state. Possible actions are create, + * update and delete. + * + * Parameters: + * features - {Array({<OpenLayers.Feature.Vector>})} + * options - {Object} Object whose possible keys are "create", "update", + * "delete", "callback" and "scope", the values referenced by the + * first three are objects as passed to the "create", "update", and + * "delete" methods, the value referenced by the "callback" key is + * a function which is called when the commit operation is complete + * using the scope referenced by the "scope" key. + * + * Returns: + * {Array({<OpenLayers.Protocol.Response>})} An array of + * <OpenLayers.Protocol.Response> objects. + */ + commit: function() { + }, + + /** + * Method: abort + * Abort an ongoing request. + * + * Parameters: + * response - {<OpenLayers.Protocol.Response>} + */ + abort: function(response) { + }, + + /** + * Method: createCallback + * Returns a function that applies the given public method with resp and + * options arguments. + * + * Parameters: + * method - {Function} The method to be applied by the callback. + * response - {<OpenLayers.Protocol.Response>} The protocol response object. + * options - {Object} Options sent to the protocol method + */ + createCallback: function(method, response, options) { + return OpenLayers.Function.bind(function() { + method.apply(this, [response, options]); + }, this); + }, + + CLASS_NAME: "OpenLayers.Protocol" +}); + +/** + * Class: OpenLayers.Protocol.Response + * Protocols return Response objects to their users. + */ +OpenLayers.Protocol.Response = OpenLayers.Class({ + /** + * Property: code + * {Number} - OpenLayers.Protocol.Response.SUCCESS or + * OpenLayers.Protocol.Response.FAILURE + */ + code: null, + + /** + * Property: requestType + * {String} The type of request this response corresponds to. Either + * "create", "read", "update" or "delete". + */ + requestType: null, + + /** + * Property: last + * {Boolean} - true if this is the last response expected in a commit, + * false otherwise, defaults to true. + */ + last: true, + + /** + * Property: features + * {Array({<OpenLayers.Feature.Vector>})} or {<OpenLayers.Feature.Vector>} + * The features returned in the response by the server. Depending on the + * protocol's read payload, either features or data will be populated. + */ + features: null, + + /** + * Property: data + * {Object} + * The data returned in the response by the server. Depending on the + * protocol's read payload, either features or data will be populated. + */ + data: null, + + /** + * Property: reqFeatures + * {Array({<OpenLayers.Feature.Vector>})} or {<OpenLayers.Feature.Vector>} + * The features provided by the user and placed in the request by the + * protocol. + */ + reqFeatures: null, + + /** + * Property: priv + */ + priv: null, + + /** + * Property: error + * {Object} The error object in case a service exception was encountered. + */ + error: null, + + /** + * Constructor: OpenLayers.Protocol.Response + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + }, + + /** + * Method: success + * + * Returns: + * {Boolean} - true on success, false otherwise + */ + success: function() { + return this.code > 0; + }, + + CLASS_NAME: "OpenLayers.Protocol.Response" +}); + +OpenLayers.Protocol.Response.SUCCESS = 1; +OpenLayers.Protocol.Response.FAILURE = 0; +/* ====================================================================== + OpenLayers/Protocol/WFS.js + ====================================================================== */ + +/* 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/Protocol.js + */ + +/** + * Class: OpenLayers.Protocol.WFS + * Used to create a versioned WFS protocol. Default version is 1.0.0. + * + * Returns: + * {<OpenLayers.Protocol>} A WFS protocol of the given version. + * + * Example: + * (code) + * var protocol = new OpenLayers.Protocol.WFS({ + * version: "1.1.0", + * url: "http://demo.opengeo.org/geoserver/wfs", + * featureType: "tasmania_roads", + * featureNS: "http://www.openplans.org/topp", + * geometryName: "the_geom" + * }); + * (end) + * + * See the protocols for specific WFS versions for more detail. + */ +OpenLayers.Protocol.WFS = function(options) { + options = OpenLayers.Util.applyDefaults( + options, OpenLayers.Protocol.WFS.DEFAULTS + ); + var cls = OpenLayers.Protocol.WFS["v"+options.version.replace(/\./g, "_")]; + if(!cls) { + throw "Unsupported WFS version: " + options.version; + } + return new cls(options); +}; + +/** + * Function: fromWMSLayer + * Convenience function to create a WFS protocol from a WMS layer. This makes + * the assumption that a WFS requests can be issued at the same URL as + * WMS requests and that a WFS featureType exists with the same name as the + * WMS layer. + * + * This function is designed to auto-configure <url>, <featureType>, + * <featurePrefix> and <srsName> for WFS <version> 1.1.0. Note that + * srsName matching with the WMS layer will not work with WFS 1.0.0. + * + * Parameters: + * layer - {<OpenLayers.Layer.WMS>} WMS layer that has a matching WFS + * FeatureType at the same server url with the same typename. + * options - {Object} Default properties to be set on the protocol. + * + * Returns: + * {<OpenLayers.Protocol.WFS>} + */ +OpenLayers.Protocol.WFS.fromWMSLayer = function(layer, options) { + var typeName, featurePrefix; + var param = layer.params["LAYERS"]; + var parts = (OpenLayers.Util.isArray(param) ? param[0] : param).split(":"); + if(parts.length > 1) { + featurePrefix = parts[0]; + } + typeName = parts.pop(); + var protocolOptions = { + url: layer.url, + featureType: typeName, + featurePrefix: featurePrefix, + srsName: layer.projection && layer.projection.getCode() || + layer.map && layer.map.getProjectionObject().getCode(), + version: "1.1.0" + }; + return new OpenLayers.Protocol.WFS(OpenLayers.Util.applyDefaults( + options, protocolOptions + )); +}; + +/** + * Constant: OpenLayers.Protocol.WFS.DEFAULTS + */ +OpenLayers.Protocol.WFS.DEFAULTS = { + "version": "1.0.0" +}; +/* ====================================================================== + OpenLayers/Request.js + ====================================================================== */ + +/* 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/Events.js + * @requires OpenLayers/Request/XMLHttpRequest.js + */ + +/** + * TODO: deprecate me + * Use OpenLayers.Request.proxy instead. + */ +OpenLayers.ProxyHost = ""; + +/** + * Namespace: OpenLayers.Request + * The OpenLayers.Request namespace contains convenience methods for working + * with XMLHttpRequests. These methods work with a cross-browser + * W3C compliant <OpenLayers.Request.XMLHttpRequest> class. + */ +if (!OpenLayers.Request) { + /** + * This allows for OpenLayers/Request/XMLHttpRequest.js to be included + * before or after this script. + */ + OpenLayers.Request = {}; +} +OpenLayers.Util.extend(OpenLayers.Request, { + + /** + * Constant: DEFAULT_CONFIG + * {Object} Default configuration for all requests. + */ + DEFAULT_CONFIG: { + method: "GET", + url: window.location.href, + async: true, + user: undefined, + password: undefined, + params: null, + proxy: OpenLayers.ProxyHost, + headers: {}, + data: null, + callback: function() {}, + success: null, + failure: null, + scope: null + }, + + /** + * Constant: URL_SPLIT_REGEX + */ + URL_SPLIT_REGEX: /([^:]*:)\/\/([^:]*:?[^@]*@)?([^:\/\?]*):?([^\/\?]*)/, + + /** + * APIProperty: events + * {<OpenLayers.Events>} An events object that handles all + * events on the {<OpenLayers.Request>} object. + * + * All event listeners will receive an event object with three properties: + * request - {<OpenLayers.Request.XMLHttpRequest>} The request object. + * config - {Object} The config object sent to the specific request method. + * requestUrl - {String} The request url. + * + * Supported event types: + * complete - Triggered when we have a response from the request, if a + * listener returns false, no further response processing will take + * place. + * success - Triggered when the HTTP response has a success code (200-299). + * failure - Triggered when the HTTP response does not have a success code. + */ + events: new OpenLayers.Events(this), + + /** + * Method: makeSameOrigin + * Using the specified proxy, returns a same origin url of the provided url. + * + * Parameters: + * url - {String} An arbitrary url + * proxy {String|Function} The proxy to use to make the provided url a + * same origin url. + * + * Returns + * {String} the same origin url. If no proxy is provided, the returned url + * will be the same as the provided url. + */ + makeSameOrigin: function(url, proxy) { + var sameOrigin = url.indexOf("http") !== 0; + var urlParts = !sameOrigin && url.match(this.URL_SPLIT_REGEX); + if (urlParts) { + var location = window.location; + sameOrigin = + urlParts[1] == location.protocol && + urlParts[3] == location.hostname; + var uPort = urlParts[4], lPort = location.port; + if (uPort != 80 && uPort != "" || lPort != "80" && lPort != "") { + sameOrigin = sameOrigin && uPort == lPort; + } + } + if (!sameOrigin) { + if (proxy) { + if (typeof proxy == "function") { + url = proxy(url); + } else { + url = proxy + encodeURIComponent(url); + } + } + } + return url; + }, + + /** + * APIMethod: issue + * Create a new XMLHttpRequest object, open it, set any headers, bind + * a callback to done state, and send any data. It is recommended that + * you use one <GET>, <POST>, <PUT>, <DELETE>, <OPTIONS>, or <HEAD>. + * This method is only documented to provide detail on the configuration + * options available to all request methods. + * + * Parameters: + * config - {Object} Object containing properties for configuring the + * request. Allowed configuration properties are described below. + * This object is modified and should not be reused. + * + * Allowed config properties: + * method - {String} One of GET, POST, PUT, DELETE, HEAD, or + * OPTIONS. Default is GET. + * url - {String} URL for the request. + * async - {Boolean} Open an asynchronous request. Default is true. + * user - {String} User for relevant authentication scheme. Set + * to null to clear current user. + * password - {String} Password for relevant authentication scheme. + * Set to null to clear current password. + * proxy - {String} Optional proxy. Defaults to + * <OpenLayers.ProxyHost>. + * params - {Object} Any key:value pairs to be appended to the + * url as a query string. Assumes url doesn't already include a query + * string or hash. Typically, this is only appropriate for <GET> + * requests where the query string will be appended to the url. + * Parameter values that are arrays will be + * concatenated with a comma (note that this goes against form-encoding) + * as is done with <OpenLayers.Util.getParameterString>. + * headers - {Object} Object with header:value pairs to be set on + * the request. + * data - {String | Document} Optional data to send with the request. + * Typically, this is only used with <POST> and <PUT> requests. + * Make sure to provide the appropriate "Content-Type" header for your + * data. For <POST> and <PUT> requests, the content type defaults to + * "application-xml". If your data is a different content type, or + * if you are using a different HTTP method, set the "Content-Type" + * header to match your data type. + * callback - {Function} Function to call when request is done. + * To determine if the request failed, check request.status (200 + * indicates success). + * success - {Function} Optional function to call if request status is in + * the 200s. This will be called in addition to callback above and + * would typically only be used as an alternative. + * failure - {Function} Optional function to call if request status is not + * in the 200s. This will be called in addition to callback above and + * would typically only be used as an alternative. + * scope - {Object} If callback is a public method on some object, + * set the scope to that object. + * + * Returns: + * {XMLHttpRequest} Request object. To abort the request before a response + * is received, call abort() on the request object. + */ + issue: function(config) { + // apply default config - proxy host may have changed + var defaultConfig = OpenLayers.Util.extend( + this.DEFAULT_CONFIG, + {proxy: OpenLayers.ProxyHost} + ); + config = config || {}; + config.headers = config.headers || {}; + config = OpenLayers.Util.applyDefaults(config, defaultConfig); + config.headers = OpenLayers.Util.applyDefaults(config.headers, defaultConfig.headers); + // Always set the "X-Requested-With" header to signal that this request + // was issued through the XHR-object. Since header keys are case + // insensitive and we want to allow overriding of the "X-Requested-With" + // header through the user we cannot use applyDefaults, but have to + // check manually whether we were called with a "X-Requested-With" + // header. + var customRequestedWithHeader = false, + headerKey; + for(headerKey in config.headers) { + if (config.headers.hasOwnProperty( headerKey )) { + if (headerKey.toLowerCase() === 'x-requested-with') { + customRequestedWithHeader = true; + } + } + } + if (customRequestedWithHeader === false) { + // we did not have a custom "X-Requested-With" header + config.headers['X-Requested-With'] = 'XMLHttpRequest'; + } + + // create request, open, and set headers + var request = new OpenLayers.Request.XMLHttpRequest(); + var url = OpenLayers.Util.urlAppend(config.url, + OpenLayers.Util.getParameterString(config.params || {})); + url = OpenLayers.Request.makeSameOrigin(url, config.proxy); + request.open( + config.method, url, config.async, config.user, config.password + ); + for(var header in config.headers) { + request.setRequestHeader(header, config.headers[header]); + } + + var events = this.events; + + // we want to execute runCallbacks with "this" as the + // execution scope + var self = this; + + request.onreadystatechange = function() { + if(request.readyState == OpenLayers.Request.XMLHttpRequest.DONE) { + var proceed = events.triggerEvent( + "complete", + {request: request, config: config, requestUrl: url} + ); + if(proceed !== false) { + self.runCallbacks( + {request: request, config: config, requestUrl: url} + ); + } + } + }; + + // send request (optionally with data) and return + // call in a timeout for asynchronous requests so the return is + // available before readyState == 4 for cached docs + if(config.async === false) { + request.send(config.data); + } else { + window.setTimeout(function(){ + if (request.readyState !== 0) { // W3C: 0-UNSENT + request.send(config.data); + } + }, 0); + } + return request; + }, + + /** + * Method: runCallbacks + * Calls the complete, success and failure callbacks. Application + * can listen to the "complete" event, have the listener + * display a confirm window and always return false, and + * execute OpenLayers.Request.runCallbacks if the user + * hits "yes" in the confirm window. + * + * Parameters: + * options - {Object} Hash containing request, config and requestUrl keys + */ + runCallbacks: function(options) { + var request = options.request; + var config = options.config; + + // bind callbacks to readyState 4 (done) + var complete = (config.scope) ? + OpenLayers.Function.bind(config.callback, config.scope) : + config.callback; + + // optional success callback + var success; + if(config.success) { + success = (config.scope) ? + OpenLayers.Function.bind(config.success, config.scope) : + config.success; + } + + // optional failure callback + var failure; + if(config.failure) { + failure = (config.scope) ? + OpenLayers.Function.bind(config.failure, config.scope) : + config.failure; + } + + if (OpenLayers.Util.createUrlObject(config.url).protocol == "file:" && + request.responseText) { + request.status = 200; + } + complete(request); + + if (!request.status || (request.status >= 200 && request.status < 300)) { + this.events.triggerEvent("success", options); + if(success) { + success(request); + } + } + if(request.status && (request.status < 200 || request.status >= 300)) { + this.events.triggerEvent("failure", options); + if(failure) { + failure(request); + } + } + }, + + /** + * APIMethod: GET + * Send an HTTP GET request. Additional configuration properties are + * documented in the <issue> method, with the method property set + * to GET. + * + * Parameters: + * config - {Object} Object with properties for configuring the request. + * See the <issue> method for documentation of allowed properties. + * This object is modified and should not be reused. + * + * Returns: + * {XMLHttpRequest} Request object. + */ + GET: function(config) { + config = OpenLayers.Util.extend(config, {method: "GET"}); + return OpenLayers.Request.issue(config); + }, + + /** + * APIMethod: POST + * Send a POST request. Additional configuration properties are + * documented in the <issue> method, with the method property set + * to POST and "Content-Type" header set to "application/xml". + * + * Parameters: + * config - {Object} Object with properties for configuring the request. + * See the <issue> method for documentation of allowed properties. The + * default "Content-Type" header will be set to "application-xml" if + * none is provided. This object is modified and should not be reused. + * + * Returns: + * {XMLHttpRequest} Request object. + */ + POST: function(config) { + config = OpenLayers.Util.extend(config, {method: "POST"}); + // set content type to application/xml if it isn't already set + config.headers = config.headers ? config.headers : {}; + if(!("CONTENT-TYPE" in OpenLayers.Util.upperCaseObject(config.headers))) { + config.headers["Content-Type"] = "application/xml"; + } + return OpenLayers.Request.issue(config); + }, + + /** + * APIMethod: PUT + * Send an HTTP PUT request. Additional configuration properties are + * documented in the <issue> method, with the method property set + * to PUT and "Content-Type" header set to "application/xml". + * + * Parameters: + * config - {Object} Object with properties for configuring the request. + * See the <issue> method for documentation of allowed properties. The + * default "Content-Type" header will be set to "application-xml" if + * none is provided. This object is modified and should not be reused. + * + * Returns: + * {XMLHttpRequest} Request object. + */ + PUT: function(config) { + config = OpenLayers.Util.extend(config, {method: "PUT"}); + // set content type to application/xml if it isn't already set + config.headers = config.headers ? config.headers : {}; + if(!("CONTENT-TYPE" in OpenLayers.Util.upperCaseObject(config.headers))) { + config.headers["Content-Type"] = "application/xml"; + } + return OpenLayers.Request.issue(config); + }, + + /** + * APIMethod: DELETE + * Send an HTTP DELETE request. Additional configuration properties are + * documented in the <issue> method, with the method property set + * to DELETE. + * + * Parameters: + * config - {Object} Object with properties for configuring the request. + * See the <issue> method for documentation of allowed properties. + * This object is modified and should not be reused. + * + * Returns: + * {XMLHttpRequest} Request object. + */ + DELETE: function(config) { + config = OpenLayers.Util.extend(config, {method: "DELETE"}); + return OpenLayers.Request.issue(config); + }, + + /** + * APIMethod: HEAD + * Send an HTTP HEAD request. Additional configuration properties are + * documented in the <issue> method, with the method property set + * to HEAD. + * + * Parameters: + * config - {Object} Object with properties for configuring the request. + * See the <issue> method for documentation of allowed properties. + * This object is modified and should not be reused. + * + * Returns: + * {XMLHttpRequest} Request object. + */ + HEAD: function(config) { + config = OpenLayers.Util.extend(config, {method: "HEAD"}); + return OpenLayers.Request.issue(config); + }, + + /** + * APIMethod: OPTIONS + * Send an HTTP OPTIONS request. Additional configuration properties are + * documented in the <issue> method, with the method property set + * to OPTIONS. + * + * Parameters: + * config - {Object} Object with properties for configuring the request. + * See the <issue> method for documentation of allowed properties. + * This object is modified and should not be reused. + * + * Returns: + * {XMLHttpRequest} Request object. + */ + OPTIONS: function(config) { + config = OpenLayers.Util.extend(config, {method: "OPTIONS"}); + return OpenLayers.Request.issue(config); + } + +}); +/* ====================================================================== + OpenLayers/Request/XMLHttpRequest.js + ====================================================================== */ + +// XMLHttpRequest.js Copyright (C) 2010 Sergey Ilinsky (http://www.ilinsky.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @requires OpenLayers/Request.js + */ + +(function () { + + // Save reference to earlier defined object implementation (if any) + var oXMLHttpRequest = window.XMLHttpRequest; + + // Define on browser type + var bGecko = !!window.controllers, + bIE = window.document.all && !window.opera, + bIE7 = bIE && window.navigator.userAgent.match(/MSIE 7.0/); + + // Enables "XMLHttpRequest()" call next to "new XMLHttpReques()" + function fXMLHttpRequest() { + this._object = oXMLHttpRequest && !bIE7 ? new oXMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP"); + this._listeners = []; + }; + + // Constructor + function cXMLHttpRequest() { + return new fXMLHttpRequest; + }; + cXMLHttpRequest.prototype = fXMLHttpRequest.prototype; + + // BUGFIX: Firefox with Firebug installed would break pages if not executed + if (bGecko && oXMLHttpRequest.wrapped) + cXMLHttpRequest.wrapped = oXMLHttpRequest.wrapped; + + // Constants + cXMLHttpRequest.UNSENT = 0; + cXMLHttpRequest.OPENED = 1; + cXMLHttpRequest.HEADERS_RECEIVED = 2; + cXMLHttpRequest.LOADING = 3; + cXMLHttpRequest.DONE = 4; + + // Public Properties + cXMLHttpRequest.prototype.readyState = cXMLHttpRequest.UNSENT; + cXMLHttpRequest.prototype.responseText = ''; + cXMLHttpRequest.prototype.responseXML = null; + cXMLHttpRequest.prototype.status = 0; + cXMLHttpRequest.prototype.statusText = ''; + + // Priority proposal + cXMLHttpRequest.prototype.priority = "NORMAL"; + + // Instance-level Events Handlers + cXMLHttpRequest.prototype.onreadystatechange = null; + + // Class-level Events Handlers + cXMLHttpRequest.onreadystatechange = null; + cXMLHttpRequest.onopen = null; + cXMLHttpRequest.onsend = null; + cXMLHttpRequest.onabort = null; + + // Public Methods + cXMLHttpRequest.prototype.open = function(sMethod, sUrl, bAsync, sUser, sPassword) { + // Delete headers, required when object is reused + delete this._headers; + + // When bAsync parameter value is omitted, use true as default + if (arguments.length < 3) + bAsync = true; + + // Save async parameter for fixing Gecko bug with missing readystatechange in synchronous requests + this._async = bAsync; + + // Set the onreadystatechange handler + var oRequest = this, + nState = this.readyState, + fOnUnload; + + // BUGFIX: IE - memory leak on page unload (inter-page leak) + if (bIE && bAsync) { + fOnUnload = function() { + if (nState != cXMLHttpRequest.DONE) { + fCleanTransport(oRequest); + // Safe to abort here since onreadystatechange handler removed + oRequest.abort(); + } + }; + window.attachEvent("onunload", fOnUnload); + } + + // Add method sniffer + if (cXMLHttpRequest.onopen) + cXMLHttpRequest.onopen.apply(this, arguments); + + if (arguments.length > 4) + this._object.open(sMethod, sUrl, bAsync, sUser, sPassword); + else + if (arguments.length > 3) + this._object.open(sMethod, sUrl, bAsync, sUser); + else + this._object.open(sMethod, sUrl, bAsync); + + this.readyState = cXMLHttpRequest.OPENED; + fReadyStateChange(this); + + this._object.onreadystatechange = function() { + if (bGecko && !bAsync) + return; + + // Synchronize state + oRequest.readyState = oRequest._object.readyState; + + // + fSynchronizeValues(oRequest); + + // BUGFIX: Firefox fires unnecessary DONE when aborting + if (oRequest._aborted) { + // Reset readyState to UNSENT + oRequest.readyState = cXMLHttpRequest.UNSENT; + + // Return now + return; + } + + if (oRequest.readyState == cXMLHttpRequest.DONE) { + // Free up queue + delete oRequest._data; +/* if (bAsync) + fQueue_remove(oRequest);*/ + // + fCleanTransport(oRequest); +// Uncomment this block if you need a fix for IE cache +/* + // BUGFIX: IE - cache issue + if (!oRequest._object.getResponseHeader("Date")) { + // Save object to cache + oRequest._cached = oRequest._object; + + // Instantiate a new transport object + cXMLHttpRequest.call(oRequest); + + // Re-send request + if (sUser) { + if (sPassword) + oRequest._object.open(sMethod, sUrl, bAsync, sUser, sPassword); + else + oRequest._object.open(sMethod, sUrl, bAsync, sUser); + } + else + oRequest._object.open(sMethod, sUrl, bAsync); + oRequest._object.setRequestHeader("If-Modified-Since", oRequest._cached.getResponseHeader("Last-Modified") || new window.Date(0)); + // Copy headers set + if (oRequest._headers) + for (var sHeader in oRequest._headers) + if (typeof oRequest._headers[sHeader] == "string") // Some frameworks prototype objects with functions + oRequest._object.setRequestHeader(sHeader, oRequest._headers[sHeader]); + + oRequest._object.onreadystatechange = function() { + // Synchronize state + oRequest.readyState = oRequest._object.readyState; + + if (oRequest._aborted) { + // + oRequest.readyState = cXMLHttpRequest.UNSENT; + + // Return + return; + } + + if (oRequest.readyState == cXMLHttpRequest.DONE) { + // Clean Object + fCleanTransport(oRequest); + + // get cached request + if (oRequest.status == 304) + oRequest._object = oRequest._cached; + + // + delete oRequest._cached; + + // + fSynchronizeValues(oRequest); + + // + fReadyStateChange(oRequest); + + // BUGFIX: IE - memory leak in interrupted + if (bIE && bAsync) + window.detachEvent("onunload", fOnUnload); + } + }; + oRequest._object.send(null); + + // Return now - wait until re-sent request is finished + return; + }; +*/ + // BUGFIX: IE - memory leak in interrupted + if (bIE && bAsync) + window.detachEvent("onunload", fOnUnload); + } + + // BUGFIX: Some browsers (Internet Explorer, Gecko) fire OPEN readystate twice + if (nState != oRequest.readyState) + fReadyStateChange(oRequest); + + nState = oRequest.readyState; + } + }; + function fXMLHttpRequest_send(oRequest) { + oRequest._object.send(oRequest._data); + + // BUGFIX: Gecko - missing readystatechange calls in synchronous requests + if (bGecko && !oRequest._async) { + oRequest.readyState = cXMLHttpRequest.OPENED; + + // Synchronize state + fSynchronizeValues(oRequest); + + // Simulate missing states + while (oRequest.readyState < cXMLHttpRequest.DONE) { + oRequest.readyState++; + fReadyStateChange(oRequest); + // Check if we are aborted + if (oRequest._aborted) + return; + } + } + }; + cXMLHttpRequest.prototype.send = function(vData) { + // Add method sniffer + if (cXMLHttpRequest.onsend) + cXMLHttpRequest.onsend.apply(this, arguments); + + if (!arguments.length) + vData = null; + + // BUGFIX: Safari - fails sending documents created/modified dynamically, so an explicit serialization required + // BUGFIX: IE - rewrites any custom mime-type to "text/xml" in case an XMLNode is sent + // BUGFIX: Gecko - fails sending Element (this is up to the implementation either to standard) + if (vData && vData.nodeType) { + vData = window.XMLSerializer ? new window.XMLSerializer().serializeToString(vData) : vData.xml; + if (!this._headers["Content-Type"]) + this._object.setRequestHeader("Content-Type", "application/xml"); + } + + this._data = vData; +/* + // Add to queue + if (this._async) + fQueue_add(this); + else*/ + fXMLHttpRequest_send(this); + }; + cXMLHttpRequest.prototype.abort = function() { + // Add method sniffer + if (cXMLHttpRequest.onabort) + cXMLHttpRequest.onabort.apply(this, arguments); + + // BUGFIX: Gecko - unnecessary DONE when aborting + if (this.readyState > cXMLHttpRequest.UNSENT) + this._aborted = true; + + this._object.abort(); + + // BUGFIX: IE - memory leak + fCleanTransport(this); + + this.readyState = cXMLHttpRequest.UNSENT; + + delete this._data; +/* if (this._async) + fQueue_remove(this);*/ + }; + cXMLHttpRequest.prototype.getAllResponseHeaders = function() { + return this._object.getAllResponseHeaders(); + }; + cXMLHttpRequest.prototype.getResponseHeader = function(sName) { + return this._object.getResponseHeader(sName); + }; + cXMLHttpRequest.prototype.setRequestHeader = function(sName, sValue) { + // BUGFIX: IE - cache issue + if (!this._headers) + this._headers = {}; + this._headers[sName] = sValue; + + return this._object.setRequestHeader(sName, sValue); + }; + + // EventTarget interface implementation + cXMLHttpRequest.prototype.addEventListener = function(sName, fHandler, bUseCapture) { + for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++) + if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture) + return; + // Add listener + this._listeners.push([sName, fHandler, bUseCapture]); + }; + + cXMLHttpRequest.prototype.removeEventListener = function(sName, fHandler, bUseCapture) { + for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++) + if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture) + break; + // Remove listener + if (oListener) + this._listeners.splice(nIndex, 1); + }; + + cXMLHttpRequest.prototype.dispatchEvent = function(oEvent) { + var oEventPseudo = { + 'type': oEvent.type, + 'target': this, + 'currentTarget':this, + 'eventPhase': 2, + 'bubbles': oEvent.bubbles, + 'cancelable': oEvent.cancelable, + 'timeStamp': oEvent.timeStamp, + 'stopPropagation': function() {}, // There is no flow + 'preventDefault': function() {}, // There is no default action + 'initEvent': function() {} // Original event object should be initialized + }; + + // Execute onreadystatechange + if (oEventPseudo.type == "readystatechange" && this.onreadystatechange) + (this.onreadystatechange.handleEvent || this.onreadystatechange).apply(this, [oEventPseudo]); + + // Execute listeners + for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++) + if (oListener[0] == oEventPseudo.type && !oListener[2]) + (oListener[1].handleEvent || oListener[1]).apply(this, [oEventPseudo]); + }; + + // + cXMLHttpRequest.prototype.toString = function() { + return '[' + "object" + ' ' + "XMLHttpRequest" + ']'; + }; + + cXMLHttpRequest.toString = function() { + return '[' + "XMLHttpRequest" + ']'; + }; + + // Helper function + function fReadyStateChange(oRequest) { + // Sniffing code + if (cXMLHttpRequest.onreadystatechange) + cXMLHttpRequest.onreadystatechange.apply(oRequest); + + // Fake event + oRequest.dispatchEvent({ + 'type': "readystatechange", + 'bubbles': false, + 'cancelable': false, + 'timeStamp': new Date + 0 + }); + }; + + function fGetDocument(oRequest) { + var oDocument = oRequest.responseXML, + sResponse = oRequest.responseText; + // Try parsing responseText + if (bIE && sResponse && oDocument && !oDocument.documentElement && oRequest.getResponseHeader("Content-Type").match(/[^\/]+\/[^\+]+\+xml/)) { + oDocument = new window.ActiveXObject("Microsoft.XMLDOM"); + oDocument.async = false; + oDocument.validateOnParse = false; + oDocument.loadXML(sResponse); + } + // Check if there is no error in document + if (oDocument) + if ((bIE && oDocument.parseError != 0) || !oDocument.documentElement || (oDocument.documentElement && oDocument.documentElement.tagName == "parsererror")) + return null; + return oDocument; + }; + + function fSynchronizeValues(oRequest) { + try { oRequest.responseText = oRequest._object.responseText; } catch (e) {} + try { oRequest.responseXML = fGetDocument(oRequest._object); } catch (e) {} + try { oRequest.status = oRequest._object.status; } catch (e) {} + try { oRequest.statusText = oRequest._object.statusText; } catch (e) {} + }; + + function fCleanTransport(oRequest) { + // BUGFIX: IE - memory leak (on-page leak) + oRequest._object.onreadystatechange = new window.Function; + }; +/* + // Queue manager + var oQueuePending = {"CRITICAL":[],"HIGH":[],"NORMAL":[],"LOW":[],"LOWEST":[]}, + aQueueRunning = []; + function fQueue_add(oRequest) { + oQueuePending[oRequest.priority in oQueuePending ? oRequest.priority : "NORMAL"].push(oRequest); + // + setTimeout(fQueue_process); + }; + + function fQueue_remove(oRequest) { + for (var nIndex = 0, bFound = false; nIndex < aQueueRunning.length; nIndex++) + if (bFound) + aQueueRunning[nIndex - 1] = aQueueRunning[nIndex]; + else + if (aQueueRunning[nIndex] == oRequest) + bFound = true; + if (bFound) + aQueueRunning.length--; + // + setTimeout(fQueue_process); + }; + + function fQueue_process() { + if (aQueueRunning.length < 6) { + for (var sPriority in oQueuePending) { + if (oQueuePending[sPriority].length) { + var oRequest = oQueuePending[sPriority][0]; + oQueuePending[sPriority] = oQueuePending[sPriority].slice(1); + // + aQueueRunning.push(oRequest); + // Send request + fXMLHttpRequest_send(oRequest); + break; + } + } + } + }; +*/ + // Internet Explorer 5.0 (missing apply) + if (!window.Function.prototype.apply) { + window.Function.prototype.apply = function(oRequest, oArguments) { + if (!oArguments) + oArguments = []; + oRequest.__func = this; + oRequest.__func(oArguments[0], oArguments[1], oArguments[2], oArguments[3], oArguments[4]); + delete oRequest.__func; + }; + }; + + // Register new object with window + /** + * Class: OpenLayers.Request.XMLHttpRequest + * Standard-compliant (W3C) cross-browser implementation of the + * XMLHttpRequest object. From + * http://code.google.com/p/xmlhttprequest/. + */ + if (!OpenLayers.Request) { + /** + * This allows for OpenLayers/Request.js to be included + * before or after this script. + */ + OpenLayers.Request = {}; + } + OpenLayers.Request.XMLHttpRequest = cXMLHttpRequest; +})(); +/* ====================================================================== + OpenLayers/Format/KML.js + ====================================================================== */ + +/* 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/Date.js + * @requires OpenLayers/Format/XML.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Geometry/Point.js + * @requires OpenLayers/Geometry/LineString.js + * @requires OpenLayers/Geometry/Polygon.js + * @requires OpenLayers/Geometry/Collection.js + * @requires OpenLayers/Request/XMLHttpRequest.js + * @requires OpenLayers/Projection.js + */ + +/** + * Class: OpenLayers.Format.KML + * Read/Write KML. Create a new instance with the <OpenLayers.Format.KML> + * constructor. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. + */ + namespaces: { + kml: "http://www.opengis.net/kml/2.2", + gx: "http://www.google.com/kml/ext/2.2" + }, + + /** + * APIProperty: kmlns + * {String} KML Namespace to use. Defaults to 2.0 namespace. + */ + kmlns: "http://earth.google.com/kml/2.0", + + /** + * APIProperty: placemarksDesc + * {String} Name of the placemarks. Default is "No description available". + */ + placemarksDesc: "No description available", + + /** + * APIProperty: foldersName + * {String} Name of the folders. Default is "OpenLayers export". + * If set to null, no name element will be created. + */ + foldersName: "OpenLayers export", + + /** + * APIProperty: foldersDesc + * {String} Description of the folders. Default is "Exported on [date]." + * If set to null, no description element will be created. + */ + foldersDesc: "Exported on " + new Date(), + + /** + * APIProperty: extractAttributes + * {Boolean} Extract attributes from KML. Default is true. + * Extracting styleUrls requires this to be set to true + * Note that currently only Data and SimpleData + * elements are handled. + */ + extractAttributes: true, + + /** + * APIProperty: kvpAttributes + * {Boolean} Only used if extractAttributes is true. + * If set to true, attributes will be simple + * key-value pairs, compatible with other formats, + * Any displayName elements will be ignored. + * If set to false, attributes will be objects, + * retaining any displayName elements, but not + * compatible with other formats. Any CDATA in + * displayName will be read in as a string value. + * Default is false. + */ + kvpAttributes: false, + + /** + * Property: extractStyles + * {Boolean} Extract styles from KML. Default is false. + * Extracting styleUrls also requires extractAttributes to be + * set to true + */ + extractStyles: false, + + /** + * APIProperty: extractTracks + * {Boolean} Extract gx:Track elements from Placemark elements. Default + * is false. If true, features will be generated for all points in + * all gx:Track elements. Features will have a when (Date) attribute + * based on when elements in the track. If tracks include angle + * elements, features will have heading, tilt, and roll attributes. + * If track point coordinates have three values, features will have + * an altitude attribute with the third coordinate value. + */ + extractTracks: false, + + /** + * APIProperty: trackAttributes + * {Array} If <extractTracks> is true, points within gx:Track elements will + * be parsed as features with when, heading, tilt, and roll attributes. + * Any additional attribute names can be provided in <trackAttributes>. + */ + trackAttributes: null, + + /** + * Property: internalns + * {String} KML Namespace to use -- defaults to the namespace of the + * Placemark node being parsed, but falls back to kmlns. + */ + internalns: null, + + /** + * Property: features + * {Array} Array of features + * + */ + features: null, + + /** + * Property: styles + * {Object} Storage of style objects + * + */ + styles: null, + + /** + * Property: styleBaseUrl + * {String} + */ + styleBaseUrl: "", + + /** + * Property: fetched + * {Object} Storage of KML URLs that have been fetched before + * in order to prevent reloading them. + */ + fetched: null, + + /** + * APIProperty: maxDepth + * {Integer} Maximum depth for recursive loading external KML URLs + * Defaults to 0: do no external fetching + */ + maxDepth: 0, + + /** + * Constructor: OpenLayers.Format.KML + * Create a new parser for KML. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + // compile regular expressions once instead of every time they are used + this.regExes = { + trimSpace: (/^\s*|\s*$/g), + removeSpace: (/\s*/g), + splitSpace: (/\s+/), + trimComma: (/\s*,\s*/g), + kmlColor: (/(\w{2})(\w{2})(\w{2})(\w{2})/), + kmlIconPalette: (/root:\/\/icons\/palette-(\d+)(\.\w+)/), + straightBracket: (/\$\[(.*?)\]/g) + }; + // KML coordinates are always in longlat WGS84 + this.externalProjection = new OpenLayers.Projection("EPSG:4326"); + + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: read + * Read data from a string, and return a list of features. + * + * Parameters: + * data - {String} or {DOMElement} data to read/parse. + * + * Returns: + * {Array(<OpenLayers.Feature.Vector>)} List of features. + */ + read: function(data) { + this.features = []; + this.styles = {}; + this.fetched = {}; + + // Set default options + var options = { + depth: 0, + styleBaseUrl: this.styleBaseUrl + }; + + return this.parseData(data, options); + }, + + /** + * Method: parseData + * Read data from a string, and return a list of features. + * + * Parameters: + * data - {String} or {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + * Returns: + * {Array(<OpenLayers.Feature.Vector>)} List of features. + */ + parseData: function(data, options) { + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + + // Loop throught the following node types in this order and + // process the nodes found + var types = ["Link", "NetworkLink", "Style", "StyleMap", "Placemark"]; + for(var i=0, len=types.length; i<len; ++i) { + var type = types[i]; + + var nodes = this.getElementsByTagNameNS(data, "*", type); + + // skip to next type if no nodes are found + if(nodes.length == 0) { + continue; + } + + switch (type.toLowerCase()) { + + // Fetch external links + case "link": + case "networklink": + this.parseLinks(nodes, options); + break; + + // parse style information + case "style": + if (this.extractStyles) { + this.parseStyles(nodes, options); + } + break; + case "stylemap": + if (this.extractStyles) { + this.parseStyleMaps(nodes, options); + } + break; + + // parse features + case "placemark": + this.parseFeatures(nodes, options); + break; + } + } + + return this.features; + }, + + /** + * Method: parseLinks + * Finds URLs of linked KML documents and fetches them + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseLinks: function(nodes, options) { + + // Fetch external links <NetworkLink> and <Link> + // Don't do anything if we have reached our maximum depth for recursion + if (options.depth >= this.maxDepth) { + return false; + } + + // increase depth + var newOptions = OpenLayers.Util.extend({}, options); + newOptions.depth++; + + for(var i=0, len=nodes.length; i<len; i++) { + var href = this.parseProperty(nodes[i], "*", "href"); + if(href && !this.fetched[href]) { + this.fetched[href] = true; // prevent reloading the same urls + var data = this.fetchLink(href); + if (data) { + this.parseData(data, newOptions); + } + } + } + + }, + + /** + * Method: fetchLink + * Fetches a URL and returns the result + * + * Parameters: + * href - {String} url to be fetched + * + */ + fetchLink: function(href) { + var request = OpenLayers.Request.GET({url: href, async: false}); + if (request) { + return request.responseText; + } + }, + + /** + * Method: parseStyles + * Parses <Style> nodes + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseStyles: function(nodes, options) { + for(var i=0, len=nodes.length; i<len; i++) { + var style = this.parseStyle(nodes[i]); + if(style) { + var styleName = (options.styleBaseUrl || "") + "#" + style.id; + + this.styles[styleName] = style; + } + } + }, + + /** + * Method: parseKmlColor + * Parses a kml color (in 'aabbggrr' format) and returns the corresponding + * color and opacity or null if the color is invalid. + * + * Parameters: + * kmlColor - {String} a kml formated color + * + * Returns: + * {Object} + */ + parseKmlColor: function(kmlColor) { + var color = null; + if (kmlColor) { + var matches = kmlColor.match(this.regExes.kmlColor); + if (matches) { + color = { + color: '#' + matches[4] + matches[3] + matches[2], + opacity: parseInt(matches[1], 16) / 255 + }; + } + } + return color; + }, + + /** + * Method: parseStyle + * Parses the children of a <Style> node and builds the style hash + * accordingly + * + * Parameters: + * node - {DOMElement} <Style> node + * + */ + parseStyle: function(node) { + var style = {}; + + var types = ["LineStyle", "PolyStyle", "IconStyle", "BalloonStyle", + "LabelStyle"]; + var type, styleTypeNode, nodeList, geometry, parser; + for(var i=0, len=types.length; i<len; ++i) { + type = types[i]; + styleTypeNode = this.getElementsByTagNameNS(node, "*", type)[0]; + if(!styleTypeNode) { + continue; + } + + // only deal with first geometry of this type + switch (type.toLowerCase()) { + case "linestyle": + var kmlColor = this.parseProperty(styleTypeNode, "*", "color"); + var color = this.parseKmlColor(kmlColor); + if (color) { + style["strokeColor"] = color.color; + style["strokeOpacity"] = color.opacity; + } + + var width = this.parseProperty(styleTypeNode, "*", "width"); + if (width) { + style["strokeWidth"] = width; + } + break; + + case "polystyle": + var kmlColor = this.parseProperty(styleTypeNode, "*", "color"); + var color = this.parseKmlColor(kmlColor); + if (color) { + style["fillOpacity"] = color.opacity; + style["fillColor"] = color.color; + } + // Check if fill is disabled + var fill = this.parseProperty(styleTypeNode, "*", "fill"); + if (fill == "0") { + style["fillColor"] = "none"; + } + // Check if outline is disabled + var outline = this.parseProperty(styleTypeNode, "*", "outline"); + if (outline == "0") { + style["strokeWidth"] = "0"; + } + + break; + + case "iconstyle": + // set scale + var scale = parseFloat(this.parseProperty(styleTypeNode, + "*", "scale") || 1); + + // set default width and height of icon + var width = 32 * scale; + var height = 32 * scale; + + var iconNode = this.getElementsByTagNameNS(styleTypeNode, + "*", + "Icon")[0]; + if (iconNode) { + var href = this.parseProperty(iconNode, "*", "href"); + if (href) { + + var w = this.parseProperty(iconNode, "*", "w"); + var h = this.parseProperty(iconNode, "*", "h"); + + // Settings for Google specific icons that are 64x64 + // We set the width and height to 64 and halve the + // scale to prevent icons from being too big + var google = "http://maps.google.com/mapfiles/kml"; + if (OpenLayers.String.startsWith( + href, google) && !w && !h) { + w = 64; + h = 64; + scale = scale / 2; + } + + // if only dimension is defined, make sure the + // other one has the same value + w = w || h; + h = h || w; + + if (w) { + width = parseInt(w) * scale; + } + + if (h) { + height = parseInt(h) * scale; + } + + // support for internal icons + // (/root://icons/palette-x.png) + // x and y tell the position on the palette: + // - in pixels + // - starting from the left bottom + // We translate that to a position in the list + // and request the appropriate icon from the + // google maps website + var matches = href.match(this.regExes.kmlIconPalette); + if (matches) { + var palette = matches[1]; + var file_extension = matches[2]; + + var x = this.parseProperty(iconNode, "*", "x"); + var y = this.parseProperty(iconNode, "*", "y"); + + var posX = x ? x/32 : 0; + var posY = y ? (7 - y/32) : 7; + + var pos = posY * 8 + posX; + href = "http://maps.google.com/mapfiles/kml/pal" + + palette + "/icon" + pos + file_extension; + } + + style["graphicOpacity"] = 1; // fully opaque + style["externalGraphic"] = href; + } + + } + + + // hotSpots define the offset for an Icon + var hotSpotNode = this.getElementsByTagNameNS(styleTypeNode, + "*", + "hotSpot")[0]; + if (hotSpotNode) { + var x = parseFloat(hotSpotNode.getAttribute("x")); + var y = parseFloat(hotSpotNode.getAttribute("y")); + + var xUnits = hotSpotNode.getAttribute("xunits"); + if (xUnits == "pixels") { + style["graphicXOffset"] = -x * scale; + } + else if (xUnits == "insetPixels") { + style["graphicXOffset"] = -width + (x * scale); + } + else if (xUnits == "fraction") { + style["graphicXOffset"] = -width * x; + } + + var yUnits = hotSpotNode.getAttribute("yunits"); + if (yUnits == "pixels") { + style["graphicYOffset"] = -height + (y * scale) + 1; + } + else if (yUnits == "insetPixels") { + style["graphicYOffset"] = -(y * scale) + 1; + } + else if (yUnits == "fraction") { + style["graphicYOffset"] = -height * (1 - y) + 1; + } + } + + style["graphicWidth"] = width; + style["graphicHeight"] = height; + break; + + case "balloonstyle": + var balloonStyle = OpenLayers.Util.getXmlNodeValue( + styleTypeNode); + if (balloonStyle) { + style["balloonStyle"] = balloonStyle.replace( + this.regExes.straightBracket, "${$1}"); + } + break; + case "labelstyle": + var kmlColor = this.parseProperty(styleTypeNode, "*", "color"); + var color = this.parseKmlColor(kmlColor); + if (color) { + style["fontColor"] = color.color; + style["fontOpacity"] = color.opacity; + } + break; + + default: + } + } + + // Some polygons have no line color, so we use the fillColor for that + if (!style["strokeColor"] && style["fillColor"]) { + style["strokeColor"] = style["fillColor"]; + } + + var id = node.getAttribute("id"); + if (id && style) { + style.id = id; + } + + return style; + }, + + /** + * Method: parseStyleMaps + * Parses <StyleMap> nodes, but only uses the 'normal' key + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseStyleMaps: function(nodes, options) { + // Only the default or "normal" part of the StyleMap is processed now + // To do the select or "highlight" bit, we'd need to change lots more + + for(var i=0, len=nodes.length; i<len; i++) { + var node = nodes[i]; + var pairs = this.getElementsByTagNameNS(node, "*", + "Pair"); + + var id = node.getAttribute("id"); + for (var j=0, jlen=pairs.length; j<jlen; j++) { + var pair = pairs[j]; + // Use the shortcut in the SLD format to quickly retrieve the + // value of a node. Maybe it's good to have a method in + // Format.XML to do this + var key = this.parseProperty(pair, "*", "key"); + var styleUrl = this.parseProperty(pair, "*", "styleUrl"); + + if (styleUrl && key == "normal") { + this.styles[(options.styleBaseUrl || "") + "#" + id] = + this.styles[(options.styleBaseUrl || "") + styleUrl]; + } + + // TODO: implement the "select" part + //if (styleUrl && key == "highlight") { + //} + + } + } + + }, + + + /** + * Method: parseFeatures + * Loop through all Placemark nodes and parse them. + * Will create a list of features + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseFeatures: function(nodes, options) { + var features = []; + for(var i=0, len=nodes.length; i<len; i++) { + var featureNode = nodes[i]; + var feature = this.parseFeature.apply(this,[featureNode]) ; + if(feature) { + + // Create reference to styleUrl + if (this.extractStyles && feature.attributes && + feature.attributes.styleUrl) { + feature.style = this.getStyle(feature.attributes.styleUrl, options); + } + + if (this.extractStyles) { + // Make sure that <Style> nodes within a placemark are + // processed as well + var inlineStyleNode = this.getElementsByTagNameNS(featureNode, + "*", + "Style")[0]; + if (inlineStyleNode) { + var inlineStyle= this.parseStyle(inlineStyleNode); + if (inlineStyle) { + feature.style = OpenLayers.Util.extend( + feature.style, inlineStyle + ); + } + } + } + + // check if gx:Track elements should be parsed + if (this.extractTracks) { + var tracks = this.getElementsByTagNameNS( + featureNode, this.namespaces.gx, "Track" + ); + if (tracks && tracks.length > 0) { + var track = tracks[0]; + var container = { + features: [], + feature: feature + }; + this.readNode(track, container); + if (container.features.length > 0) { + features.push.apply(features, container.features); + } + } + } else { + // add feature to list of features + features.push(feature); + } + } else { + throw "Bad Placemark: " + i; + } + } + + // add new features to existing feature list + this.features = this.features.concat(features); + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "kml": { + "when": function(node, container) { + container.whens.push(OpenLayers.Date.parse( + this.getChildValue(node) + )); + }, + "_trackPointAttribute": function(node, container) { + var name = node.nodeName.split(":").pop(); + container.attributes[name].push(this.getChildValue(node)); + } + }, + "gx": { + "Track": function(node, container) { + var obj = { + whens: [], + points: [], + angles: [] + }; + if (this.trackAttributes) { + var name; + obj.attributes = {}; + for (var i=0, ii=this.trackAttributes.length; i<ii; ++i) { + name = this.trackAttributes[i]; + obj.attributes[name] = []; + if (!(name in this.readers.kml)) { + this.readers.kml[name] = this.readers.kml._trackPointAttribute; + } + } + } + this.readChildNodes(node, obj); + if (obj.whens.length !== obj.points.length) { + throw new Error("gx:Track with unequal number of when (" + + obj.whens.length + ") and gx:coord (" + + obj.points.length + ") elements."); + } + var hasAngles = obj.angles.length > 0; + if (hasAngles && obj.whens.length !== obj.angles.length) { + throw new Error("gx:Track with unequal number of when (" + + obj.whens.length + ") and gx:angles (" + + obj.angles.length + ") elements."); + } + var feature, point, angles; + for (var i=0, ii=obj.whens.length; i<ii; ++i) { + feature = container.feature.clone(); + feature.fid = container.feature.fid || container.feature.id; + point = obj.points[i]; + feature.geometry = point; + if ("z" in point) { + feature.attributes.altitude = point.z; + } + if (this.internalProjection && this.externalProjection) { + feature.geometry.transform( + this.externalProjection, this.internalProjection + ); + } + if (this.trackAttributes) { + for (var j=0, jj=this.trackAttributes.length; j<jj; ++j) { + var name = this.trackAttributes[j]; + feature.attributes[name] = obj.attributes[name][i]; + } + } + feature.attributes.when = obj.whens[i]; + feature.attributes.trackId = container.feature.id; + if (hasAngles) { + angles = obj.angles[i]; + feature.attributes.heading = parseFloat(angles[0]); + feature.attributes.tilt = parseFloat(angles[1]); + feature.attributes.roll = parseFloat(angles[2]); + } + container.features.push(feature); + } + }, + "coord": function(node, container) { + var str = this.getChildValue(node); + var coords = str.replace(this.regExes.trimSpace, "").split(/\s+/); + var point = new OpenLayers.Geometry.Point(coords[0], coords[1]); + if (coords.length > 2) { + point.z = parseFloat(coords[2]); + } + container.points.push(point); + }, + "angles": function(node, container) { + var str = this.getChildValue(node); + var parts = str.replace(this.regExes.trimSpace, "").split(/\s+/); + container.angles.push(parts); + } + } + }, + + /** + * Method: parseFeature + * This function is the core of the KML parsing code in OpenLayers. + * It creates the geometries that are then attached to the returned + * feature, and calls parseAttributes() to get attribute data out. + * + * Parameters: + * node - {DOMElement} + * + * Returns: + * {<OpenLayers.Feature.Vector>} A vector feature. + */ + parseFeature: function(node) { + // only accept one geometry per feature - look for highest "order" + var order = ["MultiGeometry", "Polygon", "LineString", "Point"]; + var type, nodeList, geometry, parser; + for(var i=0, len=order.length; i<len; ++i) { + type = order[i]; + this.internalns = node.namespaceURI ? + node.namespaceURI : this.kmlns; + nodeList = this.getElementsByTagNameNS(node, + this.internalns, type); + if(nodeList.length > 0) { + // only deal with first geometry of this type + var parser = this.parseGeometry[type.toLowerCase()]; + if(parser) { + geometry = parser.apply(this, [nodeList[0]]); + if (this.internalProjection && this.externalProjection) { + geometry.transform(this.externalProjection, + this.internalProjection); + } + } else { + throw new TypeError("Unsupported geometry type: " + type); + } + // stop looking for different geometry types + break; + } + } + + // construct feature (optionally with attributes) + var attributes; + if(this.extractAttributes) { + attributes = this.parseAttributes(node); + } + var feature = new OpenLayers.Feature.Vector(geometry, attributes); + + var fid = node.getAttribute("id") || node.getAttribute("name"); + if(fid != null) { + feature.fid = fid; + } + + return feature; + }, + + /** + * Method: getStyle + * Retrieves a style from a style hash using styleUrl as the key + * If the styleUrl doesn't exist yet, we try to fetch it + * Internet + * + * Parameters: + * styleUrl - {String} URL of style + * options - {Object} Hash of options + * + * Returns: + * {Object} - (reference to) Style hash + */ + getStyle: function(styleUrl, options) { + + var styleBaseUrl = OpenLayers.Util.removeTail(styleUrl); + + var newOptions = OpenLayers.Util.extend({}, options); + newOptions.depth++; + newOptions.styleBaseUrl = styleBaseUrl; + + // Fetch remote Style URLs (if not fetched before) + if (!this.styles[styleUrl] + && !OpenLayers.String.startsWith(styleUrl, "#") + && newOptions.depth <= this.maxDepth + && !this.fetched[styleBaseUrl] ) { + + var data = this.fetchLink(styleBaseUrl); + if (data) { + this.parseData(data, newOptions); + } + + } + + // return requested style + var style = OpenLayers.Util.extend({}, this.styles[styleUrl]); + return style; + }, + + /** + * Property: parseGeometry + * Properties of this object are the functions that parse geometries based + * on their type. + */ + parseGeometry: { + + /** + * Method: parseGeometry.point + * Given a KML node representing a point geometry, create an OpenLayers + * point geometry. + * + * Parameters: + * node - {DOMElement} A KML Point node. + * + * Returns: + * {<OpenLayers.Geometry.Point>} A point geometry. + */ + point: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.internalns, + "coordinates"); + var coords = []; + if(nodeList.length > 0) { + var coordString = nodeList[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.removeSpace, ""); + coords = coordString.split(","); + } + + var point = null; + if(coords.length > 1) { + // preserve third dimension + if(coords.length == 2) { + coords[2] = null; + } + point = new OpenLayers.Geometry.Point(coords[0], coords[1], + coords[2]); + } else { + throw "Bad coordinate string: " + coordString; + } + return point; + }, + + /** + * Method: parseGeometry.linestring + * Given a KML node representing a linestring geometry, create an + * OpenLayers linestring geometry. + * + * Parameters: + * node - {DOMElement} A KML LineString node. + * + * Returns: + * {<OpenLayers.Geometry.LineString>} A linestring geometry. + */ + linestring: function(node, ring) { + var nodeList = this.getElementsByTagNameNS(node, this.internalns, + "coordinates"); + var line = null; + if(nodeList.length > 0) { + var coordString = this.getChildValue(nodeList[0]); + + coordString = coordString.replace(this.regExes.trimSpace, + ""); + coordString = coordString.replace(this.regExes.trimComma, + ","); + var pointList = coordString.split(this.regExes.splitSpace); + var numPoints = pointList.length; + var points = new Array(numPoints); + var coords, numCoords; + for(var i=0; i<numPoints; ++i) { + coords = pointList[i].split(","); + numCoords = coords.length; + if(numCoords > 1) { + if(coords.length == 2) { + coords[2] = null; + } + points[i] = new OpenLayers.Geometry.Point(coords[0], + coords[1], + coords[2]); + } else { + throw "Bad LineString point coordinates: " + + pointList[i]; + } + } + if(numPoints) { + if(ring) { + line = new OpenLayers.Geometry.LinearRing(points); + } else { + line = new OpenLayers.Geometry.LineString(points); + } + } else { + throw "Bad LineString coordinates: " + coordString; + } + } + + return line; + }, + + /** + * Method: parseGeometry.polygon + * Given a KML node representing a polygon geometry, create an + * OpenLayers polygon geometry. + * + * Parameters: + * node - {DOMElement} A KML Polygon node. + * + * Returns: + * {<OpenLayers.Geometry.Polygon>} A polygon geometry. + */ + polygon: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.internalns, + "LinearRing"); + var numRings = nodeList.length; + var components = new Array(numRings); + if(numRings > 0) { + // this assumes exterior ring first, inner rings after + var ring; + for(var i=0, len=nodeList.length; i<len; ++i) { + ring = this.parseGeometry.linestring.apply(this, + [nodeList[i], true]); + if(ring) { + components[i] = ring; + } else { + throw "Bad LinearRing geometry: " + i; + } + } + } + return new OpenLayers.Geometry.Polygon(components); + }, + + /** + * Method: parseGeometry.multigeometry + * Given a KML node representing a multigeometry, create an + * OpenLayers geometry collection. + * + * Parameters: + * node - {DOMElement} A KML MultiGeometry node. + * + * Returns: + * {<OpenLayers.Geometry.Collection>} A geometry collection. + */ + multigeometry: function(node) { + var child, parser; + var parts = []; + var children = node.childNodes; + for(var i=0, len=children.length; i<len; ++i ) { + child = children[i]; + if(child.nodeType == 1) { + var type = (child.prefix) ? + child.nodeName.split(":")[1] : + child.nodeName; + var parser = this.parseGeometry[type.toLowerCase()]; + if(parser) { + parts.push(parser.apply(this, [child])); + } + } + } + return new OpenLayers.Geometry.Collection(parts); + } + + }, + + /** + * Method: parseAttributes + * + * Parameters: + * node - {DOMElement} + * + * Returns: + * {Object} An attributes object. + */ + parseAttributes: function(node) { + var attributes = {}; + + // Extended Data is parsed first. + var edNodes = node.getElementsByTagName("ExtendedData"); + if (edNodes.length) { + attributes = this.parseExtendedData(edNodes[0]); + } + + // assume attribute nodes are type 1 children with a type 3 or 4 child + var child, grandchildren, grandchild; + var children = node.childNodes; + + for(var i=0, len=children.length; i<len; ++i) { + child = children[i]; + if(child.nodeType == 1) { + grandchildren = child.childNodes; + if(grandchildren.length >= 1 && grandchildren.length <= 3) { + var grandchild; + switch (grandchildren.length) { + case 1: + grandchild = grandchildren[0]; + break; + case 2: + var c1 = grandchildren[0]; + var c2 = grandchildren[1]; + grandchild = (c1.nodeType == 3 || c1.nodeType == 4) ? + c1 : c2; + break; + case 3: + default: + grandchild = grandchildren[1]; + break; + } + if(grandchild.nodeType == 3 || grandchild.nodeType == 4) { + var name = (child.prefix) ? + child.nodeName.split(":")[1] : + child.nodeName; + var value = OpenLayers.Util.getXmlNodeValue(grandchild); + if (value) { + value = value.replace(this.regExes.trimSpace, ""); + attributes[name] = value; + } + } + } + } + } + return attributes; + }, + + /** + * Method: parseExtendedData + * Parse ExtendedData from KML. Limited support for schemas/datatypes. + * See http://code.google.com/apis/kml/documentation/kmlreference.html#extendeddata + * for more information on extendeddata. + */ + parseExtendedData: function(node) { + var attributes = {}; + var i, len, data, key; + var dataNodes = node.getElementsByTagName("Data"); + for (i = 0, len = dataNodes.length; i < len; i++) { + data = dataNodes[i]; + key = data.getAttribute("name"); + var ed = {}; + var valueNode = data.getElementsByTagName("value"); + if (valueNode.length) { + ed['value'] = this.getChildValue(valueNode[0]); + } + if (this.kvpAttributes) { + attributes[key] = ed['value']; + } else { + var nameNode = data.getElementsByTagName("displayName"); + if (nameNode.length) { + ed['displayName'] = this.getChildValue(nameNode[0]); + } + attributes[key] = ed; + } + } + var simpleDataNodes = node.getElementsByTagName("SimpleData"); + for (i = 0, len = simpleDataNodes.length; i < len; i++) { + var ed = {}; + data = simpleDataNodes[i]; + key = data.getAttribute("name"); + ed['value'] = this.getChildValue(data); + if (this.kvpAttributes) { + attributes[key] = ed['value']; + } else { + ed['displayName'] = key; + attributes[key] = ed; + } + } + + return attributes; + }, + + /** + * Method: parseProperty + * Convenience method to find a node and return its value + * + * Parameters: + * xmlNode - {<DOMElement>} + * namespace - {String} namespace of the node to find + * tagName - {String} name of the property to parse + * + * Returns: + * {String} The value for the requested property (defaults to null) + */ + parseProperty: function(xmlNode, namespace, tagName) { + var value; + var nodeList = this.getElementsByTagNameNS(xmlNode, namespace, tagName); + try { + value = OpenLayers.Util.getXmlNodeValue(nodeList[0]); + } catch(e) { + value = null; + } + + return value; + }, + + /** + * APIMethod: write + * Accept Feature Collection, and return a string. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} An array of features. + * + * Returns: + * {String} A KML string. + */ + write: function(features) { + if(!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + var kml = this.createElementNS(this.kmlns, "kml"); + var folder = this.createFolderXML(); + for(var i=0, len=features.length; i<len; ++i) { + folder.appendChild(this.createPlacemarkXML(features[i])); + } + kml.appendChild(folder); + return OpenLayers.Format.XML.prototype.write.apply(this, [kml]); + }, + + /** + * Method: createFolderXML + * Creates and returns a KML folder node + * + * Returns: + * {DOMElement} + */ + createFolderXML: function() { + // Folder + var folder = this.createElementNS(this.kmlns, "Folder"); + + // Folder name + if (this.foldersName) { + var folderName = this.createElementNS(this.kmlns, "name"); + var folderNameText = this.createTextNode(this.foldersName); + folderName.appendChild(folderNameText); + folder.appendChild(folderName); + } + + // Folder description + if (this.foldersDesc) { + var folderDesc = this.createElementNS(this.kmlns, "description"); + var folderDescText = this.createTextNode(this.foldersDesc); + folderDesc.appendChild(folderDescText); + folder.appendChild(folderDesc); + } + + return folder; + }, + + /** + * Method: createPlacemarkXML + * Creates and returns a KML placemark node representing the given feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * + * Returns: + * {DOMElement} + */ + createPlacemarkXML: function(feature) { + // Placemark name + var placemarkName = this.createElementNS(this.kmlns, "name"); + var label = (feature.style && feature.style.label) ? feature.style.label : feature.id; + var name = feature.attributes.name || label; + placemarkName.appendChild(this.createTextNode(name)); + + // Placemark description + var placemarkDesc = this.createElementNS(this.kmlns, "description"); + var desc = feature.attributes.description || this.placemarksDesc; + placemarkDesc.appendChild(this.createTextNode(desc)); + + // Placemark + var placemarkNode = this.createElementNS(this.kmlns, "Placemark"); + if(feature.fid != null) { + placemarkNode.setAttribute("id", feature.fid); + } + placemarkNode.appendChild(placemarkName); + placemarkNode.appendChild(placemarkDesc); + + // Geometry node (Point, LineString, etc. nodes) + var geometryNode = this.buildGeometryNode(feature.geometry); + placemarkNode.appendChild(geometryNode); + + // output attributes as extendedData + if (feature.attributes) { + var edNode = this.buildExtendedData(feature.attributes); + if (edNode) { + placemarkNode.appendChild(edNode); + } + } + + return placemarkNode; + }, + + /** + * Method: buildGeometryNode + * Builds and returns a KML geometry node with the given geometry. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} + */ + buildGeometryNode: function(geometry) { + var className = geometry.CLASS_NAME; + var type = className.substring(className.lastIndexOf(".") + 1); + var builder = this.buildGeometry[type.toLowerCase()]; + var node = null; + if(builder) { + node = builder.apply(this, [geometry]); + } + return node; + }, + + /** + * Property: buildGeometry + * Object containing methods to do the actual geometry node building + * based on geometry type. + */ + buildGeometry: { + // TBD: Anybody care about namespace aliases here (these nodes have + // no prefixes)? + + /** + * Method: buildGeometry.point + * Given an OpenLayers point geometry, create a KML point. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Point>} A point geometry. + * + * Returns: + * {DOMElement} A KML point node. + */ + point: function(geometry) { + var kml = this.createElementNS(this.kmlns, "Point"); + kml.appendChild(this.buildCoordinatesNode(geometry)); + return kml; + }, + + /** + * Method: buildGeometry.multipoint + * Given an OpenLayers multipoint geometry, create a KML + * GeometryCollection. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Point>} A multipoint geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multipoint: function(geometry) { + return this.buildGeometry.collection.apply(this, [geometry]); + }, + + /** + * Method: buildGeometry.linestring + * Given an OpenLayers linestring geometry, create a KML linestring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.LineString>} A linestring geometry. + * + * Returns: + * {DOMElement} A KML linestring node. + */ + linestring: function(geometry) { + var kml = this.createElementNS(this.kmlns, "LineString"); + kml.appendChild(this.buildCoordinatesNode(geometry)); + return kml; + }, + + /** + * Method: buildGeometry.multilinestring + * Given an OpenLayers multilinestring geometry, create a KML + * GeometryCollection. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Point>} A multilinestring geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multilinestring: function(geometry) { + return this.buildGeometry.collection.apply(this, [geometry]); + }, + + /** + * Method: buildGeometry.linearring + * Given an OpenLayers linearring geometry, create a KML linearring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.LinearRing>} A linearring geometry. + * + * Returns: + * {DOMElement} A KML linearring node. + */ + linearring: function(geometry) { + var kml = this.createElementNS(this.kmlns, "LinearRing"); + kml.appendChild(this.buildCoordinatesNode(geometry)); + return kml; + }, + + /** + * Method: buildGeometry.polygon + * Given an OpenLayers polygon geometry, create a KML polygon. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Polygon>} A polygon geometry. + * + * Returns: + * {DOMElement} A KML polygon node. + */ + polygon: function(geometry) { + var kml = this.createElementNS(this.kmlns, "Polygon"); + var rings = geometry.components; + var ringMember, ringGeom, type; + for(var i=0, len=rings.length; i<len; ++i) { + type = (i==0) ? "outerBoundaryIs" : "innerBoundaryIs"; + ringMember = this.createElementNS(this.kmlns, type); + ringGeom = this.buildGeometry.linearring.apply(this, + [rings[i]]); + ringMember.appendChild(ringGeom); + kml.appendChild(ringMember); + } + return kml; + }, + + /** + * Method: buildGeometry.multipolygon + * Given an OpenLayers multipolygon geometry, create a KML + * GeometryCollection. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Point>} A multipolygon geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multipolygon: function(geometry) { + return this.buildGeometry.collection.apply(this, [geometry]); + }, + + /** + * Method: buildGeometry.collection + * Given an OpenLayers geometry collection, create a KML MultiGeometry. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Collection>} A geometry collection. + * + * Returns: + * {DOMElement} A KML MultiGeometry node. + */ + collection: function(geometry) { + var kml = this.createElementNS(this.kmlns, "MultiGeometry"); + var child; + for(var i=0, len=geometry.components.length; i<len; ++i) { + child = this.buildGeometryNode.apply(this, + [geometry.components[i]]); + if(child) { + kml.appendChild(child); + } + } + return kml; + } + }, + + /** + * Method: buildCoordinatesNode + * Builds and returns the KML coordinates node with the given geometry + * <coordinates>...</coordinates> + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} + */ + buildCoordinatesNode: function(geometry) { + var coordinatesNode = this.createElementNS(this.kmlns, "coordinates"); + + var path; + var points = geometry.components; + if(points) { + // LineString or LinearRing + var point; + var numPoints = points.length; + var parts = new Array(numPoints); + for(var i=0; i<numPoints; ++i) { + point = points[i]; + parts[i] = this.buildCoordinates(point); + } + path = parts.join(" "); + } else { + // Point + path = this.buildCoordinates(geometry); + } + + var txtNode = this.createTextNode(path); + coordinatesNode.appendChild(txtNode); + + return coordinatesNode; + }, + + /** + * Method: buildCoordinates + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns + * {String} a coordinate pair + */ + buildCoordinates: function(point) { + if (this.internalProjection && this.externalProjection) { + point = point.clone(); + point.transform(this.internalProjection, + this.externalProjection); + } + return point.x + "," + point.y; + }, + + /** + * Method: buildExtendedData + * + * Parameters: + * attributes - {Object} + * + * Returns + * {DOMElement} A KML ExtendedData node or {null} if no attributes. + */ + buildExtendedData: function(attributes) { + var extendedData = this.createElementNS(this.kmlns, "ExtendedData"); + for (var attributeName in attributes) { + // empty, name, description, styleUrl attributes ignored + if (attributes[attributeName] && attributeName != "name" && attributeName != "description" && attributeName != "styleUrl") { + var data = this.createElementNS(this.kmlns, "Data"); + data.setAttribute("name", attributeName); + var value = this.createElementNS(this.kmlns, "value"); + if (typeof attributes[attributeName] == "object") { + // cater for object attributes with 'value' properties + // other object properties will output an empty node + if (attributes[attributeName].value) { + value.appendChild(this.createTextNode(attributes[attributeName].value)); + } + if (attributes[attributeName].displayName) { + var displayName = this.createElementNS(this.kmlns, "displayName"); + // displayName always written as CDATA + displayName.appendChild(this.getXMLDoc().createCDATASection(attributes[attributeName].displayName)); + data.appendChild(displayName); + } + } else { + value.appendChild(this.createTextNode(attributes[attributeName])); + } + data.appendChild(value); + extendedData.appendChild(data); + } + } + if (this.isSimpleContent(extendedData)) { + return null; + } else { + return extendedData; + } + }, + + CLASS_NAME: "OpenLayers.Format.KML" +}); +/* ====================================================================== + OpenLayers/Protocol/WFS/v1.js + ====================================================================== */ + +/* 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/Protocol/WFS.js + */ + +/** + * Class: OpenLayers.Protocol.WFS.v1 + * Abstract class for for v1.0.0 and v1.1.0 protocol. + * + * Inherits from: + * - <OpenLayers.Protocol> + */ +OpenLayers.Protocol.WFS.v1 = OpenLayers.Class(OpenLayers.Protocol, { + + /** + * Property: version + * {String} WFS version number. + */ + version: null, + + /** + * Property: srsName + * {String} Name of spatial reference system. Default is "EPSG:4326". + */ + srsName: "EPSG:4326", + + /** + * Property: featureType + * {String} Local feature typeName. + */ + featureType: null, + + /** + * Property: featureNS + * {String} Feature namespace. + */ + featureNS: null, + + /** + * Property: geometryName + * {String} Name of the geometry attribute for features. Default is + * "the_geom" for WFS <version> 1.0, and null for higher versions. + */ + geometryName: "the_geom", + + /** + * Property: maxFeatures + * {Integer} Optional maximum number of features to retrieve. + */ + + /** + * Property: schema + * {String} Optional schema location that will be included in the + * schemaLocation attribute value. Note that the feature type schema + * is required for a strict XML validator (on transactions with an + * insert for example), but is *not* required by the WFS specification + * (since the server is supposed to know about feature type schemas). + */ + schema: null, + + /** + * Property: featurePrefix + * {String} Namespace alias for feature type. Default is "feature". + */ + featurePrefix: "feature", + + /** + * Property: formatOptions + * {Object} Optional options for the format. If a format is not provided, + * this property can be used to extend the default format options. + */ + formatOptions: null, + + /** + * Property: readFormat + * {<OpenLayers.Format>} For WFS requests it is possible to get a + * different output format than GML. In that case, we cannot parse + * the response with the default format (WFST) and we need a different + * format for reading. + */ + readFormat: null, + + /** + * Property: readOptions + * {Object} Optional object to pass to format's read. + */ + readOptions: null, + + /** + * Constructor: OpenLayers.Protocol.WFS + * A class for giving layers WFS protocol. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + * + * Valid options properties: + * url - {String} URL to send requests to (required). + * featureType - {String} Local (without prefix) feature typeName (required). + * featureNS - {String} Feature namespace (required, but can be autodetected + * during the first query if GML is used as readFormat and + * featurePrefix is provided and matches the prefix used by the server + * for this featureType). + * featurePrefix - {String} Feature namespace alias (optional - only used + * for writing if featureNS is provided). Default is 'feature'. + * geometryName - {String} Name of geometry attribute. The default is + * 'the_geom' for WFS <version> 1.0, and null for higher versions. If + * null, it will be set to the name of the first geometry found in the + * first read operation. + * multi - {Boolean} If set to true, geometries will be casted to Multi + * geometries before they are written in a transaction. No casting will + * be done when reading features. + */ + initialize: function(options) { + OpenLayers.Protocol.prototype.initialize.apply(this, [options]); + if(!options.format) { + this.format = OpenLayers.Format.WFST(OpenLayers.Util.extend({ + version: this.version, + featureType: this.featureType, + featureNS: this.featureNS, + featurePrefix: this.featurePrefix, + geometryName: this.geometryName, + srsName: this.srsName, + schema: this.schema + }, this.formatOptions)); + } + if (!options.geometryName && parseFloat(this.format.version) > 1.0) { + this.setGeometryName(null); + } + }, + + /** + * APIMethod: destroy + * Clean up the protocol. + */ + destroy: function() { + if(this.options && !this.options.format) { + this.format.destroy(); + } + this.format = null; + OpenLayers.Protocol.prototype.destroy.apply(this); + }, + + /** + * APIMethod: read + * Construct a request for reading new features. Since WFS splits the + * basic CRUD operations into GetFeature requests (for read) and + * Transactions (for all others), this method does not make use of the + * format's read method (that is only about reading transaction + * responses). + * + * Parameters: + * options - {Object} Options for the read operation, in addition to the + * options set on the instance (options set here will take precedence). + * + * To use a configured protocol to get e.g. a WFS hit count, applications + * could do the following: + * + * (code) + * protocol.read({ + * readOptions: {output: "object"}, + * resultType: "hits", + * maxFeatures: null, + * callback: function(resp) { + * // process resp.numberOfFeatures here + * } + * }); + * (end) + * + * To use a configured protocol to use WFS paging (if supported by the + * server), applications could do the following: + * + * (code) + * protocol.read({ + * startIndex: 0, + * count: 50 + * }); + * (end) + * + * To limit the attributes returned by the GetFeature request, applications + * can use the propertyNames option to specify the properties to include in + * the response: + * + * (code) + * protocol.read({ + * propertyNames: ["DURATION", "INTENSITY"] + * }); + * (end) + */ + read: function(options) { + OpenLayers.Protocol.prototype.read.apply(this, arguments); + options = OpenLayers.Util.extend({}, options); + OpenLayers.Util.applyDefaults(options, this.options || {}); + var response = new OpenLayers.Protocol.Response({requestType: "read"}); + + var data = OpenLayers.Format.XML.prototype.write.apply( + this.format, [this.format.writeNode("wfs:GetFeature", options)] + ); + + response.priv = OpenLayers.Request.POST({ + url: options.url, + callback: this.createCallback(this.handleRead, response, options), + params: options.params, + headers: options.headers, + data: data + }); + + return response; + }, + + /** + * APIMethod: setFeatureType + * Change the feature type on the fly. + * + * Parameters: + * featureType - {String} Local (without prefix) feature typeName. + */ + setFeatureType: function(featureType) { + this.featureType = featureType; + this.format.featureType = featureType; + }, + + /** + * APIMethod: setGeometryName + * Sets the geometryName option after instantiation. + * + * Parameters: + * geometryName - {String} Name of geometry attribute. + */ + setGeometryName: function(geometryName) { + this.geometryName = geometryName; + this.format.geometryName = geometryName; + }, + + /** + * Method: handleRead + * Deal with response from the read request. + * + * Parameters: + * response - {<OpenLayers.Protocol.Response>} The response object to pass + * to the user callback. + * options - {Object} The user options passed to the read call. + */ + handleRead: function(response, options) { + options = OpenLayers.Util.extend({}, options); + OpenLayers.Util.applyDefaults(options, this.options); + + if(options.callback) { + var request = response.priv; + if(request.status >= 200 && request.status < 300) { + // success + var result = this.parseResponse(request, options.readOptions); + if (result && result.success !== false) { + if (options.readOptions && options.readOptions.output == "object") { + OpenLayers.Util.extend(response, result); + } else { + response.features = result; + } + response.code = OpenLayers.Protocol.Response.SUCCESS; + } else { + // failure (service exception) + response.code = OpenLayers.Protocol.Response.FAILURE; + response.error = result; + } + } else { + // failure + response.code = OpenLayers.Protocol.Response.FAILURE; + } + options.callback.call(options.scope, response); + } + }, + + /** + * Method: parseResponse + * Read HTTP response body and return features + * + * Parameters: + * request - {XMLHttpRequest} The request object + * options - {Object} Optional object to pass to format's read + * + * Returns: + * {Object} or {Array({<OpenLayers.Feature.Vector>})} or + * {<OpenLayers.Feature.Vector>} + * An object with a features property, an array of features or a single + * feature. + */ + parseResponse: function(request, options) { + var doc = request.responseXML; + if(!doc || !doc.documentElement) { + doc = request.responseText; + } + if(!doc || doc.length <= 0) { + return null; + } + var result = (this.readFormat !== null) ? this.readFormat.read(doc) : + this.format.read(doc, options); + if (!this.featureNS) { + var format = this.readFormat || this.format; + this.featureNS = format.featureNS; + // no need to auto-configure again on subsequent reads + format.autoConfig = false; + if (!this.geometryName) { + this.setGeometryName(format.geometryName); + } + } + return result; + }, + + /** + * Method: commit + * Given a list of feature, assemble a batch request for update, create, + * and delete transactions. A commit call on the prototype amounts + * to writing a WFS transaction - so the write method on the format + * is used. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} + * options - {Object} + * + * Valid options properties: + * nativeElements - {Array({Object})} Array of objects with information for writing + * out <Native> elements, these objects have vendorId, safeToIgnore and + * value properties. The <Native> element is intended to allow access to + * vendor specific capabilities of any particular web feature server or + * datastore. + * + * Returns: + * {<OpenLayers.Protocol.Response>} A response object with a features + * property containing any insertIds and a priv property referencing + * the XMLHttpRequest object. + */ + commit: function(features, options) { + + options = OpenLayers.Util.extend({}, options); + OpenLayers.Util.applyDefaults(options, this.options); + + var response = new OpenLayers.Protocol.Response({ + requestType: "commit", + reqFeatures: features + }); + response.priv = OpenLayers.Request.POST({ + url: options.url, + headers: options.headers, + data: this.format.write(features, options), + callback: this.createCallback(this.handleCommit, response, options) + }); + + return response; + }, + + /** + * Method: handleCommit + * Called when the commit request returns. + * + * Parameters: + * response - {<OpenLayers.Protocol.Response>} The response object to pass + * to the user callback. + * options - {Object} The user options passed to the commit call. + */ + handleCommit: function(response, options) { + if(options.callback) { + var request = response.priv; + + // ensure that we have an xml doc + var data = request.responseXML; + if(!data || !data.documentElement) { + data = request.responseText; + } + + var obj = this.format.read(data) || {}; + + response.insertIds = obj.insertIds || []; + if (obj.success) { + response.code = OpenLayers.Protocol.Response.SUCCESS; + } else { + response.code = OpenLayers.Protocol.Response.FAILURE; + response.error = obj; + } + options.callback.call(options.scope, response); + } + }, + + /** + * Method: filterDelete + * Send a request that deletes all features by their filter. + * + * Parameters: + * filter - {<OpenLayers.Filter>} filter + */ + filterDelete: function(filter, options) { + options = OpenLayers.Util.extend({}, options); + OpenLayers.Util.applyDefaults(options, this.options); + + var response = new OpenLayers.Protocol.Response({ + requestType: "commit" + }); + + var root = this.format.createElementNSPlus("wfs:Transaction", { + attributes: { + service: "WFS", + version: this.version + } + }); + + var deleteNode = this.format.createElementNSPlus("wfs:Delete", { + attributes: { + typeName: (options.featureNS ? this.featurePrefix + ":" : "") + + options.featureType + } + }); + + if(options.featureNS) { + deleteNode.setAttribute("xmlns:" + this.featurePrefix, options.featureNS); + } + var filterNode = this.format.writeNode("ogc:Filter", filter); + + deleteNode.appendChild(filterNode); + + root.appendChild(deleteNode); + + var data = OpenLayers.Format.XML.prototype.write.apply( + this.format, [root] + ); + + return OpenLayers.Request.POST({ + url: this.url, + callback : options.callback || function(){}, + data: data + }); + + }, + + /** + * Method: abort + * Abort an ongoing request, the response object passed to + * this method must come from this protocol (as a result + * of a read, or commit operation). + * + * Parameters: + * response - {<OpenLayers.Protocol.Response>} + */ + abort: function(response) { + if (response) { + response.priv.abort(); + } + }, + + CLASS_NAME: "OpenLayers.Protocol.WFS.v1" +}); +/* ====================================================================== + OpenLayers/Handler/Feature.js + ====================================================================== */ + +/* 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/Handler.js + */ + +/** + * Class: OpenLayers.Handler.Feature + * Handler to respond to mouse events related to a drawn feature. Callbacks + * with the following keys will be notified of the following events + * associated with features: click, clickout, over, out, and dblclick. + * + * This handler stops event propagation for mousedown and mouseup if those + * browser events target features that can be selected. + * + * Inherits from: + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Feature = OpenLayers.Class(OpenLayers.Handler, { + + /** + * Property: EVENTMAP + * {Object} A object mapping the browser events to objects with callback + * keys for in and out. + */ + EVENTMAP: { + 'click': {'in': 'click', 'out': 'clickout'}, + 'mousemove': {'in': 'over', 'out': 'out'}, + 'dblclick': {'in': 'dblclick', 'out': null}, + 'mousedown': {'in': null, 'out': null}, + 'mouseup': {'in': null, 'out': null}, + 'touchstart': {'in': 'click', 'out': 'clickout'} + }, + + /** + * Property: feature + * {<OpenLayers.Feature.Vector>} The last feature that was hovered. + */ + feature: null, + + /** + * Property: lastFeature + * {<OpenLayers.Feature.Vector>} The last feature that was handled. + */ + lastFeature: null, + + /** + * Property: down + * {<OpenLayers.Pixel>} The location of the last mousedown. + */ + down: null, + + /** + * Property: up + * {<OpenLayers.Pixel>} The location of the last mouseup. + */ + up: null, + + /** + * Property: clickTolerance + * {Number} The number of pixels the mouse can move between mousedown + * and mouseup for the event to still be considered a click. + * Dragging the map should not trigger the click and clickout callbacks + * unless the map is moved by less than this tolerance. Defaults to 4. + */ + clickTolerance: 4, + + /** + * Property: geometryTypes + * To restrict dragging to a limited set of geometry types, send a list + * of strings corresponding to the geometry class names. + * + * @type Array(String) + */ + geometryTypes: null, + + /** + * Property: stopClick + * {Boolean} If stopClick is set to true, handled clicks do not + * propagate to other click listeners. Otherwise, handled clicks + * do propagate. Unhandled clicks always propagate, whatever the + * value of stopClick. Defaults to true. + */ + stopClick: true, + + /** + * Property: stopDown + * {Boolean} If stopDown is set to true, handled mousedowns do not + * propagate to other mousedown listeners. Otherwise, handled + * mousedowns do propagate. Unhandled mousedowns always propagate, + * whatever the value of stopDown. Defaults to true. + */ + stopDown: true, + + /** + * Property: stopUp + * {Boolean} If stopUp is set to true, handled mouseups do not + * propagate to other mouseup listeners. Otherwise, handled mouseups + * do propagate. Unhandled mouseups always propagate, whatever the + * value of stopUp. Defaults to false. + */ + stopUp: false, + + /** + * Constructor: OpenLayers.Handler.Feature + * + * Parameters: + * control - {<OpenLayers.Control>} + * layer - {<OpenLayers.Layer.Vector>} + * callbacks - {Object} An object with a 'over' property whos value is + * a function to be called when the mouse is over a feature. The + * callback should expect to recieve a single argument, the feature. + * options - {Object} + */ + initialize: function(control, layer, callbacks, options) { + OpenLayers.Handler.prototype.initialize.apply(this, [control, callbacks, options]); + this.layer = layer; + }, + + /** + * Method: touchstart + * Handle touchstart events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchstart: function(evt) { + this.startTouch(); + return OpenLayers.Event.isMultiTouch(evt) ? + true : this.mousedown(evt); + }, + + /** + * Method: touchmove + * Handle touchmove events. We just prevent the browser default behavior, + * for Android Webkit not to select text when moving the finger after + * selecting a feature. + * + * Parameters: + * evt - {Event} + */ + touchmove: function(evt) { + OpenLayers.Event.preventDefault(evt); + }, + + /** + * Method: mousedown + * Handle mouse down. Stop propagation if a feature is targeted by this + * event (stops map dragging during feature selection). + * + * Parameters: + * evt - {Event} + */ + mousedown: function(evt) { + // Feature selection is only done with a left click. Other handlers may stop the + // propagation of left-click mousedown events but not right-click mousedown events. + // This mismatch causes problems when comparing the location of the down and up + // events in the click function so it is important ignore right-clicks. + if (OpenLayers.Event.isLeftClick(evt) || OpenLayers.Event.isSingleTouch(evt)) { + this.down = evt.xy; + } + return this.handle(evt) ? !this.stopDown : true; + }, + + /** + * Method: mouseup + * Handle mouse up. Stop propagation if a feature is targeted by this + * event. + * + * Parameters: + * evt - {Event} + */ + mouseup: function(evt) { + this.up = evt.xy; + return this.handle(evt) ? !this.stopUp : true; + }, + + /** + * Method: click + * Handle click. Call the "click" callback if click on a feature, + * or the "clickout" callback if click outside any feature. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} + */ + click: function(evt) { + return this.handle(evt) ? !this.stopClick : true; + }, + + /** + * Method: mousemove + * Handle mouse moves. Call the "over" callback if moving in to a feature, + * or the "out" callback if moving out of a feature. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} + */ + mousemove: function(evt) { + if (!this.callbacks['over'] && !this.callbacks['out']) { + return true; + } + this.handle(evt); + return true; + }, + + /** + * Method: dblclick + * Handle dblclick. Call the "dblclick" callback if dblclick on a feature. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} + */ + dblclick: function(evt) { + return !this.handle(evt); + }, + + /** + * Method: geometryTypeMatches + * Return true if the geometry type of the passed feature matches + * one of the geometry types in the geometryTypes array. + * + * Parameters: + * feature - {<OpenLayers.Vector.Feature>} + * + * Returns: + * {Boolean} + */ + geometryTypeMatches: function(feature) { + return this.geometryTypes == null || + OpenLayers.Util.indexOf(this.geometryTypes, + feature.geometry.CLASS_NAME) > -1; + }, + + /** + * Method: handle + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} The event occurred over a relevant feature. + */ + handle: function(evt) { + if(this.feature && !this.feature.layer) { + // feature has been destroyed + this.feature = null; + } + var type = evt.type; + var handled = false; + var previouslyIn = !!(this.feature); // previously in a feature + var click = (type == "click" || type == "dblclick" || type == "touchstart"); + this.feature = this.layer.getFeatureFromEvent(evt); + if(this.feature && !this.feature.layer) { + // feature has been destroyed + this.feature = null; + } + if(this.lastFeature && !this.lastFeature.layer) { + // last feature has been destroyed + this.lastFeature = null; + } + if(this.feature) { + if(type === "touchstart") { + // stop the event to prevent Android Webkit from + // "flashing" the map div + OpenLayers.Event.preventDefault(evt); + } + var inNew = (this.feature != this.lastFeature); + if(this.geometryTypeMatches(this.feature)) { + // in to a feature + if(previouslyIn && inNew) { + // out of last feature and in to another + if(this.lastFeature) { + this.triggerCallback(type, 'out', [this.lastFeature]); + } + this.triggerCallback(type, 'in', [this.feature]); + } else if(!previouslyIn || click) { + // in feature for the first time + this.triggerCallback(type, 'in', [this.feature]); + } + this.lastFeature = this.feature; + handled = true; + } else { + // not in to a feature + if(this.lastFeature && (previouslyIn && inNew || click)) { + // out of last feature for the first time + this.triggerCallback(type, 'out', [this.lastFeature]); + } + // next time the mouse goes in a feature whose geometry type + // doesn't match we don't want to call the 'out' callback + // again, so let's set this.feature to null so that + // previouslyIn will evaluate to false the next time + // we enter handle. Yes, a bit hackish... + this.feature = null; + } + } else if(this.lastFeature && (previouslyIn || click)) { + this.triggerCallback(type, 'out', [this.lastFeature]); + } + return handled; + }, + + /** + * Method: triggerCallback + * Call the callback keyed in the event map with the supplied arguments. + * For click and clickout, the <clickTolerance> is checked first. + * + * Parameters: + * type - {String} + */ + triggerCallback: function(type, mode, args) { + var key = this.EVENTMAP[type][mode]; + if(key) { + if(type == 'click' && this.up && this.down) { + // for click/clickout, only trigger callback if tolerance is met + var dpx = Math.sqrt( + Math.pow(this.up.x - this.down.x, 2) + + Math.pow(this.up.y - this.down.y, 2) + ); + if(dpx <= this.clickTolerance) { + this.callback(key, args); + } + // we're done with this set of events now: clear the cached + // positions so we can't trip over them later (this can occur + // if one of the up/down events gets eaten before it gets to us + // but we still get the click) + this.up = this.down = null; + } else { + this.callback(key, args); + } + } + }, + + /** + * Method: activate + * Turn on the handler. Returns false if the handler was already active. + * + * Returns: + * {Boolean} + */ + activate: function() { + var activated = false; + if(OpenLayers.Handler.prototype.activate.apply(this, arguments)) { + this.moveLayerToTop(); + this.map.events.on({ + "removelayer": this.handleMapEvents, + "changelayer": this.handleMapEvents, + scope: this + }); + activated = true; + } + return activated; + }, + + /** + * Method: deactivate + * Turn off the handler. Returns false if the handler was already active. + * + * Returns: + * {Boolean} + */ + deactivate: function() { + var deactivated = false; + if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { + this.moveLayerBack(); + this.feature = null; + this.lastFeature = null; + this.down = null; + this.up = null; + this.map.events.un({ + "removelayer": this.handleMapEvents, + "changelayer": this.handleMapEvents, + scope: this + }); + deactivated = true; + } + return deactivated; + }, + + /** + * Method: handleMapEvents + * + * Parameters: + * evt - {Object} + */ + handleMapEvents: function(evt) { + if (evt.type == "removelayer" || evt.property == "order") { + this.moveLayerToTop(); + } + }, + + /** + * Method: moveLayerToTop + * Moves the layer for this handler to the top, so mouse events can reach + * it. + */ + moveLayerToTop: function() { + var index = Math.max(this.map.Z_INDEX_BASE['Feature'] - 1, + this.layer.getZIndex()) + 1; + this.layer.setZIndex(index); + + }, + + /** + * Method: moveLayerBack + * Moves the layer back to the position determined by the map's layers + * array. + */ + moveLayerBack: function() { + var index = this.layer.getZIndex() - 1; + if (index >= this.map.Z_INDEX_BASE['Feature']) { + this.layer.setZIndex(index); + } else { + this.map.setLayerZIndex(this.layer, + this.map.getLayerIndex(this.layer)); + } + }, + + CLASS_NAME: "OpenLayers.Handler.Feature" +}); +/* ====================================================================== + OpenLayers/StyleMap.js + ====================================================================== */ + +/* 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/Style.js + * @requires OpenLayers/Feature/Vector.js + */ + +/** + * Class: OpenLayers.StyleMap + */ +OpenLayers.StyleMap = OpenLayers.Class({ + + /** + * Property: styles + * {Object} Hash of {<OpenLayers.Style>}, keyed by names of well known + * rendering intents (e.g. "default", "temporary", "select", "delete"). + */ + styles: null, + + /** + * Property: extendDefault + * {Boolean} if true, every render intent will extend the symbolizers + * specified for the "default" intent at rendering time. Otherwise, every + * rendering intent will be treated as a completely independent style. + */ + extendDefault: true, + + /** + * Constructor: OpenLayers.StyleMap + * + * Parameters: + * style - {Object} Optional. Either a style hash, or a style object, or + * a hash of style objects (style hashes) keyed by rendering + * intent. If just one style hash or style object is passed, + * this will be used for all known render intents (default, + * select, temporary) + * options - {Object} optional hash of additional options for this + * instance + */ + initialize: function (style, options) { + this.styles = { + "default": new OpenLayers.Style( + OpenLayers.Feature.Vector.style["default"]), + "select": new OpenLayers.Style( + OpenLayers.Feature.Vector.style["select"]), + "temporary": new OpenLayers.Style( + OpenLayers.Feature.Vector.style["temporary"]), + "delete": new OpenLayers.Style( + OpenLayers.Feature.Vector.style["delete"]) + }; + + // take whatever the user passed as style parameter and convert it + // into parts of stylemap. + if(style instanceof OpenLayers.Style) { + // user passed a style object + this.styles["default"] = style; + this.styles["select"] = style; + this.styles["temporary"] = style; + this.styles["delete"] = style; + } else if(typeof style == "object") { + for(var key in style) { + if(style[key] instanceof OpenLayers.Style) { + // user passed a hash of style objects + this.styles[key] = style[key]; + } else if(typeof style[key] == "object") { + // user passsed a hash of style hashes + this.styles[key] = new OpenLayers.Style(style[key]); + } else { + // user passed a style hash (i.e. symbolizer) + this.styles["default"] = new OpenLayers.Style(style); + this.styles["select"] = new OpenLayers.Style(style); + this.styles["temporary"] = new OpenLayers.Style(style); + this.styles["delete"] = new OpenLayers.Style(style); + break; + } + } + } + OpenLayers.Util.extend(this, options); + }, + + /** + * Method: destroy + */ + destroy: function() { + for(var key in this.styles) { + this.styles[key].destroy(); + } + this.styles = null; + }, + + /** + * Method: createSymbolizer + * Creates the symbolizer for a feature for a render intent. + * + * Parameters: + * feature - {<OpenLayers.Feature>} The feature to evaluate the rules + * of the intended style against. + * intent - {String} The intent determines the symbolizer that will be + * used to draw the feature. Well known intents are "default" + * (for just drawing the features), "select" (for selected + * features) and "temporary" (for drawing features). + * + * Returns: + * {Object} symbolizer hash + */ + createSymbolizer: function(feature, intent) { + if(!feature) { + feature = new OpenLayers.Feature.Vector(); + } + if(!this.styles[intent]) { + intent = "default"; + } + feature.renderIntent = intent; + var defaultSymbolizer = {}; + if(this.extendDefault && intent != "default") { + defaultSymbolizer = this.styles["default"].createSymbolizer(feature); + } + return OpenLayers.Util.extend(defaultSymbolizer, + this.styles[intent].createSymbolizer(feature)); + }, + + /** + * Method: addUniqueValueRules + * Convenience method to create comparison rules for unique values of a + * property. The rules will be added to the style object for a specified + * rendering intent. This method is a shortcut for creating something like + * the "unique value legends" familiar from well known desktop GIS systems + * + * Parameters: + * renderIntent - {String} rendering intent to add the rules to + * property - {String} values of feature attributes to create the + * rules for + * symbolizers - {Object} Hash of symbolizers, keyed by the desired + * property values + * context - {Object} An optional object with properties that + * symbolizers' property values should be evaluated + * against. If no context is specified, feature.attributes + * will be used + */ + addUniqueValueRules: function(renderIntent, property, symbolizers, context) { + var rules = []; + for (var value in symbolizers) { + rules.push(new OpenLayers.Rule({ + symbolizer: symbolizers[value], + context: context, + filter: new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.EQUAL_TO, + property: property, + value: value + }) + })); + } + this.styles[renderIntent].addRules(rules); + }, + + CLASS_NAME: "OpenLayers.StyleMap" +}); +/* ====================================================================== + OpenLayers/Layer/Vector.js + ====================================================================== */ + +/* 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/Layer.js + * @requires OpenLayers/Renderer.js + * @requires OpenLayers/StyleMap.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Console.js + * @requires OpenLayers/Lang.js + */ + +/** + * Class: OpenLayers.Layer.Vector + * Instances of OpenLayers.Layer.Vector are used to render vector data from + * a variety of sources. Create a new vector layer with the + * <OpenLayers.Layer.Vector> constructor. + * + * Inherits from: + * - <OpenLayers.Layer> + */ +OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { + + /** + * APIProperty: events + * {<OpenLayers.Events>} + * + * Register a listener for a particular event with the following syntax: + * (code) + * layer.events.register(type, obj, listener); + * (end) + * + * Listeners will be called with a reference to an event object. The + * properties of this event depends on exactly what happened. + * + * All event objects have at least the following properties: + * object - {Object} A reference to layer.events.object. + * element - {DOMElement} A reference to layer.events.element. + * + * Supported map event types (in addition to those from <OpenLayers.Layer.events>): + * beforefeatureadded - Triggered before a feature is added. Listeners + * will receive an object with a *feature* property referencing the + * feature to be added. To stop the feature from being added, a + * listener should return false. + * beforefeaturesadded - Triggered before an array of features is added. + * Listeners will receive an object with a *features* property + * referencing the feature to be added. To stop the features from + * being added, a listener should return false. + * featureadded - Triggered after a feature is added. The event + * object passed to listeners will have a *feature* property with a + * reference to the added feature. + * featuresadded - Triggered after features are added. The event + * object passed to listeners will have a *features* property with a + * reference to an array of added features. + * beforefeatureremoved - Triggered before a feature is removed. Listeners + * will receive an object with a *feature* property referencing the + * feature to be removed. + * beforefeaturesremoved - Triggered before multiple features are removed. + * Listeners will receive an object with a *features* property + * referencing the features to be removed. + * featureremoved - Triggerd after a feature is removed. The event + * object passed to listeners will have a *feature* property with a + * reference to the removed feature. + * featuresremoved - Triggered after features are removed. The event + * object passed to listeners will have a *features* property with a + * reference to an array of removed features. + * beforefeatureselected - Triggered before a feature is selected. Listeners + * will receive an object with a *feature* property referencing the + * feature to be selected. To stop the feature from being selectd, a + * listener should return false. + * featureselected - Triggered after a feature is selected. Listeners + * will receive an object with a *feature* property referencing the + * selected feature. + * featureunselected - Triggered after a feature is unselected. + * Listeners will receive an object with a *feature* property + * referencing the unselected feature. + * beforefeaturemodified - Triggered when a feature is selected to + * be modified. Listeners will receive an object with a *feature* + * property referencing the selected feature. + * featuremodified - Triggered when a feature has been modified. + * Listeners will receive an object with a *feature* property referencing + * the modified feature. + * afterfeaturemodified - Triggered when a feature is finished being modified. + * Listeners will receive an object with a *feature* property referencing + * the modified feature. + * vertexmodified - Triggered when a vertex within any feature geometry + * has been modified. Listeners will receive an object with a + * *feature* property referencing the modified feature, a *vertex* + * property referencing the vertex modified (always a point geometry), + * and a *pixel* property referencing the pixel location of the + * modification. + * vertexremoved - Triggered when a vertex within any feature geometry + * has been deleted. Listeners will receive an object with a + * *feature* property referencing the modified feature, a *vertex* + * property referencing the vertex modified (always a point geometry), + * and a *pixel* property referencing the pixel location of the + * removal. + * sketchstarted - Triggered when a feature sketch bound for this layer + * is started. Listeners will receive an object with a *feature* + * property referencing the new sketch feature and a *vertex* property + * referencing the creation point. + * sketchmodified - Triggered when a feature sketch bound for this layer + * is modified. Listeners will receive an object with a *vertex* + * property referencing the modified vertex and a *feature* property + * referencing the sketch feature. + * sketchcomplete - Triggered when a feature sketch bound for this layer + * is complete. Listeners will receive an object with a *feature* + * property referencing the sketch feature. By returning false, a + * listener can stop the sketch feature from being added to the layer. + * refresh - Triggered when something wants a strategy to ask the protocol + * for a new set of features. + */ + + /** + * APIProperty: isBaseLayer + * {Boolean} The layer is a base layer. Default is false. Set this property + * in the layer options. + */ + isBaseLayer: false, + + /** + * APIProperty: isFixed + * {Boolean} Whether the layer remains in one place while dragging the + * map. Note that setting this to true will move the layer to the bottom + * of the layer stack. + */ + isFixed: false, + + /** + * APIProperty: features + * {Array(<OpenLayers.Feature.Vector>)} + */ + features: null, + + /** + * Property: filter + * {<OpenLayers.Filter>} The filter set in this layer, + * a strategy launching read requests can combined + * this filter with its own filter. + */ + filter: null, + + /** + * Property: selectedFeatures + * {Array(<OpenLayers.Feature.Vector>)} + */ + selectedFeatures: null, + + /** + * Property: unrenderedFeatures + * {Object} hash of features, keyed by feature.id, that the renderer + * failed to draw + */ + unrenderedFeatures: null, + + /** + * APIProperty: reportError + * {Boolean} report friendly error message when loading of renderer + * fails. + */ + reportError: true, + + /** + * APIProperty: style + * {Object} Default style for the layer + */ + style: null, + + /** + * Property: styleMap + * {<OpenLayers.StyleMap>} + */ + styleMap: null, + + /** + * Property: strategies + * {Array(<OpenLayers.Strategy>})} Optional list of strategies for the layer. + */ + strategies: null, + + /** + * Property: protocol + * {<OpenLayers.Protocol>} Optional protocol for the layer. + */ + protocol: null, + + /** + * Property: renderers + * {Array(String)} List of supported Renderer classes. Add to this list to + * add support for additional renderers. This list is ordered: + * the first renderer which returns true for the 'supported()' + * method will be used, if not defined in the 'renderer' option. + */ + renderers: ['SVG', 'VML', 'Canvas'], + + /** + * Property: renderer + * {<OpenLayers.Renderer>} + */ + renderer: null, + + /** + * APIProperty: rendererOptions + * {Object} Options for the renderer. See {<OpenLayers.Renderer>} for + * supported options. + */ + rendererOptions: null, + + /** + * APIProperty: geometryType + * {String} geometryType allows you to limit the types of geometries this + * layer supports. This should be set to something like + * "OpenLayers.Geometry.Point" to limit types. + */ + geometryType: null, + + /** + * Property: drawn + * {Boolean} Whether the Vector Layer features have been drawn yet. + */ + drawn: false, + + /** + * APIProperty: ratio + * {Float} This specifies the ratio of the size of the visiblity of the Vector Layer features to the size of the map. + */ + ratio: 1, + + /** + * Constructor: OpenLayers.Layer.Vector + * Create a new vector layer + * + * Parameters: + * name - {String} A name for the layer + * options - {Object} Optional object with non-default properties to set on + * the layer. + * + * Returns: + * {<OpenLayers.Layer.Vector>} A new vector layer + */ + initialize: function(name, options) { + OpenLayers.Layer.prototype.initialize.apply(this, arguments); + + // allow user-set renderer, otherwise assign one + if (!this.renderer || !this.renderer.supported()) { + this.assignRenderer(); + } + + // if no valid renderer found, display error + if (!this.renderer || !this.renderer.supported()) { + this.renderer = null; + this.displayError(); + } + + if (!this.styleMap) { + this.styleMap = new OpenLayers.StyleMap(); + } + + this.features = []; + this.selectedFeatures = []; + this.unrenderedFeatures = {}; + + // Allow for custom layer behavior + if(this.strategies){ + for(var i=0, len=this.strategies.length; i<len; i++) { + this.strategies[i].setLayer(this); + } + } + + }, + + /** + * APIMethod: destroy + * Destroy this layer + */ + destroy: function() { + if (this.strategies) { + var strategy, i, len; + for(i=0, len=this.strategies.length; i<len; i++) { + strategy = this.strategies[i]; + if(strategy.autoDestroy) { + strategy.destroy(); + } + } + this.strategies = null; + } + if (this.protocol) { + if(this.protocol.autoDestroy) { + this.protocol.destroy(); + } + this.protocol = null; + } + this.destroyFeatures(); + this.features = null; + this.selectedFeatures = null; + this.unrenderedFeatures = null; + if (this.renderer) { + this.renderer.destroy(); + } + this.renderer = null; + this.geometryType = null; + this.drawn = null; + OpenLayers.Layer.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: clone + * Create a clone of this layer. + * + * Note: Features of the layer are also cloned. + * + * Returns: + * {<OpenLayers.Layer.Vector>} An exact clone of this layer + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer.Vector(this.name, this.getOptions()); + } + + //get all additions from superclasses + obj = OpenLayers.Layer.prototype.clone.apply(this, [obj]); + + // copy/set any non-init, non-simple values here + var features = this.features; + var len = features.length; + var clonedFeatures = new Array(len); + for(var i=0; i<len; ++i) { + clonedFeatures[i] = features[i].clone(); + } + obj.features = clonedFeatures; + + return obj; + }, + + /** + * Method: refresh + * Ask the layer to request features again and redraw them. Triggers + * the refresh event if the layer is in range and visible. + * + * Parameters: + * obj - {Object} Optional object with properties for any listener of + * the refresh event. + */ + refresh: function(obj) { + if(this.calculateInRange() && this.visibility) { + this.events.triggerEvent("refresh", obj); + } + }, + + /** + * Method: assignRenderer + * Iterates through the available renderer implementations and selects + * and assigns the first one whose "supported()" function returns true. + */ + assignRenderer: function() { + for (var i=0, len=this.renderers.length; i<len; i++) { + var rendererClass = this.renderers[i]; + var renderer = (typeof rendererClass == "function") ? + rendererClass : + OpenLayers.Renderer[rendererClass]; + if (renderer && renderer.prototype.supported()) { + this.renderer = new renderer(this.div, this.rendererOptions); + break; + } + } + }, + + /** + * Method: displayError + * Let the user know their browser isn't supported. + */ + displayError: function() { + if (this.reportError) { + OpenLayers.Console.userError(OpenLayers.i18n("browserNotSupported", + {renderers: this. renderers.join('\n')})); + } + }, + + /** + * Method: setMap + * The layer has been added to the map. + * + * If there is no renderer set, the layer can't be used. Remove it. + * Otherwise, give the renderer a reference to the map and set its size. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + OpenLayers.Layer.prototype.setMap.apply(this, arguments); + + if (!this.renderer) { + this.map.removeLayer(this); + } else { + this.renderer.map = this.map; + + var newSize = this.map.getSize(); + newSize.w = newSize.w * this.ratio; + newSize.h = newSize.h * this.ratio; + this.renderer.setSize(newSize); + } + }, + + /** + * Method: afterAdd + * Called at the end of the map.addLayer sequence. At this point, the map + * will have a base layer. Any autoActivate strategies will be + * activated here. + */ + afterAdd: function() { + if(this.strategies) { + var strategy, i, len; + for(i=0, len=this.strategies.length; i<len; i++) { + strategy = this.strategies[i]; + if(strategy.autoActivate) { + strategy.activate(); + } + } + } + }, + + /** + * Method: removeMap + * The layer has been removed from the map. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + removeMap: function(map) { + this.drawn = false; + if(this.strategies) { + var strategy, i, len; + for(i=0, len=this.strategies.length; i<len; i++) { + strategy = this.strategies[i]; + if(strategy.autoActivate) { + strategy.deactivate(); + } + } + } + }, + + /** + * Method: onMapResize + * Notify the renderer of the change in size. + * + */ + onMapResize: function() { + OpenLayers.Layer.prototype.onMapResize.apply(this, arguments); + + var newSize = this.map.getSize(); + newSize.w = newSize.w * this.ratio; + newSize.h = newSize.h * this.ratio; + this.renderer.setSize(newSize); + }, + + /** + * Method: moveTo + * Reset the vector layer's div so that it once again is lined up with + * the map. Notify the renderer of the change of extent, and in the + * case of a change of zoom level (resolution), have the + * renderer redraw features. + * + * If the layer has not yet been drawn, cycle through the layer's + * features and draw each one. + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} + * zoomChanged - {Boolean} + * dragging - {Boolean} + */ + moveTo: function(bounds, zoomChanged, dragging) { + OpenLayers.Layer.prototype.moveTo.apply(this, arguments); + + var coordSysUnchanged = true; + if (!dragging) { + this.renderer.root.style.visibility = 'hidden'; + + var viewSize = this.map.getSize(), + viewWidth = viewSize.w, + viewHeight = viewSize.h, + offsetLeft = (viewWidth / 2 * this.ratio) - viewWidth / 2, + offsetTop = (viewHeight / 2 * this.ratio) - viewHeight / 2; + offsetLeft += this.map.layerContainerOriginPx.x; + offsetLeft = -Math.round(offsetLeft); + offsetTop += this.map.layerContainerOriginPx.y; + offsetTop = -Math.round(offsetTop); + + this.div.style.left = offsetLeft + 'px'; + this.div.style.top = offsetTop + 'px'; + + var extent = this.map.getExtent().scale(this.ratio); + coordSysUnchanged = this.renderer.setExtent(extent, zoomChanged); + + this.renderer.root.style.visibility = 'visible'; + + // Force a reflow on gecko based browsers to prevent jump/flicker. + // This seems to happen on only certain configurations; it was originally + // noticed in FF 2.0 and Linux. + if (OpenLayers.IS_GECKO === true) { + this.div.scrollLeft = this.div.scrollLeft; + } + + if (!zoomChanged && coordSysUnchanged) { + for (var i in this.unrenderedFeatures) { + var feature = this.unrenderedFeatures[i]; + this.drawFeature(feature); + } + } + } + if (!this.drawn || zoomChanged || !coordSysUnchanged) { + this.drawn = true; + var feature; + for(var i=0, len=this.features.length; i<len; i++) { + this.renderer.locked = (i !== (len - 1)); + feature = this.features[i]; + this.drawFeature(feature); + } + } + }, + + /** + * APIMethod: display + * Hide or show the Layer + * + * Parameters: + * display - {Boolean} + */ + display: function(display) { + OpenLayers.Layer.prototype.display.apply(this, arguments); + // we need to set the display style of the root in case it is attached + // to a foreign layer + var currentDisplay = this.div.style.display; + if(currentDisplay != this.renderer.root.style.display) { + this.renderer.root.style.display = currentDisplay; + } + }, + + /** + * APIMethod: addFeatures + * Add Features to the layer. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} + * options - {Object} + */ + addFeatures: function(features, options) { + if (!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + + var notify = !options || !options.silent; + if(notify) { + var event = {features: features}; + var ret = this.events.triggerEvent("beforefeaturesadded", event); + if(ret === false) { + return; + } + features = event.features; + } + + // Track successfully added features for featuresadded event, since + // beforefeatureadded can veto single features. + var featuresAdded = []; + for (var i=0, len=features.length; i<len; i++) { + if (i != (features.length - 1)) { + this.renderer.locked = true; + } else { + this.renderer.locked = false; + } + var feature = features[i]; + + if (this.geometryType && + !(feature.geometry instanceof this.geometryType)) { + throw new TypeError('addFeatures: component should be an ' + + this.geometryType.prototype.CLASS_NAME); + } + + //give feature reference to its layer + feature.layer = this; + + if (!feature.style && this.style) { + feature.style = OpenLayers.Util.extend({}, this.style); + } + + if (notify) { + if(this.events.triggerEvent("beforefeatureadded", + {feature: feature}) === false) { + continue; + } + this.preFeatureInsert(feature); + } + + featuresAdded.push(feature); + this.features.push(feature); + this.drawFeature(feature); + + if (notify) { + this.events.triggerEvent("featureadded", { + feature: feature + }); + this.onFeatureInsert(feature); + } + } + + if(notify) { + this.events.triggerEvent("featuresadded", {features: featuresAdded}); + } + }, + + + /** + * APIMethod: removeFeatures + * Remove features from the layer. This erases any drawn features and + * removes them from the layer's control. The beforefeatureremoved + * and featureremoved events will be triggered for each feature. The + * featuresremoved event will be triggered after all features have + * been removed. To supress event triggering, use the silent option. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} List of features to be + * removed. + * options - {Object} Optional properties for changing behavior of the + * removal. + * + * Valid options: + * silent - {Boolean} Supress event triggering. Default is false. + */ + removeFeatures: function(features, options) { + if(!features || features.length === 0) { + return; + } + if (features === this.features) { + return this.removeAllFeatures(options); + } + if (!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + if (features === this.selectedFeatures) { + features = features.slice(); + } + + var notify = !options || !options.silent; + + if (notify) { + this.events.triggerEvent( + "beforefeaturesremoved", {features: features} + ); + } + + for (var i = features.length - 1; i >= 0; i--) { + // We remain locked so long as we're not at 0 + // and the 'next' feature has a geometry. We do the geometry check + // because if all the features after the current one are 'null', we + // won't call eraseGeometry, so we break the 'renderer functions + // will always be called with locked=false *last*' rule. The end result + // is a possible gratiutious unlocking to save a loop through the rest + // of the list checking the remaining features every time. So long as + // null geoms are rare, this is probably okay. + if (i != 0 && features[i-1].geometry) { + this.renderer.locked = true; + } else { + this.renderer.locked = false; + } + + var feature = features[i]; + delete this.unrenderedFeatures[feature.id]; + + if (notify) { + this.events.triggerEvent("beforefeatureremoved", { + feature: feature + }); + } + + this.features = OpenLayers.Util.removeItem(this.features, feature); + // feature has no layer at this point + feature.layer = null; + + if (feature.geometry) { + this.renderer.eraseFeatures(feature); + } + + //in the case that this feature is one of the selected features, + // remove it from that array as well. + if (OpenLayers.Util.indexOf(this.selectedFeatures, feature) != -1){ + OpenLayers.Util.removeItem(this.selectedFeatures, feature); + } + + if (notify) { + this.events.triggerEvent("featureremoved", { + feature: feature + }); + } + } + + if (notify) { + this.events.triggerEvent("featuresremoved", {features: features}); + } + }, + + /** + * APIMethod: removeAllFeatures + * Remove all features from the layer. + * + * Parameters: + * options - {Object} Optional properties for changing behavior of the + * removal. + * + * Valid options: + * silent - {Boolean} Supress event triggering. Default is false. + */ + removeAllFeatures: function(options) { + var notify = !options || !options.silent; + var features = this.features; + if (notify) { + this.events.triggerEvent( + "beforefeaturesremoved", {features: features} + ); + } + var feature; + for (var i = features.length-1; i >= 0; i--) { + feature = features[i]; + if (notify) { + this.events.triggerEvent("beforefeatureremoved", { + feature: feature + }); + } + feature.layer = null; + if (notify) { + this.events.triggerEvent("featureremoved", { + feature: feature + }); + } + } + this.renderer.clear(); + this.features = []; + this.unrenderedFeatures = {}; + this.selectedFeatures = []; + if (notify) { + this.events.triggerEvent("featuresremoved", {features: features}); + } + }, + + /** + * APIMethod: destroyFeatures + * Erase and destroy features on the layer. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} An optional array of + * features to destroy. If not supplied, all features on the layer + * will be destroyed. + * options - {Object} + */ + destroyFeatures: function(features, options) { + var all = (features == undefined); // evaluates to true if + // features is null + if(all) { + features = this.features; + } + if(features) { + this.removeFeatures(features, options); + for(var i=features.length-1; i>=0; i--) { + features[i].destroy(); + } + } + }, + + /** + * APIMethod: drawFeature + * Draw (or redraw) a feature on the layer. If the optional style argument + * is included, this style will be used. If no style is included, the + * feature's style will be used. If the feature doesn't have a style, + * the layer's style will be used. + * + * This function is not designed to be used when adding features to + * the layer (use addFeatures instead). It is meant to be used when + * the style of a feature has changed, or in some other way needs to + * visually updated *after* it has already been added to a layer. You + * must add the feature to the layer for most layer-related events to + * happen. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * style - {String | Object} Named render intent or full symbolizer object. + */ + drawFeature: function(feature, style) { + // don't try to draw the feature with the renderer if the layer is not + // drawn itself + if (!this.drawn) { + return; + } + if (typeof style != "object") { + if(!style && feature.state === OpenLayers.State.DELETE) { + style = "delete"; + } + var renderIntent = style || feature.renderIntent; + style = feature.style || this.style; + if (!style) { + style = this.styleMap.createSymbolizer(feature, renderIntent); + } + } + + var drawn = this.renderer.drawFeature(feature, style); + //TODO remove the check for null when we get rid of Renderer.SVG + if (drawn === false || drawn === null) { + this.unrenderedFeatures[feature.id] = feature; + } else { + delete this.unrenderedFeatures[feature.id]; + } + }, + + /** + * Method: eraseFeatures + * Erase features from the layer. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} + */ + eraseFeatures: function(features) { + this.renderer.eraseFeatures(features); + }, + + /** + * Method: getFeatureFromEvent + * Given an event, return a feature if the event occurred over one. + * Otherwise, return null. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {<OpenLayers.Feature.Vector>} A feature if one was under the event. + */ + getFeatureFromEvent: function(evt) { + if (!this.renderer) { + throw new Error('getFeatureFromEvent called on layer with no ' + + 'renderer. This usually means you destroyed a ' + + 'layer, but not some handler which is associated ' + + 'with it.'); + } + var feature = null; + var featureId = this.renderer.getFeatureIdFromEvent(evt); + if (featureId) { + if (typeof featureId === "string") { + feature = this.getFeatureById(featureId); + } else { + feature = featureId; + } + } + return feature; + }, + + /** + * APIMethod: getFeatureBy + * Given a property value, return the feature if it exists in the features array + * + * Parameters: + * property - {String} + * value - {String} + * + * Returns: + * {<OpenLayers.Feature.Vector>} A feature corresponding to the given + * property value or null if there is no such feature. + */ + getFeatureBy: function(property, value) { + //TBD - would it be more efficient to use a hash for this.features? + var feature = null; + for(var i=0, len=this.features.length; i<len; ++i) { + if(this.features[i][property] == value) { + feature = this.features[i]; + break; + } + } + return feature; + }, + + /** + * APIMethod: getFeatureById + * Given a feature id, return the feature if it exists in the features array + * + * Parameters: + * featureId - {String} + * + * Returns: + * {<OpenLayers.Feature.Vector>} A feature corresponding to the given + * featureId or null if there is no such feature. + */ + getFeatureById: function(featureId) { + return this.getFeatureBy('id', featureId); + }, + + /** + * APIMethod: getFeatureByFid + * Given a feature fid, return the feature if it exists in the features array + * + * Parameters: + * featureFid - {String} + * + * Returns: + * {<OpenLayers.Feature.Vector>} A feature corresponding to the given + * featureFid or null if there is no such feature. + */ + getFeatureByFid: function(featureFid) { + return this.getFeatureBy('fid', featureFid); + }, + + /** + * APIMethod: getFeaturesByAttribute + * Returns an array of features that have the given attribute key set to the + * given value. Comparison of attribute values takes care of datatypes, e.g. + * the string '1234' is not equal to the number 1234. + * + * Parameters: + * attrName - {String} + * attrValue - {Mixed} + * + * Returns: + * Array({<OpenLayers.Feature.Vector>}) An array of features that have the + * passed named attribute set to the given value. + */ + getFeaturesByAttribute: function(attrName, attrValue) { + var i, + feature, + len = this.features.length, + foundFeatures = []; + for(i = 0; i < len; i++) { + feature = this.features[i]; + if(feature && feature.attributes) { + if (feature.attributes[attrName] === attrValue) { + foundFeatures.push(feature); + } + } + } + return foundFeatures; + }, + + /** + * Unselect the selected features + * i.e. clears the featureSelection array + * change the style back + clearSelection: function() { + + var vectorLayer = this.map.vectorLayer; + for (var i = 0; i < this.map.featureSelection.length; i++) { + var featureSelection = this.map.featureSelection[i]; + vectorLayer.drawFeature(featureSelection, vectorLayer.style); + } + this.map.featureSelection = []; + }, + */ + + + /** + * APIMethod: onFeatureInsert + * method called after a feature is inserted. + * Does nothing by default. Override this if you + * need to do something on feature updates. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + onFeatureInsert: function(feature) { + }, + + /** + * APIMethod: preFeatureInsert + * method called before a feature is inserted. + * Does nothing by default. Override this if you + * need to do something when features are first added to the + * layer, but before they are drawn, such as adjust the style. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + preFeatureInsert: function(feature) { + }, + + /** + * APIMethod: getDataExtent + * Calculates the max extent which includes all of the features. + * + * Returns: + * {<OpenLayers.Bounds>} or null if the layer has no features with + * geometries. + */ + getDataExtent: function () { + var maxExtent = null; + var features = this.features; + if(features && (features.length > 0)) { + var geometry = null; + for(var i=0, len=features.length; i<len; i++) { + geometry = features[i].geometry; + if (geometry) { + if (maxExtent === null) { + maxExtent = new OpenLayers.Bounds(); + } + maxExtent.extend(geometry.getBounds()); + } + } + } + return maxExtent; + }, + + CLASS_NAME: "OpenLayers.Layer.Vector" +}); +/* ====================================================================== + OpenLayers/Layer/Vector/RootContainer.js + ====================================================================== */ + +/* 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/Layer/Vector.js + */ + +/** + * Class: OpenLayers.Layer.Vector.RootContainer + * A special layer type to combine multiple vector layers inside a single + * renderer root container. This class is not supposed to be instantiated + * from user space, it is a helper class for controls that require event + * processing for multiple vector layers. + * + * Inherits from: + * - <OpenLayers.Layer.Vector> + */ +OpenLayers.Layer.Vector.RootContainer = OpenLayers.Class(OpenLayers.Layer.Vector, { + + /** + * Property: displayInLayerSwitcher + * Set to false for this layer type + */ + displayInLayerSwitcher: false, + + /** + * APIProperty: layers + * Layers that are attached to this container. Required config option. + */ + layers: null, + + /** + * Constructor: OpenLayers.Layer.Vector.RootContainer + * Create a new root container for multiple vector layer. This constructor + * is not supposed to be used from user space, it is only to be used by + * controls that need feature selection across multiple vector layers. + * + * Parameters: + * name - {String} A name for the layer + * options - {Object} Optional object with non-default properties to set on + * the layer. + * + * Required options properties: + * layers - {Array(<OpenLayers.Layer.Vector>)} The layers managed by this + * container + * + * Returns: + * {<OpenLayers.Layer.Vector.RootContainer>} A new vector layer root + * container + */ + + /** + * Method: display + */ + display: function() {}, + + /** + * Method: getFeatureFromEvent + * walk through the layers to find the feature returned by the event + * + * Parameters: + * evt - {Object} event object with a feature property + * + * Returns: + * {<OpenLayers.Feature.Vector>} + */ + getFeatureFromEvent: function(evt) { + var layers = this.layers; + var feature; + for(var i=0; i<layers.length; i++) { + feature = layers[i].getFeatureFromEvent(evt); + if(feature) { + return feature; + } + } + }, + + /** + * Method: setMap + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + OpenLayers.Layer.Vector.prototype.setMap.apply(this, arguments); + this.collectRoots(); + map.events.register("changelayer", this, this.handleChangeLayer); + }, + + /** + * Method: removeMap + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + removeMap: function(map) { + map.events.unregister("changelayer", this, this.handleChangeLayer); + this.resetRoots(); + OpenLayers.Layer.Vector.prototype.removeMap.apply(this, arguments); + }, + + /** + * Method: collectRoots + * Collects the root nodes of all layers this control is configured with + * and moveswien the nodes to this control's layer + */ + collectRoots: function() { + var layer; + // walk through all map layers, because we want to keep the order + for(var i=0; i<this.map.layers.length; ++i) { + layer = this.map.layers[i]; + if(OpenLayers.Util.indexOf(this.layers, layer) != -1) { + layer.renderer.moveRoot(this.renderer); + } + } + }, + + /** + * Method: resetRoots + * Resets the root nodes back into the layers they belong to. + */ + resetRoots: function() { + var layer; + for(var i=0; i<this.layers.length; ++i) { + layer = this.layers[i]; + if(this.renderer && layer.renderer.getRenderLayerId() == this.id) { + this.renderer.moveRoot(layer.renderer); + } + } + }, + + /** + * Method: handleChangeLayer + * Event handler for the map's changelayer event. We need to rebuild + * this container's layer dom if order of one of its layers changes. + * This handler is added with the setMap method, and removed with the + * removeMap method. + * + * Parameters: + * evt - {Object} + */ + handleChangeLayer: function(evt) { + var layer = evt.layer; + if(evt.property == "order" && + OpenLayers.Util.indexOf(this.layers, layer) != -1) { + this.resetRoots(); + this.collectRoots(); + } + }, + + CLASS_NAME: "OpenLayers.Layer.Vector.RootContainer" +}); +/* ====================================================================== + OpenLayers/Control/SelectFeature.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Handler/Feature.js + * @requires OpenLayers/Layer/Vector/RootContainer.js + */ + +/** + * Class: OpenLayers.Control.SelectFeature + * The SelectFeature control selects vector features from a given layer on + * click or hover. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.SelectFeature = OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: events + * {<OpenLayers.Events>} Events instance for listeners and triggering + * control specific events. + * + * Register a listener for a particular event with the following syntax: + * (code) + * control.events.register(type, obj, listener); + * (end) + * + * Supported event types (in addition to those from <OpenLayers.Control.events>): + * beforefeaturehighlighted - Triggered before a feature is highlighted + * featurehighlighted - Triggered when a feature is highlighted + * featureunhighlighted - Triggered when a feature is unhighlighted + * boxselectionstart - Triggered before box selection starts + * boxselectionend - Triggered after box selection ends + */ + + /** + * Property: multipleKey + * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets + * the <multiple> property to true. Default is null. + */ + multipleKey: null, + + /** + * Property: toggleKey + * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets + * the <toggle> property to true. Default is null. + */ + toggleKey: null, + + /** + * APIProperty: multiple + * {Boolean} Allow selection of multiple geometries. Default is false. + */ + multiple: false, + + /** + * APIProperty: clickout + * {Boolean} Unselect features when clicking outside any feature. + * Default is true. + */ + clickout: true, + + /** + * APIProperty: toggle + * {Boolean} Unselect a selected feature on click. Default is false. Only + * has meaning if hover is false. + */ + toggle: false, + + /** + * APIProperty: hover + * {Boolean} Select on mouse over and deselect on mouse out. If true, this + * ignores clicks and only listens to mouse moves. + */ + hover: false, + + /** + * APIProperty: highlightOnly + * {Boolean} If true do not actually select features (that is place them in + * the layer's selected features array), just highlight them. This property + * has no effect if hover is false. Defaults to false. + */ + highlightOnly: false, + + /** + * APIProperty: box + * {Boolean} Allow feature selection by drawing a box. + */ + box: false, + + /** + * Property: onBeforeSelect + * {Function} Optional function to be called before a feature is selected. + * The function should expect to be called with a feature. + */ + onBeforeSelect: function() {}, + + /** + * APIProperty: onSelect + * {Function} Optional function to be called when a feature is selected. + * The function should expect to be called with a feature. + */ + onSelect: function() {}, + + /** + * APIProperty: onUnselect + * {Function} Optional function to be called when a feature is unselected. + * The function should expect to be called with a feature. + */ + onUnselect: function() {}, + + /** + * Property: scope + * {Object} The scope to use with the onBeforeSelect, onSelect, onUnselect + * callbacks. If null the scope will be this control. + */ + scope: null, + + /** + * APIProperty: geometryTypes + * {Array(String)} To restrict selecting to a limited set of geometry types, + * send a list of strings corresponding to the geometry class names. + */ + geometryTypes: null, + + /** + * Property: layer + * {<OpenLayers.Layer.Vector>} The vector layer with a common renderer + * root for all layers this control is configured with (if an array of + * layers was passed to the constructor), or the vector layer the control + * was configured with (if a single layer was passed to the constructor). + */ + layer: null, + + /** + * Property: layers + * {Array(<OpenLayers.Layer.Vector>)} The layers this control will work on, + * or null if the control was configured with a single layer + */ + layers: null, + + /** + * APIProperty: callbacks + * {Object} The functions that are sent to the handlers.feature for callback + */ + callbacks: null, + + /** + * APIProperty: selectStyle + * {Object} Hash of styles + */ + selectStyle: null, + + /** + * Property: renderIntent + * {String} key used to retrieve the select style from the layer's + * style map. + */ + renderIntent: "select", + + /** + * Property: handlers + * {Object} Object with references to multiple <OpenLayers.Handler> + * instances. + */ + handlers: null, + + /** + * Constructor: OpenLayers.Control.SelectFeature + * Create a new control for selecting features. + * + * Parameters: + * layers - {<OpenLayers.Layer.Vector>}, or an array of vector layers. The + * layer(s) this control will select features from. + * options - {Object} + */ + initialize: function(layers, options) { + OpenLayers.Control.prototype.initialize.apply(this, [options]); + + if(this.scope === null) { + this.scope = this; + } + this.initLayer(layers); + var callbacks = { + click: this.clickFeature, + clickout: this.clickoutFeature + }; + if (this.hover) { + callbacks.over = this.overFeature; + callbacks.out = this.outFeature; + } + + this.callbacks = OpenLayers.Util.extend(callbacks, this.callbacks); + this.handlers = { + feature: new OpenLayers.Handler.Feature( + this, this.layer, this.callbacks, + {geometryTypes: this.geometryTypes} + ) + }; + + if (this.box) { + this.handlers.box = new OpenLayers.Handler.Box( + this, {done: this.selectBox}, + {boxDivClassName: "olHandlerBoxSelectFeature"} + ); + } + }, + + /** + * Method: initLayer + * Assign the layer property. If layers is an array, we need to use + * a RootContainer. + * + * Parameters: + * layers - {<OpenLayers.Layer.Vector>}, or an array of vector layers. + */ + initLayer: function(layers) { + if(OpenLayers.Util.isArray(layers)) { + this.layers = layers; + this.layer = new OpenLayers.Layer.Vector.RootContainer( + this.id + "_container", { + layers: layers + } + ); + } else { + this.layer = layers; + } + }, + + /** + * Method: destroy + */ + destroy: function() { + if(this.active && this.layers) { + this.map.removeLayer(this.layer); + } + OpenLayers.Control.prototype.destroy.apply(this, arguments); + if(this.layers) { + this.layer.destroy(); + } + }, + + /** + * Method: activate + * Activates the control. + * + * Returns: + * {Boolean} The control was effectively activated. + */ + activate: function () { + if (!this.active) { + if(this.layers) { + this.map.addLayer(this.layer); + } + this.handlers.feature.activate(); + if(this.box && this.handlers.box) { + this.handlers.box.activate(); + } + } + return OpenLayers.Control.prototype.activate.apply( + this, arguments + ); + }, + + /** + * Method: deactivate + * Deactivates the control. + * + * Returns: + * {Boolean} The control was effectively deactivated. + */ + deactivate: function () { + if (this.active) { + this.handlers.feature.deactivate(); + if(this.handlers.box) { + this.handlers.box.deactivate(); + } + if(this.layers) { + this.map.removeLayer(this.layer); + } + } + return OpenLayers.Control.prototype.deactivate.apply( + this, arguments + ); + }, + + /** + * Method: unselectAll + * Unselect all selected features. To unselect all except for a single + * feature, set the options.except property to the feature. + * + * Parameters: + * options - {Object} Optional configuration object. + */ + unselectAll: function(options) { + // we'll want an option to supress notification here + var layers = this.layers || [this.layer], + layer, feature, l, numExcept; + for(l=0; l<layers.length; ++l) { + layer = layers[l]; + numExcept = 0; + //layer.selectedFeatures is null when layer is destroyed and + //one of it's preremovelayer listener calls setLayer + //with another layer on this control + if(layer.selectedFeatures != null) { + while(layer.selectedFeatures.length > numExcept) { + feature = layer.selectedFeatures[numExcept]; + if(!options || options.except != feature) { + this.unselect(feature); + } else { + ++numExcept; + } + } + } + } + }, + + /** + * Method: clickFeature + * Called on click in a feature + * Only responds if this.hover is false. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + clickFeature: function(feature) { + if(!this.hover) { + var selected = (OpenLayers.Util.indexOf( + feature.layer.selectedFeatures, feature) > -1); + if(selected) { + if(this.toggleSelect()) { + this.unselect(feature); + } else if(!this.multipleSelect()) { + this.unselectAll({except: feature}); + } + } else { + if(!this.multipleSelect()) { + this.unselectAll({except: feature}); + } + this.select(feature); + } + } + }, + + /** + * Method: multipleSelect + * Allow for multiple selected features based on <multiple> property and + * <multipleKey> event modifier. + * + * Returns: + * {Boolean} Allow for multiple selected features. + */ + multipleSelect: function() { + return this.multiple || (this.handlers.feature.evt && + this.handlers.feature.evt[this.multipleKey]); + }, + + /** + * Method: toggleSelect + * Event should toggle the selected state of a feature based on <toggle> + * property and <toggleKey> event modifier. + * + * Returns: + * {Boolean} Toggle the selected state of a feature. + */ + toggleSelect: function() { + return this.toggle || (this.handlers.feature.evt && + this.handlers.feature.evt[this.toggleKey]); + }, + + /** + * Method: clickoutFeature + * Called on click outside a previously clicked (selected) feature. + * Only responds if this.hover is false. + * + * Parameters: + * feature - {<OpenLayers.Vector.Feature>} + */ + clickoutFeature: function(feature) { + if(!this.hover && this.clickout) { + this.unselectAll(); + } + }, + + /** + * Method: overFeature + * Called on over a feature. + * Only responds if this.hover is true. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + overFeature: function(feature) { + var layer = feature.layer; + if(this.hover) { + if(this.highlightOnly) { + this.highlight(feature); + } else if(OpenLayers.Util.indexOf( + layer.selectedFeatures, feature) == -1) { + this.select(feature); + } + } + }, + + /** + * Method: outFeature + * Called on out of a selected feature. + * Only responds if this.hover is true. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + outFeature: function(feature) { + if(this.hover) { + if(this.highlightOnly) { + // we do nothing if we're not the last highlighter of the + // feature + if(feature._lastHighlighter == this.id) { + // if another select control had highlighted the feature before + // we did it ourself then we use that control to highlight the + // feature as it was before we highlighted it, else we just + // unhighlight it + if(feature._prevHighlighter && + feature._prevHighlighter != this.id) { + delete feature._lastHighlighter; + var control = this.map.getControl( + feature._prevHighlighter); + if(control) { + control.highlight(feature); + } + } else { + this.unhighlight(feature); + } + } + } else { + this.unselect(feature); + } + } + }, + + /** + * Method: highlight + * Redraw feature with the select style. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + highlight: function(feature) { + var layer = feature.layer; + var cont = this.events.triggerEvent("beforefeaturehighlighted", { + feature : feature + }); + if(cont !== false) { + feature._prevHighlighter = feature._lastHighlighter; + feature._lastHighlighter = this.id; + var style = this.selectStyle || this.renderIntent; + layer.drawFeature(feature, style); + this.events.triggerEvent("featurehighlighted", {feature : feature}); + } + }, + + /** + * Method: unhighlight + * Redraw feature with the "default" style + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + unhighlight: function(feature) { + var layer = feature.layer; + // three cases: + // 1. there's no other highlighter, in that case _prev is undefined, + // and we just need to undef _last + // 2. another control highlighted the feature after we did it, in + // that case _last references this other control, and we just + // need to undef _prev + // 3. another control highlighted the feature before we did it, in + // that case _prev references this other control, and we need to + // set _last to _prev and undef _prev + if(feature._prevHighlighter == undefined) { + delete feature._lastHighlighter; + } else if(feature._prevHighlighter == this.id) { + delete feature._prevHighlighter; + } else { + feature._lastHighlighter = feature._prevHighlighter; + delete feature._prevHighlighter; + } + layer.drawFeature(feature, feature.style || feature.layer.style || + "default"); + this.events.triggerEvent("featureunhighlighted", {feature : feature}); + }, + + /** + * Method: select + * Add feature to the layer's selectedFeature array, render the feature as + * selected, and call the onSelect function. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + select: function(feature) { + var cont = this.onBeforeSelect.call(this.scope, feature); + var layer = feature.layer; + if(cont !== false) { + cont = layer.events.triggerEvent("beforefeatureselected", { + feature: feature + }); + if(cont !== false) { + layer.selectedFeatures.push(feature); + this.highlight(feature); + // if the feature handler isn't involved in the feature + // selection (because the box handler is used or the + // feature is selected programatically) we fake the + // feature handler to allow unselecting on click + if(!this.handlers.feature.lastFeature) { + this.handlers.feature.lastFeature = layer.selectedFeatures[0]; + } + layer.events.triggerEvent("featureselected", {feature: feature}); + this.onSelect.call(this.scope, feature); + } + } + }, + + /** + * Method: unselect + * Remove feature from the layer's selectedFeature array, render the feature as + * normal, and call the onUnselect function. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + */ + unselect: function(feature) { + var layer = feature.layer; + // Store feature style for restoration later + this.unhighlight(feature); + OpenLayers.Util.removeItem(layer.selectedFeatures, feature); + layer.events.triggerEvent("featureunselected", {feature: feature}); + this.onUnselect.call(this.scope, feature); + }, + + /** + * Method: selectBox + * Callback from the handlers.box set up when <box> selection is true + * on. + * + * Parameters: + * position - {<OpenLayers.Bounds> || <OpenLayers.Pixel> } + */ + selectBox: function(position) { + if (position instanceof OpenLayers.Bounds) { + var minXY = this.map.getLonLatFromPixel({ + x: position.left, + y: position.bottom + }); + var maxXY = this.map.getLonLatFromPixel({ + x: position.right, + y: position.top + }); + var bounds = new OpenLayers.Bounds( + minXY.lon, minXY.lat, maxXY.lon, maxXY.lat + ); + + // if multiple is false, first deselect currently selected features + if (!this.multipleSelect()) { + this.unselectAll(); + } + + // because we're using a box, we consider we want multiple selection + var prevMultiple = this.multiple; + this.multiple = true; + var layers = this.layers || [this.layer]; + this.events.triggerEvent("boxselectionstart", {layers: layers}); + var layer; + for(var l=0; l<layers.length; ++l) { + layer = layers[l]; + for(var i=0, len = layer.features.length; i<len; ++i) { + var feature = layer.features[i]; + // check if the feature is displayed + if (!feature.getVisibility()) { + continue; + } + + if (this.geometryTypes == null || OpenLayers.Util.indexOf( + this.geometryTypes, feature.geometry.CLASS_NAME) > -1) { + if (bounds.toGeometry().intersects(feature.geometry)) { + if (OpenLayers.Util.indexOf(layer.selectedFeatures, feature) == -1) { + this.select(feature); + } + } + } + } + } + this.multiple = prevMultiple; + this.events.triggerEvent("boxselectionend", {layers: layers}); + } + }, + + /** + * Method: setMap + * Set the map property for the control. + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + setMap: function(map) { + this.handlers.feature.setMap(map); + if (this.box) { + this.handlers.box.setMap(map); + } + OpenLayers.Control.prototype.setMap.apply(this, arguments); + }, + + /** + * APIMethod: setLayer + * Attach a new layer to the control, overriding any existing layers. + * + * Parameters: + * layers - Array of {<OpenLayers.Layer.Vector>} or a single + * {<OpenLayers.Layer.Vector>} + */ + setLayer: function(layers) { + var isActive = this.active; + this.unselectAll(); + this.deactivate(); + if(this.layers) { + this.layer.destroy(); + this.layers = null; + } + this.initLayer(layers); + this.handlers.feature.layer = this.layer; + if (isActive) { + this.activate(); + } + }, + + CLASS_NAME: "OpenLayers.Control.SelectFeature" +}); +/* ====================================================================== + OpenLayers/Handler/Point.js + ====================================================================== */ + +/* 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/Handler.js + * @requires OpenLayers/Geometry/Point.js + */ + +/** + * Class: OpenLayers.Handler.Point + * Handler to draw a point on the map. Point is displayed on activation, + * moves on mouse move, and is finished on mouse up. The handler triggers + * callbacks for 'done', 'cancel', and 'modify'. The modify callback is + * called with each change in the sketch and will receive the latest point + * drawn. Create a new instance with the <OpenLayers.Handler.Point> + * constructor. + * + * Inherits from: + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Point = OpenLayers.Class(OpenLayers.Handler, { + + /** + * Property: point + * {<OpenLayers.Feature.Vector>} The currently drawn point + */ + point: null, + + /** + * Property: layer + * {<OpenLayers.Layer.Vector>} The temporary drawing layer + */ + layer: null, + + /** + * APIProperty: multi + * {Boolean} Cast features to multi-part geometries before passing to the + * layer. Default is false. + */ + multi: false, + + /** + * APIProperty: citeCompliant + * {Boolean} If set to true, coordinates of features drawn in a map extent + * crossing the date line won't exceed the world bounds. Default is false. + */ + citeCompliant: false, + + /** + * Property: mouseDown + * {Boolean} The mouse is down + */ + mouseDown: false, + + /** + * Property: stoppedDown + * {Boolean} Indicate whether the last mousedown stopped the event + * propagation. + */ + stoppedDown: null, + + /** + * Property: lastDown + * {<OpenLayers.Pixel>} Location of the last mouse down + */ + lastDown: null, + + /** + * Property: lastUp + * {<OpenLayers.Pixel>} + */ + lastUp: null, + + /** + * APIProperty: persist + * {Boolean} Leave the feature rendered until destroyFeature is called. + * Default is false. If set to true, the feature remains rendered until + * destroyFeature is called, typically by deactivating the handler or + * starting another drawing. + */ + persist: false, + + /** + * APIProperty: stopDown + * {Boolean} Stop event propagation on mousedown. Must be false to + * allow "pan while drawing". Defaults to false. + */ + stopDown: false, + + /** + * APIPropery: stopUp + * {Boolean} Stop event propagation on mouse. Must be false to + * allow "pan while dragging". Defaults to fase. + */ + stopUp: false, + + /** + * Property: layerOptions + * {Object} Any optional properties to be set on the sketch layer. + */ + layerOptions: null, + + /** + * APIProperty: pixelTolerance + * {Number} Maximum number of pixels between down and up (mousedown + * and mouseup, or touchstart and touchend) for the handler to + * add a new point. If set to an integer value, if the + * displacement between down and up is great to this value + * no point will be added. Default value is 5. + */ + pixelTolerance: 5, + + /** + * Property: lastTouchPx + * {<OpenLayers.Pixel>} The last pixel used to know the distance between + * two touches (for double touch). + */ + lastTouchPx: null, + + /** + * Constructor: OpenLayers.Handler.Point + * Create a new point handler. + * + * Parameters: + * control - {<OpenLayers.Control>} The control that owns this handler + * callbacks - {Object} An object with a properties whose values are + * functions. Various callbacks described below. + * options - {Object} An optional object with properties to be set on the + * handler + * + * Named callbacks: + * create - Called when a sketch is first created. Callback called with + * the creation point geometry and sketch feature. + * modify - Called with each move of a vertex with the vertex (point) + * geometry and the sketch feature. + * done - Called when the point drawing is finished. The callback will + * recieve a single argument, the point geometry. + * cancel - Called when the handler is deactivated while drawing. The + * cancel callback will receive a geometry. + */ + initialize: function(control, callbacks, options) { + if(!(options && options.layerOptions && options.layerOptions.styleMap)) { + this.style = OpenLayers.Util.extend(OpenLayers.Feature.Vector.style['default'], {}); + } + + OpenLayers.Handler.prototype.initialize.apply(this, arguments); + }, + + /** + * APIMethod: activate + * turn on the handler + */ + activate: function() { + if(!OpenLayers.Handler.prototype.activate.apply(this, arguments)) { + return false; + } + // create temporary vector layer for rendering geometry sketch + // TBD: this could be moved to initialize/destroy - setting visibility here + var options = OpenLayers.Util.extend({ + displayInLayerSwitcher: false, + // indicate that the temp vector layer will never be out of range + // without this, resolution properties must be specified at the + // map-level for this temporary layer to init its resolutions + // correctly + calculateInRange: OpenLayers.Function.True, + wrapDateLine: this.citeCompliant + }, this.layerOptions); + this.layer = new OpenLayers.Layer.Vector(this.CLASS_NAME, options); + this.map.addLayer(this.layer); + return true; + }, + + /** + * Method: createFeature + * Add temporary features + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} A pixel location on the map. + */ + createFeature: function(pixel) { + var lonlat = this.layer.getLonLatFromViewPortPx(pixel); + var geometry = new OpenLayers.Geometry.Point( + lonlat.lon, lonlat.lat + ); + this.point = new OpenLayers.Feature.Vector(geometry); + this.callback("create", [this.point.geometry, this.point]); + this.point.geometry.clearBounds(); + this.layer.addFeatures([this.point], {silent: true}); + }, + + /** + * APIMethod: deactivate + * turn off the handler + */ + deactivate: function() { + if(!OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { + return false; + } + this.cancel(); + // If a layer's map property is set to null, it means that that layer + // isn't added to the map. Since we ourself added the layer to the map + // in activate(), we can assume that if this.layer.map is null it means + // that the layer has been destroyed (as a result of map.destroy() for + // example. + if (this.layer.map != null) { + this.destroyFeature(true); + this.layer.destroy(false); + } + this.layer = null; + return true; + }, + + /** + * Method: destroyFeature + * Destroy the temporary geometries + * + * Parameters: + * force - {Boolean} Destroy even if persist is true. + */ + destroyFeature: function(force) { + if(this.layer && (force || !this.persist)) { + this.layer.destroyFeatures(); + } + this.point = null; + }, + + /** + * Method: destroyPersistedFeature + * Destroy the persisted feature. + */ + destroyPersistedFeature: function() { + var layer = this.layer; + if(layer && layer.features.length > 1) { + this.layer.features[0].destroy(); + } + }, + + /** + * Method: finalize + * Finish the geometry and call the "done" callback. + * + * Parameters: + * cancel - {Boolean} Call cancel instead of done callback. Default + * is false. + */ + finalize: function(cancel) { + var key = cancel ? "cancel" : "done"; + this.mouseDown = false; + this.lastDown = null; + this.lastUp = null; + this.lastTouchPx = null; + this.callback(key, [this.geometryClone()]); + this.destroyFeature(cancel); + }, + + /** + * APIMethod: cancel + * Finish the geometry and call the "cancel" callback. + */ + cancel: function() { + this.finalize(true); + }, + + /** + * Method: click + * Handle clicks. Clicks are stopped from propagating to other listeners + * on map.events or other dom elements. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + click: function(evt) { + OpenLayers.Event.stop(evt); + return false; + }, + + /** + * Method: dblclick + * Handle double-clicks. Double-clicks are stopped from propagating to other + * listeners on map.events or other dom elements. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + dblclick: function(evt) { + OpenLayers.Event.stop(evt); + return false; + }, + + /** + * Method: modifyFeature + * Modify the existing geometry given a pixel location. + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} A pixel location on the map. + */ + modifyFeature: function(pixel) { + if(!this.point) { + this.createFeature(pixel); + } + var lonlat = this.layer.getLonLatFromViewPortPx(pixel); + this.point.geometry.x = lonlat.lon; + this.point.geometry.y = lonlat.lat; + this.callback("modify", [this.point.geometry, this.point, false]); + this.point.geometry.clearBounds(); + this.drawFeature(); + }, + + /** + * Method: drawFeature + * Render features on the temporary layer. + */ + drawFeature: function() { + this.layer.drawFeature(this.point, this.style); + }, + + /** + * Method: getGeometry + * Return the sketch geometry. If <multi> is true, this will return + * a multi-part geometry. + * + * Returns: + * {<OpenLayers.Geometry.Point>} + */ + getGeometry: function() { + var geometry = this.point && this.point.geometry; + if(geometry && this.multi) { + geometry = new OpenLayers.Geometry.MultiPoint([geometry]); + } + return geometry; + }, + + /** + * Method: geometryClone + * Return a clone of the relevant geometry. + * + * Returns: + * {<OpenLayers.Geometry>} + */ + geometryClone: function() { + var geom = this.getGeometry(); + return geom && geom.clone(); + }, + + /** + * Method: mousedown + * Handle mousedown. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + mousedown: function(evt) { + return this.down(evt); + }, + + /** + * Method: touchstart + * Handle touchstart. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + touchstart: function(evt) { + this.startTouch(); + this.lastTouchPx = evt.xy; + return this.down(evt); + }, + + /** + * Method: mousemove + * Handle mousemove. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + mousemove: function(evt) { + return this.move(evt); + }, + + /** + * Method: touchmove + * Handle touchmove. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + touchmove: function(evt) { + this.lastTouchPx = evt.xy; + return this.move(evt); + }, + + /** + * Method: mouseup + * Handle mouseup. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + mouseup: function(evt) { + return this.up(evt); + }, + + /** + * Method: touchend + * Handle touchend. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + touchend: function(evt) { + evt.xy = this.lastTouchPx; + return this.up(evt); + }, + + /** + * Method: down + * Handle mousedown and touchstart. Adjust the geometry and redraw. + * Return determines whether to propagate the event on the map. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + down: function(evt) { + this.mouseDown = true; + this.lastDown = evt.xy; + if(!this.touch) { // no point displayed until up on touch devices + this.modifyFeature(evt.xy); + } + this.stoppedDown = this.stopDown; + return !this.stopDown; + }, + + /** + * Method: move + * Handle mousemove and touchmove. Adjust the geometry and redraw. + * Return determines whether to propagate the event on the map. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + move: function (evt) { + if(!this.touch // no point displayed until up on touch devices + && (!this.mouseDown || this.stoppedDown)) { + this.modifyFeature(evt.xy); + } + return true; + }, + + /** + * Method: up + * Handle mouseup and touchend. Send the latest point in the geometry to the control. + * Return determines whether to propagate the event on the map. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + up: function (evt) { + this.mouseDown = false; + this.stoppedDown = this.stopDown; + + // check keyboard modifiers + if(!this.checkModifiers(evt)) { + return true; + } + // ignore double-clicks + if (this.lastUp && this.lastUp.equals(evt.xy)) { + return true; + } + if (this.lastDown && this.passesTolerance(this.lastDown, evt.xy, + this.pixelTolerance)) { + if (this.touch) { + this.modifyFeature(evt.xy); + } + if(this.persist) { + this.destroyPersistedFeature(); + } + this.lastUp = evt.xy; + this.finalize(); + return !this.stopUp; + } else { + return true; + } + }, + + /** + * Method: mouseout + * Handle mouse out. For better user experience reset mouseDown + * and stoppedDown when the mouse leaves the map viewport. + * + * Parameters: + * evt - {Event} The browser event + */ + mouseout: function(evt) { + if(OpenLayers.Util.mouseLeft(evt, this.map.viewPortDiv)) { + this.stoppedDown = this.stopDown; + this.mouseDown = false; + } + }, + + /** + * Method: passesTolerance + * Determine whether the event is within the optional pixel tolerance. + * + * Returns: + * {Boolean} The event is within the pixel tolerance (if specified). + */ + passesTolerance: function(pixel1, pixel2, tolerance) { + var passes = true; + + if (tolerance != null && pixel1 && pixel2) { + var dist = pixel1.distanceTo(pixel2); + if (dist > tolerance) { + passes = false; + } + } + return passes; + }, + + CLASS_NAME: "OpenLayers.Handler.Point" +}); +/* ====================================================================== + OpenLayers/Handler/Path.js + ====================================================================== */ + +/* 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/Handler/Point.js + * @requires OpenLayers/Geometry/Point.js + * @requires OpenLayers/Geometry/LineString.js + */ + +/** + * Class: OpenLayers.Handler.Path + * Handler to draw a path on the map. Path is displayed on mouse down, + * moves on mouse move, and is finished on mouse up. + * + * Inherits from: + * - <OpenLayers.Handler.Point> + */ +OpenLayers.Handler.Path = OpenLayers.Class(OpenLayers.Handler.Point, { + + /** + * Property: line + * {<OpenLayers.Feature.Vector>} + */ + line: null, + + /** + * APIProperty: maxVertices + * {Number} The maximum number of vertices which can be drawn by this + * handler. When the number of vertices reaches maxVertices, the + * geometry is automatically finalized. Default is null. + */ + maxVertices: null, + + /** + * Property: doubleTouchTolerance + * {Number} Maximum number of pixels between two touches for + * the gesture to be considered a "finalize feature" action. + * Default is 20. + */ + doubleTouchTolerance: 20, + + /** + * Property: freehand + * {Boolean} In freehand mode, the handler starts the path on mouse down, + * adds a point for every mouse move, and finishes the path on mouse up. + * Outside of freehand mode, a point is added to the path on every mouse + * click and double-click finishes the path. + */ + freehand: false, + + /** + * Property: freehandToggle + * {String} If set, freehandToggle is checked on mouse events and will set + * the freehand mode to the opposite of this.freehand. To disallow + * toggling between freehand and non-freehand mode, set freehandToggle to + * null. Acceptable toggle values are 'shiftKey', 'ctrlKey', and 'altKey'. + */ + freehandToggle: 'shiftKey', + + /** + * Property: timerId + * {Integer} The timer used to test the double touch. + */ + timerId: null, + + /** + * Property: redoStack + * {Array} Stack containing points removed with <undo>. + */ + redoStack: null, + + /** + * Constructor: OpenLayers.Handler.Path + * Create a new path hander + * + * Parameters: + * control - {<OpenLayers.Control>} The control that owns this handler + * callbacks - {Object} An object with a properties whose values are + * functions. Various callbacks described below. + * options - {Object} An optional object with properties to be set on the + * handler + * + * Named callbacks: + * create - Called when a sketch is first created. Callback called with + * the creation point geometry and sketch feature. + * modify - Called with each move of a vertex with the vertex (point) + * geometry and the sketch feature. + * point - Called as each point is added. Receives the new point geometry. + * done - Called when the point drawing is finished. The callback will + * recieve a single argument, the linestring geometry. + * cancel - Called when the handler is deactivated while drawing. The + * cancel callback will receive a geometry. + */ + + /** + * Method: createFeature + * Add temporary geometries + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} The initial pixel location for the new + * feature. + */ + createFeature: function(pixel) { + var lonlat = this.layer.getLonLatFromViewPortPx(pixel); + var geometry = new OpenLayers.Geometry.Point( + lonlat.lon, lonlat.lat + ); + this.point = new OpenLayers.Feature.Vector(geometry); + this.line = new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.LineString([this.point.geometry]) + ); + this.callback("create", [this.point.geometry, this.getSketch()]); + this.point.geometry.clearBounds(); + this.layer.addFeatures([this.line, this.point], {silent: true}); + }, + + /** + * Method: destroyFeature + * Destroy temporary geometries + * + * Parameters: + * force - {Boolean} Destroy even if persist is true. + */ + destroyFeature: function(force) { + OpenLayers.Handler.Point.prototype.destroyFeature.call( + this, force); + this.line = null; + }, + + /** + * Method: destroyPersistedFeature + * Destroy the persisted feature. + */ + destroyPersistedFeature: function() { + var layer = this.layer; + if(layer && layer.features.length > 2) { + this.layer.features[0].destroy(); + } + }, + + /** + * Method: removePoint + * Destroy the temporary point. + */ + removePoint: function() { + if(this.point) { + this.layer.removeFeatures([this.point]); + } + }, + + /** + * Method: addPoint + * Add point to geometry. Send the point index to override + * the behavior of LinearRing that disregards adding duplicate points. + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} The pixel location for the new point. + */ + addPoint: function(pixel) { + this.layer.removeFeatures([this.point]); + var lonlat = this.layer.getLonLatFromViewPortPx(pixel); + this.point = new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat) + ); + this.line.geometry.addComponent( + this.point.geometry, this.line.geometry.components.length + ); + this.layer.addFeatures([this.point]); + this.callback("point", [this.point.geometry, this.getGeometry()]); + this.callback("modify", [this.point.geometry, this.getSketch()]); + this.drawFeature(); + delete this.redoStack; + }, + + /** + * Method: insertXY + * Insert a point in the current sketch given x & y coordinates. The new + * point is inserted immediately before the most recently drawn point. + * + * Parameters: + * x - {Number} The x-coordinate of the point. + * y - {Number} The y-coordinate of the point. + */ + insertXY: function(x, y) { + this.line.geometry.addComponent( + new OpenLayers.Geometry.Point(x, y), + this.getCurrentPointIndex() + ); + this.drawFeature(); + delete this.redoStack; + }, + + /** + * Method: insertDeltaXY + * Insert a point given offsets from the previously inserted point. + * + * Parameters: + * dx - {Number} The x-coordinate offset of the point. + * dy - {Number} The y-coordinate offset of the point. + */ + insertDeltaXY: function(dx, dy) { + var previousIndex = this.getCurrentPointIndex() - 1; + var p0 = this.line.geometry.components[previousIndex]; + if (p0 && !isNaN(p0.x) && !isNaN(p0.y)) { + this.insertXY(p0.x + dx, p0.y + dy); + } + }, + + /** + * Method: insertDirectionLength + * Insert a point in the current sketch given a direction and a length. + * + * Parameters: + * direction - {Number} Degrees clockwise from the positive x-axis. + * length - {Number} Distance from the previously drawn point. + */ + insertDirectionLength: function(direction, length) { + direction *= Math.PI / 180; + var dx = length * Math.cos(direction); + var dy = length * Math.sin(direction); + this.insertDeltaXY(dx, dy); + }, + + /** + * Method: insertDeflectionLength + * Insert a point in the current sketch given a deflection and a length. + * The deflection should be degrees clockwise from the previously + * digitized segment. + * + * Parameters: + * deflection - {Number} Degrees clockwise from the previous segment. + * length - {Number} Distance from the previously drawn point. + */ + insertDeflectionLength: function(deflection, length) { + var previousIndex = this.getCurrentPointIndex() - 1; + if (previousIndex > 0) { + var p1 = this.line.geometry.components[previousIndex]; + var p0 = this.line.geometry.components[previousIndex-1]; + var theta = Math.atan2(p1.y - p0.y, p1.x - p0.x); + this.insertDirectionLength( + (theta * 180 / Math.PI) + deflection, length + ); + } + }, + + /** + * Method: getCurrentPointIndex + * + * Returns: + * {Number} The index of the most recently drawn point. + */ + getCurrentPointIndex: function() { + return this.line.geometry.components.length - 1; + }, + + + /** + * Method: undo + * Remove the most recently added point in the sketch geometry. + * + * Returns: + * {Boolean} A point was removed. + */ + undo: function() { + var geometry = this.line.geometry; + var components = geometry.components; + var index = this.getCurrentPointIndex() - 1; + var target = components[index]; + var undone = geometry.removeComponent(target); + if (undone) { + // On touch devices, set the current ("mouse location") point to + // match the last digitized point. + if (this.touch && index > 0) { + components = geometry.components; // safety + var lastpt = components[index - 1]; + var curptidx = this.getCurrentPointIndex(); + var curpt = components[curptidx]; + curpt.x = lastpt.x; + curpt.y = lastpt.y; + } + if (!this.redoStack) { + this.redoStack = []; + } + this.redoStack.push(target); + this.drawFeature(); + } + return undone; + }, + + /** + * Method: redo + * Reinsert the most recently removed point resulting from an <undo> call. + * The undo stack is deleted whenever a point is added by other means. + * + * Returns: + * {Boolean} A point was added. + */ + redo: function() { + var target = this.redoStack && this.redoStack.pop(); + if (target) { + this.line.geometry.addComponent(target, this.getCurrentPointIndex()); + this.drawFeature(); + } + return !!target; + }, + + /** + * Method: freehandMode + * Determine whether to behave in freehand mode or not. + * + * Returns: + * {Boolean} + */ + freehandMode: function(evt) { + return (this.freehandToggle && evt[this.freehandToggle]) ? + !this.freehand : this.freehand; + }, + + /** + * Method: modifyFeature + * Modify the existing geometry given the new point + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} The updated pixel location for the latest + * point. + * drawing - {Boolean} Indicate if we're currently drawing. + */ + modifyFeature: function(pixel, drawing) { + if(!this.line) { + this.createFeature(pixel); + } + var lonlat = this.layer.getLonLatFromViewPortPx(pixel); + this.point.geometry.x = lonlat.lon; + this.point.geometry.y = lonlat.lat; + this.callback("modify", [this.point.geometry, this.getSketch(), drawing]); + this.point.geometry.clearBounds(); + this.drawFeature(); + }, + + /** + * Method: drawFeature + * Render geometries on the temporary layer. + */ + drawFeature: function() { + this.layer.drawFeature(this.line, this.style); + this.layer.drawFeature(this.point, this.style); + }, + + /** + * Method: getSketch + * Return the sketch feature. + * + * Returns: + * {<OpenLayers.Feature.Vector>} + */ + getSketch: function() { + return this.line; + }, + + /** + * Method: getGeometry + * Return the sketch geometry. If <multi> is true, this will return + * a multi-part geometry. + * + * Returns: + * {<OpenLayers.Geometry.LineString>} + */ + getGeometry: function() { + var geometry = this.line && this.line.geometry; + if(geometry && this.multi) { + geometry = new OpenLayers.Geometry.MultiLineString([geometry]); + } + return geometry; + }, + + /** + * method: touchstart + * handle touchstart. + * + * parameters: + * evt - {event} the browser event + * + * returns: + * {boolean} allow event propagation + */ + touchstart: function(evt) { + if (this.timerId && + this.passesTolerance(this.lastTouchPx, evt.xy, + this.doubleTouchTolerance)) { + // double-tap, finalize the geometry + this.finishGeometry(); + window.clearTimeout(this.timerId); + this.timerId = null; + return false; + } else { + if (this.timerId) { + window.clearTimeout(this.timerId); + this.timerId = null; + } + this.timerId = window.setTimeout( + OpenLayers.Function.bind(function() { + this.timerId = null; + }, this), 300); + return OpenLayers.Handler.Point.prototype.touchstart.call(this, evt); + } + }, + + /** + * Method: down + * Handle mousedown and touchstart. Add a new point to the geometry and + * render it. Return determines whether to propagate the event on the map. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + down: function(evt) { + var stopDown = this.stopDown; + if(this.freehandMode(evt)) { + stopDown = true; + if (this.touch) { + this.modifyFeature(evt.xy, !!this.lastUp); + OpenLayers.Event.stop(evt); + } + } + if (!this.touch && (!this.lastDown || + !this.passesTolerance(this.lastDown, evt.xy, + this.pixelTolerance))) { + this.modifyFeature(evt.xy, !!this.lastUp); + } + this.mouseDown = true; + this.lastDown = evt.xy; + this.stoppedDown = stopDown; + return !stopDown; + }, + + /** + * Method: move + * Handle mousemove and touchmove. Adjust the geometry and redraw. + * Return determines whether to propagate the event on the map. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + move: function (evt) { + if(this.stoppedDown && this.freehandMode(evt)) { + if(this.persist) { + this.destroyPersistedFeature(); + } + if(this.maxVertices && this.line && + this.line.geometry.components.length === this.maxVertices) { + this.removePoint(); + this.finalize(); + } else { + this.addPoint(evt.xy); + } + return false; + } + if (!this.touch && (!this.mouseDown || this.stoppedDown)) { + this.modifyFeature(evt.xy, !!this.lastUp); + } + return true; + }, + + /** + * Method: up + * Handle mouseup and touchend. Send the latest point in the geometry to + * the control. Return determines whether to propagate the event on the map. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + up: function (evt) { + if (this.mouseDown && (!this.lastUp || !this.lastUp.equals(evt.xy))) { + if(this.stoppedDown && this.freehandMode(evt)) { + if (this.persist) { + this.destroyPersistedFeature(); + } + this.removePoint(); + this.finalize(); + } else { + if (this.passesTolerance(this.lastDown, evt.xy, + this.pixelTolerance)) { + if (this.touch) { + this.modifyFeature(evt.xy); + } + if(this.lastUp == null && this.persist) { + this.destroyPersistedFeature(); + } + this.addPoint(evt.xy); + this.lastUp = evt.xy; + if(this.line.geometry.components.length === this.maxVertices + 1) { + this.finishGeometry(); + } + } + } + } + this.stoppedDown = this.stopDown; + this.mouseDown = false; + return !this.stopUp; + }, + + /** + * APIMethod: finishGeometry + * Finish the geometry and send it back to the control. + */ + finishGeometry: function() { + var index = this.line.geometry.components.length - 1; + this.line.geometry.removeComponent(this.line.geometry.components[index]); + this.removePoint(); + this.finalize(); + }, + + /** + * Method: dblclick + * Handle double-clicks. + * + * Parameters: + * evt - {Event} The browser event + * + * Returns: + * {Boolean} Allow event propagation + */ + dblclick: function(evt) { + if(!this.freehandMode(evt)) { + this.finishGeometry(); + } + return false; + }, + + CLASS_NAME: "OpenLayers.Handler.Path" +}); +/* ====================================================================== + OpenLayers/Control/Attribution.js + ====================================================================== */ + +/* 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/Control.js + */ + +/** + * Class: OpenLayers.Control.Attribution + * The attribution control adds attribution from layers to the map display. + * It uses 'attribution' property of each layer. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.Attribution = + OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: separator + * {String} String used to separate layers. + */ + separator: ", ", + + /** + * APIProperty: template + * {String} Template for the attribution. This has to include the substring + * "${layers}", which will be replaced by the layer specific + * attributions, separated by <separator>. The default is "${layers}". + */ + template: "${layers}", + + /** + * Constructor: OpenLayers.Control.Attribution + * + * Parameters: + * options - {Object} Options for control. + */ + + /** + * Method: destroy + * Destroy control. + */ + destroy: function() { + this.map.events.un({ + "removelayer": this.updateAttribution, + "addlayer": this.updateAttribution, + "changelayer": this.updateAttribution, + "changebaselayer": this.updateAttribution, + scope: this + }); + + OpenLayers.Control.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: draw + * Initialize control. + * + * Returns: + * {DOMElement} A reference to the DIV DOMElement containing the control + */ + draw: function() { + OpenLayers.Control.prototype.draw.apply(this, arguments); + + this.map.events.on({ + 'changebaselayer': this.updateAttribution, + 'changelayer': this.updateAttribution, + 'addlayer': this.updateAttribution, + 'removelayer': this.updateAttribution, + scope: this + }); + this.updateAttribution(); + + return this.div; + }, + + /** + * Method: updateAttribution + * Update attribution string. + */ + updateAttribution: function() { + var attributions = []; + if (this.map && this.map.layers) { + for(var i=0, len=this.map.layers.length; i<len; i++) { + var layer = this.map.layers[i]; + if (layer.attribution && layer.getVisibility()) { + // add attribution only if attribution text is unique + if (OpenLayers.Util.indexOf( + attributions, layer.attribution) === -1) { + attributions.push( layer.attribution ); + } + } + } + this.div.innerHTML = OpenLayers.String.format(this.template, { + layers: attributions.join(this.separator) + }); + } + }, + + CLASS_NAME: "OpenLayers.Control.Attribution" +}); +/* ====================================================================== + OpenLayers/Kinetic.js + ====================================================================== */ + +/* 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/Animation.js + */ + +OpenLayers.Kinetic = OpenLayers.Class({ + + /** + * Property: threshold + * In most cases changing the threshold isn't needed. + * In px/ms, default to 0. + */ + threshold: 0, + + /** + * Property: deceleration + * {Float} the deseleration in px/ms², default to 0.0035. + */ + deceleration: 0.0035, + + /** + * Property: nbPoints + * {Integer} the number of points we use to calculate the kinetic + * initial values. + */ + nbPoints: 100, + + /** + * Property: delay + * {Float} time to consider to calculate the kinetic initial values. + * In ms, default to 200. + */ + delay: 200, + + /** + * Property: points + * List of points use to calculate the kinetic initial values. + */ + points: undefined, + + /** + * Property: timerId + * ID of the timer. + */ + timerId: undefined, + + /** + * Constructor: OpenLayers.Kinetic + * + * Parameters: + * options - {Object} + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + }, + + /** + * Method: begin + * Begins the dragging. + */ + begin: function() { + OpenLayers.Animation.stop(this.timerId); + this.timerId = undefined; + this.points = []; + }, + + /** + * Method: update + * Updates during the dragging. + * + * Parameters: + * xy - {<OpenLayers.Pixel>} The new position. + */ + update: function(xy) { + this.points.unshift({xy: xy, tick: new Date().getTime()}); + if (this.points.length > this.nbPoints) { + this.points.pop(); + } + }, + + /** + * Method: end + * Ends the dragging, start the kinetic. + * + * Parameters: + * xy - {<OpenLayers.Pixel>} The last position. + * + * Returns: + * {Object} An object with two properties: "speed", and "theta". The + * "speed" and "theta" values are to be passed to the move + * function when starting the animation. + */ + end: function(xy) { + var last, now = new Date().getTime(); + for (var i = 0, l = this.points.length, point; i < l; i++) { + point = this.points[i]; + if (now - point.tick > this.delay) { + break; + } + last = point; + } + if (!last) { + return; + } + var time = new Date().getTime() - last.tick; + var dist = Math.sqrt(Math.pow(xy.x - last.xy.x, 2) + + Math.pow(xy.y - last.xy.y, 2)); + var speed = dist / time; + if (speed == 0 || speed < this.threshold) { + return; + } + var theta = Math.asin((xy.y - last.xy.y) / dist); + if (last.xy.x <= xy.x) { + theta = Math.PI - theta; + } + return {speed: speed, theta: theta}; + }, + + /** + * Method: move + * Launch the kinetic move pan. + * + * Parameters: + * info - {Object} An object with two properties, "speed", and "theta". + * These values are those returned from the "end" call. + * callback - {Function} Function called on every step of the animation, + * receives x, y (values to pan), end (is the last point). + */ + move: function(info, callback) { + var v0 = info.speed; + var fx = Math.cos(info.theta); + var fy = -Math.sin(info.theta); + + var initialTime = new Date().getTime(); + + var lastX = 0; + var lastY = 0; + + var timerCallback = function() { + if (this.timerId == null) { + return; + } + + var t = new Date().getTime() - initialTime; + + var p = (-this.deceleration * Math.pow(t, 2)) / 2.0 + v0 * t; + var x = p * fx; + var y = p * fy; + + var args = {}; + args.end = false; + var v = -this.deceleration * t + v0; + + if (v <= 0) { + OpenLayers.Animation.stop(this.timerId); + this.timerId = null; + args.end = true; + } + + args.x = x - lastX; + args.y = y - lastY; + lastX = x; + lastY = y; + callback(args.x, args.y, args.end); + }; + + this.timerId = OpenLayers.Animation.start( + OpenLayers.Function.bind(timerCallback, this) + ); + }, + + CLASS_NAME: "OpenLayers.Kinetic" +}); +/* ====================================================================== + OpenLayers/Layer/WMS.js + ====================================================================== */ + +/* 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/Layer/Grid.js + */ + +/** + * Class: OpenLayers.Layer.WMS + * Instances of OpenLayers.Layer.WMS are used to display data from OGC Web + * Mapping Services. Create a new WMS layer with the <OpenLayers.Layer.WMS> + * constructor. + * + * Inherits from: + * - <OpenLayers.Layer.Grid> + */ +OpenLayers.Layer.WMS = OpenLayers.Class(OpenLayers.Layer.Grid, { + + /** + * Constant: DEFAULT_PARAMS + * {Object} Hashtable of default parameter key/value pairs + */ + DEFAULT_PARAMS: { service: "WMS", + version: "1.1.1", + request: "GetMap", + styles: "", + format: "image/jpeg" + }, + + /** + * APIProperty: isBaseLayer + * {Boolean} Default is true for WMS layer + */ + isBaseLayer: true, + + /** + * APIProperty: encodeBBOX + * {Boolean} Should the BBOX commas be encoded? The WMS spec says 'no', + * but some services want it that way. Default false. + */ + encodeBBOX: false, + + /** + * APIProperty: noMagic + * {Boolean} If true, the image format will not be automagicaly switched + * from image/jpeg to image/png or image/gif when using + * TRANSPARENT=TRUE. Also isBaseLayer will not changed by the + * constructor. Default false. + */ + noMagic: false, + + /** + * Property: yx + * {Object} Keys in this object are EPSG codes for which the axis order + * is to be reversed (yx instead of xy, LatLon instead of LonLat), with + * true as value. This is only relevant for WMS versions >= 1.3.0, and + * only if yx is not set in <OpenLayers.Projection.defaults> for the + * used projection. + */ + yx: {}, + + /** + * Constructor: OpenLayers.Layer.WMS + * Create a new WMS layer object + * + * Examples: + * + * The code below creates a simple WMS layer using the image/jpeg format. + * (code) + * var wms = new OpenLayers.Layer.WMS("NASA Global Mosaic", + * "http://wms.jpl.nasa.gov/wms.cgi", + * {layers: "modis,global_mosaic"}); + * (end) + * Note the 3rd argument (params). Properties added to this object will be + * added to the WMS GetMap requests used for this layer's tiles. The only + * mandatory parameter is "layers". Other common WMS params include + * "transparent", "styles" and "format". Note that the "srs" param will + * always be ignored. Instead, it will be derived from the baseLayer's or + * map's projection. + * + * The code below creates a transparent WMS layer with additional options. + * (code) + * var wms = new OpenLayers.Layer.WMS("NASA Global Mosaic", + * "http://wms.jpl.nasa.gov/wms.cgi", + * { + * layers: "modis,global_mosaic", + * transparent: true + * }, { + * opacity: 0.5, + * singleTile: true + * }); + * (end) + * Note that by default, a WMS layer is configured as baseLayer. Setting + * the "transparent" param to true will apply some magic (see <noMagic>). + * The default image format changes from image/jpeg to image/png, and the + * layer is not configured as baseLayer. + * + * Parameters: + * name - {String} A name for the layer + * url - {String} Base url for the WMS + * (e.g. http://wms.jpl.nasa.gov/wms.cgi) + * params - {Object} An object with key/value pairs representing the + * GetMap query string parameters and parameter values. + * options - {Object} Hashtable of extra options to tag onto the layer. + * These options include all properties listed above, plus the ones + * inherited from superclasses. + */ + initialize: function(name, url, params, options) { + var newArguments = []; + //uppercase params + params = OpenLayers.Util.upperCaseObject(params); + if (parseFloat(params.VERSION) >= 1.3 && !params.EXCEPTIONS) { + params.EXCEPTIONS = "INIMAGE"; + } + newArguments.push(name, url, params, options); + OpenLayers.Layer.Grid.prototype.initialize.apply(this, newArguments); + OpenLayers.Util.applyDefaults( + this.params, + OpenLayers.Util.upperCaseObject(this.DEFAULT_PARAMS) + ); + + + //layer is transparent + if (!this.noMagic && this.params.TRANSPARENT && + this.params.TRANSPARENT.toString().toLowerCase() == "true") { + + // unless explicitly set in options, make layer an overlay + if ( (options == null) || (!options.isBaseLayer) ) { + this.isBaseLayer = false; + } + + // jpegs can never be transparent, so intelligently switch the + // format, depending on the browser's capabilities + if (this.params.FORMAT == "image/jpeg") { + this.params.FORMAT = OpenLayers.Util.alphaHack() ? "image/gif" + : "image/png"; + } + } + + }, + + /** + * Method: clone + * Create a clone of this layer + * + * Returns: + * {<OpenLayers.Layer.WMS>} An exact clone of this layer + */ + clone: function (obj) { + + if (obj == null) { + obj = new OpenLayers.Layer.WMS(this.name, + this.url, + this.params, + this.getOptions()); + } + + //get all additions from superclasses + obj = OpenLayers.Layer.Grid.prototype.clone.apply(this, [obj]); + + // copy/set any non-init, non-simple values here + + return obj; + }, + + /** + * APIMethod: reverseAxisOrder + * Returns true if the axis order is reversed for the WMS version and + * projection of the layer. + * + * Returns: + * {Boolean} true if the axis order is reversed, false otherwise. + */ + reverseAxisOrder: function() { + var projCode = this.projection.getCode(); + return parseFloat(this.params.VERSION) >= 1.3 && + !!(this.yx[projCode] || (OpenLayers.Projection.defaults[projCode] && + OpenLayers.Projection.defaults[projCode].yx)); + }, + + /** + * Method: getURL + * Return a GetMap query string for this layer + * + * Parameters: + * bounds - {<OpenLayers.Bounds>} A bounds representing the bbox for the + * request. + * + * Returns: + * {String} A string with the layer's url and parameters and also the + * passed-in bounds and appropriate tile size specified as + * parameters. + */ + getURL: function (bounds) { + bounds = this.adjustBounds(bounds); + + var imageSize = this.getImageSize(); + var newParams = {}; + // WMS 1.3 introduced axis order + var reverseAxisOrder = this.reverseAxisOrder(); + newParams.BBOX = this.encodeBBOX ? + bounds.toBBOX(null, reverseAxisOrder) : + bounds.toArray(reverseAxisOrder); + newParams.WIDTH = imageSize.w; + newParams.HEIGHT = imageSize.h; + var requestString = this.getFullRequestString(newParams); + return requestString; + }, + + /** + * APIMethod: mergeNewParams + * Catch changeParams and uppercase the new params to be merged in + * before calling changeParams on the super class. + * + * Once params have been changed, the tiles will be reloaded with + * the new parameters. + * + * Parameters: + * newParams - {Object} Hashtable of new params to use + */ + mergeNewParams:function(newParams) { + var upperParams = OpenLayers.Util.upperCaseObject(newParams); + var newArguments = [upperParams]; + return OpenLayers.Layer.Grid.prototype.mergeNewParams.apply(this, + newArguments); + }, + + /** + * APIMethod: getFullRequestString + * Combine the layer's url with its params and these newParams. + * + * Add the SRS parameter from projection -- this is probably + * more eloquently done via a setProjection() method, but this + * works for now and always. + * + * Parameters: + * newParams - {Object} + * altUrl - {String} Use this as the url instead of the layer's url + * + * Returns: + * {String} + */ + getFullRequestString:function(newParams, altUrl) { + var mapProjection = this.map.getProjectionObject(); + var projectionCode = this.projection && this.projection.equals(mapProjection) ? + this.projection.getCode() : + mapProjection.getCode(); + var value = (projectionCode == "none") ? null : projectionCode; + if (parseFloat(this.params.VERSION) >= 1.3) { + this.params.CRS = value; + } else { + this.params.SRS = value; + } + + if (typeof this.params.TRANSPARENT == "boolean") { + newParams.TRANSPARENT = this.params.TRANSPARENT ? "TRUE" : "FALSE"; + } + + return OpenLayers.Layer.Grid.prototype.getFullRequestString.apply( + this, arguments); + }, + + CLASS_NAME: "OpenLayers.Layer.WMS" +}); +/* ====================================================================== + OpenLayers/Renderer/SVG.js + ====================================================================== */ + +/* 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/Renderer/Elements.js + */ + +/** + * Class: OpenLayers.Renderer.SVG + * + * Inherits: + * - <OpenLayers.Renderer.Elements> + */ +OpenLayers.Renderer.SVG = OpenLayers.Class(OpenLayers.Renderer.Elements, { + + /** + * Property: xmlns + * {String} + */ + xmlns: "http://www.w3.org/2000/svg", + + /** + * Property: xlinkns + * {String} + */ + xlinkns: "http://www.w3.org/1999/xlink", + + /** + * Constant: MAX_PIXEL + * {Integer} Firefox has a limitation where values larger or smaller than + * about 15000 in an SVG document lock the browser up. This + * works around it. + */ + MAX_PIXEL: 15000, + + /** + * Property: translationParameters + * {Object} Hash with "x" and "y" properties + */ + translationParameters: null, + + /** + * Property: symbolMetrics + * {Object} Cache for symbol metrics according to their svg coordinate + * space. This is an object keyed by the symbol's id, and values are + * an array of [width, centerX, centerY]. + */ + symbolMetrics: null, + + /** + * Constructor: OpenLayers.Renderer.SVG + * + * Parameters: + * containerID - {String} + */ + initialize: function(containerID) { + if (!this.supported()) { + return; + } + OpenLayers.Renderer.Elements.prototype.initialize.apply(this, + arguments); + this.translationParameters = {x: 0, y: 0}; + + this.symbolMetrics = {}; + }, + + /** + * APIMethod: supported + * + * Returns: + * {Boolean} Whether or not the browser supports the SVG renderer + */ + supported: function() { + var svgFeature = "http://www.w3.org/TR/SVG11/feature#"; + return (document.implementation && + (document.implementation.hasFeature("org.w3c.svg", "1.0") || + document.implementation.hasFeature(svgFeature + "SVG", "1.1") || + document.implementation.hasFeature(svgFeature + "BasicStructure", "1.1") )); + }, + + /** + * Method: inValidRange + * See #669 for more information + * + * Parameters: + * x - {Integer} + * y - {Integer} + * xyOnly - {Boolean} whether or not to just check for x and y, which means + * to not take the current translation parameters into account if true. + * + * Returns: + * {Boolean} Whether or not the 'x' and 'y' coordinates are in the + * valid range. + */ + inValidRange: function(x, y, xyOnly) { + var left = x + (xyOnly ? 0 : this.translationParameters.x); + var top = y + (xyOnly ? 0 : this.translationParameters.y); + return (left >= -this.MAX_PIXEL && left <= this.MAX_PIXEL && + top >= -this.MAX_PIXEL && top <= this.MAX_PIXEL); + }, + + /** + * Method: setExtent + * + * Parameters: + * extent - {<OpenLayers.Bounds>} + * resolutionChanged - {Boolean} + * + * Returns: + * {Boolean} true to notify the layer that the new extent does not exceed + * the coordinate range, and the features will not need to be redrawn. + * False otherwise. + */ + setExtent: function(extent, resolutionChanged) { + var coordSysUnchanged = OpenLayers.Renderer.Elements.prototype.setExtent.apply(this, arguments); + + var resolution = this.getResolution(), + left = -extent.left / resolution, + top = extent.top / resolution; + + // If the resolution has changed, start over changing the corner, because + // the features will redraw. + if (resolutionChanged) { + this.left = left; + this.top = top; + // Set the viewbox + var extentString = "0 0 " + this.size.w + " " + this.size.h; + + this.rendererRoot.setAttributeNS(null, "viewBox", extentString); + this.translate(this.xOffset, 0); + return true; + } else { + var inRange = this.translate(left - this.left + this.xOffset, top - this.top); + if (!inRange) { + // recenter the coordinate system + this.setExtent(extent, true); + } + return coordSysUnchanged && inRange; + } + }, + + /** + * Method: translate + * Transforms the SVG coordinate system + * + * Parameters: + * x - {Float} + * y - {Float} + * + * Returns: + * {Boolean} true if the translation parameters are in the valid coordinates + * range, false otherwise. + */ + translate: function(x, y) { + if (!this.inValidRange(x, y, true)) { + return false; + } else { + var transformString = ""; + if (x || y) { + transformString = "translate(" + x + "," + y + ")"; + } + this.root.setAttributeNS(null, "transform", transformString); + this.translationParameters = {x: x, y: y}; + return true; + } + }, + + /** + * Method: setSize + * Sets the size of the drawing surface. + * + * Parameters: + * size - {<OpenLayers.Size>} The size of the drawing surface + */ + setSize: function(size) { + OpenLayers.Renderer.prototype.setSize.apply(this, arguments); + + this.rendererRoot.setAttributeNS(null, "width", this.size.w); + this.rendererRoot.setAttributeNS(null, "height", this.size.h); + }, + + /** + * Method: getNodeType + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * + * Returns: + * {String} The corresponding node type for the specified geometry + */ + getNodeType: function(geometry, style) { + var nodeType = null; + switch (geometry.CLASS_NAME) { + case "OpenLayers.Geometry.Point": + if (style.externalGraphic) { + nodeType = "image"; + } else if (this.isComplexSymbol(style.graphicName)) { + nodeType = "svg"; + } else { + nodeType = "circle"; + } + break; + case "OpenLayers.Geometry.Rectangle": + nodeType = "rect"; + break; + case "OpenLayers.Geometry.LineString": + nodeType = "polyline"; + break; + case "OpenLayers.Geometry.LinearRing": + nodeType = "polygon"; + break; + case "OpenLayers.Geometry.Polygon": + case "OpenLayers.Geometry.Curve": + nodeType = "path"; + break; + default: + break; + } + return nodeType; + }, + + /** + * Method: setStyle + * Use to set all the style attributes to a SVG node. + * + * Takes care to adjust stroke width and point radius to be + * resolution-relative + * + * Parameters: + * node - {SVGDomElement} An SVG element to decorate + * style - {Object} + * options - {Object} Currently supported options include + * 'isFilled' {Boolean} and + * 'isStroked' {Boolean} + */ + setStyle: function(node, style, options) { + style = style || node._style; + options = options || node._options; + + var title = style.title || style.graphicTitle; + if (title) { + node.setAttributeNS(null, "title", title); + //Standards-conformant SVG + // Prevent duplicate nodes. See issue https://github.com/openlayers/openlayers/issues/92 + var titleNode = node.getElementsByTagName("title"); + if (titleNode.length > 0) { + titleNode[0].firstChild.textContent = title; + } else { + var label = this.nodeFactory(null, "title"); + label.textContent = title; + node.appendChild(label); + } + } + + var r = parseFloat(node.getAttributeNS(null, "r")); + var widthFactor = 1; + var pos; + if (node._geometryClass == "OpenLayers.Geometry.Point" && r) { + node.style.visibility = ""; + if (style.graphic === false) { + node.style.visibility = "hidden"; + } else if (style.externalGraphic) { + pos = this.getPosition(node); + if (style.graphicWidth && style.graphicHeight) { + node.setAttributeNS(null, "preserveAspectRatio", "none"); + } + var width = style.graphicWidth || style.graphicHeight; + var height = style.graphicHeight || style.graphicWidth; + width = width ? width : style.pointRadius*2; + height = height ? height : style.pointRadius*2; + var xOffset = (style.graphicXOffset != undefined) ? + style.graphicXOffset : -(0.5 * width); + var yOffset = (style.graphicYOffset != undefined) ? + style.graphicYOffset : -(0.5 * height); + + var opacity = style.graphicOpacity || style.fillOpacity; + + node.setAttributeNS(null, "x", (pos.x + xOffset).toFixed()); + node.setAttributeNS(null, "y", (pos.y + yOffset).toFixed()); + node.setAttributeNS(null, "width", width); + node.setAttributeNS(null, "height", height); + node.setAttributeNS(this.xlinkns, "xlink:href", style.externalGraphic); + node.setAttributeNS(null, "style", "opacity: "+opacity); + node.onclick = OpenLayers.Event.preventDefault; + } else if (this.isComplexSymbol(style.graphicName)) { + // the symbol viewBox is three times as large as the symbol + var offset = style.pointRadius * 3; + var size = offset * 2; + var src = this.importSymbol(style.graphicName); + pos = this.getPosition(node); + widthFactor = this.symbolMetrics[src.id][0] * 3 / size; + + // remove the node from the dom before we modify it. This + // prevents various rendering issues in Safari and FF + var parent = node.parentNode; + var nextSibling = node.nextSibling; + if(parent) { + parent.removeChild(node); + } + + // The more appropriate way to implement this would be use/defs, + // but due to various issues in several browsers, it is safer to + // copy the symbols instead of referencing them. + // See e.g. ticket http://trac.osgeo.org/openlayers/ticket/2985 + // and this email thread + // http://osgeo-org.1803224.n2.nabble.com/Select-Control-Ctrl-click-on-Feature-with-a-graphicName-opens-new-browser-window-tc5846039.html + node.firstChild && node.removeChild(node.firstChild); + node.appendChild(src.firstChild.cloneNode(true)); + node.setAttributeNS(null, "viewBox", src.getAttributeNS(null, "viewBox")); + + node.setAttributeNS(null, "width", size); + node.setAttributeNS(null, "height", size); + node.setAttributeNS(null, "x", pos.x - offset); + node.setAttributeNS(null, "y", pos.y - offset); + + // now that the node has all its new properties, insert it + // back into the dom where it was + if(nextSibling) { + parent.insertBefore(node, nextSibling); + } else if(parent) { + parent.appendChild(node); + } + } else { + node.setAttributeNS(null, "r", style.pointRadius); + } + + var rotation = style.rotation; + + if ((rotation !== undefined || node._rotation !== undefined) && pos) { + node._rotation = rotation; + rotation |= 0; + if (node.nodeName !== "svg") { + node.setAttributeNS(null, "transform", + "rotate(" + rotation + " " + pos.x + " " + + pos.y + ")"); + } else { + var metrics = this.symbolMetrics[src.id]; + node.firstChild.setAttributeNS(null, "transform", "rotate(" + + rotation + " " + + metrics[1] + " " + + metrics[2] + ")"); + } + } + } + + if (options.isFilled) { + node.setAttributeNS(null, "fill", style.fillColor); + node.setAttributeNS(null, "fill-opacity", style.fillOpacity); + } else { + node.setAttributeNS(null, "fill", "none"); + } + + if (options.isStroked) { + node.setAttributeNS(null, "stroke", style.strokeColor); + node.setAttributeNS(null, "stroke-opacity", style.strokeOpacity); + node.setAttributeNS(null, "stroke-width", style.strokeWidth * widthFactor); + node.setAttributeNS(null, "stroke-linecap", style.strokeLinecap || "round"); + // Hard-coded linejoin for now, to make it look the same as in VML. + // There is no strokeLinejoin property yet for symbolizers. + node.setAttributeNS(null, "stroke-linejoin", "round"); + style.strokeDashstyle && node.setAttributeNS(null, + "stroke-dasharray", this.dashStyle(style, widthFactor)); + } else { + node.setAttributeNS(null, "stroke", "none"); + } + + if (style.pointerEvents) { + node.setAttributeNS(null, "pointer-events", style.pointerEvents); + } + + if (style.cursor != null) { + node.setAttributeNS(null, "cursor", style.cursor); + } + + return node; + }, + + /** + * Method: dashStyle + * + * Parameters: + * style - {Object} + * widthFactor - {Number} + * + * Returns: + * {String} A SVG compliant 'stroke-dasharray' value + */ + dashStyle: function(style, widthFactor) { + var w = style.strokeWidth * widthFactor; + var str = style.strokeDashstyle; + switch (str) { + case 'solid': + return 'none'; + case 'dot': + return [1, 4 * w].join(); + case 'dash': + return [4 * w, 4 * w].join(); + case 'dashdot': + return [4 * w, 4 * w, 1, 4 * w].join(); + case 'longdash': + return [8 * w, 4 * w].join(); + case 'longdashdot': + return [8 * w, 4 * w, 1, 4 * w].join(); + default: + return OpenLayers.String.trim(str).replace(/\s+/g, ","); + } + }, + + /** + * Method: createNode + * + * Parameters: + * type - {String} Kind of node to draw + * id - {String} Id for node + * + * Returns: + * {DOMElement} A new node of the given type and id + */ + createNode: function(type, id) { + var node = document.createElementNS(this.xmlns, type); + if (id) { + node.setAttributeNS(null, "id", id); + } + return node; + }, + + /** + * Method: nodeTypeCompare + * + * Parameters: + * node - {SVGDomElement} An SVG element + * type - {String} Kind of node + * + * Returns: + * {Boolean} Whether or not the specified node is of the specified type + */ + nodeTypeCompare: function(node, type) { + return (type == node.nodeName); + }, + + /** + * Method: createRenderRoot + * + * Returns: + * {DOMElement} The specific render engine's root element + */ + createRenderRoot: function() { + var svg = this.nodeFactory(this.container.id + "_svgRoot", "svg"); + svg.style.display = "block"; + return svg; + }, + + /** + * Method: createRoot + * + * Parameters: + * suffix - {String} suffix to append to the id + * + * Returns: + * {DOMElement} + */ + createRoot: function(suffix) { + return this.nodeFactory(this.container.id + suffix, "g"); + }, + + /** + * Method: createDefs + * + * Returns: + * {DOMElement} The element to which we'll add the symbol definitions + */ + createDefs: function() { + var defs = this.nodeFactory(this.container.id + "_defs", "defs"); + this.rendererRoot.appendChild(defs); + return defs; + }, + + /************************************** + * * + * GEOMETRY DRAWING FUNCTIONS * + * * + **************************************/ + + /** + * Method: drawPoint + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or false if the renderer could not draw the point + */ + drawPoint: function(node, geometry) { + return this.drawCircle(node, geometry, 1); + }, + + /** + * Method: drawCircle + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * radius - {Float} + * + * Returns: + * {DOMElement} or false if the renderer could not draw the circle + */ + drawCircle: function(node, geometry, radius) { + var resolution = this.getResolution(); + var x = ((geometry.x - this.featureDx) / resolution + this.left); + var y = (this.top - geometry.y / resolution); + + if (this.inValidRange(x, y)) { + node.setAttributeNS(null, "cx", x); + node.setAttributeNS(null, "cy", y); + node.setAttributeNS(null, "r", radius); + return node; + } else { + return false; + } + + }, + + /** + * Method: drawLineString + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or null if the renderer could not draw all components of + * the linestring, or false if nothing could be drawn + */ + drawLineString: function(node, geometry) { + var componentsResult = this.getComponentsString(geometry.components); + if (componentsResult.path) { + node.setAttributeNS(null, "points", componentsResult.path); + return (componentsResult.complete ? node : null); + } else { + return false; + } + }, + + /** + * Method: drawLinearRing + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or null if the renderer could not draw all components + * of the linear ring, or false if nothing could be drawn + */ + drawLinearRing: function(node, geometry) { + var componentsResult = this.getComponentsString(geometry.components); + if (componentsResult.path) { + node.setAttributeNS(null, "points", componentsResult.path); + return (componentsResult.complete ? node : null); + } else { + return false; + } + }, + + /** + * Method: drawPolygon + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or null if the renderer could not draw all components + * of the polygon, or false if nothing could be drawn + */ + drawPolygon: function(node, geometry) { + var d = ""; + var draw = true; + var complete = true; + var linearRingResult, path; + for (var j=0, len=geometry.components.length; j<len; j++) { + d += " M"; + linearRingResult = this.getComponentsString( + geometry.components[j].components, " "); + path = linearRingResult.path; + if (path) { + d += " " + path; + complete = linearRingResult.complete && complete; + } else { + draw = false; + } + } + d += " z"; + if (draw) { + node.setAttributeNS(null, "d", d); + node.setAttributeNS(null, "fill-rule", "evenodd"); + return complete ? node : null; + } else { + return false; + } + }, + + /** + * Method: drawRectangle + * This method is only called by the renderer itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or false if the renderer could not draw the rectangle + */ + drawRectangle: function(node, geometry) { + var resolution = this.getResolution(); + var x = ((geometry.x - this.featureDx) / resolution + this.left); + var y = (this.top - geometry.y / resolution); + + if (this.inValidRange(x, y)) { + node.setAttributeNS(null, "x", x); + node.setAttributeNS(null, "y", y); + node.setAttributeNS(null, "width", geometry.width / resolution); + node.setAttributeNS(null, "height", geometry.height / resolution); + return node; + } else { + return false; + } + }, + + /** + * Method: drawText + * This method is only called by the renderer itself. + * + * Parameters: + * featureId - {String} + * style - + * location - {<OpenLayers.Geometry.Point>} + */ + drawText: function(featureId, style, location) { + var drawOutline = (!!style.labelOutlineWidth); + // First draw text in halo color and size and overlay the + // normal text afterwards + if (drawOutline) { + var outlineStyle = OpenLayers.Util.extend({}, style); + outlineStyle.fontColor = outlineStyle.labelOutlineColor; + outlineStyle.fontStrokeColor = outlineStyle.labelOutlineColor; + outlineStyle.fontStrokeWidth = style.labelOutlineWidth; + if (style.labelOutlineOpacity) { + outlineStyle.fontOpacity = style.labelOutlineOpacity; + } + delete outlineStyle.labelOutlineWidth; + this.drawText(featureId, outlineStyle, location); + } + + var resolution = this.getResolution(); + + var x = ((location.x - this.featureDx) / resolution + this.left); + var y = (location.y / resolution - this.top); + + var suffix = (drawOutline)?this.LABEL_OUTLINE_SUFFIX:this.LABEL_ID_SUFFIX; + var label = this.nodeFactory(featureId + suffix, "text"); + + label.setAttributeNS(null, "x", x); + label.setAttributeNS(null, "y", -y); + + if (style.fontColor) { + label.setAttributeNS(null, "fill", style.fontColor); + } + if (style.fontStrokeColor) { + label.setAttributeNS(null, "stroke", style.fontStrokeColor); + } + if (style.fontStrokeWidth) { + label.setAttributeNS(null, "stroke-width", style.fontStrokeWidth); + } + if (style.fontOpacity) { + label.setAttributeNS(null, "opacity", style.fontOpacity); + } + if (style.fontFamily) { + label.setAttributeNS(null, "font-family", style.fontFamily); + } + if (style.fontSize) { + label.setAttributeNS(null, "font-size", style.fontSize); + } + if (style.fontWeight) { + label.setAttributeNS(null, "font-weight", style.fontWeight); + } + if (style.fontStyle) { + label.setAttributeNS(null, "font-style", style.fontStyle); + } + if (style.labelSelect === true) { + label.setAttributeNS(null, "pointer-events", "visible"); + label._featureId = featureId; + } else { + label.setAttributeNS(null, "pointer-events", "none"); + } + var align = style.labelAlign || OpenLayers.Renderer.defaultSymbolizer.labelAlign; + label.setAttributeNS(null, "text-anchor", + OpenLayers.Renderer.SVG.LABEL_ALIGN[align[0]] || "middle"); + + if (OpenLayers.IS_GECKO === true) { + label.setAttributeNS(null, "dominant-baseline", + OpenLayers.Renderer.SVG.LABEL_ALIGN[align[1]] || "central"); + } + + var labelRows = style.label.split('\n'); + var numRows = labelRows.length; + while (label.childNodes.length > numRows) { + label.removeChild(label.lastChild); + } + for (var i = 0; i < numRows; i++) { + var tspan = this.nodeFactory(featureId + suffix + "_tspan_" + i, "tspan"); + if (style.labelSelect === true) { + tspan._featureId = featureId; + tspan._geometry = location; + tspan._geometryClass = location.CLASS_NAME; + } + if (OpenLayers.IS_GECKO === false) { + tspan.setAttributeNS(null, "baseline-shift", + OpenLayers.Renderer.SVG.LABEL_VSHIFT[align[1]] || "-35%"); + } + tspan.setAttribute("x", x); + if (i == 0) { + var vfactor = OpenLayers.Renderer.SVG.LABEL_VFACTOR[align[1]]; + if (vfactor == null) { + vfactor = -.5; + } + tspan.setAttribute("dy", (vfactor*(numRows-1)) + "em"); + } else { + tspan.setAttribute("dy", "1em"); + } + tspan.textContent = (labelRows[i] === '') ? ' ' : labelRows[i]; + if (!tspan.parentNode) { + label.appendChild(tspan); + } + } + + if (!label.parentNode) { + this.textRoot.appendChild(label); + } + }, + + /** + * Method: getComponentString + * + * Parameters: + * components - {Array(<OpenLayers.Geometry.Point>)} Array of points + * separator - {String} character between coordinate pairs. Defaults to "," + * + * Returns: + * {Object} hash with properties "path" (the string created from the + * components and "complete" (false if the renderer was unable to + * draw all components) + */ + getComponentsString: function(components, separator) { + var renderCmp = []; + var complete = true; + var len = components.length; + var strings = []; + var str, component; + for(var i=0; i<len; i++) { + component = components[i]; + renderCmp.push(component); + str = this.getShortString(component); + if (str) { + strings.push(str); + } else { + // The current component is outside the valid range. Let's + // see if the previous or next component is inside the range. + // If so, add the coordinate of the intersection with the + // valid range bounds. + if (i > 0) { + if (this.getShortString(components[i - 1])) { + strings.push(this.clipLine(components[i], + components[i-1])); + } + } + if (i < len - 1) { + if (this.getShortString(components[i + 1])) { + strings.push(this.clipLine(components[i], + components[i+1])); + } + } + complete = false; + } + } + + return { + path: strings.join(separator || ","), + complete: complete + }; + }, + + /** + * Method: clipLine + * Given two points (one inside the valid range, and one outside), + * clips the line betweeen the two points so that the new points are both + * inside the valid range. + * + * Parameters: + * badComponent - {<OpenLayers.Geometry.Point>} original geometry of the + * invalid point + * goodComponent - {<OpenLayers.Geometry.Point>} original geometry of the + * valid point + * Returns + * {String} the SVG coordinate pair of the clipped point (like + * getShortString), or an empty string if both passed componets are at + * the same point. + */ + clipLine: function(badComponent, goodComponent) { + if (goodComponent.equals(badComponent)) { + return ""; + } + var resolution = this.getResolution(); + var maxX = this.MAX_PIXEL - this.translationParameters.x; + var maxY = this.MAX_PIXEL - this.translationParameters.y; + var x1 = (goodComponent.x - this.featureDx) / resolution + this.left; + var y1 = this.top - goodComponent.y / resolution; + var x2 = (badComponent.x - this.featureDx) / resolution + this.left; + var y2 = this.top - badComponent.y / resolution; + var k; + if (x2 < -maxX || x2 > maxX) { + k = (y2 - y1) / (x2 - x1); + x2 = x2 < 0 ? -maxX : maxX; + y2 = y1 + (x2 - x1) * k; + } + if (y2 < -maxY || y2 > maxY) { + k = (x2 - x1) / (y2 - y1); + y2 = y2 < 0 ? -maxY : maxY; + x2 = x1 + (y2 - y1) * k; + } + return x2 + "," + y2; + }, + + /** + * Method: getShortString + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns: + * {String} or false if point is outside the valid range + */ + getShortString: function(point) { + var resolution = this.getResolution(); + var x = ((point.x - this.featureDx) / resolution + this.left); + var y = (this.top - point.y / resolution); + + if (this.inValidRange(x, y)) { + return x + "," + y; + } else { + return false; + } + }, + + /** + * Method: getPosition + * Finds the position of an svg node. + * + * Parameters: + * node - {DOMElement} + * + * Returns: + * {Object} hash with x and y properties, representing the coordinates + * within the svg coordinate system + */ + getPosition: function(node) { + return({ + x: parseFloat(node.getAttributeNS(null, "cx")), + y: parseFloat(node.getAttributeNS(null, "cy")) + }); + }, + + /** + * Method: importSymbol + * add a new symbol definition from the rendererer's symbol hash + * + * Parameters: + * graphicName - {String} name of the symbol to import + * + * Returns: + * {DOMElement} - the imported symbol + */ + importSymbol: function (graphicName) { + if (!this.defs) { + // create svg defs tag + this.defs = this.createDefs(); + } + var id = this.container.id + "-" + graphicName; + + // check if symbol already exists in the defs + var existing = document.getElementById(id); + if (existing != null) { + return existing; + } + + var symbol = OpenLayers.Renderer.symbol[graphicName]; + if (!symbol) { + throw new Error(graphicName + ' is not a valid symbol name'); + } + + var symbolNode = this.nodeFactory(id, "symbol"); + var node = this.nodeFactory(null, "polygon"); + symbolNode.appendChild(node); + var symbolExtent = new OpenLayers.Bounds( + Number.MAX_VALUE, Number.MAX_VALUE, 0, 0); + + var points = []; + var x,y; + for (var i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + symbolExtent.left = Math.min(symbolExtent.left, x); + symbolExtent.bottom = Math.min(symbolExtent.bottom, y); + symbolExtent.right = Math.max(symbolExtent.right, x); + symbolExtent.top = Math.max(symbolExtent.top, y); + points.push(x, ",", y); + } + + node.setAttributeNS(null, "points", points.join(" ")); + + var width = symbolExtent.getWidth(); + var height = symbolExtent.getHeight(); + // create a viewBox three times as large as the symbol itself, + // to allow for strokeWidth being displayed correctly at the corners. + var viewBox = [symbolExtent.left - width, + symbolExtent.bottom - height, width * 3, height * 3]; + symbolNode.setAttributeNS(null, "viewBox", viewBox.join(" ")); + this.symbolMetrics[id] = [ + Math.max(width, height), + symbolExtent.getCenterLonLat().lon, + symbolExtent.getCenterLonLat().lat + ]; + + this.defs.appendChild(symbolNode); + return symbolNode; + }, + + /** + * Method: getFeatureIdFromEvent + * + * Parameters: + * evt - {Object} An <OpenLayers.Event> object + * + * Returns: + * {String} A feature id or undefined. + */ + getFeatureIdFromEvent: function(evt) { + var featureId = OpenLayers.Renderer.Elements.prototype.getFeatureIdFromEvent.apply(this, arguments); + if(!featureId) { + var target = evt.target; + featureId = target.parentNode && target != this.rendererRoot ? + target.parentNode._featureId : undefined; + } + return featureId; + }, + + CLASS_NAME: "OpenLayers.Renderer.SVG" +}); + +/** + * Constant: OpenLayers.Renderer.SVG.LABEL_ALIGN + * {Object} + */ +OpenLayers.Renderer.SVG.LABEL_ALIGN = { + "l": "start", + "r": "end", + "b": "bottom", + "t": "hanging" +}; + +/** + * Constant: OpenLayers.Renderer.SVG.LABEL_VSHIFT + * {Object} + */ +OpenLayers.Renderer.SVG.LABEL_VSHIFT = { + // according to + // http://www.w3.org/Graphics/SVG/Test/20061213/htmlObjectHarness/full-text-align-02-b.html + // a baseline-shift of -70% shifts the text exactly from the + // bottom to the top of the baseline, so -35% moves the text to + // the center of the baseline. + "t": "-70%", + "b": "0" +}; + +/** + * Constant: OpenLayers.Renderer.SVG.LABEL_VFACTOR + * {Object} + */ +OpenLayers.Renderer.SVG.LABEL_VFACTOR = { + "t": 0, + "b": -1 +}; + +/** + * Function: OpenLayers.Renderer.SVG.preventDefault + * *Deprecated*. Use <OpenLayers.Event.preventDefault> method instead. + * Used to prevent default events (especially opening images in a new tab on + * ctrl-click) from being executed for externalGraphic symbols + */ +OpenLayers.Renderer.SVG.preventDefault = function(e) { + OpenLayers.Event.preventDefault(e); +}; +/* ====================================================================== + OpenLayers/Format/JSON.js + ====================================================================== */ + +/* 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. */ + +/** + * Note: + * This work draws heavily from the public domain JSON serializer/deserializer + * at http://www.json.org/json.js. Rewritten so that it doesn't modify + * basic data prototypes. + */ + +/** + * @requires OpenLayers/Format.js + */ + +/** + * Class: OpenLayers.Format.JSON + * A parser to read/write JSON safely. Create a new instance with the + * <OpenLayers.Format.JSON> constructor. + * + * Inherits from: + * - <OpenLayers.Format> + */ +OpenLayers.Format.JSON = OpenLayers.Class(OpenLayers.Format, { + + /** + * APIProperty: indent + * {String} For "pretty" printing, the indent string will be used once for + * each indentation level. + */ + indent: " ", + + /** + * APIProperty: space + * {String} For "pretty" printing, the space string will be used after + * the ":" separating a name/value pair. + */ + space: " ", + + /** + * APIProperty: newline + * {String} For "pretty" printing, the newline string will be used at the + * end of each name/value pair or array item. + */ + newline: "\n", + + /** + * Property: level + * {Integer} For "pretty" printing, this is incremented/decremented during + * serialization. + */ + level: 0, + + /** + * Property: pretty + * {Boolean} Serialize with extra whitespace for structure. This is set + * by the <write> method. + */ + pretty: false, + + /** + * Property: nativeJSON + * {Boolean} Does the browser support native json? + */ + nativeJSON: (function() { + return !!(window.JSON && typeof JSON.parse == "function" && typeof JSON.stringify == "function"); + })(), + + /** + * Constructor: OpenLayers.Format.JSON + * Create a new parser for JSON. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + + /** + * APIMethod: read + * Deserialize a json string. + * + * Parameters: + * json - {String} A JSON string + * filter - {Function} A function which will be called for every key and + * value at every level of the final result. Each value will be + * replaced by the result of the filter function. This can be used to + * reform generic objects into instances of classes, or to transform + * date strings into Date objects. + * + * Returns: + * {Object} An object, array, string, or number . + */ + read: function(json, filter) { + var object; + if (this.nativeJSON) { + object = JSON.parse(json, filter); + } else try { + /** + * Parsing happens in three stages. In the first stage, we run the + * text against a regular expression which looks for non-JSON + * characters. We are especially concerned with '()' and 'new' + * because they can cause invocation, and '=' because it can + * cause mutation. But just to be safe, we will reject all + * unexpected characters. + */ + if (/^[\],:{}\s]*$/.test(json.replace(/\\["\\\/bfnrtu]/g, '@'). + replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). + replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + /** + * In the second stage we use the eval function to compile the + * text into a JavaScript structure. The '{' operator is + * subject to a syntactic ambiguity in JavaScript - it can + * begin a block or an object literal. We wrap the text in + * parens to eliminate the ambiguity. + */ + object = eval('(' + json + ')'); + + /** + * In the optional third stage, we recursively walk the new + * structure, passing each name/value pair to a filter + * function for possible transformation. + */ + if(typeof filter === 'function') { + function walk(k, v) { + if(v && typeof v === 'object') { + for(var i in v) { + if(v.hasOwnProperty(i)) { + v[i] = walk(i, v[i]); + } + } + } + return filter(k, v); + } + object = walk('', object); + } + } + } catch(e) { + // Fall through if the regexp test fails. + } + + if(this.keepData) { + this.data = object; + } + + return object; + }, + + /** + * APIMethod: write + * Serialize an object into a JSON string. + * + * Parameters: + * value - {String} The object, array, string, number, boolean or date + * to be serialized. + * pretty - {Boolean} Structure the output with newlines and indentation. + * Default is false. + * + * Returns: + * {String} The JSON string representation of the input value. + */ + write: function(value, pretty) { + this.pretty = !!pretty; + var json = null; + var type = typeof value; + if(this.serialize[type]) { + try { + json = (!this.pretty && this.nativeJSON) ? + JSON.stringify(value) : + this.serialize[type].apply(this, [value]); + } catch(err) { + OpenLayers.Console.error("Trouble serializing: " + err); + } + } + return json; + }, + + /** + * Method: writeIndent + * Output an indentation string depending on the indentation level. + * + * Returns: + * {String} An appropriate indentation string. + */ + writeIndent: function() { + var pieces = []; + if(this.pretty) { + for(var i=0; i<this.level; ++i) { + pieces.push(this.indent); + } + } + return pieces.join(''); + }, + + /** + * Method: writeNewline + * Output a string representing a newline if in pretty printing mode. + * + * Returns: + * {String} A string representing a new line. + */ + writeNewline: function() { + return (this.pretty) ? this.newline : ''; + }, + + /** + * Method: writeSpace + * Output a string representing a space if in pretty printing mode. + * + * Returns: + * {String} A space. + */ + writeSpace: function() { + return (this.pretty) ? this.space : ''; + }, + + /** + * Property: serialize + * Object with properties corresponding to the serializable data types. + * Property values are functions that do the actual serializing. + */ + serialize: { + /** + * Method: serialize.object + * Transform an object into a JSON string. + * + * Parameters: + * object - {Object} The object to be serialized. + * + * Returns: + * {String} A JSON string representing the object. + */ + 'object': function(object) { + // three special objects that we want to treat differently + if(object == null) { + return "null"; + } + if(object.constructor == Date) { + return this.serialize.date.apply(this, [object]); + } + if(object.constructor == Array) { + return this.serialize.array.apply(this, [object]); + } + var pieces = ['{']; + this.level += 1; + var key, keyJSON, valueJSON; + + var addComma = false; + for(key in object) { + if(object.hasOwnProperty(key)) { + // recursive calls need to allow for sub-classing + keyJSON = OpenLayers.Format.JSON.prototype.write.apply(this, + [key, this.pretty]); + valueJSON = OpenLayers.Format.JSON.prototype.write.apply(this, + [object[key], this.pretty]); + if(keyJSON != null && valueJSON != null) { + if(addComma) { + pieces.push(','); + } + pieces.push(this.writeNewline(), this.writeIndent(), + keyJSON, ':', this.writeSpace(), valueJSON); + addComma = true; + } + } + } + + this.level -= 1; + pieces.push(this.writeNewline(), this.writeIndent(), '}'); + return pieces.join(''); + }, + + /** + * Method: serialize.array + * Transform an array into a JSON string. + * + * Parameters: + * array - {Array} The array to be serialized + * + * Returns: + * {String} A JSON string representing the array. + */ + 'array': function(array) { + var json; + var pieces = ['[']; + this.level += 1; + + for(var i=0, len=array.length; i<len; ++i) { + // recursive calls need to allow for sub-classing + json = OpenLayers.Format.JSON.prototype.write.apply(this, + [array[i], this.pretty]); + if(json != null) { + if(i > 0) { + pieces.push(','); + } + pieces.push(this.writeNewline(), this.writeIndent(), json); + } + } + + this.level -= 1; + pieces.push(this.writeNewline(), this.writeIndent(), ']'); + return pieces.join(''); + }, + + /** + * Method: serialize.string + * Transform a string into a JSON string. + * + * Parameters: + * string - {String} The string to be serialized + * + * Returns: + * {String} A JSON string representing the string. + */ + 'string': function(string) { + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can simply slap some quotes around it. + // Otherwise we must also replace the offending characters with safe + // sequences. + var m = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + if(/["\\\x00-\x1f]/.test(string)) { + return '"' + string.replace(/([\x00-\x1f\\"])/g, function(a, b) { + var c = m[b]; + if(c) { + return c; + } + c = b.charCodeAt(); + return '\\u00' + + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"'; + } + return '"' + string + '"'; + }, + + /** + * Method: serialize.number + * Transform a number into a JSON string. + * + * Parameters: + * number - {Number} The number to be serialized. + * + * Returns: + * {String} A JSON string representing the number. + */ + 'number': function(number) { + return isFinite(number) ? String(number) : "null"; + }, + + /** + * Method: serialize.boolean + * Transform a boolean into a JSON string. + * + * Parameters: + * bool - {Boolean} The boolean to be serialized. + * + * Returns: + * {String} A JSON string representing the boolean. + */ + 'boolean': function(bool) { + return String(bool); + }, + + /** + * Method: serialize.object + * Transform a date into a JSON string. + * + * Parameters: + * date - {Date} The date to be serialized. + * + * Returns: + * {String} A JSON string representing the date. + */ + 'date': function(date) { + function format(number) { + // Format integers to have at least two digits. + return (number < 10) ? '0' + number : number; + } + return '"' + date.getFullYear() + '-' + + format(date.getMonth() + 1) + '-' + + format(date.getDate()) + 'T' + + format(date.getHours()) + ':' + + format(date.getMinutes()) + ':' + + format(date.getSeconds()) + '"'; + } + }, + + CLASS_NAME: "OpenLayers.Format.JSON" + +}); +/* ====================================================================== + OpenLayers/Format/GeoJSON.js + ====================================================================== */ + +/* 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/Format/JSON.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Geometry/Point.js + * @requires OpenLayers/Geometry/MultiPoint.js + * @requires OpenLayers/Geometry/LineString.js + * @requires OpenLayers/Geometry/MultiLineString.js + * @requires OpenLayers/Geometry/Polygon.js + * @requires OpenLayers/Geometry/MultiPolygon.js + * @requires OpenLayers/Console.js + */ + +/** + * Class: OpenLayers.Format.GeoJSON + * Read and write GeoJSON. Create a new parser with the + * <OpenLayers.Format.GeoJSON> constructor. + * + * Inherits from: + * - <OpenLayers.Format.JSON> + */ +OpenLayers.Format.GeoJSON = OpenLayers.Class(OpenLayers.Format.JSON, { + + /** + * APIProperty: ignoreExtraDims + * {Boolean} Ignore dimensions higher than 2 when reading geometry + * coordinates. + */ + ignoreExtraDims: false, + + /** + * Constructor: OpenLayers.Format.GeoJSON + * Create a new parser for GeoJSON. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + + /** + * APIMethod: read + * Deserialize a GeoJSON string. + * + * Parameters: + * json - {String} A GeoJSON string + * type - {String} Optional string that determines the structure of + * the output. Supported values are "Geometry", "Feature", and + * "FeatureCollection". If absent or null, a default of + * "FeatureCollection" is assumed. + * filter - {Function} A function which will be called for every key and + * value at every level of the final result. Each value will be + * replaced by the result of the filter function. This can be used to + * reform generic objects into instances of classes, or to transform + * date strings into Date objects. + * + * Returns: + * {Object} The return depends on the value of the type argument. If type + * is "FeatureCollection" (the default), the return will be an array + * of <OpenLayers.Feature.Vector>. If type is "Geometry", the input json + * must represent a single geometry, and the return will be an + * <OpenLayers.Geometry>. If type is "Feature", the input json must + * represent a single feature, and the return will be an + * <OpenLayers.Feature.Vector>. + */ + read: function(json, type, filter) { + type = (type) ? type : "FeatureCollection"; + var results = null; + var obj = null; + if (typeof json == "string") { + obj = OpenLayers.Format.JSON.prototype.read.apply(this, + [json, filter]); + } else { + obj = json; + } + if(!obj) { + OpenLayers.Console.error("Bad JSON: " + json); + } else if(typeof(obj.type) != "string") { + OpenLayers.Console.error("Bad GeoJSON - no type: " + json); + } else if(this.isValidType(obj, type)) { + switch(type) { + case "Geometry": + try { + results = this.parseGeometry(obj); + } catch(err) { + OpenLayers.Console.error(err); + } + break; + case "Feature": + try { + results = this.parseFeature(obj); + results.type = "Feature"; + } catch(err) { + OpenLayers.Console.error(err); + } + break; + case "FeatureCollection": + // for type FeatureCollection, we allow input to be any type + results = []; + switch(obj.type) { + case "Feature": + try { + results.push(this.parseFeature(obj)); + } catch(err) { + results = null; + OpenLayers.Console.error(err); + } + break; + case "FeatureCollection": + for(var i=0, len=obj.features.length; i<len; ++i) { + try { + results.push(this.parseFeature(obj.features[i])); + } catch(err) { + results = null; + OpenLayers.Console.error(err); + } + } + break; + default: + try { + var geom = this.parseGeometry(obj); + results.push(new OpenLayers.Feature.Vector(geom)); + } catch(err) { + results = null; + OpenLayers.Console.error(err); + } + } + break; + } + } + return results; + }, + + /** + * Method: isValidType + * Check if a GeoJSON object is a valid representative of the given type. + * + * Returns: + * {Boolean} The object is valid GeoJSON object of the given type. + */ + isValidType: function(obj, type) { + var valid = false; + switch(type) { + case "Geometry": + if(OpenLayers.Util.indexOf( + ["Point", "MultiPoint", "LineString", "MultiLineString", + "Polygon", "MultiPolygon", "Box", "GeometryCollection"], + obj.type) == -1) { + // unsupported geometry type + OpenLayers.Console.error("Unsupported geometry type: " + + obj.type); + } else { + valid = true; + } + break; + case "FeatureCollection": + // allow for any type to be converted to a feature collection + valid = true; + break; + default: + // for Feature types must match + if(obj.type == type) { + valid = true; + } else { + OpenLayers.Console.error("Cannot convert types from " + + obj.type + " to " + type); + } + } + return valid; + }, + + /** + * Method: parseFeature + * Convert a feature object from GeoJSON into an + * <OpenLayers.Feature.Vector>. + * + * Parameters: + * obj - {Object} An object created from a GeoJSON object + * + * Returns: + * {<OpenLayers.Feature.Vector>} A feature. + */ + parseFeature: function(obj) { + var feature, geometry, attributes, bbox; + attributes = (obj.properties) ? obj.properties : {}; + bbox = (obj.geometry && obj.geometry.bbox) || obj.bbox; + try { + geometry = this.parseGeometry(obj.geometry); + } catch(err) { + // deal with bad geometries + throw err; + } + feature = new OpenLayers.Feature.Vector(geometry, attributes); + if(bbox) { + feature.bounds = OpenLayers.Bounds.fromArray(bbox); + } + if(obj.id) { + feature.fid = obj.id; + } + return feature; + }, + + /** + * Method: parseGeometry + * Convert a geometry object from GeoJSON into an <OpenLayers.Geometry>. + * + * Parameters: + * obj - {Object} An object created from a GeoJSON object + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + parseGeometry: function(obj) { + if (obj == null) { + return null; + } + var geometry, collection = false; + if(obj.type == "GeometryCollection") { + if(!(OpenLayers.Util.isArray(obj.geometries))) { + throw "GeometryCollection must have geometries array: " + obj; + } + var numGeom = obj.geometries.length; + var components = new Array(numGeom); + for(var i=0; i<numGeom; ++i) { + components[i] = this.parseGeometry.apply( + this, [obj.geometries[i]] + ); + } + geometry = new OpenLayers.Geometry.Collection(components); + collection = true; + } else { + if(!(OpenLayers.Util.isArray(obj.coordinates))) { + throw "Geometry must have coordinates array: " + obj; + } + if(!this.parseCoords[obj.type.toLowerCase()]) { + throw "Unsupported geometry type: " + obj.type; + } + try { + geometry = this.parseCoords[obj.type.toLowerCase()].apply( + this, [obj.coordinates] + ); + } catch(err) { + // deal with bad coordinates + throw err; + } + } + // We don't reproject collections because the children are reprojected + // for us when they are created. + if (this.internalProjection && this.externalProjection && !collection) { + geometry.transform(this.externalProjection, + this.internalProjection); + } + return geometry; + }, + + /** + * Property: parseCoords + * Object with properties corresponding to the GeoJSON geometry types. + * Property values are functions that do the actual parsing. + */ + parseCoords: { + /** + * Method: parseCoords.point + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "point": function(array) { + if (this.ignoreExtraDims == false && + array.length != 2) { + throw "Only 2D points are supported: " + array; + } + return new OpenLayers.Geometry.Point(array[0], array[1]); + }, + + /** + * Method: parseCoords.multipoint + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "multipoint": function(array) { + var points = []; + var p = null; + for(var i=0, len=array.length; i<len; ++i) { + try { + p = this.parseCoords["point"].apply(this, [array[i]]); + } catch(err) { + throw err; + } + points.push(p); + } + return new OpenLayers.Geometry.MultiPoint(points); + }, + + /** + * Method: parseCoords.linestring + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "linestring": function(array) { + var points = []; + var p = null; + for(var i=0, len=array.length; i<len; ++i) { + try { + p = this.parseCoords["point"].apply(this, [array[i]]); + } catch(err) { + throw err; + } + points.push(p); + } + return new OpenLayers.Geometry.LineString(points); + }, + + /** + * Method: parseCoords.multilinestring + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "multilinestring": function(array) { + var lines = []; + var l = null; + for(var i=0, len=array.length; i<len; ++i) { + try { + l = this.parseCoords["linestring"].apply(this, [array[i]]); + } catch(err) { + throw err; + } + lines.push(l); + } + return new OpenLayers.Geometry.MultiLineString(lines); + }, + + /** + * Method: parseCoords.polygon + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "polygon": function(array) { + var rings = []; + var r, l; + for(var i=0, len=array.length; i<len; ++i) { + try { + l = this.parseCoords["linestring"].apply(this, [array[i]]); + } catch(err) { + throw err; + } + r = new OpenLayers.Geometry.LinearRing(l.components); + rings.push(r); + } + return new OpenLayers.Geometry.Polygon(rings); + }, + + /** + * Method: parseCoords.multipolygon + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "multipolygon": function(array) { + var polys = []; + var p = null; + for(var i=0, len=array.length; i<len; ++i) { + try { + p = this.parseCoords["polygon"].apply(this, [array[i]]); + } catch(err) { + throw err; + } + polys.push(p); + } + return new OpenLayers.Geometry.MultiPolygon(polys); + }, + + /** + * Method: parseCoords.box + * Convert a coordinate array from GeoJSON into an + * <OpenLayers.Geometry>. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {<OpenLayers.Geometry>} A geometry. + */ + "box": function(array) { + if(array.length != 2) { + throw "GeoJSON box coordinates must have 2 elements"; + } + return new OpenLayers.Geometry.Polygon([ + new OpenLayers.Geometry.LinearRing([ + new OpenLayers.Geometry.Point(array[0][0], array[0][1]), + new OpenLayers.Geometry.Point(array[1][0], array[0][1]), + new OpenLayers.Geometry.Point(array[1][0], array[1][1]), + new OpenLayers.Geometry.Point(array[0][0], array[1][1]), + new OpenLayers.Geometry.Point(array[0][0], array[0][1]) + ]) + ]); + } + + }, + + /** + * APIMethod: write + * Serialize a feature, geometry, array of features into a GeoJSON string. + * + * Parameters: + * obj - {Object} An <OpenLayers.Feature.Vector>, <OpenLayers.Geometry>, + * or an array of features. + * pretty - {Boolean} Structure the output with newlines and indentation. + * Default is false. + * + * Returns: + * {String} The GeoJSON string representation of the input geometry, + * features, or array of features. + */ + write: function(obj, pretty) { + var geojson = { + "type": null + }; + if(OpenLayers.Util.isArray(obj)) { + geojson.type = "FeatureCollection"; + var numFeatures = obj.length; + geojson.features = new Array(numFeatures); + for(var i=0; i<numFeatures; ++i) { + var element = obj[i]; + if(!element instanceof OpenLayers.Feature.Vector) { + var msg = "FeatureCollection only supports collections " + + "of features: " + element; + throw msg; + } + geojson.features[i] = this.extract.feature.apply( + this, [element] + ); + } + } else if (obj.CLASS_NAME.indexOf("OpenLayers.Geometry") == 0) { + geojson = this.extract.geometry.apply(this, [obj]); + } else if (obj instanceof OpenLayers.Feature.Vector) { + geojson = this.extract.feature.apply(this, [obj]); + if(obj.layer && obj.layer.projection) { + geojson.crs = this.createCRSObject(obj); + } + } + return OpenLayers.Format.JSON.prototype.write.apply(this, + [geojson, pretty]); + }, + + /** + * Method: createCRSObject + * Create the CRS object for an object. + * + * Parameters: + * object - {<OpenLayers.Feature.Vector>} + * + * Returns: + * {Object} An object which can be assigned to the crs property + * of a GeoJSON object. + */ + createCRSObject: function(object) { + var proj = object.layer.projection.toString(); + var crs = {}; + if (proj.match(/epsg:/i)) { + var code = parseInt(proj.substring(proj.indexOf(":") + 1)); + if (code == 4326) { + crs = { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:OGC:1.3:CRS84" + } + }; + } else { + crs = { + "type": "name", + "properties": { + "name": "EPSG:" + code + } + }; + } + } + return crs; + }, + + /** + * Property: extract + * Object with properties corresponding to the GeoJSON types. + * Property values are functions that do the actual value extraction. + */ + extract: { + /** + * Method: extract.feature + * Return a partial GeoJSON object representing a single feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * + * Returns: + * {Object} An object representing the point. + */ + 'feature': function(feature) { + var geom = this.extract.geometry.apply(this, [feature.geometry]); + var json = { + "type": "Feature", + "properties": feature.attributes, + "geometry": geom + }; + if (feature.fid != null) { + json.id = feature.fid; + } + return json; + }, + + /** + * Method: extract.geometry + * Return a GeoJSON object representing a single geometry. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {Object} An object representing the geometry. + */ + 'geometry': function(geometry) { + if (geometry == null) { + return null; + } + if (this.internalProjection && this.externalProjection) { + geometry = geometry.clone(); + geometry.transform(this.internalProjection, + this.externalProjection); + } + var geometryType = geometry.CLASS_NAME.split('.')[2]; + var data = this.extract[geometryType.toLowerCase()].apply(this, [geometry]); + var json; + if(geometryType == "Collection") { + json = { + "type": "GeometryCollection", + "geometries": data + }; + } else { + json = { + "type": geometryType, + "coordinates": data + }; + } + + return json; + }, + + /** + * Method: extract.point + * Return an array of coordinates from a point. + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns: + * {Array} An array of coordinates representing the point. + */ + 'point': function(point) { + return [point.x, point.y]; + }, + + /** + * Method: extract.multipoint + * Return an array of point coordinates from a multipoint. + * + * Parameters: + * multipoint - {<OpenLayers.Geometry.MultiPoint>} + * + * Returns: + * {Array} An array of point coordinate arrays representing + * the multipoint. + */ + 'multipoint': function(multipoint) { + var array = []; + for(var i=0, len=multipoint.components.length; i<len; ++i) { + array.push(this.extract.point.apply(this, [multipoint.components[i]])); + } + return array; + }, + + /** + * Method: extract.linestring + * Return an array of coordinate arrays from a linestring. + * + * Parameters: + * linestring - {<OpenLayers.Geometry.LineString>} + * + * Returns: + * {Array} An array of coordinate arrays representing + * the linestring. + */ + 'linestring': function(linestring) { + var array = []; + for(var i=0, len=linestring.components.length; i<len; ++i) { + array.push(this.extract.point.apply(this, [linestring.components[i]])); + } + return array; + }, + + /** + * Method: extract.multilinestring + * Return an array of linestring arrays from a linestring. + * + * Parameters: + * multilinestring - {<OpenLayers.Geometry.MultiLineString>} + * + * Returns: + * {Array} An array of linestring arrays representing + * the multilinestring. + */ + 'multilinestring': function(multilinestring) { + var array = []; + for(var i=0, len=multilinestring.components.length; i<len; ++i) { + array.push(this.extract.linestring.apply(this, [multilinestring.components[i]])); + } + return array; + }, + + /** + * Method: extract.polygon + * Return an array of linear ring arrays from a polygon. + * + * Parameters: + * polygon - {<OpenLayers.Geometry.Polygon>} + * + * Returns: + * {Array} An array of linear ring arrays representing the polygon. + */ + 'polygon': function(polygon) { + var array = []; + for(var i=0, len=polygon.components.length; i<len; ++i) { + array.push(this.extract.linestring.apply(this, [polygon.components[i]])); + } + return array; + }, + + /** + * Method: extract.multipolygon + * Return an array of polygon arrays from a multipolygon. + * + * Parameters: + * multipolygon - {<OpenLayers.Geometry.MultiPolygon>} + * + * Returns: + * {Array} An array of polygon arrays representing + * the multipolygon + */ + 'multipolygon': function(multipolygon) { + var array = []; + for(var i=0, len=multipolygon.components.length; i<len; ++i) { + array.push(this.extract.polygon.apply(this, [multipolygon.components[i]])); + } + return array; + }, + + /** + * Method: extract.collection + * Return an array of geometries from a geometry collection. + * + * Parameters: + * collection - {<OpenLayers.Geometry.Collection>} + * + * Returns: + * {Array} An array of geometry objects representing the geometry + * collection. + */ + 'collection': function(collection) { + var len = collection.components.length; + var array = new Array(len); + for(var i=0; i<len; ++i) { + array[i] = this.extract.geometry.apply( + this, [collection.components[i]] + ); + } + return array; + } + + + }, + + CLASS_NAME: "OpenLayers.Format.GeoJSON" + +}); +/* ====================================================================== + OpenLayers/Control/DrawFeature.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Feature/Vector.js + */ + +/** + * Class: OpenLayers.Control.DrawFeature + * The DrawFeature control draws point, line or polygon features on a vector + * layer when active. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.DrawFeature = OpenLayers.Class(OpenLayers.Control, { + + /** + * Property: layer + * {<OpenLayers.Layer.Vector>} + */ + layer: null, + + /** + * Property: callbacks + * {Object} The functions that are sent to the handler for callback + */ + callbacks: null, + + /** + * APIProperty: events + * {<OpenLayers.Events>} Events instance for listeners and triggering + * control specific events. + * + * Register a listener for a particular event with the following syntax: + * (code) + * control.events.register(type, obj, listener); + * (end) + * + * Supported event types (in addition to those from <OpenLayers.Control.events>): + * featureadded - Triggered when a feature is added + */ + + /** + * APIProperty: multi + * {Boolean} Cast features to multi-part geometries before passing to the + * layer. Default is false. + */ + multi: false, + + /** + * APIProperty: featureAdded + * {Function} Called after each feature is added + */ + featureAdded: function() {}, + + /** + * APIProperty: handlerOptions + * {Object} Used to set non-default properties on the control's handler + */ + + /** + * Constructor: OpenLayers.Control.DrawFeature + * + * Parameters: + * layer - {<OpenLayers.Layer.Vector>} + * handler - {<OpenLayers.Handler>} + * options - {Object} + */ + initialize: function(layer, handler, options) { + OpenLayers.Control.prototype.initialize.apply(this, [options]); + this.callbacks = OpenLayers.Util.extend( + { + done: this.drawFeature, + modify: function(vertex, feature) { + this.layer.events.triggerEvent( + "sketchmodified", {vertex: vertex, feature: feature} + ); + }, + create: function(vertex, feature) { + this.layer.events.triggerEvent( + "sketchstarted", {vertex: vertex, feature: feature} + ); + } + }, + this.callbacks + ); + this.layer = layer; + this.handlerOptions = this.handlerOptions || {}; + this.handlerOptions.layerOptions = OpenLayers.Util.applyDefaults( + this.handlerOptions.layerOptions, { + renderers: layer.renderers, rendererOptions: layer.rendererOptions + } + ); + if (!("multi" in this.handlerOptions)) { + this.handlerOptions.multi = this.multi; + } + var sketchStyle = this.layer.styleMap && this.layer.styleMap.styles.temporary; + if(sketchStyle) { + this.handlerOptions.layerOptions = OpenLayers.Util.applyDefaults( + this.handlerOptions.layerOptions, + {styleMap: new OpenLayers.StyleMap({"default": sketchStyle})} + ); + } + this.handler = new handler(this, this.callbacks, this.handlerOptions); + }, + + /** + * Method: drawFeature + */ + drawFeature: function(geometry) { + var feature = new OpenLayers.Feature.Vector(geometry); + var proceed = this.layer.events.triggerEvent( + "sketchcomplete", {feature: feature} + ); + if(proceed !== false) { + feature.state = OpenLayers.State.INSERT; + this.layer.addFeatures([feature]); + this.featureAdded(feature); + this.events.triggerEvent("featureadded",{feature : feature}); + } + }, + + /** + * APIMethod: insertXY + * Insert a point in the current sketch given x & y coordinates. + * + * Parameters: + * x - {Number} The x-coordinate of the point. + * y - {Number} The y-coordinate of the point. + */ + insertXY: function(x, y) { + if (this.handler && this.handler.line) { + this.handler.insertXY(x, y); + } + }, + + /** + * APIMethod: insertDeltaXY + * Insert a point given offsets from the previously inserted point. + * + * Parameters: + * dx - {Number} The x-coordinate offset of the point. + * dy - {Number} The y-coordinate offset of the point. + */ + insertDeltaXY: function(dx, dy) { + if (this.handler && this.handler.line) { + this.handler.insertDeltaXY(dx, dy); + } + }, + + /** + * APIMethod: insertDirectionLength + * Insert a point in the current sketch given a direction and a length. + * + * Parameters: + * direction - {Number} Degrees clockwise from the positive x-axis. + * length - {Number} Distance from the previously drawn point. + */ + insertDirectionLength: function(direction, length) { + if (this.handler && this.handler.line) { + this.handler.insertDirectionLength(direction, length); + } + }, + + /** + * APIMethod: insertDeflectionLength + * Insert a point in the current sketch given a deflection and a length. + * The deflection should be degrees clockwise from the previously + * digitized segment. + * + * Parameters: + * deflection - {Number} Degrees clockwise from the previous segment. + * length - {Number} Distance from the previously drawn point. + */ + insertDeflectionLength: function(deflection, length) { + if (this.handler && this.handler.line) { + this.handler.insertDeflectionLength(deflection, length); + } + }, + + /** + * APIMethod: undo + * Remove the most recently added point in the current sketch geometry. + * + * Returns: + * {Boolean} An edit was undone. + */ + undo: function() { + return this.handler.undo && this.handler.undo(); + }, + + /** + * APIMethod: redo + * Reinsert the most recently removed point resulting from an <undo> call. + * The undo stack is deleted whenever a point is added by other means. + * + * Returns: + * {Boolean} An edit was redone. + */ + redo: function() { + return this.handler.redo && this.handler.redo(); + }, + + /** + * APIMethod: finishSketch + * Finishes the sketch without including the currently drawn point. + * This method can be called to terminate drawing programmatically + * instead of waiting for the user to end the sketch. + */ + finishSketch: function() { + this.handler.finishGeometry(); + }, + + /** + * APIMethod: cancel + * Cancel the current sketch. This removes the current sketch and keeps + * the drawing control active. + */ + cancel: function() { + this.handler.cancel(); + }, + + CLASS_NAME: "OpenLayers.Control.DrawFeature" +}); +/* ====================================================================== + OpenLayers/Handler/Pinch.js + ====================================================================== */ + +/* 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/Handler.js + */ + +/** + * Class: OpenLayers.Handler.Pinch + * The pinch handler is used to deal with sequences of browser events related + * to pinch gestures. The handler is used by controls that want to know + * when a pinch sequence begins, when a pinch is happening, and when it has + * finished. + * + * Controls that use the pinch handler typically construct it with callbacks + * for 'start', 'move', and 'done'. Callbacks for these keys are + * called when the pinch begins, with each change, and when the pinch is + * done. + * + * Create a new pinch handler with the <OpenLayers.Handler.Pinch> constructor. + * + * Inherits from: + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, { + + /** + * Property: started + * {Boolean} When a touchstart event is received, we want to record it, + * but not set 'pinching' until the touchmove get started after + * starting. + */ + started: false, + + /** + * Property: stopDown + * {Boolean} Stop propagation of touchstart events from getting to + * listeners on the same element. Default is false. + */ + stopDown: false, + + /** + * Property: pinching + * {Boolean} + */ + pinching: false, + + /** + * Property: last + * {Object} Object that store informations related to pinch last touch. + */ + last: null, + + /** + * Property: start + * {Object} Object that store informations related to pinch touchstart. + */ + start: null, + + /** + * Constructor: OpenLayers.Handler.Pinch + * Returns OpenLayers.Handler.Pinch + * + * Parameters: + * control - {<OpenLayers.Control>} The control that is making use of + * this handler. If a handler is being used without a control, the + * handlers setMap method must be overridden to deal properly with + * the map. + * callbacks - {Object} An object containing functions to be called when + * the pinch operation start, change, or is finished. The callbacks + * should expect to receive an object argument, which contains + * information about scale, distance, and position of touch points. + * options - {Object} + */ + + /** + * Method: touchstart + * Handle touchstart events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchstart: function(evt) { + var propagate = true; + this.pinching = false; + if (OpenLayers.Event.isMultiTouch(evt)) { + this.started = true; + this.last = this.start = { + distance: this.getDistance(evt.touches), + delta: 0, + scale: 1 + }; + this.callback("start", [evt, this.start]); + propagate = !this.stopDown; + } else if (this.started) { + // Some webkit versions send fake single-touch events during + // multitouch, which cause the drag handler to trigger + return false; + } else { + this.started = false; + this.start = null; + this.last = null; + } + // prevent document dragging + OpenLayers.Event.preventDefault(evt); + return propagate; + }, + + /** + * Method: touchmove + * Handle touchmove events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchmove: function(evt) { + if (this.started && OpenLayers.Event.isMultiTouch(evt)) { + this.pinching = true; + var current = this.getPinchData(evt); + this.callback("move", [evt, current]); + this.last = current; + // prevent document dragging + OpenLayers.Event.stop(evt); + } else if (this.started) { + // Some webkit versions send fake single-touch events during + // multitouch, which cause the drag handler to trigger + return false; + } + return true; + }, + + /** + * Method: touchend + * Handle touchend events + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Let the event propagate. + */ + touchend: function(evt) { + if (this.started && !OpenLayers.Event.isMultiTouch(evt)) { + this.started = false; + this.pinching = false; + this.callback("done", [evt, this.start, this.last]); + this.start = null; + this.last = null; + return false; + } + return true; + }, + + /** + * Method: activate + * Activate the handler. + * + * Returns: + * {Boolean} The handler was successfully activated. + */ + activate: function() { + var activated = false; + if (OpenLayers.Handler.prototype.activate.apply(this, arguments)) { + this.pinching = false; + activated = true; + } + return activated; + }, + + /** + * Method: deactivate + * Deactivate the handler. + * + * Returns: + * {Boolean} The handler was successfully deactivated. + */ + deactivate: function() { + var deactivated = false; + if (OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { + this.started = false; + this.pinching = false; + this.start = null; + this.last = null; + deactivated = true; + } + return deactivated; + }, + + /** + * Method: getDistance + * Get the distance in pixels between two touches. + * + * Parameters: + * touches - {Array(Object)} + * + * Returns: + * {Number} The distance in pixels. + */ + getDistance: function(touches) { + var t0 = touches[0]; + var t1 = touches[1]; + return Math.sqrt( + Math.pow(t0.olClientX - t1.olClientX, 2) + + Math.pow(t0.olClientY - t1.olClientY, 2) + ); + }, + + + /** + * Method: getPinchData + * Get informations about the pinch event. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Object} Object that contains data about the current pinch. + */ + getPinchData: function(evt) { + var distance = this.getDistance(evt.touches); + var scale = distance / this.start.distance; + return { + distance: distance, + delta: this.last.distance - distance, + scale: scale + }; + }, + + CLASS_NAME: "OpenLayers.Handler.Pinch" +}); + +/* ====================================================================== + OpenLayers/Handler/Polygon.js + ====================================================================== */ + +/* 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/Handler/Path.js + * @requires OpenLayers/Geometry/Polygon.js + */ + +/** + * Class: OpenLayers.Handler.Polygon + * Handler to draw a polygon on the map. Polygon is displayed on mouse down, + * moves on mouse move, and is finished on mouse up. + * + * Inherits from: + * - <OpenLayers.Handler.Path> + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Polygon = OpenLayers.Class(OpenLayers.Handler.Path, { + + /** + * APIProperty: holeModifier + * {String} Key modifier to trigger hole digitizing. Acceptable values are + * "altKey", "shiftKey", or "ctrlKey". If not set, no hole digitizing + * will take place. Default is null. + */ + holeModifier: null, + + /** + * Property: drawingHole + * {Boolean} Currently drawing an interior ring. + */ + drawingHole: false, + + /** + * Property: polygon + * {<OpenLayers.Feature.Vector>} + */ + polygon: null, + + /** + * Constructor: OpenLayers.Handler.Polygon + * Create a Polygon Handler. + * + * Parameters: + * control - {<OpenLayers.Control>} The control that owns this handler + * callbacks - {Object} An object with a properties whose values are + * functions. Various callbacks described below. + * options - {Object} An optional object with properties to be set on the + * handler + * + * Named callbacks: + * create - Called when a sketch is first created. Callback called with + * the creation point geometry and sketch feature. + * modify - Called with each move of a vertex with the vertex (point) + * geometry and the sketch feature. + * point - Called as each point is added. Receives the new point geometry. + * done - Called when the point drawing is finished. The callback will + * recieve a single argument, the polygon geometry. + * cancel - Called when the handler is deactivated while drawing. The + * cancel callback will receive a geometry. + */ + + /** + * Method: createFeature + * Add temporary geometries + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} The initial pixel location for the new + * feature. + */ + createFeature: function(pixel) { + var lonlat = this.layer.getLonLatFromViewPortPx(pixel); + var geometry = new OpenLayers.Geometry.Point( + lonlat.lon, lonlat.lat + ); + this.point = new OpenLayers.Feature.Vector(geometry); + this.line = new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.LinearRing([this.point.geometry]) + ); + this.polygon = new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Polygon([this.line.geometry]) + ); + this.callback("create", [this.point.geometry, this.getSketch()]); + this.point.geometry.clearBounds(); + this.layer.addFeatures([this.polygon, this.point], {silent: true}); + }, + + /** + * Method: addPoint + * Add point to geometry. + * + * Parameters: + * pixel - {<OpenLayers.Pixel>} The pixel location for the new point. + */ + addPoint: function(pixel) { + if(!this.drawingHole && this.holeModifier && + this.evt && this.evt[this.holeModifier]) { + var geometry = this.point.geometry; + var features = this.control.layer.features; + var candidate, polygon; + // look for intersections, last drawn gets priority + for (var i=features.length-1; i>=0; --i) { + candidate = features[i].geometry; + if ((candidate instanceof OpenLayers.Geometry.Polygon || + candidate instanceof OpenLayers.Geometry.MultiPolygon) && + candidate.intersects(geometry)) { + polygon = features[i]; + this.control.layer.removeFeatures([polygon], {silent: true}); + this.control.layer.events.registerPriority( + "sketchcomplete", this, this.finalizeInteriorRing + ); + this.control.layer.events.registerPriority( + "sketchmodified", this, this.enforceTopology + ); + polygon.geometry.addComponent(this.line.geometry); + this.polygon = polygon; + this.drawingHole = true; + break; + } + } + } + OpenLayers.Handler.Path.prototype.addPoint.apply(this, arguments); + }, + + /** + * Method: getCurrentPointIndex + * + * Returns: + * {Number} The index of the most recently drawn point. + */ + getCurrentPointIndex: function() { + return this.line.geometry.components.length - 2; + }, + + /** + * Method: enforceTopology + * Simple topology enforcement for drawing interior rings. Ensures vertices + * of interior rings are contained by exterior ring. Other topology + * rules are enforced in <finalizeInteriorRing> to allow drawing of + * rings that intersect only during the sketch (e.g. a "C" shaped ring + * that nearly encloses another ring). + */ + enforceTopology: function(event) { + var point = event.vertex; + var components = this.line.geometry.components; + // ensure that vertices of interior ring are contained by exterior ring + if (!this.polygon.geometry.intersects(point)) { + var last = components[components.length-3]; + point.x = last.x; + point.y = last.y; + } + }, + + /** + * Method: finishGeometry + * Finish the geometry and send it back to the control. + */ + finishGeometry: function() { + var index = this.line.geometry.components.length - 2; + this.line.geometry.removeComponent(this.line.geometry.components[index]); + this.removePoint(); + this.finalize(); + }, + + /** + * Method: finalizeInteriorRing + * Enforces that new ring has some area and doesn't contain vertices of any + * other rings. + */ + finalizeInteriorRing: function() { + var ring = this.line.geometry; + // ensure that ring has some area + var modified = (ring.getArea() !== 0); + if (modified) { + // ensure that new ring doesn't intersect any other rings + var rings = this.polygon.geometry.components; + for (var i=rings.length-2; i>=0; --i) { + if (ring.intersects(rings[i])) { + modified = false; + break; + } + } + if (modified) { + // ensure that new ring doesn't contain any other rings + var target; + outer: for (var i=rings.length-2; i>0; --i) { + var points = rings[i].components; + for (var j=0, jj=points.length; j<jj; ++j) { + if (ring.containsPoint(points[j])) { + modified = false; + break outer; + } + } + } + } + } + if (modified) { + if (this.polygon.state !== OpenLayers.State.INSERT) { + this.polygon.state = OpenLayers.State.UPDATE; + } + } else { + this.polygon.geometry.removeComponent(ring); + } + this.restoreFeature(); + return false; + }, + + /** + * APIMethod: cancel + * Finish the geometry and call the "cancel" callback. + */ + cancel: function() { + if (this.drawingHole) { + this.polygon.geometry.removeComponent(this.line.geometry); + this.restoreFeature(true); + } + return OpenLayers.Handler.Path.prototype.cancel.apply(this, arguments); + }, + + /** + * Method: restoreFeature + * Move the feature from the sketch layer to the target layer. + * + * Properties: + * cancel - {Boolean} Cancel drawing. If falsey, the "sketchcomplete" event + * will be fired. + */ + restoreFeature: function(cancel) { + this.control.layer.events.unregister( + "sketchcomplete", this, this.finalizeInteriorRing + ); + this.control.layer.events.unregister( + "sketchmodified", this, this.enforceTopology + ); + this.layer.removeFeatures([this.polygon], {silent: true}); + this.control.layer.addFeatures([this.polygon], {silent: true}); + this.drawingHole = false; + if (!cancel) { + // Re-trigger "sketchcomplete" so other listeners can do their + // business. While this is somewhat sloppy (if a listener is + // registered with registerPriority - not common - between the start + // and end of a single ring drawing - very uncommon - it will be + // called twice). + // TODO: In 3.0, collapse sketch handlers into geometry specific + // drawing controls. + this.control.layer.events.triggerEvent( + "sketchcomplete", {feature : this.polygon} + ); + } + }, + + /** + * Method: destroyFeature + * Destroy temporary geometries + * + * Parameters: + * force - {Boolean} Destroy even if persist is true. + */ + destroyFeature: function(force) { + OpenLayers.Handler.Path.prototype.destroyFeature.call( + this, force); + this.polygon = null; + }, + + /** + * Method: drawFeature + * Render geometries on the temporary layer. + */ + drawFeature: function() { + this.layer.drawFeature(this.polygon, this.style); + this.layer.drawFeature(this.point, this.style); + }, + + /** + * Method: getSketch + * Return the sketch feature. + * + * Returns: + * {<OpenLayers.Feature.Vector>} + */ + getSketch: function() { + return this.polygon; + }, + + /** + * Method: getGeometry + * Return the sketch geometry. If <multi> is true, this will return + * a multi-part geometry. + * + * Returns: + * {<OpenLayers.Geometry.Polygon>} + */ + getGeometry: function() { + var geometry = this.polygon && this.polygon.geometry; + if(geometry && this.multi) { + geometry = new OpenLayers.Geometry.MultiPolygon([geometry]); + } + return geometry; + }, + + CLASS_NAME: "OpenLayers.Handler.Polygon" +}); +/* ====================================================================== + OpenLayers/Control/Geolocate.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Geometry/Point.js + * @requires OpenLayers/Projection.js + */ + +/** + * Class: OpenLayers.Control.Geolocate + * The Geolocate control wraps w3c geolocation API into control that can be + * bound to a map, and generate events on location update + * + * To use this control requires to load the proj4js library if the projection + * of the map is not EPSG:4326 or EPSG:900913. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.Geolocate = OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: events + * {<OpenLayers.Events>} Events instance for listeners and triggering + * control specific events. + * + * Register a listener for a particular event with the following syntax: + * (code) + * control.events.register(type, obj, listener); + * (end) + * + * Supported event types (in addition to those from <OpenLayers.Control.events>): + * locationupdated - Triggered when browser return a new position. Listeners will + * receive an object with a 'position' property which is the browser.geolocation.position + * native object, as well as a 'point' property which is the location transformed in the + * current map projection. + * locationfailed - Triggered when geolocation has failed + * locationuncapable - Triggered when control is activated on a browser + * which doesn't support geolocation + */ + + /** + * Property: geolocation + * {Object} The geolocation engine, as a property to be possibly mocked. + * This is set lazily to avoid a memory leak in IE9. + */ + geolocation: null, + + /** + * Property: available + * {Boolean} The navigator.geolocation object is available. + */ + available: ('geolocation' in navigator), + + /** + * APIProperty: bind + * {Boolean} If true, map center will be set on location update. + */ + bind: true, + + /** + * APIProperty: watch + * {Boolean} If true, position will be update regularly. + */ + watch: false, + + /** + * APIProperty: geolocationOptions + * {Object} Options to pass to the navigator's geolocation API. See + * <http://dev.w3.org/geo/api/spec-source.html>. No specific + * option is passed to the geolocation API by default. + */ + geolocationOptions: null, + + /** + * Constructor: OpenLayers.Control.Geolocate + * Create a new control to deal with browser geolocation API + * + */ + + /** + * Method: destroy + */ + destroy: function() { + this.deactivate(); + OpenLayers.Control.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: activate + * Activates the control. + * + * Returns: + * {Boolean} The control was effectively activated. + */ + activate: function () { + if (this.available && !this.geolocation) { + // set lazily to avoid IE9 memory leak + this.geolocation = navigator.geolocation; + } + if (!this.geolocation) { + this.events.triggerEvent("locationuncapable"); + return false; + } + if (OpenLayers.Control.prototype.activate.apply(this, arguments)) { + if (this.watch) { + this.watchId = this.geolocation.watchPosition( + OpenLayers.Function.bind(this.geolocate, this), + OpenLayers.Function.bind(this.failure, this), + this.geolocationOptions + ); + } else { + this.getCurrentLocation(); + } + return true; + } + return false; + }, + + /** + * Method: deactivate + * Deactivates the control. + * + * Returns: + * {Boolean} The control was effectively deactivated. + */ + deactivate: function () { + if (this.active && this.watchId !== null) { + this.geolocation.clearWatch(this.watchId); + } + return OpenLayers.Control.prototype.deactivate.apply( + this, arguments + ); + }, + + /** + * Method: geolocate + * Activates the control. + * + */ + geolocate: function (position) { + var center = new OpenLayers.LonLat( + position.coords.longitude, + position.coords.latitude + ).transform( + new OpenLayers.Projection("EPSG:4326"), + this.map.getProjectionObject() + ); + if (this.bind) { + this.map.setCenter(center); + } + this.events.triggerEvent("locationupdated", { + position: position, + point: new OpenLayers.Geometry.Point( + center.lon, center.lat + ) + }); + }, + + /** + * APIMethod: getCurrentLocation + * + * Returns: + * {Boolean} Returns true if a event will be fired (successfull + * registration) + */ + getCurrentLocation: function() { + if (!this.active || this.watch) { + return false; + } + this.geolocation.getCurrentPosition( + OpenLayers.Function.bind(this.geolocate, this), + OpenLayers.Function.bind(this.failure, this), + this.geolocationOptions + ); + return true; + }, + + /** + * Method: failure + * method called on browser's geolocation failure + * + */ + failure: function (error) { + this.events.triggerEvent("locationfailed", {error: error}); + }, + + CLASS_NAME: "OpenLayers.Control.Geolocate" +}); +/* ====================================================================== + OpenLayers/Protocol/HTTP.js + ====================================================================== */ + +/* 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/Protocol.js + * @requires OpenLayers/Request/XMLHttpRequest.js + */ + +/** + * if application uses the query string, for example, for BBOX parameters, + * OpenLayers/Format/QueryStringFilter.js should be included in the build config file + */ + +/** + * Class: OpenLayers.Protocol.HTTP + * A basic HTTP protocol for vector layers. Create a new instance with the + * <OpenLayers.Protocol.HTTP> constructor. + * + * Inherits from: + * - <OpenLayers.Protocol> + */ +OpenLayers.Protocol.HTTP = OpenLayers.Class(OpenLayers.Protocol, { + + /** + * Property: url + * {String} Service URL, read-only, set through the options + * passed to constructor. + */ + url: null, + + /** + * Property: headers + * {Object} HTTP request headers, read-only, set through the options + * passed to the constructor, + * Example: {'Content-Type': 'plain/text'} + */ + headers: null, + + /** + * Property: params + * {Object} Parameters of GET requests, read-only, set through the options + * passed to the constructor, + * Example: {'bbox': '5,5,5,5'} + */ + params: null, + + /** + * Property: callback + * {Object} Function to be called when the <read>, <create>, + * <update>, <delete> or <commit> operation completes, read-only, + * set through the options passed to the constructor. + */ + callback: null, + + /** + * Property: scope + * {Object} Callback execution scope, read-only, set through the + * options passed to the constructor. + */ + scope: null, + + /** + * APIProperty: readWithPOST + * {Boolean} true if read operations are done with POST requests + * instead of GET, defaults to false. + */ + readWithPOST: false, + + /** + * APIProperty: updateWithPOST + * {Boolean} true if update operations are done with POST requests + * defaults to false. + */ + updateWithPOST: false, + + /** + * APIProperty: deleteWithPOST + * {Boolean} true if delete operations are done with POST requests + * defaults to false. + * if true, POST data is set to output of format.write(). + */ + deleteWithPOST: false, + + /** + * Property: wildcarded. + * {Boolean} If true percent signs are added around values + * read from LIKE filters, for example if the protocol + * read method is passed a LIKE filter whose property + * is "foo" and whose value is "bar" the string + * "foo__ilike=%bar%" will be sent in the query string; + * defaults to false. + */ + wildcarded: false, + + /** + * APIProperty: srsInBBOX + * {Boolean} Include the SRS identifier in BBOX query string parameter. + * Default is false. If true and the layer has a projection object set, + * any BBOX filter will be serialized with a fifth item identifying the + * projection. E.g. bbox=-1000,-1000,1000,1000,EPSG:900913 + */ + srsInBBOX: false, + + /** + * Constructor: OpenLayers.Protocol.HTTP + * A class for giving layers generic HTTP protocol. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + * + * Valid options include: + * url - {String} + * headers - {Object} + * params - {Object} URL parameters for GET requests + * format - {<OpenLayers.Format>} + * callback - {Function} + * scope - {Object} + */ + initialize: function(options) { + options = options || {}; + this.params = {}; + this.headers = {}; + OpenLayers.Protocol.prototype.initialize.apply(this, arguments); + + if (!this.filterToParams && OpenLayers.Format.QueryStringFilter) { + var format = new OpenLayers.Format.QueryStringFilter({ + wildcarded: this.wildcarded, + srsInBBOX: this.srsInBBOX + }); + this.filterToParams = function(filter, params) { + return format.write(filter, params); + }; + } + }, + + /** + * APIMethod: destroy + * Clean up the protocol. + */ + destroy: function() { + this.params = null; + this.headers = null; + OpenLayers.Protocol.prototype.destroy.apply(this); + }, + + /** + * APIMethod: filterToParams + * Optional method to translate an <OpenLayers.Filter> object into an object + * that can be serialized as request query string provided. If a custom + * method is not provided, the filter will be serialized using the + * <OpenLayers.Format.QueryStringFilter> class. + * + * Parameters: + * filter - {<OpenLayers.Filter>} filter to convert. + * params - {Object} The parameters object. + * + * Returns: + * {Object} The resulting parameters object. + */ + + /** + * APIMethod: read + * Construct a request for reading new features. + * + * Parameters: + * options - {Object} Optional object for configuring the request. + * This object is modified and should not be reused. + * + * Valid options: + * url - {String} Url for the request. + * params - {Object} Parameters to get serialized as a query string. + * headers - {Object} Headers to be set on the request. + * filter - {<OpenLayers.Filter>} Filter to get serialized as a + * query string. + * readWithPOST - {Boolean} If the request should be done with POST. + * + * Returns: + * {<OpenLayers.Protocol.Response>} A response object, whose "priv" property + * references the HTTP request, this object is also passed to the + * callback function when the request completes, its "features" property + * is then populated with the features received from the server. + */ + read: function(options) { + OpenLayers.Protocol.prototype.read.apply(this, arguments); + options = options || {}; + options.params = OpenLayers.Util.applyDefaults( + options.params, this.options.params); + options = OpenLayers.Util.applyDefaults(options, this.options); + if (options.filter && this.filterToParams) { + options.params = this.filterToParams( + options.filter, options.params + ); + } + var readWithPOST = (options.readWithPOST !== undefined) ? + options.readWithPOST : this.readWithPOST; + var resp = new OpenLayers.Protocol.Response({requestType: "read"}); + if(readWithPOST) { + var headers = options.headers || {}; + headers["Content-Type"] = "application/x-www-form-urlencoded"; + resp.priv = OpenLayers.Request.POST({ + url: options.url, + callback: this.createCallback(this.handleRead, resp, options), + data: OpenLayers.Util.getParameterString(options.params), + headers: headers + }); + } else { + resp.priv = OpenLayers.Request.GET({ + url: options.url, + callback: this.createCallback(this.handleRead, resp, options), + params: options.params, + headers: options.headers + }); + } + return resp; + }, + + /** + * Method: handleRead + * Individual callbacks are created for read, create and update, should + * a subclass need to override each one separately. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} The response object to pass to + * the user callback. + * options - {Object} The user options passed to the read call. + */ + handleRead: function(resp, options) { + this.handleResponse(resp, options); + }, + + /** + * APIMethod: create + * Construct a request for writing newly created features. + * + * Parameters: + * features - {Array({<OpenLayers.Feature.Vector>})} or + * {<OpenLayers.Feature.Vector>} + * options - {Object} Optional object for configuring the request. + * This object is modified and should not be reused. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, whose "priv" property references the HTTP request, this + * object is also passed to the callback function when the request + * completes, its "features" property is then populated with the + * the features received from the server. + */ + create: function(features, options) { + options = OpenLayers.Util.applyDefaults(options, this.options); + + var resp = new OpenLayers.Protocol.Response({ + reqFeatures: features, + requestType: "create" + }); + + resp.priv = OpenLayers.Request.POST({ + url: options.url, + callback: this.createCallback(this.handleCreate, resp, options), + headers: options.headers, + data: this.format.write(features) + }); + + return resp; + }, + + /** + * Method: handleCreate + * Called the the request issued by <create> is complete. May be overridden + * by subclasses. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} The response object to pass to + * any user callback. + * options - {Object} The user options passed to the create call. + */ + handleCreate: function(resp, options) { + this.handleResponse(resp, options); + }, + + /** + * APIMethod: update + * Construct a request updating modified feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * options - {Object} Optional object for configuring the request. + * This object is modified and should not be reused. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, whose "priv" property references the HTTP request, this + * object is also passed to the callback function when the request + * completes, its "features" property is then populated with the + * the feature received from the server. + */ + update: function(feature, options) { + options = options || {}; + var url = options.url || + feature.url || + this.options.url + "/" + feature.fid; + options = OpenLayers.Util.applyDefaults(options, this.options); + + var resp = new OpenLayers.Protocol.Response({ + reqFeatures: feature, + requestType: "update" + }); + + var method = this.updateWithPOST ? "POST" : "PUT"; + resp.priv = OpenLayers.Request[method]({ + url: url, + callback: this.createCallback(this.handleUpdate, resp, options), + headers: options.headers, + data: this.format.write(feature) + }); + + return resp; + }, + + /** + * Method: handleUpdate + * Called the the request issued by <update> is complete. May be overridden + * by subclasses. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} The response object to pass to + * any user callback. + * options - {Object} The user options passed to the update call. + */ + handleUpdate: function(resp, options) { + this.handleResponse(resp, options); + }, + + /** + * APIMethod: delete + * Construct a request deleting a removed feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * options - {Object} Optional object for configuring the request. + * This object is modified and should not be reused. + * + * Returns: + * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response> + * object, whose "priv" property references the HTTP request, this + * object is also passed to the callback function when the request + * completes. + */ + "delete": function(feature, options) { + options = options || {}; + var url = options.url || + feature.url || + this.options.url + "/" + feature.fid; + options = OpenLayers.Util.applyDefaults(options, this.options); + + var resp = new OpenLayers.Protocol.Response({ + reqFeatures: feature, + requestType: "delete" + }); + + var method = this.deleteWithPOST ? "POST" : "DELETE"; + var requestOptions = { + url: url, + callback: this.createCallback(this.handleDelete, resp, options), + headers: options.headers + }; + if (this.deleteWithPOST) { + requestOptions.data = this.format.write(feature); + } + resp.priv = OpenLayers.Request[method](requestOptions); + + return resp; + }, + + /** + * Method: handleDelete + * Called the the request issued by <delete> is complete. May be overridden + * by subclasses. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} The response object to pass to + * any user callback. + * options - {Object} The user options passed to the delete call. + */ + handleDelete: function(resp, options) { + this.handleResponse(resp, options); + }, + + /** + * Method: handleResponse + * Called by CRUD specific handlers. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} The response object to pass to + * any user callback. + * options - {Object} The user options passed to the create, read, update, + * or delete call. + */ + handleResponse: function(resp, options) { + var request = resp.priv; + if(options.callback) { + if(request.status >= 200 && request.status < 300) { + // success + if(resp.requestType != "delete") { + resp.features = this.parseFeatures(request); + } + resp.code = OpenLayers.Protocol.Response.SUCCESS; + } else { + // failure + resp.code = OpenLayers.Protocol.Response.FAILURE; + } + options.callback.call(options.scope, resp); + } + }, + + /** + * Method: parseFeatures + * Read HTTP response body and return features. + * + * Parameters: + * request - {XMLHttpRequest} The request object + * + * Returns: + * {Array({<OpenLayers.Feature.Vector>})} or + * {<OpenLayers.Feature.Vector>} Array of features or a single feature. + */ + parseFeatures: function(request) { + var doc = request.responseXML; + if (!doc || !doc.documentElement) { + doc = request.responseText; + } + if (!doc || doc.length <= 0) { + return null; + } + return this.format.read(doc); + }, + + /** + * APIMethod: commit + * Iterate over each feature and take action based on the feature state. + * Possible actions are create, update and delete. + * + * Parameters: + * features - {Array({<OpenLayers.Feature.Vector>})} + * options - {Object} Optional object for setting up intermediate commit + * callbacks. + * + * Valid options: + * create - {Object} Optional object to be passed to the <create> method. + * update - {Object} Optional object to be passed to the <update> method. + * delete - {Object} Optional object to be passed to the <delete> method. + * callback - {Function} Optional function to be called when the commit + * is complete. + * scope - {Object} Optional object to be set as the scope of the callback. + * + * Returns: + * {Array(<OpenLayers.Protocol.Response>)} An array of response objects, + * one per request made to the server, each object's "priv" property + * references the corresponding HTTP request. + */ + commit: function(features, options) { + options = OpenLayers.Util.applyDefaults(options, this.options); + var resp = [], nResponses = 0; + + // Divide up features before issuing any requests. This properly + // counts requests in the event that any responses come in before + // all requests have been issued. + var types = {}; + types[OpenLayers.State.INSERT] = []; + types[OpenLayers.State.UPDATE] = []; + types[OpenLayers.State.DELETE] = []; + var feature, list, requestFeatures = []; + for(var i=0, len=features.length; i<len; ++i) { + feature = features[i]; + list = types[feature.state]; + if(list) { + list.push(feature); + requestFeatures.push(feature); + } + } + // tally up number of requests + var nRequests = (types[OpenLayers.State.INSERT].length > 0 ? 1 : 0) + + types[OpenLayers.State.UPDATE].length + + types[OpenLayers.State.DELETE].length; + + // This response will be sent to the final callback after all the others + // have been fired. + var success = true; + var finalResponse = new OpenLayers.Protocol.Response({ + reqFeatures: requestFeatures + }); + + function insertCallback(response) { + var len = response.features ? response.features.length : 0; + var fids = new Array(len); + for(var i=0; i<len; ++i) { + fids[i] = response.features[i].fid; + } + finalResponse.insertIds = fids; + callback.apply(this, [response]); + } + + function callback(response) { + this.callUserCallback(response, options); + success = success && response.success(); + nResponses++; + if (nResponses >= nRequests) { + if (options.callback) { + finalResponse.code = success ? + OpenLayers.Protocol.Response.SUCCESS : + OpenLayers.Protocol.Response.FAILURE; + options.callback.apply(options.scope, [finalResponse]); + } + } + } + + // start issuing requests + var queue = types[OpenLayers.State.INSERT]; + if(queue.length > 0) { + resp.push(this.create( + queue, OpenLayers.Util.applyDefaults( + {callback: insertCallback, scope: this}, options.create + ) + )); + } + queue = types[OpenLayers.State.UPDATE]; + for(var i=queue.length-1; i>=0; --i) { + resp.push(this.update( + queue[i], OpenLayers.Util.applyDefaults( + {callback: callback, scope: this}, options.update + )) + ); + } + queue = types[OpenLayers.State.DELETE]; + for(var i=queue.length-1; i>=0; --i) { + resp.push(this["delete"]( + queue[i], OpenLayers.Util.applyDefaults( + {callback: callback, scope: this}, options["delete"] + )) + ); + } + return resp; + }, + + /** + * APIMethod: abort + * Abort an ongoing request, the response object passed to + * this method must come from this HTTP protocol (as a result + * of a create, read, update, delete or commit operation). + * + * Parameters: + * response - {<OpenLayers.Protocol.Response>} + */ + abort: function(response) { + if (response) { + response.priv.abort(); + } + }, + + /** + * Method: callUserCallback + * This method is used from within the commit method each time an + * an HTTP response is received from the server, it is responsible + * for calling the user-supplied callbacks. + * + * Parameters: + * resp - {<OpenLayers.Protocol.Response>} + * options - {Object} The map of options passed to the commit call. + */ + callUserCallback: function(resp, options) { + var opt = options[resp.requestType]; + if(opt && opt.callback) { + opt.callback.call(opt.scope, resp); + } + }, + + CLASS_NAME: "OpenLayers.Protocol.HTTP" +}); +/* ====================================================================== + OpenLayers/Control/DragPan.js + ====================================================================== */ + +/* 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/Control.js + * @requires OpenLayers/Handler/Drag.js + */ + +/** + * Class: OpenLayers.Control.DragPan + * The DragPan control pans the map with a drag of the mouse. + * + * Inherits from: + * - <OpenLayers.Control> + */ +OpenLayers.Control.DragPan = OpenLayers.Class(OpenLayers.Control, { + + /** + * Property: type + * {OpenLayers.Control.TYPES} + */ + type: OpenLayers.Control.TYPE_TOOL, + + /** + * Property: panned + * {Boolean} The map moved. + */ + panned: false, + + /** + * Property: interval + * {Integer} The number of milliseconds that should ellapse before + * panning the map again. Defaults to 0 milliseconds, which means that + * no separate cycle is used for panning. In most cases you won't want + * to change this value. For slow machines/devices larger values can be + * tried out. + */ + interval: 0, + + /** + * APIProperty: documentDrag + * {Boolean} If set to true, mouse dragging will continue even if the + * mouse cursor leaves the map viewport. Default is false. + */ + documentDrag: false, + + /** + * Property: kinetic + * {<OpenLayers.Kinetic>} The OpenLayers.Kinetic object. + */ + kinetic: null, + + /** + * APIProperty: enableKinetic + * {Boolean} Set this option to enable "kinetic dragging". Can be + * set to true or to an object. If set to an object this + * object will be passed to the {<OpenLayers.Kinetic>} + * constructor. Defaults to true. + * To get kinetic dragging, ensure that OpenLayers/Kinetic.js is + * included in your build config. + */ + enableKinetic: true, + + /** + * APIProperty: kineticInterval + * {Integer} Interval in milliseconds between 2 steps in the "kinetic + * scrolling". Applies only if enableKinetic is set. Defaults + * to 10 milliseconds. + */ + kineticInterval: 10, + + + /** + * Method: draw + * Creates a Drag handler, using <panMap> and + * <panMapDone> as callbacks. + */ + draw: function() { + if (this.enableKinetic && OpenLayers.Kinetic) { + var config = {interval: this.kineticInterval}; + if(typeof this.enableKinetic === "object") { + config = OpenLayers.Util.extend(config, this.enableKinetic); + } + this.kinetic = new OpenLayers.Kinetic(config); + } + this.handler = new OpenLayers.Handler.Drag(this, { + "move": this.panMap, + "done": this.panMapDone, + "down": this.panMapStart + }, { + interval: this.interval, + documentDrag: this.documentDrag + } + ); + }, + + /** + * Method: panMapStart + */ + panMapStart: function() { + if(this.kinetic) { + this.kinetic.begin(); + } + }, + + /** + * Method: panMap + * + * Parameters: + * xy - {<OpenLayers.Pixel>} Pixel of the mouse position + */ + panMap: function(xy) { + if(this.kinetic) { + this.kinetic.update(xy); + } + this.panned = true; + this.map.pan( + this.handler.last.x - xy.x, + this.handler.last.y - xy.y, + {dragging: true, animate: false} + ); + }, + + /** + * Method: panMapDone + * Finish the panning operation. Only call setCenter (through <panMap>) + * if the map has actually been moved. + * + * Parameters: + * xy - {<OpenLayers.Pixel>} Pixel of the mouse position + */ + panMapDone: function(xy) { + if(this.panned) { + var res = null; + if (this.kinetic) { + res = this.kinetic.end(xy); + } + this.map.pan( + this.handler.last.x - xy.x, + this.handler.last.y - xy.y, + {dragging: !!res, animate: false} + ); + if (res) { + var self = this; + this.kinetic.move(res, function(x, y, end) { + self.map.pan(x, y, {dragging: !end, animate: false}); + }); + } + this.panned = false; + } + }, + + CLASS_NAME: "OpenLayers.Control.DragPan" +}); +/* ====================================================================== + OpenLayers/Control/PinchZoom.js + ====================================================================== */ + +/* 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/Handler/Pinch.js + */ + +/** + * Class: OpenLayers.Control.PinchZoom + * + * Inherits: + * - <OpenLayers.Control> + */ +OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, { + + /** + * Property: type + * {OpenLayers.Control.TYPES} + */ + type: OpenLayers.Control.TYPE_TOOL, + + /** + * Property: pinchOrigin + * {Object} Cached object representing the pinch start (in pixels). + */ + pinchOrigin: null, + + /** + * Property: currentCenter + * {Object} Cached object representing the latest pinch center (in pixels). + */ + currentCenter: null, + + /** + * APIProperty: autoActivate + * {Boolean} Activate the control when it is added to a map. Default is + * true. + */ + autoActivate: true, + + /** + * APIProperty: preserveCenter + * {Boolean} Set this to true if you don't want the map center to change + * while pinching. For example you may want to set preserveCenter to + * true when the user location is being watched and you want to preserve + * the user location at the center of the map even if he zooms in or + * out using pinch. This property's value can be changed any time on an + * existing instance. Default is false. + */ + preserveCenter: false, + + /** + * APIProperty: handlerOptions + * {Object} Used to set non-default properties on the pinch handler + */ + + /** + * Constructor: OpenLayers.Control.PinchZoom + * Create a control for zooming with pinch gestures. This works on devices + * with multi-touch support. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * the control + */ + initialize: function(options) { + OpenLayers.Control.prototype.initialize.apply(this, arguments); + this.handler = new OpenLayers.Handler.Pinch(this, { + start: this.pinchStart, + move: this.pinchMove, + done: this.pinchDone + }, this.handlerOptions); + }, + + /** + * Method: pinchStart + * + * Parameters: + * evt - {Event} + * pinchData - {Object} pinch data object related to the current touchmove + * of the pinch gesture. This give us the current scale of the pinch. + */ + pinchStart: function(evt, pinchData) { + var xy = (this.preserveCenter) ? + this.map.getPixelFromLonLat(this.map.getCenter()) : evt.xy; + this.pinchOrigin = xy; + this.currentCenter = xy; + }, + + /** + * Method: pinchMove + * + * Parameters: + * evt - {Event} + * pinchData - {Object} pinch data object related to the current touchmove + * of the pinch gesture. This give us the current scale of the pinch. + */ + pinchMove: function(evt, pinchData) { + var scale = pinchData.scale; + var containerOrigin = this.map.layerContainerOriginPx; + var pinchOrigin = this.pinchOrigin; + var current = (this.preserveCenter) ? + this.map.getPixelFromLonLat(this.map.getCenter()) : evt.xy; + + var dx = Math.round((containerOrigin.x + current.x - pinchOrigin.x) + (scale - 1) * (containerOrigin.x - pinchOrigin.x)); + var dy = Math.round((containerOrigin.y + current.y - pinchOrigin.y) + (scale - 1) * (containerOrigin.y - pinchOrigin.y)); + + this.map.applyTransform(dx, dy, scale); + this.currentCenter = current; + }, + + /** + * Method: pinchDone + * + * Parameters: + * evt - {Event} + * start - {Object} pinch data object related to the touchstart event that + * started the pinch gesture. + * last - {Object} pinch data object related to the last touchmove event + * of the pinch gesture. This give us the final scale of the pinch. + */ + pinchDone: function(evt, start, last) { + this.map.applyTransform(); + var zoom = this.map.getZoomForResolution(this.map.getResolution() / last.scale, true); + if (zoom !== this.map.getZoom() || !this.currentCenter.equals(this.pinchOrigin)) { + var resolution = this.map.getResolutionForZoom(zoom); + + var location = this.map.getLonLatFromPixel(this.pinchOrigin); + var zoomPixel = this.currentCenter; + var size = this.map.getSize(); + + location.lon += resolution * ((size.w / 2) - zoomPixel.x); + location.lat -= resolution * ((size.h / 2) - zoomPixel.y); + + // Force a reflow before calling setCenter. This is to work + // around an issue occuring in iOS. + // + // See https://github.com/openlayers/openlayers/pull/351. + // + // Without a reflow setting the layer container div's top left + // style properties to "0px" - as done in Map.moveTo when zoom + // is changed - won't actually correctly reposition the layer + // container div. + // + // Also, we need to use a statement that the Google Closure + // compiler won't optimize away. + this.map.div.clientWidth = this.map.div.clientWidth; + + this.map.setCenter(location, zoom); + } + }, + + CLASS_NAME: "OpenLayers.Control.PinchZoom" + +}); +/* ====================================================================== + OpenLayers/Handler/Click.js + ====================================================================== */ + +/* 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/Handler.js + */ + +/** + * Class: OpenLayers.Handler.Click + * A handler for mouse clicks. The intention of this handler is to give + * controls more flexibility with handling clicks. Browsers trigger + * click events twice for a double-click. In addition, the mousedown, + * mousemove, mouseup sequence fires a click event. With this handler, + * controls can decide whether to ignore clicks associated with a double + * click. By setting a <pixelTolerance>, controls can also ignore clicks + * that include a drag. Create a new instance with the + * <OpenLayers.Handler.Click> constructor. + * + * Inherits from: + * - <OpenLayers.Handler> + */ +OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { + /** + * APIProperty: delay + * {Number} Number of milliseconds between clicks before the event is + * considered a double-click. + */ + delay: 300, + + /** + * APIProperty: single + * {Boolean} Handle single clicks. Default is true. If false, clicks + * will not be reported. If true, single-clicks will be reported. + */ + single: true, + + /** + * APIProperty: double + * {Boolean} Handle double-clicks. Default is false. + */ + 'double': false, + + /** + * APIProperty: pixelTolerance + * {Number} Maximum number of pixels between mouseup and mousedown for an + * event to be considered a click. Default is 0. If set to an + * integer value, clicks with a drag greater than the value will be + * ignored. This property can only be set when the handler is + * constructed. + */ + pixelTolerance: 0, + + /** + * APIProperty: dblclickTolerance + * {Number} Maximum distance in pixels between clicks for a sequence of + * events to be considered a double click. Default is 13. If the + * distance between two clicks is greater than this value, a double- + * click will not be fired. + */ + dblclickTolerance: 13, + + /** + * APIProperty: stopSingle + * {Boolean} Stop other listeners from being notified of clicks. Default + * is false. If true, any listeners registered before this one for + * click or rightclick events will not be notified. + */ + stopSingle: false, + + /** + * APIProperty: stopDouble + * {Boolean} Stop other listeners from being notified of double-clicks. + * Default is false. If true, any click listeners registered before + * this one will not be notified of *any* double-click events. + * + * The one caveat with stopDouble is that given a map with two click + * handlers, one with stopDouble true and the other with stopSingle + * true, the stopSingle handler should be activated last to get + * uniform cross-browser performance. Since IE triggers one click + * with a dblclick and FF triggers two, if a stopSingle handler is + * activated first, all it gets in IE is a single click when the + * second handler stops propagation on the dblclick. + */ + stopDouble: false, + + /** + * Property: timerId + * {Number} The id of the timeout waiting to clear the <delayedCall>. + */ + timerId: null, + + /** + * Property: down + * {Object} Object that store relevant information about the last + * mousedown or touchstart. Its 'xy' OpenLayers.Pixel property gives + * the average location of the mouse/touch event. Its 'touches' + * property records clientX/clientY of each touches. + */ + down: null, + + /** + * Property: last + * {Object} Object that store relevant information about the last + * mousemove or touchmove. Its 'xy' OpenLayers.Pixel property gives + * the average location of the mouse/touch event. Its 'touches' + * property records clientX/clientY of each touches. + */ + last: null, + + /** + * Property: first + * {Object} When waiting for double clicks, this object will store + * information about the first click in a two click sequence. + */ + first: null, + + /** + * Property: rightclickTimerId + * {Number} The id of the right mouse timeout waiting to clear the + * <delayedEvent>. + */ + rightclickTimerId: null, + + /** + * Constructor: OpenLayers.Handler.Click + * Create a new click handler. + * + * Parameters: + * control - {<OpenLayers.Control>} The control that is making use of + * this handler. If a handler is being used without a control, the + * handler's setMap method must be overridden to deal properly with + * the map. + * callbacks - {Object} An object with keys corresponding to callbacks + * that will be called by the handler. The callbacks should + * expect to recieve a single argument, the click event. + * Callbacks for 'click' and 'dblclick' are supported. + * options - {Object} Optional object whose properties will be set on the + * handler. + */ + + /** + * Method: touchstart + * Handle touchstart. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + touchstart: function(evt) { + this.startTouch(); + this.down = this.getEventInfo(evt); + this.last = this.getEventInfo(evt); + return true; + }, + + /** + * Method: touchmove + * Store position of last move, because touchend event can have + * an empty "touches" property. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + touchmove: function(evt) { + this.last = this.getEventInfo(evt); + return true; + }, + + /** + * Method: touchend + * Correctly set event xy property, and add lastTouches to have + * touches property from last touchstart or touchmove + * + * Returns: + * {Boolean} Continue propagating this event. + */ + touchend: function(evt) { + // touchstart may not have been allowed to propagate + if (this.down) { + evt.xy = this.last.xy; + evt.lastTouches = this.last.touches; + this.handleSingle(evt); + this.down = null; + } + return true; + }, + + /** + * Method: mousedown + * Handle mousedown. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + mousedown: function(evt) { + this.down = this.getEventInfo(evt); + this.last = this.getEventInfo(evt); + return true; + }, + + /** + * Method: mouseup + * Handle mouseup. Installed to support collection of right mouse events. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + mouseup: function (evt) { + var propagate = true; + + // Collect right mouse clicks from the mouseup + // IE - ignores the second right click in mousedown so using + // mouseup instead + if (this.checkModifiers(evt) && this.control.handleRightClicks && + OpenLayers.Event.isRightClick(evt)) { + propagate = this.rightclick(evt); + } + + return propagate; + }, + + /** + * Method: rightclick + * Handle rightclick. For a dblrightclick, we get two clicks so we need + * to always register for dblrightclick to properly handle single + * clicks. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + rightclick: function(evt) { + if(this.passesTolerance(evt)) { + if(this.rightclickTimerId != null) { + //Second click received before timeout this must be + // a double click + this.clearTimer(); + this.callback('dblrightclick', [evt]); + return !this.stopDouble; + } else { + //Set the rightclickTimerId, send evt only if double is + // true else trigger single + var clickEvent = this['double'] ? + OpenLayers.Util.extend({}, evt) : + this.callback('rightclick', [evt]); + + var delayedRightCall = OpenLayers.Function.bind( + this.delayedRightCall, + this, + clickEvent + ); + this.rightclickTimerId = window.setTimeout( + delayedRightCall, this.delay + ); + } + } + return !this.stopSingle; + }, + + /** + * Method: delayedRightCall + * Sets <rightclickTimerId> to null. And optionally triggers the + * rightclick callback if evt is set. + */ + delayedRightCall: function(evt) { + this.rightclickTimerId = null; + if (evt) { + this.callback('rightclick', [evt]); + } + }, + + /** + * Method: click + * Handle click events from the browser. This is registered as a listener + * for click events and should not be called from other events in this + * handler. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + click: function(evt) { + if (!this.last) { + this.last = this.getEventInfo(evt); + } + this.handleSingle(evt); + return !this.stopSingle; + }, + + /** + * Method: dblclick + * Handle dblclick. For a dblclick, we get two clicks in some browsers + * (FF) and one in others (IE). So we need to always register for + * dblclick to properly handle single clicks. This method is registered + * as a listener for the dblclick browser event. It should *not* be + * called by other methods in this handler. + * + * Returns: + * {Boolean} Continue propagating this event. + */ + dblclick: function(evt) { + this.handleDouble(evt); + return !this.stopDouble; + }, + + /** + * Method: handleDouble + * Handle double-click sequence. + */ + handleDouble: function(evt) { + if (this.passesDblclickTolerance(evt)) { + if (this["double"]) { + this.callback("dblclick", [evt]); + } + // to prevent a dblclick from firing the click callback in IE + this.clearTimer(); + } + }, + + /** + * Method: handleSingle + * Handle single click sequence. + */ + handleSingle: function(evt) { + if (this.passesTolerance(evt)) { + if (this.timerId != null) { + // already received a click + if (this.last.touches && this.last.touches.length === 1) { + // touch device, no dblclick event - this may be a double + if (this["double"]) { + // on Android don't let the browser zoom on the page + OpenLayers.Event.preventDefault(evt); + } + this.handleDouble(evt); + } + // if we're not in a touch environment we clear the click timer + // if we've got a second touch, we'll get two touchend events + if (!this.last.touches || this.last.touches.length !== 2) { + this.clearTimer(); + } + } else { + // remember the first click info so we can compare to the second + this.first = this.getEventInfo(evt); + // set the timer, send evt only if single is true + //use a clone of the event object because it will no longer + //be a valid event object in IE in the timer callback + var clickEvent = this.single ? + OpenLayers.Util.extend({}, evt) : null; + this.queuePotentialClick(clickEvent); + } + } + }, + + /** + * Method: queuePotentialClick + * This method is separated out largely to make testing easier (so we + * don't have to override window.setTimeout) + */ + queuePotentialClick: function(evt) { + this.timerId = window.setTimeout( + OpenLayers.Function.bind(this.delayedCall, this, evt), + this.delay + ); + }, + + /** + * Method: passesTolerance + * Determine whether the event is within the optional pixel tolerance. Note + * that the pixel tolerance check only works if mousedown events get to + * the listeners registered here. If they are stopped by other elements, + * the <pixelTolerance> will have no effect here (this method will always + * return true). + * + * Returns: + * {Boolean} The click is within the pixel tolerance (if specified). + */ + passesTolerance: function(evt) { + var passes = true; + if (this.pixelTolerance != null && this.down && this.down.xy) { + passes = this.pixelTolerance >= this.down.xy.distanceTo(evt.xy); + // for touch environments, we also enforce that all touches + // start and end within the given tolerance to be considered a click + if (passes && this.touch && + this.down.touches.length === this.last.touches.length) { + // the touchend event doesn't come with touches, so we check + // down and last + for (var i=0, ii=this.down.touches.length; i<ii; ++i) { + if (this.getTouchDistance( + this.down.touches[i], + this.last.touches[i] + ) > this.pixelTolerance) { + passes = false; + break; + } + } + } + } + return passes; + }, + + /** + * Method: getTouchDistance + * + * Returns: + * {Boolean} The pixel displacement between two touches. + */ + getTouchDistance: function(from, to) { + return Math.sqrt( + Math.pow(from.clientX - to.clientX, 2) + + Math.pow(from.clientY - to.clientY, 2) + ); + }, + + /** + * Method: passesDblclickTolerance + * Determine whether the event is within the optional double-cick pixel + * tolerance. + * + * Returns: + * {Boolean} The click is within the double-click pixel tolerance. + */ + passesDblclickTolerance: function(evt) { + var passes = true; + if (this.down && this.first) { + passes = this.down.xy.distanceTo(this.first.xy) <= this.dblclickTolerance; + } + return passes; + }, + + /** + * Method: clearTimer + * Clear the timer and set <timerId> to null. + */ + clearTimer: function() { + if (this.timerId != null) { + window.clearTimeout(this.timerId); + this.timerId = null; + } + if (this.rightclickTimerId != null) { + window.clearTimeout(this.rightclickTimerId); + this.rightclickTimerId = null; + } + }, + + /** + * Method: delayedCall + * Sets <timerId> to null. And optionally triggers the click callback if + * evt is set. + */ + delayedCall: function(evt) { + this.timerId = null; + if (evt) { + this.callback("click", [evt]); + } + }, + + /** + * Method: getEventInfo + * This method allows us to store event information without storing the + * actual event. In touch devices (at least), the same event is + * modified between touchstart, touchmove, and touchend. + * + * Returns: + * {Object} An object with event related info. + */ + getEventInfo: function(evt) { + var touches; + if (evt.touches) { + var len = evt.touches.length; + touches = new Array(len); + var touch; + for (var i=0; i<len; i++) { + touch = evt.touches[i]; + touches[i] = { + clientX: touch.olClientX, + clientY: touch.olClientY + }; + } + } + return { + xy: evt.xy, + touches: touches + }; + }, + + /** + * APIMethod: deactivate + * Deactivate the handler. + * + * Returns: + * {Boolean} The handler was successfully deactivated. + */ + deactivate: function() { + var deactivated = false; + if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { + this.clearTimer(); + this.down = null; + this.first = null; + this.last = null; + deactivated = true; + } + return deactivated; + }, + + CLASS_NAME: "OpenLayers.Handler.Click" +}); +/* ====================================================================== + OpenLayers/Control/TouchNavigation.js + ====================================================================== */ + +/* 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/Control/DragPan.js + * @requires OpenLayers/Control/PinchZoom.js + * @requires OpenLayers/Handler/Click.js + */ + +/** + * Class: OpenLayers.Control.TouchNavigation + * The navigation control handles map browsing with touch events (dragging, + * double-tapping, tap with two fingers, and pinch zoom). Create a new + * control with the <OpenLayers.Control.TouchNavigation> constructor. + * + * If you’re only targeting touch enabled devices with your mapping application, + * you can create a map with only a TouchNavigation control. The + * <OpenLayers.Control.Navigation> control is mobile ready by default, but + * you can generate a smaller build of the library by only including this + * touch navigation control if you aren't concerned about mouse interaction. + * + * Inherits: + * - <OpenLayers.Control> + */ +OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, { + + /** + * Property: dragPan + * {<OpenLayers.Control.DragPan>} + */ + dragPan: null, + + /** + * APIProperty: dragPanOptions + * {Object} Options passed to the DragPan control. + */ + dragPanOptions: null, + + /** + * Property: pinchZoom + * {<OpenLayers.Control.PinchZoom>} + */ + pinchZoom: null, + + /** + * APIProperty: pinchZoomOptions + * {Object} Options passed to the PinchZoom control. + */ + pinchZoomOptions: null, + + /** + * APIProperty: clickHandlerOptions + * {Object} Options passed to the Click handler. + */ + clickHandlerOptions: null, + + /** + * APIProperty: documentDrag + * {Boolean} Allow panning of the map by dragging outside map viewport. + * Default is false. + */ + documentDrag: false, + + /** + * APIProperty: autoActivate + * {Boolean} Activate the control when it is added to a map. Default is + * true. + */ + autoActivate: true, + + /** + * Constructor: OpenLayers.Control.TouchNavigation + * Create a new navigation control + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * the control + */ + initialize: function(options) { + this.handlers = {}; + OpenLayers.Control.prototype.initialize.apply(this, arguments); + }, + + /** + * Method: destroy + * The destroy method is used to perform any clean up before the control + * is dereferenced. Typically this is where event listeners are removed + * to prevent memory leaks. + */ + destroy: function() { + this.deactivate(); + if(this.dragPan) { + this.dragPan.destroy(); + } + this.dragPan = null; + if (this.pinchZoom) { + this.pinchZoom.destroy(); + delete this.pinchZoom; + } + OpenLayers.Control.prototype.destroy.apply(this,arguments); + }, + + /** + * Method: activate + */ + activate: function() { + if(OpenLayers.Control.prototype.activate.apply(this,arguments)) { + this.dragPan.activate(); + this.handlers.click.activate(); + this.pinchZoom.activate(); + return true; + } + return false; + }, + + /** + * Method: deactivate + */ + deactivate: function() { + if(OpenLayers.Control.prototype.deactivate.apply(this,arguments)) { + this.dragPan.deactivate(); + this.handlers.click.deactivate(); + this.pinchZoom.deactivate(); + return true; + } + return false; + }, + + /** + * Method: draw + */ + draw: function() { + var clickCallbacks = { + click: this.defaultClick, + dblclick: this.defaultDblClick + }; + var clickOptions = OpenLayers.Util.extend({ + "double": true, + stopDouble: true, + pixelTolerance: 2 + }, this.clickHandlerOptions); + this.handlers.click = new OpenLayers.Handler.Click( + this, clickCallbacks, clickOptions + ); + this.dragPan = new OpenLayers.Control.DragPan( + OpenLayers.Util.extend({ + map: this.map, + documentDrag: this.documentDrag + }, this.dragPanOptions) + ); + this.dragPan.draw(); + this.pinchZoom = new OpenLayers.Control.PinchZoom( + OpenLayers.Util.extend({map: this.map}, this.pinchZoomOptions) + ); + }, + + /** + * Method: defaultClick + * + * Parameters: + * evt - {Event} + */ + defaultClick: function (evt) { + if(evt.lastTouches && evt.lastTouches.length == 2) { + this.map.zoomOut(); + } + }, + + /** + * Method: defaultDblClick + * + * Parameters: + * evt - {Event} + */ + defaultDblClick: function (evt) { + this.map.zoomTo(this.map.zoom + 1, evt.xy); + }, + + CLASS_NAME: "OpenLayers.Control.TouchNavigation" +}); +/* ====================================================================== + OpenLayers/Protocol/WFS/v1_0_0.js + ====================================================================== */ + +/* 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/Protocol/WFS/v1.js + * @requires OpenLayers/Format/WFST/v1_0_0.js + */ + +/** + * Class: OpenLayers.Protocol.WFS.v1_0_0 + * A WFS v1.0.0 protocol for vector layers. Create a new instance with the + * <OpenLayers.Protocol.WFS.v1_0_0> constructor. + * + * Inherits from: + * - <OpenLayers.Protocol.WFS.v1> + */ +OpenLayers.Protocol.WFS.v1_0_0 = OpenLayers.Class(OpenLayers.Protocol.WFS.v1, { + + /** + * Property: version + * {String} WFS version number. + */ + version: "1.0.0", + + /** + * Constructor: OpenLayers.Protocol.WFS.v1_0_0 + * A class for giving layers WFS v1.0.0 protocol. + * + * Parameters: + * options - {Object} Optional object whose properties will be set on the + * instance. + * + * Valid options properties: + * featureType - {String} Local (without prefix) feature typeName (required). + * featureNS - {String} Feature namespace (optional). + * featurePrefix - {String} Feature namespace alias (optional - only used + * if featureNS is provided). Default is 'feature'. + * geometryName - {String} Name of geometry attribute. Default is 'the_geom'. + */ + + CLASS_NAME: "OpenLayers.Protocol.WFS.v1_0_0" +}); +/* ====================================================================== + OpenLayers/TileManager.js + ====================================================================== */ + +/* 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/Util.js + * @requires OpenLayers/BaseTypes.js + * @requires OpenLayers/BaseTypes/Element.js + * @requires OpenLayers/Layer/Grid.js + * @requires OpenLayers/Tile/Image.js + */ + +/** + * Class: OpenLayers.TileManager + * Provides queueing of image requests and caching of image elements. + * + * Queueing avoids unnecessary image requests while changing zoom levels + * quickly, and helps improve dragging performance on mobile devices that show + * a lag in dragging when loading of new images starts. <zoomDelay> and + * <moveDelay> are the configuration options to control this behavior. + * + * Caching avoids setting the src on image elements for images that have already + * been used. Several maps can share a TileManager instance, in which case each + * map gets its own tile queue, but all maps share the same tile cache. + */ +OpenLayers.TileManager = OpenLayers.Class({ + + /** + * APIProperty: cacheSize + * {Number} Number of image elements to keep referenced in this instance's + * cache for fast reuse. Default is 256. + */ + cacheSize: 256, + + /** + * APIProperty: tilesPerFrame + * {Number} Number of queued tiles to load per frame (see <frameDelay>). + * Default is 2. + */ + tilesPerFrame: 2, + + /** + * APIProperty: frameDelay + * {Number} Delay between tile loading frames (see <tilesPerFrame>) in + * milliseconds. Default is 16. + */ + frameDelay: 16, + + /** + * APIProperty: moveDelay + * {Number} Delay in milliseconds after a map's move event before loading + * tiles. Default is 100. + */ + moveDelay: 100, + + /** + * APIProperty: zoomDelay + * {Number} Delay in milliseconds after a map's zoomend event before loading + * tiles. Default is 200. + */ + zoomDelay: 200, + + /** + * Property: maps + * {Array(<OpenLayers.Map>)} The maps to manage tiles on. + */ + maps: null, + + /** + * Property: tileQueueId + * {Object} The ids of the <drawTilesFromQueue> loop, keyed by map id. + */ + tileQueueId: null, + + /** + * Property: tileQueue + * {Object(Array(<OpenLayers.Tile>))} Tiles queued for drawing, keyed by + * map id. + */ + tileQueue: null, + + /** + * Property: tileCache + * {Object} Cached image elements, keyed by URL. + */ + tileCache: null, + + /** + * Property: tileCacheIndex + * {Array(String)} URLs of cached tiles. First entry is the least recently + * used. + */ + tileCacheIndex: null, + + /** + * Constructor: OpenLayers.TileManager + * Constructor for a new <OpenLayers.TileManager> instance. + * + * Parameters: + * options - {Object} Configuration for this instance. + */ + initialize: function(options) { + OpenLayers.Util.extend(this, options); + this.maps = []; + this.tileQueueId = {}; + this.tileQueue = {}; + this.tileCache = {}; + this.tileCacheIndex = []; + }, + + /** + * Method: addMap + * Binds this instance to a map + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + addMap: function(map) { + if (this._destroyed || !OpenLayers.Layer.Grid) { + return; + } + this.maps.push(map); + this.tileQueue[map.id] = []; + for (var i=0, ii=map.layers.length; i<ii; ++i) { + this.addLayer({layer: map.layers[i]}); + } + map.events.on({ + move: this.move, + zoomend: this.zoomEnd, + changelayer: this.changeLayer, + addlayer: this.addLayer, + preremovelayer: this.removeLayer, + scope: this + }); + }, + + /** + * Method: removeMap + * Unbinds this instance from a map + * + * Parameters: + * map - {<OpenLayers.Map>} + */ + removeMap: function(map) { + if (this._destroyed || !OpenLayers.Layer.Grid) { + return; + } + window.clearTimeout(this.tileQueueId[map.id]); + if (map.layers) { + for (var i=0, ii=map.layers.length; i<ii; ++i) { + this.removeLayer({layer: map.layers[i]}); + } + } + if (map.events) { + map.events.un({ + move: this.move, + zoomend: this.zoomEnd, + changelayer: this.changeLayer, + addlayer: this.addLayer, + preremovelayer: this.removeLayer, + scope: this + }); + } + delete this.tileQueue[map.id]; + delete this.tileQueueId[map.id]; + OpenLayers.Util.removeItem(this.maps, map); + }, + + /** + * Method: move + * Handles the map's move event + * + * Parameters: + * evt - {Object} Listener argument + */ + move: function(evt) { + this.updateTimeout(evt.object, this.moveDelay, true); + }, + + /** + * Method: zoomEnd + * Handles the map's zoomEnd event + * + * Parameters: + * evt - {Object} Listener argument + */ + zoomEnd: function(evt) { + this.updateTimeout(evt.object, this.zoomDelay); + }, + + /** + * Method: changeLayer + * Handles the map's changeLayer event + * + * Parameters: + * evt - {Object} Listener argument + */ + changeLayer: function(evt) { + if (evt.property === 'visibility' || evt.property === 'params') { + this.updateTimeout(evt.object, 0); + } + }, + + /** + * Method: addLayer + * Handles the map's addlayer event + * + * Parameters: + * evt - {Object} The listener argument + */ + addLayer: function(evt) { + var layer = evt.layer; + if (layer instanceof OpenLayers.Layer.Grid) { + layer.events.on({ + addtile: this.addTile, + retile: this.clearTileQueue, + scope: this + }); + var i, j, tile; + for (i=layer.grid.length-1; i>=0; --i) { + for (j=layer.grid[i].length-1; j>=0; --j) { + tile = layer.grid[i][j]; + this.addTile({tile: tile}); + if (tile.url && !tile.imgDiv) { + this.manageTileCache({object: tile}); + } + } + } + } + }, + + /** + * Method: removeLayer + * Handles the map's preremovelayer event + * + * Parameters: + * evt - {Object} The listener argument + */ + removeLayer: function(evt) { + var layer = evt.layer; + if (layer instanceof OpenLayers.Layer.Grid) { + this.clearTileQueue({object: layer}); + if (layer.events) { + layer.events.un({ + addtile: this.addTile, + retile: this.clearTileQueue, + scope: this + }); + } + if (layer.grid) { + var i, j, tile; + for (i=layer.grid.length-1; i>=0; --i) { + for (j=layer.grid[i].length-1; j>=0; --j) { + tile = layer.grid[i][j]; + this.unloadTile({object: tile}); + } + } + } + } + }, + + /** + * Method: updateTimeout + * Applies the <moveDelay> or <zoomDelay> to the <drawTilesFromQueue> loop, + * and schedules more queue processing after <frameDelay> if there are still + * tiles in the queue. + * + * Parameters: + * map - {<OpenLayers.Map>} The map to update the timeout for + * delay - {Number} The delay to apply + * nice - {Boolean} If true, the timeout function will only be created if + * the tilequeue is not empty. This is used by the move handler to + * avoid impacts on dragging performance. For other events, the tile + * queue may not be populated yet, so we need to set the timer + * regardless of the queue size. + */ + updateTimeout: function(map, delay, nice) { + window.clearTimeout(this.tileQueueId[map.id]); + var tileQueue = this.tileQueue[map.id]; + if (!nice || tileQueue.length) { + this.tileQueueId[map.id] = window.setTimeout( + OpenLayers.Function.bind(function() { + this.drawTilesFromQueue(map); + if (tileQueue.length) { + this.updateTimeout(map, this.frameDelay); + } + }, this), delay + ); + } + }, + + /** + * Method: addTile + * Listener for the layer's addtile event + * + * Parameters: + * evt - {Object} The listener argument + */ + addTile: function(evt) { + if (evt.tile instanceof OpenLayers.Tile.Image) { + evt.tile.events.on({ + beforedraw: this.queueTileDraw, + beforeload: this.manageTileCache, + loadend: this.addToCache, + unload: this.unloadTile, + scope: this + }); + } else { + // Layer has the wrong tile type, so don't handle it any longer + this.removeLayer({layer: evt.tile.layer}); + } + }, + + /** + * Method: unloadTile + * Listener for the tile's unload event + * + * Parameters: + * evt - {Object} The listener argument + */ + unloadTile: function(evt) { + var tile = evt.object; + tile.events.un({ + beforedraw: this.queueTileDraw, + beforeload: this.manageTileCache, + loadend: this.addToCache, + unload: this.unloadTile, + scope: this + }); + OpenLayers.Util.removeItem(this.tileQueue[tile.layer.map.id], tile); + }, + + /** + * Method: queueTileDraw + * Adds a tile to the queue that will draw it. + * + * Parameters: + * evt - {Object} Listener argument of the tile's beforedraw event + */ + queueTileDraw: function(evt) { + var tile = evt.object; + var queued = false; + var layer = tile.layer; + var url = layer.getURL(tile.bounds); + var img = this.tileCache[url]; + if (img && img.className !== 'olTileImage') { + // cached image no longer valid, e.g. because we're olTileReplacing + delete this.tileCache[url]; + OpenLayers.Util.removeItem(this.tileCacheIndex, url); + img = null; + } + // queue only if image with same url not cached already + if (layer.url && (layer.async || !img)) { + // add to queue only if not in queue already + var tileQueue = this.tileQueue[layer.map.id]; + if (!~OpenLayers.Util.indexOf(tileQueue, tile)) { + tileQueue.push(tile); + } + queued = true; + } + return !queued; + }, + + /** + * Method: drawTilesFromQueue + * Draws tiles from the tileQueue, and unqueues the tiles + */ + drawTilesFromQueue: function(map) { + var tileQueue = this.tileQueue[map.id]; + var limit = this.tilesPerFrame; + var animating = map.zoomTween && map.zoomTween.playing; + while (!animating && tileQueue.length && limit) { + tileQueue.shift().draw(true); + --limit; + } + }, + + /** + * Method: manageTileCache + * Adds, updates, removes and fetches cache entries. + * + * Parameters: + * evt - {Object} Listener argument of the tile's beforeload event + */ + manageTileCache: function(evt) { + var tile = evt.object; + var img = this.tileCache[tile.url]; + if (img) { + // if image is on its layer's backbuffer, remove it from backbuffer + if (img.parentNode && + OpenLayers.Element.hasClass(img.parentNode, 'olBackBuffer')) { + img.parentNode.removeChild(img); + img.id = null; + } + // only use image from cache if it is not on a layer already + if (!img.parentNode) { + img.style.visibility = 'hidden'; + img.style.opacity = 0; + tile.setImage(img); + // LRU - move tile to the end of the array to mark it as the most + // recently used + OpenLayers.Util.removeItem(this.tileCacheIndex, tile.url); + this.tileCacheIndex.push(tile.url); + } + } + }, + + /** + * Method: addToCache + * + * Parameters: + * evt - {Object} Listener argument for the tile's loadend event + */ + addToCache: function(evt) { + var tile = evt.object; + if (!this.tileCache[tile.url]) { + if (!OpenLayers.Element.hasClass(tile.imgDiv, 'olImageLoadError')) { + if (this.tileCacheIndex.length >= this.cacheSize) { + delete this.tileCache[this.tileCacheIndex[0]]; + this.tileCacheIndex.shift(); + } + this.tileCache[tile.url] = tile.imgDiv; + this.tileCacheIndex.push(tile.url); + } + } + }, + + /** + * Method: clearTileQueue + * Clears the tile queue from tiles of a specific layer + * + * Parameters: + * evt - {Object} Listener argument of the layer's retile event + */ + clearTileQueue: function(evt) { + var layer = evt.object; + var tileQueue = this.tileQueue[layer.map.id]; + for (var i=tileQueue.length-1; i>=0; --i) { + if (tileQueue[i].layer === layer) { + tileQueue.splice(i, 1); + } + } + }, + + /** + * Method: destroy + */ + destroy: function() { + for (var i=this.maps.length-1; i>=0; --i) { + this.removeMap(this.maps[i]); + } + this.maps = null; + this.tileQueue = null; + this.tileQueueId = null; + this.tileCache = null; + this.tileCacheIndex = null; + this._destroyed = true; + } + +}); |