// --------------------------------------------------
// GSUtil
//
// Author: Jonas Ekstedt, Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview A utility class containing functions for common operations.
 * @name GSUtil
 */

/**
 * <code>GSUtil</code> should not be instantiated, all of its methods are called
 * statically.
 * @constructor 
 * @static
 */
function GSUtil() {}

/**
 * A function used to extend one class with another. This function is described
 * in detail on Kevin Lindsay's website: <a href="http://kevlindev.com">http://kevlindev.com</a>
 * 
 * @param {Object} subClass the inheriting class, or subclass
 * @param {Object} baseClass the class from which to inherit
 */
GSUtil.extend = function(subClass, baseClass) {
    function inheritance() {}
    inheritance.prototype = baseClass.prototype;
    
    subClass.prototype = new inheritance();
    subClass.prototype.constructor = subClass;
    subClass.baseConstructor = baseClass;
    subClass.superClass = baseClass.prototype;
};

/**
 * Copies all properties of the source object to the target
 * object
 * @param {Object} source the object whose properties will be copied
 * to the target object
 * @param {Object} target the object that will receive the properties
 * from the source object
 * @return {Object} the modified target object
 */
GSUtil.merge = function(source, target) {
    if(!target) {
        target = {};
    }
    for(var property in source) {
        target[property] = source[property];
    }
    return target;
};

/**
 * Returns an array of values for the property specified by
 * <code>propertyName</code> by extracting the property value
 * from each of the input array's elements
 *
 * @param {Array} ar the input array
 * @param {String} propertyName the name of the property value to be
 * retrieved from the array elements
 * @return {Array} an Array of property values
 */
GSUtil.getPropertyArray = function(ar, propertyName) {
    var props = [];
    for(var i = 0, l = ar.length; i < l; i++) {
        var e = ar[i];
        if(e[propertyName] != undefined) {
            props.push(e[propertyName]);
        }
    }
    return props;
};

/**
 * Retrieves the minimum value from a number array
 *
 * @param {Array} ar the array to retrieve the minimum number from
 * @return {number} the minimum value from the array or <code>Infinity</code>
 * if the array has no values
 * @see #getMaximum
 */
GSUtil.getMinimum = function(ar) {
    var min = Infinity;
    for(var i = 0, l = ar.length; i < l; i++) {
        min = Math.min(min, ar[i]);
    }
    return min;
};

/**
 * Retrieves the maximum value from a number array
 *
 * @param {Array} ar the array to retrieve the maximum number from
 * @return {number} the maximum value from the array or <code>-Infinity</code>
 * if the array has no values
 * @see #getMinimum
 */
GSUtil.getMaximum = function(ar) {
    var max = -Infinity;
    for(var i = 0, l = ar.length; i < l; i++) {
        max = Math.max(max, ar[i]);
    }
    return max;
};

/**
 * Finds the x coordinate of the object <code>obj</code> relative to the
 * browser window coordinate space
 * @deprecated use {@link #getMousePixelCoordinate} instead
 * @param {Object} obj the object to find the x coordinate of
 * @return {int} the x coordinate of the object specified
 * @see #findPoxY
 */
GSUtil.findPosX = function(obj) {
    var curleft = 0;
    if (obj.offsetParent) {
        while (obj.offsetParent) {
            curleft += obj.offsetLeft;
            obj = obj.offsetParent;
        }
    } else if (obj.x) {
        curleft += obj.x;
    }
    return curleft;
};

/**
 * Finds the y coordinate of the object <code>obj</code> relative to the
 * browser window coordinate space
 * @deprecated use {@link #getMousePixelCoordinate} instead
 * @param {Object} obj the object to find the y coordinate of
 * @return {int} the y coordinate of the object specified
 * @see #findPoxX
 */
GSUtil.findPosY = function(obj) {
    var el = obj;
    var curtop = 0;
    if (obj.offsetParent) {
        while (obj.offsetParent) {
            curtop += obj.offsetTop;
            obj = obj.offsetParent;
        }
    } else if (obj.y) {
        curtop += obj.y;
    }
    // opera & (safari absolute) incorrectly account for body offsetTop
    if(_browser.isOpera || ((_browser.isWebkit && el.style.position == "absolute") || (_browser.isSafari && el.style.position == "relative"))) {
        curtop -= document.body.offsetTop;
    }

    return curtop;
};

/**
 * Returns the pixel coordinate at which the given click event occurred.
 * If the container parameter is provided the coordinate returned will be
 * relative to the container's coordinate space otherwise it will be
 * relative to the upper-left corner of the window.
 * @param {Event} e the click event to get the pixel coordinate for
 * @param {HTMLElement} container an element that is an ancestor of the
 * event src element
 * @return {GSPoint} the pixel coordinate of the event
 */    
GSUtil.getMousePixelCoordinate = function(e, container){
    if(!e) e = window.event;
    if(typeof e.pageX != "undefined") {
        var pos = GSUtil.getElementPosition(container);
        return new GSPoint(e.pageX - pos.x, e.pageY - pos.y);
    } else if(typeof e.offsetX != "undefined") {
        var src = e.target || e.srcElement;
        var offset = GSUtil.getOffsetFromAncestor(src, container);
        return new GSPoint(e.offsetX + offset.x, e.offsetY + offset.y);    
    } else {
        return new GSPoint(0, 0);
    }    
};

/**
 * Get the position of the upper-left corner of the element relative to the 
 * upper-left corner of the window
 * @param {HTMLElement} elem the element to get the position for
 * @return {GSPoint} the pixel coordinate of the specified element relative to the
 * window object
 */
GSUtil.getElementPosition = function(elem) {
    var pos = new GSPoint(0, 0);

    while(elem) {
        pos.x += elem.offsetLeft;
        pos.y += elem.offsetTop;
        elem = elem.offsetParent;
    }
    return pos;
};

/**
 * Gets the width/height dimensions of the current browser window.
 * @return {Object} an object with width and height properties specified in pixels
 */
GSUtil.getWindowSize = function() {
    var size = {};
    if (self.innerHeight) {
        size.width = self.innerWidth;
        size.height = self.innerHeight;
    } else if (document.documentElement && document.documentElement.clientHeight) {
        size.width = document.documentElement.clientWidth;
        size.height = document.documentElement.clientHeight;
    } else if (document.body) {
        size.width = document.body.clientWidth;
        size.height = document.body.clientHeight;
    }
    return size;
};

/**
 * Returns the pixel offset of the src element from the specified ancestor
 * element.
 * @param {HTMLElement} src the element whose offset from a given ancestor
 * element should be calculated
 * @param {HTMLElement} ancestor an ancestor of the src element
 * @return {GSPoint} the pixel offset of the src element from its ancestor
 */    
GSUtil.getOffsetFromAncestor = function(src, ancestor) {
    var offset = new GSPoint(0, 0);
    while(src && src != ancestor) {
        offset.x += src.offsetLeft;
        offset.y += src.offsetTop;
        src = src.offsetParent;
    }
    return offset;
};

/**
 * Returns the coordinate (pixels) where the specified mouse event occurred
 *
 * @param {MouseEvent} e the mouse event
 * @return {GSPoint} the coordinate (pixels) where the specified mouse event occurred
 */
GSUtil.getMousePos = function(e) {
    var ev = e ? e : window.event;
    if (typeof ev.pageX != "undefined")
        return new GSPoint(ev.pageX, ev.pageY);
    else if (typeof ev.clientX != "undefined")
        return new GSPoint(ev.clientX + document.body.scrollLeft, ev.clientY + document.body.scrollTop);
    else
        return new GSPoint(0, 0);
};

/**
 * Used to nullify an event by stopping it from propagating
 * to ancestors in the DOM and cancels any default action
 * associated with the event.
 * @param {Event} e the mouse event to cancel
 */
GSUtil.cancelEvent = function(e) {
    e = e ? e : window.event;
    GSUtil.eventStopPropagation(e);
    GSUtil.eventPreventDefault(e);
    return false;
};

/**
 * Cross-browser version of Event.stopPropagation
 * @private
 *
 * @param {Event} e the event
 */
GSUtil.eventStopPropagation = function(e) {
    if (e.stopPropagation) {
        e.stopPropagation();
    } else {
        e.cancelBubble = true;
    }
};

/**
 * Cross-browser version of Event.preventDefault()
 * @private
 *
 * @param {Event} e the event
 */
GSUtil.eventPreventDefault = function(e) {
    if (e.preventDefault) {
        e.preventDefault();
    } else {
        e.returnValue = false;
    }
};

/**
 * Sets the position of the specified HTML element
 *
 * @param {HTMLElement} elem the HTML element to position
 * @param {int} left the left position
 * @param {int} top the top position
 * @param {int} right the right position
 * @param {int} bottom the bottom position
 */
GSUtil.positionElement = function(elem, left, top, right, bottom) {
    var position = GSUtil.getComputedStyle(elem, "position");
    if(position != "absolute" && position != "relative") {
        elem.style.position = "absolute";
    }
    if(left || left == 0) elem.style.left = left + "px";
    if(top || top == 0) elem.style.top = top + "px";
    if(bottom || bottom == 0) elem.style.bottom = bottom + "px";
    if(right || right == 0) elem.style.right = right + "px";
};

/**
 * Creates a DOM element using the namespace-aware <code>createElementNS()</code>
 * method if the browser supports it (uses XHTML namespace), otherwise uses
 * <code>createElement()</code>
 * @param {String} name the name of the element to create
 * @return {HTMLElement} a DOM element
 */
GSUtil.createElement = function(name) {
    if(document.createElementNS) {
        return document.createElementNS(_globals.xmlns, name);
    } else {
        return document.createElement(name);
    }
};

/**
 * Parses an XHTML string into a DOM fragment and appends it to the target
 * element
 * @param {HTMLElement} element the element the XHTML content should be
 * appended to
 * @param {String} xhtml the XHTML content to be appended
 */
GSUtil.innerXHTML = function(element, xhtml) {
    if(window.DOMParser) { // FF, Safari, Opera
        var parser = new DOMParser();
        var doc = parser.parseFromString(xhtml, 'application/xhtml+xml');
        var root = doc.documentElement;
        for(var i = 0, l = root.childNodes.length; i < l; i++) {
            element.appendChild(document.importNode(root.childNodes[i], true));
        }
    } else { // IE, doesn't support XHTML anyway so just use innerHTML
        element.innerHTML = xhtml;
    }
};

/**
 * Creates and positions a content container.
 *
 * @param {HTMLElement} parent the parent element the new container should be appended to
 * @param {String} className the CSS class name to use for the new container (optional)
 * @param {int} width the width of the new container in pixels
 * @param {int} height the height of the new container in pixels
 * @param {int} left the x axis offset of the new container relative to its parent elements
 * top left corner
 * @param {int} top the y axis offset of the new container relative to its parent elements
 * @param {int} zIndex the zIndex value for the container
 * top left corner
 * @return {HTMLElement} the container element
 */
GSUtil.createContainer = function(parent, className, width, height, left, top, visibility, zIndex) {
    var container = GSUtil.createElement("div");
    parent.appendChild(container);
    container.className = className;
    if(width) 
        container.style.width = width == "auto" ? "auto" : width + "px";
    if(height)
        container.style.height = height == "auto" ? "auto" : height + "px";
    if(left || top || left == 0 || top == 0) {
        container.style.position = "absolute";
        container.style.left = left + "px";
        container.style.top = top + "px";
    }
    if(visibility) {
        container.style.visibility = visibility;
    }
    if(zIndex != undefined) {
        container.style.zIndex = zIndex;
    }

    return container;
};


/**
 * Creates and optionally positions a new DOM image.
 * Additionally this function abstracts browser-specific techniques 
 * for display of PNG images with alpha transparency.
 *
 * @param {String} id the image id
 * @param {String} src the image src URL
 * @param {int} width the image width
 * @param {int} height the image height
 * @param {int} left the x coordinate in the parent's coordinate system at which 
 * to anchor this image
 * @param {int} top the y coordinate in the parent's coordinate system at which 
 * to anchor this image
 * @param {int} zIndex the image's stacking order
 * @param {String} className an optional CSS class to apply to the image
 * @param {String} alt optional text to display as a tool-tip when the mouse is over the
 * image
 * @param {String} sizingMethod the sizing method to apply when rendering PNG images with
 * alpha transparency in IE using the AlphaImageLoader filter
 * @return {HTMLElement} an HTMLImageElement if not a PNG image and not Internet Explorer, otherwise
 * an HTMLDivElement (uses AlphaImageLoader for PNG transparency)
 */
GSUtil.createImage = function(id, src, width, height, left, top, zIndex, className, alt, sizingMethod) {
    var img = null;
    var fileType = src.substring(src.lastIndexOf(".") + 1);
    if(fileType.toLowerCase() == "png" && _browser.pngAlpha) {
        sizingMethod = sizingMethod ? sizingMethod : 'scale';
        img = GSUtil.createElement("div");
        img.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "'," +
            "sizingMethod='" + sizingMethod + "')";
    } else {
        img = GSUtil.createElement("img");
        img.src = src;
        img.border = 0;
        img.margin = 0;
        img.padding = 0;
    }
    img.id = id;
    if(width || height) {
        img.width = width;
        img.height = height;
        
        img.style.width = width + "px";
        img.style.height = height + "px";
    }

    if(left || top || left == 0 || top == 0) {
        img.style.position = "absolute";
        img.style.left = left + "px";
        img.style.top = top + "px";
    }

    if(className != undefined)
        img.className = className;
    if(zIndex != null)
        img.style.zIndex = zIndex;
    if(alt != undefined)
        img.title = alt;
    if(_browser.isIE) {
        img.setAttribute("galleryimg", "no");
        img.unselectable = "on";
        img.onselectstart = GSUtil.cancelEvent;
    } else {
        img.style.MozUserSelect = "none";
    }
    return img;
};

/**
 * Sets the image source where the image given may either by an HTMLImageElement
 * or a DIV using the AlphaImageLoader to load the image resource
 *
 * @param {Object} image the image to set the source for
 * @param {String} src the new source for the image
 * @see #getImageSrc
 */
GSUtil.setImageSrc = function(image, src) {
    var fileType = src.substring(src.lastIndexOf(".") + 1);
    if(fileType.toLowerCase() == "png" && _browser.pngAlpha) {
        image.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "'," +
            "sizingMethod='image')";
    } else {
        image.src = src;
    }
};

/**
 * Returns the image source as a String where the image given may
 * either by an HTMLImageElement or a DIV using the AlphaImageLoader
 * to load the image resource
 *
 * @param {Object} image the image to get the source from
 * @return {String} the image source
 * @see #setImageSrc
 */
GSUtil.getImageSrc = function(image) {
    var src = "";
    if(image.src != undefined)
        src = image.src;

    if(image.style.filter) {
        var pattern = /src='([^']+)'/;
        var result = image.style.filter.match(pattern);
        src = result[1];
    }
    return src;
};

/**
 * Creates a DOM representation of an image map. If a function is specified
 * it is added to the area of the image map as an onclick handler
 *
 * @param {String} name the name of this image map
 * @param {String} shape the shape of the image map area (rect, poly, circle)
 * @param {Array} coords the coordinates describing the area of the shape
 * @param {String} href the link to assign to the image map's shape
 * @param {String} alt text to display when the mouse runs over the area of the shape
 * @param {String} eventType the type of event that should trigger the specified function
 * @param {Function} func an optional function that can be assigned to the
 * area of the image map as an onclick handler

 * @param {Object} peer reference to the Javascript object that owns this
 * map
 * @return {HTMLElement} the image map element
 */
GSUtil.createImageMap = function(name, shape, coords, href, alt, eventType, func, peer) {
    var map = GSUtil.createElement("map");
    map.id = name; /* Required for XHTML compliance */
    map.setAttribute("name", name);
    var area = GSUtil.createElement("area");
    area.setAttribute("shape", shape);
    area.setAttribute("coords", coords.toString());
    area.setAttribute("href", href);
    area.setAttribute("title", alt);
    if(peer != undefined)
    area.peer = peer;

    if(func != undefined)
    GSEventManager.addEventListener(area, eventType, func);

    map.appendChild(area);

    return map;
};

/**
 * This function abstracts browser-specific techniques for display of background PNG images with
 * alpha transparency.
 *
 * @private
 *
 * @param {String} url the image src URL
 * @param {String} repeat can be one of: <code>repeat-x</code>, <code>repeat-y</code>, <code>no-repeat</code>
 * @return {String} a CSS style rule
 */
GSUtil.getTransparentBackgroundImageStyleRule = function(url, repeat) {
    if(_browser.pngAlpha) {
        return "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\"" + url +
        "\", sizingMethod=\"scale\"); background-repeat: " + repeat;
    } else {
        return "background: url(" + url + ") " + repeat;
    }
};

/**
 * Sets the opacity of the object specified by <code>obj</code> using the
 * browser-specific extension for this function.
 * @obj the object to set the opacity value for
 * @opacity the opacity value (as a percentage)
 */
GSUtil.setOpacity = function(obj, opacity) {
    opacity = (opacity == 100)?99.999:opacity;

    // IE/Win
    obj.style.filter = "alpha(opacity:" + opacity + ")";

    // Safari<1.2, Konqueror
    obj.style.KHTMLOpacity = opacity/100;

    // Older Mozilla and Firefox
    obj.style.MozOpacity = opacity/100;

    // Safari 1.2, newer Firefox and Mozilla, CSS3
    obj.style.opacity = opacity/100;
};

/**
 * Returns the specified computed style for the given element
 *
 * @param {Element} element the element to compute the style for
 * @param {String} prop the property to get the value for
 * @return the requested style
 */
GSUtil.getComputedStyle = function(element, prop) {
    // From http://ajaxian.com/archives/javascript-tip-watch-out-for
    if (typeof element == 'string') element = document.getElementById(element);
	
	if (element.style[prop]) {
		// inline style property
		return element.style[prop];
	} else if (element.currentStyle) {
		// external stylesheet for Explorer
		return element.currentStyle[prop];
	} else if (document.defaultView && document.defaultView.getComputedStyle) {
		// external stylesheet for Mozilla and Safari 1.3+
		prop = prop.replace(/([A-Z])/g, "-$1");
		prop = prop.toLowerCase();
                var style = document.defaultView.getComputedStyle(element,"");
                if(style) {
                    return style.getPropertyValue(prop);
                }
		return null;
	} else {
		return null;
	}
};

/**
 * Convenience function that returns the feature from the specified
 * array with a matching id. If no match is found <code>undefined</code>
 * is returned
 *
 * @param {Array} a an array of features
 * @param {String} id the id of the feature to retrieve from the array
 * @return {GSMapFeature} the matching feature or <code>undefined</code> if no match is
 * found
 */
GSUtil.getFeatureById = function(a, id) {
    for(var i = 0, l = a.length; i < l; i++) {
        if(a[i].id == id) return a[i];
    }
    return undefined;
};

/**
 * Rounds the given value to the specified number of decimal places
 *
 * @param {Number} value the value to be rounded
 * @param {Number} rlength the number of decimal places to round to
 * @return {Number} the rounded number
 */
GSUtil.roundNumber = function(value, rlength) {
    return Math.round(value * Math.pow(10, rlength)) / Math.pow(10, rlength);
};

/**
 * Returns the filename part of the given path
 * @param {String} path the path to return the basename for
 * @return {String} the filename part of the given path
 */
GSUtil.getBaseName = function(path) {
    return path.substring(path.lastIndexOf("/") + 1);
};

/**
 * Returns the filename part of the given path less the file extension if the
 * file has one
 * @param {String} path the path to return the basename for
 * @return {String} the filename part of the given path
 */
GSUtil.getBaseNameWithoutExtension = function(path) {
    var baseName = GSUtil.getBaseName(path);
    return baseName.substring(0, baseName.indexOf("."));
};

/**
 * Returns the portion of the given path up to, but not including,
 * the last slash '/' character
 * @param {String} path the path to return the dirname for
 * @return {String} the path less the filename portion
 */
GSUtil.getDirName = function(path) {
    return path.substring(0, path.lastIndexOf("/"));
};

/**
 * Clones the given object. By default this function makes a shallow
 * copy of the object. Takes an optional argument <code>deep</code>,
 * that when <code>true</code> will make a deep copy of the target
 * object.
 * @param {Object} obj the object to be cloned
 * @param {boolean} deep a flag indicating that a deep copy of the
 * object should be made when set to <code>true</code>
 * @return {Object} a copy of the given object
 */
GSUtil.clone = function(obj, deep) {
    try {
        var objectClone = new obj.constructor();
        for (var property in obj) {
            if (!deep) {
                if(property != 'eventListeners') {
                    objectClone[property] = obj[property];
                }
            } else if(property != 'eventListeners') {
                if (typeof obj[property] == 'object') {
                    objectClone[property] = GSUtil.clone(obj[property], deep);
                } else {
                    objectClone[property] = obj[property];
                }
            }
        }
        return objectClone;
    } catch(e) {
        return undefined;
    }
};

/**
 * Converts the given XML string to an XMLDocument
 * @param {String} str the XML string to be converted
 * @return {XMLDocument} an XMLDocument or <code>undefined</code> if the string
 * was not successfully parsed
 */
GSUtil.toXML = function(str) {
    try{
        if(typeof ActiveXObject != "undefined" && typeof GetObject!="undefined") {
            var doc = new ActiveXObject("Microsoft.XMLDOM");
            doc.loadXML(str);
            return doc;
        } else if(typeof DOMParser != "undefined") {
            return(new DOMParser()).parseFromString(str,"text/xml");
        } else{
            return undefined;
        }
    } catch(e) {
        alert("parse error: " + e.message);
        return undefined;
    }
};

/**
 * Returns the boolean equivalents for the string
 * values <code>true, false, yes, no</code>, or uses
 * the standard javascript rules for string to boolean
 * conversion if the value of <code>str</code> is not
 * one of these values.
 * @param {String} str the string value to test
 * @return {boolean} the boolean equivalent of the given string value
 */
GSUtil.stringToBoolean = function(str) {
    if(!str) {
        return false;
    }

    if(str == "true") {
        return true;
    }
    if(str == "false") {
        return false;
    }
    if(str == "yes") {
        return true;
    }
    if(str == "no") {
        return false;
    }

    return str == true;
};

/**
 * Allows an object's properties to be accessed without
 * regard to the property name's case.
 * @param {Object} obj the object to retrieve the property from
 * @param {String} name the name of the property
 * @param defaultValue an optional default value that should be used if
 * the named property is undefined
 * @return the property value or <code>undefined</code> if no property
 * of this name exists and a default value has not been supplied
 */
GSUtil.getCaseInsensitiveProperty = function(obj, name, defaultValue) {
    if(!obj || !(obj instanceof Object)) {
        return undefined;
    }
    var value = undefined;
    if(obj[name] != undefined) {
        value = obj[name];
    } else if(obj[name.toLowerCase()]) {
        value = obj[name.toLowerCase()];
    }
    // IE will set the onload property of a form element to the string "null"
    // if the event handler hasn't been defined
    if(value != undefined && (typeof value).toLowerCase() == "string" && value != "null") {
        return value;
    } else {
        return value != undefined ? value : (defaultValue != undefined ? defaultValue : undefined);
    }
};



/**
 * @private
 */
GSUtil.populateTemplate = function(template, model, secondaryModel) {
    for (var key in model) {
        var pattern = new RegExp("\{" + key + "\}", "g");
        template = template.replace(pattern, model[key] ? model[key] : "");
    }

    if (secondaryModel) {
        for (var key in secondaryModel) {
            var pattern = new RegExp("\{" + key + "\}", "g");
            template = template.replace(pattern, secondaryModel[key] ? secondaryModel[key] : "");
        }
    }
    return template;
};

/**
 * @private
 */
GSUtil.joinAssociativeArray = function(array, pairSeparator, keyValueSeparator) {
    var pairs = [];
    for (var key in array)
        pairs.push(key + keyValueSeparator + (array[key] != undefined ? array[key]: ""));

    return pairs.join(pairSeparator);
};


/**
 * @private
 */
GSUtil.getParameter = function(obj, key, defaultValue) {
    if (obj[key] == undefined) {
        if (arguments.length < 3)
            throw new Error("Missing parameter: " + key);
        else
            return defaultValue;
    } else {
        return obj[key];
    }
};

/**
 * Makes the provided HTML element unselectable
 * @param {HTMLElement} elem the HTML element to make unselectable
 */
GSUtil.makeUnselectable = function(elem) {
    if(typeof elem.style.MozUserSelect == "string") {
        elem.style.MozUserSelect = "none";
    } else if(typeof elem.style.KhtmlUserSelect != "undefined") {
        elem.style.KhtmlUserSelect = "none";
    } else if(typeof elem.onselectstart != "undefined") {
        elem.setAttribute("galleryimg", "no");
        elem.onselectstart = function() { return false; };
    }
};

/**
 * Serializes the specified object to a JSON string
 * @param {Obj} obj the object to serialize
 * @return {String} the serialized value
 */
GSUtil.objToJson = function(obj) {
    var buffer = ['{'];
    var p, b;
    for(p in obj) {
        if(obj.hasOwnProperty(p)) {
            if(b) {
                buffer.push(',');
            }
            b = true;
            buffer.push(p, ':', ((typeof obj[p]) == 'object' && obj[p].toJson ? obj[p].toJson() : GSUtil.serialize(obj[p])));
        }
    }
    buffer.push('}');
    return buffer.join('');
};

/**
 * Serializes the specified array to a JSON string
 * @param {Array} a the array to serialize
 * @return {String} the serialized value
 */
GSUtil.arrayToJson = function(a) {
    var buffer = ['['];
    var b;
    for(var i = 0, l = a.length; i < l; i++) {
        if(b) {
            buffer.push(',');
        }
        b = true;
        buffer.push(a[i].toJson ? a[i].toJson() : GSUtil.serialize(a[i]));
    }
    buffer.push(']');
    return buffer.join('');
};

/**
 * Returns a string representation of the specified
 * value
 * @param {Mixed} val the value to serialize
 * @return {String} the serialized value
 */
GSUtil.serialize = function(val) {
    switch(typeof val) {
    case 'object':
    if(val instanceof Array) {
        return GSUtil.arrayToJson(val);    
    } else {
        return GSUtil.objToJson(val);
    }
    case 'string':
    return '"' + val + '"';
    case 'number':
    return isFinite(val) ? String(val) : "null";
    case 'boolean':
    return String(val);
    default:
    return "";
    }
};

/**
 * Converts property names containing dashes '-' to
 * camel case
 * @param {Object} obj the object with property names to convert
 * @return {Object} the fixed object
 */
GSUtil.toCC = function(obj) {
    var fixed = {};
    for(var p in obj) {
        var s = p;
        for(var exp=/-([a-z])/; exp.test(s); s = s.replace(exp, RegExp.$1.toUpperCase()));
        fixed[s] = obj[p];
    }
    return fixed;
};

/**
 * Gets the minimum bounding rectangle for a collection of map features
 * @param {Array} features an array of features to calculate the minimum bounding
 * rectangle for
 * @return {GSBounds} the minimum bounding rectangle
 */
GSUtil.getMinimumBoundingRectangle = function(features) {
    var bounds = [];
    for(var i = 0, l = features.length; i < l; i++) {
        var feature = features[i];
        if(feature.getBounds) {
            bounds.push(feature.getBounds());
        }
    }
    var minX = GSUtil.getMinimum(GSUtil.getPropertyArray(bounds, "minX"));
    var minY = GSUtil.getMinimum(GSUtil.getPropertyArray(bounds, "minY"));
    var maxX = GSUtil.getMaximum(GSUtil.getPropertyArray(bounds, "maxX"));
    var maxY = GSUtil.getMaximum(GSUtil.getPropertyArray(bounds, "maxY"));
    return new GSBounds(minX, minY, maxX, maxY);
};

/**
 * Wraps the specified function in another function, locking its execution
 * scope to the provided scope object. Any additional arguments passed to this
 * function will be prepended to the argument list at execution time
 * @param {Function} func the function to bind
 * @param {Object} object the object that will be bound to the <code>this</code>
 * keyword within the wrapped function
 * @return {Function} the bound function
 */
GSUtil.bind = function(func, object) {
    var args = GSUtil.toArray(arguments);
    func = args.shift(), object = args.shift();
    return function() {
        return func.apply(object, args.concat(GSUtil.toArray(arguments)));
    }
};

/**
 * An event-specific variant of the bind function. Ensures the bound function
 * will always receive the current event object as the first argument at execution
 * time
 * @param {Function} func the function to bind
 * @param {Object} object the object that will be bound to the <code>this</code>
 * keyword within the wrapped function
 * @return {Function} the bound function
 */
GSUtil.bindAsEventListener = function(func, object) {
    var args = GSUtil.toArray(arguments);
    func = args.shift(), object = args.shift();
    return function(event) {
        return func.apply(object, [event || window.event].concat(args));
    }
};

/**
 * Converts a collection, such as the <code>Function.arguments</code> property
 * to an Array
 * @param {Object} collection the collection to convert to an Array
 * @return {Array} the collection as an Array
 */
GSUtil.toArray = function(collection) {
    var a = [];
    for(var i = 0, l = collection.length; i < l; i++) {
        a.push(collection[i]);
    }
    return a;
};
// --------------------------------------------------
// SmartFIND Global Variables
//
// Author: Adam Ratcliffe, Jonas Ekstedt
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * Global variables
 */
_globals = {
    xmlns : 'http://www.w3.org/1999/xhtml',
    svgns : 'http://www.w3.org/2000/svg',
    vmlns : 'urn:schemas-microsoft-com:vml',
    vmlnsPrefix : 'v',
    nzCenterX : 2530000,
    nzCenterY : 5990000,
    nzTopY : 6748000,
    resourceURL : "http://api.geosmart.co.nz/images/sfapi-v3-b/",
    tilePxWidth : 256,
    tilePxHeight : 256,
    tilesURL : (document.location.protocol.indexOf('http') == -1 ? 'http:' : document.location.protocol) + "//tiles.geosmart.co.nz/nz/v14/carto",
    multipleTileHosts: true,
    loggingEnabled: false,
    loggingBaseURL : 'http://logs.geosmart.co.nz/',
    loggingSessionTimeout: 20 * (1000 * 60),
    maps: [],
    DEBUG : false
};

var GSKeyEvents = {
    KEY_BACKSPACE: 8,
    KEY_TAB:       9,
    KEY_RETURN:   13,
    KEY_ESC:      27,
    KEY_LEFT:     37,
    KEY_UP:       38,
    KEY_RIGHT:    39,
    KEY_DOWN:     40,
    KEY_DELETE:   46,
    KEY_HOME:     36,
    KEY_END:      35,
    KEY_PAGEUP:   33,
    KEY_PAGEDOWN: 34
};

/**
 * @class A debug console
 * @private
 */
function Console() {
    win = null;
}

Console.prototype = {

    debug: function(msg) {
    if((this.win == null) || (this.win.closed)) {
        this.win = window.open("", "console", "width=600,height=300,screenX=400,resizable,scrollbars=yes");
        this.win.document.open("text/html");
    }
    this.win.focus();
    msg = new Date().toLocaleTimeString() + " " + msg + "<br/>";
    this.win.document.writeln(msg);
    }
};

var _console = new Console();
// --------------------------------------------------
// GSAnimator
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// ----------------------------------------------------

/**
 * @fileOverview A general purpose animation class.
 * @name GSAnimator
 */

/**
 * @constructor
 * @param {GSPoint} from the start position for the animation
 * @param {GSPoint} to the end position for the animation
 * @param {int} framerate the rate at which the animation frames should be drawn
 * (in milliseconds)
 * @param {int} duration the length of the animation (in milliseconds)
 * @param {Function} setvalue a user-defined function that is called every frame
 * with a reference to the animator instance and the current position as
 * arguments
 * @param {Function} onstart an optional user-defined function that is called when the
 * animation is started
 * @param {Function} onfinish an optional user-defined function that is called when the
 * animation has completed
 */
function GSAnimator(from, to, framerate, duration, setvalue, onstart, onfinish) {
    this.from = from;
    this.to = to;
    this.framerate = framerate;
    this.duration = duration;
    this.setvalue = setvalue;
    this.onstart = onstart;
    this.onfinish = onfinish;
    
    /**
     * @private
     */
    this.currentValue = from;

    /**
     * @private
     */
    this.startTime = 0;

    /**
     * @private
     */
    this.timer = undefined;
}

/**
 * Starts the animation.
 */
GSAnimator.prototype.animate = function() {
    if(!this.timer) {
        this.startTime = new Date().getTime() - this.framerate;
    }
    this.timer = setInterval(GSUtil.bind(this.frame, this), this.framerate);
    if(this.onstart) this.onstart(this);
};

/**
 * Called repeatedly to calculate the new position of the
 * element to be animated
 * @private
 */
GSAnimator.prototype.frame = function() {
    var time = new Date().getTime() - this.startTime;
    
    if(time >= this.duration) {
        this.stopAnimate();
        this.currentValue = this.to;
        this.setvalue(this, this.currentValue);

        if (this.onfinish) {
            setTimeout(GSUtil.bind(this.onfinish, this, this), 0);
        }
    } else {
        var scale = 0.5 - (0.5 * Math.cos(Math.PI * Math.max(time, 0) / this.duration));
        var x = this.interpolate(this.from.x, this.to.x, scale);
        var y = this.interpolate(this.from.y, this.to.y, scale);
        this.currentValue = new GSPoint(x, y);
        this.setvalue(this, this.currentValue);
    }
};

/**
 * Stops the animation. Generally there is no need to call this method
 * explicitly as it is called by the animator itself when the animation
 * has completed.
 */
GSAnimator.prototype.stopAnimate = function () {
    if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
    }
};

/**
 * Calculates a value intermediate between the <code>from</code> and
 * <code>to</code> values.
 * @private
 */
GSAnimator.prototype.interpolate = function(from, to, ease) {
    return from + (to - from) * ease;
};
// --------------------------------------------------
// GSBounds
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview Represents a 2-dimensional bounding box. If a <code>GSBounds</code> instance is
 * in the NZMG coordinate system the minimum coordinate represents the bottom left corner
 * of the bounding box as the value of the y axis increases from bottom to top.
 * @name GSBounds
 */

/**
 * Creates a new <code>GSBounds</code> object. The new instance may optionally be
 * initialized with its bounds.
 * @constructor
 * @param {float} minX the x coordinate of the bottom left corner of the bounding box 
 * @param {float} minY the y coordinate of the bottom left corner of the bounding box 
 * @param {float} maxX the x coordinate of the top right corner of the bounding box 
 * @param {float} maxY the y coordinate of the top right corner of the bounding box 
 */
function GSBounds(minX, minY, maxX, maxY) {
    
    /**
     * The x coordinate of the bottom left corner of the bounding box 
     * @type float
     */
    this.minX = parseFloat(minX);

    /**
     * The y coordinate of the bottom left corner of the bounding box 
     * @type float
     */
    this.minY = parseFloat(minY);

    /**
     * The x coordinate of the top right corner of the bounding box 
     * @type float
     */
    this.maxX = parseFloat(maxX);

    /**
     * The y coordinate of the top right corner of the bounding box 
     * @type float
     */
    this.maxY = parseFloat(maxY);
}

/**
 * Returns <code>true</code> if the specified point is contained within this
 * bounding box.
 * @param {GSPoint} point the point to test
 * @return {boolean} <code>true</code> if the point is within this bounding box
 */
GSBounds.prototype.contains = function(point) {
    return (point.x >= this.minX && point.x <= this.maxX) && (point.y >= this.minY && point.y <= this.maxY);
};

/**
 * Outputs this bounds properties as a String
 *
 * @return {String} this bounds properties as a String
 */
GSBounds.prototype.toString = function() {
    return "Bounds: minX=[" + this.minX + "], minY=[" + this.minY + "]" +
    ", maxX=[" + this.maxX + "], maxY=[" + this.maxY + "]";
};
// --------------------------------------------------
// GSBrowser 
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005,2006
//
// --------------------------------------------------

/**
 * @fileOverview Provides a namespace for properties related to the user browser, version,
 * and host platform.
 * <p>
 * An instance of <code>GSBrowser</code> is created when the SmartFIND API is loaded 
 * in the user's browser. It's properties can be accessed in the following way:
 * </p>
 * <pre lang="javascript">
 * if(_browser.isFirefox) {
 *   // do firefox code
 * } else if(_browser.isIE) {
 *   // do IE code
 * }</pre>
 * @name GSBrowser
 */

/**
 * @class
 * @static
 */
function GSBrowser() {
    var ua = navigator.userAgent.toLowerCase();

    /**
     * @property {boolean} <code>true</code> if the browser is Firefox
     */
    this.isFirefox = ua.indexOf("firefox") != -1;
    this.firefoxVersion = parseFloat(ua.substring(ua.indexOf("firefox") + 8));
    
    /**
     * @property {boolean} <code>true</code> if the browser is WebKit-based e.g. Safari
     */
    this.isWebkit = ua.indexOf("applewebkit") != -1;
    this.webkitVersion = ua.substr(ua.indexOf("applewebkit") + 12, 3);

    /**
     * @property {boolean} <code>true</code> if the browser is Mozilla Gecko-based e.g. Firefox
     */
    this.isGecko = !this.isWebkit && ua.indexOf('gecko') != -1;

    /**
     * @property {boolean} <code>true</code> if the browser is Camino
     */
    this.isCamino = ua.indexOf('camino') != -1;
    this.caminoVersion = parseFloat(ua.substring(ua.indexOf("camino") + 7));

    /**
     * @property {boolean} <code>true</code> if the browser is Internet Explorer
     */
    this.isIE = ((ua.indexOf("msie") != -1) && (ua.indexOf("opera") == -1));

    this.versionMinor = parseFloat(navigator.appVersion);
    if (this.isIE && this.versionMinor >= 4) {
        this.versionMinor = parseFloat( ua.substring( ua.indexOf('msie ') + 5 ) );
    }
    this.versionMajor = parseInt(this.versionMinor);

    /**
     * @property {boolean} <code>true</code> if the browser is IE6
     */
    this.isIE6x = (this.isIE && this.versionMajor == 6);

    /**
     * @property {boolean} <code>true</code> if the browser is IE6 or greater
     */
    this.isIE6up = (this.isIE && this.versionMajor >= 6);

    /**
     * @property {boolean} <code>true</code> if the browser is IE7
     */
    this.isIE7x = (this.isIE && this.versionMajor == 7);

    /**
     * @property {boolean} <code>true</code> if the browser is Opera
     */
    this.isOpera = ua.indexOf("opera") != -1;

    this.isWin = (ua.indexOf('win') != -1);
    this.isWin32 = (this.isWin && ( ua.indexOf('95') != -1 || ua.indexOf('98') != -1 || ua.indexOf('nt') != -1 || ua.indexOf('win32') != -1 || ua.indexOf('32bit') != -1) );

    this.pngAlpha = (this.isIE6x && this.isWin32);

    this.svg = ((this.isFirefox && this.firefoxVersion >= 1.5) || (this.isCamino && this.caminoVersion >= 1.0) || (this.isOpera && this.versionMajor >= 8) || (this.isWebkit && this.webkitVersion >= 500));
}

/**
 * Tests if the browser is compatible with the SmartFIND API
 * @return {boolean} <code>true</code> if the browser is compatible
 */
