// This is just here to tell JS lint that we need to use a global variable
/*global TVI , document, alert, console, window $ */

/// Declare Namespace

TVI = { version: "0.2" };

// Create function for applying config options to classes
TVI.apply = function(o, c, d){

    // o = object   : The object that is having stuff applied to it.
    // c = config   : The config options that will be applied to the object
    // d = defaults : Any default options that
    
    // See if defaults have been supplied
    if (d) {
        // If they have then call the apply function again with the defaults as the config
        TVI.apply(o, d);
    }

    // Check the object and config are valid objects
    if (o && c && typeof c == 'object') {
        // Loop through the config and add all the properties and methods
        for (var p in c) {
			// Check that we're only dealing with config members, not stuff inherited through the prototype
            if (c.hasOwnProperty(p) && c[p] !== undefined) {
		        o[p] = c[p];
		    }
        }
    }
	
    // Return the original object
    return o;
};


// Closed function - is not global but is instantiated and run immediately
(function() {

        // ********** Set up TVI variable and methods ********** //

        TVI.apply(TVI, {

            /**
            * Counter for creating uniqu ID's for components.
            * @property
            * @type integer
            */
            idSeed: 0,

            /**
            * Function for creating a unique ID for any control
            * @property
            * @type Function
            */
            createID: function(o, prefix) {
                prefix = prefix || "TVI-auto-";
                if (o.id === undefined || o.id === null || o.id === '') {
                    var id = prefix + (++this.idSeed);
                    o.id = id;
                }
                return o;
            },

            /**
            * A reusable empty function
            * @property
            * @type Function
            */
            emptyFn: function() { },

            /**
            * List of field types used in forms
            * @property
            * @type Array
            */
            fieldTypes: [
		        'textBox',
		        'textArea',
		        'dropDownList',
		        'checkBox',
			    'password',
			    'multiRadio',
			    'multiCheckBox'
		    ],

            /**
            * List of allowed data types when communicating with a database
            * @property
            * @type Array
            */
            dataTypes: [
			    'varchar',
			    'text',
			    'boolean',
			    'integer',
			    'decimal',
			    'date'
		    ],

            /**
            * Ajax - wraps up the jQuery Ajax function and adds defaults
            */
            ajax: function(params) {

                // Defaults for ajax calls
                var defaults = {
                    type: 'POST',
                    contentType: 'application/json; charset=utf-8',
                    dataType: 'json',
                    data: '{}'
                }

                var successFunction = params.success;
                var failureFunction = params.failure;

                // Overwrite the default parameters with the ones passed in
                var ajaxParams = TVI.apply(defaults, params);

                // Intercept the success function and add some functionality
                ajaxParams.success = function(data) {

                    // Parse the response
                    var response = JSON.parse(data.d);

                    // Check that the call was successful
                    if (response.success === true) {

                        // Run the success function if it exists
                        if (successFunction) {
                            successFunction.call(this, response);
                        }
                    }
                    else {
                        // Run the failure function if it exists
                        TVI.logError(response);
                        if (failureFunction) {
                            failureFunction.call(this, response);
                        }
                    }

                }

                // Intercept the error function and pass it through to the default ajax error function.
                ajaxParams.error = function() {

                    var errorCode = params.errorCode || '010004';
                    var errorMessage = params.errorMessage || 'Error occured during Ajax call';

                    // Log the error to the console
                    TVI.logError({
                        "code": errorCode,
                        "message": errorMessage
                    });

                    // Run the error function if it exists
                    if (params.error !== undefined && params.error !== null) {
                        params.error.call();
                    }
                };

                // Make the ajax call
                $.ajax(ajaxParams);

            },

            /**
            * Check to see if the Firebug console exists for error logging
            */
            firebugCheck: function() {

                // Attempt top initialise the console if it doesn't exist
                if (typeof console == 'undefined') {
                    if (typeof loadFirebugConsole == 'function') {
                        loadFirebugConsole();
                    }
                }

                // Now check to see if the console exists
                if ($.browser.mozilla && window.console !== null && window.console !== undefined) {
                    return true;
                }
                else {
                    return false;
                }
            },

            /**
            * Log an error to the Firebug console.
            * @param {Object} errorObject - An object containing an error code and error message.
            */
            logError: function(errorObject) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {

                    // Check to see if there is an errors colleciton
                    if (errorObject.errors) {

                        // Loop through
                        var errorsLength = errorObject.errors.length;
                        for (i = 0; i < errorsLength; i++) {
                            console.error('TVI Error: ' + errorObject.errors[i].code + ' - ' + errorObject.errors[i].message);
                        }
                    }
                    else {
                        console.error('TVI Error: ' + errorObject.code + ' - ' + errorObject.message);
                    }

                }
            },

            /**
            * Log a warning to the Firebug error console.
            * @param {string} warning - The warning to be shown
            */
            logWarning: function(warning) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.warn(warning);
                }
            },

            /**
            * Log a comment to the Firebug console.
            * @param {string} comment - The comment to show
            */
            logComment: function(comment) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.info(comment);
                }
            },

            /**
            * Wraps up Firebug's console.dir function.
            * @param {Object} o - The object to bad added to the console
            */
            logDir: function(o) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.dir(o);
                }
            },

            /**
            * Start a timer in Firebug's console.
            * @param {string} name - The name of the timer. Must match the logTimerEnd name.
            */
            logTimer: function(name) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.time(name);
                }
            },

            /**
            * Stops a timer in Firebug's console.
            * @param {string} name - The name of the timer. Must match the start timer name.
            */
            logTimerEnd: function(name) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.timeEnd(name);
                }
            },

            /**
            * Starts a group of nested comments/warnings/errors in Firebug's console.
            * @param {string} name - The name of the group. Must match the logGroupEnd name.
            */
            logGroup: function(name) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.group(name);
                }
            },

            /**
            * Ends a group of nested comments/warnings/errors in Firebug's console.
            * @param {string} name - The name of the group. Must match the start name.
            */
            logGroupEnd: function(name) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.groupEnd(name);
                }
            },

            /**
            * Starts Firebug's profiler.
            * @param {string} title - Optional title for the profiler report.
            */
            logProfile: function(title) {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.profile(title);
                }
            },

            /**
            * Stops Firebug's profiler.
            */
            logProfileEnd: function() {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.profileEnd();
                }
            },

            /**
            * Prints an interactive stack trace of JavaScript execution at the point where it is called.
            */
            logTrace: function() {

                // Console only exists in Firebug so check it exists before trying to write to it
                if (TVI.firebugCheck() === true) {
                    console.trace();
                }
            }

        });

    })();



