/*
	http://easthampshire.org/scripts/map.js
	By Premasagar Rose, dharmafly.com
*/
var GOOGLE_AJAX_SEARCH_API_KEY = 'ABQIAAAAkG7EFjX9geYc0Ao98hwO7RQthu1jCwwn4NAqrlMdB4m6ySAPkBQxURGEk3gjtTlaRbFVq2Avz1cJcQ';
var GEO_CACHE_URL = '/geocode';
var SEARCH_URL = '/searchmap';
var LOCALINFO = {
	extension:'.txt',
	delimiters:{
		//items:String.fromCharCode(13, 10, 13, 10), // Double carriage return = '\u000d' + '\u000a' + '\u000d' + '\u000a'
		//keys:String.fromCharCode(13, 10), // Single carriage return
		items:String.fromCharCode(10, 13), // Double carriage return = '\u000d' + '\u000a' + '\u000d' + '\u000a'
		keys:String.fromCharCode(10), // Single carriage return
		keyVal:'|'
	}
};
var IMAGE_PATH = '/images/map/';
var IMAGE_URLS = {
	crosshair: 'map-crosshair.png',
	crosshair_ie: 'map-crosshair.gif',
	marker:''
};

// jQuery Static extensions
$.extend($, {
	// Debug log
	_: function() {
		var payload;
		if (arguments.length === 1){
			payload = arguments[0];
		}
		else {
			payload = [];
			$.each(arguments, function(){
				payload.push(this);
			});
		}
		if (typeof console !== 'undefined' && typeof console.log !== 'undefined'){ // Firebug console
			console.log(payload);
		}
		else {
			try {
				alert((typeof payload === 'object' && payload !== null) ? payload.toSource() : payload);
			}
			catch(e){
				alert('DEBUG: Could not convert toSource: ' + payload);
			}
		}
		// $('body').prepend('<code>' + payload.toSource() + '</code>');
	},
	
	getRemoteScript: function(url, callback){
		$.getJSON(url + '&callback=?', callback);
	},
	
	regex: {
		COORD: /[+\-]?\d{1,3}(\.\d*)?/,
		POSTCODE: /([A-PR-UWYZ0-9][A-HK-Y0-9][AEHMNPRTVXY0-9]?[ABEHMNPRVWXY0-9]?) +([0-9][ABD-HJLN-UW-Z]{2}|GIR 0AA)/,
		
		exclusive: function(regex){
			var source;
			if (typeof regex === 'string'){
				regex = this[regex];
			}
			if (!regex || !regex.source){
				return false;
			}
			source = regex.source.replace(/^\^|\$$/g, ''); // FUTURE: Needs lookbehind to prevent escaped string $ from being replaced
			return new RegExp('^' + source + '$');
		}
	},
	
	isLatLng: function(l){
		switch (typeof l){
			case 'number':
			return true;
			
			case 'string':
			return this.regex.exclusive('COORD').test(l);
			
			default:
			return false;
		}
	},
	
	geocode: function(address, callback){		
		function googleLocalSearch(address, callback){
			var url = 'http://www.google.com/uds/GlocalSearch?q=' + address + '&key=' + GOOGLE_AJAX_SEARCH_API_KEY + '&v=1.0&callback=?';
			$.getJSON(url, function(data){
				var result;
				if (data && data.responseData && data.responseData.results && data.responseData.results[0]){
					result = data.responseData.results[0];
					callback(new GLatLng(result.lat, result.lng));
				}
				else {
					callback(false);
				}
			});
		}
	
		function googleMaps(address, callback){
			var method = arguments.callee;
		
			if (!method.geocoder){
				method.geocoder = new GClientGeocoder();
				method.geocoder.setBaseCountryCode(this.baseCountryCode);
			}
			function progressiveGeocode(point){ // Progressively attempt to geocode each part of the address (sometimes Google geocoder does not understand addresses when they are too specific)
				if (!point && address.indexOf(',') !== -1){
					address = address.replace(/^[^,]*,/, '');
					method.geocoder.getLatLng(address, arguments.callee);
				}
				else {
					callback(point);
				}
			}
			method.geocoder.getLatLng(address, progressiveGeocode);
		}
		
		googleLocalSearch(address, function(point){
			if (point){
				callback(point);
			}
			else {
				googleMaps(address, callback);
			}
		});
	},
	
	postcode: function(str){
		var match = str.toUpperCase().match(this.regex.POSTCODE);
		return match ? match[0] : false;
	}
});


