// 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;
    
    this.markerStyle = null;

    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 = 100;
Clusterer.defaultGridSize = 15;

// { untilZoomLevel: value, untilDeeperZoomLevel: anotherValue, etc... } Use a large number for the last one
Clusterer.defaultMinMarkersPerCluster = { 10: 2, 13: 3, 1000: 4}

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


Clusterer.Display = function ( clusterer )
{    
    var i, j, marker, cluster;

    clearTimeout( clusterer.timeout );
    

    var newZoomLevel = clusterer.map.getZoom();
    if( !clusterer.gridOffset ||
        newZoomLevel != clusterer.currentZoomLevel ) {
        //N: This is the offset we will use for the grid until the zoom level changes. In this way we can keep the clusters

        bounds = clusterer.map.getBounds();
        ne = bounds.getNorthEast();
        sw = bounds.getSouthWest();

        clusterer.gridOffset = new GLatLng(ne.lat(), sw.lng()); //clusterer.map.getCenter();

        var sz =  clusterer.map.getSize();
        clusterer.cellSize = sz.width / clusterer.gridSize;

        clusterer.gridPos = function(latLng) {
            var o = clusterer.map.fromLatLngToDivPixel(clusterer.gridOffset);
            var p = clusterer.map.fromLatLngToDivPixel(latLng);

            return { 
                x: Math.floor((p.x - o.x)/clusterer.cellSize),
                y: Math.floor((p.y - o.y)/clusterer.cellSize)
            };
        }

        clusterer.mapPos = function(gridPos) {
            var o = clusterer.map.fromLatLngToDivPixel(clusterer.gridOffset);
            
            var p = new GPoint(gridPos.x*clusterer.cellSize + o.x, gridPos.y*clusterer.cellSize + o.y);
            return clusterer.map.fromDivPixelToLatLng(p);
        }

        /*:N*/
    }

    
    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.
    /*N: 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;
        }
    }*/

    //N: Take down all clusters
    for( i = 0; i < clusterer.clusters.length; i++ ) {
        cluster = clusterer.clusters[i];
        if( cluster != null && 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 ( /*N:*/ true  /*:N*/ || visibleMarkers.length > clusterer.maxVisibleMarkers )
    {

        var clusters = [];
        
        sw = bounds.getSouthWest();
        ne = bounds.getNorthEast();

        var g0 = clusterer.gridPos(new GLatLng(ne.lat(), sw.lng()));        
        
        xWidth = clusterer.gridPos(ne).x - g0.x + 1;

        for( i = 0, n = visibleMarkers.length; i < n; i++ ) {

            marker = visibleMarkers[i];
            var pos = clusterer.gridPos(marker.getPoint());

            var index = (pos.x - g0.x) + (pos.y - g0.y)*xWidth;
            
            cluster = clusters[index];
            if( !cluster ) {

                ne = clusterer.mapPos({
                    x: pos.x + 1,
                    y: pos.y
                });
                sw = clusterer.mapPos({
                    x: pos.x,
                    y: pos.y + 1
                    });
                
                cluster = {};
                cluster.clusterer = clusterer;
                cluster.bounds = new GLatLngBounds(sw, ne);                
                
                cluster.markers = [];
                cluster.markerCount = 0;
                cluster.onMap = false;
                cluster.marker = null;
                clusters[index] = cluster;
            }

            ++cluster.markerCount;
            cluster.markers.push(marker);
            marker.inCluster = true;
        }

        var cs = [];
        for( var c in clusters ) {            
            cs.push(clusters[c]);
        }
        clusterer.clusters = cs;        

        

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


        var minMarkers = 2;        
        for( var level in clusterer.minMarkersPerCluster ) {           
            if( level > clusterer.currentZoomLevel ) {
                minMarkers = clusterer.minMarkersPerCluster[level];
               
                break;
            }
        }

        // 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 < minMarkers )
            {
                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.

                // N: ..and the bounds spanned by the markers
                var mne = null, msw = null;
                var xTotal = 0.0, yTotal = 0.0;
                for ( j = 0; j < cluster.markers.length; ++j )
                {
                    marker = cluster.markers[j];
                    if ( marker != null )
                    {
                        pos = marker.getPoint();
                        if( !msw ) {                            
                            msw = pos;
                            mne = pos;
                        } else {                            
                            if( pos.lng() < msw.lng() ) {
                                msw = new GLatLng(msw.lat(), pos.lng());
                            }
                            if( pos.lng() > mne.lng() ) {
                                mne = new GLatLng(mne.lat(), pos.lng());
                            }
                            if( pos.lat() < msw.lat() ) {
                                msw = new GLatLng(pos.lat(), msw.lng());
                            }
                            if( pos.lat() > mne.lat() ) {
                                mne = new GLatLng(pos.lat(), mne.lng());
                            }
                        }

                        xTotal += ( pos.lng() );
                        yTotal += ( pos.lat() );
                    }                                    
                }                
                var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount );
                //marker = new GMarker( location, { icon: clusterer.icon } );                
                cluster.minBounds = new GLatLngBounds(msw, mne);
                
                marker = new ClusterMarker_(location, cluster.markerCount, clusterer.markerStyle, cluster.minBounds);
                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">';
    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 ) + '">';
            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 );
};








//////////////////////////



function ClusterMarker_(latlng, count, styles, bounds) {
    
    var index = 0;
    var dv = count;
    while (dv !== 0) {
        dv = parseInt(dv / 10, 10);
        index ++;
    }

    if (styles.length < index) {
        index = styles.length;
    }
    this.url_ = styles[index - 1].url;
    this.height_ = styles[index - 1].height;
    this.width_ = styles[index - 1].width;
    

    // this.textColor_ = styles[index - 1].opt_textColor;
    this.textColor_ = "#ffffff";
    this.anchor_ = styles[index - 1].opt_anchor;
    this.latlng_ = latlng;
    this.index_ = index;
    this.styles_ = styles;
    this.text_ = count;
    this.bounds = bounds;
}


ClusterMarker_.prototype = new GOverlay();

ClusterMarker_.prototype.getPoint = function() {
    return this.latlng_;
}


/**
 * Initialize cluster marker.
 * @private
 */
ClusterMarker_.prototype.initialize = function (map) {
    this.map_ = map;
    var div = document.createElement("div");
    var latlng = this.latlng_;
    var pos = map.fromLatLngToDivPixel(latlng);
    pos.x -= parseInt(this.width_ / 2, 10);
    pos.y -= parseInt(this.height_ / 2, 10);
    var mstyle = "";
    if (document.all) {
        mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");';
    } else {
        mstyle = "background:url(" + this.url_ + ");";
    }
    if (typeof this.anchor_ === "object") {
        if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) {
            mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;';
        } else {
            mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
        }
        if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) {
            mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;';
        } else {
            mstyle += 'width:' + this.width_ + 'px;text-align:center;';
        }
    } else {
        mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
        mstyle += 'width:' + this.width_ + 'px;text-align:center;';
    }
    var txtColor = this.textColor_ ? this.textColor_ : 'black';
    // RKK: Forced font change from Arial, 11px to Arial Narrow, 12px
    div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" +
    pos.x + "px;color:" + txtColor +  ";position:absolute;font-size:12px;" +
    'font-family:Arial Narrow,sans-serif;font-weight:bold';
    div.innerHTML = this.text_;
    map.getPane(G_MAP_MAP_PANE).appendChild(div);


    var bounds = this.bounds;
    GEvent.addDomListener(div, "click", function () {        
        var zoom = map.getBoundsZoomLevel(bounds, map.getSize());                
        // Don't zoom too much. It's confusing
        //map.setCenter(bounds.getCenter(), zoom);
        map.setCenter(bounds.getCenter(), zoom - 1);
    });
    
    GEvent.addDomListener(div, "mouseover", function() {
        //TODO: i18n
        div.title = "Klik her for at se flere steder";
    });
    this.div_ = div;
};

