/**
 * @fileOverview
 * FFHTopo :: Map API abstraction library
 *
 * Description:
 * 	abstraction of basic functionality of Google Maps API
 * 
 * Depends:
 *	FFHTopo Utils (mtutils.js)
 *
 * @author Steven Barth steven-at-midlink-dot-org
 * @license <a href="http://www.gnu.org/licenses/gpl.html">GNU General Public License v3 or later</a>
 * @revision $Id: mtmap-google.js 5M 2007-11-04 14:07:05Z (lokal) $
 */
/** 
 * Anpassungen durch Matthias Schäfer, tox-freifunk gmx de, 24.08.2009 20:00 UTC
 * - Scrollen über Mausrad
 */
 
mtMapGoogleVersion = 0.10;
mtMapGoogleVersionString = mtMapGoogleVersion + " $Rev: 5M $"
 
/**
 * Map Abstraction for Google Maps API 2.x
 * 
 * This is the base class for Map API calls.
 * All abstraction classes with the same version number should have the same calling conventions.
 * 
 * @version 0.10
 * @param {HTMLElement} layer The layer in which the map will be drawn.
 * @constructor
 */
function mtMapGoogle(layer) {
	var map = new GMap2(layer);
	var geocoder = new GClientGeocoder();
	var gclocale = "";
	
	map.enableScrollWheelZoom();

    var icon = new GIcon();
    icon.image = "resource/cloud.png";
    icon.iconSize = new GSize(96, 96);
    icon.iconAnchor = new GPoint(48, 48);
    icon.infoWindowAnchor = new GPoint(48, 48);
    var clusterer = new Clusterer(map);
    clusterer.SetIcon(icon);
    clusterer.SetMaxVisibleMarkers(50)
    clusterer.SetMinMarkersPerCluster(5)
    clusterer.SetMaxLinesPerInfoBox(15)
	
	
	this.type = "google";
	
	/**
	 * Adds a specific overlay to the map
	 * @param {GOverlay} obj The overlay to be added
	 */
	this.addClusterOverlay = function(obj) {
		clusterer.AddMarker(obj);
	}
	
	/**
	 * Adds a specific overlay to the map
	 * @param {GOverlay} obj The overlay to be added
	 */
	this.addOverlay = function(obj) {
		map.addOverlay(obj);
		obj.onMap = true;
	}
	
	/**
	 * Adds standard user interface controls to the map
	 */
	this.addUiControls = function() {
		map.addControl(new GLargeMapControl());
	    map.addControl(new GMapTypeControl());
	    map.addControl(new GScaleControl());
	    new GKeyboardHandler(map);
	}
	
	/**
	 * Calculates the angle for a vector from p1 to p2
	 * 
	 * @param {GLatLng} p1 source point
	 * @param {GLatLng} p2 destination point
	 * 
	 * @return {float} angle
	 */
	this.angle = function(p1, p2) {
	    var p3 = this.createPoint(p1.lat(), p2.lng());
	    var dang = Math.atan(this.distance(p2, p3) / this.distance(p1, p3)) / (Math.PI / 180.0);
	
	    if (p1.lat() < p2.lat() && p1.lng() < p2.lng()) {
	      dang = 90 - dang;
	    }
	
	    if (p1.lat() < p2.lat() && p1.lng() > p2.lng()) {
	      dang = 270 + dang;
	    }
	
	    if (p1.lat() > p2.lat() && p1.lng() < p2.lng()) {
	      dang = 90 + dang;
	    }
	
	    if (p1.lat() > p2.lat() && p1.lng() > p2.lng()) {
	      dang = 270 - dang;
	    }

    	return dang;
	}
	
	/**
	 * Binds a specific callback function to an event fired by an object
	 * 
	 * This function is a wrapper for GEvent.addListener
	 * 
	 * The object will be avaiable as "this" to the callback function.
	 * If the object is the map itself, the callback will have two parameters:
	 * object and point, where object is the reference to the overlay clicked on (otherwise null)
	 * and point is an instance of GLatLng with the coordinates of the event
	 * 
	 * Currently "click" and "dblclick" are supported events on all Map APIs
	 * although others might work for some APIs
	 * 
	 * @param {GOverlay} object
	 * @param {String} event
	 * @param {Function} callback
	 * 
	 * @return {GEventHandler} Event handle for unbindEventHandler() 
	 */
	this.bindEventHandler = function(object, event, callback) {
		return GEvent.addListener(object, event, callback);
	}
	
	/**
	 * Binds a specific callback function that offers data for map information windows to a marker 
	 * 
	 * The callback function should return an Array with one or more information objects like
	 * [{title: "Title #1", descr: "Some information"}, {title: "Title #2", descr: "Some more information"}]  
	 * 
	 * @param {GMarker} marker The marker to which the function will be bound
	 * @param {Function} callback The callback function that offers the information data
	 */
	this.bindInfoWindow = function(marker, callback) {
		marker._mtRetrInfo = callback;
		GEvent.addListener(marker, 'click', this._openInfoWindow);
	}
	
	/**
	 * Removes all overlays from the map
	 */
	this.clearOverlays = function() {
		map.clearOverlays();
	}
	
	/**
	 * Creates a color pool which offers 10 distinguishable color references in maptype specific format
	 * 
	 * It will return the colors:
	 * '#FF0000', '#008000', '#0000FF', '#000000', '#FFFF00',	'#A52A2A', '#808080', '#FFA500', '#800080', '#FFFFFF'
	 * in this order
	 * 
	 * @constructor
	 */
	this.colorPool = function() {
		var ccol = 0;
		var colr = ['#FF0000', '#008000', '#0000FF', '#000000', '#FFFF00',
			'#A52A2A', '#808080', '#FFA500', '#800080', '#FFFFFF'];
			
		/**
		 * Fetches the next color reference
		 * 
		 * @return {String} color
		 */
		this.fetch = function() {
			if (ccol < colr.length) { 
				ccol++;
			}
			return colr[ccol-1];
		}
	}
	
	/**
	 * Converts a maptype specific color reference fetched from colorPool() to an
	 * hexadecimal color code
	 * 
	 * @param {String} color color reference
	 * @return {String} hexadecimal color code
	 */
	this.colorToHex = function(color) {
		return color;
	}
	
	/**
	 * Creates and returns a group object which can handler multiple overlays at once
	 * 
	 * This is an emulation of a part of the VEShapeLayer class from Virtual Earth
	 * 
	 * @return {_GMapOverlayGroup} Group Object
	 */
	this.createGroup = function(cluster) {
		return new this._GMapOverlayGroup(this, cluster);
	}
	
	/**
	 * Creates a new map type specific marker object
	 * 
	 * This is a wrapper for the GMarker constructor
	 * 
	 * @param {GLatLng} point maptype specific point returned by createPoint()
	 * @param {GIcon} icon maptype specific icon format returned by iconPool()
	 *
	 * @return {GMarker} marker 
	 */
	this.createMarker = function(point, icon) {
		return new GMarker(point, icon);
	}
	
	/**
	 * Creates a new maptype specific point from geo coordinates
	 * 
	 * This is a wrapper for the GLatLng constructor
	 * 
	 * @param {float} lat Latitude
	 * @param {float} lng Longitude
	 * 
	 * @return {GLatLng} point
	 */
	this.createPoint = function(lat, lng) {
		return new GLatLng(lat, lng);
	}
	
	/**
	 * Creates a new maptype specific polyline object
	 * 
	 * This is a wrapper for the GPolyline constructor
	 * 
	 * @param {Array} points array of points, see: createPoint()
	 * @param {Object} color color object, see: colorPool()
	 * @param {integer} width line width in pixels
	 * @param {float} opac opacity as floating point number between 0 and 1
	 * 
	 * @return {GPolyline} polyline
	 */
	this.createPolyline = function(points, color, width, opac) {
		return new GPolyline(points, color, width, opac);
	}
	
	/**
	 * Calculates the distance between points a and b in meters
	 * 
	 * @param {GLatLng} a
	 * @param {GLatLng} b
	 * 
	 * @return {float} distance (m)
	 */
	this.distance = function(a, b) {
		var alpha = (90 - a.lat()) * (Math.PI / 180.0);
		var beta  = (90 - b.lat()) * (Math.PI / 180.0);
		var gamma = (b.lng() - a.lng()) * (Math.PI / 180.0);
		var c = Math.acos(Math.sin(alpha) * Math.sin(beta) * Math.cos(gamma) + Math.cos(alpha) * Math.cos(beta));
		return c * 6367000;
	}
	
	/**
	 * Starts an asynchronous address resolving attempt for a given address
	 * If this succeeds the callback function will be called with a point object as only parameter
	 * otherwise the parameter will be null
	 * 
	 * @param {String} adr the address to be resolved
	 * @param {Function} callback the callback function
	 */
	this.geocode = function(adr, callback) {
		geocoder.getLatLng(adr + gclocale, callback);
	}
	
	/**
	 * Returns an object that describes the current mapstate
	 * 
	 * @return {object}
	 */
	this.getMapState = function() {
		return {
			lat: this.map.getCenter().lat(),
			lng: this.map.getCenter().lng(),
			zoom: this.map.getZoom(),
			type: this.map.getCurrentMapType().getUrlArg()
		}
	}
	
	/**
	 * Returns the maptype specific map object
	 * Be aware of the API differences 
	 * 
	 * @return {GMap2} native map object
	 */
	this.getMapObject = function() {
		return map;
	}
	
	/**
	 * Extracts the URL from the maptype specific icon object
	 * 
	 * @param {GIcon} icon icon
	 * @return {String} URL
	 */
	this.iconToUrl = function(icon) {
		return icon.image;
	}
	
	/**
	 * Creates a color pool which offers 10 distinguishable color references in maptype specific format.
	 * 
	 * It will return icons of the following colors:
	 * 'black', 'green', 'blue', 'yellow', 'red', 'brown', 'gray', 'orange', 'purple', 'white'
	 * in this order
	 * 
	 * @constructor
	 */
	this.iconPool = function() {
		var cico = 0;
		var colr = ['black', 'green', 'blue', 'yellow', 'red', 
		'brown', 'gray', 'orange', 'purple', 'white'];
		
		/**
		 * Fetches the next icon reference
		 * 
		 * @return {GIcon} icon
		 */
		this.fetch = function() {
			if (cico < colr.length) { 
				cico++;
			}
			var icon = new GIcon();
	        icon.image = "http://labs.google.com/ridefinder/images/mm_20_" + colr[cico-1] + ".png";
	        icon.shadow = "http://labs.google.com/ridefinder/images/mm_20_shadow.png";
	        icon.iconSize = new GSize(12, 20);
	        icon.shadowSize = new GSize(22, 20);
	        icon.iconAnchor = new GPoint(6, 19);
	        icon.infoWindowAnchor = new GPoint(5, 1);
			return icon;
		}
	}
	
	/**
	 * Extracts the point from a given marker
	 * 
	 * @param {GMarker} marker marker
	 * @return {GLatLng} point
	 */
	this.markerGetPoint = function(marker) {
		return marker.getPoint();
	}
	
	/**
	 * Extracts the latitude and longitude information from a given point into an array
	 * 
	 * @param {GLatLng} point point
	 * @return {Array} [latitude, longitude]
	 */
	this.pointGetLatLng = function(point) {
		return [point.lat(), point.lng()];
	}
	
	/**
	 * Removes given overlay from the map
	 * 
	 * @param {GOverlay} obj overlay
	 */
	this.removeClusterOverlay = function(obj) {
		clusterer.RemoveMarker(obj);
	}
	
	/**
	 * Removes given overlay from the map
	 * 
	 * @param {GOverlay} obj overlay
	 */
	this.removeOverlay = function(obj) {
		obj.onMap = false;
		map.removeOverlay(obj);
	}	
	
	/**
	 * Sets the map center to a specific point and zooms to the given zoomlevel
	 * 
	 * @param {GLatLng} point point
	 * @param {integer} zoom zoomlevel
	 */
	this.setCenter = function(point, zoom) {
		map.setCenter(point, zoom);
	}
	
	/**
	 * Sets locale information of the geocoder
	 * These will be automatically appended to any given address lookup request
	 * 
	 * @param {String} locale
	 */
	this.setGeocoderLocale = function(locale) {
		gclocale = locale;
	}

	/**
	 * Sets the map center to the given marker and opens the information window for it
	 * 
	 * @param {GMarker} marker marker
	 */
	this.showMarker = function(marker) {
		map.panTo(marker.getPoint());
		this._openInfoWindow(marker);
		if (marker.onMap == false) {
			this.addOverlay(marker);
		}
	}
	
	/**
	 * Unbinds given event handler
	 * 
	 * @param {GEventHandler} handler handler
	 */
	this.unbindEventHandler = function(handler) {
		GEvent.removeListener(handler);
	}
	
	/**
	 * @ignore
	 */
	this._openInfoWindow = function(marker) {
		if (this._mtRetrInfo) {
			var marker = this;
		}
		if (marker._mtRetrInfo) {
			var info = marker._mtRetrInfo();
			var infowin = $n('div').applyStyle({fontSize: '0.7em'});
			
			for (var i=0; i<info.length; i++) {
				var heading = $n('div', null, info[i].title);
				heading.style.fontWeight = 'bold';
				infowin.append([heading, info[i].descr, $n('br')]);
			}
			marker.openInfoWindow(infowin);
		}
	}
	
	/**
	 * VEShapeLayer emulation class
	 * 
	 * Stores a group of map overlays
	 * 
	 * You should not instantiate this class manually, use mtMapGoogle.createGroup() instead
	 * 
	 * @constructor
	 * @param {GMap2} gmap map abstraction object
	 */
	this._GMapOverlayGroup = function(gmap, cluster) {
		var cluster = cluster;
		var a = [];
		var shown = false;
		var gmap = gmap;
		
		/**
		 * Adds an overlay to the group
		 * 
		 * @param {GOverlay} shape overlay
		 */
		this.AddShape = function(shape) {
			a.push(shape);
			if (shown) {
				if (cluster) {
					gmap.addClusterOverlay(shape);
				} else {
					gmap.addOverlay(shape);
				}
			}
		}
		
		/**
		 * Deletes an overlay from the group
		 * 
		 * @param {GOverlay} shape overlay
		 */		
		this.DeleteShape = function(shape) {
			if (shown) {
				if (cluster) {
					gmap.removeClusterOverlay(shape);
				} else {
					gmap.removeOverlay(shape);
				}
			}
			a.remove(shape);
		}

		/**
		 * Deletes all overlays from the group
		 */
		this.DeleteAllShapes = function() {
			a.clear();
		}
		
		/**
		 * Shows all overlays of the group on the map
		 */
		this.Show = function() {
			if (cluster) {
				a.iterate(gmap.addClusterOverlay);
			} else {
				a.iterate(gmap.addOverlay);
			}
			shown = true;
		}
		
		/**
		 * Hides all overlay of the group from the map 
		 */
		this.Hide = function() {
			if (cluster) {
				a.iterate(gmap.removeClusterOverlay);
			} else {
				a.iterate(gmap.removeOverlay);
			}
			shown = false;
		}
		
		/**
		 * @ignore
		 */
		this._each = function(callback) {
			a.iterate(callback);
		}
	}	
}

