//
// Taken from prototype.js...
//

Object.extend = function(destination, source) 
{
		for (property in source) 
		{
				destination[property] = source[property];
		}

		return destination;
}

//
// ...end of prototype.js functions
//

// Utility functions

function getRandomNumber(maxNumber)
{
	if (Math.random && Math.floor)
	{
		var randomNumber = Math.floor(Math.random() * maxNumber);
		return randomNumber;
	}
}

// ====== Create the Euclidean Projection for the flat map ======
// == Constructor ==
var slTileSize = 256.0;

// We map a 10k x 10k square positioned at the origin onto Lat/Long (0, 0) to (-90, 90)
var slMapFactor = 90.0 / 10000.0;

// The furthest out zoom level that we display voice-enabled markers for
var slVoiceMarkerMaxZoom = 3;

// The size of the chunks we handle the tile info markers in (i.e 4x4 tile chunks)
var slVoiceMarkerChunkSize = 4;

// For naming variables uniquely
var slMonotonicCounter = 1;

// Max/min zoom levels for SL maps (they are mapped to GMap zoom levels in a centralised place)
var slMinZoomLevel = 7; // Zoomed out as much as possible
var slMaxZoomLevel = 1; // Zoomed in as much as possible

// Delay for mouse hover action (mouse has to be still for this many milliseconds)
var slMouseHoverDelay = 1000;

// Web service URLs
var slMarkerInfoService = "https://secure-web15.secondlife.com/app/voicemap/index.php";//"http://secondlife.com/app/voicemap/index.php";

// Resource URLs
var slVoiceMarkerIcon = "http://www.insertyourhostnamehere.com/maps/voice_marker.png";

function slGetNewVarName(varStem)
{
	var varName = varStem + "_" + slMonotonicCounter;
	slMonotonicCounter++;
	return varName;
}

function EuclideanProjection(NumZoomLevels)
{
		this.pixelsPerLonDegree=[];
		this.pixelsPerLonRadian=[];
		this.pixelOrigo=[];
		this.tileBounds=[];
		var BitmapSize = 256;
		var c=1;
		
		for(var d=0; d < NumZoomLevels; d++)
		{
				var e= BitmapSize / 2;
				this.pixelsPerLonDegree.push(BitmapSize / 360);
				this.pixelsPerLonRadian.push(BitmapSize / (2*Math.PI));
				this.pixelOrigo.push(new GPoint(e,e));
				this.tileBounds.push(c);
				BitmapSize *= 2;
				c*=2
		}
}

// == Attach it to the GProjection() class ==
EuclideanProjection.prototype=new GProjection();


// == A method for converting latitudes and longitudes to pixel coordinates == 
EuclideanProjection.prototype.fromLatLngToPixel=function(LatLng,zoom)
{
		// Work out the position on the SL map, which is a notional 10k square positioned at the origin, 
		// i.e. we are mapping the 90 LatLng square onto the 10k SL square
		var RawMapX = LatLng.lng() / slMapFactor;
		var RawMapY = -LatLng.lat() / slMapFactor;
		
		// Now map this 10k square onto a 1:1 bitmap of the entire SL map, based
		// on the size of SL map tiles (at zoom level 1, the closest)
		var RawPixelX = RawMapX * slTileSize;
		var RawPixelY = RawMapY * slTileSize;
		
		// Now account for the fact that the map may be zoomed out
		zoom = slConvertGMapZoomToSLZoom(zoom);
		var ZoomFactor = Math.pow(2, zoom - 1);

		var PixelX = RawPixelX / ZoomFactor;
		var PixelY = RawPixelY / ZoomFactor;
		
		return new GPoint(PixelX, PixelY)
};

// == a method for converting pixel coordinates to latitudes and longitudes ==
EuclideanProjection.prototype.fromPixelToLatLng=function(pos,zoom,c)
{
		// First, account for the fact that the map may be zoomed out
		zoom = slConvertGMapZoomToSLZoom(zoom);
		var ZoomFactor = Math.pow(2, zoom - 1);

		var RawPixelX = pos.x * ZoomFactor;
		var RawPixelY = pos.y * ZoomFactor;
		
		// Now map this 1:1 bitmap position onto a 10k square of SL tiles, located at the origin
		var RawMapX = RawPixelX / slTileSize;
		var RawMapY = RawPixelY / slTileSize;
		
		// Now map this 10k SL square onto a 90 LatLng square
		var Lng = RawMapX * slMapFactor;
		var Lat = RawMapY * slMapFactor;
		
		return new GLatLng(-Lat,Lng,c)
};

// == a method that checks if the x/y value is in range ==
EuclideanProjection.prototype.tileCheckRange=function(pos, zoom, tileSize)
{
		if ((pos.x < 0) || (pos.y < 0)) 
				return false;
				
		return true
}

