//
// 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 = 1;						// currently my external code only supports one.

// 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 = 100;

// Web service URLs
var slMarkerInfoService = "http://secondlife.com/app/voicemap/index.php";
var slLLOverlayInfoService = "http://www.nandnerd.info/overlayrequest.php";

// Resource URLs
var slVoiceMarkerIcon = "http://s3.amazonaws.com/static-secondlife-com/_img/icons/voicemap.png";
// Overlay is dynamic

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.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;
        
        if (this.options != undefined)
        {
            if (this.options.hasPanningControls == false)
            {
                addPanControls = false;
            }
            
            if (this.options.hasZoomControls == false)
            {
                addZoomControls = false;
            }
        }
        
        // Use GMaps native controls
        if (addZoomControls || addPanControls)
        {
       		this.GMap.addControl(new GLargeMapControl());
		}
            
        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(); });
        
        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);				// nNn get rid of that annoying tool tip.
		if (tileInfo.name)
		{
			document.getElementById('regionNameField').value = tileInfo.name;
		}
	}
}

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 Labs");
    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 scriptURL = slLLOverlayInfoService + "?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.createLLOverlay = function(x, y, URL)
{
    if (this.GMap != null)
    {
        // Create the GMarker
        var gicon = new GIcon();
        gicon.image = URL;

        gicon.iconSize = new GSize(256, 256);
        gicon.shadowSize = gicon.iconSize;
            
        gicon.iconAnchor = new GPoint(128, 128);
        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.5; // nNn centre marker in tile
			var marker = this.createLLOverlay(tileInfo.x + tileOffset, 
												tileInfo.y + tileOffset, tileInfo.overlayURL);
	
			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()
{
	// 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);
}