// Clusterer.js - marker clustering routines for Google Maps apps
//
// Using these routines is very easy.
//
// 1) Load the routines into your code:
//
//        <script src="http://www.acme.com/javascript/Clusterer.js" type="text/javascript"></script>
//
// 2) Create a Clusterer object, passing it your map object:
//
//        var clusterer = new Clusterer( map );
//
// 3) Wherever you now do map.addOverlay( marker ), instead call
//    clusterer.AddMarker( marker, title ).  The title is just a
//    short descriptive string to use in the cluster info-boxes.
//
// 4) If you are doing any map.removeOverlay( marker ) calls, change those
//    to clusterer.RemoveMarker( marker ).
//
// That's it!  Everything else happens automatically.
//
//
// The current version of this code is always available at:
// http://www.acme.com/javascript/
//
//
// Copyright © 2005,2006 by Jef Poskanzer <jef@mail.acme.com>.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// For commentary on this license please see http://www.acme.com/license.html


// Constructor.
Clusterer = function ( map )
    {
    this.map = map;
    this.markers = [];
    this.clusters = [];
    this.timeout = null;
    this.currentZoomLevel = map.getZoom();

    this.maxVisibleMarkers = Clusterer.defaultMaxVisibleMarkers;
    this.gridSize = Clusterer.defaultGridSize;
    this.minMarkersPerCluster = Clusterer.defaultMinMarkersPerCluster;
    this.maxLinesPerInfoBox = Clusterer.defaultMaxLinesPerInfoBox;
    this.icon = Clusterer.defaultIcon;

    GEvent.addListener( map, 'zoomend', Clusterer.MakeCaller( Clusterer.Display, this ) );
    GEvent.addListener( map, 'moveend', Clusterer.MakeCaller( Clusterer.Display, this ) );
    GEvent.addListener( map, 'infowindowclose', Clusterer.MakeCaller( Clusterer.PopDown, this ) );
    };