// == a method that returns the width of the tilespace ==      
EuclideanProjection.prototype.getWrapWidth=function(zoom) 
{
		return this.tileBounds[zoom]*256;
}

function landCustomGetTileUrl(pos, zoom) 
{
		zoom = slConvertGMapZoomToSLZoom(zoom);
		
		var Factor = Math.pow(2, zoom - 1);
		
		var x = pos.x;
		var y = pos.y;

		f = "http://secondlife.com/apps/mapapi/grid/map_image/"+x+"-"+y+"-"+zoom+"-0.html";
		
		return f;
}

/////////////////////////////////////////////////////////////////////////////////////////////////
// SL Map API ///////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////

function slConvertGMapZoomToSLZoom(zoom)
{
		// We map SL zoom levels to farthest out zoom levels for GMaps, as the Zoom control will then
		// remove ticks for any zoom levels higher than we allow. (We map it in this way because it doesn't
		// do the same for zoom levels lower than we allow).
		return 8 - zoom;
}

function slConvertSLZoomToGMapZoom(zoom)
{
		// We map SL zoom levels to farthest out zoom levels for GMaps, as the Zoom control will then
		// remove ticks for any zoom levels higher than we allow. (We map it in this way because it doesn't
		// do the same for zoom levels lower than we allow).
		return 8 - zoom;
}

// ------------------------------------
//
//              XYPoint
//
//
// ------------------------------------

function XYPoint(x,y)
{
		this.x = x;
		this.y = y;
}

XYPoint.prototype.GetGLatLng = function()
{
		// Apply stupid SL Y coord hack
		// TODO: is this the best place to do this?
		HackedY = 1279.0 - this.y;
		
		// As we've inverted the Y axis, tile origin is flipped to other edge, so we add 1
		// NB. Could do this in one operation via 1280 - this.y, but I prefer to do it
		// explicitly here, as not doing this is a common error and most people have
		// difficulty with it unless it's spelled out.
		HackedY += 1;
		
		return new GLatLng(-HackedY * slMapFactor, this.x * slMapFactor);
}

XYPoint.prototype._SetFromGLatLng = function(gpos)
{
		this.x = gpos.lng() / slMapFactor;
		this.y = -gpos.lat() / slMapFactor;
		
		// Invert the SL coord hack (gorgeous!)
		this.y -= 1;
		this.y = 1279.0 - this.y;
}

// ------------------------------------
//
//               Bounds
//
// ------------------------------------

function Bounds(xMin, xMax, yMin, yMax)
{
		if (arguments.length == 4)
		{
				this.xMin = xMin;
				this.xMax = xMax;
				this.yMin = yMin;
				this.yMax = yMax;
		}
		else
		{
				this.xMin = 0;
				this.xMax = 0;
				this.yMin = 0;
				this.yMax = 0;
		}
}

Bounds.prototype._SetFromGLatLngBounds = function(gbounds)
{
		var SW = new XYPoint();
		var NE = new XYPoint();
		
		SW._SetFromGLatLng(gbounds.getSouthWest());
		NE._SetFromGLatLng(gbounds.getNorthEast());
		
		this.xMin = SW.x;
		this.yMin = SW.y;
		
		this.xMax = NE.x;
		this.yMax = NE.y;
}

// ------------------------------------
//
//       SLPoint - UNIMPLEMENTED!
//
// ------------------------------------

function SLPoint(regionName, localX, localY)
{
		this.x = 0;
		this.y = 0;
}


// ------------------------------------
//
//               Img
//
// ------------------------------------

function Img(imgURL, imgWidth, imgHeight, hasAlpha)
{
		this.isAlpha = function()
		{
				return this.alpha;
		};
		
		this.URL = imgURL;
		this.width = imgWidth;
		this.height = imgHeight;
		
		if (hasAlpha)
		{
				this.alpha = true;
		}
		else
		{
				this.alpha = false;
		}
}


// ------------------------------------
//
//               Icon
//
// ------------------------------------

function Icon(imageMain, imageShadow)
{
		this.hasShadow=function()
		{
				if (this.shadowImg)
				{
						return true;
				}
				else
				{
						return false;
				}
		};
		
		this.mainImg=imageMain;
		
		if (imageShadow)
		{
				this.shadowImg = imageShadow;
		}
}


// ------------------------------------
//
//              Marker
//
// ------------------------------------

function Marker(icons, pos, options)
{
		this.icons = icons;
		this.slCoord = pos;
		this.options= new MarkerOptions(options);
		
};

function MarkerOptions(options)
{
		this.clickHandler=false;
		this.onMouseOverHandler=false;
		this.onMouseOutHandler=false;
		this.centerOnClick=false;
		this.autopanOnClick=true;
		this.autopanPadding=45;
		this.verticalAlign="middle";
		this.horizontalAlign="center";
		this.zLayer=0;
		
		if (options)
				Object.extend(this, options);
		
};