// jQuery Element extensions
$.extend($.fn, {
	wait:function(time, type) {
        time = time || 1000;
        type = type || "fx";
        return this.queue(type, function() {
            var self = this;
            window.setTimeout(function() {
                $(self).dequeue();
            }, time);
        });
    },

	normalize:function() {
	  return $.trim(this.text().replace(/\s{2,}/gm,' '));
	},
	
	selfOrChild:function(selector, asCollection){
		var matches;
		asCollection = asCollection || false;
		matches = $(this).filter(selector).length ? $(this).filter(selector) : $(this).find(selector);
		return asCollection ? matches : matches.eq(0);
	},
	
	geo:function(){ // Return geo object from this collection of elements
		var points, asArray, toPopulate;
		points = [];
		asArray = (arguments.length && typeof arguments[0] === 'boolean') ? arguments[0] : (this.length > 1);
		toPopulate = (arguments.length && typeof arguments[0] === 'object' && arguments[0].latitude && arguments[0].longitude && $.isLatLng(arguments[0].latitude) && $.isLatLng(arguments[0].longitude)) ? arguments[0] : false;
		
		this.each(function(i){
			var geo, lat, lon, abbr;
			// Use this element, if it has className 'geo', or look for a child element
			geo = $(this).selfOrChild('.geo');
			
			// Test if .geo element uses ABBR design pattern
			if (geo.length && $.nodeName(geo.get(0), 'abbr')){
				lat = $.trim(geo.attr('title').split(';')[0]);
				lon = $.trim(geo.attr('title').split(';')[1]);
			}
			abbr = (lat && lon && $.isLatLng(lat) && $.isLatLng(lon));
			
			// If lat/lon not found in ABBR then look for .latitute and .longitude children
			if (!abbr){
				lat = geo.find('.latitude').eq(0).normalize();
				lon = geo.find('.longitude').eq(0).normalize();
			}
			
			if (toPopulate){			
				lat = String(Number(toPopulate.latitude));
				lon = String(Number(toPopulate.longitude));
				
				if (abbr){
					geo.attr('title', lat + ';' + lon);
				}
				else if (geo.length && geo.find('.latitude').length && geo.find('.longitude').length){
					geo.find('.latitude').text(lat);
					geo.find('.longitude').text(lon);
				}
				else {
					$(this).append(
						'<span title="Latitude &amp; longitude" class="geo">(' +
						'<span class="latitude">' + lat + '</span>,' +
						'<span class="longitude">' + lon + '</span>' +
						')</span>');
				}
			}
			// If valid lat/lon found, then add GLatLng object to points array
			points[i] = ($.isLatLng(lat) && $.isLatLng(lon)) ? {latitude:Number(lat), longitude:Number(lon)} : false;
		});		
		
		return (asArray) ? points : points[0];
	},
	
	containsGeo:function(){
		var valid = false;
		this.each(function(i){
			if ($(this).geo()){
				valid = true;
				return false;
			}
		});
		return valid;
	},
	
	getBounds:function(){ // Return GLatLngBounds from geo of these DOM nodes
		var n, s, e, w;
		
		this.each(function(i){
			var geo = $(this).geo();
			
			if (!geo){
				return true;
			}
			if (typeof n === 'undefined' || geo.latitude > n){
				n = geo.latitude;
			}
			if (typeof s === 'undefined' || geo.latitude < s){
				s = geo.latitude;
			}
			if (typeof e === 'undefined' || geo.longitude > e){
				e = geo.longitude;
			}
			if (typeof w === 'undefined' || geo.longitude < w){
				w = geo.longitude;
			}
		});
		return (n && s && e && w) ? {
			southWest:{latitude:s, longitude:w},
			northEast:{latitude:n, longitude:e}
		} : false;
	},
	
	getLatLng:function(){
		var points, asArray;
		asArray = arguments.length ? (arguments[0] === true) : (this.length > 1);
		points = $(this).geo(true);
		$.each(points, function(i, point){
			points[i] = point ? new GLatLng(point.latitude, point.longitude) : false;
		});
		return (asArray) ? points : points[0];
	},
	
	getLatLngBounds:function(){ // Return GLatLngBounds from geo of these DOM nodes
		var bounds = $(this).getBounds();
		return bounds ? new GLatLngBounds(
			new GLatLng(bounds.southWest.latitude, bounds.southWest.longitude),
			new GLatLng(bounds.northEast.latitude, bounds.northEast.longitude)
		) : false;
	},
	
	adr:function(){
		return $(this).selfOrChild('.adr');
	},
	
	address:function(type){
		var adr, address;
		type = type || 'text';
		adr = this.adr();
		
		switch (type){
			case 'text':
			address = adr
				.clone()
				.find('.geo')
					.remove()
					.end()
				.normalize();
			address = address.replace(/,(\s*,)+/g, ','); // Remove multiple commas
			address = address.replace(/^,\s*/g, ''); // Remove starting commas
			return address ? address : false;
			
			case 'node':
			return adr.length ? adr : false;
			
			default:
			return false;
		}
	},
	
	geocode: function(innerCallback, addToNode){
		var that, postcode, query;
		that = this;
		addToNode = addToNode || false;
		
		function cacheGeo(lat, lon){
			var cacheUrl = GEO_CACHE_URL;
			if ($(that).attr('id') && lat && lon){
				$.post(cacheUrl, {
					id:$(that).attr('id'),
					latitude:lat,
					longitude:lon
				});
			}
		}
		
		function callback(data){
			if (data){
				cacheGeo(data.lat(), data.lng());
				if (addToNode){
					$(that).geo({latitude:data.lat(), longitude:data.lng()});
				}
			}
			innerCallback.call($(that), data);
		}
		
		postcode = $(this).postcode(true); // Only full, valid postcodes
		query = postcode && $(this).isUK() ?
			postcode + ', UK' :
			$(this).clone()
				.find('.extended-address').remove().end()
				.find('.postal-code').remove().end()
				.address();
		
		if (!query){
			callback(false);
		}
		$.geocode(query, callback);
	},
	
	isUK: function(){
		var country = $(this)
			.selfOrChild('.adr')
			.find('.country-name')
			.text();
		return country.toLowerCase() === 'uk' || country.toLowerCase() === 'united kingdom';
	},
	
	postcode: function(strict){
		var postcode;
		strict = strict || false;
		postcode = $(this)
			.selfOrChild('.adr')
			.find('.postal-code')
			.text();
		if (strict){
			postcode = $.postcode(postcode);
		}
		return postcode ? postcode : false;
	},
	
	googleMapsUrl: function(){
		var query, urlBase;		
		urlBase = 'http://maps.google.co.uk/maps';
		
		if ($(this).postcode(true) && $(this).isUK()){
			query = $(this).postcode() + ', UK';
		}
		else {
			query = $(this).address();
		}
		return query ? urlBase + '?q=' + query  : false;
	},
	
	directions: function(map, directionsNode, from, to, callback){
		var geo, queryThis, queryThat;
		
		callback = callback || function(){};
		geo = $(this).geo();
		if (geo){
			queryThis = geo.latitude + ',' + geo.longitude;
		}
		else if ($(this).postcode(true) && $(this).isUK()){
			queryThis = $(this).postcode() + ', UK';
		}
		else {
			queryThis = $(this).address();
		}
		queryThat = from ? from : to;
		if (!queryThis || !queryThat){
			return false;
		}
		$.geocode(queryThat, function(data){
			var query, directions;
			
			if (data){
				queryThat = data.lat() + ',' + data.lng();
			}
			query = from ? 'from: ' + queryThat + ' to: ' + queryThis : 'from: ' + queryThis + ' to:' + queryThat;
			directions = new GDirections(map, directionsNode);			
			GEvent.addListener(directions, 'load', function(){callback(true);});
			GEvent.addListener(directions, 'error', function(){callback(false);});
			directions.load(query);
		});		
		return true;
	}
});


