/*
 * This is a faster version of GMarkerManager.
 *
 * I don't know what GMarkerManager does internally, but it is slow in IE.
 * This implementation is using a simple grid-based spatial index.
 *
 * Steve Reid / 2007-02-02
 */


/*
 * Methods that are (almost) compatible with GMarkerManager
 */

function SMarkerManager(gmap2) {
	this.gm = gmap2;
	this.minZoomLevel = 0;
	this.maxZoomLevel = 19;

	// used by getMarkerCount
	this.markerCount = [];
	for (var i = this.minZoomLevel; i <= this.maxZoomLevel; i++) {
		this.markerCount[i] = 0;
	}

	// used for debugging
	this.totalMarkerCount = 0;
	this.visibleMarkerCount = 0;
	this.visibleGridCount = 0;
	this.visibleGridSize = [0,0];
	
	// This is our grid-based spatial index
	this.markersByGrid = {};

	// This is used for marker identification across zoom levels.
	this.markersByID = {};

	// This identifies what has been added to the map currently
	this.addedGrids = {};
	this.addedMarkerIDs = {};
	
	// used by _updateMap to determine if it really needs to recheck status
	this.last_n = 0;
	this.last_s = 0;
	this.last_e = 0;
	this.last_w = 0;
	this.last_z = 0;
	this.updateFinished = false;
	
	// Add event
	var sm = this;
	//GEvent.addListener(this.gm, 'moveend', function() { sm._updateMap(); });
	GEvent.addListener(this.gm, 'moveend', function() { window.setTimeout(function() { sm._updateMap(); }, 1);});
	this._updateMap();
}

SMarkerManager.prototype.addMarkers = function() {
	var markers = arguments[0];
	var minZoom = this.minZoomLevel;
	var maxZoom = this.maxZoomLevel;
	
	switch (arguments.length) {
		default:
			throw "Wrong number of arguments";
		case 3:
			maxZoom = arguments[2];
			// fall-through
		case 2:
			minZoom = arguments[1];
	}
	
	for (var i = 0; i < markers.length; i++) {
		addMarker(markers[i], minZoom, maxZoom);
	}
}

SMarkerManager.prototype.addMarker = function() {
	var marker = arguments[0];
	var minZoom = this.minZoomLevel;
	var maxZoom = this.maxZoomLevel;

	switch (arguments.length) {
		default:
			throw "Wrong number of arguments";
		case 3:
			maxZoom = arguments[2];
			// fall-through
		case 2:
			minZoom = arguments[1];
	}
	
	var point = marker.getPoint();
	var id = this._getMarkerID(marker);
	
	if (typeof(this.markersByID[id]) != 'undefined') {
		//throw "Duplicate marker: " + marker.getPoint();
		return;
	}
	
	this.markersByID[id] = marker;
	
	// add the marker to the grid
	for (var zoom = minZoom; zoom <= maxZoom; zoom++) {
		var grid = this._toGrid(point.lat(), point.lng(), zoom).join(',');
		if (typeof(this.markersByGrid[grid]) == 'undefined') {
			this.markersByGrid[grid] = [];
		}
		this.markersByGrid[grid].push(marker);

		// if the marker is in a grid that is on the map, add it to the map
		if (typeof(this.addedGrids[grid]) != 'undefined') {
			this.gm.addOverlay(marker);
			this.visibleMarkerCount++;
			this.addedMarkerIDs[id] = true;
		}
		
		this.markerCount[zoom]++;
	}
	
	this.totalMarkerCount++;
}

SMarkerManager.prototype.refresh = function() {
	// we don't require any refresh, this is just for compatibility
}

SMarkerManager.prototype.getMarkerCount = function(zoom) {
	return this.markerCount[zoom];
}

/*
 * Methods that do not exist in GMarkerManager
 */

SMarkerManager.prototype.unload = function() {
	this.gm = null;
	this.markersByGrid = null;
	this.markersByID = null;
	this.addedGrids = null;
	this.addedMarkerIDs = null;
}

/* This method is untested:
SMarkerManager.prototype.removeMarker = function(gmarker) {
	var point = marker.getPoint();
	var id = this._getMarkerID(gmarker);

	if (typeof(this.markersByID[id]) == 'undefined') {
		//throw "Can't remove marker that hasn't been added: " + gmarker.getPoint();
		return;
	}
	
	delete(this.markersByID[id]);

	// Remove the marker from the grid
	for (var zoom = this.minZoom; zoom <= this.maxZoom; zoom++) {
		var grid = this._toGrid(point.lat(), point.lng(), zoom).join(',');
		var gridContent = this.markersByGrid[grid];
		var newGridContent = [];
		for (var i = 0; i < gridContent.length; i++) {
			var m = gridContent[i];
			if (this._getMarkerID(m) != id) {
				newGridContent.push(m);
			}
		}
		this.markersByGrid[grid] = newGridContent;

		// if the marker is in a grid that is on the map, remove it from the map
		if (typeof(this.addedGrids[grid]) != 'undefined') {
			this.gm.removeOverlay(marker);
			this.visibleMarkerCount--;
			delete(this.addedMarkerIDs[id]);
		}
		
		this.markerCount[zoom]--;
	}
	
	this.totalMarkerCount--;
}
*/