GSBrowser.prototype.isSmartfindCompatible = function() {
    return (this.isIE6up || this.isGecko || this.isWebkit);
};

/**
 * Tests if the browser is capable of displaying map features
 * created with the SmartFIND Vector Graphics API
 * @return {boolean} <code>true</code> if the browser is vector graphics compatible
 */
GSBrowser.prototype.isVectorGraphicsCapable = function() {
    return (this.isIE6up || this.svg);
};

_browser = new GSBrowser();
// --------------------------------------------------
// GSControl
//
// Author: Jonas Ekstedt, Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview A control that belongs to a map. This class is abstract and 
 * should not be used directly.
 * <p>
 * The <code>GSControl</code> interface defines two required methods that must be
 * implemented by subclasses: {@link GSControl#render}, in which the control DOM
 * elements are created; and {@link GSControl#getDefaultPosition} which returns
 * positioning information used by the map in determining where to place the control.
 * </p>
 * Map control classes use the GSUtil.extend() function to inherit methods and properties from
 * the GSControl base class. In the example below the second argument that's provided to the base
 * class constructor is a string uniquely identifying the map control.
 * @example
  * GSUtil.extend(GSCompactZoomControl, GSControl);
 *
 * function GSCompactZoomControl() {
 *     GSCompactZoomControl.baseConstructor.call(this, GSMap.COMPACT_ZOOM_CONTROL);
 * }
 * @name GSControl
 */

/**
 * @class A control that belongs to a map. This class is abstract and 
 * should not be used directly.
 * @constructor
 * @param name a unique identifier for the map control
 */
function GSControl(name) {

    /**
     * The control name
     *
     * @private
     */
    this.name = name;

    /**
     * The HTML element representing this control
     * @private
     * @type HTMLElement
     */
    this.element;

    /**
     * Map operated on by this control
     * 
     * @private
     */
    this.map = null;

    /**
     * An object literal with an <code>anchor</code> property that specifies the corner of the map viewport
     * in which the control should be positioned and an <code>offset</code> property which is a
     * <code>GSPoint</code> instance describing the x and y offset from the anchor position.
     * The <code>anchor</code> property should reference one of the static GSControl positioning constants.
     * @type Object
     * @private
     */
    this.position = null;

    /**
     * <code>true</code> if the map control is currently hidden
     * @private
     */
    this.hidden = false;
};

/**
 * Sets the control DOM element on this control
 * @param {HTMLElement} element the DOM element
 * @private
 */
GSControl.prototype.setElement = function(element) {
    this.element = element;
    this.element.style.zIndex = 10;
    if(!this.element.style.cursor) {
        this.element.style.cursor = 'default';
    }
};
    
/**
 * Sets the position of this control
 * @param {Object} position specifies the position of this control on the map
 * @private
 */
GSControl.prototype.setPosition = function(position) {
    if(!position.offset) position.offset = new GSPoint(0, 0);
    switch(position.anchor) {
    case GSControl.ANCHOR_TOP_LEFT:
        GSUtil.positionElement(this.element, position.offset.x, position.offset.y);
        break;
    case GSControl.ANCHOR_TOP_RIGHT:
        GSUtil.positionElement(this.element, null, position.offset.x, position.offset.y);
        break;
    case GSControl.ANCHOR_BOTTOM_LEFT:
        GSUtil.positionElement(this.element, position.offset.x, null, null, position.offset.y);
        break;
    case GSControl.ANCHOR_BOTTOM_RIGHT:
        GSUtil.positionElement(this.element, null, null, position.offset.x, position.offset.y);
        break;
    case GSControl.ANCHOR_CENTER:
        GSUtil.positionElement(this.element,
                               ((this.map.pxWidth - this.element.offsetWidth) / 2) + position.offset.x,
                               ((this.map.pxHeight - this.element.offsetHeight) / 2) + position.offset.y);
        break;
    }
    this.position = position;
};

/**
 * Returns the positioning information for this control.
 * @return {Object} a position object specifying where the control is anchored and the offset
 * of the control from the edge of the map viewport
 */
GSControl.prototype.getPosition = function() {
    return this.position;
};

/**
 * Called by the map instance the control is added to, this method creates the DOM elements for this control.
 * Within <code>render()</code> the control class is required to return a reference to outermost DOM element to the map
 * @param {GSMap} map the map instance the control belongs to
 */
GSControl.prototype.render = function(map) {};

/**
 * Returns to the map the position in the map viewport where the control should appear.
 * The control positioning information is specified using an object literal with an <code>anchor</code>
 * property that specifies the corner of the map viewport in which the control should be positioned and
 * an <code>offset</code> property which is a <code>GSPoint</code> instance describing the x and y offset
 * from the anchor position. The <code>anchor</code> property should reference one of the static GSControl
 * positioning constants.
 * <p>This positioning information can be overriden at the time the control is added to
 * the map by passing a position object as the second argument to {@link GSMap#addControl}</p>
 * @return {Object} a position object specifying where the control should be anchored and the offset of the control
 * from the edge of the map viewport, for example:
 * <pre>
 * {anchor: GSControl.ANCHOR_TOP_LEFT, offset: new GSPoint(7, 7)}
 * </pre>
 * @type Object
 */
GSControl.prototype.getDefaultPosition = function() {
    return {anchor: GSControl.ANCHOR_TOP_LEFT, offset: new GSPoint(7, 7)};
};

/**
 * Returns the width of the control in pixels. This method must be overriden
 * by control subclasses if they wish to be taken into account when positioning
 * the info window
 * @return the width of the control
 * @type int
 */
GSControl.prototype.getWidth = function() {
    return 0;
};

/**
 * Returns the height of the control in pixels. This method must be overriden
 * by control subclasses if they wish to be taken into account when positioning
 * the info window
 * @return the height of the control
 * @type int
 */
GSControl.prototype.getHeight = function() {
    return 0;
};

/**
 * Returns the orientation of the control, 'horizontal' or 'vertical'. This method should be overriden
 * by control subclasses
 * @return the orientation of the control
 * @type String
 */
GSControl.prototype.getOrientation = function() {
    return undefined;
};

/**
 * Called by the map when this control is removed so that it can do
 * any clean up necessary. This method should not be called directly, to remove
 * a map control use {@link GSMap#removeControl} instead.
 * @param {GSMap} map the map instance this control has been added to
 */
GSControl.prototype.remove = function(map) {
    try {
        map.container.removeChild(this.element);
    } catch(e) {
        // do nothing
    }
    this.element = null;
};

/**
 * Hides the map control
 */
GSControl.prototype.hide = function() {
    this.element.style.visibility = 'hidden';
    this.hidden = true;
};

/**
 * Shows the map control
 */
GSControl.prototype.show = function() {
    this.element.style.visibility = 'visible';
    this.hidden = false;
};

/**
 * Constant for specifying that a map control should be anchored in the top left
 * corner of the map viewport
 * @final
 * @type int
 */
GSControl.ANCHOR_TOP_LEFT = 0;

/**
 * Constant for specifying that a map control should be anchored in the top right
 * corner of the map viewport
 * @final
 * @type int
 */
GSControl.ANCHOR_TOP_RIGHT = 1;

/**
 * Constant for specifying that a map control should be anchored in the bottom left
 * corner of the map viewport
 * @final
 * @type int
 */
GSControl.ANCHOR_BOTTOM_LEFT = 2;

/**
 * Constant for specifying that a map control should be anchored in the bottom right
 * corner of the map viewport
 * @final
 * @type int
 */
GSControl.ANCHOR_BOTTOM_RIGHT = 3;

/**
 * Constant for specifying that a map control should be anchored in the center
 * of the map viewport
 * @final
 * @type int
 */
GSControl.ANCHOR_CENTER = 4;


/**
 * @fileOverview A compact zoom control that is rendered as a map overlay
 * @name GSCompactZoomControl
 */

GSUtil.extend(GSCompactZoomControl, GSControl);

/**
 * @extends GSControl
 * @constructor
 */
function GSCompactZoomControl() {
    GSCompactZoomControl.baseConstructor.call(this, GSMap.COMPACT_ZOOM_CONTROL);

    /**
     * The map control width
     * @private
     * @type int
     */
    this.width = 22;

    /**
     * The map control height
     * @private
     * @type int
     */
    this.height = 43;
}

/**
 * Renders this zoom control
 * @private
 */
GSCompactZoomControl.prototype.render = function(map) {
    this.map = map;
    var element = GSUtil.createElement("div");
    element.style.width = this.width + 'px';
    element.style.height = this.height + 'px';

    var shadow = GSUtil.createImage("gscompactzoomcontrol-shadow", _globals.resourceURL + "smallZoomBG.png", 22, 43, 0, 0, 0, "noprint", undefined, "image");
    GSEventManager.addEventListener(shadow, "contextmenu", GSUtil.cancelEvent);
    element.appendChild(shadow);

    var zoomIn = GSUtil.createImage("gscompactzoomcontrol_zoomin", _globals.resourceURL + "zoomIn.png", 14, 13, 2, 3, 10, "noprint", "Zoom in", "image");
    zoomIn.style.cursor = "pointer";
    element.appendChild(zoomIn);

    GSEventManager.addEventListener(zoomIn, 'click', GSUtil.bindAsEventListener(this.zoomIn, this));   
    GSEventManager.addEventListener(zoomIn, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(zoomIn, "contextmenu", GSUtil.cancelEvent);

    var zoomOut = GSUtil.createImage("gscompactzoomcontrol_zoomout", _globals.resourceURL + "zoomOut.png", 14, 13, 2, 22, 10, "noprint", "Zoom out", "image");
    zoomOut.style.cursor = "pointer";
    element.appendChild(zoomOut);

    GSEventManager.addEventListener(zoomOut, 'click', GSUtil.bindAsEventListener(this.zoomOut, this));   
    GSEventManager.addEventListener(zoomOut, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(zoomOut, "contextmenu", GSUtil.cancelEvent);

    return element;
};

/**
 * Zooms the map in
 * @private
 */
GSCompactZoomControl.prototype.zoomIn = function(e) {
    GSUtil.cancelEvent(e);
    var zoomLevel = this.map.getZoomLevel();
    if(zoomLevel - 1 < 0) return;
    this.map.zoom(zoomLevel - 1);
};

/**
 * Zooms the map out
 * @private
 */
GSCompactZoomControl.prototype.zoomOut = function(e) {
    GSUtil.cancelEvent(e);
    var zoomLevel = this.map.getZoomLevel();
    if(zoomLevel + 1 >= this.map.scaleRange.length) return;
    this.map.zoom(zoomLevel + 1);
};/**
 * @fileOverview Multi-functional map control that provides panning and zooming
 * @name GSMapControl
 */

GSUtil.extend(GSMapControl, GSControl);

/**
 * @class Multi-functional map control that provides panning and zooming
 * controls
 * @extends GSControl
 * @constructor
 * @param {Array} labels an array of label objects specifying text labels to display
 * for a given zoom label. An example array could be:
 * <pre>
 * [{level: 9, value: 'Region'}, {level: 3, value: 'Suburb'}, {level: 0, value: 'Street'}]
 * </pre>
 */
function GSMapControl(labels) {
    // Call base constructor
    GSMapControl.baseConstructor.call(this, GSMap.MAP_CONTROL);

    /**
     * The map control width
     * @private
     * @type int
     */
    this.width = 54;

    /**
     * The map control height
     * @private
     * @type int
     */
    this.height = 55;

    /**
     * @private
     */
    this.zoomControl = new GSZoomControl(labels);
}

/**
 * Returns the width of this control (in pixels)
 * @return the control width
 */
GSMapControl.prototype.getWidth = function() {
    return this.width;
};

/**
 * Returns the height of this control (in pixels)
 * @return the control height
 */
GSMapControl.prototype.getHeight = function() {
    return this.height;
};

/**
 * Renders this map control
 * @private
 */
GSMapControl.prototype.render = function(map) {
    this.map = map;

    var element = GSUtil.createElement("div");
    element.style.width = this.width + 'px';
    element.style.height = this.height + 'px';

    var shadow = GSUtil.createImage("gsmapcontrol-shadow", _globals.resourceURL + "compassBG.png", 54, 55, 0, 0, 100, "noprint", undefined, "image");
    GSEventManager.addEventListener(shadow, "contextmenu", GSUtil.cancelEvent);
    element.appendChild(shadow);
    
    var up = GSUtil.createImage("gsmapcontrol_up", _globals.resourceURL + "arrowUp.png", 14, 13, 18, 3, 100, "noprint", "Move up", "image");
    up.style.cursor = "pointer";

    GSEventManager.addEventListener(up, "click", GSUtil.bindAsEventListener(this.panTo, this, 'up'));
    GSEventManager.addEventListener(up, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(up, "contextmenu", GSUtil.cancelEvent);

    element.appendChild(up);

    var left = GSUtil.createImage("gsmapcontrol_left", _globals.resourceURL + "arrowLeft.png", 14, 13, 3, 19, 100, "noprint", "Move left", "image");
    left.style.cursor = "pointer";
    left.direction = "left";

    GSEventManager.addEventListener(left, "click", GSUtil.bindAsEventListener(this.panTo, this, 'left'));
    GSEventManager.addEventListener(left, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(left, "contextmenu", GSUtil.cancelEvent);

    element.appendChild(left);

    var right = GSUtil.createImage("gsmapcontrol_right", _globals.resourceURL + "arrowRight.png", 14, 13, 33, 19, 100, "noprint", "Move right", "image");
    right.style.cursor = "pointer";
    right.direction = "right";

    GSEventManager.addEventListener(right, "click", GSUtil.bindAsEventListener(this.panTo, this, 'right'));
    GSEventManager.addEventListener(right, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(right, "contextmenu", GSUtil.cancelEvent);

    element.appendChild(right);

    var down = GSUtil.createImage("gsmapcontrol_down", _globals.resourceURL + "arrowDown.png", 14, 13, 18, 35, 100, "noprint", "Move down", "image");
    down.style.cursor = "pointer";
    down.direction = "down";
    GSEventManager.addEventListener(down, "click", GSUtil.bindAsEventListener(this.panTo, this, 'down'));
    GSEventManager.addEventListener(down, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(down, "contextmenu", GSUtil.cancelEvent);

    element.appendChild(down);

    // render the zoom control
    var zoomContainer = this.zoomControl.render(map);
    map.container.appendChild(zoomContainer);
    this.zoomControl.setElement(zoomContainer);

    return element;
};

/**
 * Hides the map control
 */
GSMapControl.prototype.hide = function() {
    this.zoomControl.hide();
    GSMapControl.superClass.hide.call(this);
};

/**
 * Shows the map control
 */
GSMapControl.prototype.show = function() {
    this.zoomControl.show();
    GSMapControl.superClass.show.call(this);
};

/**
 * Sets the position of this control
 * @param {Object} position specifies the position of this control on the map
 * @private
 */
GSMapControl.prototype.setPosition = function(position) {
    if(position.anchor == GSControl.ANCHOR_TOP_LEFT || position.anchor == GSControl.ANCHOR_BOTTOM_LEFT) {
        this.zoomControl.setPosition({anchor: position.anchor, offset: new GSPoint(21, 65)});    
    } else {
        this.zoomControl.setPosition({anchor: position.anchor, offset: new GSPoint(25, 65)});    
    }
    
    GSMapControl.superClass.setPosition.call(this, position);
};

/**
 * Returns the panning function parameterized 
 * @private
 */
GSMapControl.prototype.panTo = function(e, direction) {
    var percentOfWindow = 0.25;
    var bounds = this.map.getBounds();
    var mapCenter = this.map.getMapCenter();
    var width = bounds.maxX - bounds.minX;
    var height = bounds.maxY - bounds.minY;
    var centerX = mapCenter.x;
    var centerY = mapCenter.y;
    switch(direction) {
    case "up":
        var delta = percentOfWindow * height;
        centerY += delta;
        break;
    case "left":
        var delta = percentOfWindow * width;
        centerX -= delta;
        break;
    case "right":
        var delta = percentOfWindow * width;
        centerX += delta;
        break;
    case "down":
        var delta = percentOfWindow * height;
        centerY -= delta;
        break;
    }
    var coordinate = new GSPoint(centerX, centerY);

    this.map.centerAtCoordinate(coordinate);
        
    GSUtil.cancelEvent(e);
};

/**
 * Called by the map when this control is removed so that it do
 * any clean up necessary. This method should not be called directly, to remove
 * a map control use {@link GSMap#removeControl} instead.
 * @private
 */
GSMapControl.prototype.remove = function(map) {
    this.zoomControl.remove(map);

    // call the super class to complete removal of this control
    GSMapControl.superClass.remove.call(this, map);
};
/**
 * @fileOverview A control that allows the user to select the area of the map to be viewed by dragging
 * a rectangle around it
 * @name GSRubberbandControl
 */

GSUtil.extend(GSRubberbandControl, GSControl);

/**
 * @class A control that allows the user to select the area of the map to be viewed by dragging
 * a rectangle around it
 * @extends GSControl
 * @constructor
 * @param {Object} [options] an object literal specifying optional parameters for control initialization:
 * @config {String} modifierKey specifies the modifier key that should be used in conjunction with the
 * mouse click in order to initiate dragging the selection rectangle
 * @config {String} fill the color of the interior of the selection rectange. It takes a
 * paint value which may be <code>none</code> indicating that no paint is applied, or a color value specified using
 * either an HTML4 keyword, RGB hex value, or rgb(...) functional value
 * @config {int} fillOpacity specifies the opacity of the selection rectange fill color. Accepts a value from 0
 * (fully transparent) to 100 (fully opaque)
 * @config {String} stroke the color of the border of the selection rectangle. It takes a
 * paint value which may be <code>none</code> indicating that no paint is applied, or a color value specified using
 * either an HTML4 keyword, RGB hex value, or rgb(...) functional value
 * @config {int} strokeWidth the width of the stroke of the selection rectangle
 */
function GSRubberbandControl(options) {
    this.options = {modifierKey: undefined,
                    fill: "#a80510",
                    fillOpacity: 25,
                    stroke: "#a80510",
                    strokeOpacity: 100,
                    strokeWidth: 1
    };
    GSUtil.merge(options, this.options);
    GSRubberbandControl.baseConstructor.call(this, 'rubberband-control');
    this.dragging = false;
    this.eventListeners = {};
}

/**
 * @private
 */
GSRubberbandControl.prototype.getDefaultPosition = function() {
    return {anchor: GSControl.ANCHOR_CENTER};
};

/**
 * Renders this control
 * @param {GSMap} map the map instance this control is to be rendered upon
 * @private
 */
GSRubberbandControl.prototype.render = function(map) {
    this.map = map;
    this.map.addListener(this);
    GSEventManager.bind(document, "keypress", this, this.handleKeyPress);

    return this.createElement();
};

/**
 * The rubberband control listens for key events and if esc is received
 * resets the controls state
 * @param {Event} e the key event received
 * @private
 */
GSRubberbandControl.prototype.handleKeyPress = function(e) {
    if(e.keyCode == 27) { // reset control state if esc key is pressed
        this.reset();
    }
};

/**
 * Creates the DOM representation of this control
 * @private
 */
GSRubberbandControl.prototype.createElement = function() {
    var element = GSUtil.createElement("div");
    element.style.visibility = "hidden";

    this.fill = GSUtil.createElement("div");
    with(this.fill.style) {
        position = "absolute";
        backgroundColor = this.options.fill;
    }
    GSUtil.setOpacity(this.fill, this.options.fillOpacity);
    element.appendChild(this.fill);

    this.stroke = GSUtil.createElement('div');
    with(this.stroke.style) {
        position = "absolute";
        border = this.options.strokeWidth + "px solid " + this.options.stroke;
    }
    GSUtil.setOpacity(this.stroke, this.options.strokeOpacity);
    element.appendChild(this.stroke);
    return element;
};

/**
 * Map listener interface
 * @param {GSMap} the map instance that called this method
 * @param {GSPoint} coordinate the coordinate of the user mouse click translated
 * to NZMG
 * @param {Event} e the mouse event
 * @private
 */
GSRubberbandControl.prototype.mapClicked = function(map, coordinate, e) {
    if(!this.dragging) {
        if(this.options.modifierKey) {
            var modifier = this.options.modifierKey.toLowerCase() + 'Key';
            if(e[modifier]) {
                this.dragStart(e);
            }
        } else {
            this.dragStart(e);
        }
    } else {
        this.dragStop(e);
    }
    GSUtil.cancelEvent(e);
};

/**
 * Called when the dragging of the rectangle is initiated
 * @param {Event} e the source mouse event
 * @private
 */
GSRubberbandControl.prototype.dragStart = function(e) {
    this.dragging = true;
    this.eventListeners["mousemove"] = GSEventManager.bind(document, "mousemove", this, this.dragMove);
    this.startPos = GSUtil.getMousePos(e);
    GSUtil.positionElement(this.element, this.startPos.x, this.startPos.y);
    document.body.style.cursor = "crosshair";
    this.element.style.visibility = "visible";
};

/**
 * Called repeatedly as the rectangle is dragged
 * @param {Event} e the source mouse event
 * @private
 */
GSRubberbandControl.prototype.dragMove = function(e) {
    var pos = GSUtil.getMousePos(e);
    var width = pos.x - this.startPos.x;
    var height = pos.y - this.startPos.y;
    this.element.style.width = Math.abs(width) + "px";
    this.element.style.height = Math.abs(height) + "px";
    this.fill.style.width = Math.abs(width) + "px";
    this.fill.style.height = Math.abs(height) + "px";
    this.stroke.style.width = Math.abs(width) - 2 + "px";
    this.stroke.style.height = Math.abs(height) - 2  + "px";
    if(width < 0) {
        this.element.style.left = (this.startPos.x - Math.abs(width)) + "px";
    }
    if(height < 0) {
        this.element.style.top = (this.startPos.y - Math.abs(height)) + "px";
    }
    GSUtil.cancelEvent(e);
};

/**
 * Called when the user initiates zooming to the region they have defined
 * @param {Event} e the source mouse event
 * @private
 */
GSRubberbandControl.prototype.dragStop = function(e) {
    GSEventManager.release(this.eventListeners["mousemove"]);
    this.zoom(e);
    this.reset();
    this.dragging = false;
};

/**
 * Resets the state of this control, either after zooming or when the user
 * dismisses the control by hitting the esc key
 * @private
 */
GSRubberbandControl.prototype.reset = function() {
    with(this.element.style) {
        width = "0px";
        height = "0px";
        visibility = "hidden";
    }    
    with(this.fill.style) {
        width = "0px";
        height = "0px";
    } 
    with(this.stroke.style) {
        width = "0px";
        height = "0px";
    } 
    document.body.style.cursor = "default";
};

/**
 * Zooms the map to the selected region
 * @param {Event} e the source mouse event
 * @private
 */
GSRubberbandControl.prototype.zoom = function(e) {
    var min = this.map.translateToRealWorldCoordinate(this.startPos);
    var max = this.map.getMouseCoordinate(e);
    var bounds = new GSBounds(min.x, min.y, max.x, max.y);
    this.map.setBounds(bounds);
};/**
 * @fileOverview Map control that allows a user to snapback to a saved map position
 * @name GSSnapbackControl
 */

GSUtil.extend(GSSnapbackControl, GSControl);

/**
 * @class Map control that allows a user to snapback to a saved map position
 * @extends GSControl
 * @param {Object} [options] an object literal specifying the control options to set:
 * @config {String} tooltip the text to display in the control's tool tip
 * @constructor
 * @private
 */
function GSSnapbackControl(options) {
    GSSnapbackControl.baseConstructor.call(this, GSMap.SNAPBACK_CONTROL);
    if(!options) { options = {}; }
    this.options = options;

    /**
     * The control width
     * @private
     * @type int
     */
    this.width = 16;

    /**
     * The control height
     * @private
     * @type int
     */
    this.height = 16;

    /**
     * The text to display in the control's tool tip
     * @private
     * @type String
     */
    this.tooltip = this.options.tooltip  ? this.options.tooltip : 'Snapback to saved map position';
}

/**
 * Returns the width of this control (in pixels)
 * @return the control width
 */
GSSnapbackControl.prototype.getWidth = function() {
    return this.width;
};

/**
 * Returns the height of this control (in pixels)
 * @return the control height
 */
GSSnapbackControl.prototype.getHeight = function() {
    return this.height;
};

/**
 * @private
 */
GSSnapbackControl.prototype.getDefaultPosition = function() {
    return {anchor: GSControl.ANCHOR_TOP_RIGHT, offset: new GSPoint(7, 7)};
};

/**
 * Renders this control
 * @param {GSMap} map the map instance this control is to be rendered upon
 * @private
 */
GSSnapbackControl.prototype.render = function(map) {
    this.map = map;

    var element = GSUtil.createImage('snapback-control', _globals.resourceURL + 'snapback.png',
                                     this.width, this.height, undefined, undefined, 10, 'noprint', this.tooltip, 'image');
    element.style.cursor = 'pointer';
    GSEventManager.bind(element, 'click', this, this.snapback);

    return element;
};

/**
 * Snaps the map back to its saved position
 * @param {Event} e the mouse click event that triggered the snapback
 * @private
 */
GSSnapbackControl.prototype.snapback = function(e) {
    GSUtil.cancelEvent(e);
    this.map.snapback();
};
/**
 * @fileOverview The map scalebar control
 * @name GSScalebarControl
 */

GSUtil.extend(GSScalebarControl, GSControl);

/**
 * @class The map scalebar control
 * @extends GSControl
 * @constructor
 * @private
 */
function GSScalebarControl() {
    GSSnapbackControl.baseConstructor.call(this, GSMap.SCALEBAR_CONTROL);
    
    /**
     * The width of this control in pixels
     * @private
     * @type int   
     */
    this.width = 220;

    /**
     * The height of this control in pixels
     * @private
     * @type int   
     */
    this.height = 30;
    
    /**
     * Array of preloaded scalebar images
     * @private
     * @type Array   
     */
    this.scalebarPreloads = [];
}

/**
 * Returns the width of this control (in pixels)
 * @return the control width
 */
GSScalebarControl.prototype.getWidth = function() {
    return this.width;
};

/**
 * Returns the height of this control (in pixels)
 * @return the control height
 */
GSScalebarControl.prototype.getHeight = function() {
    return this.height;
};

/**
 * @private
 */
GSScalebarControl.prototype.getDefaultPosition = function() {
    return {anchor: GSControl.ANCHOR_BOTTOM_LEFT, offset: new GSPoint(7, 7)};
};

/**
 * Renders this control
 * @param {GSMap} map the map instance this control is to be rendered upon
 * @private
 */
GSScalebarControl.prototype.render = function(map) {
    this.map = map;

    this.preloadScalebarImages();

    var src = _globals.resourceURL + 'scalebar/' + map.scaleIdx + '.png';
    element = GSUtil.createImage('scalebar', src, this.width, this.height, null, null, 20, 'noprint', '', 'image');

    GSEventManager.addEventListener(element, 'contextmenu', GSUtil.cancelEvent); // ignore right-click mouse events

    this.map.addListener(this);

    return element;
};

/**
 * Preloads the scalebar images
 * @private
 */
GSScalebarControl.prototype.preloadScalebarImages = function() {    
    var img;
    for(var i = 0, l = this.map.scaleRange.length; i < l; i++) {
        img = new Image();
        img.src = _globals.resourceURL + 'scalebar/' + i + '.png';
        this.scalebarPreloads.push(img);            
    }    
};

/**
 * Event handler for map bounds changes
 */
GSScalebarControl.prototype.mapBoundsChanged = function(map, oldBounds, newBounds) {
    this.updateScalebar();
};

/**
 * Event handler for map bounds changes
 */
GSScalebarControl.prototype.mapZoomed = function(map, oldZoomLevel, newZoomLevel) {
    this.updateScalebar();
};

/**
 * Called in response to map zoom events to update the scalebar image
 * @private
 */
GSScalebarControl.prototype.updateScalebar = function() {
    var src = _globals.resourceURL + 'scalebar/' + this.map.scaleIdx + '.png';
    GSUtil.setImageSrc(this.element, src);
};
/**
 * @fileOverview A zoom control showing discrete zoom levels with text labels
 * @name GSTextZoomControl
 */

GSUtil.extend(GSTextZoomControl, GSControl);

/**
 * @class A zoom control showing discrete zoom levels with text labels
 * @extends GSControl
 * @constructor
 * @param {Array} labels an array of objects specifying the labels to display for the zoom control
 */
function GSTextZoomControl(labels) {

    // Call superclass constructor
    GSTextZoomControl.baseConstructor.call(this, GSMap.TEXT_ZOOM_CONTROL);

    /**
     * The labels displayed in this control. Each label is an object literal with a <code>level</code> property, the level
     * the map will be zoomed to, and a <code>value</code> property which will be used as the value for the label
     * Default is [{level: 9, value: "Region"}, {level: 7, value: "District"},
     * {level: 3, value: "Suburb"}, {level: 1, value: "Street"}]
     *
     * @type Array
     */
    this.labels = labels;
    if(!this.labels) {
        this.labels = [{level: 9, value: "Region"}, {level: 7, value: "District"},
                       {level: 3, value: "Suburb"}, {level: 1, value: "Street"}];
    }
};

/**
 * Returns the width of this control (in pixels)
 * @return the control width
 */
GSTextZoomControl.prototype.getWidth = function() {
    return this.element.offsetWidth;
};

/**
 * Returns the height of this control (in pixels)
 * @return the control height
 */
GSTextZoomControl.prototype.getHeight = function() {
    return this.element.offsetHeight;
};

/**
 * Renders this zoom control
 * @private
 */
GSTextZoomControl.prototype.render = function(map) {
    var element = GSUtil.createElement('div');

    for(var i = 0, l = this.labels.length; i < l; i++) {
        var link = GSUtil.createElement('a');
        link.href = "#";

        GSEventManager.addEventListener(link, 'click', GSUtil.bindAsEventListener(this.zoom, this, this.labels[i].level));
        link.className = 'GSTextZoomControl';
        link.innerHTML = this.labels[i].value;

        element.appendChild(link);

        if(i < l-1) {
            element.appendChild(document.createTextNode(" | "));
        }
    }
    this.element = element;
    return element;
};

/**
 * Zooms to the specified zoom level
 * @private
 */
GSTextZoomControl.prototype.zoom = function(e, level) {
    this.map.zoom(level);
    GSUtil.cancelEvent(e);
};
/**
 * @fileOverview Large zoom control provides a slider as well as controls for
 * discrete zoom increments
 * @name GSZoomControl
 */

GSUtil.extend(GSZoomControl, GSControl);

/**
 * @class Large zoom control provides a slider as well as controls for
 * discrete zoom increments
 * controls
 * @extends GSControl
 * @constructor
 *
 * @param {Array} labels an array of label objects specifying text labels to display
 * for a given zoom label. An example array could be:
 * <pre>
 * [{level: 9, value: 'Region'}, {level: 3, value: 'Suburb'}, {level: 0, value: 'Street'}]
 * </pre>
 */
function GSZoomControl(labels) {
    // Call base constructor
    GSZoomControl.baseConstructor.call(this, GSMap.ZOOM_CONTROL);

    this.sliderBar = null;
    this.sliderThumb = null;
    this.labelContainer = null;
    this.zooming = false;

    /**
     * The width of the map control
     * @private
     * @type int
     */
    this.width = 22;

    /**
     * The height of the map control
     * @private
     * @type int
     */
    this.height = 135;

    /**
     * An array of labels for the zoom control
     * @private
     * @type Array
     */
    this.labels = labels;
}

/**
 * Returns the width of this control (in pixels)
 * @return the control width
 */
GSZoomControl.prototype.getWidth = function() {
    return this.width;
};

/**
 * Returns the height of this control (in pixels)
 * @return the control height
 */
GSZoomControl.prototype.getHeight = function() {
    return this.height;
};

/**
 * Renders this zoom control
 * @private
 */
GSZoomControl.prototype.render = function(map) {
    this.map = map;
    var element = GSUtil.createElement("div");
    element.style.width = this.width + 'px';
    element.style.height = this.height + 'px';

    // register this control with the map so that zoom
    // notifications can be received
    this.map.addListener(this);

    var sliderContainer = GSUtil.createElement("div");
    sliderContainer.style.position = "absolute";
    sliderContainer.style.left = "2px";
    sliderContainer.style.top = "0px";
    element.appendChild(sliderContainer);

    var zoomInBtn = GSUtil.createImage("gszoomcontrol_zoomin", _globals.resourceURL + "zoomIn.png", 14, 13, 2, 3, 110, "noprint", "Zoom in", "image");
    zoomInBtn.style.cursor = "pointer";
    sliderContainer.appendChild(zoomInBtn);
 
    var zoomOutBtn = GSUtil.createImage("gszoomcontrol_zoomout", _globals.resourceURL + "zoomOut.png", 14, 13, 2, 115, 110, "noprint", "Zoom out", "image");
    zoomOutBtn.style.cursor = "pointer";
    sliderContainer.appendChild(zoomOutBtn);
    
    this.sliderBar = GSUtil.createImage("gszoomcontrol_sliderbar", _globals.resourceURL + "sliderBG.png", 22, 135, 0, 0, 100, "noprint", "Click to set zoom level", "image");
    this.sliderBar.style.cursor = "pointer";

    sliderContainer.appendChild(this.sliderBar);

    this.sliderThumb = GSUtil.createImage("gszoomcontrol_sliderthumb", _globals.resourceURL + "sliderThumb.png", 15, 12, 1, GSZoomControl.SLIDER_MAX - 4, 115, "noprint", "Drag to zoom", "image");
    if(!_browser.isWebkit) {
        this.sliderThumb.style.cursor = "url(" + _globals.resourceURL + "cursor-grab.cur),-moz-grab";   
    } else {
        this.sliderThumb.style.cursor = 'pointer';
    }
    
    // slider labels
    if(this.labels) {
        this.labelContainer = GSUtil.createElement("div");
        this.labelContainer.style.position = "absolute";
        this.labelContainer.style.display = "none";
        sliderContainer.appendChild(this.labelContainer);
        
        for(var i = 0, l = this.labels.length; i < l; i++) {
            this.createLabel(this.labelContainer, this.labels[i]);
        }
    }

    // --------------------------------------------------
    // Event Handling
    // --------------------------------------------------

    GSEventManager.addEventListener(zoomInBtn, 'click', GSUtil.bindAsEventListener(this.zoomIn, this));   
    GSEventManager.addEventListener(zoomInBtn, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(zoomInBtn, "contextmenu", GSUtil.cancelEvent);

    GSEventManager.addEventListener(zoomOutBtn, 'click', GSUtil.bindAsEventListener(this.zoomOut, this));   
    GSEventManager.addEventListener(zoomOutBtn, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(zoomOutBtn, "contextmenu", GSUtil.cancelEvent);

    GSDrag.init(this.sliderThumb, null, 1, 1, GSZoomControl.SLIDER_MIN, GSZoomControl.SLIDER_MAX);

    // assign drag handler to dom-drag hook
    this.sliderThumb.onDragStart = GSUtil.bind(this.dragStartHandler, this);
    this.sliderThumb.onDragEnd = GSUtil.bind(this.dragEndHandler, this);

    GSEventManager.bind(this.sliderBar, "click", this, this.sliderClicked);
    GSEventManager.addEventListener(this.sliderBar, "dblclick", GSUtil.cancelEvent);
    GSEventManager.addEventListener(this.sliderBar, "contextmenu", GSUtil.cancelEvent);

    sliderContainer.appendChild(this.sliderThumb);

    if(this.labels) {
        GSEventManager.addEventListener(this.sliderBar, 'mouseover', GSUtil.bindAsEventListener(this.showLabels, this));
        GSEventManager.addEventListener(this.sliderBar, 'mouseout', GSUtil.bindAsEventListener(this.delayHideLabels, this));
    }

    // initialize the position of the slider thumb
    this.mapBoundsChanged(this.map);

    return element;
};

/**
 * Shows the zoom slider labels
 * @private
 */
GSZoomControl.prototype.showLabels = function(e) {
    this.labelContainer.style.display = 'block';
    GSUtil.cancelEvent(e);
};

/**
 * Hides the zoom slider labels after a brief delay
 * @private
 */
GSZoomControl.prototype.delayHideLabels = function(e) {
    setTimeout(GSUtil.bind(this.hideLabels, this), 3000);
    GSUtil.cancelEvent(e);
};

/**
 * Hides the zoom slider labels
 * @private
 */
GSZoomControl.prototype.hideLabels = function() {
    this.labelContainer.style.display = 'none';
};

/**
 * Creates a label for the zoom control
 * @private
 */
GSZoomControl.prototype.createLabel = function(parent, label) {
    var labelDiv = GSUtil.createElement("div");
    labelDiv.style.cursor = "pointer";
    labelDiv.style.position = "absolute";
    labelDiv.style.left = "16px";
    labelDiv.style.top =
    (GSZoomControl.SLIDER_OFFSET + (label.level * GSZoomControl.SLIDER_INCREMENT)) + "px";
    labelDiv.style.zIndex = 115;
    parent.appendChild(labelDiv);
    
    var img = GSUtil.createImage(null, _globals.resourceURL + "zoomLabel.png", 82, 17, 0, -2, 0, "noprint", undefined, "image");
    labelDiv.img = img;
    labelDiv.appendChild(img);

    var text = GSUtil.createElement("div");
    text.style.width = "76px";
    text.style.paddingRight = "6px";
    text.style.textAlign = "right";
    text.style.fontFamily = "Arial, Helvetica, Sans-serif";
    text.style.fontWeight = "bold";
    text.style.fontSize = "11px";
    text.style.lineHeight = "11px";
    text.style.position = "absolute";
    text.style.top = '1px';
    text.innerHTML = label.value;
    GSUtil.makeUnselectable(text);
    labelDiv.appendChild(text);
    
    // event handling
    GSEventManager.addEventListener(labelDiv, "click", GSUtil.bindAsEventListener(this.zoom, this, label.level));
    GSEventManager.addEventListener(labelDiv, "mouseover", GSUtil.bindAsEventListener(this.onLabelOver, labelDiv));
    GSEventManager.addEventListener(labelDiv, "mouseout", GSUtil.bindAsEventListener(this.onLabelOut, labelDiv));    
    GSEventManager.addEventListener(labelDiv, "contextmenu", GSUtil.cancelEvent);
};

/**
 * Zooms the map to the specified zoom level
 * @private
 */
GSZoomControl.prototype.zoom = function(e, level) {
    this.map.zoom(level);
    GSUtil.cancelEvent(e);    
};

/**
 * Label mouseover handler
 * @private
 */
GSZoomControl.prototype.onLabelOver = function(e) {
    GSUtil.setImageSrc(this.img, _globals.resourceURL + "zoomLabelOver.png");
};

/**
 * Label mouseout handler
 * @private
 */
GSZoomControl.prototype.onLabelOut = function(e) {
    GSUtil.setImageSrc(this.img, _globals.resourceURL + "zoomLabel.png");
};

/**
 * Zooms the map in
 * @private
 */
GSZoomControl.prototype.zoomIn = function(e) {
    GSUtil.cancelEvent(e);
    var zoomLevel = this.map.getZoomLevel();
    if(zoomLevel - 1 < 0) return;
    this.map.zoom(zoomLevel - 1);
};

/**
 * Zooms the map out
 * @private
 */
GSZoomControl.prototype.zoomOut = function(e) {
    GSUtil.cancelEvent(e);
    var zoomLevel = this.map.getZoomLevel();
    if(zoomLevel + 1 >= this.map.scaleRange.length) return;
    this.map.zoom(zoomLevel + 1);
};

/**
 * Event handler for dom-drag onDragStart event
 * @private
 */
GSZoomControl.prototype.dragStartHandler = function(x, y) {
    if(!_browser.isWebkit) {
        this.sliderThumb.style.cursor = "url(" + _globals.resourceURL + "cursor-grabbing.cur),-moz-grabbing";   
    }
};

/**
 * Event handler for dom-drag onDragEnd event
 * @private
 */
GSZoomControl.prototype.dragEndHandler = function(x, y) {
    if(!_browser.isWebkit) {
        this.sliderThumb.style.cursor = "url(" + _globals.resourceURL + "cursor-grab.cur),-moz-grab";
    }
    this.sliderMoved(x, y);
};

/**
 * Event handler function called when the user clicks the zoom bar slider
 * @private
 * @param {Event} e the click event
 */
GSZoomControl.prototype.sliderClicked = function(e) {
    GSUtil.cancelEvent(e);                               

    var mouseX, mouseY;
    if(_browser.isIE) {
        mouseX = e.offsetX;
        mouseY = e.offsetY;
    } else {
        mouseX = (window.pageXOffset + e.clientX - GSUtil.findPosX(this.sliderBar));
        mouseY = (window.pageYOffset + e.clientY - GSUtil.findPosY(this.sliderBar));
    }

    // only respond to mouse events within the slider bounds
    if(mouseY < GSZoomControl.SLIDER_MIN || mouseY > GSZoomControl.SLIDER_MAX) {
        return
    }

    // move the thumb
    var control = this;
    var onFrame = GSUtil.bind(this.onSlideFrame, this);
    var onComplete = GSUtil.bind(this.onSlideComplete, this, mouseX, mouseY);
    var from = new GSPoint(parseInt(this.sliderThumb.style.left), parseInt(this.sliderThumb.style.top));
    var to = new GSPoint(parseInt(this.sliderThumb.style.left), mouseY);
    var animator = new GSAnimator(from, to, 16, 500, onFrame, null, onComplete);
    animator.animate();
};

/**
 * @private
 */
GSZoomControl.prototype.onSlideFrame = function(animator, point) {
    this.sliderThumb.style.top = point.y + "px";
};

/**
 * @private
 */
GSZoomControl.prototype.onSlideComplete = function(mouseX, mouseY, animator) {
    this.sliderMoved(mouseX, mouseY);
};

/**
 * Called to update the map's zoom level when the thumb is moved
 * along the slider bar
 * @private
 * @param {int} the current x position of the thumb (this is constant)
 * @param {int} the current y position of the thumb
 */
GSZoomControl.prototype.sliderMoved = function(x, y) {
    this.zooming = true;
    var zoomLevel = Math.round((y - GSZoomControl.SLIDER_OFFSET) / GSZoomControl.SLIDER_INCREMENT);
    this.map.zoom(zoomLevel);
};

/**
 * Listener method called when the bounds of the map instance this control belongs
 * to have changed
 *
 * @private
 * @param {GSMap} map the map instance that was zoomed
 * @param {Number} oldBounds the map bounds prior to the bounds being changed
 * @param {Number} newBounds the map bounds after the bounds have been changed
 */
GSZoomControl.prototype.mapBoundsChanged = function(map, oldBounds, newBounds) {
    var newZoomLevel = this.map.scaleIdx;
    this.mapZoomed(this.map, null, newZoomLevel);
};

/**
 * Listener method called when the map instance this control belongs to has
 * been zoomed
 *
 * @private
 * @param {GSMap} map the map instance that was zoomed
 * @param {Number} oldZoomLevel the map zoom level prior to being zoomed
 * @param {Number} newZoomLevel the map zoom level after being zoomed
 */
GSZoomControl.prototype.mapZoomed = function(map, oldZoomLevel, newZoomLevel) {
    var y = Math.round(newZoomLevel * GSZoomControl.SLIDER_INCREMENT);
    y += GSZoomControl.SLIDER_OFFSET;
    y = y < GSZoomControl.SLIDER_MIN ? GSZoomControl.SLIDER_MIN : y > GSZoomControl.SLIDER_MAX ? GSZoomControl.SLIDER_MAX : y;
    this.sliderThumb.style.top = y + "px";
};

/**
 * Called by the map when this control is removed so that it do
 * any clean up necessary
 * @private
 */
GSZoomControl.prototype.remove = function(map) {
    this.map.removeListener(this);

    this.labelContainer = null;
    this.sliderBar = null;

    // clean up DOM references added by dom-drag.
    // todo: replace dom-drag
    GSDrag.obj = null;
    this.sliderThumb.root = null;
    this.sliderThumb = null;

    // call the super class to complete removal of this control
    GSZoomControl.superClass.remove.call(this, map);
};

/**
 * @private
 */
GSZoomControl.SLIDER_OFFSET = 22;
GSZoomControl.SLIDER_INCREMENT = 7;
GSZoomControl.SLIDER_MIN = 22;
GSZoomControl.SLIDER_MAX = 102;
// --------------------------------------------------
// GSSize
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview Encapulates the width and height of a component (in integer precision) in a single
 * object
 * @name GSDimension
 */

/**
 * Creates a new <code>GSDimension</code>. The new <code>GSDimension</code> instance may optionally
 * be initialized with its width and height.
 * @constructor
 */
function GSDimension(width, height) {
    
    /**
     * The width measurement
     * @type int
     */
    this.width = parseInt(width);

    /**
     * The width measurement
     * @type int
     */
    this.height = parseInt(height);
}

/**
 * Outputs the dimension's properties as a String
 *
 * @return {String} the dimension's properties as a String
 */
GSDimension.prototype.toString = function() {
    return "Dimension: width=[" + this.width + "], h=[" + this.height + "]";
};
// --------------------------------------------------
// GSEventBroadcaster
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * <code>GSEventBroadcaster</code> should not be instantiated, all of its methods are called
 * statically.
 * <code>GSEventBroadcaster</code> is a convenience class for adding listener
 * capabilities to Javascript objects
 * @class
 * @private
 */
function GSEventBroadcaster() {}

/**
 * Adds support for listeners to the object specified. After <code>obj</code>
 * has been initialized it will have 3 additional methods:
 * <p>
 * <pre>
 * addListener(obj)
 * </pre>
 * This adds the specified object as a listener
 * </p>
 * <p>
 * <pre>
 * removeListener(obj)
 * </pre>
 * This removes the specified object as a listener
 * </p>
 * <p>
 * <pre>
 * broadcastMessage(callbackMethod, arg0..argN)
 * </pre>
 * This invokes the specified callback method on all listeners that support
 * the method.
 * </p>
 *
 * @private
 *
 * @param {Object} the object to add listener support to
 */
GSEventBroadcaster.initialize = function(obj) {
    obj.listeners = [];
    obj.broadcastMessage = this.broadcastMessage;
    obj.addListener = this.addListener;
    obj.removeListener = this.removeListener;
};

/**
 * Invokes the callback method provided as the first argument to this function
 * on all listeners that support the method. Any arguments to the callback method
 * can be specified as additional parameters
 *
 * @private
 */
GSEventBroadcaster.broadcastMessage = function() {
    var eventName = arguments[0];
    var args = [];
    for(var i = 1, l = arguments.length; i < l; i++) {
        args.push(arguments[i]);
    }
    var listenersCopy = [];
    for(var i = 0, l = this.listeners.length; i < l; i++) {
        listenersCopy.push(this.listeners[i]);
    }
    for(var i = 0; i < listenersCopy.length; i++) {            
        if(listenersCopy[i][eventName]) {
            listenersCopy[i][eventName].apply(listenersCopy[i], args);
        }
    }
};

/**
 * Adds the specified object as a listener on this object. The listener
 * object should provide callback methods for any events it wishes to
 * support
 *
 * @private
 *
 * @param {Object} obj the object that should be added as a listener
 * @return <code>true</code> if the object was successfully added as a listener
 * @type boolean
 */
GSEventBroadcaster.addListener = function(obj) {
    this.removeListener(obj);
    this.listeners.push(obj);
    return true;
};

/**
 * Removes the specified object as a listener from this object
 *
 * @private
 *
 * @param {Object} obj the object that should be removed as a listener
 * @return <code>true</code> if the specified listener object was removed
 * successfully
 * @type boolean
 */
GSEventBroadcaster.removeListener = function(obj) {
    for(var i = 0, l = this.listeners.length; i < l; i++) {
        if(this.listeners[i] === obj) {
            this.listeners.splice(i, 1);
            return true;
        }
    }
    return false;
};
// --------------------------------------------------
// GSEventManager
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview GSEventManager provides an interface for managing the registration of event handlers
 * for DOM objects.
 * <p>
 * The {@link #addEventListener} and {@link #removeEventListener} methods use object detection
 * to determine which browser-specific event registration method to use.
 * </p>
 * <p>
 * The {@link #bind} and {@link #release} methods work similarly to the add and remove listener
 * methods but differ in that the {@link #bind} method calls the handler function as a method
 * of the observer object.
 * </p>
 * <p>
 *  All methods of <code>GSEventManager</code> should be called statically, for example:
 * </p>
 * <p>
 * <code>
 * GSEventManager.addEventListener(obj, "click", function() {alert("clicked");});
 * </code>
 * </p>
 * @name GSEventManager
 */

/**
 * <code>GSEventManager</code> should not be instantiated, all of its methods are called
 * statically.
 * @constructor
 * @static
 */
function GSEventManager() {}

/**
 * Binds an event on the subject to a method of the observing class. Returns a token
 * that can be passed to the {@link GSEventManager#release} method to remove the binding.
 * @param {Object} subject the object whose state is being observed
 * @param {String} eventType the type of event which the observing object is subscribing
 * to
 * @param {Object} observer the object that is listening for events of a specific type
 * occurring on the subject
 * @param {Function} method the method of the observer that should be bound to the target
 * method of the subject
 * @return {Object} a token identifying the binding to be passed to the {@link GSEventManager#release} method
 */
GSEventManager.bind = function(subject, eventType, observer, method) {
    var args = [];
    for(var i = 4, l = arguments.length; i < l; i++) {
        args.push(arguments[i]);
    }
    var adapter = GSEventManager.createAdapter(observer, method, args);
    return GSEventManager.addEventListener(subject, eventType, adapter);
};

/**
 * Creates an adapter function that will call the given method as a function of the
 * specified observer
 * @private
 * @param {Object} observer the object that the method will be called as a member of
 * @param {Function} method the method that will be called as a member of the specified
 * object
 */
GSEventManager.createAdapter = function(observer, method) {
    var args = GSUtil.toArray(arguments);
    observer = args.shift();
    method = args.shift();
    
    return function(event) {
        return method.apply(observer, [event || window.event].concat(args[0]));
    }
};

/**
 * Removes the binding of the subject event to a method on it's observer.
 * @param {Object} token a token identifying the binding to release
 */
GSEventManager.release = function(token) {
    GSEventManager.removeEventListener(token);
};

/**
 * Registers the given function <code>func</code> as a handler for events of the specified 
 * type occurring on the source object, <code>obj</code>. This method encapsulates the
 * appropriate browser-specific calls to register the handler. This function returns a token
 * that can be passed to the {@link GSEventManager#removeEventListener} to remove the event
 * listener.
 *
 * @param {Object} obj the object to add the listener to
 * @param {String} eventType the event type to add the listener for. Can be one of:
 * <code>click, mousedown, mouseup, mouseover, mouseout</code>
 * @param {Function} func The function to invoke when the event is triggered
 * @return {Object} a token that can be passed to the {@link GSEventManager#removeEventListener} to remove
 * the event listener.
 */
GSEventManager.addEventListener = function(obj, eventType, func) {
    // todo: AR - since Safari 2.0.4 addEventListener() supports the dblclick event. This code
    // should be updated to test the Safari version and do the right thing based upon version.
    if(_browser.isWebkit && eventType == "dblclick") {
        obj["on" + eventType] = func;
        return GSEventCache.add(obj, eventType, func);
    }

    if (obj.attachEvent) {
        obj.attachEvent("on" + eventType, func);
    } else {
        obj.addEventListener(eventType, func, false);
    }

    return GSEventCache.add(obj, eventType, func);
};

/**
 * Removes the given function <code>func</code> as a handler for the events of the
 * specified type on the source object.  This method encapsulates the
 * appropritate browser-specific calls to de-register the handler.
 *
 * @param {Object} token a token identifying the registered event handler to remove
 */
GSEventManager.removeEventListener = function(token) {
    GSEventCache.remove(token);
};

var GSEventCache = function() {

    var handlerId = 0;
    
    var handlers = {};

    var cache = {
        
    handlerId : handlerId,
            
    handlers: handlers,
        
    nextHandlerId : function() {
            return "h" + this.handlerId++;
        },
        
    add : function(obj, eventType, func) {
            var id = this.nextHandlerId();
            this.handlers[id] = arguments;
            return id;
        },

    flush : function() {
            for(var i in this.handlers) {
                this.remove(i);
            }
        },
            
    remove : function(id) {
            try {
                var item = this.handlers[id];
                if (item) {
                    if(item[0].removeEventListener){
                        item[0].removeEventListener(item[1], item[2], item[3]);
                    }
                                    
                    if(item[1].substring(0, 2) != "on"){
                        item[1] = "on" + item[1];
                    }
                                    
                    if(item[0].detachEvent){
                        item[0].detachEvent(item[1], item[2]);
                    }
                                    
                    item[0][item[1]] = null;
                }

                this.handlers[id] = undefined;
                delete this.handlers[id];
            } catch(e) {
                // do nothing
            }
        }
    };
    return cache;
}();

// clean up maps when page unloaded
function GSUnload() {
    var map = null;
    for(var i = 0, l = _globals.maps.length; i < l; i++) {
        map = _globals.maps[i];
        map.remove();
        for(var p in map) {
            map[p] = null;
        }
    }
}
GSEventManager.addEventListener(window, 'unload', GSUnload);// --------------------------------------------------
// GSMouseWheel
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2006
//
// Based upon the work detailed at: http://adomas.org/javascript-mouse-wheel/
// --------------------------------------------------

/**
 * @fileOverview Encapsulates browser differences in mouse wheel event handling.
 * @name GSMouseWheel
 */

/**
 * <code>GSMouseWheel</code> should not be instantiated, all of its methods are called
 * statically.
 * @constructor
 */
function GSMouseWheel() {}
   
/**
 * Binds the specified function to mouse wheel event's occurring on the subject.
 * The function will be run in the context of the observer.  If the value of
 * preventDefault is <code>true</code> the default action for the mouse wheel
 * will not be run.
 * @param {HTMLElement} subject an HTML element that can generate mouse wheel
 * events
 * @param {Object} observer the object that the handler function should be run in
 * the context of
 * @param {Function} func the handler function to be executed each time a mouse
 * wheel event is fired
 * @param {String} [modifierKey] a modifier to use to actuate mouse wheel zooming. Any of the following modifiers may be specified:
 * <code>alt, ctrl, meta, shift</code></li>
 * @return {Object} a token that can be passed to <code>GSEventManager.release()</code> to remove
 * the event listener
 */ 
GSMouseWheel.bind = function(subject, observer, func, modifierKey) {
    var eventType = (window.addEventListener && !_browser.isWebkit) ? 'DOMMouseScroll' : 'mousewheel';
    return GSEventManager.addEventListener(subject, eventType, GSUtil.bindAsEventListener(this.wheelHandler, observer, func, modifierKey));
};

/**
 * A handler function for mouse wheel events. Produces scrolling deltas that
 * can be used in the user-supplied function to determine the direction in which the
 * wheel has been scrolled.
 * @param {Event} event the mouse wheel event
 * @param {Function} func the handler function to be executed each time a mouse
 * wheel event is fired
 * @param {String} [modifierKey] a modifier to use to actuate mouse wheel zooming
 * @private
 */
GSMouseWheel.wheelHandler = function(event, func, modifierKey) {
    if(modifierKey == undefined || event[modifierKey + 'Key']) {
        var delta = 0;
        if (event.wheelDelta) { // IE/Opera
            delta = event.wheelDelta/120;
            /* In Opera 9, delta differs in sign as compared to IE */
            if (window.opera) {
                delta = -delta;
            }
        } else if (event.detail) { // Mozilla case
            /* In Mozilla, sign of delta is different than in IE. Also, delta is multiple of 3. */
            delta = -event.detail/3;
        }
        /* If delta is nonzero, handle it.
         * Basically, delta is now positive if wheel was scrolled up,
         * and negative, if wheel was scrolled down.
         */
        if (delta) {
            func.call(this, delta);
        }
        GSUtil.cancelEvent(event);
    }
};

// --------------------------------------------------
// GSIcon
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview Specifies the images that are used to represent a feature on the map.
 * The way in which the icon is created is dependent upon the icon image's file type.
 * For example PNG images with alpha transparency will use the
 * <a target="_blank" href="http://msdn.microsoft.com/workshop/author/filter/reference/filters/alphaimageloader.asp">AlphaImageLoader</a>
 * filter on Internet Explorer 6 to ensure transparency is handled correctly.
 * <p>
 * When creating a <code>GSIcon</code>, at a minimum, the <code>imageSrc</code> and 
 * <code>imageSize</code> properties must be set.
 * </p>
 * <p>
 * If the {@link #imageMap} property is set, an image map defining the clickable part
 * of the icon is created for <a target="_blank" href="http://www.mozilla.org/">Gecko</a>-based browsers.
 * </p>
 * @name GSIcon
 */

/**
 * Creates a new <code>GSIcon</code>. If a reference to an existing icon is provided
 * as the <code>copy</code> parameter its properties will be copied to the new icon
 * instance. 
 * @constructor
 * @param {GSIcon} copy an existing icon whose properties will be copied to the new icon
 */
function GSIcon(copy) {

    /**
     * The URL for the icon image
     * @type String
     */
    this.imageSrc = undefined;
    if(copy) {
        this.imageSrc = copy.imageSrc;
    }

    /**
     * The size of the icon foreground image
     * @type GSDimension
     */
    this.imageSize = undefined;
    if(copy) {
        this.imageSize = copy.imageSize;
    }

    /**
     * The URL for the icon print image
     * @type String
     */
    this.printSrc = undefined;
    if(copy) {
        this.printSrc = copy.printSrc;
    }

    /**
     * The size of the icon print image
     * @type GSDimension
     */
    this.printSize = undefined;
    if(copy) {
        this.printSize = copy.printSize;
    }

    /**
     * The URL for the icon shadow image
     * @type String
     */
    this.shadowSrc = undefined;
    if(copy) {
        this.shadowSrc = copy.shadowSrc;
    }

    /**
     * The size of the icon shadow image
     * @type GSDimension
     */
    this.shadowSize = undefined;
    if(copy) {
        this.shadowSize = copy.shadowSize;
    }

    /**
     * The text to be displayed as a tool tip when the user mouses over the icon
     * @type String
     */
    this.alt = undefined;

    /**
     * The pixel offset for the top left corner of the icon image relative to the feature's pixel coordinate
     * @type GSPoint
     */
    this.iconOffset = new GSPoint(0, 0);
    if(copy) {
        this.iconOffset = new GSPoint(copy.iconOffset.x, copy.iconOffset.y);
    }

    /**
     * The pixel offset relative to the top left corner of the icon
     * that should be used to position the info window with respect to this icon's
     * pixel coordinate
     * @type GSPoint
     */
    this.iconInfoWindowOffset = new GSPoint(0, 0);
    if(copy) {
        this.iconInfoWindowOffset = new GSPoint(copy.iconInfoWindowOffset.x, copy.iconInfoWindowOffset.y);
    }

    /**
     * An int array that specifies the x/y coordinates of the image map that defines
     * the clickable part of the icon image
     * @type Array
     */
    this.imageMap = undefined;
    if(copy) {
        this.imageMap = copy.imageMap;
    }

    /**
     * The image map shape. Can be one of <code>rect</code>, <code>circle</code>, 
     * <code>poly</code>. Defaults to <code>poly</code>.
     * @type String
     */
    this.imageMapShape = "poly";
    if(copy) {
        this.imageMapShape = copy.imageMapShape;
    }

    /**
     * The layer this icon has been added to
     * @private
     * @type GSLayer
     */
    this.layer = undefined;

    /**
     * <code>true</code> if this icon should be displayed on the map
     * @private
     * @type boolean
     */
    this.visible = true;

    /**
     * @private
     */
    this.arcHeight = 20;

}

/**
 * Static variable that is incremented to provide a unique identifier
 * for each icon image map created
 * @private
 */
GSIcon.iconId = 0;

/**
 * Returns the next identifier in the sequence maintained by the
 * <code>GSIcon</code> class
 * @private
 * @return {int} a unique identifier
 */
GSIcon.nextId = function() {
    return GSIcon.iconId++;
};

/**
 * @private
 */
GSIcon.prototype.initialize = function() {
    this.image = GSUtil.createImage("", this.imageSrc, this.imageSize.width, this.imageSize.height,
                                    0, 0, 10, "noprint gsicon_image", this.alt, "image");
    GSEventManager.addEventListener(this.image, "contextmenu", GSUtil.cancelEvent);
    this.image.style.cursor = "pointer";

    this.dragCrosshair = GSUtil.createImage("", _globals.resourceURL + "dragCross.png", 16, 16,
                                            0, 0, 0, "noprint", null, "image");
    this.dragCrosshair.style.display = "none";

    if(_globals.DEBUG)
        this.image.style.border = "1px solid #FF0000";

    if(this.printSrc != null) {
        this.printImage = GSUtil.createImage("", this.printSrc, this.printSize.width, this.printSize.height,
                                             0, 0, 0, "noscreen gsicon_printImage", null, "image");
    }

    if(this.shadowSrc != null) {            
        this.shadow = GSUtil.createImage("", this.shadowSrc, this.shadowSize.width, this.shadowSize.height,
                                         0, 0, 0, "noprint gsicon_shadowImage", null, "image");
        GSEventManager.addEventListener(this.shadow, "contextmenu", GSUtil.cancelEvent);
    }

    if(_browser.isGecko && this.imageMap != null) {
        var iconId = "map_" + GSIcon.nextId();
        this.image.setAttribute("usemap", iconId);
        this.htmlImageMap = GSUtil.createImageMap(iconId, this.imageMapShape, this.imageMap, "#", this.alt);
        GSEventManager.addEventListener(this.htmlImageMap, "contextmenu", GSUtil.cancelEvent);
    }    
};

/**
 * Sets the visibility of this icon on the map
 *
 * @param {boolean} visible <code>true</code> if this icon should
 * be visible on the map
 * @private
 */
GSIcon.prototype.setVisible = function(visible) {
    this.visible = visible;
};

/**
 * Adds this icon to the specified layer
 * @param {GSLayer} layer the layer instance to add this icon to
 * @private
 */
GSIcon.prototype.addToLayer = function(layer) {
    this.layer = layer;
    this.layer.iconContainer.appendChild(this.image);

    this.layer.iconContainer.appendChild(this.dragCrosshair);

    if(this.printImage) {
        this.layer.iconContainer.appendChild(this.printImage);
    }

    if(this.shadow) {            
        this.layer.iconShadowContainer.appendChild(this.shadow);
    }

    if(this.htmlImageMap) {
        this.layer.iconContainer.appendChild(this.htmlImageMap);
    }        
};

/**
 * Displays this icon on the map
 *
 * @private
 *
 * @param {GSLayer} the layer to render this feature on
 * @param {GSPoint) coordinate the point in the map image's coordinate
 * system at which to render this icon
 * @return {boolean} <code>true</code> if this icon could be rendered in the current map extents
 */
GSIcon.prototype.render = function(layer, coordinate) {
    if(layer.map.contains(coordinate)) {
        var pos = layer.map.translateToMapCoordinate(coordinate);
        var left = (pos.x + this.iconOffset.x);
        var top = (pos.y + this.iconOffset.y);

        GSUtil.positionElement(this.image, left, top);

        if(this.shadow) {
            GSUtil.positionElement(this.shadow, left, top);
        }

        if(this.printImage) {
            GSUtil.positionElement(this.printImage, left, top);
        }       

        var v = this.visible ? "visible" : "hidden";
        if(this.printImage)
            this.printImage.style.visibility = v;
        
        if(this.shadow)
            this.shadow.style.visibility = v;
        
        this.image.style.visibility = v;
   
        if(_globals.DEBUG)
            _console.debug("GSIcon: left: " + this.image.style.left + ", top: " + this.image.style.top);
        return v == 'visible';
    } else {
        this.image.style.visibility = "hidden";
        if(this.printImage)
            this.printImage.style.visibility = "hidden";
        if(this.shadow)
            this.shadow.style.visibility = "hidden";
        return false;
    }
};

/**
 * Repositions the crosshair during dragging
 * @param {int} x the current x coordinate of the feature this icon belongs to
 * @param {int} y the current y coordinate of the feature this icon belongs to
 * @private
 */
GSIcon.prototype.repositionDragCrosshair = function(x, y) {
    GSUtil.positionElement(this.dragCrosshair, x - 8, y - 8);// position centre of crosshair on coordinate
};

/**
 * Returns the target object for all events triggered on this icon
 * @private
 * @return {HTMLElement} the target object for all events triggered on this icon
 */
GSIcon.prototype.getEventTarget = function() {
    //return this.htmlImageMap ? this.htmlImageMap.firstChild : this.image;
    return this.image;
};

/**
 * Allows this icon to do any clean up necessary before
 * it is destroyed
 * @private
 */
GSIcon.prototype.remove = function() {
    try {
        this.layer.iconContainer.removeChild(this.image);
        this.image = null;
    
        if(this.htmlImageMap != null) {
            this.layer.iconContainer.removeChild(this.htmlImageMap);
            this.htmlImageMap = null;
        }
    
        if(this.printImage != null) {
            this.layer.iconContainer.removeChild(this.printImage);
            this.printImage = null;
        }
    
        if(this.shadow != null) {
            this.layer.iconShadowContainer.removeChild(this.shadow);
            this.shadow = null;
        }
    } catch(e) {
        // do nothing
    }
};

/**
 * Outputs information about this icon as a String
 *
 * @return {String} information about this icon as a String
 */
GSIcon.prototype.toString = function() {
    var out = "";
    out += ("image src: " + this.imageSrc + "<br/>");
    out += ("print image src: " + this.printSrc + "<br/>");
    out += ("shadow image src: " + this.shadowSrc + "<br/>");
    out += ("icon offset: " + this.iconOffset + "<br/>");
    out += ("icon info window offset: " + this.iconInfoWindowOffset + "<br/>");
    if(_browser.isGecko && this.imageMap)
        out += ("image map: " + this.imageMap.name);
    return out;
};

/**
 * A default icon to be used for point features that don't specify an icon.
 * @private
 */
GSIcon.defaultIcon = new GSIcon();
GSIcon.defaultIcon.imageSrc = _globals.resourceURL + "defaultIcon.png";
GSIcon.defaultIcon.printSrc = _globals.resourceURL + "defaultIcon.gif";
GSIcon.defaultIcon.shadowSrc = _globals.resourceURL + "defaultIconShadow.png";
GSIcon.defaultIcon.imageSize = new GSDimension(22, 16);
GSIcon.defaultIcon.printSize = new GSDimension(22, 16);
GSIcon.defaultIcon.shadowSize = new GSDimension(25, 19);
if(_browser.isGecko) {
    GSIcon.defaultIcon.imageMap = [0,0,0,14,16,14,18,15,21,15,21,12,19,10,19,0];
 }
GSIcon.defaultIcon.iconOffset = new GSPoint(-22, -16);
GSIcon.defaultIcon.iconInfoWindowOffset = new GSPoint(10, -1);
// --------------------------------------------------
// GSLayer
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview <code>GSLayer</code> objects are used to work with collections of features.
 * Layers allow all features owned by the layer to be added, removed, or updated
 * collectively.
 * <p>
 * <code>GSLayer</code> objects should not be instantiated directly but created through
 * a call to the {@link GSMap} object that will contain the layer:
 * </p>
 * <p>
 * <code>var roadLayer = myMap.createLayer("roads");</code>
 * </p>
 * <p>
 * By default a newly created layer is visible on the map. This means that calling either
 * the layer's {@link #addFeature} or {@link #addFeatures} method will cause the added
 * features to appear upon the map immediately. In situations where this isn't the desired
 * behaviour the layers {@link #visible} property can be set to <code>false</code>
 * before any features are added.
 * @name GSLayer
 * </p>
 */

/**
 * Creates a new <code>GSLayer</code>. The <code>name</code> parameter is required and
 * is used to reference this layer in map operations that work with layers.
 * @constructor
 * @param {String} name the unique name for this layer
 * @param {GSMap} map the map instance this layer belongs to
 * @param {Object} options an object literal specifying options for this layer. The following options
 * may be specified:
 * @config {int} zIndex the zIndex to display this layer at
 */
function GSLayer(name, map, options) {    
    if(!options) {options = {};}

    /**
     * The type of this class as a string value
     * @private
     * @type String
     */
    this.type = 'GSLayer';
    
    /**
     * The layer name
     *
     * @type String
     */
    this.name = name;

    /**
     * The z-index the layer should occupy within the map content
     * hierarchy
     * @private
     * @type int
     */
    this.zIndex = options.zIndex ? options.zIndex : 0;

    /**
     * <code>true</code> if the features contained by this layer should be visible on
     * the map
     * @private
     * @type boolean
     */
    this.visible = true;

    /**
     * @private
     * @type Array
     */
    this.data = [];

    /**
     * A reference to the <code>GSMap</code> instance this layer has been created
     * for
     * @private
     * @type GSMap
     */
    this.map = map;
    
    this.createOverlayContainers();
}

/**
 * Sets the z-index property of this layer
 * @param {int} zIndex the zIndex to use for this layer
 */
GSLayer.prototype.setZIndex = function(zIndex) {
    this.zIndex = zIndex;
    if(this.contentContainer) {
        this.contentContainer.style.zIndex = zIndex;
    }
};

/**
 * Sets the visibility of the features contained by this layer
 *
 * @param {boolean} visible <code>true</code> if this layer's features should be
 * visible
 */
GSLayer.prototype.setVisible = function(visible) {
    this.visible = visible;
    for(var i = 0, l = this.data.length; i < l; i++) {
        this.data[i].setVisible(visible);
        if(visible) {
            this.data[i].render();
        }
    }
};

/**
 * Returns a boolean value indicating the visibility of the features added to this layer
 * @return {boolean} <code>true</code> if this layer's features are currently visible on the map
 */
GSLayer.prototype.isVisible = function() {
    return this.visible;
};

/**
 * Returns this layer's features
 * @return {Array} an array of map features
 */
GSLayer.prototype.getFeatures = function() {
    return this.data;
};

/**
 * Adds the features in the specified array to this layer.
 *
 * @param {Array} features an array of features to be added to this layer
 */
GSLayer.prototype.addFeatures = function(features) {
    for(var i = 0, l = features.length; i < l; i++) {
        this.addFeature(features[i]);
    }
};

/**
 * Adds the features specified in JSON notation to this layer. Currently
 * this method can be used to instantiate point features only.
 * <p>
 * The minimum properties required for each POI data object are:
 * </p>
 * <ul>
 * <li><strong>name</strong> - this will be displayed in the POI feature tooltip</li>
 * <li><strong>x</strong> - the NZMG X coordinate</li>
 * <li><strong>y</strong> - the NZMG Y coordinate</li>
 * <li><strong>jsclass</strong> - the map feature class that should be instantiated, in this case <code>GSPointFeature</code></li>
 * </ul>
 * @param {Array} data an array of map features described in JSON notation
 * @param {Function} onCreate an optional callback function that will be called after each
 * map feature has been created. It takes 2 parameters: the first is the instance
 * of the feature class that's been created, and the 2nd is the current array element
 * @param {mixed} [args] any number of optional, user-defined arguments that will be
 * passed to the <code>onCreate</code> callback function
 * @throws {Error} if the feature data specifies a non-existent
 * map feature class to instantiate
 */
GSLayer.prototype.addFeaturesJson = function(data, onCreate) {
    // get any additional arguments passed to the function
    var args;
    if(arguments.length > 2) {
        args = [];
        for(var i = 2, l = arguments.length; i < l; i++) {
            args.push(arguments[i]);
        }
    }
    for(var i = 0, l = data.length; i < l; i++) {
        var className = data[i].jsclass;
        if(className) {
            if(window[className]) {
                var feature = new window[className](data[i]);
                if(onCreate) {
                    onCreate(feature, data[i], args);
                }
                this.addFeature(feature);
            } else {
                throw new Error('Trying to instantiate a non-existent class: ' + className);
            }
        }
    }
};

/**
 * Adds the specified feature to this layer. 
 * @param {Object} feature the map feature to add
 */
GSLayer.prototype.addFeature = function(feature) {
    this.data.push(feature);
    feature.setVisible(this.visible);
    feature.addToLayer(this);
    
    // if the feature is snappable add it to the map's
    // list of snappables
    if(feature.snappable) {
        this.map.addSnappable(feature);
    }
};

/**
 * Removes the specified feature from this layer.
 * @param {Object} feature the map feature to remove
 * @return {GSMapFeature} the map feature that was removed or <code>null</code>
 * if the feature was not contained within this layer
 */
GSLayer.prototype.removeFeature = function(feature) {
    for (var i = 0, l = this.data.length; i < l; i++) {
        if (feature == this.data[i]) {
            feature.remove();
            return this.data.splice(i, 1);
        }
    }
    return null;
};

/**
 * Removes all features from this layer
 */
GSLayer.prototype.clear = function() {
    for(var i = 0, l = this.data.length; i < l; i++) {
        this.data[i].remove();
    }
    this.data.length = 0;
};

/**
 * Called by the map when removing this layer
 */
GSLayer.prototype.remove = function() {
    this.clear();
    this.map.layerContainer.removeChild(this.contentContainer);    
    this.contentContainer = null;
    this.lineContainer = null;
    this.iconShadowContainer = null;
    this.iconContainer = null;
};

/**
 * Returns the minimum bounds (NZMG) that encompasses all features
 * that belong to this layer
 * @return {GSBounds} the minimum bounds that encompass this layer's features
 */
GSLayer.prototype.getBounds = function() {
    return GSUtil.getMinimumBoundingRectangle(this.data);
};

/**
 * Creates the containers that hold content overlaid over the background map image
 * @private
 */
GSLayer.prototype.createOverlayContainers = function() {
    this.contentContainer = GSUtil.createContainer(this.map.layerContainer, null, null, null, 0, 0, null, this.zIndex);
    this.contentContainer.id = 'gs_layer_' + this.name;

    this.lineContainer = GSUtil.createContainer(this.contentContainer, null, null, null, 0, 0, null, 10);
    
    this.iconShadowContainer = GSUtil.createContainer(this.contentContainer, null, null, null, 0, 0, null, 30);
    this.iconContainer = GSUtil.createContainer(this.contentContainer, null, null, null, 0, 0, null, 40);
};

/**
 * Creates the container for vector elements. Will either use SVG or VML DOM depending upon
 * host browser
 * @private
 */
GSLayer.prototype.createVectorContainer = function() {    
    var container = null;
    var map = this.map;
    if(_browser.svg) {
        container = document.createElementNS(_globals.svgns, 'svg');
        this.contentContainer.appendChild(container);
        container.style.zIndex = 20;
        GSUtil.positionElement(container, 0, 0);
        container.setAttributeNS(null, 'width', map.pxWidth + 'px');
        container.setAttributeNS(null, 'height', map.pxHeight + 'px');
        container.mapResized = GSUtil.bind(this.mapResized, container, map);
    } else if(_browser.isIE6up) {
        container = document.createElement(_globals.vmlnsPrefix + ":group");
        this.contentContainer.appendChild(container);
        container.style.zIndex = 20;
        GSUtil.positionElement(container, 0, 0);
        container.style.width = map.pxWidth + "px";
        container.style.height = map.pxHeight + "px";        
        container.setAttribute("coordsize",  map.pxWidth + " " + map.pxHeight);
        container.mapResized = GSUtil.bind(this.mapResizedIE, container, map);
    }
    map.addListener(container);
    return container;
};

/**
 * @private
 */
GSLayer.prototype.mapResized = function(map) {
    this.setAttributeNS(null, 'width', map.pxWidth + 'px');
    this.setAttributeNS(null, 'height', map.pxHeight + 'px');
};

/**
 * @private
 */
GSLayer.prototype.mapResizedIE = function(map) {
    this.style.width = map.pxWidth + "px";
    this.style.height = map.pxHeight + "px";
    this.setAttribute("coordsize",  map.pxWidth + " " + map.pxHeight);    
};

/**
 * Outputs a JSON representation of this layer
 * @return {String} a JSON string
 */
GSLayer.prototype.toJson = function() {
    var obj = {
    type: this.type,
    zIndex: this.zIndex,
    visible: this.visible,
    feature: this.data
    };
    return GSUtil.objToJson(obj);
};

/**
 * Returns a string representation of this layer for debugging purposes
 * @return {String} the string representation of the features contained by this layer
 */
GSLayer.prototype.toString = function() {
    var s = "";
    for(var i = 0, l = this.data.length; i < l; i++) {
        s += this.data[i].toString();
    }
    return s;
};
// --------------------------------------------------
// GSMap
//
// Author: Adam Ratcliffe, Jonas Ekstedt
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview GSMap encapsuates functionality for map creation, management of map feature layers, and map navigation.
 * @name GSMap
 */

/**
 * Constructs a new <code>GSMap</code> that is initialized with the
 * properties provided in the <code>options</code> object literal.
 * <p> Most of the available options may be set on the new map
 * instance after it has been created using the {@link GSMap#setMapOptions} method.</p>
 * @example
 * <pre lang="javascript">
 * var options = {
 *   centerX: 2530000,
 *   centerY: 5990000,
 *   width: 600,
 *   height: 400,
 *   zoomLevel: 3
 * };
 * var map = new GSMap('map', options);
 * </pre>
 * @constructor
 * @param {Mixed} container may be either a reference to the DOM element that will contain the map,
 * or the value of the <code>id</code> property of that element.
 * @param {Object} [options] an object literal specifying the map options to set:
 * @config {boolean} centerOnDblClick when <code>true</code> enables map recentering when double-clicking on the map
 * @config {boolean} dragToPan when <code>true</code> enables map panning by moving the mouse while
 * holding down the left mouse button.
 * @config {boolean} useMouseWheelZooming if <code>true</code> mouse wheel zooming is enabled for the map
 * @config {String} mouseWheelModiferKey specifies a modifier to use to actuate mouse wheel zooming
 * @config {boolean} useInfoWindow if <code>true</code> the info window is enabled for the map
 * @config {boolean} useScalebar if <code>true</code> a scalebar will be displayed on the map
 * @config {boolean} resizeable if <code>true</code> the map will be sized to fit its container element
 * @config {int} zoomLevel an integer value specifying the initial zoom level of the map, ranging
 * from 0 (maximum zoom in) to 11 (maximum zoom out)
 * @config {float} centerX the x coordinate the map should be initially centered on, currently the NZMG projection
 * is supported only
 * @config {float}centerY the y coordinate the map should be initially centered on, currently
 * the NZMG projection is supported only
 * @config {int} width the width of the map viewport in pixels. This option should be used exclusively of the
 * <code>resizeable</code> option
 * @config {int} height the height of the map viewport in pixels. This option should be used exclusively of the
 * <code>resizeable</code> option
 * @config {String} customCursor specifies the mouse cursor to display for the map viewport
 * @throws {Error} if a DOM element is not specified as the first argument
 */
function GSMap(container, options) {
    _globals.maps.push(this);

    /**
     * The options the map has been intitialized with
     * @private
     */
    this.options = options ? options : {};

    // add support for listeners
    GSEventBroadcaster.initialize(this);

    /**
     * The containing element for this map instance
     *
     * @private
     * @type HTMLElement
     */
    if(arguments[0]) {
        if((typeof arguments[0]) == 'string') {
            this.container = document.getElementById(container);
        } else {
            this.container = container;
        }                
    } else {
        throw new Error("Cannot create map, no container argument provided.");
    }

    /**
     * When <code>true</code> features such as the SmartFIND logo are disabled. Defaults to
     * <code>true</code>.
     * @private
     * @type boolean
     */
    this.isChildMap = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "isChildMap", false));    

    /**
     * When <code>true</code> the GeoSmart logo is displayed on the map. Defaults to
     * <code>true</code>.
     * @private
     * @type boolean
     */
    this.useLogo = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "useLogo", true));    
    if(this.isChildMap) {
        this.useLogo = false;
    }

    /**
     * When <code>true</code> enables map recentering when double-clicking on map. Defaults to
     * <code>true</code>.
     * @private
     * @type boolean
     */
    this.centerOnDblClick = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "centerOnDblClick", true));

    /**
     * When <code>true</code> enables map panning by moving the mouse while holding down the
     * left mouse button. Defaults to <code>true</code>.
     * @private
     * @type boolean
     */
    this.dragToPan = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "dragToPan", true));

    /**
     * <code>true</code> if mouse wheel zooming is enabled for this map. Defaults to <code>true</code>
     * @private
     * @type boolean
     */
    this.useMouseWheelZooming = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "useMouseWheelZooming", true));

    /**
     * Specifies a modifier to use to actuate mouse wheel zooming. Any of the following modifiers may be specified:
     * <code>alt, ctrl, meta, shift</code></li>
     * @private
     * @type String
     */
    this.mouseWheelModifierKey = GSUtil.getCaseInsensitiveProperty(this.options, "mouseWheelModifierKey");

    /**
     * <code>true</code> if info window is enabled for this map. Defaults to <code>true</code>
     * @private
     * @type boolean
     */
    this.useInfoWindow = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "useInfoWindow", true));

    /**
     * <code>true</code> if the scalebar is enabled for the map
     * @private
     * @type boolean
     */
    this.useScalebar = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "useScalebar", true));

    /**
     * Scale range (mpx)
     * @private
     */
    this.scaleRange = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
    this.scaleRange.indexOf = function(element) {
        for(var i = 0, l = this.length; i < l; i++) {
            if(element == this[i]) {
                return i;
            }
        }
        return -1;
    };

    /**
     * Pointer into the scale range
     * @private
     */
    this.scaleIdx = Number(GSUtil.getCaseInsensitiveProperty(this.options, "zoomLevel", this.scaleRange.length - 1));

    /**
     * The map scale expressed as the ratio metres:pixels
     * @private
     * @type int
     */
    this.mpx = this.scaleRange[this.scaleIdx];

    /**
     * The x coordinate this map is centred on, currently the NZMG
     * projection is supported only
     *
     * @private
     * @type float
     */
    this.centerX = GSUtil.getCaseInsensitiveProperty(this.options, "centerX") ? parseFloat(GSUtil.getCaseInsensitiveProperty(this.options, "centerX")) : 2530000;

    /**
     * The y coordinate this map is centred on, currently the NZMG
     * projection is supported only
     *
     * @private
     * @type float
     */
    this.centerY = GSUtil.getCaseInsensitiveProperty(this.options, "centerY") ? parseFloat(GSUtil.getCaseInsensitiveProperty(this.options, "centerY")) : 5990000;

    /**
     * The width of the map viewport in pixels
     *
     * @private
     * @type int
     */
    this.pxWidth = GSUtil.getCaseInsensitiveProperty(this.options, "width") ? Number(GSUtil.getCaseInsensitiveProperty(this.options, "width")) : 500;

    /**
     * The height of the map viewport in pixels
     *
     * @private
     * @type int
     */
    this.pxHeight = GSUtil.getCaseInsensitiveProperty(this.options, "height") ? Number(GSUtil.getCaseInsensitiveProperty(this.options, "height")) : 400;

    /**
     * If <code>true</code> specifies that the map can be resized in
     * response to changes in the size of its container
     *
     * @private
     * @type boolean
     */
    this.resizeable = false;
    if(this.options.width && this.options.height) {
        this.resizeable = false;
    } else {
        this.resizeable = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(this.options, "resizeable", true));
    }

    /**
     * A custom cursor to display for the map viewport
     * @private
     * @type String
     */
    this.customCursor = GSUtil.getCaseInsensitiveProperty(this.options, "customCursor");

    // Calculate the initial extents for the map
    this.recalculateMapExtents();

    // create and initialize map container hierarchy
    this.initDOM();

    /**
     * Event listeners associated with this map instance
     * @private
     */
    this.eventListeners = {};
    this.addMapEventListeners();

    /**
     * An array of feature layers
     *
     * @private
     * @type Array
     */
    this.layers = new Array();
    this.createLayer("base");

    /**
     * An array of controls
     *
     * @private
     * @type Array
     */
    this.controls = new Array();
    this.initControls();

    /**
     * Features added to this map which can be 'snapped' to
     *
     * @private
     * @type Array
     */
    this.snappables = new Array();

    // add geosmart logo
    if(this.useLogo) {
        this.addLogo();
    }

    /**
     * This map's info window. One info window only is created
     * for each map instance
     * @private
     * @type GSInfoWindow
     */
    this.infoWindow = undefined;
    if(this.useInfoWindow) {
        this.infoWindow = new GSInfoWindow2(this, this.options.infoWindowOptions);
    }

    if(this.useMouseWheelZooming) {
        this.enableMouseWheelZooming();
    }

    if(GSUtil.getCaseInsensitiveProperty(this.options, "centerX") &&
       GSUtil.getCaseInsensitiveProperty(this.options, "centerY") &&
       GSUtil.getCaseInsensitiveProperty(this.options, "zoomLevel") != undefined) {
        this.update(function() {
                // let listeners know the map has been zoomed
                this.broadcastMessage("mapZoomed", this, 0, this.scaleIdx);
            });
    }

    /**
     * API logger
     * @private
     */
    this.logger = new GSLogger(this);

    /**
     * <code>true</code> if a pan operation is occurring on this map instance
     * @private
     * @type boolean
     */
    this.panning = false;
};