///////////////////////////////


function Map(initObj){ // initObj is string with the DOM container id, or an object to extend the map instance
	if (typeof initObj === 'string'){
		this.id = initObj;
	}
	else {
		$.extend(this, initObj);
	}
	this.initialise();
}

$.extend(Map, {
	inject: function(maps){ // If the current DOM has a node with id referenced in the passed 'maps' object, then inject the map into the DOM and initialise
		$.each(maps, function(key, map){
			if ($('#' + this.id).length){
				maps[key] = new Map(this);
			}
		});
		return maps;
	},	
	
	image: function(id){
		var filename;
		if (id.match(/\.(png|gif|jpe?g)/)){
			filename = id;
		}
		else if (!IMAGE_URLS[id]) {
			return false;
		}
		else {
			filename = IMAGE_URLS[id];
		}
		return  IMAGE_PATH + filename;
	}
});

////////////////////////

Map.prototype = {
	// Default settings
	lat: 51.02, // Unless overridden by particular map instance, the map will start with this lat/lon
	lon: -0.918,
	zoom: 12, // Default zoom for startup and reset
	cityZoom: 14, // Zoom for repositioning to city level
	width: '100%', // Map width; CSS string or number of pixels
	height: 400, // Map height; CSS string or number of pixels
	infoWindowMaxWidth: 250, // Maximum width of the map infoWindow (the map balloon)
	baseCountryCode: 'uk', // Bias results within this ccTLD when geocoding an address
	controls: [ // Google Map controls to load map with
		GLargeMapControl,
		GHierarchicalMapTypeControl,
		GOverviewMapControl
	],
	lang:{
		SEARCHING:"Searching...",
		NO_RESULTS:"Sorry - no results were found",
		AJAX_ERROR:"Sorry - we couldn't access the search results. Please try again.",
		WELCOME:"<h3>Welcome to the East Hampshire interactive map</h3><p>Here you can view a map which can show news, and events and much more in your local area.</p><p>To move around the map just drag your mouse over it; use the zoom in and out buttons to change the scale of the map.</p>",
		DIRECTIONS_TITLE:"Driving Directions",
		DIRECTIONS_LABEL:"Directions from:",
		DIRECTIONS_LABEL_DETAILS:"Postcode or address",
		DIRECTIONS_LOADING:"Loading directions...",
		DIRECTIONS_ERROR:"Sorry, we couldn't find any directions.",
		PAGINATION_PREV:"<",
		PAGINATION_NEXT:">"
	},
	
	// To be populated as script initialises
	id: '', // to contain container DOM ID; passed to constructor,
	dom: null, // to contain container DOM node; on init()
	gmap: null, // to contain GMap2 object; on init()
	
	// Methods	
	geoXml: $.extend(function(url, newStatus){ // Add & remove GeoXML overlay (KML or GeoRSS)
	    var layers = arguments.callee.layers;
		
		if (typeof layers[url] === 'undefined'){
			layers[url] = {
				status:0, // 0: inactive, 1: active
				geoXml:new GGeoXml(url/*  + '?r=' + Math.random() */)
			};
		}
		if (typeof newStatus === 'boolean'){
			newStatus = newStatus ? 1 : 0;
		}
		if (typeof newStatus === 'number'){
			layers[url].status = newStatus;
			switch (newStatus){
				case 0:
					this.gmap.removeOverlay(layers[url].geoXml);
				break;
				
				case 1:
					this.gmap.addOverlay(layers[url].geoXml);
				break;
				
				default:
					throw new Error('Map.layer: invalid status');
			}
		}
		return layers[url].geoXml;
	}, {layers:[]}),
	
	
	layer: $.extend(function(url, newStatus, markerOptions){ // Add & remove layer
	    var that, layers;		
		that = this;
		layers = arguments.callee.layers;
		
		function getRemoteItems(url, callback){
			$.get(url, function(dataStr){
				var items, data;
				items = [];
				data = dataStr.split(LOCALINFO.delimiters.items);
				
				// Each data point
				$.each(data, function(i, itemStr){
					var item = {};
					// Each property & value for the data point
					$.each(itemStr.split(LOCALINFO.delimiters.keys), function(j, keyValStr){
						var keyVal = keyValStr.split(LOCALINFO.delimiters.keyVal);
						// if kevVal pair exists
						if (keyVal.length === 2 && keyVal[0] !== ''){
							item[$.trim(keyVal[0])] = $.trim(keyVal[1]);
						}
					});
					// Check that LatLng is present & add to array
					if (item.LL){
						items.push(item);
					}				
				});
				callback(items);
			}, 'text');
		}
		
		function addMarkers(items){
			var markers = [];			
			markerOptions = markerOptions || {};
			
			$.each(items, function(i, item){
				var marker, infoWindow, ll, icon;
				
				ll = item.LL.split(',');
				icon = $.extend(new GIcon(G_DEFAULT_ICON), markerOptions);
				marker = new GMarker(new GLatLng(ll[0], ll[1]), {icon:icon});
				that.gmap.addOverlay(marker);
				
				// Info Window (map balloon)
				infoWindow = $('<div id="localinfo_iw" class="map-infowindow"></div>');
				$.each(item, function(key, val){
					var className;
					className = key.toLowerCase().replace(/[^a-z]/g, '');
				
					switch (key){
						case 'LL':
						return true;
						
						case 'Name':
						infoWindow.prepend('<h3>' + val + '</h3>');
						return true;
						
						default:						
						infoWindow
							.append('<span class="' + className + '"><strong>' + key + ':</strong> ' + val + '</span>');
					}
				});
				marker.bindInfoWindow(infoWindow.get(0), {maxWidth:that.infoWindowMaxWidth});
				markers.push(marker);
			});
			return markers;
		}
		
		function showHideMarkers(url, newStatus){
			if (typeof newStatus === 'boolean'){
				newStatus = newStatus ? 1 : 0;
			}
			if (typeof newStatus === 'number'){
				layers[url].status = newStatus;
				switch (newStatus){
					case 0: // Remove markers
						if (!layers[url].markers){
							return false;
						}
						$.each(layers[url].markers, function(i, marker){
							marker.hide();
						});
						that.gmap.closeInfoWindow();
					break;
					
					case 1: // Add markers
						if (!layers[url].markers){
							layers[url].markers = addMarkers(layers[url].items);
						}
						$.each(layers[url].markers, function(i, marker){
							marker.show();
						});
					break;
					
					default:
						throw new Error('Map.layer: invalid status');
				}
				return layers[url].markers;
			}
			return false;
		}
		
		function toggleMarkers(url){
			var newStatus = !layers[url].status ? 1 : 0;
			showHideMarkers(url, newStatus);
			return newStatus;
		}
		
		if (typeof layers[url] === 'undefined'){
			getRemoteItems(url, function(items){
				// Store items
				layers[url] = {
					status:1, // 0: inactive, 1: active
					items:items,
					markers:addMarkers(items)
				};
				showHideMarkers(url, newStatus);
			});			
			return true;
		}
		else {
			return showHideMarkers(url, newStatus);
		}
	}, {layers:[]}),
	
	
	resize: function(){ // Resize the container DOM node
		var w, h;
		w = arguments.length ? arguments[0] : (this.width ? this.width : $(this.dom).width());
		h = arguments.length > 1 ? arguments[1] : (this.height ? this.height : $(this.dom).height());
		
		if (typeof w === 'number'){
			w += 'px';
		}
		if (typeof h === 'number'){
			h += 'px';
		}
		$(this.dom)
			.width(w)
			.height(h)
			.trigger('resize');
	},
	update: function(){
		var lat, lon, zoom;
		
		lat = arguments.length ? arguments[0] : this.lat;
		lon = arguments.length > 1 ? arguments[1] : this.lon;
		zoom = arguments.length > 2 ? arguments[2] : this.zoom;
		this.gmap.setCenter(new GLatLng(lat, lon), zoom);
	},
	clear: function(){
		this.gmap.clearOverlays();
	},	
	initialise: function(){
		var that = this;
		
		this.dom = $('#' + this.id).get(0);
		if (this.dom && GBrowserIsCompatible()) {
			this.resize();
			this.gmap = new GMap2(this.dom);
			if ($.isFunction(this.init)){ // Optional init function on map instances
				if (this.init() === false){ // init function returned false
					return false;
				}
			}
			else { // Move map to default lat, lon and zoom
				this.update();
			}
			
			// Map behaviour
			this.gmap.enableScrollWheelZoom();
			
			// Map controls	
			$.each(this.controls, function(i, Control){
				that.controls[i] = new Control();
				that.gmap.addControl(that.controls[i]);
			});
			return true;
		}
		return false;
	}
};