/*
 * Methods that should be considered private
 */

// Returns a key for our grid-based spatial index
SMarkerManager.prototype._toGrid = function(latitude, longitude, zoom) {
	var gridlng = Math.round( longitude * (1 << zoom) * 0.005 );
	var gridlat = Math.round( latitude  * (1 << zoom) * 0.010 );
	return [gridlat, gridlng, zoom];
}

// We can't use markers directly as keys in arrays because they are all considered equal, so make an ID.
// Including our class name in the variables names to avoid name clashes in global and google-internal code.
var _smarkermanager_nextMarkerID = 1;
SMarkerManager.prototype._getMarkerID = function(marker) {
	if (typeof(marker._smarkermanager_markerid) == 'undefined') {
		marker._smarkermanager_markerid = _smarkermanager_nextMarkerID++;
	}
	return marker._smarkermanager_markerid;
}


// Called when the map view changes at all.
SMarkerManager.prototype._updateMap = function() {
	// Get n/s/e/w/z. We want n/s/e/w in our grid format, not regular latitude/longitude.
	var z = this.gm.getZoom();
	var bounds = this.gm.getBounds();
	var nell = bounds.getNorthEast();
	var swll = bounds.getSouthWest();
	var ne = this._toGrid(nell.lat(), nell.lng(), z);
	var sw = this._toGrid(swll.lat(), swll.lng(), z);
	var n = ne[0];
	var e = ne[1];
	var s = sw[0];
	var w = sw[1];
	
	// Has the view changed enough for a different set of grids to be visible?
	if (this.updateFinished && n == this.last_n && e == this.last_e && s == this.last_s && w == this.last_w && z == this.last_z) {
		return; // no change
	}

	this.visibleMarkerCount = 0;
	this.visibleGridSize = [ (e-w)+1, (n-s)+1 ];
	this.visibleGridCount = ((e-w)+1) * ((n-s)+1);	

	// Figure out what needs to be on the new view
	var visibleGrids = {}; 
	var visibleMarkerIDs = {};
	for (var gridlat = s; gridlat <= n; gridlat++) {
		for (var gridlng = w; gridlng <= e; gridlng++) {
			var grid = [gridlat, gridlng, z].join(',');
			visibleGrids[grid] = true;
			var markers = this.markersByGrid[grid];
			if (typeof(markers) != 'undefined') {
				for (var i = 0; i < markers.length; i++) {
					var id = this._getMarkerID(markers[i]);
					visibleMarkerIDs[id] = true;
				}
				this.visibleMarkerCount += markers.length;
			}
		}
	}

	// We count add and remove batches seperately so the user doesn't have to wait for all adds 
	// or all removes before the other operation begins.
	var batchLimit = 20;
	var addBatchSize = 0;
	var removeBatchSize = 0;

	// Remove what was on the old view but is not on the new view
	for (var id in this.addedMarkerIDs) {
		if (removeBatchSize >= batchLimit) break;
		if (typeof(visibleMarkerIDs[id]) == 'undefined') {
			// This marker was on the old view, but is not on the new view, so remove it.
			this.gm.removeOverlay(this.markersByID[id]);
			delete(this.addedMarkerIDs[id]);
			removeBatchSize++;
		}
	}
	
	// Add what is in the new view but was not in the old view
	for (var id in visibleMarkerIDs) {
		if (addBatchSize >= batchLimit) break;
		if (typeof(this.addedMarkerIDs[id]) == 'undefined') {
			// This marker is on the new view, but was not on the old view, so add it.
			this.gm.addOverlay(this.markersByID[id]);
			this.addedMarkerIDs[id] = true;
			addBatchSize++;
		}
	}

	// Update state for next time
	this.addedGrids = visibleGrids;
	this.last_n = n;
	this.last_s = s;
	this.last_e = e;
	this.last_w = w;
	this.last_z = z;

	// If we reached batchLimit, there might be more work to do.
	if (addBatchSize < batchLimit && removeBatchSize < batchLimit) {
		this.updateFinished = true;
	}
	else {
		this.updateFinished = false;
		var smm = this;
		window.setTimeout(function() { smm._updateMap() }, 1);
	}
	
}