Clusterer.defaultMaxVisibleMarkers = 150;
Clusterer.defaultGridSize = 5;
Clusterer.defaultMinMarkersPerCluster = 5;
Clusterer.defaultMaxLinesPerInfoBox = 10;

Clusterer.defaultIcon = new GIcon();
Clusterer.defaultIcon.image = 'http://www.acme.com/resources/images/markers/blue_large.PNG';
Clusterer.defaultIcon.shadow = 'http://www.acme.com/resources/images/markers/shadow_large.PNG';
Clusterer.defaultIcon.iconSize = new GSize( 30, 51 );
Clusterer.defaultIcon.shadowSize = new GSize( 56, 51 );
Clusterer.defaultIcon.iconAnchor = new GPoint( 13, 34 );
Clusterer.defaultIcon.infoWindowAnchor = new GPoint( 13, 3 );
Clusterer.defaultIcon.infoShadowAnchor = new GPoint( 27, 37 );


// Call this to change the cluster icon.
Clusterer.prototype.SetIcon = function ( icon )
    {
    this.icon = icon;
    };


// Changes the maximum number of visible markers before clustering kicks in.
Clusterer.prototype.SetMaxVisibleMarkers = function ( n )
    {
    this.maxVisibleMarkers = n;
    };


// Sets the minumum number of markers for a cluster.
Clusterer.prototype.SetMinMarkersPerCluster = function ( n )
    {
    this.minMarkersPerCluster = n;
    };