/**
 * Remove this overlay.
 * @private
 */
ClusterMarker_.prototype.remove = function () {
    this.div_.parentNode.removeChild(this.div_);
};

/**
 * Copy this overlay.
 * @private
 */
ClusterMarker_.prototype.copy = function () {
    return new ClusterMarker_(this.latlng_, this.index_, this.text_, this.styles_, this.bounds);
};

/**
 * Redraw this overlay.
 * @private
 */
ClusterMarker_.prototype.redraw = function (force) {
    if (!force) {
        return;
    }
    var pos = this.map_.fromLatLngToDivPixel(this.latlng_);
    pos.x -= parseInt(this.width_ / 2, 10);
    pos.y -= parseInt(this.height_ / 2, 10);
    this.div_.style.top =  pos.y + "px";
    this.div_.style.left = pos.x + "px";
};

/**
 * Hide this cluster marker.
 */
ClusterMarker_.prototype.hide = function () {
    this.div_.style.display = "none";
};

/**
 * Show this cluster marker.
 */
ClusterMarker_.prototype.show = function () {
    this.div_.style.display = "";
};

/**
 * Get whether the cluster marker is hidden.
 * @return {Boolean}
 */
ClusterMarker_.prototype.isHidden = function () {
    return this.div_.style.display === "none";
};