/**
 * Constant for specifying that an API function should not message any subscribed map listeners.
 * WARNING - only use this constant if you're sure of what you're doing
 * @private
 * @final
 * @type String
 */
GSMap.SUPPRESS_MESSAGING = 1;

/**
 * Constant for specifying a regular zoom control to be placed on the map
 *
 * @type String
 */
GSMap.ZOOM_CONTROL = "zoom";

/**
 * Constant for specifying a compact zoom control to be placed on the map
 *
 * @final
 * @type String
 */
GSMap.COMPACT_ZOOM_CONTROL = "compact-zoom";

/**
 * Constant for specifying a text zoom control
 *
 * @final
 * @type String
 */
GSMap.TEXT_ZOOM_CONTROL = "text-zoom";

/**
 * Constant for specifying a pan control to be placed on the map
 *
 * @final
 * @type String
 */
GSMap.MAP_CONTROL = "map";

/**
 * Constant denoting the map snapback control
 *
 * @final
 * @type String
 */
GSMap.SNAPBACK_CONTROL = 'snapback';

/**
 * Constant denoting the map scalebar control
 *
 * @final
 * @type String
 */
GSMap.SCALEBAR_CONTROL = 'scalebar';

/**
 * Creates and initializes map container hierarchy
 * @private
 */