// Sets the maximum number of lines in an info box.
Clusterer.prototype.SetMaxLinesPerInfoBox = function ( n )
    {
    this.maxLinesPerInfoBox = n;
    };


// Call this to add a marker.
Clusterer.prototype.AddMarker = function ( marker, title )
    {
    if ( marker.setMap != null )
	marker.setMap( this.map );

    marker.title = title;
    marker.onMap = false;
    this.markers.push( marker );
    this.DisplayLater();
    };


// Call this to remove a marker.
Clusterer.prototype.RemoveMarker = function ( marker )
    {
    for ( var i = 0; i < this.markers.length; ++i )
	if ( this.markers[i] == marker )
	    {
	    if ( marker.onMap )
		this.map.removeOverlay( marker );
	    for ( var j = 0; j < this.clusters.length; ++j )
		{
		var cluster = this.clusters[j];
		if ( cluster != null )
		    {
		    for ( var k = 0; k < cluster.markers.length; ++k )
			if ( cluster.markers[k] == marker )
			    {
			    cluster.markers[k] = null;
			    --cluster.markerCount;
			    break;
			    }
		    if ( cluster.markerCount == 0 )
			{
			this.ClearCluster( cluster );
			this.clusters[j] = null;
			}
		    else if ( cluster == this.poppedUpCluster )
			Clusterer.RePop( this );
		    }
		}
	    this.markers[i] = null;
	    break;
	    }
    this.DisplayLater();
    };


