//
//  Map of how SL map relates to the Google map:
//
//  ( 0, 0) Google map pixels
//  (90, 0) lat, long
//  (0, slGridEdgeSizeInRegions) grid_x, grid_y
//  /\
//   |------------------------------------+
//   |                                    |
//   |                                    |
//   |                                    |
//   |                                    |
//   |                                    |
//   |                                    |
//   |                                    |
//   |                                    |
//   | xxx                                |
//   | xxx                                |       (big, big) Google map pixels
//   ------------------------------------------>  (0,90) lat, long
//                                                (slGridEdgeSizeInRegions, 0) grid_x, grid_y
//
//  As this map shows, SL is mapped to the upper right quadrant of the
//  'world' map as used by Google lat/long coordinates.  A large scaling value
//  called slGridEdgeSizeInRegions is used to set the largest region coordinate
//  at the top and far right edge of the displayed area.  At the current
//  value of 2^20 = 1M, this creates a map area with room for 1 trillion sims.
//  The little xxx's in the diagram shows where the populated sims are in SL
//  today.
//
//

// === Constants ===
var SL_OCEAN_COLOR = "#1D475F";   // color when no region tile available

//
// 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;
	}
}

var slDebugMap = false;

//
//  Add 2 hosts so that we get faster performance on clients with lots
//  of bandwidth but possible browser limits on number of open files
//

var slTileHost1 = "http://map.secondlife.com.s3.amazonaws.com";
var slTileHost2 = "http://map.secondlife.com";

// ====== Create the Euclidean Projection for the flat map ======
// == Constructor ==
// Map tiles are found in directories of format s3server/00055/00066
// the directories are all of the same length.  
// Of course, we will someday exceed even this limit.  Bill Gates would be proud.  :)
var slTileDirectoryNameLength = 5;

var slTileSize = 256.0;

// The maximum width/height of the SL grid in regions:
// 2^20 regions on a side = 1,048,786  ("This should be enough for anyone")
// *NOTE: This must be a power of 2 and divisible by 2^(max zoom) = 256
var slGridEdgeSizeInRegions = 1048576;

// We map a 1,048,576 (2^20) regions-per-side square positioned at the origin onto Lat/Long (0, 0) to (-90, 90)
var slMapFactor = 90.0 / slGridEdgeSizeInRegions;

// Max/min zoom levels for SL maps (they are mapped to GMap zoom levels in a centralised place)
var slMinZoomLevel = 8; // 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;

// Do we want to display hover information for the map?
var slShowHoverTips = false;

function EuclideanProjection(NumZoomLevels)
{
    this.pixelsPerLonDegree=[];
    this.pixelsPerLonRadian=[];
    this.pixelOrigo=[];
    this.tileBounds=[];
    var BitmapSize = 512;
    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)
{
    var RawMapX = LatLng.lng() / slMapFactor;
    var RawMapY = -LatLng.lat() / slMapFactor;
    
    // Now map this 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 (the bounding box of the map) ==      
EuclideanProjection.prototype.getWrapWidth=function(zoom) 
{
	return this.tileBounds[zoom] * slGridEdgeSizeInRegions;		
}

//  This Function returns the appropriate image tile from the S3 storage site corresponding to the
//  input location and zoom level on the google map.
function landCustomGetTileUrl(pos, zoom) 
{
    var sl_zoom = slConvertGMapZoomToSLZoom(zoom);

    var regions_per_tile_edge = Math.pow(2, sl_zoom - 1);
    
    var x = pos.x * regions_per_tile_edge;
    var y = pos.y * regions_per_tile_edge;

    // Adjust Y axis flip location by zoom value, in case the size of the whole
    // world is not divisible by powers-of-two.
    var offset = slGridEdgeSizeInRegions;
    offset -= offset % regions_per_tile_edge;
    y = offset - y;

    // Google tiles are named (numbered) based on top-left corner, but ours
    // are named by lower-left corner.  Since we flipped in Y, correct the
    // name.  JC
    y -= regions_per_tile_edge;
    
    // We name our tiles based on the grid_x, grid_y of the region in the
    // lower-left corner.
    x -= x % regions_per_tile_edge;
    y -= y % regions_per_tile_edge; 

    //  Pick a server
    
    if (((x / regions_per_tile_edge) % 2) == 1)
       var host_name = slTileHost1;
    else
       var host_name = slTileHost2; 

    //  Get image tiles from Amazon S3
    f = host_name + "/map-" + sl_zoom + "-" + x + "-" + y + "-objects.jpg";
    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()
{
    // Invert Y axis
	var corrected_y = slGridEdgeSizeInRegions - this.y;
    var lat = -corrected_y * slMapFactor;
    var lng = this.x * slMapFactor;
	return new GLatLng(lat, lng);
}
 

XYPoint.prototype._SetFromGLatLng = function(gpos)
{
    this.x = gpos.lng() / slMapFactor;
    this.y = -gpos.lat() / slMapFactor;
    // Invert Y axis back
    this.y = slGridEdgeSizeInRegions - 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.onStateChangedClickHandler=null;
		this.zoomMin = slMinZoomLevel;
		this.zoomMax = slMaxZoomLevel;
		
		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);
                var mapOpts = {"mapTypes": mapTypes,
                               "backgroundColor": SL_OCEAN_COLOR };
				this.GMap = new GMap2(mapDiv, mapOpts);

				// 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);
						});
						
				/*
				GEvent.addListener(
								this.GMap, 
								"dblclick", 
								function(marker, point) 
								{
                                                                                slMapDoubleClickHandler(slMap, marker, point);
								});
				*/
				
				GEvent.addListener(
						this.GMap, 
						"moveend", 
						function() { slMap.onStateChangedHandler(); });

		if (slShowHoverTips)
		{
			// Enable, If we want mouse move 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();
	}
}

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);

        this.showTileToolTip();
}