GSMap.prototype.initDOM = function() {
    // Set additional attributes on container to enable postioning of features
    if(GSUtil.getComputedStyle(this.container, "position") != "absolute") {
        this.container.style.position = "relative";
    }
    this.container.style.overflow = "hidden";

    if(!this.resizeable) {
        this.container.style.width = this.pxWidth + "px";
        this.container.style.height = this.pxHeight + "px";
    } else {
        this.sizeToContainerDimensions();
    }

    // preload cursor images
    if(this.dragToPan && !this.customCursor) {
        new Image().src = _globals.resourceURL + "cursor-grab.cur";
        new Image().src = _globals.resourceURL + "cursor-grabbing.cur";
        //this.container.style.cursor = "url(" + _globals.resourceURL + "cursor-grab.cur),-moz-grab";
    }
    this.setCursor();

    // create containers to hold different types of map elements
    this.createOverlayContainers();
};

/**
 * Adds any map controls specified by the map options
 * @private
 */
GSMap.prototype.initControls = function() {    
    if(this.useScalebar) { // add scalebar if enabled    
        this.addControl(GSMap.SCALEBAR_CONTROL);
    }

    var mapControls = GSUtil.getCaseInsensitiveProperty(this.options, "mapControls");
    if(mapControls && mapControls != "none") {
        this.addControl(mapControls);
    }
};

/**
 * Sets the map mouse cursor
 * @private
 */
GSMap.prototype.setCursor = function(cursor) {    
    if (this.customCursor) {
        this.container.style.cursor = this.customCursor;
    } else {
        if(_browser.isWebkit) { // hand cursor position is off in webkit, therefore don't use for now
            this.container.style.cursor = 'default';
        } else {
            cursor = this.panning ? "url(" + _globals.resourceURL + "cursor-grabbing.cur),-moz-grabbing" : "url(" + _globals.resourceURL + "cursor-grab.cur),-moz-grab";
            this.container.style.cursor = cursor;
        }
    }
};

/**
 * Adds a logo to the map
 * @private
 */
GSMap.prototype.addLogo = function() {
    var geosmartLogo = GSUtil.createImage("geosmartLogo", _globals.resourceURL + "smartfindLogo.png", 120, 19, undefined, undefined, 40, "noprint", "GeoSmart Web Site");
    geosmartLogo.style.position = "absolute";
    geosmartLogo.style.cursor = "pointer";
    GSEventManager.addEventListener(geosmartLogo, "contextmenu", GSUtil.cancelEvent);    
    GSUtil.positionElement(geosmartLogo, undefined, undefined, 7, 7);
    this.container.appendChild(geosmartLogo);
    GSEventManager.addEventListener(geosmartLogo, "click", function(e) {
            window.open("http://www.geosmart.co.nz", "geosmart");
            GSUtil.cancelEvent(e);
        });
};

/**
 * Updates the map's configuration properties.
 * @param {Object} [options] an object literal specifying the map options to set. Supported options are:
 * @config {boolean} centerOnDblClick when <code>true</code> enables map recentering when double-clicking on the map.
 * @config {boolean} dragToPan when <code>true</code> enables map panning by moving the mouse while holding down the
 * left mouse button. 
 * @config {boolean} useMouseWheelZooming if <code>true</code> mouse wheel zooming is enabled for the map.
 * @config {String} mouseWheelModifierKey specifies a modifier to use to actuate mouse wheel zooming.
 * Any of the following modifiers may be specified: <code>alt, ctrl, meta, shift</code></li>
 * @config {boolean} useInfoWindow if <code>true</code> the info window is enabled for the map.
 * @config {boolean} useScalebar if <code>true</code> a scalebar will be displayed on the map.
 * @config {boolean} resizeable if <code>true</code> the map will be sized to fit its container element.
 * @throws {Error} if a non-supported option is provided
 */
GSMap.prototype.setMapOptions = function(options) {
    for(var i in options) {
        switch(i) {
            case 'centerOnDblClick':
            this.centerOnDblClick = options[i];
            break;
            case 'dragToPan':
            this.dragToPan = options[i];
            break;
            case 'useMouseWheelZooming':
            this.useMouseWheelZooming = options[i];
            if(this.useMouseWheelZooming) {
                this.enableMouseWheelZooming();
            } else {
                this.disableMouseWheelZooming();
            }
            break;
        case 'mouseWheelModifierKey':
            this.mouseWheelModifierKey = options[i];
            break;
            case 'useInfoWindow':
            this.useInfoWindow = options[i];
            break;
            case 'useScalebar':
            this.useScalebar = options[i];
            if(this.useScalebar) {
                this.addControl(GSMap.SCALEBAR_CONTROL);
            } else {
                this.removeControl(GSMap.SCALEBAR_CONTROL);
            }
            break;
            case 'resizeable':
            this.resizeable = options[i];
            if(this.resizeable) {
                this.enableResizing();
            } else {
                this.disableResizing();
            }
            break;
        default:
            throw new Error('Property "' + i + '" not supported by GSMap');
            break;
        }
    }
};

/**
 * Enables mouse wheel zooming
 * @private
 */
GSMap.prototype.enableMouseWheelZooming = function() {
    var handleMouseWheel = function(delta) {
        if(delta < 0) { // zoom out
            if(this.scaleIdx+1 <= (this.scaleRange.length-1)/*max zoom*/) {
                this.zoom(this.scaleIdx+1);
            }
        } else { // zoom in
            if(this.scaleIdx-1 >= 0/*min zoom*/) {
                this.zoom(this.scaleIdx-1);
            }
        }
    };
    this.eventListeners["mousewheel_zooming"] = GSMouseWheel.bind(this.container, this, handleMouseWheel, this.mouseWheelModifierKey);
};

/**
 * Disables mouse wheel zooming
 * @private
 */
GSMap.prototype.disableMouseWheelZooming = function() {
    GSEventManager.release(this.eventListeners["mousewheel_zooming"]);
};

/**
 * Enable map resizing based upon container dimensions
 * @private
 */
GSMap.prototype.enableResizing = function() {
    this.eventListeners["resize"] = GSEventManager.bind(window, "resize", this, this.updateSize);
    this.resizeable = true;
};

/**
 * Enable map resizing based upon container dimensions
 * @private
 */
GSMap.prototype.disableResizing = function() {
    if(this.eventListeners["resize"]) {
        GSEventManager.release(this.eventListeners["resize"]);
    }
    this.resizeable = false;
};

/**
 * Sizes the map window to its container's dimensions
 * @private
 */
GSMap.prototype.sizeToContainerDimensions = function() {
    this.pxWidth = this.container.offsetWidth;
    this.pxHeight = this.container.offsetHeight;
};

/**
 * Sets the context menu for this map
 * @public
 * @param {GSContextMenu} contextMenu the context menu to display in response
 * to right-click events on the map
 */
GSMap.prototype.setContextMenu = function(contextMenu) {
    this.contextMenu = contextMenu;
    GSContextMenuManager.setContextMenu('map', this.contextMenu);
    this.contextMenu.addToMap(this);
    this.enableContextMenu();
};

/**
 * Removes the map's context menu if it has been set
 * @return the context menu that was removed, or <code>undefined</code> if
 * no context menu was set
 * @public
 */
GSMap.prototype.removeContextMenu = function() {
    var contextMenu = this.contextMenu;
    if(contextMenu) {
        this.disableContextMenu();
        contextMenu.remove();
        this.contextMenu = undefined;
    }
    return contextMenu;
};

/**
 * Enables the map's context menu if it has been set
 * @public
 * @return a context menu for this feature
 * @type GSContextMenu
 */
GSMap.prototype.enableContextMenu = function() {
    if(this.contextMenu) {
        GSEventManager.removeEventListener(this.eventListeners["contextmenu"]);
        this.eventListeners["contextmenu"] = 
            GSEventManager.addEventListener(this.container, "contextmenu", GSUtil.bindAsEventListener(this.displayContextMenu, this));
    }
    return this.contextMenu;
};

/**
 * Disables the map's context menu
 * @public
 */
GSMap.prototype.disableContextMenu = function() {
    GSEventManager.removeEventListener(this.eventListeners["contextmenu"]);
    this.eventListeners["contextmenu"] = GSEventManager.addEventListener(this.container, "contextmenu", GSUtil.cancelEvent);
};

/**
 * Displays the map's context menu in response to a right-click event
 * @param {Event} e the mouse event
 * @private
 */
GSMap.prototype.displayContextMenu = function(e) {
    if(GSContextMenuManager.activeMenu && GSContextMenuManager.activeMenu.showing) {
        GSContextMenuManager.activeMenu.hide();
    }
    var pos = GSUtil.getMousePixelCoordinate(e, this.container);
    this.contextMenu.show(pos, this);
    GSContextMenuManager.contextMenuOpened(this.contextMenu);
    GSUtil.cancelEvent(e);
};

/**
 * Destroys any resources created by this map instance
 */
GSMap.prototype.remove = function(){
    this.clearLayers();

    for(var i = 0, l = this.controls.length; i < l; i++) {
        this.removeControl(this.controls[i]);
    }

    if(this.infoWindow) {
        this.infoWindow.remove();
    }

    while(this.container.hasChildNodes()) {
        this.container.removeChild(this.container.firstChild);
    }
    
    // flush the event cache if child map
    if(!this.isChildMap) {
        GSEventCache.flush();
    }
};

/**
 * Recalculates the dimensions of the map from its container. This method is called automatically
 * in reponse to window resize events when the <code>resizeable</code> map option is set to
 * <code>true</code>. If the container DOM element is resized independently of a window resize event
 * this method should be called explicitly to allow the map to resize to the dimensions.
 */
GSMap.prototype.updateSize = function() {
    var oldSize = new GSDimension(this.pxWidth, this.pxHeight);
    
    this.sizeToContainerDimensions();
    this.recalculateMapExtents();

    // let listeners know the map has been resized. We do this before the new
    // background image is fetched as the image is scaled by the browser immediately
    // and the features need to be repositioned at this time
    var updateFunc = function() {
        this.broadcastMessage("mapResized", this, oldSize, new GSDimension(this.pxWidth, this.pxHeight));
    };
    this.update(updateFunc);
};

/**
 * Returns the center point of the map as an NZMG coordinate
 * @return {GSPoint} the map center point as an NZMG coordinate
 */
GSMap.prototype.getMapCenter = function() {
    return new GSPoint(this.centerX, this.centerY);
};

/**
 * Returns the current map zoom level
 * @return {int} the current map zoom level
 */
GSMap.prototype.getZoomLevel = function() {
    return this.scaleIdx;
};

/**
 * Returns the size, in pixels, of this map
 * @return {GSDimension} the size of the map
 * @see #setSize
 */
GSMap.prototype.getSize = function() {
    return new GSDimension(this.pxWidth, this.pxHeight);
};

/**
 * Sets the size in pixels of the map to be created. Note this method doesn't
 * update the map to match the new size, call one of the map positioning methods
 * to do this.
 *
 * @param {GSDimension} size the size of the map to be created in pixels
 * @see #getSize
 * @throws {Error} if the map is not configured as resizeable
 */
GSMap.prototype.setSize = function(size) {
    if(this.resizeable) {
        this.pxWidth = size.width;
        this.pxHeight = size.height;
    } else {
        throw new Error('Map is not configured as resizeable. Use GSMap.setMapOptions() to set map as resizeable before calling GSMap.setSize()');
    }
};

/**
 * Sets the map extents to those specified by the bounds object
 *
 * @param {GSBounds} bounds the new map bounds
 * @param {Function} [onBoundsChanged] function that will be invoked when the map bounds have been set
 * @see #getBounds
 */
GSMap.prototype.setBounds = function(bounds, onBoundsChanged) {
    var oldMpx = this.mpx;
    var oldBounds = new GSBounds(this.minX, this.minY, this.maxX, this.maxY);

    var minX = bounds.minX;
    var minY = bounds.minY;
    var maxX = bounds.maxX;
    var maxY = bounds.maxY;

    var width = maxX - minX;
    var height = maxY - minY;

    var deltaX = width/2;
    var deltaY = height/2;

    this.centerX = minX + deltaX;
    this.centerY = minY + deltaY;

    this.mpx = Math.ceil(Math.max(width/this.pxWidth, height/this.pxHeight));

    // fit it to one of the predefined map scale values
    this.mpx = this.fitToScaleRange(this.mpx);
    this.scaleIdx = this.scaleRange.indexOf(this.mpx);

    var metresWidth = (this.pxWidth * this.mpx);
    var metresHeight = (this.pxHeight * this.mpx);

    this.minX = this.centerX - (metresWidth/2);
    this.minY = this.centerY - (metresHeight/2);
    this.maxX = this.centerX + (metresWidth/2);
    this.maxY = this.centerY + (metresHeight/2);

    this.centerX = this.minX + ((this.maxX - this.minX) / 2);
    this.centerY = this.minY + ((this.maxY - this.minY) / 2);

    var newBounds = new GSBounds(this.minX, this.minY, this.maxX, this.maxY);

    var newMpx = this.mpx;
    this.update(function() {
        // let listeners know the map has been zoomed
        this.broadcastMessage("mapBoundsChanged", this, oldBounds, newBounds);
        
        if(onBoundsChanged) onBoundsChanged();
    });
};

/**
 * Creates the containers that hold content overlaid over the background map image
 * @private
 */
GSMap.prototype.createOverlayContainers = function() {
    this.tileContainer = GSUtil.createContainer(this.container, null, null, null, 0, 0, null, 5);
    this.tileContainer.id = "tileContainer";
    this.contentContainer = GSUtil.createContainer(this.container, "contentContainer", null, null, 0, 0, null, 10);
    this.contentContainer.style.width = '21600px';
    this.contentContainer.style.height = '21600px';
    this.imageContainer = GSUtil.createContainer(this.contentContainer, "imageContainer", null, null, 0, 0, null, 5);
    this.layerContainer = GSUtil.createContainer(this.contentContainer, "layerContainer", null, null, 0, 0, null, 10);
};

/**
 * @private
 */
GSMap.prototype.dragStart = function(e) {
    if(this.panning) { // if panning programatically don't do this
        return;
    }
    if(this.dragToPan) {
        this.panning = true;

        this.setCursor();

        e = e ? e : window.event;
     
        var rightclick = false;
        if (e.which) {
            rightclick = (e.which == 3);
        } else if (e.button) {
            rightclick = (e.button == 2);
        }
         
        // Do not allow drag with right click.  It interferes with the contenxt menu on Webkit. Also we don't want
        // to cancel the click event for Firefox because it stops the contextmenu even firing.
        if (!rightclick) {
        
            this.startPixel = new GSPoint(e.clientX, e.clientY);
            this.startTilePixel = new GSPoint(parseInt(this.tileContainer.style.left), parseInt(this.tileContainer.style.top));
            this.startNzmg = new GSPoint(this.centerX, this.centerY);
        
            this.eventListeners["drag_mousemove"] = GSEventManager.bind(document, "mousemove", this, this.dragMove);
            this.eventListeners["drag_mouseup"] = GSEventManager.bind(document, "mouseup", this, this.dragStop);
             
            GSUtil.cancelEvent(e);
        
        }
        
    }
};

/**
 * Event handler function called repeatedly as the map is dragged by the user
 * @private
 * @param {Event} e the mousemove event
 */
GSMap.prototype.dragMove = function(e) {
    e = e ? e : window.event;
    if(!this.hasDragged(e)) {
        return;
    }

    GSUtil.positionElement(this.contentContainer, (e.clientX - this.startPixel.x), (e.clientY - this.startPixel.y));
    GSUtil.positionElement(this.tileContainer, (this.startTilePixel.x + (e.clientX - this.startPixel.x)), (this.startTilePixel.y + (e.clientY - this.startPixel.y)));
    GSUtil.cancelEvent(e);
};

/**
 * Event handler function called when the user releases the mouse after dragging
 * @private
 * @param {Event} e the mouseup event
 */
GSMap.prototype.dragStop = function(e) {
    e = e ? e : window.event;
    
    if(this.hasDragged(e)) {
        var deltaX = (e.clientX - this.startPixel.x) * this.mpx;
        var deltaY = (e.clientY - this.startPixel.y) * this.mpx;
        if(deltaX != 0 || deltaY != 0) {
            var oldBounds = this.getBounds();
            var onCenter = GSUtil.bind(function() {
                this.broadcastMessage("mapBoundsChanged", this, oldBounds, this.getBounds());                
                GSUtil.positionElement(this.contentContainer, 0, 0);
                // log
                this.logger.log('map', {map_op: 'drag', tile_count: this.tileCount});
            }, this);
            this.centerAtCoordinate({x: this.startNzmg.x - (e.clientX - this.startPixel.x) * this.mpx, y: this.startNzmg.y + (e.clientY - this.startPixel.y) * this.mpx}, onCenter, GSMap.SUPPRESS_MESSAGING);            
        }
    }
    
    GSEventManager.release(this.eventListeners["drag_mousemove"]);
    GSEventManager.release(this.eventListeners["drag_mouseup"]);

    this.startPixel = undefined;
    this.startTilePixel = undefined;
    this.startNzmg = undefined;
    this.dragMoveToken = undefined;
    this.dragStopToken = undefined;

    this.panning = false;

    this.setCursor();

    GSUtil.cancelEvent(e);
};

/**
 * Event handler function called when the user clicks on the map
 * @private
 * @param {Event} e the click event
 */
GSMap.prototype.click = function(e) {
    if(!this.panning) {
        e = e ? e : window.event;
        var coordinate = this.getMouseCoordinate(e);
        this.broadcastMessage("mapClicked", this, coordinate, e);
    } else {
        this.panning = false;
    }
    if(GSContextMenuManager.activeMenu && GSContextMenuManager.activeMenu.showing) {
        GSContextMenuManager.activeMenu.hide();
    }
    GSUtil.cancelEvent(e);
};

/**
 * Event handler function called when the user double clicks on the map
 * @private
 * @param {Event} e the click event
 */
GSMap.prototype.dblclick = function(e) {
    var coordinate = this.getMouseCoordinate(e);
    if(this.centerOnDblClick) {
        this.panTo(coordinate);
    }
    this.broadcastMessage("mapDblClicked", this, coordinate, e);
    GSUtil.cancelEvent(e);
};

/**
 * Adds event listeners to the map image to capture user events
 * @private
 */
GSMap.prototype.addMapEventListeners = function() {
    this.eventListeners["mousedown"] = GSEventManager.bind(this.container, "mousedown", this, this.dragStart);
    this.eventListeners["click"] = GSEventManager.bind(this.container, "click", this, this.click);
    this.eventListeners["dblclick"] = GSEventManager.bind(this.container, "dblclick", this, this.dblclick);
    this.eventListeners["contextmenu"] = GSEventManager.addEventListener(this.container, "contextmenu", GSUtil.cancelEvent);    


    // if resizeable, bind map resize function to window resize event
    if(this.resizeable) {
        this.enableResizing();
    }
};

/**
 * Translates the position of a mouse event to a real world coordinate (NZMG)
 * @private
 *
 * @param {Event} e the mouse event
 * @return {GSPoint} the mouse position translated as a real world coordinate
 */
GSMap.prototype.getMouseCoordinate = function(e) {
    return this.translateToRealWorldCoordinate(GSUtil.getMousePixelCoordinate(e, this.container));
};

/**
 * Returns <code>true</code> if the mouse has been moved greater than 2 pixels from its
 * mousedown position
 * @private
 * @param {Event} e the current mouse event
 */
GSMap.prototype.hasDragged = function(e) {
    if(!this.startPixel) {
        return false;
    }
    var tolerance = 2;
    var coord = new GSPoint(e.clientX, e.clientY);
    
    var dragged = (Math.abs(this.startPixel.x - coord.x) > tolerance) || (Math.abs(this.startPixel.y - coord.y) > tolerance);
    return dragged;
};

/**
 * Zooms to the specified zoom level
 * @param {int} zoomLevel an integer value from 0 (maximum zoom in) to 11 (maximum zoom out)
 * @param {Function} [onZoom] function that will be invoked when the map has been zoomed to the new zoom level
 */
GSMap.prototype.zoom = function(zoomLevel, onZoom) {
    var oldZoomLevel = this.scaleIdx;

    zoomLevel = parseInt(zoomLevel);

    if(oldZoomLevel == zoomLevel) { // nothing to do
        return; 
    }

    if(zoomLevel < 0) {zoomLevel = 0;}
    if(zoomLevel >= this.scaleRange.length) {zoomLevel = this.scaleRange.length-1;}
             
    this.mpx = this.scaleRange[zoomLevel];
    this.scaleIdx = zoomLevel;

    this.recalculateMapExtents(true);
    this.update(function() {
        // let listeners know the map has been zoomed
        this.broadcastMessage("mapZoomed", this, oldZoomLevel, this.scaleIdx);
        
        // call user-provided function if exists
        if(onZoom) onZoom();
        
        // log
        this.logger.log('map', {map_op: 'zoom', tile_count: this.tileCount});
    });
};

/**
 * Pans the map to the specified coordinate. If the coordinate is within the visible part of 
 * the map panning is animated, otherwise the map is directly centered on the coordinate.
 * @param {GSPoint} coordinate the real world coordinate (NZMG) to pan the map to
 * @param {Function} [onCenter] function that will be invoked when the map has been panned to the specified coordinate
 */
GSMap.prototype.panTo = function(coordinate, onPan) {    
    if(this.panning) {
        return;
    }
    var bounds = this.getBounds();
    if(!bounds.contains(coordinate)) {
        return this.centerAtCoordinate(coordinate, onPan);
    }
    this.panning = true;

    var startNzmg = this.getMapCenter();
    var from = this.translateToMapCoordinate(new GSPoint(this.centerX, this.centerY));
    var to = this.translateToMapCoordinate(coordinate);
    var startTilePixel = new GSPoint(parseInt(this.tileContainer.style.left), parseInt(this.tileContainer.style.top));    
    var onFrame = GSUtil.bind(this.onPanFrame, this, startTilePixel, from, to);
    var onComplete = GSUtil.bind(this.onPanComplete, this, onPan);
    var animator = new GSAnimator(from, to, 16, 850, onFrame, null, onComplete);
    animator.animate();
};

/**
 * Called repeatedly during panning
 * @private
 */
GSMap.prototype.onPanFrame = function(startTilePixel, from, to, animator, point) {
    point.x = Math.floor(point.x);
    point.y = Math.floor(point.y);

    var left = startTilePixel.x + (from.x - point.x);
    var top = startTilePixel.y + (from.y - point.y);

    GSUtil.positionElement(this.contentContainer, (from.x - point.x), (from.y - point.y));
    GSUtil.positionElement(this.tileContainer, left, top);
};

/**
 * Called on completion of panning animation
 * @private
 */
GSMap.prototype.onPanComplete = function(onPan, animator) {
    var onCenter = function() {
        GSUtil.positionElement(this.contentContainer, 0, 0);
        this.panning = false;
        if(onPan) {
            onPan();
        }
        this.logger.log('map', {map_op: 'panTo', tile_count: this.tileCount});    
    };
    this.centerAtCoordinate(this.translateToRealWorldCoordinate(animator.to), GSUtil.bind(onCenter, this));
};

/**
 * Centers the map on the specified coordinate
 *
 * @param {GSPoint} coordinate the real world coordinate (NZMG) to center the map on
 * @param {Function} [onCenter] function that will be invoked when the map has been recentered
 * @param {boolean} [suppressMessaging] set to <code>true</code> to prevent this function from notifying
 * listeners upon completion has been centered on the specified coordinate
 */
GSMap.prototype.centerAtCoordinate = function(coordinate, onCenter, suppressMessaging) {
    var oldBounds = new GSBounds(this.minX, this.minY, this.maxX, this.maxY);

    this.centerX = coordinate.x;
    this.centerY = coordinate.y;

    this.recalculateMapExtents();

    var newBounds = new GSBounds(this.minX, this.minY, this.maxX, this.maxY);

    this.update(function() {
            // let listeners know the map has been zoomed
            if(!suppressMessaging) {
                this.broadcastMessage("mapBoundsChanged", this, oldBounds, newBounds);
                
                // log
                this.logger.log('map', {map_op: 'centerAtCoordinate', tile_count: this.tileCount});
            }
            // call user-provided function if exists
            if(onCenter) {
                onCenter();
            }
        });
};

/**
 * Centers the map on the provided coordinate at the given zoom level
 *
 * @param {GSPoint} coordinate the real world coordinate (NZMG) to center the map on
 * @param {int} zoomLevel an integer value from 0 (maximum zoom in) to 11 (maximum zoom out)
 * @param {Function} [onCenter] function that will be invoked when the map has been centered on the specified coordinate
 */
GSMap.prototype.centerAndZoom = function(coordinate, zoomLevel, onCenter) {
    this.broadcastMessage("mapZoom", this);
    var oldZoomLevel = this.scaleIdx;

    this.centerX = coordinate.x;
    this.centerY = coordinate.y;

    if(zoomLevel != undefined) {
        zoomLevel = parseInt(zoomLevel);
        if (zoomLevel < 0 || zoomLevel >= this.scaleRange.length) {
            alert("Zoom level out of range: " + zoomLevel);
            return;
        }   

        this.mpx = this.scaleRange[zoomLevel];
        this.scaleIdx = zoomLevel;
    }

    this.recalculateMapExtents();

    this.update(function() {
        // let listeners know the map has been zoomed
        this.broadcastMessage("mapZoomed", this, oldZoomLevel, this.scaleIdx);
        
        // call user-provided function if exists
        if(onCenter) onCenter();
        
        // log
        this.logger.log('map', {map_op: 'centerAndZoom', tile_count: this.tileCount});
    });
};

/**
 * Centers the map on the center of New Zealand, Nelson, and at such a scale that the full
 * extent of the country is visible
 *
 * @param {Function} [onCenter] function that will be invoked when the map has been centered
 */
GSMap.prototype.centerOnNewZealand = function(onCenter) {
    var oldZoomLevel = this.scaleIdx;

    this.centerX = _globals.nzCenterX;
    this.centerY = _globals.nzCenterY;
    
    for(var i = 0, l = this.scaleRange.length; i < l; i++) {
        if (_globals.nzCenterY + ((this.scaleRange[i] * this.pxHeight) / 2) > _globals.nzTopY) {
            this.mpx = this.scaleRange[i];
            this.scaleIdx = i;
            break;
        }
    }

    this.recalculateMapExtents();

    this.update(function() {
            // let listeners know the map has been zoomed
            this.broadcastMessage("mapZoomed", this, oldZoomLevel, this.scaleIdx);
            
            // call user-provided function if exists
            if(onCenter) onCenter();

            // log
            this.logger.log('map', {map_op: 'centerOnNewZealand', tile_count: this.tileCount});
        });
};

/**
 * Fits the specified scale to the map's internal scale range. This
 * enables consistency of zooming operations etc. If the specified
 * scale exactly matches a scale on the map's range it will be returned
 * unchanged otherwise the nearest scale that is larger than the specified
 * scale will be returned
 *
 * @private
 * @param {int} scale the scale (metres to pixels) to be fitted
 * @return {int} the fitted scale
 */
GSMap.prototype.fitToScaleRange = function(scale) {
    var newScale = this.scaleRange[0];
    for(var i = 0, l = this.scaleRange.length; i < l; i++) {
        if((typeof this.scaleRange[i]).toString().toLowerCase() != "function") {
            var mapScale = this.scaleRange[i];
            if(mapScale == scale) {
                return scale;
            } else if(mapScale > scale) {
                newScale = mapScale;
                break;
            }
        }
    }
    return newScale;
};

/**
 * Adds a control to the map
 *
 * @param {Object} control the control object, or a String constant identifying the type of
 * control to add to the map.
 * @param {Object} [position] specifies the position of this control on the map.
 * The control's position is specified as an object literal with 2 properties <code>anchor</code> and
 * <code>offset</code>
 * <pre>
 * map.addControl(GSMap.MAP_CONTROL,
 *   {anchor: GSControl.ANCHOR_TOP_LEFT, offset: new GSPoint(7, 7)});
 * </pre>
 * @see #ZOOM_CONTROL
 * @see #COMPACT_ZOOM_CONTROL
 * @see #TEXT_ZOOM_CONTROL
 * @see #MAP_CONTROL
 * @return {GSControl} the map control that was added
 */
GSMap.prototype.addControl = function(control, position) {
    var obj = null;
    var haveControl = false;
    if(GSControl.prototype.isPrototypeOf) {
        haveControl = GSControl.prototype.isPrototypeOf(control);
    } else if(control.__proto__) {
        haveControl = (typeof control).toLowerCase() == "object";
    }

    if(haveControl) {
        obj = control;
    } else {
        obj = this.instantiateControl(control);
    }

    if(obj != null) {
        this.controls.push(obj);
        var controlContainer = obj.render(this);
        this.container.appendChild(controlContainer);
        obj.setElement(controlContainer);
        obj.setPosition(position ? position : obj.getDefaultPosition());
    }
    return obj;
};

/**
 * Removes a control from the map
 *
 * @param {GSControl} control the control object
 */
GSMap.prototype.removeControl = function(control) {
    var name = "";

    var haveControl = false;
    if(GSControl.prototype.isPrototypeOf) {
        haveControl = GSControl.prototype.isPrototypeOf(control);
    } else if(control.__proto__) {
        haveControl = (typeof control).toLowerCase() == "object";
    }

    if(haveControl) {
        name = control.name;
    } else {
        name = control;
    }

    for(var i = 0, l = this.controls.length; i < l; i++) {
        if(this.controls[i].name == name) {
            this.controls[i].remove(this);
            this.controls.splice(i, 1);
            return true;
        }
    }
    return false;
};

/**
 * Returns the named map control, or <code>null</code> if no control
 * has been added to the map with the given name
 * @param {String} name the name of the map control to return
 * @return {GSControl} the map control
 */
GSMap.prototype.getControl = function(name) {
    for(var i = 0, l = this.controls.length; i < l; i++) {
        if(this.controls[i].name == name) {
            return this.controls[i];
        }
    }
    return null;
};

/**
 * Hides all map controls
 */
GSMap.prototype.hideControls = function() {
    for(var i = 0, l = this.controls.length; i < l; i++) {
        this.controls[i].hide();
    }
};

/**
 * Shows all map controls
 */
GSMap.prototype.showControls = function() {
    for(var i = 0, l = this.controls.length; i < l; i++) {
        this.controls[i].show();
    }
};

/**
 * Instantiates the named control. Returns <code>undefined</code> if
 * <code>name</code> does not refer to a known control
 *
 * @private
 * @param {int} name
 */
GSMap.prototype.instantiateControl = function(name) {
    var control;
    switch(name) {
    case GSMap.ZOOM_CONTROL:
        control = new GSZoomControl();
        break;
    case GSMap.COMPACT_ZOOM_CONTROL:
        control = new GSCompactZoomControl();
        break;
    case GSMap.MAP_CONTROL:
        control = new GSMapControl();
        break;
    case GSMap.TEXT_ZOOM_CONTROL:
        control = new GSTextZoomControl();
        break;
    case GSMap.SNAPBACK_CONTROL:
        control = new GSSnapbackControl();
        break;
    case GSMap.SCALEBAR_CONTROL:
        control = new GSScalebarControl();
        break;
    }
    return control;
};