// ------------------------------------
//
//             MapWindow
//
// ------------------------------------

function MapWindow(text, options)
{
		this.text = text;
		this.options = options;
}

MapWindow.prototype.getGMapOptions = function()
{
		var width = 252;
		if (this.options && this.options.width)
				width = this.options.width;
				
		return {maxWidth: width};
}

// ------------------------------------
//
//            SLMapOptions
//
// ------------------------------------

function SLMapOptions(options)
{
		this.hasZoomControls=true;
		this.hasPanningControls=true;
		this.hasOverviewMapControl=true;
		this.doubleClickHandler=null;
		this.singleClickHandler=null;
		this.onStateChangedClickHandler=null;
		this.zoomMin = 6;
		this.zoomMax = 1;
		
		if (options)
				Object.extend(this, options);
				
		if (this.zoomMin > slMinZoomLevel)
				this.zoomMin = slMinZoomLevel;
				
		if (this.zoomMax < slMaxZoomLevel)
				this.zoomMax = slMaxZoomLevel;
				
};

// ------------------------------------
//
//               SLMap
//
// ------------------------------------


function SLMap(map_element, map_options)
{
	this.ID = null;
	this.showingHoverWindow = false;
	
		if (GBrowserIsCompatible()) 
		{
				this.options = new SLMapOptions(map_options);
				this.mapProjection = new EuclideanProjection(18);

				// Create our custom map types and initialise map with them
				var mapTypes = this.CreateMapTypes();
				var mapDiv = this.CreateMapDiv(map_element);
				this.GMap = new GMap2(mapDiv, {"mapTypes" : mapTypes});

				// Link GMap back to us
				this.GMap.slMap = this;
				
				// No GMap info windows open yet
				this.currentMapWindow = null;

		// No voice markers yet
		this.voiceMarkers = [];
						
				var addZoomControls = true;
				var addPanControls = true;
				var overviewMapControl = true;
				
				if (this.options != undefined)
				{
						if (this.options.hasPanningControls == false)
						{
								addPanControls = false;
						}
						
						if (this.options.hasZoomControls == false)
						{
								addZoomControls = false;
						}
						
						if (this.options.hasOverviewMapControl == false)
						{
								overviewMapControl = false;
						}

				}
				
				// Use GMaps native controls
				if (addZoomControls || addPanControls)
				{
					this.GMap.addControl(new GSmallMapControl());
				}
				
				if (overviewMapControl)
				{
					this.GMap.addControl(new GOverviewMapControl());
				}
				
				// Use GMaps xtra control methods
				this.GMap.enableContinuousZoom();
				this.GMap.enableScrollWheelZoom();
				
				this.GMap.setCenter(new GLatLng(0, 0), 16);
	
				// Allow user to switch map types
				this.GMap.addControl(new GMapTypeControl());

				// Install our various event handlers
				var slMap = this;
				
				GEvent.addListener(
						this.GMap, 
						"click", 
						function(marker, point) 
						{
								slMapClickHandler(slMap, marker, point);
						});
						
				if (this.options && this.options.doubleClickHandler)
				{
						GEvent.addListener(
								this.GMap, 
								"dblclick", 
								function(marker, point) 
								{
										slMapDoubleClickHandler(slMap, marker, point);
								});
				}
				
				GEvent.addListener(
						this.GMap, 
						"moveend", 
						function() { slMap.onStateChangedHandler(); });

		if (!this.options || !this.options.disableVoiceInfo)
		{
			// Voice marker info/tooltips enabled, so we need mouse move/mouse out handlers
					GEvent.addListener(
							this.GMap, 
							"mousemove", 
							function(pos) { slMap.onMouseMoveHandler(pos); });
			
					GEvent.addListener(
							this.GMap, 
							"mouseout", 
							function(pos) { slMap.onMouseOutHandler(pos); });
		}

				
				GEvent.addListener(
						this.GMap, 
						"dragstart", 
						function() 
						{
								slMapDragHandler(slMap);
						});        
						
				// Moved this to the end as GMaps seemed to fail if I did it right
				// after map creation, and I don't have time to debug other people's code.
			this.GMarkerManager = new GMarkerManager(this.GMap);
						
		}
		else
		{
				// Browser does not support Google Maps
				this.GMap = null;
		}
}

SLMap.prototype.onStateChangedHandler = function()
{
	// Service user supplied handler if it exists
	if (this.options && this.options.onStateChangedHandler)
	{
				this.options.onStateChangedHandler();
	}
	
	// Now add any markers for voice info etc
	this.ensureMarkersUpToDate();
}

