/* ---------------------------------------------------------------------- *\
	Explore the UK Map init functions (call via dom & tab:loaded events)
\* ---------------------------------------------------------------------- */

function initMaps() {
	if (!inited['map1']) inited['map1'] = initFindMap();
	if (!inited['map2']) inited['map2'] = initExploreMap();
	if (!inited['map3']) inited['map3'] = initSaverMap();
	if (!inited['map4']) inited['map4'] = initFriendsMap();
	if (!inited['map5']) inited['map5'] = initEventsMap();
	if (!inited['map6']) inited['map6'] = initJourneyMap();
	// Event.observe(window, 'unload', GUnload);
}

function initMap(id, markers) {
	if ($(id) && GBrowserIsCompatible()) {

		// Initialise the map itself
		var map = new TLMap(id);
		if (markers) {
			map.setMarkers(markers);
		}

		// Initialise map switcher
		// (its own object so we can call methods 
		// on it from within other function calls)
		map.switcher = new TLMapSwitcher(map);

		// Initialise external controls
		initMapPanel(map);
		initMapReset(map);

		return map;
	}
}


function TLMapSwitcher(map) {
	var div = $(map.id + 'Switcher');
	if (!div) return;

	this.map = map;
	this.div = div;

	this.img = this.div.down('img');
	this.link = this.div.down('a');
	if (!this.img || !this.link) return;

	// Preload images
	this.images = {};
	this.images.spain = new Image(188,149);
	this.images.spain.src = '/assets/img/maps/switcher_spain.gif';
	this.images.uk = new Image(188,149);
	this.images.uk.src = '/assets/img/maps/switcher_uk.gif';

	// Set click handler
	this.div.setStyle({cursor:'pointer'});
	this.div.observe('click', this.clickHandler.bind(this));
}

TLMapSwitcher.prototype.clickHandler = function(e) {
	if (e) e.stop();

	if (this.link.innerHTML.indexOf('Spanish') > -1) {
		this.showSwitchToUK();
		this.map.panToSpain();
	} else {
		this.showSwitchToSpain();
		this.map.panToUK();
	}

	if (e) e.element().blur();
}

TLMapSwitcher.prototype.showSwitchToUK = function() {
	this.img.src = this.images.uk.src;
	this.link.update(this.link.innerHTML.replace(/spanish/i,'UK'));
}

TLMapSwitcher.prototype.showSwitchToSpain = function() {
	this.img.src = this.images.spain.src;
	this.link.update(this.link.innerHTML.replace(/uk/i,'Spanish'));
}


function initMapPanel(map) {
	// Bail if any pre-requisites are unavailable
	if (!map.panel || !map.fullsize) return;

	var btn = map.panel.down('.jsCheckAvailability');
	if (!btn) return;
	var form = $(btn.form);
	if (!form) return;
	var action = form.down('input[type=hidden]');

	btn.observe('click', function(){
		// Temporarily set the action to a different value so we can do an availability
		// search, storing the original value so we can reinstate it again later on.
		if (action && action.value != 'availabilitySearch') {
			action.oldValue = action.value;
			action.value = 'availabilitySearch';
		}
	});

	form.observe('submit', function(e){
		availabilityFormHandler(map);
		if (e) e.stop();
	});
}

function initMapReset(map) {
	var link = $(map.id + 'Reset');
	if (!link) var link = $('mapReset');
	if (link) {
		link.observe('click',function(e){ map.reset(); e.stop(); });
	}
}

function initMapShowCentre(map,centre) {
	var cbox = $(map.id + 'ShowCentre');
	cbox.observe('click',function(e){
		if (this.checked) {
			map.setMarkersWithType('cityCentre',[centre]);
		} else {
			map.removeMarkersWithType('cityCentre');
		}
	});
}

/* ---------------------------------------------------------------------- *\
	Search & Book / Explore the UK map initialisers
\* ---------------------------------------------------------------------- */

function initFindMap(markers)	 {
	var map = initMap('findMap',markers);
	if (map) {

		// Load default markers
		if (!markers) {
			new Ajax.Request('/ajax/', {
				parameters: {action:'allHotels'},
				onSuccess: function(transport){
					var markerType = (map.fullsize) ? 'hotel':'tiny';
					map.setMarkersWithType(markerType,transport.responseJSON);
				}
			});
		}

		// Submit the form if it's pre-populated so we
		// can link directly to arbitrary locations
		var loc = map.getAvailabilityLocation();
		if (loc) {
			if (cmpLatLngs(loc, map.locations.es)) {
				// Jump directly to the spanish map without panning
				// (used on the search & book page as direct link)
				map.switcher.showSwitchToUK();
				map.map.setCenter(map.locations.es, map.defaultZoom);
			} else {
				availabilityFormHandler(map);
			}
		}
	}
	return map;
}

function initExploreMap(markers) {
	var map = initMap('exploreMap',markers);
	if (map) initPOICategories('POI',map);
	return map;
}

function initSaverMap(markers)	 {
	var map = initMap('saverMap',markers);
	if (map) {

		// Submit the form if it's pre-populated
		if (map.getAvailabilityLocation()) {
			availabilityFormHandler(map);
		}
	}
	return map;
}

function initFriendsMap(markers) {
	var map = initMap('friendsMap',markers);
	if (map) {
		// Load default markers
		if (!markers) {
			new Ajax.Request('/ajax/',{
				parameters: {action:'getFriendsAndFamily'},
				onComplete: function(transport) {
					var friends = transport.responseJSON;
					var markers = showFriendsAndFamily(map,friends);

					// Clicked "Find Hotels" in marker window on "My Travelodge - Saved Places",
					// so show the sub-form and select the specified friend in the dropdown
					if (markers && selectedFriendId) {
						for (var i=0; i<markers.length; i++) {
							if (markers[i].id == selectedFriendId) {
								var found = markers[i]; break;
							}
						}
						if (found) {
							// Select the specified friend location
							var latlng = found.getLatLng();
							map.showAvailabilityOptions();
							map.setFriendsAndFamilyLocation(latlng);
							map.jumpToLatLng(latlng.lat(),latlng.lng());
							// Submit an availability search
							var btn = map.panel.down('.jsCheckAvailability');
							if (btn) btn.click();
							// Open the info panel for the selected marker
							GEvent.trigger(found,'click');
						}
					}
				}
			});
		}

		// Submit the form if it's pre-populated
		if (map.getAvailabilityLocation()) {
			availabilityFormHandler(map);
		}
	}
	return map;
}

function initEventsMap(markers)	 {
	var map = initMap('eventsMap',markers);
	if (map) initPOICategories('Event',map);
	return map;
}

function initJourneyMap() {
	var map = initMap('journeyMap');
	if (map) {
		new Ajax.Request('/ajax/', {
			parameters: {action:'allHotels'},
			onSuccess: function(transport){
				var markerType = (map.fullsize) ? 'hotel':'tiny';
				map.setMarkersWithType(markerType,transport.responseJSON);
			}
		});
		initDirectionsMapPanel(map);
	}
	return map;
}

/* ---------------------------------------------------------------------- *\
	Helper functions for map initialisers above
\* ---------------------------------------------------------------------- */

