var map = {
	
	// Either 'single' or 'categories'.
	type: 'single',
	
	// Centre point and initial zoom.
	lat: 0,
	lon: 0,
	zoom: 10,
	
	// The category of objects we'll be displaying (if type is 'categories').
	categoryId: 0,
	
	// Will be the Google Maps object.
	gmap: null,
	
	// Will be an array of objects containing data for markers.
	// Each object/hash has lon, lat, label and html keys.
	markersData: [],
	
	// Will be an array of Google Map marker objects.
	gmarkers: [],
	
	// Could be an array of data about shapes. The 'coords' key will be like:
	// "51.513558,-0.104268;51.513552,-0.104518;..."
	// There will also be an 'html' key for the infowindow.
	shapesData: [],
	
	// Will be an array of Google Map polyline/shape objects.
	gshapes: [],
	
	// For the large map displaying everything in a category, we store the labels for the sidebar
	// here. Like "Bride Lane" => 6726.
	labelsData: {},
	
	// In category maps, this will be an alphabetical list of all the labels/keys in map.labelsData.
	labelsDataKeys: [],
	
	// We store the currently-open infowindow here so we can close it when necessary.
	openInfowindow: null,
	
	// Path to where the map marker images are kept.
	markersPath: '/images/maps/markers/',
	
	initialize: function(options) {
		
		if (options.type) {
			map.type = options.type;
		}
		if (options.lat) {
			map.lat = options.lat;
		}
		if (options.lon) {
			map.lon = options.lon;
		}
		if (options.zoom) {
			map.zoom = options.zoom;
		}
		
		if (options.categoryId) {
			map.categoryId = options.categoryId;
		}
		
		if (options.markers) {
			// We can pass an array of hashes in as markers, which we do for 'single' maps.
			// For 'categories' maps we will read the data in from an XML file.
			map.markersData = options.markers;
		}
		
		if (options.shapes) {
			// We can pass an array of hashes in as markers, which we do for 'single' maps.
			// For 'categories' maps we will read the data in from an XML file.
			map.shapesData = options.shapes;
		}
		
		map.initializeMap();
	},
	
	
	/**
	 * Stuff that's common to both kinds of map.
	 */
	initializeMap: function() {
		map.gmap = new google.maps.Map(
			document.getElementById('map'), 
			{
				zoom: map.zoom,
				center: new google.maps.LatLng(map.lat, map.lon),
				mapTypeId: google.maps.MapTypeId.ROADMAP
			}
		);
		
		map.infoWindow = new google.maps.InfoWindow({
			maxWidth: 250
		});
		
		if (map.type == 'categories') {
			map.initializeCategoriesMap();
		} else if (map.type == 'single') {
			map.initializeSingleMap();
		}
	},
	

	/**
	 * The big map that displays lots of locations from one category.
	 */
	initializeCategoriesMap: function() {
		map.setSidebarBottomHeight();
		
		map.createMarkers();
		
		// Don't put things here that require createMarkers() to have run, as the AJAX
		// can take time to complete and the order will be wrong.
		
		// Set up the clickable sidebar of locations.
		$('ol.locations').find('a').live('click', function(){
			// Each anchor will have a CSS id like 'm34' or 's35'.
			
			// Either 'm' or 's' (marker or shape).
			var type = $(this).attr('id').substring(0,1);
			var idx = $(this).attr('id').substring(1);
			
			if (type == 'm') {
				google.maps.event.trigger(map.gmarkers[idx], 'click');
			} else {
				google.maps.event.trigger(map.gshapes[idx], 'click');
			}
			return false;
		});
		
		// Do this again whenever the window size is changed.
		$(window).bind('resize', function() {
			map.setSidebarBottomHeight();
		});
	},
	
	
	/**
	 * The small map on an individual entry page in the Encyclopedia.
	 */
	initializeSingleMap: function() {
		map.drawMarkers();
		map.drawShapes();
	},
	
	
	/**
	 * Loads the data from xml.php for map.categoryId and then calls drawMarkers().
	 */
	createMarkers: function() {
		$.ajax({
			type: 'GET',
			url: 'xml.php',
			dataType: 'xml',
			data: {c: map.categoryId},
			success: function(xml) {
				$(xml).find('marker').each(function(){
					map.markersData.push({
						lat: parseFloat($(this).attr('lat')),
						lon: parseFloat($(this).attr('lng')),
						html: $(this).attr('html'),
						label: $(this).attr('label')
					});
				});
				$(xml).find('shape').each(function(){
					map.shapesData.push({
						coords: $(this).attr('latlng'),
						// The lat/lon are used for the position of the shape's infowindow.
						lat: parseFloat($(this).attr('lat')),
						lon: parseFloat($(this).attr('lng')),
						html: $(this).attr('html'),
						label: $(this).attr('label')
					});
				});

				// We need a combined list of all markers and shapes so that we can list them 
				// alphabetically in the sidebar, and give the markers the correct number
				// according to their position amidst the markers+shapes list.
				$.each(map.markersData, function(n, marker){
					if (n > 1) {
						// Don't use the first two markers, which are always Pepys' homes.
						map.labelsData[marker.label] = 'm'+n;
					}
				});
				$.each(map.shapesData, function(n, shape){
					map.labelsData[shape.label] = 's'+n;
				});
				
				// Sort the labels, as the marker and shape labels will be one after the other, not
				// in alphabetical order.
				for (var label in map.labelsData) {
					if (label === 'length' || !map.labelsData.hasOwnProperty(label)) continue;
					map.labelsDataKeys.push(label);
				}
				map.labelsDataKeys.sort();

				// Now, draw!
				map.drawMarkers();
				map.drawShapes();
				map.drawSidebarList();
			},
			error: function(XMLHttpRequest, textStatus, errorThrown) {
				map.error("Can't fetch XML location data : "+textStatus + ', '+errorThrown);
			}
		});	
	},
	
	
	/**
	 * Just draws the markers that are in map.markersData.
	 */
	drawMarkers: function() {
		
		if (map.markersData.length == 0) {
			return;
		}

		$.each(map.markersData, function(n, marker){
			if (map.markersData.length == 1) {
				icon_url = map.markersPath+'brown_Marker.png';
				
			} else if (map.type == 'categories') {
				if (n == 0) {
					// The first item is Pepys' home, so a special icon.
					// From http://www.google.com/mapfiles/markerA.png
					icon_url = map.markersPath+'markerA.png';
				} else if (n == 1) {
					// The second item is also Pepys' home, so a special icon.
					icon_url = map.markersPath+'markerB.png';
				} else {
					// Get the correct position of this marker in the combined list of
					// markers and keys, so the marker icon has the correct number.
					var idx = $.inArray(marker.label, map.labelsDataKeys) + 1; // 1-based.
					icon_url = map.markersPath+'brown_Marker' + idx + '.png';
				}
			} else {
				icon_url = map.markersPath+'brown_Marker' + (n+1) + '.png';
			}

			var mapMarker = new google.maps.Marker({
				position: new google.maps.LatLng(marker.lat, marker.lon), 
				map: map.gmap, 
				title: marker.label,
				icon: new google.maps.MarkerImage(
					icon_url,
					new google.maps.Size(20, 34),
					new google.maps.Point(0, 0),
					new google.maps.Point(9, 34)
				),
				shadow: new google.maps.MarkerImage(
					// From http://www.google.com/mapfiles/shadow50.png
					map.markersPath+'shadow50.png?v=2',
					new google.maps.Size(47, 34),
					new google.maps.Point(0, 0),
					new google.maps.Point(18, 25)
				)
			});
			map.gmarkers.push(mapMarker);
			
			google.maps.event.addListener(mapMarker, 'click', function() {
				map.infoWindow.close();
				map.infoWindow.setContent(marker.html);
				map.infoWindow.open(map.gmap, mapMarker);
			});
		});
	},
	
	
	/**
	 * Draws polylines and polygons.
	 * If the list of coordinates has identical first and last points, we draw a filled polygon.
	 * Otherwise, we draw a polyline.
	 */
	drawShapes: function() {
		if (map.shapesData.length == 0) {
			return;
		}
		
		$.each(map.shapesData, function(n, shape){
			// coords will now be an array of strings, each one like "-0.104268,51.513558".
			var coords = shape.coords.split(';');
			var path = [];
			$.each(coords, function(m, coord){
				var ll = coord.split(',');
				path.push(new google.maps.LatLng(ll[0], ll[1]));
			});
			
			if (coords[0] == coords[coords.length-1]) {
				// This is a polygon.
				var polything = new google.maps.Polygon({
					paths: path,
					strokeColor: '#663333',
					strokeOpacity: 1.0,
					strokeWeight: 1,
					fillColor: '#BDA450',
					fillOpacity: 0.5
				});
				
			} else {
				// This is a polyline.
				// Fat line, to act as a border.
				var polything = new google.maps.Polyline({
					path: path,
					strokeColor: '#663333',
					strokeOpacity: 1.0,
					strokeWeight: 6
				});
				polything.setMap(map.gmap);

				// Thin line.
				var polything = new google.maps.Polyline({
					path: path,
					strokeColor: '#BDA450',
					strokeOpacity: 1.0,
					strokeWeight: 4
				});
			}

			polything.setMap(map.gmap);

			map.gshapes.push(polything);

			google.maps.event.addListener(polything, 'click', function() {
				map.infoWindow.close();
				map.infoWindow.setContent(shape.html);
				map.infoWindow.setPosition(new google.maps.LatLng(shape.lat, shape.lon));
				map.infoWindow.open(map.gmap);
			});
		});
	},
	
	
	/**
	 * Draws the list of locations in the sidebar for maps of type 'categories'.
	 */
	drawSidebarList: function() {
		var sidebarListHTML = '';
		$.each(map.labelsDataKeys, function(n, label){
			sidebarListHTML += '<li><a href="#" id="' +map.labelsData[label]+ '">'+label+'</a></li>';
		});
		$('#map-selector').html('<ol class="locations">'+sidebarListHTML+'</ol>');
	},
	
	
	/**
	 * Set the height of the bit that lists all the locations. 
	 * So it expands to fill the sidebar height that's left after the top section.
	 */
	setSidebarBottomHeight: function() {
		$('#map-sidebar-bottom').height(
			$(window).height() - $('#map-sidebar-top').height()
		);
	},
	
	error: function(msg) {
		alert(msg);
	}
}