SLMap.prototype.onMouseMoveHandler = function(pos)
{
	// We just got a mouse move, so the user isn't 'hovering' right now
	this.resetHoverTimeout(true);
	this.hoverPos = pos;
	
	// If we're showing a tooltip, close it
	if (this.showingHoverWindow)
	{
		this.GMap.closeInfoWindow();
	}
}

SLMap.prototype.onMouseOutHandler = function(pos)
{
	// Mouse is leaving map - clear tooltip timers
	this.clearHoverTimeout(true);
}

SLMap.prototype.clearHoverTimeout = function()
{
	if (this.ID != null)
	{
		window.clearTimeout(this.ID);
		this.ID = null;
	}
}

SLMap.prototype.resetHoverTimeout = function(forceTimerSet)
{
	var timerWasSet = (this.ID != null);
	this.clearHoverTimeout();
	
	if (timerWasSet || forceTimerSet)
	{		
		var map = this;
		this.ID = window.setTimeout(function() { map.mousehoverHandler(); }, slMouseHoverDelay);
	}
}

SLMap.prototype.mousehoverHandler = function()
{
	// Get tile co-ordinate
	tilePos = new XYPoint;
	tilePos._SetFromGLatLng(this.hoverPos);
	
	var tileX = Math.floor(tilePos.x);
	var tileY = Math.floor(tilePos.y);

	// Check for existing info about this tile
	tileInfo = this.getVoiceTileInfo(tileX, tileY);
	
	if (tileInfo.error)
	{
		// No info on this tile yet - maybe re-trigger the hover timer so we try again
		// later, so hopefully the web service has returned marker info by then?
	}
	else
	{
		this.showTileToolTip(tileInfo);
	}
}

SLMap.prototype.showTileToolTip = function(tileInfo)
{
	var map = this;
	this.ID = null;

	var HoverText = "";
	
	if (tileInfo.name)
		HoverText = "<b>" + tileInfo.name + "</b><br/>";
	
	if (tileInfo.voiceEnabled === true)
	{	
		HoverText += "Voice enabled.";
	}
	else if (tileInfo.voiceEnabled === false)
	{
		HoverText += "Not voice enabled.<br/><br/><font size=-1>Voice is not currently<br/>planned for this region.</font>";
	}
	else
	{
		var date = tileInfo.voiceEnabled;
		HoverText += "Not voice enabled.<br/><br/><font size=-1>Voice will be enabled on:<br/>" + date.toDateString() + "</font>";
	}
		
	this.GMap.openInfoWindowHtml(this.hoverPos, HoverText, { onCloseFn: function() { map.hoverWindowCloseHandler(); }});
	this.showingHoverWindow = true;
}

SLMap.prototype.hoverWindowCloseHandler = function()
{
	// Window has just closed, so reset any hover timer, so a window doesn't appear immediately
	this.showingHoverWindow = false;

	this.resetHoverTimeout(false);	
}

SLMap.prototype.CreateMapTypes = function()
{
	var mapTypes = [];
	
		var copyCollection = new GCopyrightCollection('SecondLife');
		var copyright = new GCopyright(1, new GLatLngBounds(new GLatLng(0, 0), new GLatLng(-90, 90)), 0, "(C) 2007 Linden Lab");
		copyCollection.addCopyright(copyright);

		// Create the 'Land' type of map
		var landTilelayers = [new GTileLayer(copyCollection, 10, 16)];
		landTilelayers[0].getTileUrl = landCustomGetTileUrl;
		
		var landMap = new GMapType(landTilelayers, this.mapProjection, "Land", {errorMessage:"No SL data available"});
		landMap.getMinimumResolution = function() { return slConvertGMapZoomToSLZoom(slMinZoomLevel); };
		landMap.getMaximumResolution = function() { return slConvertGMapZoomToSLZoom(slMaxZoomLevel); };

		mapTypes.push(landMap);
		
	return mapTypes;
}