/**
 * Re-centers the map on the named layer. Specifically the minimum bounding box required
 * to display all features in the layer is calculated and the map is resized to match
 * the extents of this bounding box.
 * <p>
 * In many cases the features belonging to the layer that the map is being centered on 
 * should not be visible until after the map has been re-centered.
 * To do this call {@link GSLayer#setVisible} with an argument of <code>false</code>
 * before adding the feature data to it. <code>centerOnLayer()</code> will automatically set
 * the layer features to be visible after the map has been recentered.
 * </p>
 *
 * @param {String} name the name of the layer to center the map on
 * @param {Function} [onCenter] function that will be invoked when the map has been re-centered
 * @throws {Error} if the named layer does not exist
 */
GSMap.prototype.centerOnLayer = function(name, onCenter) {
    var layer = this.getLayer(name);
    if(layer == undefined) {
        throw new Error('Layer [' + name + '] does not exist, map will not be re-centered');
    }
    
    if(!layer.isVisible()) {
        layer.mapBoundsChanged = function(map, oldBounds, newBounds) {
            this.setVisible(true);
            map.removeListener(this);
        };
        this.addListener(layer);
    }

    var bounds = layer.getBounds();

    // pad the bounds 5% to allow space for icons
    var width = bounds.maxX - bounds.minX;
    var height = bounds.maxY - bounds.minY;
    var padW = width * 0.05;
    var padH = height * 0.05;

    bounds.minX -= padW;
    bounds.maxX += padW;
    bounds.minY -= padH;
    bounds.maxY += padH;

    this.setBounds(bounds, onCenter);
};

/**
 * Creates a new layer with the specified name
 *
 * @param {String} name the layer name
 * @param {Object} [options] an object literal specifying optional parameters for layer initialization.
 * @config {int} zIndex the zIndex within the map content hierarchy the layer should be displayed at
 * @config {String} type if set to 'route' will create an instance of <code>GSRouteLayer</code>, a specialized
 * layer type that provides additional features related to the display of vehicle routes
 * @config {boolean} waypointsVisible <code>true</code> if route points should be displayed upon the map (specific to GSRouteLayer)
 * @config {String} directionsServiceUrl the directions service URL. Typically this will refer to a web proxy to avoid disclosing authentication credentials (specific to GSRouteLayer)
 * @config {boolean} returnToStart <code>true</code> if the route calculated should use the start point as the end point (specific to GSRouteLayer)
 * @return {GSLayer} the layer that was created
 */
GSMap.prototype.createLayer = function(name, options) {
    options = options ? options : {};

    var layer = this.getLayer(name);
    if(layer != undefined) {
        this.removeLayer(name);
    }

    if(options.type == 'route') {
        layer = new GSRouteLayer(name, this, options);
    } else {
        layer = new GSLayer(name, this, options);
    }
    this.layers.push(layer);
    return layer;
};

/**
 * Removes a feature layer from the map
 *
 * @param {String} name the layer name
 * @return {boolean} <code>true</code> if the layer was removed
 */
GSMap.prototype.removeLayer = function(name) {
    if(name == "base") {
        return;
    }
    for(var i = 0, l = this.layers.length; i < l; i++) {
        var layer = this.layers[i];
        if(layer.name == name) {
            layer.remove();
            this.layers.splice(i, 1);
            return true;
        }
    }
    return false;
};

/**
 * Removes all user-defined layers from the map
 */
GSMap.prototype.clearLayers = function() {
    for(var i = 0, l = this.layers.length; i < l; i++) {
        this.layers[i].clear();
    }
};

/**
 * Returns the feature layers for this map instance
 * @return {Array} an array of <code>GSLayer</code> objects
 */
GSMap.prototype.getLayers = function() {
    return this.layers;
};

/**
 * Returns the named map layer
 *
 * @param {String} name of the layer to return
 * @return {GSLayer} the named layer
 */
GSMap.prototype.getLayer = function(name) {
    for(var i = 0, l = this.layers.length; i < l; i++) {
        var layer = this.layers[i];
        if(layer.name == name) {
            return layer;
        }
    }
    return null;
};

/**
 * A convenience function for adding a feature to the map
 * without explicitly creating a layer first. Features added in this
 * way are owned by the map's 'base' layer.
 * @param {MapFeature} feature a feature to be added to the map
 * @see #removeFeature
 */
GSMap.prototype.addFeature = function(feature) {
    var baseLayer = this.getLayer("base");
    baseLayer.addFeature(feature);
};

/**
 * A convenience function for removing a feature that was added
 * directly to the map
 * @param {GSMapFeature} feature the feature to remove
 * @see #addFeature
 */
GSMap.prototype.removeFeature = function(feature) {
    var baseLayer = this.getLayer("base");
    baseLayer.removeFeature(feature);
};

/**
 * Recalculates the map window extents based upon the current map center
 * and map scale.
 *
 * @private
 *
 * @param {boolean} <code>true</code> if the map window extents are being
 * recalculated for a zoom event
 */
GSMap.prototype.recalculateMapExtents = function(zooming) {
    var metresWidth = this.pxWidth * this.mpx;
    var metresHeight = this.pxHeight * this.mpx;

    if(zooming && this.infoWindow && this.infoWindow.canAnchorOnInfoWindow()) {
        var coordinate = this.infoWindow.coordinate;
        var deltaX = coordinate.x - this.minX;
        var deltaY = coordinate.y - this.minY;
        var oldMetresWidth = this.maxX - this.minX;
        var oldMetresHeight = this.maxY - this.minY;
        var percentWidth = deltaX / oldMetresWidth;
        var percentHeight = deltaY / oldMetresHeight;

        this.minX = coordinate.x - (percentWidth * metresWidth);
        this.maxX = coordinate.x + ((1 - percentWidth) * metresWidth);
        this.minY = coordinate.y - (percentHeight * metresHeight);
        this.maxY = coordinate.y + ((1 - percentHeight) * metresHeight);

        this.centerX = this.minX + (metresWidth/2);
        this.centerY = this.minY + (metresHeight/2);
    } else {
        this.minX = this.centerX - (metresWidth/2);
        this.minY = this.centerY - (metresHeight/2);
        this.maxX = this.centerX + (metresWidth/2);
        this.maxY = this.centerY + (metresHeight/2);
    }
};

/**
 * Translates an NZMG <code>X</code>/<code>y</code> coordinate
 * to it's corresponding pixel values in the screen map's coordinate space.
 *
 * @param {GSPoint} coordinate an NZMG <code>X</code>/<code>Y</code>
 * coordinate
 * @return {GSPoint} the NZMG coordinate translated to a pixel coordinate
 */
GSMap.prototype.translateToMapCoordinate = function(coordinate) {
    var x = coordinate.x;
    var y = coordinate.y;
    var deltaX = (x - this.minX);
    var deltaY = (this.maxY - y);
    return new GSPoint((deltaX/this.mpx), (deltaY/this.mpx));
};

/**
 * Translates a coordinate in the screen map's coordinate space into a real
 * world coordinate (NZMG)
 *
 * @param {GSPoint} coordinate a pixel coordinate in the map's coordinate space
 * @param {GSBounds} [bounds] an optional bounding box that may be used if an alternative
 * coordinate space is required
 * @return {GSPoint} the map coordinate translated to an NZMG coordinate
 */
GSMap.prototype.translateToRealWorldCoordinate = function(coordinate, bounds) {
    if(!bounds) {
        bounds = new GSBounds(this.minX, this.minY, this.maxX, this.maxY);
    }
    var metresWidth = this.pxWidth * this.mpx;
    var metresHeight = this.pxHeight * this.mpx;

    var x = bounds.minX + (metresWidth * (coordinate.x / this.pxWidth));
    var y = bounds.maxY - (metresHeight * (coordinate.y /this.pxHeight));
    return new GSPoint(Math.round(x), Math.round(y));
};

/**
 * Tests if the specified coordinate is within the current map extents
 * @param {GSPoint} coordinate the coordinate to test
 * @return {boolean} <code>true</code> if the coordinate is within the current map extents
 */
GSMap.prototype.contains = function(coordinate) {
    return (coordinate.x > this.minX && coordinate.x < this.maxX && coordinate.y > this.minY && coordinate.y < this.maxY);
};

/**
 * Returns the bounds of the map viewport as NZMG coordinates
 *
 * @return {GSBounds} a <code>GSBounds</code> object representing the bounds
 * of the map viewport in NZMG
 * @see #setBounds
 */
GSMap.prototype.getBounds = function() {
    return new GSBounds(this.minX, this.minY, this.maxX, this.maxY);
};

/**
 * Invoked when a map feature receives a click event
 * @private
 *
 * @param {GSPointFeature} feature the feature that was clicked
 */
GSMap.prototype.featureClicked = function(feature) {
    this.broadcastMessage("mapClicked", this, feature, e);
};

/**
 * Updates the map background image tiles
 *
 * @private
 *
 * @param {Function} postUpdateFunc function to be executed after
 * the new map image has been retrieved
 */
GSMap.prototype.update = function(postUpdateFunc) {
    this.tilesLoaded = this.tileCount = 0;
    var tileWidth = _globals.tilePxWidth * this.mpx;
    var tileHeight = _globals.tilePxHeight * this.mpx;

    var tilesMinX = Math.floor(this.minX / tileWidth) * tileWidth;
    var tilesMaxX = Math.floor(this.maxX / tileWidth) * tileWidth;
    var tilesMinY = Math.floor(this.minY / tileHeight) * tileHeight;
    var tilesMaxY = Math.ceil(this.maxY / tileHeight) * tileHeight;

    while(this.tileContainer.firstChild) {
        this.tileContainer.removeChild(this.tileContainer.firstChild);
    }

    GSUtil.positionElement(this.tileContainer, Math.floor((tilesMinX - this.minX) / this.mpx), Math.ceil((this.maxY - tilesMaxY) / this.mpx));

    var onTileLoad = GSUtil.bind(this.tileLoaded, this);

    var yIdx = xIdx = 0;
    for(var ty = tilesMaxY; ty >= tilesMinY; ty -= tileHeight) {
        xIdx = 0;    
        for(var tx = tilesMinX; tx <= tilesMaxX; tx += tileWidth) {
            this.tileCount++;
            var src = this.buildTileUrl(tx, ty, this.mpx);
            var img = this.createTileImage(src, xIdx * _globals.tilePxWidth, yIdx * _globals.tilePxHeight, onTileLoad);               
            this.tileContainer.appendChild(img);
            xIdx++;
        }
        yIdx++;
    }

    if (postUpdateFunc) {postUpdateFunc.call(this)};
};

/**
 * @private
 */
GSMap.prototype.createTileImage = function(src, left, top, onload) {
    var img = GSUtil.createElement('img');
    img.onload = onload;
    img.onError = onload;
    img.border = 0;
    img.style.position = "absolute";
    img.style.left = left + "px";
    img.style.top = top + "px";
    GSUtil.makeUnselectable(img);
    img.src = src;
    return img;
};

/**
 * Called repeatedly as each tile is loaded. Once all tiles have been
 * loaded the postUpdate callback function will be executed if provided
 * @private
 */
GSMap.prototype.tileLoaded = function() {
    if(++this.tilesLoaded == this.tileCount) {
        this.broadcastMessage("mapTilesLoaded", this);
    }
};

/**
 * Opens the info window with a blowup of the map specified by the coordinate
 * and mpx parameters
 *
 * @param {GSPoint} coordinate the coordinate at which to position the info window
 * @param {Object} [options] an object literal specifying content and display options
 * for the info window. The following parameters may be specified:
 * @config {int} zoomLevel an integer value from 0 (maximum zoom in) to 11 (maximum zoom out).
 * @config {GSPoint} size the size of the blowup map.
 * @config {GSPoint} offset pixel offset from the specified coordinate to position the info window at
 * @config {Function} onopen a function to be called when the info window is opened
 * @config {Function} onclose a function to be called when the info window is closed
 */
GSMap.prototype.openMapBlowup = function(coordinate, options) {
    options = options ? options : {};
    if(options.offset == undefined) {options.offset = new GSPoint(0, 0);}
    if(options.zoomLevel == undefined) {options.zoomLevel = 0;}
    if(options.size == undefined) {options.size = new GSDimension(180, 180);}    

    var backgroundColor = GSUtil.getComputedStyle(this.container, 'background-color');
    if(!backgroundColor) backgroundColor = "#c5c5c5";

    var html = GSUtil.createElement("div");
    html.style.backgroundColor = backgroundColor;
    html.style.border = "1px solid #a0a0a4";

    var mapOptions = {width: options.size.width + 15,
                      height: options.size.height,
                      isChildMap: true,
                      resizeable: false,
                      useScalebar: false,
                      centerOnDblClick: true,
                      dragToPan: true,
                      useInfoWindow: false,
                      useMouseWheelZooming: false
    };
    var map = new GSMap(html, mapOptions);    
    map.addControl(GSMap.COMPACT_ZOOM_CONTROL);
    map.addControl(GSMap.SNAPBACK_CONTROL);

    var onopen = GSUtil.bind(map.onMapBlowupOpen, map, coordinate, options);
    var onclose = GSUtil.bind(map.onMapBlowupClose, map, options);
    this.infoWindow.open(coordinate, html, {offset: options.offset, onopen: onopen, onclose: function() {
                if(options.onclose) {
                    options.onclose(map);
                }
                map.remove();
            }});
};

/**
 * Called after the info window has opened to initialize the blowup map
 * @private
 */
GSMap.prototype.onMapBlowupOpen = function(coordinate, options) {
    var map = this;
    this.centerAndZoom(coordinate, options.zoomLevel, function() {
            map.markForSnapback();
            if(options.onopen) {
                options.onopen(map);
            }
        });
};

/**
 * Called after the info window has closed to cleanup the blowup map
 * @private
 */
GSMap.prototype.onMapBlowupClose = function(options) {
    if(options.onclose) {
        options.onclose(this);
    }
    this.remove();
};

/**
 * Displays the info window at the given coordinate. If the window is already open at some other
 * location it will be closed and thereafter reopened at the new location. The info window content may
 * be provided either as an HTML string, or a DOM node.
 *
 * @param {GSPoint} coordinate the point which the info window refers to
 * @param {Mixed} html the HTML content of the info window
 * @param {Object} [options] an object literal specifying content and display options
 * for the info window. The following parameters may be specified:
 * @config {GSPoint} offset pixel offset from the specified coordinate to position the info window at.
 * @config {Function} onopen a function to be called when the info window is opened
 * @config {Function} onclose a function to be called when the info window is closed
 * @config {String} title the text to display in the info window title
 */
GSMap.prototype.openInfoWindow = function(coordinate, html, options) {
    this.infoWindow.open(coordinate, html, options);
};

/**
 * Close the info window if it is open
 *
 * @param {GSPoint} coordinate the point which the info window refers to
 * @param {String} html the HTML content of the info window
 */
GSMap.prototype.closeInfoWindow = function() {
    this.infoWindow.close();
};

/**
 * Returns this map's info window instance
 * @return {GSInfoWindow} the map's info window or <code>undefined</code> if the info window is not enabled
 * for this map instance
 */
GSMap.prototype.getInfoWindow = function() {
    if(this.infoWindow) {
        return this.infoWindow;
    } else {
        return undefined;
    }
};

/**
 * Adds a reference to a map feature that may be snapped to during a vector
 * move operation
 * @param {Object} feature the snappable to be added
 * @private
 */
GSMap.prototype.addSnappable = function(feature) {
    this.snappables.push(feature);
};

/**
 * Returns an array of snappable map features
 * @return {Array} an array of snappable map features
 * @private
 */
GSMap.prototype.getSnappables = function() {
    return this.snappables;
};

/**
 * Removes the specified snappable from this map's list of snappables
 * @param {Object} snappable the snappable to remove
 * @return {boolean} <code>true</code> if the snappable was succesfully removed
 * @private
 */
GSMap.prototype.removeSnappable = function(snappable) {
    for(var i = this.snappables.length, s = null; s = this.snappables[i]; i--) {
        if(s === snappable) {
            this.snappables.splice(i, 1);
            return true;
        }
    }
    return false;
};

/**
 * Builds the URL to retrieve the image tile for a given coordinate at the specified
 * scale.
 *
 * @private
 * @param {int} x the minium x value (NZMG) of the map tile to retrieve
 * @param {int} y the minium y value (NZMG) of the map tile to retrieve
 * @param {int} mpx the scale to retrieve the map tile at
 * @return {String} the tile URL
 */
GSMap.prototype.buildTileUrl = function(x, y, mpx) {
    var img = x + "_" + y + ".png";
    if(_globals.multipleTileHosts) {
        var suffix = GSCrypt.hex_md5(img).charCodeAt(0) & 3;
        var url = _globals.tilesURL.replace('tiles.', 'tiles' + suffix + '.') + "/" + mpx + "/" + x + "/" + img;
        return url;
    } else {
        return _globals.tilesURL + "/" + mpx + "/" + x + "/" + img;
    }
};

/**
 * Marks the current map position and zoom level for snapping back to later by {@link GSMap#snapback}
 * @param {boolean} persistent whether the snapback position should be remembered across user. Cookie
 * support is required for persisting the map's snapback state. This feature is unavailable in Safari
 * when the enclosing document is served with an XHTML mime-type as the document.cookie property is
 * unsupported for XHTML documents.
 * sessions
 * @throws {Error} if the snapback state is to be persisted and the host browser does not have
 * cookies enabled
 */
GSMap.prototype.markForSnapback = function(persistent) {
    this.snapbackState = {coordinate: this.getMapCenter(), zoomLevel: this.scaleIdx};
    if(persistent) {
        if(GSCookie.accept()) {
            GSCookie.set("gssnapback", GSUtil.serialize(this.snapbackState), 90/*days to expire*/);            
        } else {
            throw new Error('Cookies must be enabled to persist map snapback state');
        }
    }
};

/**
 * Restores the map view saved by {@link GSMap#markForSnapback}
 * @return {boolean} <code>true</code> if the map was successfully snapped-back
 */
GSMap.prototype.snapback = function() {
    if(!this.snapbackState) {
        var val = GSCookie.get("gssnapback");
        if(val) {
            this.snapbackState = eval("(" + val + ")");
        }
    }
    if(this.snapbackState) {
        try {
            this.centerAndZoom(this.snapbackState.coordinate, this.snapbackState.zoomLevel);
            return true;
        } catch (e) {
            return false;
        }
    }
    return false;
};

/**
 * Outputs a JSON representation of this map
 * @return {String} a JSON string
 */
GSMap.prototype.toJson = function() {
    var obj = {map: {
        type: 'GSMap',
        mpx: this.mpx,
        width: this.pxWidth,
        height: this.pxHeight,
        useScalebar: this.useScalebar,
        baseResourceUrl: GSUtil.getDirName(location.href),
        boundingBox: this.getBounds()
        }, layers: this.layers};
    return GSUtil.objToJson(obj);
};

/**
 * Creates a raster image representation of the current map view using a server-side print
 * service and returns the URL for the image
 * @param {String} printServiceUrl the URL for the print service. Typically this will be a proxy
 * that will forward the request to the actual print service
 * @return {Object} a response object containing the URI of the image created e.g.
 * <pre>
 * var response = map.print([proxy-url]);
 * var uri = response.uri;
 * </pre>
 */
GSMap.prototype.print = function(printServiceUrl, options) {
    if(!options) options = {};
    var obj = {map: {
        type: 'GSMap',
        scale: options.scale !== undefined ? options.scale : true,
        mpx: this.mpx,
        width: options.width ? options.width : this.pxWidth,
        height: options.height ? options.height : this.pxHeight,
        useScalebar: options.useScalebar !== undefined ? options.useScalebar : this.useScalebar,
        useLogo: options.useLogo !== undefined ? options.useLogo : this.useLogo,
        baseResourceUrl: GSUtil.getDirName(location.href),
        boundingBox: this.getBounds()
        }, layers: this.layers};
    var data = encodeURIComponent(GSUtil.objToJson(obj));

    var req = new XMLHttpRequest();
    req.onreadystatechange = function() {
        if(req.readyState == 4) {
            if(req.status >= 200 && req.status < 300) {
                if(options.onSuccess) {
                    options.onSuccess(eval("(" + req.responseText + ")"));
                }
            } else {
                if(options.onFailure) {
                    options.onFailure(eval("(" + req.responseText + ")"));
                }
            }
        }
    };
    var imageFormat = options.imageFormat !== undefined ? options.imageFormat : 'image/jpeg';
    if(options.method && options.method == 'get') {
        printServiceUrl += '?d=' + data;
        req.open('get', printServiceUrl, true);
        req.setRequestHeader('Accept', imageFormat);
        req.send();
    } else {
        req.open('post', printServiceUrl, true);
        req.setRequestHeader('Content-Type', 'application/json');
        req.setRequestHeader('Accept', imageFormat);
        req.send(data);
    }
};

// If IE6 fake native XHR object
if(!window.XMLHttpRequest && window.ActiveXObject) {
    window.XMLHttpRequest = function() {
        try {
            req = new ActiveXObject('Msxml2.XMLHTTP');
      	} catch(e) {
            try {
                req = new ActiveXObject('Microsoft.XMLHTTP');
            } catch(e) {
                throw 'Could not instantiate XMLHttpRequest object: ' + e;
            }
        }
        return req;
    }
 }

try {
    document.write('<style type="text/css" media="screen">.noscreen{display: none;}.noprint{display:block}</style>');
    document.write('<style type="text/css" media="print">.noscreen{display: block;}.noprint{display:none}</style>');
 } catch(e) {
    // XML document type, document.write() not supported
 }
// --------------------------------------------------
// GSPoint
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview A coordinate that may be used to specify either a pixel coordinate in the
 * map image's coordinate space or a real world coordinate expressed as an NZMG point
 * @name GSPoint
 */

/**
 * Creates a new <code>GSPoint</code>. The new point may optionally be initialized with
 * its <code>x</code> and <code>y</code> values.
 * @constructor
 */
function GSPoint(x, y) {
    
    /**
     * This point's x coordinate. May either be an integer type if this point represents
     * a pixel coordinate or a floating point type if an NZMG coordinate
     * @type Number
     */
    this.x = Number(x);
    
    /**
     * This point's y coordinate. May either be an integer type if this point represents
     * a pixel coordinate or a floating point type if an NZMG coordinate
     * @type Number
     */
    this.y = Number(y);
}

/**
 * Outputs this point's properties as a String
 *
 * @return {String} this point's properties as a String
 */
GSPoint.prototype.toString = function() {
    return "Point: x=[" + this.x + "], y=[" + this.y + "]";
};

/**
 * An equality function
 * @return <code>true</code> if the point being compared refers to
 * the same coordinate as this point.
 * @type boolean
 */
GSPoint.prototype.equals = function(point) {
    return (point.x == this.x && point.y == this.y);
};
// --------------------------------------------------
// GSMapFeature
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2007
//
// --------------------------------------------------

/**
 * @fileOverview GSMapFeature is an abstract base class for all feature types rendered onto the map
 * @name GSMapFeature
 */

/**
 * This class is abstract and should not be used directly.
 * @constructor
 */
function GSMapFeature() {

    // add support for listeners
    GSEventBroadcaster.initialize(this);

    /**
     * The type of this class as a string value
     * @private
     * @type String
     */
    this.type = 'GSMapFeature';

    /**
     * The DOM element representing this map feature
     * @private
     * @type Element
     */
    this.graphicsEl = undefined;

    /**
     * The layer this feature belongs to
     * @private
     * @type GSLayer
     */
    this.layer = undefined;

    /**
     * A reference to the map instance the feature has been added to
     * @private
     * @type GSLayer
     */
    this.map = undefined;

    /**
     * <code>true</code> if this feature should be visible on the map
     * @private
     * @type boolean
     */
    this.visible = true;

    /**
     * A tool tip for the map feature
     * @private
     * @type GSTip
     */
    this.tip = undefined;

    /**
     * Associative array of bound event listeners
     * @private
     * @type Object
     */
    this.eventListeners = {};

    /**
     * An array of menu items to be dynamically added to the context
     * menu for the feature, if a context menu has been set for the feature.
     * @private
     * @type Array
     */
    this.contextMenuItems = [];
    
    /**
     * Arbitrary name-value pairs associated with this feature.
     * @private
     * @type Object
     */
    this.properties = {};
}

/**
 * Returns all user-defined properties for this feature
 * @return user-defined properties
 * @type Object
 */
GSMapFeature.prototype.getProperties = function() {
  return this.properties;  
};

/**
 * Sets a new property for this feature
 * @param {String} name the name of the property to set
 * @param {Mixed} value the value to set
 */
GSMapFeature.prototype.setProperty = function(name, value) {
  this.properties[name] = value;  
};

/**
 * Gets the named property for this feature
 * @param {String} name the name of the property to get
 */
GSMapFeature.prototype.getProperty = function(name) {
  return this.properties[name];  
};

/**
 * Deletes the named property for this feature
 * @param {String} name the name of the property to delete
 */
GSMapFeature.prototype.deleteProperty = function(name) {
  delete this.properties[name];  
};

/**
 * Adds this map feature to the specified layer
 * @private
 *
 * @param {GSLayer} layer the layer instance to add this feature to
 */
GSMapFeature.prototype.addToLayer = function(layer) {
    this.layer = layer;
    this.map = layer.map;
    this.map.addListener(this);
    if(this.tip) {
        this.tip.addToParent(this.map.container); 
    }
    if(this.contextMenu && !this.contextMenu.onmap) {
        this.contextMenu.addToMap(this.map);
    }
    this.onmap = true;
    this.render();
};

/**
 * Called by the map when the map view has changed
 *
 * @private
 */
GSMapFeature.prototype.render = function() {};

/**
 * Calls the handler function provided when the given event is triggered on this feature.
 * Any DOM Level 2 mouse event type may be specified for the <code>eventType</code>
 * argument:
 * <ul>
 * <li><strong>click</strong> - the click event occurs when the mouse button is clicked over the feature.</li>
 * <li><strong>mousedown</strong> - the mousedown event occurs when the mouse button is pressed over the feature</li> 
 * <li><strong>mouseup</strong> - the mouseup event occurs when the mouse button is released over the feature</li>
 * <li><strong>mouseover</strong> - the mouseover event occurs when the mouse is moved onto the feature</li>
 * <li><strong>mousemove</strong> - the mousemove event occurs when the mouse is moved while it is over the feature</li>
 * <li><strong>mouseout</strong> - the mouseout event occurs when the mouse is moved away from the feature</li>
 * </ul>
 * 
 * @param {String} eventType the event that should trigger the handler function
 * @param {Function} func the function to be invoked when the event is triggered
 * @return {Object} a token that can be passed to the <code>GSMapFeature.removeEventHandler()</code>
 * method to remove the event handler
 */
GSMapFeature.prototype.addEventHandler = function(eventType, func) {
    var token = GSEventManager.bind(this.graphicsEl, eventType, this, function() {
            func.apply(this, arguments);
        });
    this.addListenerToken(eventType, token);
    return token;
};

/**
 * Removes the event handler function represented by the <code>token</code> parameter
 * from this feature
 *
 * @param {Object} token a token identifying the event handler to be removed
 */
GSMapFeature.prototype.removeEventHandler = function(token) {
    GSEventManager.release(token);
};

/**
 * Sets a tool tip for this map feature
 * @param {String} text the text to display in the tool tip
 * @config {boolean} fixed if <code>true</code> the tip is statically positioned relative to
 * its owning element. If not fixed the tip will follow the mouse while its within the boundaries
 * of the owning element (defaults to <code>false</code>)
 * @config {String} text the text to display in the tool tip. If not specified GSTip will use
 * the value of the elements <code>title</code> attribute if set
 * @config {int} showDelay the delay the onShow method is called, defaults to 100ms
 * @config {int} hideDelay the delay the onHide method is called, defaults to 100ms
 * @config {int} maxDisplayChars the maximum number of characters that will be displayed in
 * the tool tip
 * @config {String} className the CSS class name to apply to the tip element, if not provided
 * the tip will use a default style
 * @config {GSPoint} offset the pixel offset for the tip. If the <code>fixed</code> option is <code>true</code>
 * the tip is positioned relative to the top-left corner of the owning element, otherwise it is positioned
 * relative to the mouse position
 * @config {boolean} fixed if <code>true</code> the tip element will be statically positioned
 * relative to the owning element. If <code>false</code> the tip element will follow the mouse,
 * defaults to <code>true</code>
 */
GSMapFeature.prototype.setTip = function(text, options) {
    if(!this.graphicsEl) {
        throw new Error('Cannot set tool tip, graphics element does not exist');
    }
    if(this.tip) {
        this.tip.remove();
    }    
    if(!options) {
        options = {};
    }
    options.text = text;
    this.tip = new GSTip(this.graphicsEl, options);
};

/**
 * Sets the visibility of this map feature
 *
 * @param {boolean} value <code>true</code> if this feature should be
 * visible on the map
 */
GSMapFeature.prototype.setVisible = function(value) {
    this.visible = value;
};

/**
 * Returns a boolean value indicating the visibility of this map feature
 * @return {boolean} <code>true</code> if this map feature is currently visible on the map
 */
GSMapFeature.prototype.isVisible = function() {
    return this.visible;
};

/**
 * Adds an event listener token to the internal map maintained by this map feature
 * @private
 * @param {String} eventType the event listener type
 * @param {String} listenerToken a key to the listener entry in the event cache
 */
GSMapFeature.prototype.addListenerToken = function(eventType, listenerToken) {
    if(this.eventListeners[eventType]) {
        if((typeof this.eventListeners[eventType]).toLowerCase() != 'array') {
            this.eventListeners[eventType] = [this.eventListeners[eventType]];
        }
        this.eventListeners[eventType].push(listenerToken);
    } else {
        this.eventListeners[eventType] = listenerToken;
    }    
};

/**
 * Allows this feature to do any clean up necessary before
 * it is destroyed
 * @private
 */
GSMapFeature.prototype.remove = function() {
    for(var i in this.eventListeners) {        
        if((typeof this.eventListeners[i]).toLowerCase() == 'array') {
            for(var j = this.eventListeners[i].length, token = null; token = this.eventListeners[i][j]; j--) {
                GSEventManager.release(token);
            }
        } else {
            GSEventManager.release(this.eventListeners[i]);
        }
    }
    if(this.onmap) {
        this.map.removeListener(this);
    }
    if(this.tip) {
        this.tip.remove();
    }
    this.graphicsEl = null;
};

/**
 * Outputs a JSON representation of this map feature
 * @return {String} a JSON string
 */
GSMapFeature.prototype.toJson = function() {
    var obj = {
    type: this.type
    };
    return GSUtil.objToJson(obj);
};

// --------------------------------------------------
// Listener Interface
// --------------------------------------------------

/**
 * Called when the map instance this feature has been added to
 * is resized
 * @private
 *
 * @param {GSMap} map the map instance this feature is added to
 * @param {GSDimension} oldSize the map size in pixels prior to re-sizing
 * @param {GSDimension} newSize the map size in pixels after re-sizing
 */
GSMapFeature.prototype.mapResized = function(map, oldSize, newSize) {
    this.render();
};

/**
 * Called when the map instance this feature has been added to
 * is zoomed
 * @private
 *
 * @param {GSMap} map the map instance this feature is added to
 * @param {int} oldZoomLevel the map zoom level prior to this zoom event
 * @param {int} newZoomLevel the map zoom level after this zoom event
 */
GSMapFeature.prototype.mapZoomed = function(map, oldZoomLevel, newZoomLevel) {
    this.render();
};

/**
 * Called when the bounds are changed for the map instance this feature has been added to
 * @private
 *
 * @param {GSMap} map the map instance this feature is added to
 * @param {GSBounds} oldBounds the map bounds prior to the map being moved
 * @param {GSBounds} newBounds the map bounds after the map has been moved
 */
GSMapFeature.prototype.mapBoundsChanged = function(map, oldBounds, newBounds) {
    this.render();
};

/**
 * Called when the map instance this feature has been added to has been clicked
 * @param {GSMap} map the map instance this feature is added to
 * @param {GSPoint} coordinate the NZMG coordinate for the point on the map
 * that was clicked
 * @param {MouseEvent} event the click event
 */
GSMapFeature.prototype.mapClicked = function(map, coordinate, event) {
    if(this.contextMenu && this.contextMenu.showing) {
        this.contextMenu.hide();
    }
};

// --------------------------------------------------
// End Listeners
// --------------------------------------------------

/**
 * Displays the specified HTML markup in the info window above this feature
 *
 * @param {String} html the HTML markup to display in the info window.
 * @param {Object} [options] optional parameters for configuring the map's
 * info window
 * @config {String} title the text to be displayed in the info window title bar
 * @config {boolean} maximizable if <code>true</code> a <strong>maximize</strong> button will be provided for maximizing
 * the info window
 * @config {Mixed} maxContent a DOM node or HTML string to be displayed when the info window is maximized
 * @config {GSDimension} maxSize the size the info window body should be maximized to. If not specified will resize to
 * relative to the map window up to the internal maximum size (1000 x 800px)
 * @config {Function} onopen a function to be called when the info window is opened
 * @config {Function} onclose a function to be called when the info window is closed
 */
GSMapFeature.prototype.showInfoWindow = function(html, options) {
    if(this.onmap) {        
        if(this.map.infoWindow) {
            html = html != undefined ? html : this.infoHtml;
            if(html == undefined) {
                throw new Error('Tried to open info window for feature without any HTML content');
            }
            var iwOptions = this.getInfoWindowOptions();
            var position = this.getInfoWindowPosition();
            this.map.openInfoWindow(position, html, GSUtil.merge(options, iwOptions));
            this.isShowingInfo = true;
        } else {
            throw new Error('Info window is not enabled for the map this feature has been added to');
        }
    }
};

/**
 * Returns default info window options for this feature
 * @return default options for the info window
 * @type Object
 * @private
 */
GSMapFeature.prototype.getInfoWindowOptions = function() {
    // to be overriden by subclasses
};

/**
 * Returns the point to position the info window at for this feature
 * @return the point to position the info window at
 * @type GSPoint
 * @private
 */
GSMapFeature.prototype.getInfoWindowPosition = function() {
    // to be overriden by subclasses
};

/**
 * Called when the map's info window is opened
 * @private
 */
GSMapFeature.prototype.infoWindowOpened = function(infoWindow) {
    // to be overriden by subclasses
};

/**
 * Called when the map's info window is closed
 * @private
 */
GSMapFeature.prototype.infoWindowClosed = function(infoWindow) {
    if(this.isShowingInfo)
        this.isShowingInfo = false;
};

/**
 * Sets the context menu for this feature
 * @public
 * @param {GSContextMenu} contextMenu the context menu to display in response
 * to right-click events on the feature
 */
GSMapFeature.prototype.setContextMenu = function(contextMenu) {
    this.contextMenu = contextMenu;
    this.enableContextMenu();
};

/**
 * Adds a context menu item for this feature. The menu item will be dynamically
 * added to the context menu set for this feature when the context menu is displayed.
 * When the context menu is closed the menu item will be automatically removed.
 * Requires that a context menu has first been set for this feature.
 * @param {GSMenuItem} menuItem the menu item to set
 */
GSMapFeature.prototype.addContextMenuItem = function(menuItem) {
    if(!this.contextMenu) {
        throw new Error('Must set a context menu before adding a context menu item');
    }
    this.contextMenuItems.push({item: menuItem, pos: 0});
};

/**
 * Adds a context menu item for this feature at the specified position.
 * The menu item will be dynamically added to the context menu set for this feature
 * when the context menu is displayed.
 * When the context menu is closed the menu item will be automatically removed.
 * Requires that a context menu has first been set for this feature.
 * @param {GSMenuItem} menuItem the menu item to set
 * @param {int} pos the position that the menu item will be added to in the
 * context menu
 */
GSMapFeature.prototype.addContextMenuItemAt = function(menuItem, pos) {
    if(!this.contextMenu) {
        throw new Error('Must set a context menu before adding a context menu item');
    }
    this.contextMenuItems.push({item: menuItem, pos: pos});
};

/**
 * Enables this feature's context menu. If a context menu has been set
 * for the owning map that will be used, otherwise a new context menu
 * will be created.
 * @public
 * @return a context menu for this feature
 * @type GSContextMenu
 */
GSMapFeature.prototype.enableContextMenu = function() {
    if(this.contextMenu) {
        GSEventManager.removeEventListener(this.eventListeners['contextmenu']);
        this.eventListeners['contextmenu'] = 
            GSEventManager.addEventListener(this.graphicsEl, 'contextmenu', GSUtil.bindAsEventListener(this.displayContextMenu, this));
    }
    return this.contextMenu;
};

/**
 * Disables this feature's context menu
 * @public
 */
GSMapFeature.prototype.disableContextMenu = function() {
    GSEventManager.removeEventListener(this.eventListeners['contextmenu']);
    this.eventListeners['contextmenu'] = GSEventManager.addEventListener(this.graphicsEl, 'contextmenu', GSUtil.cancelEvent);
};

/**
 * Returns a reference to this feature's context menu. Will be <code>undefined</code> if the
 * context menu has not been enabled for the feature.
 * @public
 * @return a context menu for this feature
 * @type GSContextMenu
 */
GSMapFeature.prototype.getContextMenu = function() {
    return this.contextMenu;
};

/**
 * Displays the features's context menu in response to a right-click event
 * @param {Event} e the mouse event
 * @private
 */
GSMapFeature.prototype.displayContextMenu = function(e) {
    if(GSContextMenuManager.activeMenu && GSContextMenuManager.activeMenu.showing) {
        GSContextMenuManager.activeMenu.hide();
    }
    for(var i = 0, l = this.contextMenuItems.length; i < l; i++) {
        var obj = this.contextMenuItems[i];
        this.contextMenu.addItemAt(obj.item, obj.pos);
    }
    this.contextMenu.addListener(this);
    var pos = GSUtil.getMousePixelCoordinate(e, this.map.container);
    this.contextMenu.show(pos, this);
    GSContextMenuManager.contextMenuOpened(this.contextMenu);
    GSUtil.cancelEvent(e);
};

/**
 * Callback invoked when the context menu for this feature is hidden. Enables
 * the menu to be restored to it's previous state
 * @private
 */
GSMapFeature.prototype.contextMenuHide = function(cxtMenu) {
    for(var i = 0, l = this.contextMenuItems.length; i < l; i++) {
        var obj = this.contextMenuItems[i];
        this.contextMenu.removeItem(obj.item);
    }
    this.contextMenu.removeListener(this);
};


// --------------------------------------------------
// GSPointFeature
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview <code>GSPointFeature</code> represents a point of interest (POI)
 * feature.
 * @name GSPointFeature
 */

GSUtil.extend(GSPointFeature, GSMapFeature);