// ********** TVI UTIL ********** //
TVI.util = {};

TVI.apply(TVI.util, {
	
	/**
     * Adds a new option to a select element.
     * @param {Object} e - the select element we are adding the option to.
     * @param {String} v - the value of the option we want to add.
     * @param {String} t - the text of the option we want to add.
     */
	addSelectOption: function(e, v, t){
		var option = document.createElement("option");
	    option.value = v;
	    
		// Set the text to the same as the value if it hasn't been supplied
		if(t === null || t === "" || t === undefined){
			option.text = v;
		}
		else{
			option.text = t;
		}
	
	    // get current options
	    var o = e.options;
	
	    // get number of options
	    var oL = o.length;
	
	    if (!e.cache) {
	        e.cache = {};
	        // loop through existing options, adding to cache
	        for (var l = 0; l < oL; l++) {
	            e.cache[o[l].value] = l;
	        }
	    }
	
	    // add to cache if it isn't already
	    if (typeof e.cache[v] == "undefined") {
	        e.cache[v] = oL;
	    }
	    e.options[e.cache[v]] = option;
	},
	
	/**
     * Selects an item in the jQuery Select list based on it's value.
     * @param {Object} e - the select element we are adding the option to.
     * @param {String} v - the value of the option we want to select.
     */
	selectOption: function(e, v){
		
		// Find the select list
		e.each(function(){

			// Shortcut to options and length of the options
			o = this.options;
			optionsLength = this.options.length;
			
			// Loop through all the options
			for (var i = 0; i < optionsLength; i++) {
				
				// If the option matches then select it
				if(o[i].value == v){
					o[i].selected = true;
				}

			}
		});

	},
	
	
	/**
     * Returns the text of the selected item in the select list passed in
     * @param {Object} e - the jQuery select element we are adding the option to.
     */
	getSelectedOptionText: function(e){
		
		return e[0].options[e[0].selectedIndex].text;
	},
	
	
	/**
     * converts all special characters in a string to HTML entities and returns the string
     * @param {Object} s - the string to entitify.
     */
	stringToEntities: function(s){
		
		return $('<div/>').text(s).html().replace('"',"&quot;");
	},
	
	/**
     * converts all HTML entities in a string to original characters and returns the string
     * @param {Object} s - the string to deEntitify.
     */
	stringFromEntities: function(s){
		
		$('#temp').html(s);
		return $('#temp').html();
	},
	
	/**
     * converts all quotes in a string to their ascii code equivelants
     * @param {Object} s - the string to encode.
     */
	encodeQuotes: function(s){
		
		var output = s;
		s = s.replace(/"/g,'&#34;');
		s = s.replace(/'/g,'&#39;');
		return s;
	},
	
	/**
     * converts all ascii quote codes in a string back into quotes
     * @param {Object} s - the string to encode.
     */
	decodeQuotes: function(s){
		
		var output = s;
		s = s.replace(/&#34;/g,'"');
		s = s.replace(/&#39;/g,"'");
		return s;
	}

	
}); // End of TVI.util



// ***************************************************************** //
// ******** Override and extend default objects and methods ******** //
// ***************************************************************** //

// Add an Array.indexOf method to IE
if(!Array.indexOf){
	Array.prototype.indexOf = function(obj){
		var L = this.length;
			for(var i=0; i<L; i++){
				if(this[i]==obj){
				return i;
			}
		}
		return -1;
	};
}

// Adds a Trim() method to strings
String.prototype.trim = function() { 
    return this.replace(/^\s+|\s+$/g, '');
};



//*******************************************************//
//                                                       //
//    TVI Component Manager - Last Updated 01/09/2008    //
//                                                       //
//*******************************************************//

// This is just here to tell JS lint that we need to use a global variable
/*global TVI , document, $ , that */

/** 
* COMPONENT MANAGER - A registry for all components created by the TVI JavaScript Framework.
* @author	Jon Hobbs
* @version 1.0
*/
TVI.ComponentManager = function(allowFunctions) {
	
	this.allowFunctions = allowFunctions === true;
	
	return {
		
		/**
		 * Do we allow functions to be stored in this collection? Always yes because we're using it to store components
		 * @property allowFunctions
		 * @type {boolean}
		 */
		allowFunctions: this.allowFunctions,
		/**
		 * The number of components in the registry
		 * @property length
		 * @type {Integer}
		 */
		length:0,
		/**
		 * An Array of objects, each one being a TVI component.
		 * @property items
		 * @type {Array}
		 */
		items: [],
		/**
		 * An Array of keys, which hold the ID of each component.
		 * @property keys
		 * @type {Array}
		 */
		keys: [],
		/**
		 * An object containing a map of keys to components.
		 * @property map
		 * @type {Object}
		 */
		map: {},
		
		/**
		 * add (public)
	     * Adds a new component to the registry
	     * @param {string}   k		The key of the component to add to the key collection.
	     * @param {object}   o		The component to add to the registry.
	     */
		register: function(k, o){
			
			// Check to see if a key has been supplied
	        if(arguments.length == 1){
				// If no key has been supplied then the component is the first parameter, not the second
	            o = arguments[0];
				// And we need to try to get the key for this component
	            k = this.getKey(o);
	        }
			// Check to see if we now have a key
	        if(typeof k == "undefined" || k === null){
				// if we haven't then register the component anyway, with a null key and don't addid to the map
	            this.length++;
	            this.items.push(o);
	            this.keys.push(null);
	        }else{
				// If we have got a key then check to see if the component is already in the gegistry
	            var old = this.map[k];
	            if(old){
					// If the component is already in the registry then call the replace function instead of adding it.
	                return this.replace(k, o);
	            }
				// If we have a key and the component is not already in the registry then add it and add it to the map.
	            this.length++;
	            this.items.push(o);
	            this.map[k] = o;
	            this.keys.push(k);
	        }
	        return o;
	    },
		
		/**
		 * getKey (public)
	     * Returns the ID of a component to use as the key
	     * @param {o} config	The component to get the key for
	     */
		getKey : function(o){
	         return o.id;
	    },
		
		/**
		 * replace (public)
	     * Replaces a component in the registry
	     * @param {string}   k		The key of the component to add to the key collection.
	     * @param {object}   o		The component to add to the registry.
	     */
		replace : function(k, o){
			
			// Check to see if a key has been supplied
	        if(arguments.length == 1){
				// If no key has been supplied then the component is the first parameter, not the second
	            o = arguments[0];
				// And we need to try to get the key for this component
	            k = this.getKey(o);
	        }
			// Get the old item that is being replaced
	        var old = this.item(k);
			// If no key has been supplied, or the old item couldn't be found then call the add function 
			// instead of replacing anything.
	        if(typeof k == "undefined" || k === null || typeof old == "undefined"){
	             return this.add(k, o);
	        }
			// If the old item has been found then replace it in the items array and in the map
	        var index = this.indexOfKey(k);
	        this.items[index] = o;
	        this.map[k] = o;
	        return o;
	    },
		
		
		/**
		* Returns index of the compnent in the registry
		* @param {Object} o The component to find the index of.
		* @return {Integer} index of the component.
		*/
		indexOf : function(o){
			return this.items.indexOf(o);
		},
	
		/**
		* Returns index of the component using the supplied key.
		* @param {String} key 	The key to find the index of.
		* @return {Integer} index of the key.
		*/
		indexOfKey : function(k){
			return this.keys.indexOf(k);
		},
		
		/**
		* Returns the item associated with the passed key OR index. Key has priority over index.
		* @param {String/Number} k The key or index of the item.
		* @return {Object} The item associated with the passed key.
		*/
		item : function(k){
			
			// Create the item. If a key has been supplied then it will find the item in the map and return it.
			// If it isn't found then k must ben an index, so return the item from the items array
		    var item = typeof this.map[k] != "undefined" ? this.map[k] : this.items[k];
			
			// If the item isn't a function, or functions are allowed then return it, otherwise return null
		    return typeof item != 'function' || this.allowFunctions ? item : null;
		}
			
	};
	
}(); 





/// Visual Studio Intellisense reference
/// <reference path="TVI.js" />

/** 
* MAP COORDINATE - An object representing an OSGB1936 longitude/latitude pair
* @param {Object} config	Contains all the config options
*/
TVI.mapCoordinate = function(config) {

	/**
	 * @cfg {string} longitude
	 * An OSGB1936 longitude value
	 */
	/**
	 * @cfg {string} latitude
	 * An OSGB1936 latitude value
	 */
	
	// Apply config
	TVI.apply(this, config);
	
};

/** 
* GMAP - An object representing a google map
* @param {Object} config	Contains all the config options
*/
TVI.GMap = function(config) {

	/**
	 * @cfg {string} id
	 * The unique id of the map
	 */
	/**
	 * @cfg {object} map
	 * The google map object
	 */
	/**
	 * @cfg {string} longitude
	 * An OSGB1936 longitude value
	 */
	/**
	 * @cfg {string} latitude
	 * An OSGB1936 latitude value
	 */
	/**
	 * @cfg {integer} zoomLevel
	 * The zoom level of the map, an integer between 1 and 20.
	 */
	 /**
	 * @cfg {string} view
	 * The view type. Accepts - "normal", "satellite", "hybrid", "physical"
	 */
	/**
	 * @cfg {string} mapControlType
	 * The type of main map control to use - Possible values - large,small
	 */
	/**
	 * @cfg {boolean} showViewButtons
	 * Whether to show the buttos to switch the view type (e.g. map/satellite/hybrid)
	 */
	/**
	 * @cfg {boolean} showViewNormal
	 * Whether to show a button which switches to the normal map view
	 */
	/**
	 * @cfg {boolean} showViewSatellite
	 * Whether to show a button which switches to the satellite view
	 */
	/**
	 * @cfg {boolean} showViewHybrid
	 * Whether to show a button which switches to the hybrid view
	 */
	/**
	 * @cfg {boolean} showViewPhysical
	 * Whether to show a button which switches to the physical view
	 */
	/**
	 * @cfg {boolean} customViewButtons
	 * Whether to use custom view buttons or not
	 */
	/**
	 * @cfg {string} viewButtonsContainerStyle
	 * The style to be used on the container of the view buttons
	 */
    /**
     * @cfg {string} viewButtonStyle
     * A style to be used on custom view buttons
     */
	/**
	 * @cfg {string} viewNormalButtonStyle
	 * The style to be used on the custom normal map view button
	 */
	/**
	 * @cfg {string} viewSatelliteButtonStyle
	 * The style to be used on the custom satellite view button
	 */
	/**
	 * @cfg {string} viewHybridButtonStyle
	 * The style to be used on the custom hybrid view button
	 */
	/**
	 * @cfg {string} viewPhysicalButtonStyle
	 * The style to be used on the custom physical view button
	 */
	/**
	 * @cfg {integer} customViewButtonsX
	 * The X Position of the custom view buttons
	 */
	/**
	 * @cfg {integer} customViewButtonsY
	 * The Y Position of the custom view buttons
	 */
	/**
	 * @cfg {boolean} customPanZoomButtons
	 * Whether to use custom view buttons or not
	 */
	/**
	 * @cfg {boolean} panZoomButtonsContainerStyle
	 * Whether to use custom view buttons or not
	 */
    /**
     * @cfg {string} panZoomButtonStyle
     * A style to be used on all pan and zoom buttons
     */
	/**
	 * @cfg {string} panUpButtonStyle
	 * The style to be used on the custom pan up button
	 */
	/**
	 * @cfg {string} panLeftButtonStyle
	 * The style to be used on the custom pan up button
	 */
	/**
	 * @cfg {string} panRightButtonStyle
	 * The style to be used on the custom pan up button
	 */
	/**
	 * @cfg {string} panDownButtonStyle
	 * The style to be used on the custom pan up button
	 */
	/**
	 * @cfg {string} panMiddleStyle
	 * The style to be used on the custom pan up button
	 */
    /**
     * @cfg {string} zoomInButtonStyle
     * The style to be used on the custom zoom in button
     */
    /**
     * @cfg {string} zoomOutButtonStyle
     * The style to be used on the custom zoom out button
     */
    /**
     * @cfg {integer} customPanZoomButtonsX
     * The X Position of the custom pan and zoom buttons
     */
    /**
     * @cfg {integer} customPanZoomButtonsY
     * The Y Position of the custom pan and zoom buttons
     */
	/**
	 * @cfg {string} useCustomMarker
	 * Whether to use a custom marker 
	 */
	/**
	 * @cfg {boolean} customMarkerURL
	 * The URL of the image to use for the custom marker
	 */
    /**
     * @cfg {boolean} customMarkerShadowURL
     * The URL of the image to use for the custom marker shadow
     */
	/**
	 * @cfg {integer} customMarkerWidth
	 * The width of the custom marker image in pixels
	 */
	/**
	 * @cfg {integer} customMarkerHeight
	 * The height of the custom marker image in pixels
	 */
	/**
	 * @cfg {integer} useMarkerManager
	 * Whether to use a marker manager. Use this when using a large number of markers
	 */
	/**
	 * @cfg {integer} useClusteredMarkerManager
	 * Whether to use a cluster.js marker manager, designed specifically for clustering.
	 */
	/**
	 * @cfg {boolean} clusterMarkerURL
	 * The URL of the image to use for the cluster marker
	 */
	/**
	 * @cfg {integer} clusterMarkerWidth
	 * The width of the cluster marker image in pixels
	 */
	/**
	 * @cfg {integer} customMarkerHeight
	 * The height of the cluster marker image in pixels
	 */

	// Declare defaults
	var defaults = {
		longitude: '-0.170889',
		latitude: '51.505697',
		zoomLevel: 13,
		view: 'normal',
		mapControlType: 'large',
		showViewButtons: true,
		showViewNormal: true,
		showViewSatellite: true,
		showViewHybrid: true,
		showViewPhysical: false,
		customViewButtons: false,
		viewButtonsContainerStyle: 'gMapViewButtonsContainer',
		viewButtonStyle: 'gMapViewButton',
		viewNormalButtonStyle: 'gMapViewNormalButton',
		viewSatelliteButtonStyle: 'gMapViewSatelliteButton',
		viewHybridButtonStyle: 'gMapViewHybridButton',
		viewPhysicalButtonStyle: 'gMapViewPhysicalButton',
		customPanZoomButtons: false,
		panZoomButtonsContainerStyle: 'gMapPanZoomButtonsContainer',
		panZoomButtonStyle: 'gMapPanZoomButton',
		panUpButtonStyle: 'gMapPanUpButton',
		panLeftButtonStyle: 'gMapPanLeftButton',
		panRightButtonStyle: 'gMapPanRightButton',
		panDownButtonStyle: 'gMapPanDownButton',
		panMiddleStyle: 'gMapPanMiddle',
		zoomInButtonStyle: 'gMapZoomInButton',
		zoomOutButtonStyle: 'gMapZoomOutButton',
		useCustomMarker: false,
		useMarkerManager: false,
		useClusteredMarkerManager: false
	};
	
	// Apply config
	TVI.apply(this, config, defaults);
	
	// Create unique id if one doesn't already exist
	TVI.createID(this);
	
	// Register with the component manager
	TVI.ComponentManager.register(this.id, this);
	
	this.initialize();

};

TVI.GMap.prototype = {

    /**
    * @property {object} markers
    * An associative array of all the markers belonging to the map
    */
    markers: {},

    /**
    * @property {object} customMarker
    * A google custom marker object to use for markers
    */
    customMarker: {},

    /**
    * @property {object} customMarkerOptions
    * A google custom marker options object to use for markers
    */
    customMarkerOptions: {},

    /**
    * @property {object} clusterMarker
    * A google custom marker object to use for cluster markers
    */
    clusterMarker: {},

    /**
    * @property {object} clusteredMarkerManager
    * A marker manager to use when clustering is necessary - requires clusterer2.js to be loaded.
    */
    clusteredMarkerManager: {},

    /**
    * initialize (public)
    * Creates the google map
    */
    initialize: function() {

        var component = this;

        // Check that the Google maps scripts are loaded
        if (!GBrowserIsCompatible()) {
            TVI.logError({ "code": "080002", "message": "The Google Maps script is not loaded." });
            return;
        }

        // Create the map
        component.map = new GMap2(document.getElementById(component.id));
        component.map.enableScrollWheelZoom();

        // Add the pan/zoom controls if necessary
        if (component.customPanZoomButtons === false) {
            switch (component.mapControlType) {
                case 'radius':
                    component.map.addControl(new GLargeMapControl());
                    break;
                case 'small':
                    component.map.addControl(new GSmallMapControl());
                    break;
                default:
                    // No controls added by default
            }
        }


        // Add the switch view buttons if necessary
        if (component.showViewButtons && component.customViewButtons === false) {

            component.map.addControl(new GMapTypeControl());

            // Remove the normal view button if it isn't being used
            if (component.showViewNormal === false) {
                component.map.removeMapType(G_NORMAL_MAP);
            }
            // Remove the satellite view button if it isn't being used
            if (component.showViewSatellite === false) {
                component.map.removeMapType(G_SATELLITE_MAP);
            }
            // Remove the hybrid view button if it isn't being used
            if (component.showViewHybrid === false) {
                component.map.removeMapType(G_HYBRID_MAP);
            }
            // Remove the physical view button if it isn't being used
            if (component.showViewPhysical === false) {
                component.map.removeMapType(G_PHYSICAL_MAP);
            }

        }

        // Add custom switch view buttons if necessary
        if (component.customViewButtons === true) {

            var viewButtons = []

            var customUI = component.map.getDefaultUI();

            var customViewControls = function() { };
            customViewControls.prototype = new GControl();

            customViewControls.prototype.initialize = function(map) {
                var container = document.createElement("div");
                container.setAttribute('class', component.viewButtonsContainerStyle);

                function removeSelected() {
                    $("#" + component.id + " ." + component.viewNormalButtonStyle).removeClass('selected');
                    $("#" + component.id + " ." + component.viewSatelliteButtonStyle).removeClass('selected');
                    $("#" + component.id + " ." + component.viewHybridButtonStyle).removeClass('selected');
                    $("#" + component.id + " ." + component.viewPhysicalButtonStyle).removeClass('selected');
                }

                // Create Map button if necessary
                if (component.showViewNormal === true) {
                    var mapDiv = document.createElement('div');
                    mapDiv.setAttribute('class', component.viewNormalButtonStyle + " " + component.viewButtonStyle);
                    mapDiv.appendChild(document.createElement('a'));
                    container.appendChild(mapDiv);
                    GEvent.addDomListener(mapDiv, "click", function() {
                        removeSelected();
                        $(mapDiv).addClass('selected');
                        map.setMapType(G_NORMAL_MAP);
                    });
                    if (component.view == 'normal') {
                        $(mapDiv).addClass('selected');
                    }
                }

                // Create Satellite button if necessary
                if (component.showViewSatellite === true) {
                    var satDiv = document.createElement('div');
                    satDiv.setAttribute('class', component.viewSatelliteButtonStyle + " " + component.viewButtonStyle);
                    var satDivAnchor = document.createElement('a');
                    satDiv.appendChild(satDivAnchor);
                    container.appendChild(satDiv);
                    GEvent.addDomListener(satDiv, "click", function() {
                        removeSelected();
                        $(satDiv).addClass('selected');
                        map.setMapType(G_SATELLITE_MAP);
                    });
                    if (component.view == 'satellite') {
                        $(satDiv).addClass('selected');
                    }
                }

                // Create Hybrid button if necessary
                if (component.showViewHybrid === true) {
                    var hybDiv = document.createElement('div');
                    hybDiv.setAttribute('class', component.viewHybridButtonStyle + " " + component.viewButtonStyle);
                    var hybDivAnchor = document.createElement('a');
                    hybDiv.appendChild(hybDivAnchor);
                    container.appendChild(hybDiv);
                    GEvent.addDomListener(hybDiv, "click", function() {
                        removeSelected();
                        $(hybDiv).addClass('selected');
                        map.setMapType(G_HYBRID_MAP);
                    });
                    if (component.view == 'hybrid') {
                        $(hybDiv).addClass('selected');
                    }
                }

                // Create Physical button if necessary
                if (component.showViewPhysical === true) {
                    var physDiv = document.createElement('div');
                    physDiv.setAttribute('class', component.viewPhysicalButtonStyle + " " + component.viewButtonStyle);
                    var physDivAnchor = document.createElement('a');
                    physDiv.appendChild(physDivAnchor);
                    container.appendChild(physDiv);
                    GEvent.addDomListener(physDiv, "click", function() {
                        removeSelected();
                        $(physDiv).addClass('selected');
                        map.setMapType(G_PHYSICAL_MAP);
                    });
                    if (component.view == 'physical') {
                        $(physDiv).addClass('selected');
                    }
                }


                // Add the container to the map
                map.getContainer().appendChild(container);
                return container;
            }


            customViewControls.prototype.getDefaultPosition = function() {
                return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(component.customViewButtonsX, component.customViewButtonsY));
            }

            component.map.addControl(new customViewControls());
        }


        // Add custom pan and zoom buttons if necessary
        if (component.customPanZoomButtons === true) {

            var customUI = component.map.getDefaultUI();

            var customPanZoomControls = function() { };
            customPanZoomControls.prototype = new GControl();

            customPanZoomControls.prototype.initialize = function(map) {
                var container = document.createElement("div");
                container.setAttribute('class', component.panZoomButtonsContainerStyle);

                // Create Up button
                var upDiv = document.createElement('div');
                upDiv.setAttribute('class', component.panUpButtonStyle + " " + component.panZoomButtonStyle);
                upDiv.appendChild(document.createElement('a'));
                container.appendChild(upDiv);
                GEvent.addDomListener(upDiv, "click", function() {
                    map.panDirection(0, 1);
                });

                // Create Left button
                var leftDiv = document.createElement('div');
                leftDiv.setAttribute('class', component.panLeftButtonStyle + " " + component.panZoomButtonStyle);
                leftDiv.appendChild(document.createElement('a'));
                container.appendChild(leftDiv);
                GEvent.addDomListener(leftDiv, "click", function() {
                    map.panDirection(1, 0);
                });

                // Create Right button
                var rightDiv = document.createElement('div');
                rightDiv.setAttribute('class', component.panRightButtonStyle + " " + component.panZoomButtonStyle);
                rightDiv.appendChild(document.createElement('a'));
                container.appendChild(rightDiv);
                GEvent.addDomListener(rightDiv, "click", function() {
                    map.panDirection(-1, 0);
                });

                // Create Middle
                var middleDiv = document.createElement('div');
                middleDiv.setAttribute('class', component.panMiddleStyle + " " + component.panZoomButtonStyle);
                middleDiv.appendChild(document.createElement('a'));
                container.appendChild(middleDiv);

                // Create Down button
                var downDiv = document.createElement('div');
                downDiv.setAttribute('class', component.panDownButtonStyle + " " + component.panZoomButtonStyle);
                downDiv.appendChild(document.createElement('a'));
                container.appendChild(downDiv);
                GEvent.addDomListener(downDiv, "click", function() {
                    map.panDirection(0, -1);
                });

                // Create Zoom In button
                var zoomInDiv = document.createElement('div');
                zoomInDiv.setAttribute('class', component.zoomInButtonStyle + " " + component.panZoomButtonStyle);
                zoomInDiv.appendChild(document.createElement('a'));
                container.appendChild(zoomInDiv);
                GEvent.addDomListener(zoomInDiv, "click", function() {
                    map.zoomIn();
                });

                // Create Zoom Out button
                var zoomOutDiv = document.createElement('div');
                zoomOutDiv.setAttribute('class', component.zoomOutButtonStyle + " " + component.panZoomButtonStyle);
                zoomOutDiv.appendChild(document.createElement('a'));
                container.appendChild(zoomOutDiv);
                GEvent.addDomListener(zoomOutDiv, "click", function() {
                    map.zoomOut();
                });

                // Add the container to the map
                map.getContainer().appendChild(container);
                return container;
            }

            customPanZoomControls.prototype.getDefaultPosition = function() {
                return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(component.customPanZoomButtonsX, component.customPanZoomButtonsY));
            }

            component.map.addControl(new customPanZoomControls());
        }


        //////////////////////////////////////////////////////////////////////////////////////////////////

        // Add a custom marker if necessary
        if (component.useCustomMarker) {

            // Set up the marker
            component.customMarker = new GIcon(G_DEFAULT_ICON);
            component.customMarker.image = component.customMarkerURL;

            // Add a custom shadow if necessary
            if (component.customMarkerShadowURL != undefined) {
                component.customMarker.shadow = component.customMarkerShadowURL || null;
            }

            component.customMarker.iconSize = new GSize(component.customMarkerWidth, component.customMarkerHeight);

            // Create the marker options to apply to all markers
            component.customMarkerOptions = { icon: component.customMarker };

        }

        // Create a clustered marker manager if necessary
        if (component.useClusteredMarkerManager) {

            // Initialise clustered marker manager
            component.clusteredMarkerManager = new Clusterer(component.map);
            component.clusteredMarkerManager.SetMaxVisibleMarkers(100);

            // Initialise the cluster marker

            var clusterIcon = new GIcon();
            clusterIcon.image = "i/gMapClusterMarker.png";
            clusterIcon.iconSize = new GSize(48, 64);
            clusterIcon.iconAnchor = new GPoint(24, 64);

            component.clusteredMarkerManager.SetIcon(clusterIcon);

        }

        // Position the map
        component.setCentre({
            longitude: component.longitude,
            latitude: component.latitude,
            zoomLevel: component.zoomLevel
        });

        // Change the view if necessary
        if (component.view !== 'normal') {
            component.setView({
                view: component.view
            });
        }


    },


    getZoomLevel: function() {

        return this.map.getZoom()

    },


    /**
    * setCentre (public)
    * repositions the map with the centre at the supplied coordinates.
    */
    setCentre: function(params) {

        var component = this;

        // Use the default zoom level if one isn't supplied
        if (params.zoomLevel === undefined || params.zoomLevel === null) {
            params.zoomLevel = component.zoomLevel;
        }

        // Set the centre of the map
        if (params.animate) {
            component.map.panTo(new GLatLng(params.latitude, params.longitude), params.zoomLevel);
        }
        else {
            component.map.setCenter(new GLatLng(params.latitude, params.longitude), params.zoomLevel);
        }

    },


    /**
    * createMarker (public)
    * Creates a new marker but doesn't add it to the map
    * @param {number} id - the ID of the database record that this marker represents
    * @param {number} longitude - the longitude coordinate to place the marker at 
    * @param {number} longitude - the latitude coordinate to place the marker at 
    * @param {function} markerClicked - function to run when the marker is clicked
    * @param {function} markerHovered - function to run when the marker is hovered
    * @param {function} markerUnHovered - function to run when the mouse moves off a marker
    */
    createMarker: function(params) {

        var component = this;

        // Create a new marker using the custom marker if necessary
        var newMarker;
        
        if (component.useCustomMarker) {
            newMarker = new GMarker(new GLatLng(params.latitude, params.longitude), component.customMarkerOptions);
        }
        else {
            newMarker = new GMarker(new GLatLng(params.latitude, params.longitude));
        }

        // Add the database ID to the marker
        newMarker.id = params.id;

        // Add a click event handler if one has been supplied
        if (params.markerClicked !== undefined && params.markerClicked !== null) {

            GEvent.addListener(newMarker, "click", params.markerClicked);
        }

        // Add a hover event handler if one has been supplied
        if (params.markerHovered !== undefined && params.markerHovered !== null) {

            GEvent.addListener(newMarker, "mouseover", params.markerHovered);
        }

        // Add a mouse out event handler if one has been supplied
        if (params.markerUnHovered !== undefined && params.markerUnHovered !== null) {

            GEvent.addListener(newMarker, "mouseout", params.markerUnHovered);
        }

        return newMarker;
    },


    /**
    * addMarker (public)
    * Adds a marker to the map
    * @param {number} id - the ID of the database record that this marker represents
    * @param {number} longitude - the longitude coordinate to place the marker at 
    * @param {number} longitude - the latitude coordinate to place the marker at 
    * @param {function} markerClicked - function to run when the marker is clicked
    */
    addMarker: function(params) {


        var component = this;

        // Check to see if the marker already exists on the map
        if (component.markers[params.id]) {
            return;
        }

        // Add the ID to the list of items on the map
        component.markers[params.id] = { longitude: params.longitude, latitude: params.latitude };

        // Create the marker using the marker factory function
        var newMarker = component.createMarker(params);

        // Check to see if we're using googles marker manager
        if (component.useMarkerManager === true) {
            // TODO
        }
        // Check to see if we're using a clustered marker manager
        else if (component.useClusteredMarkerManager === true) {
            component.clusteredMarkerManager.AddMarker(newMarker);
        }
        // Else assume we're not using any kind of marker manager
        else {                    
            component.map.addOverlay(newMarker);
        }


    },


    getMarker: function(id) {

        return this.markers[id];

    },


    clearMarkers: function() {

        var component = this;

        component.markers = {};
        component.map.clearOverlays();



    },


    /**
    * addDragEvent (public)
    * Adds an event which will run when the map is dragged
    */
    mapDragged: function(params) {

        var component = this;

        // Check that a handler function has been passed in
        if (params.handler) {

            // Add the handler to the drag event
            GEvent.addListener(component.map, "moveend", params.handler);
        }
    },


    /**
    * getBounds (public)
    * Gets the upper and lower bounds of the current map viewport in OS coordinates
    */
    getBounds: function() {

        // Set up some variables
        var component = this;
        var bounds = {};

        // Get the bounds
        bounds.longMin = component.map.getBounds().getSouthWest().x;
        bounds.longMax = component.map.getBounds().getNorthEast().x;
        bounds.latMin = component.map.getBounds().getSouthWest().y;
        bounds.latMax = component.map.getBounds().getNorthEast().y;

        //calculate centre
        bounds.longitude = bounds.longMin + ((bounds.longMax - bounds.longMin) / 2);
        bounds.latitude = bounds.latMin + ((bounds.latMax - bounds.latMin) / 2);

        // Return the bounds object
        return bounds;

    },

    /**
    * checkResize (public)
    * Gets the map to check if it's container has been resized
    */
    checkResize: function() {

        var component = this;
        component.map.checkResize();

    },


    /**
    * fitMarkers (public)
    * Centres and zooms the map to fit all the markers
    */
    fitMarkers: function() {

        TVI.logTimer('GMap.fitMarkers()');

        var component = this;

        // Set up the variables to hold the bounds
        var lowestLong;
        var highestLong;
        var lowestLat;
        var highestLat;
        var first = true;

        // Loop through all the markers
        for (marker in component.markers) {

            var markerLongitude = parseFloat(component.markers[marker].longitude);
            var markerLatitude = parseFloat(component.markers[marker].latitude);

            // If it's the first one then just set the bounds
            if (first === true) {

                lowestLong = markerLongitude;
                highestLong = markerLongitude;
                lowestLat = markerLatitude;
                highestLat = markerLatitude;

                first = false;

            }
            else { // Otherwise, check to see if it's outside the current bounds and move the bounds if it is.

                if (markerLongitude < lowestLong) {
                    lowestLong = markerLongitude;
                }
                if (markerLongitude > highestLong) {
                    highestLong = markerLongitude;
                }
                if (markerLatitude < lowestLat) {
                    lowestLat = markerLatitude;
                }
                if (markerLatitude > highestLat) {
                    highestLat = markerLatitude;
                }

            }
        }

        // Centre and zoom the map
        var markerBounds = new GLatLngBounds(new GLatLng(lowestLat, lowestLong), new GLatLng(highestLat, highestLong));
        var zoomLevel = component.map.getBoundsZoomLevel(markerBounds);
        var centreLongitude = lowestLong + ((highestLong - lowestLong) / 2);
        var centreLatitude = lowestLat + ((highestLat - lowestLat) / 2) + ((highestLat - lowestLat) * 0.14);

        component.setCentre({
            longitude: centreLongitude,
            latitude: centreLatitude,
            zoomLevel: zoomLevel
        });

        TVI.logTimerEnd('GMap.fitMarkers()');

    },


    /**
    * setView (public)
    * Changes the view of the map. Accepts - "normal", "satellite", "hybrid", "physical"
    */
    setView: function(params) {

        var component = this;

        // Check which view to switch the map to
        switch (params.view) {
            case 'normal':
                component.map.setMapType(G_NORMAL_MAP);
                break;
            case 'satellite':
                component.map.setMapType(G_SATELLITE_MAP);
                break;
            case 'hybrid':
                component.map.setMapType(G_HYBRID_MAP);
                break;
            case 'physical':
                component.map.setMapType(G_PHYSICAL_MAP);
                break;
            default:
                // Log an error if no valid view has been provided
                TVI.logError({ "code": "080005", "message": "No valid view was provided to Gmap.changeView()" });
        }

    }


}


// ********** TVI MAPPING ********** //
TVI.Mapping = {};

TVI.apply(TVI.Mapping, {

    /**
    * Adds a new option to a select element.
    * @param {string} postcode - The postcode to return co-ordinates for.
    * @param {String} accountCode - The Postcode Anywhere account code to use.
    * @param {String} apiKey - The Postcode Anywhere API key to use.
    * @param {function} success - Function to run when the call to postcode anywhere succeeds.
    * @param {function} failure - function to run when the call to postcode anywhere fails.
    * @param {function} error - function to run when the call to postcode anywhere error.
    * @param {string} paGetLatLongUrl - URL of the handler.
    */
    paGetLatLong: function(params) {

        var that = this;

        // Apply Defaults
        var paGetLatLongUrl = params.paGetLatLongUrl || 'Handlers/Mapping.aspx/paGetLatLong';

        // Create the JSON data
        var jsonData = '';
        jsonData += '{"config":{';
            jsonData += '"postcode":"' + params.postcode + '"';
        jsonData += '}}';


        // Make an ajax call to get the co-ordinates
        TVI.ajax({
            url: paGetLatLongUrl,
            data: jsonData,
            success: params.success,
            failure: params.failure,
            error: params.error,
            errorCode: '080001',
            errorMessage: 'Failed to get Lat & Long coordinates from Postcode Anywhere.'
        });

    },
    
    
    /**
    * Checks for a valid UK postcode.
    * @param {string} postcode - The postcode to validate.
    */
    isValidPostcode: function(postcode){
    
        var regex = /[A-Z]{1,2}[0-9]{1,2} ?[0-9][A-Z]{2}/i;
        
	    return regex.test(postcode);
    
    }


});