SLMap.prototype.CreateMapDiv = function(mainDiv)
{
	var SLMap = this;

	// Create a unique ID for the region name field
	var inputFieldID = "slRegionNameField_" + getRandomNumber(10000);
	
	// Create a click handler
	var clickHandler = function() 
	{ 
		var textField = document.getElementById(inputFieldID);
		
		if (textField)
		{
			SLMap.gotoRegion(textField.value); 
		}
		else
		{
			alert("Can't find textField!");
		}
		
		return false;
	};
	
	// Create a div to be the main map container as a child of the main div
	var mapDiv = document.createElement("div");
	
	// Match parent height
	mapDiv.style.height = "100%";
	
//	// Now create the div for the text input form
//	var form = document.createElement("form");
//	form.name = "slregionname";
//	form.style.textAlign = "center";
//	form.style.padding = "4px";
//	form.onsubmit = clickHandler;
//	
//	// Label for the text field
//	var formLabel = document.createTextNode("Enter region name:");
//	var formLabelSpan = document.createElement("span");
//	formLabelSpan.style.fontSize = "80%";
//	formLabelSpan.appendChild(formLabel);
//
//	// Text field for the region name
//	var formText = document.createElement("input");
//	
//	formText.type = "text";
//	formText.name = "regionname";
//	formText.id = inputFieldID;
//	formText.value = "Ahern";
//	formText.size = 15;
//
//	// Button to activate 'go to region'
//	var formButton = document.createElement("input");
//	formButton.type = "submit";
//	formButton.value = "Go!";
//	formButton.onsubmit = clickHandler;
//	
//	// Put form on the page
//	form.appendChild(formLabelSpan);	
//	form.appendChild(formText);
//	form.appendChild(formButton);
//
//	mainDiv.appendChild(form);
	
	mainDiv.appendChild(mapDiv);

	return mapDiv;
}

SLMap.prototype.gotoRegion = function(regionName)
{
	var SLMap = this;
	
		// Add a dynamic script to get this region position, and then trigger a map center
		// change based on the results
		var varName = "slRegionPos_result";
		
		var scriptURL = "https://cap.secondlife.com/cap/0/d661249b-2b5a-4436-966a-3d3b8d7a574f?var=" + varName + "&sim_name=" + encodeURIComponent(regionName);

		// Once the script has loaded, we use the result to center the map on the position
		var onLoadHandler = function () 
		{
			if (slRegionPos_result.error)
			{
				alert("The region name '" + regionName + "' was not recognised.");
		}
		else
		{
				var x = slRegionPos_result.x;
				var y = slRegionPos_result.y;
//			alert("Going to " + x + "," + y);
				
				var pos = new XYPoint(x, y);
				SLMap.panOrRecenterToSLCoord(pos);
		}
		};
						
		slAddDynamicScript(scriptURL, onLoadHandler);
}

SLMap.prototype.centerAndZoomAtSLCoord = function(pos, zoom)
{
		if (this.GMap != null)
		{
				// Enforce zoom limits specified by client
				zoom = this._forceZoomToLimits(zoom);

				this.GMap.setCenter(pos.GetGLatLng(), slConvertSLZoomToGMapZoom(zoom));
		}
}

SLMap.prototype.disableDragging = function()
{
		if (this.GMap != null)
		{
				this.GMap.disableDragging();
		}
}

SLMap.prototype.enableDragging = function()
{
		if (this.GMap != null)
		{
				this.GMap.enableDragging();
		}
}

SLMap.prototype.getViewportBounds = function()
{
		if (this.GMap != null)
		{
				gLatLngBounds = this.GMap.getBounds();
				
				viewBounds = new Bounds();
				viewBounds._SetFromGLatLngBounds(gLatLngBounds);
				return viewBounds;
		}
}

SLMap.prototype.getMapCenter = function()
{
		if (this.GMap != null)
		{
				gCenter = this.GMap.getCenter();
				
				center = new XYPoint();
				center._SetFromGLatLng(gCenter);
				return center;
		}
}

function slMapDragHandler(slMap)
{
		if (slMap.currentMapWindow != null)
		{
				if (slMap.currentMapWindow.options)
				{
						if (slMap.currentMapWindow.options.closeOnMove)
						{
								slMap.GMap.closeInfoWindow();
								slMap.currentMapWindow = null;
						}
				}
		}    
}

function slMapClickHandler(slMap, gmarker, point)
{
		if (gmarker == null)
		{
				// Generic click on map
				if (slMap.options && slMap.options.singleClickHandler)
				{
						slCoord = new XYPoint;
						slCoord._SetFromGLatLng(point);
						slMap.options.singleClickHandler(slCoord.x, slCoord.y);
				}
		}
		else
		{
				// Handle clicking on a marker
				var slMarker = gmarker.slMarker;

		if (slMarker)
		{        
					if (slMarker.options.centerOnClick)
					{
							slMap.panOrRecenterToSLCoord(slMarker.slCoord);
					}
					
					if (slMarker.options.clickHandler)
					{
							slMarker.options.clickHandler(slMarker);
					}
		}
		}
}

function slMapDoubleClickHandler(slMap, gmarker, point)
{
		if (gmarker == null)
		{
				// Generic double-click on map
				if (slMap.options && slMap.options.doubleClickHandler)
				{
						slCoord = new XYPoint;
						slCoord._SetFromGLatLng(point);
						slMap.options.doubleClickHandler(slCoord.x, slCoord.y);
				}
		}
		else
		{
				// Handle clicking on a marker
				var slMarker = gmarker.slMarker;
				
				if (slMarker.options.clickHandler)
				{
						slMarker.options.clickHandler(slMarker);
				}
		}
}