function availabilityFormHandler(map) {
	// console.log('Submitted availability form');

	var btn = map.panel.down('.jsCheckAvailability');
	if (!btn) return;
	var form = $(btn.form);
	if (!form) return;
	var action = form.down('input[type=hidden]');
	if (action) {
		var performingAction = action.value;
	}

	new Facebox(function(){
		var fbox = this;

		function disambiguationHandler(loc) {
			// Focus on the selected location
			map.jumpToLatLng(loc.lat,loc.lng);
			// Show a location marker in the center of the map
			map.setMarkersWithType('searchLocation',[loc]);
			// Show the availability sub-form
			map.showAvailabilityOptions();
			// Clear any existing availability search markers or marker state
			map.updateMarkersWithAvailability();
			// Zoom out to a point where there's at least one Travelodge visible
			map.zoomToShowHotel();
			if (performingAction == 'addFriendsAndFamily'
			||  performingAction == 'availabilitySearch') {
				// Once we've chosen a concrete coordinate; re-submit the form
				// to store the person (since we've already clicked "add to map").
				availabilityFormHandler(map);
			}
			fbox.close();
		}

		form.request({
			onComplete:function(transport){
				if (action && action.oldValue) action.value = action.oldValue;
				if (transport.responseJSON) {
					var data = transport.responseJSON;
					if (data.locations) {
						if (data.locations.length == 1) {
							// Probably a postcode search returning a single location
							// Skip the disambiguation stuff and go straight there
							disambiguationHandler(data.locations[0]);
						} else {
							fbox.reveal(getDisambiguationList(data.locations,disambiguationHandler));
						}
					} else if (data.availability) {
						map.updateMarkersWithAvailability(data.availability);
						map.zoomToShowAvailability(data.availability);
						fbox.close();
					} else if (data.friends) {
						showFriendsAndFamily(map,data.friends);
						fbox.close();
					}
				} else {
					fbox.reveal(transport.responseText);
				}
			}
		});
	});
}

function showFriendsAndFamily(map,friends) {
	// Add icons to the map
	var markers = map.setMarkersWithType('friends',friends, !map.fullsize);

	// Add items to the f&f panel dropdown
	if (map.panel) {
		var sel = map.panel.down('.jsFriendsList');
		if (sel) {
			for (var i=sel.options.length-1; i>=0; i--) {
				sel.options[i] = null;
			}
			for (var i=0; i<friends.length; i++) {
				var loc = friends[i].lat + ', ' + friends[i].lng;
				var opt = new Option(friends[i].name,loc);
				sel.options[i] = opt;
			}
		}
	}

	return markers;
}

function getDisambiguationList(locations,clickHandler){
	var body = new Element('div',{'class':'padded'}).update('<h4>Did you mean...</h4>');
	var list = new Element('ul');

	locations.each(function(loc){
		var item = new Element('li');
		var link = new Element('a').writeAttribute({href:'#'}).update(loc.text);
		link.observe('click',function(e){ clickHandler(loc); e.stop(); });
		list.insert(item.insert(link));
	});

	return body.insert(list);
}

function initPOICategories(type, map) {
	// Function to call when ticking boxes or changing select
	function updatePOIs(){
		var form = map.panel.down('form');
		form.request({
			onComplete:function(transport){
				map.setMarkersWithType(type.toLowerCase(),transport.responseJSON);
			}
		});
	}

	// Init category checkboxes
	var div = $(type + 'Categories');
	if (div) {
		div.select('input').each(function(input){
			var fieldset = $(input.id + 'Children');
			if (fieldset) fieldset.hide();
			var clickHandler = function(){
				input.up('li').toggleClassName('open');
				if (fieldset) {
					fieldset.toggle()
					fieldset.select('input').each(function(child){
						if (input.checked == false) {
							child.checked = false;
						}
					});
				}
				updatePOIs();
			}
			input.observe('click',clickHandler);
			if (input.checked) clickHandler();
		});
	}
	var select = $('EventMonthFilter');
	if (select) {
		select.observe('change', updatePOIs);
	}
}

/* ---------------------------------------------------------------------- *\
	Other map initialisers
\* ---------------------------------------------------------------------- */

function initDirectionsMap(data) {
	var id = 'directionsMap';
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id);
		if (map) {
			if (data.hotel) {
				map.jumpToLatLng(data.hotel.lat,data.hotel.lng);
				map.setResetLocation();

				map.setMarkersWithType('hotel',[data.hotel]);
				map.setMarkersWithType('hotelPoi',data.poi);
				map.setMarkersWithType('transport',data.transport);
				map.setMarkersWithType('cityCentre',[data.place]);

				initDirectionsMapPanel(map);
				initMapReset(map);
				initMapShowCentre(map,data.place);
			}
			if (data.selectedPoiId) {
				initDirectionsMapSelectedMarker(map,'hotelPoi', data.selectedPoiId);
			}
			if (data.selectedTransportId) {
				initDirectionsMapSelectedMarker(map,'transport', data.selectedTransportId);
			}
		}
	}
	return map;
}

function initDirectionsMapSelectedMarker(map, type, selectedId) {
	// If there's a selected marker, find it, make
	// sure it's visible, and open its info window.
	var hotel = map.markers['hotel'][0];
	var markers = map.markers[type];
	if (hotel && markers) {
		for (var i=0; i<markers.length; i++) {
			if (markers[i].id == selectedId) {
				map.fitSpecifiedMarkers([hotel, markers[i]]);
				GEvent.trigger(markers[i],'click');
				break;
			}
		}
	}
}

function initDirectionsMapPanel(map) {
	var id = map.id;
	var panel = $(id + 'Panel');
	if (!panel) return;

	var resultsDiv = panel.down('#' + id + 'Results');
	var directions = new GDirections(map.map, resultsDiv);
	var geocoder = new GClientGeocoder();
	GEvent.addListener(directions, 'error', function(){ handleDirectionsError(directions,resultsDiv); });

	/*
	function geocodeLookup(a,b){

		function geocodeLookupHelper(a,b){
			// If there's no geocoder available, just let Google do their best
			// with the string as is, and no proper lookup. Similarly, if the
			// string is already a formatted latlng, just use it as is.
			var aResolved = (!geocoder || isLatLng(a)) ? a : null;
			var bResolved = (!geocoder || isLatLng(b)) ? b : null;

			// If either of the above were left unset, run a google geocode
			// request on the original strings and see if that helps. Calling
			// loadDirections() each time with the results we have so far.
			if (!aResolved) {
				geocoder.getLatLng(a, function(p){
					aResolved = (p) ? strFromLatLng(p) : a;
					loadDirections(aResolved,bResolved);
				});
			}
			if (!bResolved) {
				geocoder.getLatLng(b, function(p){
					bResolved = (p) ? strFromLatLng(p) : b;
					loadDirections(aResolved,bResolved);
				});
			}

			// This can be called up to 3 times, but will only execute when
			// both a & b have been resolved to something other than null.
			loadDirections(aResolved,bResolved);
		}

		// We resolve postcodes locally because Google's directions API
		// only supports partial UK postcodes, and not the "in code"
		var postcodes = [];
		if (hasPostcode(a)) { a = getPostcode(a); postcodes.push(a); }
		if (hasPostcode(b)) { b = getPostcode(b); postcodes.push(b); }

		if (postcodes.length == 0) {
			geocodeLookupHelper(a,b);
		} else {
			new Ajax.Request('/ajax/',{
				parameters: {'action':'postcodeCoords','postcodes':postcodes},
				onComplete: function(transport){
					var data = transport.responseJSON;
					if (data) {
						if (data[a]) a = strFromLatLng(data[a]);
						if (data[b]) b = strFromLatLng(data[b]);
						geocodeLookupHelper(a,b);
					} else {
						new Facebox(transport.responseText);
					}
				}
			});
		}
	}
	*/

	function locationLookup(a,b){

		// Define a helper function to deal with each of our location lookups in turn
		function locationLookupHelper(fbox, set, n) {
			function callback(loc){
				set[n].latlng = strFromLatLng(loc);
				if (set[0].latlng && set[1].latlng) {
					// Found both locations, close the facebox and load directions
					fbox.close();
					loadDirections(set[0].latlng, set[1].latlng);
				} else {
					// Not done yet, trigger facebox loading state and lookup the next one
					fbox.loading();
					locationLookupHelper(fbox, set, n+1);
				}
			}

			if (set[n].latlng) {
				// This one's already resolved, skip the ajax
				callback(set[n].latlng);
			} else {
				// Lookup the string address and resolve it to a latlng
				new Ajax.Request('/ajax/',{
					parameters: {'action':'locationSearch','location':set[n].str},
					onComplete: function(transport){
						var data = transport.responseJSON;
						if (data && data.locations) {
							if (data.locations.length == 1) {
								callback(data.locations[0]);
							} else {
								fbox.reveal(getDisambiguationList(data.locations,callback));
							}
						} else {
							fbox.reveal(transport.responseText);
						}
					}
				});
			}
		}

		// Make a little data structure to iterate through and carry
		// around our the source strings alongside the resolved latlngs
		function asLatLng(str) { return isLatLng(a) ? a : null; }

		var locationSet = [];
		locationSet.push({'str':a, 'latlng':asLatLng(a)});
		locationSet.push({'str':b, 'latlng':asLatLng(b)});

		new Facebox(function(){
			locationLookupHelper(this,locationSet,0);
		});
	}

	function loadDirections(a,b){
		if (a && b) {
			var str = 'from: '+a+' to: '+b;
			directions.clear();	// Doesn't seem to remove error messages
			resultsDiv.update(''); // So we clear the div out ourselves
			directions.load(str);
			map.updateMarkersWithAvailability();
			map.hideAvailabilityOptions();
		}
	}

	function formHandler(e){
		var a = panel.down('#locationA').value;
		var b = panel.down('#locationB').value;
		if (panel.down('#locationBLatLng')) {
			var bLatLng = panel.down('#locationBLatLng').value;
			if (bLatLng) b = bLatLng;
		}

		if (a && b) {
			// Resolve postcodes locally, but lookup full addresses with Google's geocoder API
			// geocodeLookup(a,b);
			// Resolve everything locally, temporarily(?) put in place at Travelodge's request
			locationLookup(a,b);
		}

		if (e) e.stop();
	}

	var btn = panel.down('#'+id+'Search');
	if (!btn) return;
	var form = $(btn.form);
	if (!form) return;

	var chk = panel.down('#stepByStep');
	if (chk) {
		chk.observe('click',function(e){
			if (chk.checked) {
				resultsDiv.show();
			} else {
				resultsDiv.hide();
			}
		});
	}

	form.observe('submit',formHandler);
	formHandler(); // For any pre-loaded input
}