/////




var maps = { // Required prop: 'id'
	interactiveMap: {
		id:'interactive-map',
		width:'100%',
		height:500,
		lat:50.984802,
		lon:-1.053314,
		zoom:10,
		paginationId:'paginationControl',
		itemsContainerSelector:'.search-items .items', // Selector for the items DOM container
		itemsSelector:'> *', // Selector for items, within the main items container selector
		searchUrlBase: SEARCH_URL, // Url base path to use for Ajax search
		statusDuration:2000, // Duration to show status reports, in milliseconds (not used for reports that are awaiting a specific response - e.g. 'Searching...')
		
		items: function(){ // optional arg: contents to replace items with
			var newItems = arguments.length ? arguments[0] : null;
			if (newItems){
				$(this.itemsContainerSelector)
					.replaceWith(newItems);
			}
			return $(this.itemsContainerSelector + ' ' + this.itemsSelector);
		},
		
		searchUrl: function(){ // Url to use for Ajax search
			return this.searchUrlBase;
		},
		
		status: function(msg, duration){
			var status = $('#interactive-map-status');
			
			if (!status.length){
				return false;
			}
		
			if (msg){
				status
					.text(msg)
					.fadeIn('fast');
				
				if (duration !== true){
					if (typeof duration !== 'number'){
						duration = this.statusDuration;
					}
					status
						.wait(duration)
						.fadeOut('slow');
				}
			}
			else {
				status
					.fadeOut('slow');
			}
		},
		
		plot: function(items){
			var that, bounds;
			
			that = this;
			items = items || this.items();
			
			// Get map bounds from DOM markers
			bounds = items.getLatLngBounds();
			if (!bounds){
				return false;
			}
			
			// Map position
			this.lat = bounds.getCenter().lat();
			this.lon = bounds.getCenter().lng();
			this.zoom = this.gmap.getBoundsZoomLevel(bounds);
			// If this is too close to determine distance, then zoom out a bit
			if (this.zoom > this.cityZoom){
				this.zoom = this.cityZoom;
			}
			this.update();
			
			// Markers
			items.each(function(i, item){
				var latlng, infoWindow, marker;
				
				latlng = $(this).getLatLng();
				// If no geo, then move on - this shouldn't happen
				if (!latlng){
					return true;
				}
				// Info Window (map balloon)
				infoWindow = $('<div class="map-infowindow"></div>')
					.append($(this).find('.title').clone())
					.append(
						$(this).find('.adr').clone() // Clone the location element
						.find('.geo').remove().end() // Remove geo data
					);
				marker = new GMarker(latlng);
				that.gmap.addOverlay(marker);
				marker.bindInfoWindow(infoWindow.get(0), {maxWidth:that.infoWindowMaxWidth});
				
				GEvent.addListener(marker, 'infowindowopen', function(){
					$(item)
						.addClass('selected')
						.children()
							.not('abbr, span, address') // Keep these elements as display:inline not display:block
								.show('fast')
								.end()
							.filter('abbr, span, address')
								.css({display:'inline'});
		        });
				GEvent.addListener(marker, 'infowindowclose', function(){
					$(item)
						.removeClass('selected')
						.children()
							.not('.title')
								.hide('fast');
		        });				
				$(this).click(function(e){
					// Don't follow hyperlinks with this click
					e.preventDefault();
					if (!$(this).hasClass('selected')){
						marker.openInfoWindow(infoWindow.get(0));
					}
				});
			});
			
			// If there's only one item, then activate its marker
			if (items.length === 1){
				items.click();
			}
			
			return true;
		},
		
		search:function(type, terms, page){
			var that;
			that = this;
			type = type || 'events';
			terms = terms || '';
			page = page || 1;
		
			// Don't do anything if there is an empty string or zero-value
			if (!terms){
				return false;
			}
			// Set status to 'searching'
			this.status(this.lang.SEARCHING, true);
			
			$.ajax({
				url:this.searchUrl(),
				data: {
					terms: terms,
					type: type,
					pn: page
				},
				success:function(data){
					var items;
				
					function updateMap(){						
						// If no items then display "no results" status report to user
						if (!items || !items.children().length){
							that.status(that.lang.NO_RESULTS);
							return false;
						}
					
						that.status(); // Clear status report
						that.items(items); // Replace DOM contents with new items
						that.pagination(data); // Extract pagination control, prime it with Ajax handlers and insert into DOM
						that.clear(); // Clear the map of markers
						that.plot(); // Plot new markers on the map
					}
					
					
					// If data is the ul.items element, or contains the ul.items element, then grab it
					items = $(data).is('ul.items') ? $(data).filter('ul.items').eq(0) : $(data).find('ul.items').eq(0);
					// Set up toProcess counter to track items that need processing
					items.data('toProcess', 0);
					
					// See if all items contain valid geo data
					items.children().each(function(i, item){
						if (!$(item).containsGeo()){
							// Add 1 to the toProcess counter
							items.data('toProcess', items.data('toProcess') + 1);
							
							$(item).geocode(function(data){
								// Remove 1 from the toProcess counter
								items.data('toProcess', items.data('toProcess') - 1);
								
								if (!data){
									$(item).remove();
								}

								// If all items processed, then plot the map
								if (!items.data('toProcess')){
									updateMap();
								}
							}, true);
						}
					});
					
					// If all items processed, then plot the map
					if (!items.data('toProcess')){
						updateMap();
					}
				},
				error:function(){
					that.status(that.lang.AJAX_ERROR);
				}
			});
			
			return true;
		},
		
		pagination: function(container){
			var that, p;
			that = this;
			container = container || document;
			
			// Remove existing pagination
			$(this.itemsContainerSelector)
				.parent()
				.find('[id=' + this.paginationId + ']')
				.remove();
			
			// Find new pagination control
			p = $(container).filter('[id=' + this.paginationId + ']');
			if (!p.length){
				p = $(container).find('[id=' + this.paginationId + ']');
			}
			if (!p.length || !p.find('a').length){
				return false;
			}
			
			// Add click handler to links in the pagination control
			p.find('a').each(function(){
				var thisPage = Number($(this).text());
				// For Prev / Next links, find a number link with the same href
				if (String(thisPage) !== $.trim($(this).text())){
					thisPage = Number(
						p.find('[href=' + $(this).attr('href') + ']')
							.not(this)
							.text()
					);
				}
				$(this).data('page', thisPage);
				$(this).click(function(){
					var type, terms, page;						
					type = $('#cat-contain').data('type');
					terms = $('#cat-contain').data('terms');
					page = $(this).data('page');
					if (type && terms && page){
						that.search(type, terms, page);
					}
					return false;
				});
			});
			
			// Text of Prev/Next links
			p.find(':first').text(this.lang.PAGINATION_PREV);
			p.find(':last').text(this.lang.PAGINATION_NEXT);
			
			// Insert new pagination
			p.insertAfter($(this.itemsContainerSelector));
			return p;
		},
		
		init:function(){
			var that, infoWindow;
			that = this;

			// Nav controls
			// Bind function to form fields in the nav
			function performSearch(){
				var page = 1;				
				if (!$(this).val() || !$(this).attr('name')){
					return false;
				}
				$('#cat-contain')
					.data('type', $(this).attr('name'))
					.data('terms', $(this).val())
					// Reset other controls
					.find('select').not(this)
						.attr('selectedIndex',0);
				
				return that.search($(this).attr('name'), $(this).val(), page);
			}
			
			$('#cat-contain')
				// Submit buttons
				.find('input[type=submit]')
					.click(function(e){
						e.preventDefault();
						$(this)
							.parents('form')
							.find('input[type=text]')
								.eq(0)
								.each(performSearch);					
					})
					.end()
			
				// Selects and text inputs
				.find('select')
				.change(performSearch);
				
			$('#localinfo-options a')
				.click(function(e){
					var url, checkbox, showhide, markerOptions;
					
					url = $(this).attr('href').replace(/^(.*?)([^\/]*)\.kml$/, '$1$2' + LOCALINFO.extension);
					checkbox = $(this).find('input[type="checkbox"]');
					showhide = !checkbox.attr('checked'); // Reverse showhide from current setting
					
					checkbox.attr('checked', showhide ? 'checked' : '');
					$(this).parents('li').eq(0).toggleClass('active');
					markerOptions = {
						image: Map.image(url.replace(new RegExp('^(.*?)([^\/]*)' + LOCALINFO.extension + '.*$'), '$2.png')),
						iconSize:new GSize(32,32),
						shadow:Map.image('shadow.png'),
						shadowSize: new GSize(49, 32),
					    iconAnchor: new GPoint(16, 16),
					    infoWindowAnchor: new GPoint(16, 16)
					};
					/* if ($.browser.msie && parseInt($.browser.version, 10) <= 6){
						markerOptions.image = markerOptions.image.replace(/\.png$/, '_ie.png');
					} */
					that.layer(url, showhide, markerOptions);
					
					return false;
				});
			$('#localinfo-options input[type="checkbox"]')
				.attr('checked', '')
				.click(function(){
					var that = this;
					window.setTimeout(function(){
						$(that).parents('a').eq(0).click();
					},5);
					return false;
				});
			$('#localinfo-showhide').click(function(){
				$(this)
					.toggleClass('show')
					.toggleClass('hide');
				$('#localinfo')
					.toggle('fast');
			});
			
			// Plot markers on the map
			if (!this.plot()){
				// If no bounds, then update the map to the default state
				this.update();
				infoWindow = $('<div class="map-infowindow" style="width:250px;">' + this.lang.WELCOME + '</div>');
				this.gmap.openInfoWindow(this.gmap.getCenter(), infoWindow.get(0));
				return true;
			}
		}
	},

	eventMap: {
		id:'event-map',
		zoom: 15,
		init: function(){
			var that, item;
			that = this;
			item = $('.vevent');
			
			function createMarker(data){
				var infoWindow;
				that.lat = data.lat();
				that.lon = data.lng();
				that.update();
				
				// Info Window (map balloon)
				infoWindow = $('<div class="map-infowindow"></div>')
					.append('<span class="title">' + item.find('.summary').text() + '</span>')
					.append(
						item
						.find('.location')
							.clone() // Clone the location element
							.find('.geo')
								.remove() // Remove geo data
								.end()
					)
					.append(
						$('<ul class="meta"></ul>')
							.append(
								$('<li></li>')
									.append(
										$('<a>Driving Directions</a>')
											.click(function(){
												var iw, iw_node;
												iw = that.gmap.getInfoWindow();
												iw_node = $(this).parents('.map-infowindow').get(0);
												
												// Replace link with text input
												$(this).replaceWith('<label for="directions-from">' + that.lang.DIRECTIONS_LABEL + '</label><input type="text" id="directions-from" class="start" name="directions-from" value="' + that.lang.DIRECTIONS_LABEL_DETAILS + '" /><input id="directions-go" type="button" value="Go" />');
												
												// Redraw infoWindow
												that.marker.closeInfoWindow();
												that.marker.openInfoWindow(iw_node);
												
												// Text field
												$('#directions-from')
													.keypress(function(e){
														if ($(this).val() === that.lang.DIRECTIONS_LABEL_DETAILS){
															$(this)
																.removeClass('start')
																.val('');
														}
														// If return key pressed, submit
														if (e.keyCode === 13){
															$('#directions-go').click();
														}
													})
													.click(function(){
														if ($(this).val() === that.lang.DIRECTIONS_LABEL_DETAILS){
															$(this).select();
														}
													});
													// Wait till infoWindow open, then select input field
													window.setTimeout(function(){
														$('#directions-from').select();
													},500);
												
												// Submit button
												$('#directions-go')
													.click(function(){
														var from = $('#directions-from').val();												
														if (!from || from === that.lang.DIRECTIONS_LABEL_DETAILS){
															return false;
														}
														// Use postcode if supplied
														if ($.postcode(from)){
															from = $.postcode(from) + ', UK';
														}
														// Add header title
														if (!$('#driving-directions h3').length){
															$('#driving-directions')
																.prepend('<h3>' + that.lang.DIRECTIONS_TITLE + '</h3><p class="status"></p>');
														}
														// Loading status
														$('#driving-directions .status').text(that.lang.DIRECTIONS_LOADING);
														// Empty directions element
														$('#driving-directions .directions').empty();
														// Directions request
														item.directions(that.gmap, $('#driving-directions .directions').get(0), from, null, function(status){
															if(status){
																// Clear the loading status
																$('#driving-directions .status').empty();																// Remove the initial marker
																that.gmap.removeOverlay(that.marker);																// Hide overview control
																$.each(that.controls, function(i, control){
																	if (typeof control.hide === 'function'){
																		control.hide();
																	}
																});
															}
															else {
																$('#driving-directions .status').text(that.lang.DIRECTIONS_ERROR);
															}
														});
													});
											})
									)
							)
							.append('<li><a href="' + item.googleMapsUrl() + '">Google Maps</a></li>')
					);
				
				// Marker
				that.marker = new GMarker(that.gmap.getCenter());
				that.gmap.addOverlay(that.marker);
				that.marker.bindInfoWindow(infoWindow.get(0), {maxWidth:that.infoWindowMaxWidth});
				that.marker.openInfoWindow(infoWindow.get(0));
				$('#venue-box').click(function(){
					that.marker.openInfoWindow(infoWindow.get(0));
				});
			}
			
			// Geocode address
			if (!item.find('.adr').containsGeo()){
				item.find('.adr').geocode(function(data){
					if (!data){
						return; // Location could not be geocoded
					}					
					createMarker(data);
				}, true);
			}
			else {
				createMarker($('.vevent').getLatLng());
			}
		}
	},

	newsMap: {
		id:'add-news-map',
		crosshair: {
			src: (!$.browser.msie || parseInt($.browser.version, 10) >= 7) ? Map.image('crosshair') : Map.image('crosshair_ie') // IE6 & less get their own special crosshair image
		},
		
		// From the lat/lon fields, update the map position
		updateFromFields: function(){ // Update map centre with lat/lon input fields
			if ($.trim($('#lat').val()) !== '' && $.trim($('#lon').val()) !== ''){
				this.update(parseFloat($('#lat').val()), parseFloat($('#lon').val()), this.cityZoom);
				return true;
			}
			return false;
		},
		// Use the map position to update the lat/lon fields
		updateToFields: function(){
			$('#lat')
				.val(this.gmap.getCenter().lat())
				.trigger('mutate', 'map');
			$('#lon')
				.val(this.gmap.getCenter().lng())
				.trigger('mutate', 'map');
		},
		init: function(){
			var that, moveHandler;
			that = this;			
			
			// Bind a handler function to the 'mutate' event of the lat/lon fields, so that when both fields are updated, the 'latLonUpdate' event of the #geo fieldset element triggers
			$('#lat, #lon').bind('mutate', function(e, source){
				$(this).data('updated', true); // Flag the field as updated
				if ($('#lat').data('updated') && $('#lon').data('updated')){
					$('#lat, #lon')
						.data('updated', false); // Reset fields
					$('#geo')
						.trigger('latLonUpdate', source); // Trigger event
				}
			});
			
			// On map 'moveend' event, update the lat/lon fields
			moveHandler = GEvent.bind(this.gmap, 'moveend', this, this.updateToFields);
			
			// When lat/lon fields are updated, update the map position; binds a function to the #geo fieldset element's 'latLonUpdate' event
			$('#geo')
				.bind('latLonUpdate', function(e, source){
					if (source !== 'map'){
						// Remove 'moveend' event listener while the map is updated by the lat/lon fields (otherwise, when the map finishes updating, it will set the fields again, creating a loop). Then replace 'moveend' event listener
						GEvent.removeListener(moveHandler);
						that.updateFromFields();
						moveHandler = GEvent.bind(that.gmap, 'moveend', that, that.updateToFields);
					}
				});
			
			// Add crosshair image over map
			$('<img id="map-crosshair" src="' + this.crosshair.src + '" alt="" />')
				.appendTo($(this.dom))
				.css({
					position:'absolute',
					left:'50%',
					top:'50%'
				}).
				load(function(){
					$(this).css({
						margin: (0 - Math.round($(this).height() / 2)) + 'px 0 0 ' + (0 - Math.round($(this).width() / 2)) + 'px'
					});
				});
			
			// On initialising the map, update the map position with the lat/lon fields. If the fields are not populated, then update with the map's default values
			if (!this.updateFromFields()){
				this.update();
			}
		}
	}
};


/////

// Unload Google Maps to prevent memory leaks
$('body').unload(GUnload);
// Add inline styles to the head - for styles that should only be present when JavaScript is active
// FUTURE: Allow head injection on specific map pages
$('head').append('<style type="text/css">#main #map-main .search-items .items > * > * { display:none; } #main #map-main .search-items .items > * > .title { display:block; }</style>');

$(function(){
	Map.inject(maps);
});