SLMap.prototype.clickMarker = function(marker)
{
		// Simulate a GMap click event on the centre of this marker
		slMapClickHandler(this, marker.gmarker, marker.gmarker.getPoint());
}

SLMap.prototype.addMarker = function(marker, mapWindow)
{
		if (this.GMap != null)
		{
				// Create the GMarker
				var markerImg = marker.icons[0];
				
				var gicon = new GIcon();
				gicon.image = markerImg.mainImg.URL;

				gicon.iconSize = new GSize(markerImg.mainImg.width, markerImg.mainImg.height);
				
				if (markerImg.shadowImg)
				{
						gicon.shadow = markerImg.shadowImg.URL;
						gicon.shadowSize = new GSize(markerImg.shadowImg.width, markerImg.shadowImg.height);
				}
				else
				{
						gicon.shadowSize = gicon.iconSize;
				}
						
				// Work out hotspot of marker
				var hotspotX = gicon.iconSize.width / 2;
				
				if (marker.options.horizontalAlign == "left")
						hotspotX = 0;
				else if (marker.options.horizontalAlign == "right")
						hotspotX = gicon.iconSize.width;
						
				var hotspotY = gicon.iconSize.height/ 2;
				
				if (marker.options.verticalAlign == "top")
						hotspotY = 0;
				else if (marker.options.verticalAlign == "bottom")
						hotspotY = gicon.iconSize.height;
						
				gicon.iconAnchor = new GPoint(hotspotX, hotspotY);
				
				// TODO: need to change this? It's probably ok for most cases
				gicon.infoWindowAnchor = gicon.iconAnchor;

				// Add the GMarker to the map
				var point = marker.slCoord.GetGLatLng();
				
				// The SL marker 'owns' the GMarker, and we insert a link from GMarker
				// back to SL marker to assist callback/event processing
				var isClickable = false;
				if (mapWindow ||
						marker.options.centerOnClick || 
						marker.options.clickHandler ||
						marker.options.onMouseOverHandler ||
						marker.options.onMouseOutHandler)
				{
						// Mouse over/out events are not clicks, but if we're not clickable or draggable, then
						// GMaps doesn't send us any events.
						isClickable = true;
				}
						
				var markerZIndex = 0;
				
				if (marker.options.zLayer)
						markerZIndex = marker.options.zLayer;
						
				var gmarkeroptions = 
						{
								icon: gicon, 
								clickable: isClickable, 
								draggable: false,
								zIndexProcess: function() { return markerZIndex; }
						};
				
				marker.gmarker = new GMarker(point, gmarkeroptions);
				
				marker.gmarker.slMarker = marker;
				
				if (mapWindow)
				{
						GEvent.addListener(marker.gmarker, "click", 
								function() 
								{
										marker.gmarker.openInfoWindowHtml(mapWindow.text, mapWindow.getGMapOptions());
										this.currentMapWindow = mapWindow;
								});
				}
				
				if (marker.options.onMouseOverHandler)
				{
						GEvent.addListener(marker.gmarker, "mouseover",
								function()
								{
										marker.options.onMouseOverHandler(marker);
								});
				}
				
				if (marker.options.onMouseOutHandler)
				{
						GEvent.addListener(marker.gmarker, "mouseout",
								function()
								{
										marker.options.onMouseOutHandler(marker);
								});
				}
				
				this.GMap.addOverlay(marker.gmarker);
		}
}

SLMap.prototype.removeMarker = function(marker)
{
		if (this.GMap != null)
		{
				if (marker.gmarker)
				{
						this.GMap.removeOverlay(marker.gmarker);
						marker.gmarker = null;
				}
		}
}

SLMap.prototype.removeAllMarkers = function()
{
		if (this.GMap != null)
		{
				this.GMap.clearOverlays();
		}
}

SLMap.prototype.addMapWindow = function(mapWindow, pos)
{
		if (this.GMap != null)
		{                           
				this.GMap.openInfoWindowHtml(pos.GetGLatLng(), mapWindow.text, mapWindow.getGMapOptions());
				this.currentMapWindow = mapWindow;
		}
}

SLMap.prototype.zoomIn = function()
{
		if (this.GMap != null)
		{
				if (this.options && this.options.zoomMax)
				{
						// Client specified zoom limit, so enforce it
						if (this.getCurrentZoomLevel() <= this.options.zoomMax)
								return;
				}
				
				// Ok to zoom in
				this.GMap.zoomIn();
		}
}

SLMap.prototype.zoomOut = function()
{
		if (this.GMap != null)
		{                           
				if (this.options && this.options.zoomMin)
				{
						// Client specified zoom limit, so enforce it
						if (this.getCurrentZoomLevel() >= this.options.zoomMin)
								return;
				}
				
				this.GMap.zoomOut();
		}
}