function initParkingMap(data) {
	var id = 'parkingMap';
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id);
		if (map && data) {
			// map.maximumZoom = 16;
			// map.jumpToLatLng(parking.lat, parking.lng);
			var markersA = map.setMarkersWithType('hotel',[data.hotel],true);
			var markersB = map.setMarkersWithType('parking',[data.parking]);
			map.fitSpecifiedMarkers(markersA.concat(markersB));
		}
	}
	return map;
}

function initConfirmationMap(hotel) {
	if (!hotel) return;
	var id = 'confirmationMap' + hotel.id;
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id);
		if (map) {
			// map.maximumZoom = 16;
			map.jumpToLatLng(hotel.lat, hotel.lng);
			map.setMarkersWithType('hotel',[hotel],true);
		}
	}
	return map;
}

function initRegionalHotelsMap(data) {
	var id = 'regionalHotelsMap';
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id, {navigable:true});
		if (map && data) {
			var hotels = map.setMarkersWithType('hotel', data.hotels);
			var prices = map.setMarkersWithType('priceMedium', data.hotels, false);
			var centres = map.setMarkersWithType('cityCentre', [data.place]);
			var markers = hotels.concat(prices).concat(centres);

			map.fitSpecifiedMarkers(markers);
			map.setResetLocation();

			initMapShowCentre(map,data.place);
		}
		initMapReset(map);
	}
	return map;
}

function initSearchResultsMap(hotels) {
	var id = 'searchResultsMap';
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id, {navigable:true});
		if (map && hotels) {
			if (map.div.hasClassName('largeMap')) {
				var markersA = map.setMarkersWithType('hotel',hotels);
				var markersB = map.setMarkersWithType('price',hotels);
			} else {
				var markersA = map.setMarkersWithType('hotel',hotels,true);
				var markersB = map.setMarkersWithType('priceMedium',hotels);
			}
			map.fitSpecifiedMarkers(markersA.concat(markersB));
			map.setResetLocation();
			initMapReset(map);
		}
	}
	return map;
}

function initSaverResultsMap(hotels) {
	var id = 'saverResultsMap';
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id);
		if (map && hotels) {
			// var markersA = map.setMarkersWithType('availability',hotels);
			// var markersB = map.setMarkersWithType('price',hotels);
			map.fitMarkersWithType('hotel',hotels);
			map.updateMarkersWithAvailability(hotels);
			// map.setResetLocation();
			// initMapReset(map);
		}
	}
	return map;
}

function initSavedPlacesMap() {
	var id = 'savedPlacesMap';
	if ($(id) && GBrowserIsCompatible()) {
		var map = new TLMap(id);
		if (map) {
			// Load default markers
			new Ajax.Request('/ajax/',{
				parameters: {action:'getFriendsAndFamily'},
				onComplete: function(transport) {
					var friends = transport.responseJSON;
					showFriendsAndFamily(map,friends);
				}
			});
		}
	}
}

/* ---------------------------------------------------------------------- *\
	Helper functions for other map initialisers above
\* ---------------------------------------------------------------------- */

function hasPostcode(str) {
	return (str.match(/\b[A-Z]{1,2}[0-9A-Z]{1,2}[A-Z]? *[0-9][A-Z]{2}\b/i)) ? true:false;
}
function getPostcode(str) {
	return str.match(/\b[A-Z]{1,2}[0-9A-Z]{1,2}[A-Z]? *[0-9][A-Z]{2}\b/i)[0];
}

function isLatLng(str) {
	return (str.match(/^-?[0-9]+\.[0-9]+, -?[0-9]+\.[0-9]+$/)) ? true:false;
}

function strFromLatLng(loc) {
	if (!loc) return;
	// Accepts either our homebrew marker data object with lat/lng properties
	// or an instance of GLatLng (usually retrieved from GMarker.getLatLng())
	if (typeof loc == 'string') {
		// If we're passed in a string, "normalise" it. Sometimes
		// there's a comma with no space, sometimes there's spaces.
		var loc = loc.split(/ *, */).join(', ');
	} else {
		var lat = (typeof loc.lat == 'string') ? loc.lat : loc.lat();
		var lng = (typeof loc.lng == 'string') ? loc.lng : loc.lng();
		var loc = lat + ', ' + lng;
	}
	return loc;
}

function cmpLatLngs(latlngA,latlngB) {
	if (!latlngA || !latlngB) return;
	var latlngA = strFromLatLng(latlngA);
	var latlngB = strFromLatLng(latlngB);
	return (latlngA == latlngB) ? true:false;
}

function handleDirectionsError(directions,div) {
	// See documentation for a reference to the codes and their definitions:
	// http://code.google.com/apis/maps/documentation/reference.html#GGeoStatusCode
	var code = directions.getStatus().code;
	var mesg = null;
	var html = '';

	switch (code) {
	 	case G_GEO_BAD_REQUEST:			mesg = "The directions request could not be successfully parsed."; break;
		case G_GEO_SERVER_ERROR: 		mesg = "We couldn't process your request. Unfortunately, the reason for the failure is not known."; break;
	 	case G_GEO_MISSING_QUERY: 		mesg = "The directions request we received was empty."; break;
		case G_GEO_UNKNOWN_ADDRESS: 	mesg = "We couldn't find a corresponding geographic location for one of the specified addresses."; break;
	 	case G_GEO_UNAVAILABLE_ADDRESS:	mesg = "We can't return directions for part of the requested route due to legal or contractual reasons."; break;
	 	case G_GEO_UNKNOWN_DIRECTIONS:	mesg = "We couldn't compute directions between the locations requested. This may have been because there was no route available between the two points, or because we do not have routing data for that region."; break;
	 	case G_GEO_BAD_KEY: 			mesg = "This application's registration key is either invalid or does not match the domain for which it was given."; break;
	 	case G_GEO_TOO_MANY_QUERIES:	mesg = "This application has used up it's allotted number of requests for the current 24 hour period."; break;
		default:						mesg = "An unknown error occurred."; break;
	}

	html += "<h4>Sorry</h4>";
	html += "<p>" + mesg + "</p>";
	if (code) {
		html += "<p><small style='color:#aaa;'>Error code: " + code + "</small></p>";
	}

	// div.update(html);
	new Facebox(html);
}