Clusterer.prototype.DisplayLater = function ()
    {
    if ( this.timeout != null )
	clearTimeout( this.timeout );
    this.timeout = setTimeout( Clusterer.MakeCaller( Clusterer.Display, this ), 50 );
    };


Clusterer.Display = function ( clusterer )
    {
    var i, j, marker, cluster;

    clearTimeout( clusterer.timeout );

    var newZoomLevel = clusterer.map.getZoom();
    if ( newZoomLevel != clusterer.currentZoomLevel )
	{
	// When the zoom level changes, we have to remove all the clusters.
	for ( i = 0; i < clusterer.clusters.length; ++i )
	    if ( clusterer.clusters[i] != null )
		{
		clusterer.ClearCluster( clusterer.clusters[i] );
		clusterer.clusters[i] = null;
		}
	clusterer.clusters.length = 0;
	clusterer.currentZoomLevel = newZoomLevel;
	}

    // Get the current bounds of the visible area.
    var bounds = clusterer.map.getBounds();

    // Expand the bounds a little, so things look smoother when scrolling
    // by small amounts.
    var sw = bounds.getSouthWest();
    var ne = bounds.getNorthEast();
    var dx = ne.lng() - sw.lng();
    var dy = ne.lat() - sw.lat();
    if ( dx < 300 && dy < 150 )
	{
	dx *= 0.10;
	dy *= 0.10;
	bounds = new GLatLngBounds(
	  new GLatLng( sw.lat() - dy, sw.lng() - dx ),
	  new GLatLng( ne.lat() + dy, ne.lng() + dx ) );
	}

    // Partition the markers into visible and non-visible lists.
    var visibleMarkers = [];
    var nonvisibleMarkers = [];
    for ( i = 0; i < clusterer.markers.length; ++i )
	{
	marker = clusterer.markers[i];
	if ( marker != null )
	    if ( bounds.contains( marker.getPoint() ) )
		visibleMarkers.push( marker );
	    else
		nonvisibleMarkers.push( marker );
	}

    // Take down the non-visible markers.
    for ( i = 0; i < nonvisibleMarkers.length; ++i )
	{
	marker = nonvisibleMarkers[i];
	if ( marker.onMap )
	    {
	    clusterer.map.removeOverlay( marker );
	    marker.onMap = false;
	    }
	}

    // Take down the non-visible clusters.
    for ( i = 0; i < clusterer.clusters.length; ++i )
	{
	cluster = clusterer.clusters[i];
	if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap )
	    {
	    clusterer.map.removeOverlay( cluster.marker );
	    cluster.onMap = false;
	    }
	}

    // Clustering!  This is some complicated stuff.  We have three goals
    // here.  One, limit the number of markers & clusters displayed, so the
    // maps code doesn't slow to a crawl.  Two, when possible keep existing
    // clusters instead of replacing them with new ones, so that the app pans
    // better.  And three, of course, be CPU and memory efficient.
    if ( visibleMarkers.length > clusterer.maxVisibleMarkers )
	{
	// Add to the list of clusters by splitting up the current bounds
	// into a grid.
	var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
	var latInc = latRange / clusterer.gridSize;
	var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 );
	for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc )
	    for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc )
		{
		cluster = new Object();
		cluster.clusterer = clusterer;
		cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) );
		cluster.markers = [];
		cluster.markerCount = 0;
		cluster.onMap = false;
		cluster.marker = null;
		clusterer.clusters.push( cluster );
		}

	// Put all the unclustered visible markers into a cluster - the first
	// one it fits in, which favors pre-existing clusters.
	for ( i = 0; i < visibleMarkers.length; ++i )
	    {
	    marker = visibleMarkers[i];
	    if ( marker != null && ! marker.inCluster )
		{
		for ( j = 0; j < clusterer.clusters.length; ++j )
		    {
		    cluster = clusterer.clusters[j];
		    if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) )
			{
			cluster.markers.push( marker );
			++cluster.markerCount;
			marker.inCluster = true;
			}
		    }
		}
	    }

	// Get rid of any clusters containing only a few markers.
	for ( i = 0; i < clusterer.clusters.length; ++i )
	    if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster )
		{
		clusterer.ClearCluster( clusterer.clusters[i] );
		clusterer.clusters[i] = null;
		}

	// Shrink the clusters list.
	for ( i = clusterer.clusters.length - 1; i >= 0; --i )
	    if ( clusterer.clusters[i] != null )
		break;
	    else
		--clusterer.clusters.length;

	// Ok, we have our clusters.  Go through the markers in each
	// cluster and remove them from the map if they are currently up.
	for ( i = 0; i < clusterer.clusters.length; ++i )
	    {
	    cluster = clusterer.clusters[i];
	    if ( cluster != null )
		{
		for ( j = 0; j < cluster.markers.length; ++j )
		    {
		    marker = cluster.markers[j];
		    if ( marker != null && marker.onMap )
			{
			clusterer.map.removeOverlay( marker );
			marker.onMap = false;
			}
		    }
		}
	    }

	// Now make cluster-markers for any clusters that need one.
	for ( i = 0; i < clusterer.clusters.length; ++i )
	    {
	    cluster = clusterer.clusters[i];
	    if ( cluster != null && cluster.marker == null )
		{
		// Figure out the average coordinates of the markers in this
		// cluster.
		var xTotal = 0.0, yTotal = 0.0;
		for ( j = 0; j < cluster.markers.length; ++j )
		    {
		    marker = cluster.markers[j];
		    if ( marker != null )
			{
			xTotal += ( + marker.getPoint().lng() );
			yTotal += ( + marker.getPoint().lat() );
			}
		    }
		var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount );
		marker = new GMarker( location, { icon: clusterer.icon } );
		cluster.marker = marker;
		GEvent.addListener( marker, 'click', Clusterer.MakeCaller( Clusterer.PopUp, cluster ) );
		}
	    }
	}

    // Display the visible markers not already up and not in clusters.
    for ( i = 0; i < visibleMarkers.length; ++i )
	{
	marker = visibleMarkers[i];
	if ( marker != null && ! marker.onMap && ! marker.inCluster )
	    {
	    clusterer.map.addOverlay( marker );
	    if ( marker.addedToMap != null )
		marker.addedToMap();
	    marker.onMap = true;
	    }
	}

    // Display the visible clusters not already up.
    for ( i = 0; i < clusterer.clusters.length; ++i )
	{
	cluster = clusterer.clusters[i];
	if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() ) )
	    {
	    clusterer.map.addOverlay( cluster.marker );
	    cluster.onMap = true;
	    }
	}

    // In case a cluster is currently popped-up, re-pop to get any new
    // markers into the infobox.
    Clusterer.RePop( clusterer );
    };