/**
 * Creates a new <code>GSPointFeature</code> that is initialized with the properties specified
 * in the <code>options</code> associative array. Of these properties the <code>icon</code>
 * property must be specified if a custom icon is to be used, otherwise the point feature will
 * default to using the default SmartFIND icon type.
 * <p>
 * <pre lang="javascript">
 * // create the feature's icon
 * var icon = new GSIcon();
 * icon.imageSrc = "http://www.example.com/images/icon.png";
 * icon.imageSize = new GSDimension(22, 16);
 *
 * // create the coordinate the feature will be centered on
 * var coord = new GSPoint(2530000, 5990000);
 *
 * var options = {
 *   id: "12345",
 *   coordinate: coord,
 *   icon: icon,
 *   name: "Nelson"};
 *   
 * var feature = new GSPointFeature(options);
 *
 * layer.addFeature(feature);
 * </pre>
 * </p>
 * @extends GSMapFeature
 * @constructor
 * @param {Object} [options] an associative array of named arguments used to
 * initialize the point feature object, the following parameters may be specified:
 * @config {int} id a unique id for this point feature
 * @config {GSPoint} coordinate the coordinate this point feature is centered on
 * @config {GSIcon} icon an instance of <code>GSIcon</code> used to represent this feature visually on the map
 * @config {String} name the name of this point feature
 * @config {String} suburb the suburb containing this point feature
 * @config {String} district the district containing this point feature
 * @config {String} region the region containing this point feature
 */
function GSPointFeature(options) {

    // Call superclass constructor
    GSPointFeature.baseConstructor.call(this);    

    /**
     * The type of this class as a string value
     * @private
     * @type String
     */
    this.type = "GSPointFeature";

    /**
     * Unique id for this point feature
     * @type String
     */
    this.id = GSUtil.getCaseInsensitiveProperty(options, "id");

    /**
     * The coordinate this point feature is centered on
     * @type GSPoint
     */
    this.coordinate = GSUtil.getCaseInsensitiveProperty(options, "coordinate");
    if(this.coordinate == undefined) {
        var x = GSUtil.getCaseInsensitiveProperty(options, "x");
        var y = GSUtil.getCaseInsensitiveProperty(options, "y");
        this.coordinate = (x && y) ? new GSPoint(x, y) : new GSPoint(0, 0);
    }

    /**
     * The name of this point feature
     * @type String
     */
    this.name = GSUtil.getCaseInsensitiveProperty(options, "name");

    /**
     * The suburb containing this point feature
     * @type String
     */
    this.suburb = GSUtil.getCaseInsensitiveProperty(options, "suburb");

    /**
     * The district containing this point feature
     * @type String
     */
    this.district = GSUtil.getCaseInsensitiveProperty(options, "district");
    
    /**
     * The region containing this point feature
     * @type String
     */
    this.region = GSUtil.getCaseInsensitiveProperty(options, "region");

    /**
     * Identifies the type of icon to use for this feature. This should be specified if the
     * icon factory is to be used to create the icon for this feature.
     * @private
     * @type String
     */
    this.iconType = undefined;

    /**
     * HTML markup to display in this feature's info window.
     * @type String
     */
    this.infoHtml = GSUtil.getCaseInsensitiveProperty(options, "infoHtml");

    /**
     * If <code>true</code> this point feature should be draggable.
     * @type boolean
     * @private
     */
    this.draggable = GSUtil.stringToBoolean(GSUtil.getCaseInsensitiveProperty(options, "draggable", false));

    /**
     * An optional text label that may be associated with the feature
     * @private
     */
    this.label = undefined;

    /**
     * Flat that is <code>true</code> if the info window is currently
     * displayed for this feature
     * @type boolean
     * @private
     */
    this.isShowingInfo = false;

    /**
     * <code>true</code> if this feature is currently visible within the map extents
     * @type boolean
     * @private
     */
    this.visibleOnMap = false;

    /**
     * The icon used to represent this feature on the map
     * @private
     * @type GSIcon
     */
    this.icon = GSUtil.getCaseInsensitiveProperty(options, "icon");
    if(!this.icon) {
        this.icon = new GSIcon(GSIcon.defaultIcon);
    }    
    this.icon.alt = this.name;
    this.icon.initialize();

    this.graphicsEl = this.icon.getEventTarget();

    if(this.draggable) { // if draggable, enable dragging
        this.enableDragging();
    }
    this.icon.setVisible(this.visible);    
}

/**
 * Adds this map feature to the specified layer
 * @private
 *
 * @param {GSLayer} layer the layer instance to add this feature to
 */
GSPointFeature.prototype.addToLayer = function(layer) {
    this.icon.addToLayer(layer);
    if(this.label) {
        this.label.addToLayer(layer);    
    }
    GSPointFeature.superClass.addToLayer.call(this, layer);
};

/**
 *@private
 */
GSPointFeature.prototype.enableDragging = function() {
    GSEventManager.bind(this.graphicsEl, 'mousedown', this, this.startDrag);
};

/**
 *@private
 */
GSPointFeature.prototype.startDrag = function(e) {
    this.dragStartPos = GSUtil.getMousePixelCoordinate(e);
    
    if(this.isShowingInfo) {
        this.map.infoWindow.close();
    }
    
    this.eventListeners['mousemove'] = GSEventManager.bind(document, 'mousemove', this, this.dragMove);
    this.eventListeners['mouseup'] = GSEventManager.bind(document, 'mouseup', this, this.dragEnd);

    GSUtil.cancelEvent(e);
};

/**
 *@private
 */
GSPointFeature.prototype.dragMove = function(e) {
    if(!this.hasDragged(e)) {
        return;
    }
    if(!this.dragging) {
        this.dragging = true;
        this.icon.dragCrosshair.style.display = 'block';
        document.body.style.cursor = 'move';
        this.broadcastMessage('featureDragStart', this);           
    } else {
        var pos = GSUtil.getMousePixelCoordinate(e, this.map.container);
        this.icon.repositionDragCrosshair(pos.x, pos.y - 10);

        this.coordinate = this.map.translateToRealWorldCoordinate(new GSPoint(pos.x, pos.y - 10));

        pos = new GSPoint(pos.x + this.icon.iconOffset.x, pos.y + this.icon.iconOffset.y);
        
        var y = pos.y - this.icon.arcHeight;
        GSUtil.positionElement(this.icon.image, pos.x, y);
        if(this.icon.shadow) {
            GSUtil.positionElement(this.icon.shadow, pos.x, y);
        }
        
        this.broadcastMessage('featureDrag', this);
    }
        
    GSUtil.cancelEvent(e);
};

/**
 *@private
 */
GSPointFeature.prototype.dragEnd = function(e) {
    if(this.hasDragged(e)) {
        var pos = GSUtil.getMousePixelCoordinate(e, this.map.container);

        this.coordinate = this.map.translateToRealWorldCoordinate(new GSPoint(pos.x, pos.y - 10));
        this.renderIcon();
        
        this.dragging = false;
        this.icon.dragCrosshair.style.display = 'none';
        document.body.style.cursor = 'default';
        this.broadcastMessage('featureDragEnd', this);        
    }

    GSEventManager.release(this.eventListeners['mousemove']);
    GSEventManager.release(this.eventListeners['mouseup']);
    
    GSUtil.cancelEvent(e);
};

/**
 * Creates a copy of this feature
 * 
 * @return {GSPointFeature} a clone of this feature
 */
GSPointFeature.prototype.clone = function() {
    // clear properties that shouldn't be cloned, saving
    // them into temp variables for restoring after cloning
    var layer = this.layer;
    var icon = this.icon;
    var tip = this.tip;
    this.layer = undefined;
    this.icon = undefined;
    this.tip = undefined;

    var clone = GSUtil.clone(this);
    clone.icon = new GSIcon(icon);
    clone.icon.alt = this.name;
    clone.icon.initialize();
    clone.onmap = false;
    clone.isShowingInfo = false;

    // restore feature properties
    this.layer = layer;
    this.icon = icon;
    this.tip = tip;
    
    return clone;
};

/**
 * Sets a label for this point feature
 * @param {GSLabel} label the label to set
 * </ul>
 */
GSPointFeature.prototype.setLabel = function(label) {
    if(!this.graphicsEl) {
        throw new Error('Cannot set label, graphics element does not exist');
    }
    if(this.label) {
        this.label.remove();
    }
    this.label = label;
    this.label.graphicsEl.style.zIndex = 1000;
    this.label.setCoordinate(this.coordinate);
    this.label.updatePosition = GSUtil.bind(this.updateLabelPosition, this.label, this);
};

/**
 * Positions the label for this feature
 * @param {GSPoint} pos the point in the map coordinate space the label should
 * be positioned relative to
 * @param {GSPointFeature} feature a reference to this feature
 * @private
 */
GSPointFeature.prototype.updateLabelPosition = function(feature, pos) {
    var left = pos.x;
    var top = pos.y;
    if(this.options.position.left !== undefined) {
        left = pos.x + this.options.position.left;
    } else if(this.options.position.right !== undefined) {
        left = pos.x - (this.textEl.offsetWidth + this.options.position.right);
    } else {
        left = pos.x + feature.icon.iconOffset.x + ((feature.icon.image.offsetWidth - this.backgroundEl.offsetWidth) / 2);            
    }
    if(this.options.position.top !== undefined) {
        top = pos.y + this.options.position.top;
    } else if(this.options.position.bottom !== undefined) {
        top = pos.y - (this.textEl.offsetHeight + this.options.position.bottom);
    }
    GSUtil.positionElement(this.graphicsEl, left, top);    
};

/**
 * Sets the visibility of this point feature
 *
 * @param {boolean} visible <code>true</code> if this feature should be
 * visible on the map
 */
GSPointFeature.prototype.setVisible = function(visible) {
    this.visible = visible;
    if(this.icon) {
        this.icon.setVisible(visible);
        if(this.onmap) {
            this.renderIcon();
        }
    }
    if(this.label) {
        this.label.setVisible(visible);
    }
    if(!visible) {
        if(this.isShowingInfo) {
            this.map.infoWindow.close();
        }
    }
};

/**
 * Returns a boolean value indicating the visibility of this point feature
 * @return <code>true</code> if this point feature is currently visible on the map
 * @type boolean
 */
GSPointFeature.prototype.isVisible = function() {
    return this.visible;
};

/**
 * Returns a boolean value indicating if this point feature is draggable
 * @return <code>true</code> if this point feature is draggable
 * @type boolean
 */
GSPointFeature.prototype.isDraggable = function() {
    return this.draggable;
};

/**
 * Sets the coordinate for this point feature
 * @param {GSPoint} coordinate the coordinate to position the feature at
 */
GSPointFeature.prototype.setCoordinate = function(coordinate) {
    this.coordinate = coordinate;
    this.render();
};

/**
 * Sets the foreground image for the feature's icon. Neither the image size
 * or the shadow image for the icon is effected by this method.  If a more
 * complete change to the feature's icon is required use
 * <code>GSPointFeature.setIcon()</code> instead.
 * @param {String} url the url of the image to use for the icon foreground
 */
GSPointFeature.prototype.setImage = function(url) {
    GSUtil.setImageSrc(this.icon.image, url);
    this.renderIcon();
};

/**
 * Sets the icon used by this point feature
 * @param {GSIcon} icon the icon to set
 */
GSPointFeature.prototype.setIcon = function(icon) {
    if(this.layer) { // remove old icon    
        this.icon.remove(this.layer);
    }

    // set new icon and generate DOM objects
    this.icon = icon;
    this.icon.initialize();
    
    // update this point features reference to the icon DOM element
    this.graphicsEl = this.icon.getEventTarget();

    // if draggable, enable dragging
    if(this.draggable) {
        this.enableDragging();
    }

    // copy over event handlers to new icon
    for(var i in this.eventListeners) {
        if((typeof this.eventListeners[i]).toLowerCase() == 'array') {
            for(var j = this.eventListeners[i].length, token = null; token = this.eventListeners[i][j]; j--) {
                var handler = GSEventCache.handlers[token];
                GSEventManager.bind(this.graphicsEl, handler[1], this, handler[2]);
            }
        } else {
            var handler = GSEventCache.handlers[this.eventListeners[i]];
            GSEventManager.bind(this.graphicsEl, handler[1], this, handler[2]);
        }
    }

    if(this.layer) {
        this.icon.addToLayer(this.layer);    
        if(this.onmap) { // if this point is on the map render the icon
            this.renderIcon();
        }
    }
};

/**
 * Returns <code>true</code> if the mouse has been moved greater than 2 pixels from its
 * mousedown position
 * @private
 * @param {Event} e the current mouse event
 */
GSPointFeature.prototype.hasDragged = function(e) {
    if(!this.dragStartPos) {
        return false;
    }
    var tolerance = 2;
    var coord = GSUtil.getMousePixelCoordinate(e);
    var dragged = (Math.abs(this.dragStartPos.x - coord.x) > tolerance) || (Math.abs(this.dragStartPos.y - coord.y) > tolerance);
    return dragged;
};

/**
 * Opens a blowup map centered on this feature.
 * @param {int} zoomLevel an integer value from 0 (maximum zoom in) to 16 (maximum zoom out)
 */
GSPointFeature.prototype.openMapBlowup = function(zoomLevel) {
    zoomLevel = zoomLevel ? zoomLevel : 0;
    if(this.onmap) {
        this.isShowingInfo = true;

        var offsetX = this.icon.iconOffset.x + this.icon.iconInfoWindowOffset.x;
        var offsetY = this.icon.iconOffset.y + this.icon.iconInfoWindowOffset.y;

        var clone = this.clone();
        
        this.map.openMapBlowup(this.coordinate, {zoomLevel: 0, offset: new GSPoint(offsetX, offsetY),
                    onopen: function(map) {
                    map.addFeature(clone);
                }});
    }
};

/**
 * Returns default info window options for this feature
 * @return default options for the info window
 * @type Object
 * @private
 */
GSPointFeature.prototype.getInfoWindowOptions = function() {
    var offsetX = this.icon.iconOffset.x + this.icon.iconInfoWindowOffset.x;
    var offsetY = this.icon.iconOffset.y + this.icon.iconInfoWindowOffset.y;    
    var iwOptions = {
        offset: new GSPoint(offsetX, offsetY),
        title: this.name ? this.name : ''
    };
    return iwOptions;
};

/**
 * Returns the point to position the info window at for this feature
 * @return the point to position the info window at
 * @type GSPoint
 * @private
 */
GSPointFeature.prototype.getInfoWindowPosition = function() {
    return this.coordinate;
};

/**
 * If this feature has a point within the specified number of pixels of the 
 * map pixel coordinate provided as the first argument that point is returned,
 * otherwise <code>null</code> is returned
 * @param {GSPoint} pxCoord the pixel coordinate to test
 * @param {int} tolerance the maximum number of pixels that a point can be from
 * the specified pixel coordinate to be considered
 * @private
 */
GSPointFeature.prototype.closestPoint = function(pxCoord, tolerance) {
    var thisPxCoord = this.map.translateToMapCoordinate(this.coordinate);
    var deltaX = Math.abs(thisPxCoord.x - pxCoord.x);
    var deltaY = Math.abs(thisPxCoord.y - pxCoord.y);
    if(deltaX < tolerance && deltaY < tolerance) {
        return {point: this.coordinate, delta: Math.min(deltaX, deltaY)};
    } else {
        return null;
    }
};

// --------------------------------------------------
// Listener Interface
// --------------------------------------------------


// --------------------------------------------------

/**
 * Gets the minimum bounding box necessary to contain this map feature
 * @return the bounds of this map feature
 * @type GSBounds
 */
GSPointFeature.prototype.getBounds = function() {
    return new GSBounds(this.coordinate.x, this.coordinate.y, this.coordinate.x, this.coordinate.y);
};

/**
 * Displays this feature on the map
 *
 * @private
 */
GSPointFeature.prototype.render = function() {
    this.renderIcon();
};

/**
 * @private
 */
GSPointFeature.prototype.renderIcon = function() {    
    var wasVisibleOnMap = this.visibleOnMap;
    this.visibleOnMap = this.icon.render(this.layer, this.coordinate);
    if(!wasVisibleOnMap && this.visibleOnMap) {
        this.broadcastMessage('featureVisibleOnMap', this);
    } else if(wasVisibleOnMap && !this.visibleOnMap) {
        this.broadcastMessage('featureNotVisibleOnMap', this);
    }
};

/**
 * Allows this feature to do any clean up necessary before
 * it is destroyed
 * @private
 */
GSPointFeature.prototype.remove = function() {
    GSPointFeature.superClass.remove.call(this);

    if(this.onmap) {
        this.icon.remove(this.layer);
        
        // hide info window if showing
        if(this.isShowingInfo) {
            this.map.infoWindow.close();
        }
    }
    if(this.label) {
        this.label.remove();
    }
};

/**
 * Outputs a JSON representation of this map feature
 * @return a JSON string
 * @type string
 */
GSPointFeature.prototype.toJson = function() {
    var obj = {
    type: this.type,
    coordinate: this.coordinate,
    iconImageSrc: this.icon.imageSrc,
    iconOffset: this.icon.iconOffset,
    visible: this.visible
    };
    return GSUtil.objToJson(obj);
};

/**
 * Outputs a GML representation of this map feature. Output is a GML 2.x string.
 * @return a GML string
 * @type string
 */
GSPointFeature.prototype.toGML = function() {
    var sb = [];
    sb.push('<Point xmlns:gml="http://www.opengis.net/gml" srsName="EPSG:27200">');
    sb.push('<gml:coordinates>' + this.coordinate.x + ',' + this.coordinate.y + '</gml:coordinates>');
    sb.push('</Point>');
    return sb.join('');
};

/**
 * Outputs a Well-Known Text (WKT) representation of this map feature.
 * @return a WKT string
 * @type string
 */
GSPointFeature.prototype.toWKT = function() {
    return 'POINT(' + this.coordinate.x + ' ' + this.coordinate.y + ')';
};

/**
 * Outputs this features properties as a String
 *
 * @return this features properties as a String
 * @type String
 */
GSPointFeature.prototype.toString = function() {
    var out = "Feature:<br/>";
    for(p in this) {
        if(this[p] instanceof Function) {
            out += (p + " : [Function]<br/>");
        } else {
            out += (p + " : " + this[p] + "<br/>");
        }
    }
    return out;
};
/**************************************************
 * dom-drag.js
 * 09.25.2001
 * www.youngpup.net
 **************************************************
 * 10.28.2001 - fixed minor bug where events
 * sometimes fired off the handle, not the root.
 **************************************************/

var GSDrag = {

    obj : null,

    init : function(o, oRoot, minX, maxX, minY, maxY, bSwapHorzRef, bSwapVertRef, fXMapper, fYMapper)
    {
        GSEventManager.addEventListener(o, 'mousedown', GSUtil.bindAsEventListener(GSDrag.start, o));
        GSEventManager.addEventListener(o, 'contextmenu', GSDrag.stopPropagation);

        o.hmode			= bSwapHorzRef ? false : true ;
        o.vmode			= bSwapVertRef ? false : true ;

        o.root = oRoot && oRoot != null ? oRoot : o ;

        if (o.hmode  && isNaN(parseInt(o.root.style.left  ))) o.root.style.left   = "0px";
        if (o.vmode  && isNaN(parseInt(o.root.style.top   ))) o.root.style.top    = "0px";
        if (!o.hmode && isNaN(parseInt(o.root.style.right ))) o.root.style.right  = "0px";
        if (!o.vmode && isNaN(parseInt(o.root.style.bottom))) o.root.style.bottom = "0px";

        o.minX	= typeof minX != 'undefined' ? minX : null;
        o.minY	= typeof minY != 'undefined' ? minY : null;
        o.maxX	= typeof maxX != 'undefined' ? maxX : null;
        o.maxY	= typeof maxY != 'undefined' ? maxY : null;

        o.xMapper = fXMapper ? fXMapper : null;
        o.yMapper = fYMapper ? fYMapper : null;

        o.root.onDragStart	= new Function();
        o.root.onDragEnd	= new Function();
        o.root.onDrag		= new Function();
    },

    start : function(e)
    {
        GSDrag.stopPropagation(e);
                
        var o = GSDrag.obj = this;
        e = GSDrag.fixE(e);
        var y = parseInt(o.vmode ? o.root.style.top  : o.root.style.bottom);
        var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right );
        o.root.onDragStart(x, y);

        var pos = GSUtil.getMousePos(e);
        o.lastMouseX	= pos.x;
        o.lastMouseY	= pos.y;

        if (o.hmode) {
            if (o.minX != null)	o.minMouseX	= pos.x - x + o.minX;
            if (o.maxX != null)	o.maxMouseX	= o.minMouseX + o.maxX - o.minX;
        } else {
            if (o.minX != null) o.maxMouseX = -o.minX + pos.x + x;
            if (o.maxX != null) o.minMouseX = -o.maxX + pos.x + x;
        }

        if (o.vmode) {
            if (o.minY != null)	o.minMouseY	= pos.y - y + o.minY;
            if (o.maxY != null)	o.maxMouseY	= o.minMouseY + o.maxY - o.minY;
        } else {
            if (o.minY != null) o.maxMouseY = -o.minY + pos.y + y;
            if (o.maxY != null) o.minMouseY = -o.maxY + pos.y + y;
        }

        document.onmousemove	= GSDrag.drag;
        document.onmouseup		= GSDrag.end;
        return false;
    },

    drag : function(e)
    {

        GSDrag.stopPropagation(e);

        e = GSDrag.fixE(e);
        var o = GSDrag.obj;

        var pos = GSUtil.getMousePos(e);
        var ex	= pos.x;
        var ey	= pos.y;

        var y = parseInt(o.vmode ? o.root.style.top  : o.root.style.bottom);
        var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right );
        var nx, ny;

        if (o.minX != null) ex = o.hmode ? Math.max(ex, o.minMouseX) : Math.min(ex, o.maxMouseX);
        if (o.maxX != null) ex = o.hmode ? Math.min(ex, o.maxMouseX) : Math.max(ex, o.minMouseX);
        if (o.minY != null) ey = o.vmode ? Math.max(ey, o.minMouseY) : Math.min(ey, o.maxMouseY);
        if (o.maxY != null) ey = o.vmode ? Math.min(ey, o.maxMouseY) : Math.max(ey, o.minMouseY);

        nx = x + ((ex - o.lastMouseX) * (o.hmode ? 1 : -1));
        ny = y + ((ey - o.lastMouseY) * (o.vmode ? 1 : -1));

        if (o.xMapper)		nx = o.xMapper(y);
        else if (o.yMapper)	ny = o.yMapper(x);

            GSDrag.obj.root.style[o.hmode ? "left" : "right"] = nx + "px";
        GSDrag.obj.root.style[o.vmode ? "top" : "bottom"] = ny + "px";
        GSDrag.obj.lastMouseX	= ex;
        GSDrag.obj.lastMouseY	= ey;

        GSDrag.obj.root.onDrag(nx, ny, e);
                
        return false;
    },

    end : function(e)
    {
        GSDrag.stopPropagation(e);

        e = GSDrag.fixE(e);

        document.onmousemove = null;   
        document.onmouseup   = null;
        GSDrag.obj.root.onDragEnd(	parseInt(GSDrag.obj.root.style[GSDrag.obj.hmode ? "left" : "right"]), 
                                        parseInt(GSDrag.obj.root.style[GSDrag.obj.vmode ? "top" : "bottom"]), e);
        GSDrag.obj = null;
    },

    fixE : function(e)
    {
        if (typeof e == 'undefined') e = window.event;
        if (typeof e.layerX == 'undefined') e.layerX = e.offsetX;
        if (typeof e.layerY == 'undefined') e.layerY = e.offsetY;
        return e;
    },

    stopPropagation : function(e)
    {
        e = e ? e : window.event;
        if (e.stopPropagation) {
            e.preventDefault();
            e.stopPropagation();
        } else {
            e.cancelBubble = true;
            e.returnValue = false;
        }
    }
};
// --------------------------------------------------
//
// Copyright GeoSmart Limited 2005
//
// --------------------------------------------------

/**
 * @fileOverview <code>GSRasterImageFeature</code> represents a raster image feature
 * @name GSRasterImageFeature
 */

GSUtil.extend(GSRasterImageFeature, GSMapFeature);

/**
 * Creates a new <code>GSRasterImageFeature</code>.
 *
 * The raster image feature can be used for overlaying raster images served from a web service.
 * The raster image feature listens to map change events.
 * When such an event occurs (eg. the map has been moved) a user provided callback is fired off with the extent
 * and zoom level of the map.
 * <p>
 * <pre lang="javascript">
 * var layer = myMap.createLayer("raster");
 * // Create the callback for constructing the image URL
 * var func = function(zoomLevel, bounds, dimension) {
 *     return "http://foo/my-image-webservice?zoomLevel=" + zoomLevel + "&amp;minx=" + bounds.minX + "&amp;miny=" + bounds.minY + "&amp;maxx=" + bounds.maxX + "&amp;maxy=" + bounds.maxY + "&amp;width=" + dimension.width + "&amp;height=" + dimension.height;
 * }
 *
 * var feature = new GSRasterImageFeature(func);
 * layer.addFeature(feature);
 * </pre>
 * </p>
 * @param {Function} callback a callback function for creating the URL for the overlayed image.
 *
 * The callback function receives the following arguments:
 * <ul>
 *   <li><strong>zoomLevel</strong> - <int> the current zoom level</li>
 *   <li><strong>bounds</strong> - <GSBounds> the bounds of the current map</li>
 *   <li><strong>dimension</strong> - <GSDimension> the pixel width and height of the map</li>
 * </ul>
 * @constructor
 */
function GSRasterImageFeature(callback) {
    
    // Call superclass constructor
    GSRasterImageFeature.baseConstructor.call(this);

    /**
     * The type of this class as a string value
     * @private
     * @type String
     */
    this.type = "GSRasterImageFeature";

    /**
     * A callback function that will build the URL for the image feature
     * @private
     * @type Function
     */
    this.callback = callback;

    /**
     * The DOM image
     * @private
     * @type Image
     */
    this.image = null;
};

/**
 * Sets the visibility of this feature
 *
 * @param {boolean} visible <code>true</code> if this feature should be
 * visible on the map
 */
GSRasterImageFeature.prototype.setVisible = function(visible) {
    this.visible = visible;
    if(this.graphicsEl) {
        this.graphicsEl.style.visibility = visible ? "visible": "hidden";
    }
};

/**
 * @private
 */
GSRasterImageFeature.prototype.render = function() {
    if (!this.layer) {
        throw new Error("GSRasterImageFeature has not been added to a map");
    }

    // remove current image
    if (this.graphicsEl) {
        //this.layer.lineContainer.removeChild(this.graphicsEl);
        this.layer.map.imageContainer.removeChild(this.graphicsEl);
    }

    this.graphicsEl = GSUtil.createElement("img");
    this.graphicsEl.className = 'gsimagefeature';
    GSUtil.makeUnselectable(this.graphicsEl);
    this.graphicsEl.src = this.callback(this.map.scaleIdx, this.map.getBounds(), this.map.getSize());
    //this.layer.lineContainer.appendChild(this.graphicsEl);
    this.layer.map.imageContainer.appendChild(this.graphicsEl);

};

/**
 * @private
 */
GSRasterImageFeature.prototype.remove = function() {
    if(this.graphicsEl) {
        //this.layer.lineContainer.removeChild(this.graphicsEl);
        this.layer.map.imageContainer.removeChild(this.graphicsEl);
    }
    if(this.map) {
        this.map.removeListener(this);
    }

    this.map = null;
};
/**
 * @fileOverview Class that represents the map's info window. This class should not be instantiated
 * directly as each map instance will create its own info window instance.
 * @name GSInfoWindow2
 */

/**
 * @constructor Constructs a new info window
 * @param {GSMap} map the map instance the info window has been created for
 */
function GSInfoWindow2(map) {
    /**
     * A reference to the map object this info window belongs to
     * @private
     * @type GSMap
     */
    this.map = map;

    // register this info window to listen for changes in map state
    this.map.addListener(this);

    /**
     * The coordinate this info window is currently showing at
     * @private
     * @type GSPoint
     */
    this.coordinate;

    /**
     * The pixel offset that should be used to position the info
     * window relative to the coordinate it is being displayed for
     * @private
     * @type GSPoint
     */
    this.offset = new GSPoint(0, 0);

    /**
     * If this info window is open this will be set to
     * <code>true</code>
     * @private
     * @type boolean
     */
    this.isOpen = false;

    /**
     * <code>true</code> if this info window is currently visible
     * @private
     * @type boolean
     */
    this.hidden = true;

    /**
     * Buttons belonging to the info window
     * @private
     * @type Object
     */
    this.buttons = {};

    /**
     * The root element for the info window
     * @private
     * @type HTMLElement
     */
    this.element = undefined;
    
    /**
     * When the info window is maximized this property is set to a <code>GSDimension</code>
     * object holding the original size of the info window body
     * @private
     * @type GSDimension
     */
    this.restoreSize = undefined;

    /**
     * <code>true</code> if the window is currently maximized
     * @private
     * @type boolean
     */
    this.maximized = false;

    this.createWindow();

    this.preloadImages();
        
    // Only propagate these events to the map if the event occurs in the info
    // window content area
    GSEventManager.bind(this.element, "click", this, this.processMouseEvent);
    GSEventManager.bind(this.element, "mousedown", this, this.processMouseEvent);
    GSEventManager.bind(this.element, "dblclick", this, this.processMouseEvent);
    GSEventManager.bind(window, "resize", this, this.windowResizeHandler);
};

/**
 * Width of the callout
 * @private
 * @type int
 */
GSInfoWindow2.CALLOUT_WIDTH = 24;

/**
 * Height of the callout
 * @private
 * @type int
 */
GSInfoWindow2.CALLOUT_HEIGHT = 40;

/**
 * Width of the info window border
 * @private
 * @type int
 */
GSInfoWindow2.BORDER_WIDTH = 1;

/**
 * Height of the info window title area
 * @private
 * @type int
 */
GSInfoWindow2.TITLE_HEIGHT = 24;

/**
 * Minimum width for the info window body
 * @private
 * @type int
 */
GSInfoWindow2.MIN_BODY_WIDTH = 100;

/**
 * Minimum height for the info window body
 * @private
 * @type int
 */
GSInfoWindow2.MIN_BODY_HEIGHT = 40;

/**
 * Maximum width for the info window body
 * @private
 * @type int
 */
GSInfoWindow2.MAX_BODY_WIDTH = 1000;

/**
 * Maximum height for the info window body
 * @private
 * @type int
 */
GSInfoWindow2.MAX_BODY_HEIGHT = 800;

/**
 * Horizontal margin for body content
 * @private
 * @type int
 */
GSInfoWindow2.H_BODY_MARGIN = 10;

/**
 * Image resources to preload
 * @private
 * @type Array
 */
GSInfoWindow2.PRELOAD_IMAGES = [_globals.resourceURL + 'iw2/buttons.gif', _globals.resourceURL + 'iw2/callout.gif',
                               _globals.resourceURL + 'iw2/hd-sprite.gif', _globals.resourceURL + 'iw2/left.gif',
                               _globals.resourceURL + 'iw2/mid.gif', _globals.resourceURL + 'iw2/right.gif'];

/**
 * Preloads images referenced in the static PRELOAD_IMAGES array
 * @private
 */
GSInfoWindow2.prototype.preloadImages = function() {
    var images = [];
    var img = null;
    for(var i = 0, l = GSInfoWindow2.PRELOAD_IMAGES.length; i < l; i++) {
        images.push(new Image().src = GSInfoWindow2.PRELOAD_IMAGES[i]);
    }
};

/**
 * Processes mouse events occurring on the info window. If the event falls
 * within the info window content area prevent it from propagating to the
 * map below
 * @private
 */
GSInfoWindow2.prototype.processMouseEvent = function(e) {
    GSUtil.eventStopPropagation(e);
};

/**
 * <code>true</code> if the info windows position can be anchored within the current
 * map view while zooming
 * @return {boolean} <code>true</code> if the info window is open and its position
 * is within the current map viewport
 * @private
 */
GSInfoWindow2.prototype.canAnchorOnInfoWindow = function() {
    return this.isOpen && this.map.contains(this.coordinate);
};

/**
 * Displays the specified HTML markup in the info window at the given
 * <code>x</code> and <code>y</code> coordinates
 * @param {GSPoint} coordinate the coordinate to position the info window at (NZMG)
 * @param {Mixed} html the HTML content to display in the info window, may either be a string or an HTML DOM node
 * @param {Object} [options] an object literal specifying content and display options
 * for the info window. The following parameters may be specified:
 * @config {GSPoint} offset pixel offset from the specified coordinate to position the info window at
 * @config {String} title the text to be displayed in the info window title bar
 * @config {boolean} maximizable if <code>true</code> a <strong>maximize</strong> button will be provided for maximizing
 * the info window
 * @config {Mixed} maxContent a DOM node or HTML string to be displayed when the info window is maximized
 * @config {GSDimension} maxSize the size the info window body should be maximized to. If not specified will resize to
 * relative to the map window up to the internal maximum size (1000 x 800px)
 * @config {Function} onopen a function to be called when the info window is opened
 * @config {Function} onclose a function to be called when the info window is closed
 * </ul>
 */
GSInfoWindow2.prototype.open = function(coordinate, html, options) {
    this.options = {
    maximizable: false,
    maxContent: '',
    maxSize: new GSDimension(GSInfoWindow2.MAX_BODY_WIDTH, GSInfoWindow2.MAX_BODY_HEIGHT)
    };
    GSUtil.merge(options, this.options);

    // if already open, call onclose method
    if(this.isOpen) {
        this.close();
    }

    this.isOpen = true;

    this.onclose = this.options.onclose;

    // set positioning state
    this.coordinate = coordinate;
    if(this.options.offset) {
        this.offset = this.options.offset;
    }

    // set the info window content, converting if necessary
    if((typeof html).toLowerCase() == 'string') {
        var div = GSUtil.createElement('div');
        div.innerHTML = html;
        html = div;
    }    
    this.setContent(html);

    // set title
    this.setTitle(this.options.title);

    // show maximize button if enabled
    if(this.options.maximizable) {
        if((typeof this.options.maxContent).toLowerCase() == 'string') {
            this.maxContent = document.createElement('div');
            this.maxContent.innerHTML = this.options.maxContent;
        } else {
            this.maxContent = this.options.maxContent;
        }   
        this.buttons.maximizeBtn.style.display = 'block';
    } else {
        this.buttons.maximizeBtn.style.display = 'none';
    }

    // update the info window's position
    var map = this.map;
    var onUpdate = GSUtil.bind(function() {
            if(this.options.onopen) {
                this.options.onopen();
            }
            map.broadcastMessage("infoWindowOpened", this);
        }, this);
    this.updatePosition(onUpdate);
};

/**
 * Returns a <code>GSPoint</code> instance that specifies the minimum
 * offset from the top and right of the map viewport that can be applied
 * to the info window
 * @private
 * @return {GSPoint} the edge constraint
 */
GSInfoWindow2.prototype.getEdgeConstraint = function() {
    if(!this.map) {
        return new GSPoint(0, 0);
    }
    var constraint = {top: 5, right: 5, bottom: 5, left: 5};
    for(var i = 0, l = this.map.controls.length; i < l; i++) {
        var control = this.map.controls[i];
        var width = control.getWidth();
        var height = control.getHeight();
        if(width && height) {
            var orientation = control.getOrientation() ? control.getOrientation() : (width > height ? 'horizontal' : 'vertical');
            var pos = control.getPosition();
            switch(pos.anchor) {
            case GSControl.ANCHOR_TOP_LEFT:
                if(orientation == 'horizontal') {
                    constraint.top = Math.max(constraint.top, 5 + pos.offset.y + height);                    
                } else {
                    constraint.left = Math.max(constraint.left, 5 + pos.offset.x + width);
                }
                break;
            case GSControl.ANCHOR_BOTTOM_LEFT:
                if(orientation == 'horizontal') {
                    constraint.bottom = Math.max(constraint.bottom, 5 + pos.offset.y + height);                                  
                } else {
                    constraint.left = Math.max(constraint.left, 5 + pos.offset.x + width);
                }
                break;
            case GSControl.ANCHOR_TOP_RIGHT:
                if(orientation == 'horizontal') {
                    constraint.top = Math.max(constraint.top, 5 + pos.offset.y + height);
                } else {
                    constraint.right = Math.max(constraint.right, 5 + pos.offset.x + width);
                }
                break;
            case GSControl.ANCHOR_BOTTOM_RIGHT:
                if(orientation == 'horizontal') {
                    constraint.bottom = Math.max(constraint.bottom, 5 + pos.offset.y + height);
                } else {
                    constraint.right = Math.max(constraint.right, 5 + pos.offset.x + width);
                }
                break;
            }
        }
    }
    return constraint;
};

/**
 * Updates the dimensions and position of the info window. Should be explicitly
 * called after the info window's content has been changed
 */
GSInfoWindow2.prototype.update = function() {
    this.updateSize();
    this.updatePosition();
};

/**
 * @private
 */
GSInfoWindow2.prototype.updateSize = function(bodySize) {    
    this.element.style.width = 'auto'; // remove previous width setting
    
    if(!bodySize) bodySize = new GSDimension();
    var bodyWidth = bodySize.width || Math.max(this.body.offsetWidth, GSInfoWindow2.MIN_BODY_WIDTH);
    var bodyHeight = bodySize.height || Math.max(this.body.offsetHeight, GSInfoWindow2.MIN_BODY_HEIGHT);
    var width = bodyWidth + ((GSInfoWindow2.H_BODY_MARGIN) * 2);
    var height = bodyHeight + GSInfoWindow2.TITLE_HEIGHT + 10;

    this.element.style.width = width + 'px';
    this.element.style.height = height + 'px';
    this.strut.style.height = (bodyHeight + 10) + 'px';
    this.callout.style.left = ((width - GSInfoWindow2.CALLOUT_WIDTH) / 2) + 'px'; 

    // if size of info window has changed will need to reposition
    this.reposition();
};

/**
 * @private
 */
GSInfoWindow2.prototype.updatePosition = function(onUpdate) {

    // minimum buffer between the popup and the map edge
    var edgeConstraint = this.getEdgeConstraint();
    
    var dim = this.getSize();
    var iwWidth = dim.width;
    var iwHeight = dim.height;
    
    var x = this.offset.x;
    var y = this.offset.y - (iwHeight + GSInfoWindow2.CALLOUT_HEIGHT/*remove info window callout height*/);
    var offset = new GSPoint(x, y);

    var centerX, centerY;
    var pos = this.map.translateToMapCoordinate(this.coordinate);
    if(this.coordinate.x > this.map.minX && this.coordinate.x < this.map.maxX && this.coordinate.y > this.map.minY && this.coordinate.y < this.map.maxY) { // coordinate is within map extents
        var left = pos.x + offset.x;
        var top = pos.y + offset.y;

        var deltaX = 0;
        var deltaY = 0;

        if((left + (iwWidth/2) + edgeConstraint.right) > this.map.pxWidth) { // too far right
            deltaX = this.map.pxWidth - (left + (iwWidth/2) + edgeConstraint.right);
        } else if((left - (iwWidth/2) - edgeConstraint.left) < 0) { // too far left
            deltaX = ((iwWidth/2) + edgeConstraint.left) - left;
        }

        if((top - edgeConstraint.top) < 0) {
            deltaY = 0 - (top - edgeConstraint.top);
        } else if((top + iwHeight + GSInfoWindow2.CALLOUT_HEIGHT + edgeConstraint.bottom) > this.map.pxHeight) { // too far down
            deltaY = this.map.pxHeight - (top + iwHeight + GSInfoWindow2.CALLOUT_HEIGHT + edgeConstraint.bottom - this.offset.y);
        }

        // if info window can be displayed within the map extents show it
        if(deltaX == 0 && deltaY == 0) {
            if(onUpdate) {
                onUpdate();
            }
            return;
        }
        centerX = this.map.centerX - (deltaX * this.map.mpx);
        centerY = this.map.centerY + (deltaY * this.map.mpx);
    } else { // coordinate is outside of map extents, recenter on coordinate
        var deltaX = (this.map.pxWidth / 2) > (offset.x + (iwWidth/2) + edgeConstraint.left) ? 0 : (offset.x + (iwWidth/2) + edgeConstraint.left) - (this.map.pxWidth / 2);
        var deltaY = (this.map.pxHeight / 2) > (Math.abs(offset.y) + edgeConstraint.top) ? 0 : (Math.abs(offset.y) + edgeConstraint.top) - (this.map.pxHeight/ 2);
        
        centerX = this.coordinate.x + (deltaX * this.map.mpx);
        centerY = this.coordinate.y + (deltaY * this.map.mpx);
    }
    
    var map = this.map;
    this.map.panTo(new GSPoint(centerX, centerY), onUpdate);
};