/* ---------------------------------------------------------------------- *\
	Travelodge map object: wraps the GMaps API with helpers and defaults
\* ---------------------------------------------------------------------- */

function TLMap(id,options) {
	// Get the map div and assign a new Google Map instance to it
	this.id = id;
	this.div = $(this.id);
	this.map = new GMap2(this.div);
	this.fullsize = this.div.hasClassName('largeMap');
	this.options = options;

	// Get a reference to the lefthand panel controls
	this.panel = $(this.id + 'Panel');

	// Set some default location and zoom level info
	this.locations = {
		'gb': new GLatLng(54.55, -4.35),
		'es': new GLatLng(40.25, -4.2)
	};
	this.defaultLocation = this.locations['gb'];
	this.defaultZoom = this.fullsize ? 6:5;
	this.bigIconZoom = this.defaultZoom + 4;
	this.maximumZoom = 16;

	// Set the map focus
	this.map.setCenter(this.defaultLocation, this.defaultZoom);
	this.setResetLocation();

	// Set up an icon manager, a holding bag for references to markers and a
	// global marker manager (a seperate cluster manager is also used in places)
	this.icons = new TLMapIconSet();
	this.markers = {};
	this.manager = new GMarkerManager(this.map);

	if (this.fullsize || (this.options && this.options.navigable)) {
		this.clustering = new TLClusterManager(this,{'clusteringEnabled':true});

		// Position controls and allow for mousewheel zooming
		var posTL = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(10,10));
		var posTR = new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(10,10));

		this.map.addControl(new GMapTypeControl(), posTL);
		this.map.addControl(new GLargeMapControl(), posTR);
		this.map.enableScrollWheelZoom();
		GEvent.addDomListener(this.div, "DOMMouseScroll", this.handleMouseWheel);
		GEvent.addDomListener(this.div, "mousewheel", this.handleMouseWheel);

		this.limitZoom();
		this.limitPan(); // See comments in method

	} else {
		var self = this;
		GEvent.addListener(this.map, 'click', function(overlay,latlng) {
			self.setAvailabilityLocation(latlng);
		});

		this.map.disableDoubleClickZoom();
		// this.map.disableDragging();
	}

	// Draw blue border and rounded corners around the map
	this.createStyledOverlay();

	// Clean up when we close, or navigate away from, the current page
	Event.observe(window, 'unload', GUnload);
}

TLMap.prototype.createStyledOverlay = function() {
	// Draw a border and rounded corners over the main map div.
	// Have to do this with individual keylines rather than a
	// bordered div so that the map is still clickable underneath.
	// Overlapping divs would steal click events from the map below.

	var W = this.div.getWidth();
	var H = this.div.getHeight();

	var version = navigator.appVersion.split("MSIE");
	if (version) {
		version = parseFloat(version[1]);
		if (version >= 5.5 && version < 7 && document.body.filters) {
			var isIE6 = true;
		}
	}

	function createCorner(x,y, w,h, img) {
		var src = '/assets/img/maps/hotels/brushes/corners/' + img + '.png';
		var div = new Element('div');
		div.setStyle({left: x + 'px', top: y + 'px'});
		div.setStyle({width: w + 'px', height: h + 'px', position:'absolute'});
		if (!isIE6) {
			div.setStyle({background:'url(' + src + ') no-repeat'});
		} else {
			// IE6 PNG transparency fix
			// div.setStyle({filter:"progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + src + "', sizing='scale')"});
		}
		return div;
	}

	function createBorder(x,y, w,h) {
		var div = new Element('div');
		div.setStyle({left: x + 'px', top: y + 'px'});
		div.setStyle({width: w + 'px', height: h + 'px', position:'absolute'});
		if (h > 1) {
			div.setStyle({background:'#8ed8f8'});
		} else {
			// Fix for difficulties in IE6 with displaying a 1px high div. Font-size and line-height
			// help to reduce it down to 2px, but can't find a way to get it down below that.
			div.setStyle({borderTop:'1px solid #8ed8f8', fontSize:'1px', lineHeight:'1px'});
		}
		return div;
	}

	var wrap = new Element('div');
	wrap.setStyle({position:'relative'});
	wrap.setStyle({top:(-H)+'px'});

	var borders = [ [0,0, 1,0], [0,1, 1,0], [0,0, 0,1], [1,0, 0,1]];
	borders.each(function(border){
		var w = (border[2] ? W:1);
		var h = (border[3] ? H:1);
		wrap.insert(createBorder((W-w) * border[0], (H-h) * border[1], w,h));
	});

	var corners = [ [0,0, 'tl'], [1,0, 'tr'], [1,1, 'br'], [0,1, 'bl'] ];
	corners.each(function(corner){
		var w = 6;
		var h = 6;
		wrap.insert(createCorner((W-w) * corner[0], (H-h) * corner[1], w,h, corner[2]));
	});

	this.div.insert({after:wrap});
}

TLMap.prototype.showAvailabilityOptions = function() {
	if (this.panel) {
		var availOpts = this.panel.down('.step2');
		if (availOpts) availOpts.show();
	}
}
TLMap.prototype.hideAvailabilityOptions = function() {
	if (this.panel) {
		var availOpts = this.panel.down('.step2');
		if (availOpts) availOpts.hide();
	}
}
TLMap.prototype.setAvailabilityLocation = function(latlng) {
	// Set the value of the location field in the
	// availability form to the specified coord.
	var coord = (latlng) ? latlng.lat() + ", " + latlng.lng() : '';
	var field = this.getAvailabilityLocationField();
	if (field) field.value = coord;
}
TLMap.prototype.getAvailabilityLocation = function() {
	var field = this.getAvailabilityLocationField();
	if (field) return field.value;
}
TLMap.prototype.getAvailabilityLocationField = function() {
	if (this.panel) {
		var field = this.panel.down('#location');
		if (!field) {
			// The above selector won't work in IE for whatever
			// reason, have to find it another way...
			var inputs = this.panel.select('input');
			for (var i=0; i<inputs.length; i++) {
				if (inputs[i].id == 'location') var field = inputs[i];
			}
		}
		return field;
	}
}
TLMap.prototype.setFriendsAndFamilyLocation = function(latlng) {
	// Select the specified location in the friendsList
	// dropdown in the availability form.
	var coord = latlng.lat() + ", " + latlng.lng();
	if (this.panel) {
		var field = this.panel.down('.jsFriendsList');
		if (field) {
			for (var i=0; i<field.options.length; i++) {
				if (field.options[i].value == coord) break;
			}
			field.selectedIndex = i;
		}
	}
}
TLMap.prototype.setAvailabilityDate = function(day,month) {
	// Cycle through the day & month dropdown options and
	// find values matching the inputs, then select these.
	if (this.panel) {
		var daySel = this.panel.down('.jsDaySelect');
		var monSel = this.panel.down('.jsMonSelect');
		if (daySel && monSel) {
			for (var i=0; i<daySel.options.length; i++) {
				if (daySel.options[i].value == day) {
					daySel.selectedIndex = i; break;
				}
			}
			for (var i=0; i<monSel.options.length; i++) {
				if (monSel.options[i].value == month) {
					monSel.selectedIndex = i; break;
				}
			}
		}
	}
}

TLMap.prototype.reset = function(){
	this.map.setCenter(this.resetLocation, this.resetZoom);
	this.removeMarkersWithType('searchLocation');
	this.updateMarkersWithAvailability();
	this.hideAvailabilityOptions();
	this.setAvailabilityLocation();
}
TLMap.prototype.setResetLocation = function() {
	this.resetLocation = this.map.getCenter();
	this.resetZoom = this.map.getZoom();
}