SLMap.prototype.getCurrentZoomLevel = function()
{
		if (this.GMap != null)
		{                           
				return slConvertGMapZoomToSLZoom(this.GMap.getZoom());
		}
}

SLMap.prototype._forceZoomToLimits = function(zoom)
{
		// Enforce zoom limits specified by client
		if (this.options && this.options.zoomMax)
		{
				if (zoom < this.options.zoomMax)
						zoom = this.options.zoomMax;
		}
		
		if (this.options && this.options.zoomMin)
		{
				if (zoom > this.options.zoomMin)
						zoom = this.options.zoomMin;
		}
		
		return zoom;
}

SLMap.prototype.setCurrentZoomLevel = function(zoom)
{
		if (this.GMap != null)
		{                           
				// Enforce zoom limits specified by client
				zoom = this._forceZoomToLimits(zoom);
				
				this.GMap.setZoom(slConvertSLZoomToGMapZoom(zoom));
		}
}

SLMap.prototype.panBy = function(x, y)
{
		if (this.GMap != null)
		{
				var pos = this.GMap.getCenter();
				
				var tileSize = new XYPoint(x, y);
				
				var offset = this.mapProjection.fromPixelToLatLng(tileSize, this.GMap.getZoom());
				
				var newPos = new GLatLng(pos.lat() + offset.lat(), pos.lng() + offset.lng());
				this.GMap.panTo(newPos);
		}
}

SLMap.prototype.panLeft = function()
{
		this.panBy(-slTileSize, 0);
}

SLMap.prototype.panRight = function()
{
		this.panBy(slTileSize, 0);
}

SLMap.prototype.panUp = function()
{
		this.panBy(0, -slTileSize);
}

SLMap.prototype.panDown = function()
{
		this.panBy(0, slTileSize);
}

SLMap.prototype.panOrRecenterToSLCoord = function(pos, forceCenter)
{
		if (this.GMap != null)
		{
				this.GMap.panTo(pos.GetGLatLng());
		}
}

SLMap.prototype.showMarkersForTile = function(xPos, yPos)
{
	// Check to see if we already have these markers
	if (this.voiceMarkers[xPos]) 
	{
		if (this.voiceMarkers[xPos][yPos])
		{
			// We already have these markers, or are currently fetching them
			return;
		}
	}
	
	// Need to add markers - init array if required
	if (!this.voiceMarkers[xPos])
	{
		this.voiceMarkers[xPos] = [];
	}

	// Make sure we only do this once - this slot will be filled in with the actual
	// tile info when it arrives (see addMarkersToMap)
	this.voiceMarkers[xPos][yPos] = true;
	
	// Fetch information about these markers	
	var varName = slGetNewVarName("slMarkerInfo");
		var scriptURL = slMarkerInfoService + "?var=" + varName + "&x=" + xPos + "&y=" + yPos + "&blocksize=" + slVoiceMarkerChunkSize;

		var SLMap = this;

//	alert ("Fetching info for (" + xPos + ", " + yPos + ")");    

		// Once the script has loaded, we use the result to center the map on the position
		var onLoadHandler = function () 
		{
			// Pick up the value
			var tileInfo = eval(varName);
			
			if (tileInfo.error)
			{
				// Unable to find voice marker info - fail silently
		}
		else
		{
			SLMap.addMarkersToMap(tileInfo, xPos, yPos);
		}
		};
						
		slAddDynamicScript(scriptURL, onLoadHandler);
}

SLMap.prototype.createVoiceMarker = function(x, y)
{
		if (this.GMap != null)
		{
				// Create the GMarker
				var gicon = new GIcon();
				gicon.image = slVoiceMarkerIcon;

				gicon.iconSize = new GSize(17, 45);
				gicon.shadowSize = gicon.iconSize;
						
				gicon.iconAnchor = new GPoint(8, 22);
				gicon.infoWindowAnchor = gicon.iconAnchor;

				var gmarkeroptions = 
						{
								icon: gicon, 
								clickable: false, 
								draggable: false
						};
				
				var slPos = new XYPoint(x, y);
				var point = slPos.GetGLatLng();
				
				var gmarker = new GMarker(point, gmarkeroptions);

		return gmarker;        
		}
		
		return null;
}