/**
 * Returns the size of the info window
 * @return {GSDimension} the info window size
 */
GSInfoWindow2.prototype.getSize = function() {
    return new GSDimension(this.element.offsetWidth, this.element.offsetHeight);
};

/**
 * Updates the content of the info window. If the info window is not already open this method will do nothing.
 * @param {Mixed} html the HTML content to display in the info window, may either be a string or an HTML DOM node
 */
GSInfoWindow2.prototype.updateContent = function(html) {
    if(!this.isOpen) return;

    if((typeof html).toLowerCase() == 'string') {
        var div = GSUtil.createElement("div");
        div.innerHTML = html;
        html = div;
    }    
    this.setContent(html);
    this.updatePosition();
};

/**
 * Hides this info window
 */
GSInfoWindow2.prototype.close = function(e) {
    if(e) {
        GSUtil.cancelEvent(e);
    }

    // execute onclose function if defined
    if(this.onclose) {
        this.onclose();
    }

    if(this.maximized) {
        this.restore();
    }

    this.hide();

    this.isOpen = false;
    this.map.broadcastMessage("infoWindowClosed", this);
};

/**
 * Hides the info window without closing it. To reveal the
 * info window call show {@link GSInfoWindow2#show}
 */
GSInfoWindow2.prototype.hide = function() {
    this.element.style.visibility = 'hidden';        
    this.hidden = true;
};

/**
 * Makes the info window visible if currently hidden
 */
GSInfoWindow2.prototype.show = function() {
    this.element.style.visibility = 'visible';        
    this.hidden = false;
};

/**
 * Returns <code>true</code> if the info window is currently hidden.
 * @return {boolean} <code>true</code> if the info window is hidden
 */
GSInfoWindow2.prototype.isHidden = function() {
    return this.hidden;
};

/**
 * Returns the NZMG coordinate the info window is positioned on
 * @return {GSPoint} an NZMG coordinate
 */
GSInfoWindow2.prototype.getCoordinate = function() {
    return this.coordinate;
};

/**
 * Returns the offset, in pixels, of the tip of the info window
 * from the NZMG coordinate at which the info window is positioned
 * @return {GSPoint} a pixel offset
 */
GSInfoWindow2.prototype.getPixelOffset = function() {
    return this.offset;
};

/**
 * Destroys any resources created by the info window
 * @private
 */
GSInfoWindow2.prototype.remove = function() {
    this.map.contentContainer.removeChild(this.element);
    this.element = null;
};

// --------------------------------------------------
// Info Window Listener Interface
// --------------------------------------------------

/**
 * Called when this info window's map instance is resized
 * @private
 */
GSInfoWindow2.prototype.mapResized = function() {
    this.reposition();
};

/**
 * Called when this info window's map instance is zoomed
 * @private
 */
GSInfoWindow2.prototype.mapZoomed = function() {
    this.reposition();
};

/**
 * Called when this info window's map instance is moved
 * @private
 */
GSInfoWindow2.prototype.mapBoundsChanged = function() {
    this.reposition();
};

/**
 * Repositions the info window if it's currently open.
 * @private
 */
GSInfoWindow2.prototype.reposition = function() {
    if(!this.isOpen) return;

    var dim = this.getSize();
    var iwHeight = dim.height;
    var iwWidth = dim.width;
    var pos = this.map.translateToMapCoordinate(this.coordinate);
    var x = pos.x + this.offset.x - (iwWidth/2);
    var y = pos.y + (this.offset.y - (iwHeight + GSInfoWindow2.CALLOUT_HEIGHT/*add info window callout height*/));
    GSUtil.positionElement(this.element, x, y);
    this.show();
};

/**
 * Sets the info window content
 * @private
 * 
 * @param {Element} content the HTML DOM fragment to set as the
 * info window content
 * @param {boolean} resize if <code>true</code> will resize the info window after setting the content
 * @return the existing info window content
 * @type HTMLElement
 */
GSInfoWindow2.prototype.setContent = function(content, resize) {
    if(resize === undefined) {resize = true;}
    content.className = 'info-window';
    var oldContent = null;
    if(this.body.firstChild) {
        oldContent = this.body.replaceChild(content, this.body.firstChild);    
    } else {
        this.body.appendChild(content);
    }
    if(resize) {
        this.updateSize();
    }
    return oldContent;
};

/**
 * Creates the info window foreground
 * @private
 */
GSInfoWindow2.prototype.createWindow = function() {
    this.element = this.createElement('div', {
        styles: {position: 'absolute', visibility: 'hidden', cursor: 'default', zIndex: 1000}
        });
    GSEventManager.addEventListener(this.element, 'contextmenu', GSUtil.cancelEvent);
    this.map.contentContainer.appendChild(this.element);

    var frame = this.createElement('div', {
        styles: {position: 'absolute', top: '0px', left: '0px', width: '100%', height: '100%'}
        });
    this.element.appendChild(frame);

    // top
    var tl = this.createElement('div', {
        styles: {padding: '0 0 0 9px', background: 'url(' +
                    _globals.resourceURL + 'iw2/hd-sprite.gif) no-repeat left top'}
        });
    frame.appendChild(tl);
    var tr = this.createElement('div', {
        styles: {padding: '0 9px 0 0', background: 'url(' +  _globals.resourceURL + 'iw2/hd-sprite.gif) no-repeat right -24px'}
        });
    tl.appendChild(tr);
    var title = this.createElement('div', {
        attributes: {'class': 'iw-title'},
        styles: {height: '24px', background: 'url(' + _globals.resourceURL + 'iw2/hd-sprite.gif) repeat-x 0px -48px'}
        });
    tr.appendChild(title);
    this.titleText = this.createElement('div', {
        styles: {fontSize: '12px', fontFamily: '"Lucida Grande", Arial, Helvetica, sans-serif',
                    lineHeight: '12px', padding: '5px 0 0 0', textAlign: 'center'}
        });    
    title.appendChild(this.titleText);
    
    // bottom
    var bl = this.createElement('div', {
        styles: {padding: '0 0 0 8px', background: 'url(' +
                    _globals.resourceURL + 'iw2/left.gif) no-repeat left bottom'}
        });
    frame.appendChild(bl);
    var br = this.createElement('div', {
        styles: {padding: '0 8px 0 0', background: 'url(' +  _globals.resourceURL + 'iw2/right.gif) no-repeat right bottom'}
        });
    bl.appendChild(br);
    this.strut = this.createElement('div', {
        attributes: {'class': 'iw-strut'},
        styles: {height: '70px', background: 'url(' +  _globals.resourceURL + 'iw2/mid.gif) repeat-x center bottom'}
        });
    br.appendChild(this.strut);
    
    // callout
    this.callout = this.createElement('div', {
        attributes: {'class': 'callout'},
                styles: {width: '24px', height: '42px', background: 'url(' +  _globals.resourceURL + 'iw2/callout.gif) no-repeat', position: 'absolute', bottom: ((GSInfoWindow2.CALLOUT_HEIGHT + 1) * -1) + 'px'}
        });
    GSUtil.makeUnselectable(this.callout);
    frame.appendChild(this.callout);

    // content
    this.body = this.createElement('div', {
        attributes: {'class': 'iw-body'},
        styles: {position: 'absolute', top: '28px', background: '#fff', margin: '0px ' + GSInfoWindow2.H_BODY_MARGIN + 'px'}
        });
    this.element.appendChild(this.body);

    // maximize/restore button
    this.buttons.maximizeBtn = this.createElement('a', {
        attributes: {href: '#', title: 'Maximize'},
                styles: {display: 'block', overflow: 'hidden', position: 'absolute', right: '22px', top: '5px', width: '14px', height: '14px', background: 'url(' + _globals.resourceURL + 'iw2/buttons.gif) no-repeat -14px 0px', cursor: 'pointer'}
        });
    GSEventManager.addEventListener(this.buttons.maximizeBtn, 'click', GSUtil.bindAsEventListener(this.maximize, this));
    GSEventManager.addEventListener(this.buttons.maximizeBtn, 'mouseover', GSUtil.bindAsEventListener(this.maximizeBtnOver, this.buttons.maximizeBtn));
    GSEventManager.addEventListener(this.buttons.maximizeBtn, 'mouseout', GSUtil.bindAsEventListener(this.maximizeBtnOut, this.buttons.maximizeBtn));
    this.element.appendChild(this.buttons.maximizeBtn);

    this.buttons.restoreBtn = this.createElement('a', {
        attributes: {href: '#', title: 'Restore'},
                styles: {display: 'none', overflow: 'hidden', position: 'absolute', right: '22px', top: '5px', width: '14px', height: '14px', background: 'url(' + _globals.resourceURL + 'iw2/buttons.gif) no-repeat -28px 0px', cursor: 'pointer'}
        });
    GSEventManager.addEventListener(this.buttons.restoreBtn, 'click', GSUtil.bindAsEventListener(this.restore, this));
    GSEventManager.addEventListener(this.buttons.restoreBtn, 'mouseover', GSUtil.bindAsEventListener(this.restoreBtnOver, this.buttons.restoreBtn));
    GSEventManager.addEventListener(this.buttons.restoreBtn, 'mouseout', GSUtil.bindAsEventListener(this.restoreBtnOut, this.buttons.restoreBtn));
    this.element.appendChild(this.buttons.restoreBtn);

    // close button
    this.buttons.closeBtn = this.createElement('a', {
        attributes: {href: '#', title: 'Close'},
                styles: {display: 'block', overflow: 'hidden', position: 'absolute', right: '5px', top: '5px', width: '14px', height: '14px', background: 'url(' + _globals.resourceURL + 'iw2/buttons.gif) no-repeat 0px 0px', cursor: 'pointer'}
        });
    GSEventManager.addEventListener(this.buttons.closeBtn, 'click', GSUtil.bindAsEventListener(this.close, this));
    GSEventManager.addEventListener(this.buttons.closeBtn, 'mouseover', GSUtil.bindAsEventListener(this.closeBtnOver, this.buttons.closeBtn));
    GSEventManager.addEventListener(this.buttons.closeBtn, 'mouseout', GSUtil.bindAsEventListener(this.closeBtnOut, this.buttons.closeBtn));
    this.element.appendChild(this.buttons.closeBtn);
};

/**
 * @private
 */
GSInfoWindow2.prototype.createElement = function(name, options) {
    if(!options) {options = {};}
    if(!options.attributes) {options.attributes = {};}
    if(!options.styles) {options.styles = {};}
    var element = null;
    if(document.createElementNS) {
        element = document.createElementNS(_globals.xmlns, name);
    } else {
        element = document.createElement(name);
    }
    for(var i in options.attributes) {
        if(element.setAttributeNS) {
            element.setAttributeNS(null, i, options.attributes[i]);
        } else {
            element.setAttribute(i, options.attributes[i]);
        }
    }
    for(var i in options.styles) {
        element.style[i] = options.styles[i];
    }
    return element;
};

/**
 * Event handler for info window maxmize action
 */
GSInfoWindow2.prototype.maximize = function(e) {
    if(e) {
        GSUtil.cancelEvent(e);
    }

    if(this.maximized) {return;}

    // store the size of the info window body for minimizing to later
    this.restoreSize = new GSDimension(this.body.offsetWidth, this.body.offsetHeight);

    var f = GSUtil.bind(function() {
            this.map.hideControls();

            this.sizeToMapWindow();

            this.buttons.maximizeBtn.style.display = 'none';
            this.buttons.restoreBtn.style.display = 'block';

            this.restoreContent = this.setContent(this.maxContent, false);

            this.maximized = true;            

            map.broadcastMessage('infoWindowMaximized', this);
    }, this);

    var point = new GSPoint(Math.round(this.coordinate.x),
                            Math.round(this.map.centerY + (this.coordinate.y - this.map.minY) - (this.getEdgeConstraint().top * this.map.mpx)));
    var mapCenter = this.map.getMapCenter();
    var deltaX = Math.abs(point.x / this.map.mpx - mapCenter.x / this.map.mpx);
    var deltaY = Math.abs(point.y / this.map.mpx - mapCenter.y / this.map.mpx);
    if(deltaX < 20 && deltaY < 20) {
        this.map.centerAtCoordinate(point, f);
    } else {
        this.map.panTo(point, f);    
    }
};

/**
 * Event handler for info window restore action
 */
GSInfoWindow2.prototype.restore = function(e) {
    if(e) {
        GSUtil.cancelEvent(e);
    }
    if(!this.maximized) {return;}

    this.body.replaceChild(this.restoreContent, this.maxContent);

    this.updateSize(this.restoreSize);

    this.buttons.restoreBtn.style.display = 'none';
    this.buttons.maximizeBtn.style.display = 'block';

    this.map.showControls();

    this.maximized = false;

    map.broadcastMessage('infoWindowRestored', this);
};

/**
 * Called in response to browser window resize events to enable a maximized info window to be
 * resized to accommodate window size changes
 * @private
 */
GSInfoWindow2.prototype.windowResizeHandler = function() {
    if(this.maximized) {
        var point = new GSPoint(Math.round(this.coordinate.x), Math.round(this.map.centerY + (this.coordinate.y - this.map.minY) - (this.getEdgeConstraint().top * this.map.mpx)));
        var mapCenter = this.map.getMapCenter();
        var deltaX = Math.abs(point.x / this.map.mpx - mapCenter.x / this.map.mpx);
        var deltaY = Math.abs(point.y / this.map.mpx - mapCenter.y / this.map.mpx);
        if(deltaX < 20 && deltaY < 20) {
            this.map.centerAtCoordinate(point, GSUtil.bind(this.sizeToMapWindow, this));
        } else {
            this.map.panTo(point, GSUtil.bind(this.sizeToMapWindow, this));    
        }
    }
};

/**
 * Sets the info window title
 * @param {String} title the text to set as the info window title
 * @private
 */
GSInfoWindow2.prototype.setTitle = function(title) {
    while(this.titleText.firstChild) {
        this.titleText.removeChild(this.titleText.firstChild);
    }
    var text = title ? this.truncateTitle(title) : '';
    this.titleText.appendChild(document.createTextNode(text));
};

/**
 * @private
 */
GSInfoWindow2.prototype.truncateTitle = function(title) {
    var maxChars = Math.floor(this.element.offsetWidth / 7) - 3;
    if(title.length <= maxChars) {
        return title;
    }
    return title.substr(0, maxChars) + '...';
};

/**
 * Resizes the info window to fit the map window
 * @private
 */
GSInfoWindow2.prototype.sizeToMapWindow = function() {
    var edgeConstraint = this.getEdgeConstraint();
    var w = this.map.pxWidth - (edgeConstraint.left * 2);
    w = w < this.options.maxSize.width ? w : this.options.maxSize.width;
    var h = this.map.pxHeight - (edgeConstraint.top * 2) - GSInfoWindow2.CALLOUT_HEIGHT + this.offset.y;
    h = h < this.options.maxSize.height ? h : this.options.maxSize.height;

    this.element.style.width = w + 'px';
    this.element.style.height = h + 'px';            
    this.strut.style.height = (h - GSInfoWindow2.TITLE_HEIGHT) + 'px';
    this.callout.style.left = ((w - GSInfoWindow2.CALLOUT_WIDTH) / 2) + 'px'; 
            
    this.reposition();    
};

/**
 * Event handler for close button mouse over state
 * @private
 */
GSInfoWindow2.prototype.closeBtnOver = function(e) {
    this.style.backgroundPosition = '0px -14px';
};

/**
 * Event handler for close button mouse out state
 * @private
 */
GSInfoWindow2.prototype.closeBtnOut = function(e) {
    this.style.backgroundPosition = '0px 0px';
};

/**
 * Event handler for maximize button mouse over state
 * @private
 */
GSInfoWindow2.prototype.maximizeBtnOver = function(e) {
    this.style.backgroundPosition = '-14px -14px';
};

/**
 * Event handler for maximize button mouse out state
 * @private
 */
GSInfoWindow2.prototype.maximizeBtnOut = function(e) {
    this.style.backgroundPosition = '-14px 0px';
};

/**
 * Event handler for restore button mouse over state
 * @private
 */
GSInfoWindow2.prototype.restoreBtnOver = function(e) {
    this.style.backgroundPosition = '-28px -14px';
};

/**
 * Event handler for restore button mouse out state
 * @private
 */
GSInfoWindow2.prototype.restoreBtnOut = function(e) {
    this.style.backgroundPosition = '-28px 0px';
};

// --------------------------------------------------
// GSLogger
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2007
//
// --------------------------------------------------

/**
 * Constructs a new <code>GSLogger</code>
 * @class GSLogger logs map events to a server-side logging service
 * @throw an error if the user query parameter was not supplied in the API script URL
 * @private
 */
function GSLogger(map) {
    if(!_globals.loggingEnabled) {return; }
    this.map = map;
    this.sessionid;
    this.user = this.getAPIUser();
    this.version = this.getAPIVersion();
    this.lastlogtime;
    this.transport = new Image();
    if(!this.user) {
        throw new Error('Cannot log, user parameter not provided in API query');
    }
    this.startSession();
}

/**
 * Start a new logging session. This function attempts to get a sessionid
 * from the 'gssessionid' cookie. If the cookie does not exist a new
 * pseudo UUID is created and stored in the 'gssessionid' cookie.
 * @private
 */
GSLogger.prototype.startSession = function() {    
    var sessionid = this.getSessionIdFromCookie();
    if(sessionid) {
        this.sessionid = sessionid;
    } else {
        this.sessionid = this.uuid();
        if(GSCookie.accept()) {
            GSCookie.set('gssessionid', this.sessionid);
        }
    }
};

/**
 * Removes the 'gssessionid' cookie and unsets the sessionid
 * stored on this logger
 * @private
 */
GSLogger.prototype.stopSession = function() {
    GSCookie.erase('gssessionid');
    this.sessionid = undefined;
};

/**
 * Attempts to get the API user name from the script tag that
 * imports the Maps API
 * @private
 * @return the API user name
 * @type String
 * @throw an error if the API was loaded from a non-Geosmart domain
 */
GSLogger.prototype.getAPIUser = function() {
    var scriptTag = this.getAPIScriptTag();
    if(!scriptTag) {
        throw new Error('Cannot log, API loaded from a non-Geosmart domain');
    }
    var regExp = /user=([^&]+)(&|$)/;
    var user = scriptTag.src.match(regExp)[1];
    return user;
};

/**
 * Attempts to get the version string from the script tag that
 * imports the Maps API
 * @private
 * @return the API version string
 * @type String
 */
GSLogger.prototype.getAPIVersion = function() {
    var scriptTag = this.getAPIScriptTag();
    if(!scriptTag) {
        throw new Error('Cannot log, API loaded from a non-Geosmart domain');
    }
    var regExp = /&v=([^&]+)(&|$)/;
    var version = scriptTag.src.match(regExp)[1];
    return version;
};

/**
 * Returns the HTML script tag referencing the Maps API, or <code>undefined</code>
 * if it cannot be found
 * @private
 * @return the API script tag
 * @type HTMLElement
 */
GSLogger.prototype.getAPIScriptTag = function() {
    var scripts = document.getElementsByTagName('script');
    var regExp = /api\.geosmart\.co\.nz/;
    for(var i = 0, l = scripts.length; i < l; i++) {
        var script = scripts[i];
        if(regExp.test(script.src)) {
            return script;
        }
    }
    return undefined;
};

/**
 * Sends a log message to the server-side logger
 * <p>
 * <strong>Examples:</strong>
 * </p>
 * <pre>
 * this.logger.log('map', {mapOp: 'zoom'});
 * this.logger.log('tiles', {numTiles: 16});
 * </pre>
 * @param {String} log the log that the message should be sent to
 * @param {Object} arg an object literal specifying key value pairs to be logged
 * and value property
 * @throw an error if the <code>log</code> parameter is not specified
 * @private
 */
GSLogger.prototype.log = function(log, arg) {
    if(!_globals.loggingEnabled) {return; }
    if(arguments.length == 1 && (typeof arguments[0]).toLowerCase() != 'string') {
        throw new Error('Cannot log, must name a log file to log to');
    } 

    if(this.lastlogtime && (new Date() - this.lastlogtime > _globals.loggingSessionTimeout)) {
        this.stopSession();
        this.startSession();
    }

    var mapCenter = this.map.getMapCenter();
    arg.center_x = mapCenter.x;
    arg.center_y = mapCenter.y;
    arg.zoom_level = this.map.scaleIdx;
    arg.user = this.user;
    arg.api_version = this.version;
    arg.session_id = this.sessionid;

    var query = '';
    for(var i in arg) {
        if((typeof arg[i]).toLowerCase() == 'function') continue;
        query += (i + '=' + encodeURIComponent(arg[i]) + '&');
    }

    var url = _globals.loggingBaseURL + log + '.log?' + query;
    this.transport.src = url;
    this.lastlogtime = new Date();
};

/**
 * Retrieves the current session id from the 'gssessionid' cookie
 * if it exists, or <code>undefined</code> if it does not
 * @private
 * @return the current session id
 * @type String
 */
GSLogger.prototype.getSessionIdFromCookie = function() {
    if(GSCookie.accept()) {
        return GSCookie.get('gssessionid');
    } else {
        return undefined;
    }
};

/**
 * Generates a pseudo Universally Unique Identifier (UUID)
 * @private
 * @return a UUID
 * @type String
 */
GSLogger.prototype.uuid = function() {
    var result, i, j;
    result = '';
    for(j = 0; j < 32; j++) {
        if( j == 8 || j == 12|| j == 16|| j == 20) result = result + '-';
        i = Math.floor(Math.random()*16).toString(16).toUpperCase();
        result = result + i;
    }
    return result
};

// --------------------------------------------------
// GSRouteLayer
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2007
//
// --------------------------------------------------

/**
 * @fileOverview <code>GSRouteLayer</code> extends <code>GSLayer</code> to provide additional features
 * related to the display of vehicle routes
 * @name GSRouteLayer
 */

GSUtil.extend(GSRouteLayer, GSLayer);

/**
 * <p>
 * <code>GSLayer</code> objects should not be instantiated directly but created through
 * a call to the {@link GSMap} object that will contain the layer:
 * </p>
 * <p>
 * <code>var roadLayer = myMap.createLayer('myRoute', {route: true});</code>
 * </p>
 * <p>
 * By default a newly created layer is visible on the map. This means that calling either
 * the layer's {@link #addFeature} or {@link #addFeatures} method will cause the added
 * features to appear upon the map immediately. In situations where this isn't the desired
 * behaviour the layers {@link #visible} property can be set to <code>false</code>
 * before any features are added.
 * </p>
 * @extends GSLayer
 * @constructor
 * @param {String} name the unique name for this layer
 * @param {GSMap} map the map instance this layer belongs to
 * @param {Object} [options] an object literal specifying options for this layer. The following options
 * may be specified:
 * @config {int} zIndex the zIndex to display this layer at
 * @config {boolean} waypointsVisible <code>true</code> if route points should be displayed upon the map
 * @config {String} directionsServiceUrl the directions service URL. Typically this will refer to a web proxy to avoid disclosing authentication credentials
 * @config {boolean} returnToStart <code>true</code> if the route calculated should use the start point as the end point
 * @config {Object} routeStyle optional properties that effect the display of the route. These are:
 * <ul>
 *  <li><strong>color</strong> - an rgb value expressed as a comma-separated string e.g. 3,74,152
 *  <li><strong>weight</strong> - the line weight in pixels e.g. 5
 *  <li><strong>opacity</strong> - the opacity of the line expressed as an integer from 0 (fully transparent) to 255 (fully opaque)
 * </ul> 
 */
function GSRouteLayer(name, map, options) {    
    if(!options) {options = {};}
    if(!options.routeStyle) {options.routeStyle = {};}

    // Call superclass constructor
    GSRouteLayer.baseConstructor.call(this, name, map, options);

    /**
     * The type of this class as a string value
     * @private
     * @type String
     */
    this.type = 'GSRouteLayer';

    /**
     * <code>true</code> if the route waypoints should be displayed on the map
     * @private
     * @type boolean
     */
    this.waypointsVisible = options.waypointsVisible !== undefined ? options.waypointsVisible : true;

    /**
     * <code>true</code> if the route calculated should use the start point as the end point
     * @private
     * @type boolean
     */
    this.returnToStart = options.returnToStart !== undefined ? options.returnToStart : false;

    /**
     * The color of the route path
     * @private
     * @type String
     */
    this.color = options.routeStyle.color ? options.routeStyle.color : '3,74,152';

    /**
     * The weight of the route path
     * @private
     * @type int
     */
    this.lineWeight = options.routeStyle.weight !== undefined ? options.routeStyle.weight : 5;
    
    /**
     * The opacity of the route path
     * @private
     * @type int
     */
    this.opacity = options.routeStyle.opacity !== undefined ? options.routeStyle.opacity : 255;
    
    /**
     * The Directions Service URL. Typically this will refer to a web proxy to avoid disclosing authentication
     * credentials
     * @private
     * @type String
     */
    this.directionsServiceUrl = options.directionsServiceUrl;
}

/**
 * Adds the specified feature to this layer. 
 * @param {Object} feature the map feature to add
 */
GSRouteLayer.prototype.addFeature = function(feature) {
    this.data.push(feature);
    if(feature.type == 'GSPointFeature') {
        if(this.visible && this.waypointsVisible) {
            feature.setVisible(true);            
        } else {
            feature.setVisible(false);            
        }
    } else {
        feature.setVisible(this.visible);
    }
    feature.addToLayer(this);
    
    // if the feature is snappable add it to the map's
    // list of snappables
    if(feature.snappable) {
        this.map.addSnappable(feature);
    }
};

/**
 * Inserts the specified feature before a reference feature in the list of features
 * maintained by this route layer. If <code>referenceFeature</code> is <code>null</code>,
 * <code>newFeature</code> is inserted at the end of the list of features
 * @param {GSPointFeature} newFeature the new feature to add to this route layer
 * @param {GSPointFeature} referenceFeature the feature to insert new feature before
 * @see #insertFeatureAfter
 */
GSRouteLayer.prototype.insertFeatureBefore = function(newFeature, referenceFeature) {
    for (var i = 0, l = this.data.length; i < l; i++) {
        if (referenceFeature == this.data[i]) {
            this.data.splice(i, 0, newFeature);
            return;
        }
    }
    this.data.push(newFeature);
};

/**
 * Inserts the specified feature after a reference feature in the list of features
 * maintained by this route layer. If <code>referenceFeature</code> is <code>null</code>,
 * <code>newFeature</code> is inserted at the end of the list of features
 * @param {GSPointFeature} newFeature the new feature to add to this route layer
 * @param {GSPointFeature} referenceFeature the feature to insert new feature after
 * @see #insertFeatureBefore
 */
GSRouteLayer.prototype.insertFeatureAfter = function(newFeature, referenceFeature) {
    for (var i = 0, l = this.data.length; i < l; i++) {
        if (referenceFeature == this.data[i]) {
            this.data.splice(i + 1, 0, newFeature);
            return;
        }
    }
    this.data.push(newFeature);
};

/**
 * Hides the route waypoints
 */
GSRouteLayer.prototype.hideWaypoints = function() {
    this.waypointsVisible = false;
    this.setVisible(this.visible);
};

/**
 * Shows the route waypoints
 */
GSRouteLayer.prototype.showWaypoints = function() {
    this.waypointsVisible = true;
    this.setVisible(this.visible);
};

/**
 * Gets the directions service url
 * @return {String} the directions service url
 */
GSRouteLayer.prototype.getDirectionsServiceUrl = function() {
    return this.directionsServiceUrl;
};

/**
 * Sets the directions service url
 * @param {String} directionsServiceUrl the directions service url to set
 */
GSRouteLayer.prototype.setDirectionsServiceUrl = function(directionsServiceUrl) {
    this.directionsServiceUrl = directionsServiceUrl;
};

/**
 * Sets the <code>returnToStart</code> option for the route
 * @param {boolean} returnToStart <code>true</code> if the route start point should
 * also be used as its end point
 */
GSRouteLayer.prototype.setReturnToStart = function(returnToStart) {
    this.returnToStart = returnToStart;
};

/**
 * Calculates and displays a route between the point features added to this layer
 */
GSRouteLayer.prototype.calculateRoute = function() {
    if(!this.directionsServiceUrl) {
        throw 'Must set directions service URL before calculating route.';
    }
    var numPoints = 0;
    for(var i = 0, l = this.data.length; i < l; i++) {
        if(this.data[i].type == 'GSPointFeature') {
            numPoints++;
        }
    }
    if(numPoints < 2) { // don't do anything if insufficient points to calculate a route
        if(this.routePath) {            
            this.routePath.remove();
            this.routePath = null;
        }
        return;
    }
    var callback = GSUtil.bind(this.imageCallback, this);
    if(!this.routePath) {
        this.routePath = new GSRasterImageFeature(callback);
        this.addFeature(this.routePath);
    } else {
        this.routePath.callback = callback;
        this.routePath.render();
    }
};

/**
 * Removes the route display from the layer
 */
GSRouteLayer.prototype.removeRoute = function() {
    if(this.routePath) {
        this.removeFeature(this.routePath);
        this.routePath = undefined;
    }
};

/**
 * Called in response to map state changes
 * @private
 */
GSRouteLayer.prototype.imageCallback = function(zoomLevel, bounds, dimension) {
    var url = (this.directionsServiceUrl.indexOf('?') != -1 ? this.directionsServiceUrl + '&' : this.directionsServiceUrl + '?') +
    'stops=' + this.serializeStops();
    url += "&xmin=" + bounds.minX + "&ymin=" + bounds.minY + "&xmax=" + bounds.maxX + "&ymax=" + bounds.maxY;
    url += "&width=" + dimension.width + "&height=" + dimension.height;
    url += "&color=" + encodeURIComponent(this.color) + "&weight=" + this.lineWeight + "&opacity=" + this.opacity;
    return url;
};

/**
 * Serializes the point features added to this layer to a format suitable for
 * sending to the Directions Service
 */
GSRouteLayer.prototype.serializeStops = function() {
    var buffer = [];
    for(var i = 0, l = this.data.length; i < l; i++) {
        var feature = this.data[i];
        if(feature.type == 'GSPointFeature') {
            buffer.push(feature.coordinate.x + ':' + feature.coordinate.y + ':' + feature.name);
        }
    }
    if(this.returnToStart) {
        buffer.push(this.data[0].coordinate.x + ':' + this.data[0].coordinate.y + ':' + this.data[0].name);
    }
    return encodeURIComponent(buffer.join(','));
};

/**
 * Removes all features from this layer
 */
GSRouteLayer.prototype.clear = function() {
    GSRouteLayer.superClass.clear.call(this); 
    this.routePath = undefined;
};

/**
 * Sets the visibility of the features contained by this layer
 *
 * @param {boolean} visible <code>true</code> if this layer's features should be
 * visible
 */
GSRouteLayer.prototype.setVisible = function(visible) {
    this.visible = visible;
    for(var i = 0, l = this.data.length; i < l; i++) {
        var feature = this.data[i];
        if(feature.type == 'GSPointFeature') {
            if(visible && this.waypointsVisible) {
                feature.setVisible(true);            
            } else {
                feature.setVisible(false);            
            }
        } else {
            feature.setVisible(visible);            
        }
        feature.render();
    }
};

/**
 * Outputs a JSON representation of this layer
 * @return {String} a JSON string
 */
GSRouteLayer.prototype.toJson = function() {
    var obj = {
        type: this.type,
        zIndex: this.zIndex,
        visible: this.visible,
        waypointsVisible: this.waypointsVisible,
        routeStyle: {
            color: this.color,
            weight: this.lineWeight,
            opacity: this.opacity
        }
    };
    var features = [];
    for(var i = 0, l = this.data.length; i < l; i++) {
        var feature = this.data[i];
        if(feature.type != 'GSRasterImageFeature') {
            features.push(feature);
        }
    }
    obj.feature = features;
    return GSUtil.objToJson(obj);
};
// --------------------------------------------------
// GSCookie
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2007
//
// This class was sourced from the Dojo project I think
// --------------------------------------------------

var GSCookie = {
  set: function(name, value, daysToExpire) {
    var expire = '';
    if (daysToExpire != undefined) {
      var d = new Date();
      d.setTime(d.getTime() + (86400000 * parseFloat(daysToExpire)));
      expire = '; expires=' + d.toGMTString();
    }
    return (document.cookie = escape(name) + '=' + escape(value || '') + expire);
  },
  get: function(name) {
    var cookie = document.cookie.match(new RegExp('(^|;)\\s*' + escape(name) + '=([^;\\s]*)'));
    return (cookie ? unescape(cookie[2]) : null);
  },
  erase: function(name) {
    var cookie = GSCookie.get(name) || true;
    GSCookie.set(name, '', -1);
    return cookie;
  },
  accept: function() {
    if (typeof navigator.cookieEnabled == 'boolean') {
        if(document.cookie !== undefined) {
            return navigator.cookieEnabled;
        } else {
            return false;
        }
    }
    GSCookie.set('_test', '1');
    return (GSCookie.erase('_test') === '1');
  }
};
/**
 * @fileOverview A context menu for the map
 * @name GSContextMenu
 */

/**
 * @class A UI control providing contextual actions when actuated by
 * a right-click on the map
 * @param {Object} [options] an object literal specifying the menu item options to set:
 * @config {String} fontFamily a prioritized list of font family names and/or generic family names used to set the
 * style of the menu item label text e.g. Lucida Grande,Helvetica,Sans-serif
 * @config {String} fontSize sets the size of the menu item label text e.g. 12px
 * @config {String} highlightColor the color of the text when the menu item is highlighted
 * @config {String} highlightBackgroundColor the color of the menu item background when a menu item is highlighted
 * @config {String} highlightBackgroundImage an image to display for the menu item background when a menu item is highlighted
 * @constructor
 */
function GSContextMenu(options) {
    this.options = options ? options : {};
    this.edgeOffset = 5;
    this.menuItems = [];
    this.selectedIndex = -1;
    this.eventListeners = {};
    this.element;
    this.map;
    this.onmap = false;
    this.showing = false;
    this.initialize();
}

/**
 * Adds this context menu to the specified map instance
 * @param {GSMap} map the map instance
 * @private
 */
GSContextMenu.prototype.addToMap = function(map) {
    this.map = map;
    map.container.appendChild(this.element);
    this.onmap = true;
};

/**
 * Returns this context menu's menu items
 * @return an array of menu items
 * @type Array
 */
GSContextMenu.prototype.getItems = function() {
    return this.menuItems;
};

/**
 * Adds a menu item to the context menu after any existing menu items
 * @param {GSMenuItem} menuItem the menu item to add to the context menu
 */
GSContextMenu.prototype.addItem = function(menuItem) {    
    menuItem.parent = this;
    this.menuItems.push(menuItem);
    menuItem.addToMenu(this);
};

/**
 * Adds a menu item to the context menu at the position specified by the index
 * parameter (zero-based)
 * @param {GSMenuItem} menuItem the menu item to add to the context menu
 * @param {int} index the position to add the menu item at
 */
GSContextMenu.prototype.addItemAt = function(menuItem, index) {
    menuItem.parent = this;
    this.menuItems.splice(index, 0, menuItem);
    menuItem.addToMenu(this, index);
};

/**
 * Removes the specified menu item from this context menu
 * @param {GSMenuItem} menuItem the menu item to remove from the context menu
 */
GSContextMenu.prototype.removeItem = function(menuItem) {
    for(var i = 0, l = this.menuItems.length; i < l; i++) {
        if(this.menuItems[i] === menuItem) {
            this.menuItems.splice(i, 1)[0].remove();
            return;
        }
    }
};

/**
 * Removes the menu item at the specified index (zero-based)
 * @param {int} index the position of the menu item to remove
 */
GSContextMenu.prototype.removeItemAt = function(index) {
    this.menuItems.splice(index, 1)[0].remove();
};

/**
 * Hides the context menu
 */
GSContextMenu.prototype.hide = function() {
    this.broadcastMessage("contextMenuHide", this);

    this.element.style.visibility = 'hidden';

    this.unsubscribeForKeyEvents();
    
    this.selectedIndex = -1;
    this.updateItemsSelection();

    GSContextMenuManager.contextMenuHidden(this);

    this.showing = false;
};

/**
 * Shows the context menu
 * @param {GSPoint} [position] the pixel offset relative to the top-left corner of the map viewport to position the context menu
 * at. If not specified will show the context menu at its current position
 * @param {mixed} [target] the API component that triggered the show method on the context menu
 */
GSContextMenu.prototype.show = function(position, target) {
    this.broadcastMessage("contextMenuShow", this, target);

    if(position) {
        this.position = position;

        var size = [this.element.offsetWidth, this.element.offsetHeight];

        // position top or bottom?
        var vPos = this.position.y + size[1] + this.edgeOffset > this.map.pxHeight ? 'bottom' : 'top';
        
        // position left or right?
        var hPos = this.position.x + size[0] + this.edgeOffset > this.map.pxWidth ? 'right' : 'left';

        if(vPos == 'top') {
            this.element.style.top = this.position.y + 'px';
            this.element.style.bottom = 'auto';
        } else {
            this.element.style.top = 'auto';
            this.element.style.bottom = (this.map.pxHeight - this.position.y) + 'px';;
        }

        if(hPos == 'left') {
            this.element.style.left = this.position.x + 'px';
            this.element.style.right = 'auto';
        } else {
            this.element.style.left = 'auto';
            this.element.style.right = (this.map.pxWidth - this.position.x) + 'px';
        }
    }
    this.element.style.visibility = 'visible';

    this.subscribeToKeyEvents();

    this.showing = true;

};

/**
 * Enables this menu to respond to key events
 * @private
 */
GSContextMenu.prototype.subscribeToKeyEvents = function() {
    this.eventListeners['keydown'] =
        GSEventManager.addEventListener(document, 'keydown', GSUtil.bindAsEventListener(this.keyHandler, this));
};

/**
 * Disables this menu from responding to key events
 * @private
 */
GSContextMenu.prototype.unsubscribeForKeyEvents = function() {
    GSEventManager.removeEventListener(this.eventListeners['keydown']);
};

/**
 * Processes key events this menu is interested in
 * @param {Event} e the key event to process
 * @private
 */