TLMap.prototype.panToUK = function(){ this.panToLocation(this.locations['gb']); }
TLMap.prototype.panToSpain = function(){ this.panToLocation(this.locations['es']); }

TLMap.prototype.panToLocation = function(location){
	this.defaultLocation = location;
	this.map.setZoom(this.defaultZoom);

	var p1 = this.map.fromLatLngToDivPixel(this.map.getCenter());
	var p2 = this.map.fromLatLngToDivPixel(location);

	var dx = p1.x - p2.x;
	var dy = p1.y - p2.y;
	var dz = new GSize(dx,dy);

	// Doesn't seem to be any "panning ended" style event handlers so I'm taking the hacky approach
	// to stop us doing any limit checking while we're animating, and then to reset the limit after
	this.panningTo = location;
	this.map.panBy(dz);
}

TLMap.prototype.jumpToLatLng = function(lat,lng,zoom){
	if (!zoom) {
		var zoom = this.maximumZoom - 2;
	}
	var loc = new GLatLng(lat,lng);
	this.map.setCenter(loc,zoom);
	this.setAvailabilityLocation(loc);
}

TLMap.prototype.zoomToShowHotel = function() {
	var markers = (this.markers.hotel) ? this.markers.hotel : this.markers.availability;
	if (!markers) return;

	var center = this.map.getCenter();
	var bounds = this.map.getBounds();

	var nearest = null;
	var d1 = null;
	for (var i = markers.length-1; i>=0; i--) {
		var latlng = markers[i].getLatLng();
		// If we find any markers within the current bounds, we don't need to continue
		if (bounds.containsLatLng(latlng)) return;
		// Otherwise, try and find the nearest one
		var d2 = latlng.distanceFrom(center);
		if (d2 < d1 || d1 === null) {
			d1 = d2;
			nearest = markers[i];
		}
	}

	if (nearest) {
		bounds.extend(nearest.getLatLng());
		var zoomLvl = this.map.getBoundsZoomLevel(bounds) - 1;
		if (zoomLvl > this.maximumZoom) zoomLvl = this.maximumZoom;
		if (zoomLvl < this.defaultZoom) zoomLvl = this.defaultZoom;
		if (zoomLvl < this.map.getZoom()) {
			this.map.setCenter(center, zoomLvl);
		}
	}
}

TLMap.prototype.zoomToShowAvailability = function(locations) {
	// This method is based on TLMap.prototype.fitSpecifiedMarkers

	var bounds = new GLatLngBounds();
	for (var i = locations.length-1; i>=0; i--) {
		var latlng = new GLatLng(locations[i].lat, locations[i].lng);
		bounds.extend(latlng);
	}

	// Go one greater than the suggested zoom level to try and make sure
	// that the map is filled with availability data without outliers which
	// haven't been searched upon.
	var zoomLvl = this.map.getBoundsZoomLevel(bounds) + 1;

	// Only zoom in, don't zoom out
	if (zoomLvl > this.map.getZoom()) {
		if (zoomLvl > this.maximumZoom) {
			zoomLvl = this.maximumZoom;
		}
		this.map.setCenter(bounds.getCenter(), zoomLvl);
	} else {
		// If we're already zoomed in, make sure that at least one hotel is visible
		this.zoomToShowHotel();
	}
}

TLMap.prototype.limitZoom = function() {
	// Set a minimum zoom level by overwriting the getMinimumResolution() method
	var self = this;
	var mt = this.map.getMapTypes();
	for (var i=0; i<mt.length; i++) {
		mt[i].getMinimumResolution = function() { return self.defaultZoom; }
		mt[i].getMaximumResolution = function() { return self.maximumZoom; }
	}
}
TLMap.prototype.limitPan = function() {
	// Doesn't work for now, can't limit panning when focussed on a country
	// and still allow the "show spanish hotels" link to work. We can temporarily
	// prevent checking of limit, but can't restart it again because we have no
	// way of knowing when the pan animation has stopped.

	// Set an allowed region which the map's center point must stay within
	this.setLimitedPan();
	// Add a move listener to restrict the bounds range
	// GEvent.bind(this.map, 'move', this, this.panHandler);
}

TLMap.prototype.setLimitedPan = function() {
	var pad = 0;
	var bounds = this.map.getBounds();
	var sw = bounds.getSouthWest();
	var ne = bounds.getNorthEast();
	var sw = new GLatLng( sw.lat()-pad, sw.lng()-pad );
	var ne = new GLatLng( ne.lat()+pad, ne.lng()+pad );

	this.limit = new GLatLngBounds(sw, ne);
	// console.log("Set limit to center on: "+ this.limit.getCenter());
}

TLMap.prototype.panHandler = function() {

	// If we're doing an animated pan of the map, don't check limits until we're done
	if (this.panningTo) {
		var c = this.map.getCenter();
		// console.log("Panning to: "+ this.panningTo.lat() +", "+ this.panningTo.lng());
		// console.log("Center at:  "+ c.lat() +", "+ c.lng());
		// console.log("Distance?:  "+ c.distanceFrom(this.panningTo));
		if (c.distanceFrom(this.panningTo) < 1000) {
			this.setLimitedPan();
			this.panningTo = null;
		} else {
			return;
		}
	}

	var bounds = this.map.getBounds();

	// If the map position is in range, just return
	if (this.limit.containsBounds(bounds)) return;

	var lat1 = bounds.getSouthWest().lat();
	var lng1 = bounds.getSouthWest().lng();
	var lat2 = bounds.getNorthEast().lat();
	var lng2 = bounds.getNorthEast().lng();

	var minLat = this.limit.getSouthWest().lat();
	var minLng = this.limit.getSouthWest().lng();
	var maxLat = this.limit.getNorthEast().lat();
	var maxLng = this.limit.getNorthEast().lng();

	var dlat = 0, dlng = 0;

	if (lat1 < minLat) dlat = minLat - lat1;
	if (lat2 > maxLat) dlat = maxLat - lat2;
	if (lng1 < minLng) dlng = minLng - lng1;
	if (lng2 > maxLng) dlng = maxLng - lng2;

	var c = this.map.getCenter();
	var lat = c.lat() + dlat;
	var lng = c.lng() + dlng;

	var latlng = new GLatLng(lat,lng);
	// console.log('Current center is: ' + strFromLatLng(this.map.getCenter()));
	// console.log('Setting center to: ' + strFromLatLng(latlng));
	this.map.setCenter(latlng);
}

TLMap.prototype.handleMouseWheel = function(e) {
	// Stop scrollwheel event bubbling up when mouse is over map div
	// (Code nabbed from elsewhere, seems to work, but it may not be ideal)
	if (e.cancelable) e.preventDefault();
	return false;
}

/* ---------------------------------------------------------------------- *\
	Dealing with markers
\* ---------------------------------------------------------------------- */

TLMap.prototype.updateMarkersWithAvailability = function(locations) {
	var markers = this.markers['hotel'];
	if (!markers) {
		markers = this.setMarkersWithType('availability',locations);
	}
	if (locations) {
		var selectedLatLng = this.getAvailabilityLocation();

		for (var i=0; i<markers.length; i++) {
			var marker = markers[i];
			for (var j=locations.length-1; j>=0; j--) {
				if (locations[j].id == marker.id) {
					marker.setImage(this.icons.availabilityIconImage(locations[j].typ));

					// If this marker is also the currently selected location, then its infoWindow
					// is probably open. Trigger its click event to re-open it & update its contents.
					if (selectedLatLng && cmpLatLngs(selectedLatLng, marker.getLatLng())) {
						GEvent.trigger(marker,'click');
					}

					// delete locations[j];
					break;
				}
			}
		}

		this.setMarkersWithType('price',locations);
	} else {
		// If no locations were passed in, we want to clear all availability visuals.
		// Change any black & white hotel icons back to colour and remove price bubbles.
		var img = this.icons.availabilityIconImage();
		for (var i=0; i<markers.length; i++) {
			var marker = markers[i];
			if (marker.image != img) {
				marker.setImage(img);
			}
		}
		this.removeMarkersWithType('price');
	}
}