Clusterer.PopUp = function ( cluster )
    {
    var clusterer = cluster.clusterer;
    var html = '<table width="300" style="font-size: 0.8em">';
    var n = 0;
    for ( var i = 0; i < cluster.markers.length; ++i )
	{
	var marker = cluster.markers[i];
	if ( marker != null )
	    {
	    ++n;
	    html += '<tr><td>';
	    if ( marker.getIcon().smallImage != null )
		html += '<img src="' + marker.getIcon().smallImage + '">';
	    else
		html += '<img src="' + marker.getIcon().image + '" width="' + ( marker.getIcon().iconSize.width / 2 ) + '" height="' + ( marker.getIcon().iconSize.height / 2 ) + '">';
		if (marker._mtRetrInfo) {
			html += '</td><td>' + marker._mtRetrInfo()[0].title + '</td></tr>';
		} else {
	    	html += '</td><td>' + marker.title + '</td></tr>';
	    }
	    if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox  )
		{
		html += '<tr><td colspan="2">...and ' + ( cluster.markerCount - n ) + ' more</td></tr>';
		break;
		}
	    }
	}
    html += '</table>';
    clusterer.map.closeInfoWindow();
    cluster.marker.openInfoWindowHtml( html );
    clusterer.poppedUpCluster = cluster;
    };