GSContextMenu.prototype.keyHandler = function(e) {
    switch(e.keyCode) {
    case GSKeyEvents.KEY_ESC:
        this.hide();
        return;
    case GSKeyEvents.KEY_RETURN:
        this.dispatchClickEvent(this.menuItems[this.selectedIndex]);
        return;
    case GSKeyEvents.KEY_UP:
        this.selectPrevious();
        return;
    case GSKeyEvents.KEY_DOWN:
        this.selectNext();
        return;
    }
};

/**
 * Selects the next menu item
 * @private
 */
GSContextMenu.prototype.selectNext = function() {
    if(this.selectedIndex < this.menuItems.length - 1) {
        var selectedIndex = this.selectedIndex;
        do {
            selectedIndex += 1;
        } while(!this.menuItems[selectedIndex].isFocusTraversable() && selectedIndex < this.menuItems.length - 1);
        if(this.menuItems[selectedIndex].isFocusTraversable()) {
            this.selectedIndex = selectedIndex;
        }
        this.updateItemsSelection();
    }
};

/**
 * Selects the previous menu item
 * @private
 */
GSContextMenu.prototype.selectPrevious = function() {
    if(this.selectedIndex > 0) {
        var selectedIndex = this.selectedIndex;
        do {
            selectedIndex -= 1;
        } while(!this.menuItems[selectedIndex].isFocusTraversable() && selectedIndex > 0);
        if(this.menuItems[selectedIndex].isFocusTraversable()) {
            this.selectedIndex = selectedIndex;
        }
        this.updateItemsSelection();
    }
};

/**
 * Selects the specified menu item
 * @param {GSMenuItem} menuItem the menu item to select
 */
GSContextMenu.prototype.selectItem = function(menuItem) {
    for(var i = 0, l = this.menuItems.length; i < l; i++) {
        if(this.menuItems[i] === menuItem) {
            this.selectedIndex = i;
            this.updateItemsSelection();
            return;
        }
    }
};

/**
 * Dispatches a click event to the specified menu item
 * @param {GSMenuItem} menuItem the menu item to dispatch the click event to
 * @private
 */
GSContextMenu.prototype.dispatchClickEvent = function(menuItem) {
    var e = document.createEvent('MouseEvents');
    e.initEvent('click', true, true);
    menuItem.element.dispatchEvent(e);
};

/**
 * Updates the selection state of the menu items
 * @private
 */
GSContextMenu.prototype.updateItemsSelection = function() {
    for(var i = 0, l = this.menuItems.length; i < l; i++) {
        if(i == this.selectedIndex) {
            this.menuItems[i].select();   
        } else {
            this.menuItems[i].deselect();   
        }
    }            
};

/**
 * Initializes the context menu, called once when the context menu is
 * created
 * @private
 */
GSContextMenu.prototype.initialize = function() {
    this.element = document.createElement('div');
    this.element.className = 'gs_contextmenu';
    with(this.element.style) {
        visibility = 'hidden';
        position = 'absolute';
        zIndex = 1000;
        color = '#000';
        backgroundColor = '#f0f0f0';
        borderColor = '#ccc rgb(103, 103, 103) rgb(103, 103, 103) #ccc';
        borderStyle = 'solid';
        borderWidth = '1px';
        fontFamily = this.options.fontFamily ? this.options.fontFamily : 'Arial, Helvetica, Sans-serif';
        fontSize = this.options.fontSize ? this.options.fontSize : '11px';
    }

    // add support for listeners
    GSEventBroadcaster.initialize(this);

    GSEventManager.addEventListener(this.element, 'contextmenu', GSUtil.cancelEvent);

    if(this.options.highlightBackgroundImage) { // preload menu item background image
        new Image().src = this.options.highlightBackgroundImage;
    }
};

/**
 * Removes this context menu from the map instance it belongs to
 */ 
GSContextMenu.prototype.remove = function() {
    if(this.map) {
        map.container.removeChild(this.element);
    }
    this.onmap = false;
};
/**
 * @fileOverview A menu item
 * @name GSMenuItem
 */

/**
 * @class A UI control providing a single menu item in a menu
 * @param {Object} [options] an object literal specifying the menu item options to set:
 * @config {boolean} enabled when <code>true</code> the menu item is enabled and will respond to user events. Defaults to <code>true</code>
 * @config {boolean} isSeparator when <code>true</code> the menu item will be rendered as a menu separator
 * @config {String} icon the URL of an icon to use with the label text. Icons are rendered to the left of the label text.
 * @config {Function} action a function to be invoked when this menu item is clicked. If this menu item is not enabled the
 * action will not be executed
 * @constructor
 */
function GSMenuItem(label, options) {
    this.options = options ? options : {};
    this.label = label;
    this.enabled = this.options.enabled !== undefined ? this.options.enabled : true;
    this.isSeparator = this.options.isSeparator !== undefined ? this.options.isSeparator : false;
    //this.mnemonicIndex = this.options.mnemonicIndex !== undefined ? this.options.mnemonicIndex : -1;
    this.parent;
    this.element;
    this.initialize();
}

/**
 * Called once to initialize this control when it is created
 * @private
 */
GSMenuItem.prototype.initialize = function() {
    this.element = document.createElement('div');
    this.element.className = 'gs_menuitem';
    this.element.style.clear = 'left';
    if(this.isSeparator) {
        this.element.className += ' gs_separator';
        this.element.style.width = '150px';      
        this.element.style.borderTop = '1px solid #ccc';
        this.element.appendChild(document.createElement('div'));
    } else {
        this.element.style.width = '130px';
        this.element.style.padding = '3px 10px';       
        if(!this.enabled) {
            this.applyDisabledStyle();
        } else {
            this.applyEnabledStyle();
        }

        var html = '';
        if(this.options.icon) {
            html += '<img src="' + this.options.icon + '" alt="" style="float: left; margin-right: 3px"/>';
        }
        html += this.label;
        this.element.innerHTML = html;
        
        this.addEventHandlers();
    }

    if(this.options.action) {
        GSEventManager.addEventListener(this.element, 'click',
                                        GSUtil.bindAsEventListener(this.performAction, this, this.options.action));
    }
};

/**
 * Adds default event handlers to this menu item
 * @private
 */
GSMenuItem.prototype.addEventHandlers = function() {
    GSEventManager.addEventListener(this.element, 'mouseover', GSUtil.bindAsEventListener(this.mouseoverHandler, this));
    GSEventManager.addEventListener(this.element, 'contextmenu', GSUtil.cancelEvent);
    GSEventManager.addEventListener(this.element, 'dblclick', GSUtil.cancelEvent);
};

/**
 * Adds this menu item to the specified menu
 * @param {GSContextMenu} menu the menu to add this menu item to
 * @param {int} [index] the position to add the menu item at in the menu
 * @private
 */
GSMenuItem.prototype.addToMenu = function(menu, index) {
    this.menu = menu;
    var parentElement = this.menu.element;
    if(index !== undefined) {
        var refElement = null;
        for(var i = 0, l = parentElement.childNodes.length; i < l; i++) {
            if(i == index) {
                refElement = parentElement.childNodes[i];
                break;
            }
        }
        parentElement.insertBefore(this.element, refElement);
    } else {
        parentElement.appendChild(this.element);
    }
};

/**
 * Removes this menu item from it's parent menu
 */
GSMenuItem.prototype.remove = function() {
    this.element.parentNode.removeChild(this.element);
};

/**
 * Adds an event handler function to this menu item
 * Any DOM Level 2 mouse event type may be specified for the <code>eventType</code>
 * argument:
 * <ul>
 * <li><strong>click</strong> - the click event occurs when the mouse button is clicked over the feature.</li>
 * <li><strong>mousedown</strong> - the mousedown event occurs when the mouse button is pressed over the feature</li> 
 * <li><strong>mouseup</strong> - the mouseup event occurs when the mouse button is released over the feature</li>
 * <li><strong>mouseover</strong> - the mouseover event occurs when the mouse is moved onto the feature</li>
 * <li><strong>mousemove</strong> - the mousemove event occurs when the mouse is moved while it is over the feature</li>
 * <li><strong>mouseout</strong> - the mouseout event occurs when the mouse is moved away from the feature</li>
 * </ul>
 * 
 * @param {String} eventType the event that should trigger the handler function
 * @param {Function} func the function to be invoked when the event is triggered
 * @return {Object} a token that can be passed to the <code>GSMapFeature.removeEventHandler()</code>
 * method to remove the event handler
 */
GSMenuItem.prototype.addEventHandler = function(eventType, func) {   
    var token = GSEventManager.addEventListener(this.element, eventType, GSUtil.bindAsEventListener(func, this));    
    return token;    
};

/**
 * Removes the event handler function represented by the <code>token</code> parameter
 * from this menu item
 * @param {Object} token a token identifying the event handler to be removed
 */
GSMenuItem.prototype.removeEventHandler = function(token) {
    GSEventManager.release(token);
};

/**
 * Returns <code>true</code> if this menu item should be able to receive focus
 * @return <code>true</code> if the menu item is able to receive focus
 */
GSMenuItem.prototype.isFocusTraversable = function() {
    return this.enabled && !this.isSeparator;
};

/**
 * Sets the <code>enabled</code> property of the menu item
 * @param {boolean} enabled <code>true</code> if the menu item should be enabled
 */
GSMenuItem.prototype.setEnabled = function(enabled) {
    this.enabled = enabled;
    if(this.enabled) {
        this.applyEnabledStyle();
    } else {
        this.applyDisabledStyle();
    }
};

/**
 * Executes the supplied function if this menu item is currently enabled
 * @param {Event} event the mouse event received by this menu item
 * @param {Function} action the action to perform if this menu item is enabled
 * @private
 */
GSMenuItem.prototype.performAction = function(event, action) {
    if(this.enabled) {
        action(this);
        this.parent.hide();        
    }
};

/**
 * The default mouseover handler for this menu item
 * @private
 */
GSMenuItem.prototype.select = function() {
    this.element.style.color = this.menu.options.highlightColor ? this.menu.options.highlightColor : '#fff';
    if(this.menu.options.highlightBackgroundImage) {
        this.element.style.background = 'url(' + this.menu.options.highlightBackgroundImage + ') repeat-x';
    } else {
        this.element.style.backgroundColor = this.menu.options.highlightBackgroundColor ? this.menu.options.highlightBackgroundColor : '#336fcb';  
    }
};

/**
 * The default mouseout handler for this menu item
 * @private
 */
GSMenuItem.prototype.deselect = function() {
    if(this.enabled) {
        this.applyEnabledStyle();
    } else {
        this.applyDisabledStyle();
    }
    //this.element.style.backgroundColor = '#f0f0f0';
    this.element.style.background = 'none';
};

/**
 * Applies style properties for the enabled state
 * @private
 */
GSMenuItem.prototype.applyEnabledStyle = function() {
    this.element.style.color = '#000';
    this.element.style.cursor = 'pointer';
};

/**
 * Applies style properties for the disabled state
 * @private
 */
GSMenuItem.prototype.applyDisabledStyle = function() {
    this.element.style.color = '#8c8c8c';
    this.element.style.cursor = 'default';
};

// --------------------------------------------------
// Event Handlers
// --------------------------------------------------

/**
 * Handler for mouseover events
 * @private
 */
GSMenuItem.prototype.mouseoverHandler = function(e) {
    if(this.isFocusTraversable()) {
        this.menu.selectItem(this);
    }
};
// --------------------------------------------------
// GSLabel
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2007
//
// --------------------------------------------------

/**
 * @fileOverview <code>GSLabel</code> represents a text feature.
 * @name GSLabel
 */

GSUtil.extend(GSLabel, GSMapFeature);

/**
 * @class Creates a new <code>GSLabel</code> feature that is initialized with a text string and optional
 * properties specified by the <code>options</code> object literal
 * @param {String} text the label text
 * @param {GSPoint} coordinate the NZMG coordinate at which the top-left corner of the label should
 * be rendered
 * @param {Object} [options] an object literal specifying the label options to set:
 * @config {String} color the color to render label the text, may be specified using either an HTML4 keyword,
 * RGB hex value, or rgb(...) functional value
 * @config {String} backgroundColor the color to render the label background, may be specified using either
 * an HTML4 keyword, RGB hex value, or rgb(...) functional value
 * @config {int} borderRadius the radius of the curve to use for the rounded corners of the label background
 * @config {float} opacity the opacity of the label background, a floating point number between 0 and 1
 * @config {int} padding the padding (in pixels) between the label text and the edge of the label background
 * @config {int} fontSize the font size in pixels
 * @config {String} fontWeight the weight of the font
 * @config {String} fontFamily the font family to use for the label text
 * @config {Object} position an object literal specifying positioning values for the label. The following
 * properties may be specified:
 * <ul>
 *  <li><strong>top</strong> - the top edge of the label will be positioned relative to the label y coordinate,
 *  offset by the value of this property</li>
 *  <li><strong>right</strong> - the right edge of the label will be positioned relative to the label x coordinate,
 *  offset by the value of this property</li>
 *  <li><strong>top</strong> - the bottom edge of the label will be positioned relative to the label y coordinate,
 *  offset by the value of this property</li>
 *  <li><strong>left</strong> - the left edge of the label will be positioned relative to the label x coordinate,
 *  offset by the value of this property</li>
 * </ul>
 * @config {Mixed} displayLevels the map zoom levels this label should be displayed at. May be specified
 * either as an array of numeric values OR expressed as a range string (values are inclusive),
 * for example <code>2-5</code>
 * @config {boolean} textShadow will use the CSS3 text-shadow style if supported by the host browser. Defaults
 * to <code>true</code>
 */
function GSLabel(text, coordinate, options) {

    // Call superclass constructor
    GSLabel.baseConstructor.call(this);

    /**
     * The type of this class as a string value
     * @private
     * @type String
     */
    this.type = 'GSLabel';

    /**
     * The label text
     * @private
     * @type String
     */
    this.text = text;
    
    /**
     * The DOM element containing the label text
     * @private
     * @type HTMLElement
     */
    this.textEl = null;

    /**
     * The DOM element containing the label background
     * @private
     * @type HTMLElement
     */
    this.backgroundEl = null;
    
    /**
     * The NZMG coordinate at which the top-left corner of the label should be rendered
     * @private
     */
    this.coordinate = coordinate;

    /**
     * Blink effect properties, not <code>null</code> if blink effect is active
     * @private
     */
    this.blink = null;

    /**
     * Default options for the label
     * @private
     */
    this.options = {
        color: '#fff',
        backgroundColor: '#000',
        opacity: 0.6,
        padding: 1,
        fontSize: 12,
        fontWeight: 'normal',
        fontFamily: 'Arial, Helvetica, Sans-serif',
        position: {},
        textShadow: true
    };
    GSUtil.merge(options, this.options);

    this.options.borderRadius = this.options.borderRadius ? this.options.borderRadius :
        (Number(this.options.fontSize) + Number(this.options.padding) * 2) / 2,

    this.initialize();
}

/**
 * Initializes this feature
 * @private
 */
GSLabel.prototype.initialize = function() {
    this.graphicsEl = document.createElement('div');
    this.graphicsEl.className = 'gs_label';
    with(this.graphicsEl.style) {
        fontSize = this.options.fontSize + 'px';
        lineHeight = this.options.fontSize + 'px';
        fontWeight = this.options.fontWeight;
        fontFamily = this.options.fontFamily;
        visibility = 'visible';
    }

    this.backgroundEl = document.createElement('div');
    with(this.backgroundEl.style) {
        position = 'absolute';
        padding = this.options.padding + 'px 4px';
        backgroundColor = this.options.backgroundColor;
        MozBorderRadius = this.options.borderRadius + 'px';
        khtmlBorderRadius = this.options.borderRadius + 'px';
        webkitBorderRadius = this.options.borderRadius + 'px';
        opacity = this.options.opacity;
        filter = 'Alpha(opacity=' + (this.options.opacity * 100) + ')';
    }
    this.backgroundEl.innerHTML = this.text;
    this.graphicsEl.appendChild(this.backgroundEl);

    this.textEl = document.createElement('div');
    with(this.textEl.style) {
        position = 'absolute';
        zIndex = 2;
        color = this.options.color;        
        padding = this.options.padding + 'px 4px';
        opacity = '1';
        filter = 'Alpha(opacity=100)';
    }    
    if(this.options.textShadow) {
        this.textEl.style.textShadow = '0 1px 2px ' + this.options.backgroundColor;
    }
    this.textEl.innerHTML = this.text;
    this.graphicsEl.appendChild(this.textEl);
};

/**
 * Sets the label text
 * @param text the text to set
 */
GSLabel.prototype.setText = function(text) {
    this.textEl.innerHTML = text;
    this.backgroundEl.innerHTML = text;
    this.render();
};

/**
 * Returns the text value of the label
 * @return the label text
 */
GSLabel.prototype.getText = function() {
    return this.text;
};

/**
 * Shows the label background
 */
GSLabel.prototype.showBackground = function() {
    this.backgroundEl.style.display = 'block';
};

/**
 * Hides the label background
 */
GSLabel.prototype.hideBackground = function() {
    this.backgroundEl.style.display = 'none';
};

/**
 * Sets the color of the label text
 * @param {String} color the color to render the label text, may be specified 
 * using either an HTML4 keyword, RGB hex value, or rgb(...) functional value
 */
GSLabel.prototype.setColor = function(color) {
    this.options.color = color;
    this.textEl.style.color = color;
};

/**
 * Sets the background color of the label
 * @param {String} color the color to render the label background, may be specified 
 * using either an HTML4 keyword, RGB hex value, or rgb(...) functional value
 */
GSLabel.prototype.setBackgroundColor = function(color) {
    this.options.backgroundColor = color;
    this.backgroundEl.style.backgroundColor = color;
};

/**
 * Sets the opacity of the label background
 * @paramm {float} opacity the opacity of the label background, a floating point number between 0 and 1
 */
GSLabel.prototype.setOpacity = function(opacity) {
    this.options.opacity = opacity;
    this.backgroundEl.style.opacity = opacity;
    this.backgroundEl.style.filter = 'Alpha(opacity=' + (opacity * 100) + ')';
};

/**
 * Sets the coordinate this label is positioned at
 * @param {GSPoint} coordinate the new coordinate to position the label at
 * @param {Object} [position] an object literal specifying positioning values for the label
 */
GSLabel.prototype.setCoordinate = function(coordinate, position) {
    this.coordinate = coordinate;
    this.render();
};

/**
 * Sets the display levels this label should be displayed at.
 * @param {Mixed} displayLevels - the map zoom levels this label should be displayed at. May be specified
 * either as an array of numeric values OR expressed as a range string (values are inclusive),
 * for example <code>2-5</code>
 */
GSLabel.prototype.setDisplayLevels = function(displayLevels) {
    this.options.displayLevels = displayLevels;
    this.render();
};

/**
 * 'Blinks' the label for the specified duration
 * @param {int} duration the duration in milliseconds of the blink effect
 */
GSLabel.prototype.blink = function(duration) {
    this.blink = {
    duration: duration,
    start: new Date().getTime(),
    tic: setInterval(GSUtil.bind(this.doBlink, this), 250)
    };
};

/**
 * Called repeatedly to 'blink' the label
 * @private
 */
GSLabel.prototype.doBlink = function() {
    this.setVisible(!this.isVisible());
    if(new Date().getTime() - this.blink.start >= this.blink.duration) {
        clearInterval(this.blink.tic);
        this.blink = null;
    }
};

/**
 * Sets the visibility of this label
 * @param {boolean} value <code>true</code> if this label should be
 * visible on the map
 */
GSLabel.prototype.setVisible = function(value) {
    this.visible = value;
    if(this.onmap && this.visible) {
        this.render();
    } else {
        this.graphicsEl.style.visibility = 'hidden';
    }
};

/**
 * Adds this label to the specified layer instance
 * @private
 *
 * @param {GSLayer} layer the layer instance to add this feature to
 */
GSLabel.prototype.addToLayer = function(layer) {
    layer.contentContainer.appendChild(this.graphicsEl);
    GSLabel.superClass.addToLayer.call(this, layer);
};

/**
 * Renders this label on the map
 * @private
 */
GSLabel.prototype.render = function() {
    if(this.onmap && this.isVisible() && this.shouldRender(this.layer.map) && this.layer.map.contains(this.coordinate)) {
        var pos = this.layer.map.translateToMapCoordinate(this.coordinate);            
        this.updatePosition(pos);
        this.graphicsEl.style.visibility = 'visible';        
    } else {
        this.graphicsEl.style.visibility = 'hidden';
    }
};

/**
 * Positions the label
 * @param {GSPoint} pos the point in the map coordinate space the label should
 * be positioned relative to
 * @private
 */
GSLabel.prototype.updatePosition = function(pos) {
    var left = pos.x;
    var top = pos.y;
    if(this.options.position.left !== undefined) {
        left = pos.x + this.options.position.left;
    } else if(this.options.position.right !== undefined) {
        left = pos.x - (this.textEl.offsetWidth + this.options.position.right);
    }
    if(this.options.position.top !== undefined) {
        top = pos.y + this.options.position.top;
    } else if(this.options.position.bottom !== undefined) {
        top = pos.y - (this.textEl.offsetHeight + this.options.position.bottom);
    }
    GSUtil.positionElement(this.graphicsEl, left, top);              
};

/**
 * Returns <code>true</code> if this label should be rendered at the current zoom level
 * @param {GSMap} map the map instance the label is to be rendered on to
 * @private
 */
GSLabel.prototype.shouldRender = function(map) {
    var displayLevels = this.options.displayLevels;
    if(displayLevels) {
        var zoomLevel = map.scaleIdx;
        if(displayLevels instanceof Array) {
            for(var i = 0, l = displayLevels.length; i < l; i++) {
                if(displayLevels[i] === zoomLevel) {return true};
            }
            return false;
        } else if((typeof displayLevels).toLowerCase() == 'string') {
            var matches = displayLevels.match(/([0-9]+)-([0-9]+)/);
            return zoomLevel >= Number(matches[1]) && zoomLevel <= Number(matches[2]);
        } else {
            return true;
        }
    } else {
        return true;
    }
};

/**
 * Calls the handler function provided when the given event is triggered on this feature.
 * Any DOM Level 2 mouse event type may be specified for the <code>eventType</code>
 * argument:
 * <ul>
 * <li><strong>click</strong> - the click event occurs when the mouse button is clicked over the feature.</li>
 * <li><strong>mousedown</strong> - the mousedown event occurs when the mouse button is pressed over the feature</li> 
 * <li><strong>mouseup</strong> - the mouseup event occurs when the mouse button is released over the feature</li>
 * <li><strong>mouseover</strong> - the mouseover event occurs when the mouse is moved onto the feature</li>
 * <li><strong>mousemove</strong> - the mousemove event occurs when the mouse is moved while it is over the feature</li>
 * <li><strong>mouseout</strong> - the mouseout event occurs when the mouse is moved away from the feature</li>
 * </ul>
 * 
 * @param {String} eventType the event that should trigger the handler function
 * @param {Function} func the function to be invoked when the event is triggered
 * @return {Object} a token that can be passed to the <code>GSMapFeature.removeEventHandler()</code>
 * method to remove the event handler
 */
GSLabel.prototype.addEventHandler = function(eventType, func) {
    if(eventType === 'click') {
        this.graphicsEl.style.cursor = 'pointer';
    }
    GSLabel.superClass.addEventHandler.call(this, eventType, func);
};

/**
 * Removes this label
 */
GSLabel.prototype.remove = function() {
    if(this.graphicsEl.parentNode) {
        this.graphicsEl.parentNode.removeChild(this.graphicsEl);
    }
    GSLabel.superClass.remove.call(this);
};
// --------------------------------------------------
// GSTip
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2007
//
// --------------------------------------------------

/**
 * @fileOverview a tool tip class
 * @name GSTip
 */

/**
 * @class Creates a new tool tip
 * @param {HTMLElement} element the element to set the tool tip for
 * @param {Object} [options] an object literal specifying optional configuration properties
 * @config {boolean} fixed if <code>true</code> the tip is statically positioned relative to
 * its owning element. If not fixed the tip will follow the mouse while its within the boundaries
 * of the owning element (defaults to <code>false</code>)
 * @config {String} text the text to display in the tool tip. If not specified GSTip will use
 * the value of the elements <code>title</code> attribute if set
 * @config {int} showDelay the delay the onShow method is called, defaults to 100ms
 * @config {int} hideDelay the delay the onHide method is called, defaults to 100ms
 * @config {int} maxDisplayChars the maximum number of characters that will be displayed in
 * the tool tip
 * @config {String} className the CSS class name to apply to the tip element, if not provided
 * the tip will use a default style
 * @config {GSPoint} offset the pixel offset for the tip. If the <code>fixed</code> option is <code>true</code>
 * the tip is positioned relative to the top-left corner of the owning element, otherwise it is positioned
 * relative to the mouse position
 * @config {boolean} fixed if <code>true</code> the tip element will be statically positioned
 * relative to the owning element. If <code>false</code> the tip element will follow the mouse,
 * defaults to <code>true</code>
 * @return a reference to the newly created tip object
 * @type GSTip
 */
function GSTip(element, options) {
    
    /**
     * The element to associate the tip with
     * @private
     * @type HTMLElement
     */
    this.element = element;

    /**
     * Default options for the tool tip
     * @private
     * @type Object
     */
    this.options = {
        fixed: false,
        showDelay: 100,
        hideDelay: 100,
        maxDisplayChars: 999999,
        offset: new GSPoint(16, 16)
    };
    GSUtil.merge(options, this.options);
 
    var text = this.options.text ? this.options.text : (this.element.title ? this.element.title : '');
    if(text.length > this.options.maxDisplayChars) {
        text = text.substring(0, this.options.maxDisplayChars) + '&hellip;';    
    }
    this.element.removeAttribute('title');

    /**
     * The tool tip text
     * @private
     * @type String
     */
    this.text = text;

    /**
     * <code>true</code> if the tip is currently visible
     * @private
     * @type boolean
     */
    this.visible = false;

    /**
     * Event handlers bound to the tool tip
     * @private
     */
    this.eventHandlers = {};

    this.build();

    return this;
}

/**
 * Builds the tool tip
 * @private
 */
GSTip.prototype.build = function() {        
    this.tipEl = document.createElement('div');
    with(this.tipEl.style) {
        display = 'none';
        position = 'absolute';
        zIndex = 100000;
    }
    if(this.options.className) {
        this.tipEl.className = this.options.className;
    } else {
        this.tipEl.className = 'gs_tip';
        with(this.tipEl.style) { // use default style
            color = '#000';
            backgroundColor = '#f0f0f0';
            fontFamily = 'Arial, Helvetica, Sans-serif';
            fontSize = '11px';
            borderColor = '#999 rgb(103, 103, 103) rgb(103, 103, 103) #999';
            borderStyle = 'solid';
            borderWidth = '1px';
            padding = '2px 4px';
        }
    }    
    this.tipEl.innerHTML = this.text;    
    this.addEventHandlers();
};

/**
 * @private
 */
GSTip.prototype.addToParent = function(parent) {
    this.parentNode = parent;
    this.parentNode.appendChild(this.tipEl);
    return this;
};

/**
 * Adds event handlers to the tip owner
 * @private
 */
GSTip.prototype.addEventHandlers = function() {
    var mouseOverHandler = GSUtil.bind(this.show, this);
    this.eventHandlers['mouseover'] = GSEventManager.addEventListener(this.element, 'mouseover', mouseOverHandler);
    
    var mouseOutHandler = GSUtil.bind(this.hide, this);
    this.eventHandlers['mouseout'] = GSEventManager.addEventListener(this.element, 'mouseout', mouseOutHandler);

    var mouseMoveHandler = GSUtil.bindAsEventListener(this.onMouseMove, this);
    this.eventHandlers['mousemove'] = GSEventManager.addEventListener(this.element, 'mousemove', mouseMoveHandler);
};

/**
 * Positions the tip relative to its tip owner
 * @private
 */
GSTip.prototype.position = function() {
    var pos = new GSPoint(parseInt(GSUtil.getComputedStyle(this.element, 'left')), parseInt(GSUtil.getComputedStyle(this.element, 'top')));
    this.positionRelativeToParent(pos);
};

/**
 * @private
 */
GSTip.prototype.positionRelativeToParent = function(pos) {
    var dimensions = this.getDimensions();
    var w = dimensions.width;
    var h = dimensions.height;
    
    var exceedsWidth = (pos.x + w + this.options.offset.x) > parseInt(GSUtil.getComputedStyle(this.parentNode, 'width'));
    var exceedsHeight = (pos.y + h + this.options.offset.y) > parseInt(GSUtil.getComputedStyle(this.parentNode, 'height'));
    
    if(exceedsWidth) {
        this.tipEl.style.left = 'auto';
        this.tipEl.style.right = this.options.offset.x + 'px';
    } else {
        this.tipEl.style.left = pos.x + this.options.offset.x + 'px';
        this.tipEl.style.right = 'auto';
    }    
    if(exceedsHeight) {
        this.tipEl.style.top = 'auto';
        this.tipEl.style.bottom = this.options.offset.y + 'px';
    } else {
        this.tipEl.style.top = pos.y + this.options.offset.y + 'px';
        this.tipEl.style.bottom = 'auto';
    }    
};

/**
 * @private
 */
GSTip.prototype.getDimensions = function() {
    this.tipEl.style.left = '-1000px';
    this.tipEl.style.right = 'auto';

    if(!this.visible) {
        this.tipEl.style.display = 'block';
    }

    var w = parseInt(GSUtil.getComputedStyle(this.tipEl, 'width'));
    var h = parseInt(GSUtil.getComputedStyle(this.tipEl, 'height'));
    var pl = parseInt(GSUtil.getComputedStyle(this.tipEl, 'padding-left'));
    var pr = parseInt(GSUtil.getComputedStyle(this.tipEl, 'padding-right'));
    var pt = parseInt(GSUtil.getComputedStyle(this.tipEl, 'padding-top'));
    var pb = parseInt(GSUtil.getComputedStyle(this.tipEl, 'padding-bottom'));

    if(!this.visible) {
        this.tipEl.style.display = 'none';
    }

    var dimensions = new GSDimension(w + pl + pr, h + pt + pb);
    return dimensions;
};

/**
 * Shows the tool tip
 */
GSTip.prototype.show = function() {
    this.position();
    setTimeout(GSUtil.bind(this.onShow, this, this.tipEl), this.options.showDelay);
};

/**
 * Hides the tool tip
 */
GSTip.prototype.hide = function() {
    setTimeout(GSUtil.bind(this.onHide, this, this.tipEl), this.options.hideDelay);
};

/**
 * Called to display the tool tip after the <code>showDelay</code> interval specified
 * in the tip's constructor function.  May be overriden to alter the default onShow
 * behaviour
 */
GSTip.prototype.onShow = function(element) {
    element.style.display = 'block';
    this.visible = true;
};

/**
 * Sets the text to display for this tool tip
 */
GSTip.prototype.setText = function(text) {
    this.text = text;
    this.tipEl.innerHTML = this.text;
};

/**
 * Called to hide the tool tip after the <code>hideDelay</code> interval specified
 * in the tip's constructor function.  May be overriden to alter the default onHide
 * behaviour
 */
GSTip.prototype.onHide = function(element) {
    element.style.display = 'none';
    this.visible = false;
};

/**
 * If <code>options.fixed</code> is <code>false</code> will be called repeatedly as
 * the mouse is moved over the target element
 * @private
 */
GSTip.prototype.onMouseMove = function(e) {
    if(!this.options.fixed) {
        var pos = GSUtil.getMousePixelCoordinate(e, this.parentNode);
        /*
        this.tipEl.style.left = pos.x + this.options.offset.x + 'px';
        this.tipEl.style.top = pos.y + this.options.offset.y + 'px';
*/
        this.positionRelativeToParent(pos);
    } else {
        var pos = GSUtil.getElementPosition(this.element);
        /*
        this.tipEl.style.left = pos.x + this.options.offset.x + 'px';
        this.tipEl.style.top = pos.y + this.options.offset.y + 'px';
*/
        this.positionRelativeToParent(pos);
    }
};

/**
 * Removes this tip
 * @private
 */
GSTip.prototype.remove = function() {
    for(var i in this.eventHandlers) {
        GSEventManager.release(this.eventHandlers[i]);
    }
    this.parentNode.removeChild(this.tipEl);
};

/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

var GSCrypt = {};

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
GSCrypt.hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
GSCrypt.chrsz = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
GSCrypt.hex_md5 = function(s){ return GSCrypt.binl2hex(GSCrypt.core_md5(GSCrypt.str2binl(s), s.length * GSCrypt.chrsz));};
GSCrypt.str_md5 = function(s){ return GSCrypt.binl2str(GSCrypt.core_md5(GSCrypt.str2binl(s), s.length * GSCrypt.chrsz));};

/*
 * Perform a simple self-test to see if the VM is working
 */
GSCrypt.md5_vm_test = function()
{
  return GSCrypt.hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
};

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
GSCrypt.core_md5 = function(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = GSCrypt.md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = GSCrypt.md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = GSCrypt.md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = GSCrypt.md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = GSCrypt.md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = GSCrypt.md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = GSCrypt.md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = GSCrypt.md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = GSCrypt.md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = GSCrypt.md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = GSCrypt.md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = GSCrypt.md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = GSCrypt.md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = GSCrypt.md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = GSCrypt.md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = GSCrypt.md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = GSCrypt.md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = GSCrypt.md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = GSCrypt.md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = GSCrypt.md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = GSCrypt.md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = GSCrypt.md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = GSCrypt.md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = GSCrypt.md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = GSCrypt.md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = GSCrypt.md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = GSCrypt.md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = GSCrypt.md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = GSCrypt.md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = GSCrypt.md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = GSCrypt.md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = GSCrypt.md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = GSCrypt.md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = GSCrypt.md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = GSCrypt.md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = GSCrypt.md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = GSCrypt.md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = GSCrypt.md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = GSCrypt.md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = GSCrypt.md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = GSCrypt.md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = GSCrypt.md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = GSCrypt.md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = GSCrypt.md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = GSCrypt.md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = GSCrypt.md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = GSCrypt.md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = GSCrypt.md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = GSCrypt.md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = GSCrypt.md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = GSCrypt.md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = GSCrypt.md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = GSCrypt.md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = GSCrypt.md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = GSCrypt.md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = GSCrypt.md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = GSCrypt.md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = GSCrypt.md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = GSCrypt.md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = GSCrypt.md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = GSCrypt.md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = GSCrypt.md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = GSCrypt.md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = GSCrypt.md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = GSCrypt.safe_add(a, olda);
    b = GSCrypt.safe_add(b, oldb);
    c = GSCrypt.safe_add(c, oldc);
    d = GSCrypt.safe_add(d, oldd);
  }
  return Array(a, b, c, d);

};

/*
 * These functions implement the four basic operations the algorithm uses.
 */
GSCrypt.md5_cmn = function(q, a, b, x, s, t)
{
  return GSCrypt.safe_add(GSCrypt.bit_rol(GSCrypt.safe_add(GSCrypt.safe_add(a, q), GSCrypt.safe_add(x, t)), s),b);
};
GSCrypt.md5_ff = function(a, b, c, d, x, s, t)
{
  return GSCrypt.md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
};
GSCrypt.md5_gg = function(a, b, c, d, x, s, t)
{
  return GSCrypt.md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
};
GSCrypt.md5_hh = function(a, b, c, d, x, s, t)
{
  return GSCrypt.md5_cmn(b ^ c ^ d, a, b, x, s, t);
};
GSCrypt.md5_ii = function(a, b, c, d, x, s, t)
{
  return GSCrypt.md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
};

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
GSCrypt.safe_add = function(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
};

/*
 * Bitwise rotate a 32-bit number to the left.
 */
GSCrypt.bit_rol = function(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
};

/*
 * Convert a string to an array of little-endian words
 * If GSCrypt.chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
GSCrypt.str2binl = function(str)
{
  var bin = Array();
  var mask = (1 << GSCrypt.chrsz) - 1;
  for(var i = 0; i < str.length * GSCrypt.chrsz; i += GSCrypt.chrsz)
    bin[i>>5] |= (str.charCodeAt(i / GSCrypt.chrsz) & mask) << (i%32);
  return bin;
};

/*
 * Convert an array of little-endian words to a string
 */
GSCrypt.binl2str = function(bin)
{
  var str = "";
  var mask = (1 << GSCrypt.chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += GSCrypt.chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  return str;
};

/*
 * Convert an array of little-endian words to a hex string.
 */
GSCrypt.binl2hex = function(binarray)
{
  var hex_tab = GSCrypt.hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
};

// --------------------------------------------------
// GSContextMenuManager
//
// Author: Adam Ratcliffe
// Copyright GeoSmart Limited 2008
//
// --------------------------------------------------

/**
 * @fileOverview GSContextMenuManager facilitates context menu access
 * @name GSContextMenuManager
 */

function GSContextMenuManager() {}

/**
 * Default options for context menus
 * @static
 * @private
 */
GSContextMenuManager.defaultOptions = {
    fontFamily: 'Lucida Grande, Arial, Helvetica, Sans-serif',
    fontSize: '12px'
};

/**
 * A list of context menus managed by the context menu manager
 * @static
 * @private
 * @type Object
 */
GSContextMenuManager.menus = {};

/**
 * The active context menu
 * @private
 * @type GSContextMenu
 */
GSContextMenuManager.activeMenu;

/**
 * Returns the map's context menu if it exists, otherwise creates a new context menu using
 * options if provided
 * @param {Object} [options] options for the context menu
 * @static
 */
GSContextMenuManager.getContextMenu = function(options) {
    if(GSContextMenuManager.menus['map']) { // map context menu if it exists
        return GSContextMenuManager.menus['map'];
    } else if(GSContextMenuManager.menus['default']) { // default context menu if exists
        return GSContextMenuManager.menus['default'];
    } else { // create and return default context menu
        var options = GSUtil.merge(GSContextMenuManager.defaultOptions, options);
        var cxtMenu = new GSContextMenu(options);
        GSContextMenuManager.menus['default'] = cxtMenu;
        return cxtMenu;
    }
};

/**
 * Returns the map's context menu if it exists, otherwise creates a new context menu
 * @param {GSMap} map the map instance the context menu should be obtained for
 * @static
 */
GSContextMenuManager.getContextMenuByName = function(name) {
    var cxtMenu = null;
    if(GSContextMenuManager.menus[name]) {
        cxtMenu = GSContextMenuManager.menus[name];
    }
    return cxtMenu;
};

/**
 * Sets a named context menu 
 * @param {String} name the context menu name
 * @param {GSContextMenu} cxtMenu the context menu to set
 */
GSContextMenuManager.setContextMenu = function(name, cxtMenu) {
    GSContextMenuManager.menus[name] = cxtMenu;
};

/**
 * Called by a context menu enabled object when it opens its context menu
 * @param {GSContextMenu) cxtMenu the context menu that was opened
 */
GSContextMenuManager.contextMenuOpened = function(cxtMenu) {
    GSContextMenuManager.activeMenu = cxtMenu;
};

/**
 * Called by a context menu enabled object when it hides its context menu
 * @param {GSContextMenu) cxtMenu the context menu that was opened
 */
GSContextMenuManager.contextMenuHidden = function(cxtMenu) {
    GSContextMenuManager.activeMenu = null;
};