TLMap.prototype.removeMarkers = function() {
	this.removeMarkersWithType('default');
}
TLMap.prototype.setMarkers = function(locations) {
	return this.setMarkersWithType('default',locations);
}
TLMap.prototype.fitMarkers = function(locations) {
	return this.fitMarkersWithType('default',locations);
}

TLMap.prototype.removeMarkersWithType = function(type) {
	var markers = this.markers[type];
	if (markers) {
		for (var i = markers.length-1; i>=0; i--) {
			this.map.removeOverlay(markers[i]);
		}
	}
}

TLMap.prototype.setMarkersWithType = function(type,locations,inert) {
	this.removeMarkersWithType(type);

	this.markers[type] = [];
	if (locations) {
		for (var i=0; i<locations.length; i++) {
			if (locations[i].lat == 0 && locations[i].lng == 0) continue;
			var marker = this[type + 'Marker'](locations[i],inert);
			if (marker) {
				if (locations[i].id) marker.id = locations[i].id;
				this.markers[type].push(marker);
				this.map.addOverlay(marker);
			}
		}
	}

	if (this.clustering) {
		this.clustering.updateMarkers();
	}
	return this.markers[type];
}

TLMap.prototype.fitMarkersWithType = function(type,locations) {
	var markers = this.setMarkersWithType(type,locations);
	this.fitSpecifiedMarkers(markers);
	return markers;
}

TLMap.prototype.fitSpecifiedMarkers = function(markers) {
	var bounds = new GLatLngBounds();
	for (var i = markers.length-1; i>=0; i--) {
		bounds.extend(markers[i].getLatLng());
	}

	var zoomLvl = this.map.getBoundsZoomLevel(bounds);
	if (zoomLvl > this.maximumZoom) {
		zoomLvl = this.maximumZoom;
	}

	this.map.setCenter(bounds.getCenter(), zoomLvl);
}

TLMap.prototype.defaultMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng,{clickable:false});
	return marker;
}
TLMap.prototype.tinyMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.tinyIcon(), true);
	return marker;
}
TLMap.prototype.poiMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.poiIcon());
	var self = this;
	GEvent.addListener(marker, 'click', function(){
		self.showAvailabilityOptions();
		self.setAvailabilityLocation(this.getLatLng());
		new Ajax.Request('/ajax/', {
			parameters: {'action':'poiInfo', 'id':location.id},
			onSuccess: function(transport){ marker.openInfoWindow(transport.responseText); }
		});
	});
	GEvent.addListener(marker, 'infowindowopen', function(){
		var win = self.map.getInfoWindow();
		if (!win) return;
		var div = $(win.getContentContainers()[0]);
		if (!div) return;
		div.select('a').each(function(link){
			link.observe('click',function(e){
				var btn = self.panel.down('.jsCheckAvailability');
				if (btn) btn.click();
				e.stop();
			});
		});
	});
	return marker;
}
TLMap.prototype.hotelPoiMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.poiIcon());
	var self = this;
	GEvent.addListener(marker, 'click', function(){
		new Ajax.Request('/ajax/', {
			parameters: {'action':'hotelPoiInfo', 'id':location.id},
			onSuccess: function(transport){ marker.openInfoWindow(transport.responseText); }
		});
	});
	return marker;
}
TLMap.prototype.transportMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.transportIcon());
	var self = this;
	GEvent.addListener(marker, 'click', function(){
		new Ajax.Request('/ajax/', {
			parameters: {'action':'transportInfo', 'id':location.id},
			onSuccess: function(transport){ marker.openInfoWindow(transport.responseText); }
		});
	});
	return marker;
}
TLMap.prototype.eventMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.eventIcon());
	var self = this;
	GEvent.addListener(marker, 'click', function(){
		self.showAvailabilityOptions();
		self.setAvailabilityLocation(this.getLatLng());
		self.setAvailabilityDate(location.day,location.mon);
		new Ajax.Request('/ajax/', {
			parameters: {'action':'eventInfo', 'id':location.id},
			onSuccess: function(transport){ marker.openInfoWindow(transport.responseText); }
		});
	});
	GEvent.addListener(marker, 'infowindowopen', function(){
		var win = self.map.getInfoWindow();
		if (!win) return;
		var div = $(win.getContentContainers()[0]);
		if (!div) return;
		div.select('a').each(function(link){
			link.observe('click',function(e){
				var btn = self.panel.down('.jsCheckAvailability');
				if (btn) btn.click();
				e.stop();
			});
		});
	});
	return marker;
}
TLMap.prototype.friendsMarker = function(location,inert) {
	if (!inert) inert = false;
	var latlng = new GLatLng(location.lat, location.lng);
	var opts = { icon: this.icons.friendsIcon(), clickable: !inert };
	if (location.name) opts.title = location.name;
	var marker = new GMarker(latlng, opts);

	if (!inert) {
		var self = this;
		var isSavedPlacesMap = (self.map.getContainer().id == 'savedPlacesMap') ? 1:0;

		GEvent.addListener(marker, 'click', function(){
			self.showAvailabilityOptions();
			self.setFriendsAndFamilyLocation(marker.getLatLng());
			new Ajax.Request('/ajax/', {
				parameters: {'action':'friendsAndFamilyInfo', 'id':location.id, 'showHotelsLink':isSavedPlacesMap},
				onSuccess: function(transport){ marker.openInfoWindow(transport.responseText); }
			});
		});

		GEvent.addListener(marker, 'infowindowopen', function(){
			var win = self.map.getInfoWindow();
			if (!win) return;
			var div = $(win.getContentContainers()[0]);
			if (!div) return;
			var link = div.down('a#removeLink');
			link.observe('click',function(e){
				new Ajax.Request('/ajax/', {
					parameters: {'action':'removeFriendsAndFamily', 'id':location.id},
					onSuccess: function(transport){
						var data = transport.responseJSON;
						if (data) showFriendsAndFamily(self,data);
					}
				});
				e.stop();
			});
		});

	}
	return marker;

}
TLMap.prototype.hotelMarker = function(location,inert) {
	if (!inert) inert = false;
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, {
		icon: this.icons.availabilityIcon(location.typ),
		title: (location.name) ? location.name : 'Click to see details of this hotel.',
		clickable: !inert
	});
	if (!inert) {
		this.hotelMarkerInitialiser(marker, location);
	}
	return marker;
}
TLMap.prototype.availabilityMarker = TLMap.prototype.hotelMarker

TLMap.prototype.priceMediumMarker = function(location,inert) {
	var inert = (typeof inert == 'undefined') ? true : inert;
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.priceIcon(location,'med'), inert);
	if (!inert) {
		this.hotelMarkerInitialiser(marker, location);
	}
	return marker;
}
TLMap.prototype.priceMarker = function(location,inert) {
	if (location.typ == 'none' || !location.cost) return;
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.priceIcon(location));
	this.hotelMarkerInitialiser(marker, location);
	return marker;
}
TLMap.prototype.searchLocationMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.poiIcon(), true);
	return marker;
}
TLMap.prototype.cityCentreMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.cityCentreIcon(), true);
	return marker;
}
TLMap.prototype.parkingMarker = function(location,inert) {
	var latlng = new GLatLng(location.lat, location.lng);
	var marker = new GMarker(latlng, this.icons.parkingIcon(), true);
	return marker;
}