SLMap.prototype.addMarkersToMap = function(voiceEnabledTiles, x, y)
{
	// Store this block of tile info
	this.voiceMarkers[x][y] = voiceEnabledTiles;
	
	var Mgr = this.GMarkerManager;
	var markerSet = [];
	
	for (i in voiceEnabledTiles)
	{
		var tileInfo = voiceEnabledTiles[i];
		
		if (tileInfo.voiceEnabled === true)
		{
			// This tile is voice enabled so add a marker to indicate this.
			var tileOffset = 0.15;
			var marker = this.createVoiceMarker(tileInfo.x + tileOffset, 
												tileInfo.y + tileOffset);
	
			markerSet.push(marker);
		}
	}
	
	if (markerSet.length > 0)
	{
		// >1 markers fetched, so add them to the map
		Mgr.addMarkers(markerSet, slConvertSLZoomToGMapZoom(slVoiceMarkerMaxZoom));
		Mgr.refresh();
	}
}

SLMap.prototype.getVoiceTileChunkPos = function(x, y)
{
	var xChunk = Math.floor(x / slVoiceMarkerChunkSize) * slVoiceMarkerChunkSize;
	var yChunk = Math.floor(y / slVoiceMarkerChunkSize) * slVoiceMarkerChunkSize;
	
	return {"x" : xChunk, "y" : yChunk };
}

SLMap.prototype.getVoiceTileInfo = function(x, y)
{
	var ChunkPos = this.getVoiceTileChunkPos(x, y);
	
	tilesInfo = this.voiceMarkers[ChunkPos.x][ChunkPos.y];

	if (tilesInfo && (tilesInfo !== true))
	{
		for (i in tilesInfo)
		{
			var tileInfo = tilesInfo[i];
			
			if ((tileInfo.x == x) && (tileInfo.y == y))
			{
				// Found the right tile
				return tileInfo;
			}
		}
		
		// Tile is not voice enabled
		return {"x": x, "y" : y, "voiceEnabled" : false };
	}
	
	// tile not found - return error
	return {error: true};	
}

SLMap.prototype.ensureMarkersUpToDate = function()
{
	if (this.options && this.options.disableVoiceInfo)
	{
		// Voice markers disabled
		return;
	}

	// Check zoom settings - if too far out, we don't do tile markers
	if (this.getCurrentZoomLevel() > slVoiceMarkerMaxZoom)
		return;
		
	// Ok, work out which tile chunks are visible, and fetch any that we don't yet have
	var viewBounds = this.getViewportBounds();
	
	var xMin = viewBounds.xMin;
	var xMax = viewBounds.xMax;
	var yMin = viewBounds.yMin;
	var yMax = viewBounds.yMax;
	
	xMin = Math.floor(xMin / slVoiceMarkerChunkSize) * slVoiceMarkerChunkSize;
	yMin = Math.floor(yMin / slVoiceMarkerChunkSize) * slVoiceMarkerChunkSize;

	xMax = (Math.floor(xMax / slVoiceMarkerChunkSize) + 1) * slVoiceMarkerChunkSize;
	yMax = (Math.floor(yMax / slVoiceMarkerChunkSize) + 1) * slVoiceMarkerChunkSize;
	
	for (x = xMin; x <= xMax; x += slVoiceMarkerChunkSize)
	{
		for (y = yMin; y <= yMax; y += slVoiceMarkerChunkSize)
		{
			this.showMarkersForTile(x, y);
		}
	}
}

function slAddDynamicScript(scriptURL, onLoadHandler)
{
		var script = document.createElement('script');
		script.src = scriptURL;
		script.type = "text/javascript";

		if (onLoadHandler)
		{
			// Install the specified onload handler

		// Need to use ready state change for IE as it doesn't support onload for scripts
		script.onreadystatechange = function () 
		{
//			alert(script.src + ": " + script.readyState);
		
			if ((script.readyState == 'complete')  || (script.readyState == 'loaded'))
			{
				onLoadHandler();
			}
		}

		// Standard onload for Firefox/Safari/Opera etc
		script.onload = onLoadHandler;
	}
			
		document.body.appendChild(script);
}


function gotoSLURL(x,y) 
{
		// Work out region co-ords, and local co-ords within region
		var int_x = Math.floor(x);
		var int_y = Math.floor(y);

		var local_x = Math.round((x - int_x) * 256);
		var local_y = Math.round((y - int_y) * 256);

		// Add a dynamic script to get this region name, and then trigger a URL change
		// based on the results
		var scriptURL = "https://cap.secondlife.com/cap/0/b713fe80-283b-4585-af4d-a3b7d9a32492?var=slRegionName&grid_x=" + int_x + "&grid_y="+ int_y;

		// Once the script has loaded, we use the result to teleport the user into SL    
		var onLoadHandler = function () 
		{
			if (slRegionName.error)
			{
				alert("The co-ordinates of the SLURL (" + x + ", " + y + ") were not recognised as being in a SecondLife region.");
		}
		else
		{
					var url = "secondlife://" + slRegionName.replace(/\s/, "_") + "/" + local_x + "/" + local_y;
	//      alert(url);
					document.location = url;
		}
		};

		slAddDynamicScript(scriptURL, onLoadHandler);
}