SLMap.prototype.getRegionName = function()
{
                var text = "Test Region Name";
                return text;
}

SLMap.prototype.showTileToolTip = function()
{
	var map = this;
	this.ID = null;

	var HoverText = "";
	
	if (true)
		//HoverText = "<b>" + this.getRegionName() + "</b><br/>";
                HoverText = this.getRegionName();
		
	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"});
		var landMap = new GMapType(landTilelayers, this.mapProjection, "Land" );
		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 = "http://slurl.com/get-region-coords-by-name"
                + "?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)
{
	// DEBUG:  Show various data about the region clicked
                 

    /*
	alert("GLatLng: " + point.toString()
                      //+"\n Gpoint: " + slMap.GMap.mapProjection.fromLatLngToPixel(point)
                      //+"\n SLCoord: " + slCoord.x.toString() + "," + slCoord.y.toString()
                      +"\n slZoom: " + slZoom.toString()
                      +"\n GZoom: " + gZoom.toString()
                      +"\n SL Region: " + slCoord.x.toString() + "," + slCoord.y.toString()
                      //+"\n URL: " + tileURL
                      );
                */
                                
	if (gmarker == null)
	{
		// Generic click on map teleports directly to the location
        slCoord = new XYPoint;
		slCoord._SetFromGLatLng(point);
        gotoSLURL(slCoord.x, slCoord.y, slMap);
	}
	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)
		{
				// on a double-click on land, simply teleport directly to the location!
                slCoord = new XYPoint;
				slCoord._SetFromGLatLng(point);
                gotoSLURL(slCoord.x, slCoord.y, slMap);
		}
		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());
		}
}

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);
}

// Open a map info window with a link to teleport to the region
function gotoSLURL(x,y,slMap) 
{
		// 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 = "http://slurl.com/get-region-name-by-coords"
                + "?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 == null || slRegionName.error)
			{
				//alert("The co-ordinates of the SLURL (" + x + ", " + y + ") were not recognised as being in a SecondLife region.");
            }
            else
            {
                var url = "secondlife://" + encodeURIComponent(slRegionName)
                        + "/" + local_x + "/" + local_y;
				var debugInfo = '';
				if (slDebugMap)
				{
					debugInfo = ' x: ' + int_x + ' y: ' + int_y;
				}
                
                var mapWindow = new MapWindow('<b>'+ slRegionName + '</b><br>'
						+ debugInfo
                        + '<a href="'+ url +'" class="teleport-button">Teleport Now</a>');
                slMap.addMapWindow(mapWindow, new XYPoint(x,y));                    
            }
		};

		slAddDynamicScript(scriptURL, onLoadHandler);
}