TLMap.prototype.hotelMarkerInitialiser = function(marker, location) {
	var self = this;
	GEvent.addListener(marker, 'click', function(){
		new Ajax.Request('/ajax/', {
			parameters: {'action':'hotelInfo', 'id':location.id},
			onSuccess: function(transport){
				marker.openInfoWindow(transport.responseText); // standard gmaps infowindow
				// marker.openTLInfoWindow(self.map, 'infoWindow', html); // extinfowindow
			}
		});
		/*
		marker.openTLInfoWindow(self.map, 'infoWindow', 'Loading...', {
			ajaxUrl:'/ajax/?action=hotelInfo&id=' + location.id
		});
		*/
	});
	GEvent.addListener(marker, 'infowindowopen', function(){
		var win = self.map.getInfoWindow();
		if (!win) return;
		var div = $(win.getContentContainers()[0]);
		if (!div) return;

		var link = div.down('a.jsDetailsLink');
		if (!link) return;
		link.href = '/ajax/?action=hotelInfoFull&id=' + location.id;
		new Facebox(link);

		var link = div.down('a.jsAvailabilityLink')
		if (!link) return;
		if (self.panel) {
			var btn = self.panel.down('.jsCheckAvailability');
		}
		if (btn) {
			link.observe('click',function(e){
				self.showAvailabilityOptions();
				self.setAvailabilityLocation(marker.getLatLng());
				btn.click();
				e.stop();
			});
		} else {
			// Hide the "Check availability" link if we're not on
			// an "Explore the UK" map with an availability form
			link.hide();
		}
	});
}

/* ---------------------------------------------------------------------- *\
	Map icon set manager: helps to prevent a proliferation of instances
\* ---------------------------------------------------------------------- */

function TLMapIconSet() {
	this.icons = {};
}

TLMapIconSet.prototype.tinyIcon = function() {
	var key = 'tiny';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = "/assets/img/maps/icons/hotel_tiny.png";
		icon.iconSize = new GSize(12, 10);
		icon.iconAnchor = new GPoint(6, 5);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.availabilityIcon = function(typ) {
	var key = 'availability:' + typ;
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = this.availabilityIconImage(typ);
		// icon.shadow = "/assets/img/maps/icons/locations/travelodge_shadow.png";
		icon.iconSize = new GSize(20, 16);
		// icon.shadowSize = new GSize(24, 20);
		icon.iconAnchor = new GPoint(10, 8);
		icon.infoWindowAnchor = new GPoint(10, 8);
		this.icons[key] = icon;
	}
	return this.icons[key];
}
TLMapIconSet.prototype.availabilityIconImage = function(typ) {
	return '/assets/img/maps/icons/locations/travelodge' + ((typ == 'none') ? '_unavailable' : '') + '.png'
}