Clusterer.RePop = function ( clusterer )
    {
    if ( clusterer.poppedUpCluster != null )
	Clusterer.PopUp( clusterer.poppedUpCluster );
    };


Clusterer.PopDown = function ( clusterer )
    {
    clusterer.poppedUpCluster = null;
    };


Clusterer.prototype.ClearCluster = function ( cluster )
    {
    var i, marker;

    for ( i = 0; i < cluster.markers.length; ++i )
	if ( cluster.markers[i] != null )
	    {
	    cluster.markers[i].inCluster = false;
	    cluster.markers[i] = null;
	    }
    cluster.markers.length = 0;
    cluster.markerCount = 0;
    if ( cluster == this.poppedUpCluster )
	this.map.closeInfoWindow();
    if ( cluster.onMap )
	{
	this.map.removeOverlay( cluster.marker );
	cluster.onMap = false;
	}
    };


// This returns a function closure that calls the given routine with the
// specified arg.
Clusterer.MakeCaller = function ( func, arg )
    {
    return function () { func( arg ); };
    };


// Augment GMarker so it handles markers that have been created but
// not yet addOverlayed.

GMarker.prototype.setMap = function ( map )
    {
    this.map = map;
    };

GMarker.prototype.addedToMap = function ()
    {
    this.map = null;
    };

GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow;
GMarker.prototype.openInfoWindow = function ( node, opts )
    {
    if ( this.map != null )
	return this.map.openInfoWindow( this.getPoint(), node, opts );
    else
	return this.origOpenInfoWindow( node, opts );
    };

GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml;
GMarker.prototype.openInfoWindowHtml = function ( html, opts )
    {
    if ( this.map != null )
	return this.map.openInfoWindowHtml( this.getPoint(), html, opts );
    else
	return this.origOpenInfoWindowHtml( html, opts );
    };

GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs;
GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts )
    {
    if ( this.map != null )
	return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts );
    else
	return this.origOpenInfoWindowTabs( tabNodes, opts );
    };

GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml;
GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts )
    {
    if ( this.map != null )
	return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts );
    else
	return this.origOpenInfoWindowTabsHtml( tabHtmls, opts );
    };

GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup;
GMarker.prototype.showMapBlowup = function ( opts )
    {
    if ( this.map != null )
	return this.map.showMapBlowup( this.getPoint(), opts );
    else
	return this.origShowMapBlowup( opts );
    };