TLMapIconSet.prototype.priceIcon = function(loc,size) {
	if (!size) var size = 'lrg';
	if (!loc.cost) loc.cost = 0;
	if (!loc.number) loc.number = '';
	if (!loc.letter) loc.letter = '';
	if (!loc.currency) loc.currency = '';

	var key = 'price:'+size+':'+loc.cost+':'+loc.letter+':'+loc.number+':'+loc.currency;
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = '/assets/img/maps/price.php?size=' + size + '&cost=' + loc.cost + '&letter=' + loc.letter + '&number=' + loc.number + '&currency=' + loc.currency + '&bogus=.png';
		icon.shadow = '/assets/img/maps/icons/prices/bubble_' + size + '_shadow.png';
		icon.iconSize = new GSize(55, (size=='lrg' ? 45 : 35));
		icon.shadowSize = new GSize(59, (size=='lrg' ? 49 : 39));
		icon.iconAnchor = new GPoint(10, (size=='lrg' ? 55 : 45));
		icon.infoWindowAnchor = new GPoint(27, 22);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.hotelIcon = function() {
	var key = 'hotel';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();

		// Standard single-lodge icon
		icon.image = "/assets/img/maps/icons/locations/travelodge.png";
		icon.shadow = "/assets/img/maps/icons/locations/travelodge_shadow.png";
		icon.iconSize = new GSize(20, 16);
		icon.shadowSize = new GSize(24, 20);
		icon.iconAnchor = new GPoint(10, 8);
		icon.infoWindowAnchor = new GPoint(10, 8);

		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.clusterIcon = function(n) {
	var key = 'cluster'+n;
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();

		// Maximum icon size/type is "5"
		var type = Math.min(5, Math.ceil((n+1) / 10));
		var size = 15 + (3 * type);

		// Dynamically sized multiple-lodge icon
		icon.image = '/assets/img/maps/icon.php?src=' + 'icons/cluster_' + type + '.png' + '&n=' + n + '&bogus=.png';
		icon.shadow = '/assets/img/maps/icons/cluster_' + type + '_shadow.png';
		icon.iconSize = new GSize(size, size);
		icon.shadowSize = new GSize(size+2, size+3);
		icon.iconAnchor = new GPoint(size/2, size/2);
		icon.infoWindowAnchor = new GPoint(size/2, size/2);

		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.cityCentreIcon = function() {
	var key = 'cityCentre';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = '/assets/img/maps/icons/city_centre.png';
		icon.shadow = '/assets/img/maps/icons/city_centre_shadow.png';
		icon.iconSize = new GSize(98, 35);
		icon.shadowSize = new GSize(101, 39);
		icon.iconAnchor = new GPoint(48, 55);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.poiIcon = function() {
	var key = 'poi';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = "/assets/img/maps/icons/locations/poi.png";
		// icon.shadow = "/assets/img/maps/icons/locations/generic_shadow.png";
		icon.iconSize = new GSize(21, 20);
		// icon.shadowSize = new GSize(24, 24);
		icon.iconAnchor = new GPoint(10, 10);
		icon.infoWindowAnchor = new GPoint(10, 10);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.eventIcon = function() {
	var key = 'event';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = "/assets/img/maps/icons/locations/event.png";
		// icon.shadow = "/assets/img/maps/icons/locations/generic_shadow.png";
		icon.iconSize = new GSize(21, 20);
		// icon.shadowSize = new GSize(24, 24);
		icon.iconAnchor = new GPoint(10, 10);
		icon.infoWindowAnchor = new GPoint(10, 10);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.transportIcon = function() {
	var key = 'transport';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = "/assets/img/maps/icons/locations/transport.png";
		// icon.shadow = "/assets/img/maps/icons/locations/generic_shadow.png";
		icon.iconSize = new GSize(21, 20);
		// icon.shadowSize = new GSize(24, 24);
		icon.iconAnchor = new GPoint(10, 10);
		icon.infoWindowAnchor = new GPoint(10, 10);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.parkingIcon = function() {
	var key = 'parking';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = "/assets/img/maps/icons/locations/parking02.png";
		// icon.shadow = "/assets/img/maps/icons/locations/generic_shadow.png";
		icon.iconSize = new GSize(21, 20);
		// icon.shadowSize = new GSize(24, 24);
		icon.iconAnchor = new GPoint(10, 10);
		icon.infoWindowAnchor = new GPoint(10, 10);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

TLMapIconSet.prototype.friendsIcon = function() {
	var key = 'friends';
	if (typeof(this.icons[key]) === 'undefined') {
		var icon = new GIcon();
		icon.image = "/assets/img/maps/icons/locations/friend.png";
		// icon.shadow = "/assets/img/maps/icons/locations/generic_shadow.png";
		icon.iconSize = new GSize(21, 20);
		// icon.shadowSize = new GSize(24, 24);
		icon.iconAnchor = new GPoint(10, 10);
		icon.infoWindowAnchor = new GPoint(10, 10);
		this.icons[key] = icon;
	}
	return this.icons[key];
}

/* ---------------------------------------------------------------------- *\
	Map cluster manager
\* ---------------------------------------------------------------------- */

/*
	This is a reworded version of the following:
	-----------------------------------------------------
	ClusterMarker Version 1.1.2
	A marker manager for the Google Maps API
	http://googlemapsapi.martinpearman.co.uk/clustermarker

	Copyright Martin Pearman 2008.
	Last updated 20th January 2008

	This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
	This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
	You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

function TLClusterManager(tlMap, options) {
	this.tlMap = tlMap;
	this.map = this.tlMap.map;

	this.markers = {};
	this.markersIconBounds = [];

	this.clusters = [];
	this.clustersEventListeners = [];

	if (typeof(options) == 'undefined') {
		options = {};
	}

	this.clusteringEnabled 	= (options.clusteringEnabled===false) 	? options.clusteringEnabled	: true;
	this.fitMapToMarkers 	= (options.fitMapToMarkers===false) 	? options.fitMapToMarkers 	: true;
	this.borderPadding 		= (options.borderPadding) 				? options.borderPadding 	: 256;
	this.intersectPadding 	= (options.intersectPadding) 			? options.intersectPadding 	: 3;

	this.cancelMoveEnd = false;
	GEvent.bind(this.map, 'moveend', this, this.moveEnd);
	GEvent.bind(this.map, 'zoomend', this, this.zoomEnd);
	GEvent.bind(this.map, 'maptypechanged', this, this.refresh);
}

TLClusterManager.prototype.toggleClustering = function(toggle) {
	this.clusteringEnabled = toggle;
	this.refresh();
}

TLClusterManager.prototype.moveEnd = function() {
	if (this.cancelMoveEnd) {
		this.cancelMoveEnd = false;
	} else {
		this.refresh();
	}
}

TLClusterManager.prototype.zoomEnd = function() {
	this.cancelMoveEnd = true;
	this.clusteringEnabled = (this.map.getZoom() > this.tlMap.bigIconZoom) ? false:true;
	this.refresh();
}

TLClusterManager.prototype.refresh = function() {
	// Remove any cluster markers and event listeners then clear both cluster
	// marker and event arrays. Use array pop method instead if quicker.

	this.removeClusters();

	this.flagContainedMarkers();
	this.flagIntersectingMarkers();

	this.showClusters();
	this.showMarkers();
}

TLClusterManager.prototype.updateMarkers = function() {
	this.markers = [];
	this.markersIconBounds = [];
	for (key in this.tlMap.markers) {
		if (!this.tlMap.markers.hasOwnProperty(key)) continue;
		if (/price|cityCentre/.test(key)) continue;
		for (var i = this.tlMap.markers[key].length-1; i>=0; i--) {
			this.tlMap.markers[key][i].isVisible = false;
			this.tlMap.markers[key][i].hide();
			this.markers.push(this.tlMap.markers[key][i]);
		}
	}
	this.refresh();
}

TLClusterManager.prototype.showClusters = function() {
	for (var i = this.clusters.length-1; i>=0; i--) {
		this.map.addOverlay(this.clusters[i]);
	}
}

TLClusterManager.prototype.removeClusters = function() {
	for (var i = this.clusters.length-1; i>=0; i--) {
		this.map.removeOverlay(this.clusters[i]);
	}
	for (var i = this.clustersEventListeners.length-1; i>=0; i--) {
		GEvent.removeListener(this.clustersEventListeners[i]);
	}
	this.clusters = [];
	this.clustersEventListeners = [];
}

TLClusterManager.prototype.showMarkers = function() {
	for (var i = this.markers.length-1; i>=0; i--) {
		var marker = this.markers[i];
		if (!marker.isVisible && marker.makeVisible) {
			marker.show();
			marker.isVisible = true;
		}
		if (marker.isVisible && !marker.makeVisible) {
			marker.hide();
			marker.isVisible = false;
		}
	}
}

TLClusterManager.prototype.flagContainedMarkers = function() {
	// Iterate through the markers and only set makeVisible to
	// true if the marker is inside the map bounds & padding

	var pad = this.borderPadding;

	var zoom 		= this.map.getZoom();
	var projection 	= this.map.getCurrentMapType().getProjection();
	var bounds 		= this.map.getBounds();

	var sw = bounds.getSouthWest();
	sw = projection.fromLatLngToPixel(sw, zoom);
	sw = new GPoint(sw.x-pad, sw.y+pad);
	sw = projection.fromPixelToLatLng(sw, zoom, true);

	var ne = bounds.getNorthEast()
	ne = projection.fromLatLngToPixel(ne, zoom);
	ne = new GPoint(ne.x+pad, ne.y-pad);
	ne = projection.fromPixelToLatLng(ne, zoom, true);

	var paddedBounds = new GLatLngBounds(sw, ne);

	for (var i = this.markers.length-1; i>=0; i--) {
		var marker = this.markers[i];
		marker.makeVisible = paddedBounds.containsLatLng(marker.getLatLng()) ? true:false;
	}
}

TLClusterManager.prototype.flagIntersectingMarkers = function() {
	var zoom = this.map.getZoom();

	if (this.clusteringEnabled && zoom < this.map.getCurrentMapType().getMaximumResolution()) {

		// Iterate through the markers and make clusters using the indices
		// of those that overlap, setting makeVisible to false in the process

		if (typeof(this.markersIconBounds[zoom]) == 'undefined') {
			this.markersIconBounds[zoom] = this.getMarkersBoundsArray();
		}

		for (var i = this.markers.length-1; i>=0; i--) {
			var marker = this.markers[i];
			if (marker.makeVisible) {
				var clusterGroup = [];
				for (var j = i-1; j>=0; j--) {
					var ib1 = this.markersIconBounds[zoom][i];
					var ib2 = this.markersIconBounds[zoom][j];
					if (this.markers[j].makeVisible && ib1.intersects(ib2)) {
						clusterGroup.push(j);
					}
				}
				if (clusterGroup.length !== 0) {
					clusterGroup.push(i);
					for (var j = clusterGroup.length-1; j>=0; j--) {
						this.markers[clusterGroup[j]].makeVisible = false;
					}
					this.clusters.push(this.clusterMarker(clusterGroup));
				}
			}
		}
	}
}

TLClusterManager.prototype.getMarkersBoundsArray = function() {
	// Returns an array of icon bounds for the map's current zoom level
	var iconBounds = [];

	var pad = 2; // this.intersectPadding;
	var projection = this.map.getCurrentMapType().getProjection();
	var zoom = this.map.getZoom();

	for (var i = this.markers.length-1; i>=0; i--) {
		var marker = this.markers[i];

		var iconSize = marker.getIcon().iconSize;
		var w = iconSize.width;
		var h = iconSize.height;

		var p1 = projection.fromLatLngToPixel(marker.getLatLng(), zoom);
		var p2 = marker.getIcon().iconAnchor;

		var sw = new GPoint(p1.x-p2.x-pad, 	 p1.y-p2.y+pad+h);
		var ne = new GPoint(p1.x-p2.x+pad+w, p1.y-p2.y-pad);
		sw = projection.fromPixelToLatLng(sw, zoom, true);
		ne = projection.fromPixelToLatLng(ne, zoom, true);

		iconBounds[i] = new GLatLngBounds(sw, ne);
	}

	return iconBounds;
}

TLClusterManager.prototype.setMapToFitMarkers = function() {
	var bounds = new GLatLngBounds();
	for (var i = this.markers.length-1; i>=0; i--) {
		var marker = this.markers[i];
		bounds.extend(marker.getLatLng());
	}
	this.map.setCenter(bounds.getCenter(), this.map.getBoundsZoomLevel(bounds));
}

TLClusterManager.prototype.clusterMarker = function(indices) {
	var bounds = new GLatLngBounds();
	for (var i = indices.length-1; i>=0; i--) {
		bounds.extend(this.markers[indices[i]].getLatLng());
	}

	var latLng = bounds.getCenter();
	var marker = new GMarker(latLng, {
		icon: this.tlMap.icons.clusterIcon(indices.length),
		title: 'Click to zoom in and see '+indices.length+' locations.'
	});

	var self = this; // Curry map reference into event handler
	var listen = GEvent.addListener(marker, 'click', function() {
		// Center & zoom map to contain all map markers in cluster group when clicked
		self.map.setCenter(bounds.getCenter(), self.map.getBoundsZoomLevel(bounds));
		self.tlMap.setAvailabilityLocation(bounds.getCenter());
		self.tlMap.showAvailabilityOptions();
	})
	this.clustersEventListeners.push(listen);

	return marker;
}
