function MarkerSpreader(map, threshold, spreadRadiusFunction) {
    this.map = map;
    this.threshold = (threshold ? threshold : -1); // the threshold is in meters
    this.spreadRadiusFunction = (spreadRadiusFunction ?
        spreadRadiusFunction : this.defaultSpreadRadiusFunction);
}

/**
 * Returns the radius, in longitude degrees, that will be used to spread the
 * markers of a cluster.
 * @param center a google.map.LatLng object that represents the center of the
 * cluster
 * @param numberOfElements The number of elements in the cluster
 * @return The radius, in degrees, that will be used to spread the
 * markers of a cluster.
 */
MarkerSpreader.prototype.defaultSpreadRadiusFunction = function(center, numberOfElements) {
    if(numberOfElements < 10)
        return 0.04;
    else
        return 0.08;
}

MarkerSpreader.prototype.compareMarkers = function(marker1, marker2) {
    var ll1 = marker1.getLatLng();
    var ll2 = marker2.getLatLng();
    if((window.spreader.threshold <= 0 && ll1.equals(ll2)) ||
       (window.spreader.threshold > 0 && ll1.distanceFrom(ll2) < this.threshold)) {
        return 0;
    }
    else {
        if(ll1.lat() < ll2.lat()) {
            return -1;
        }
        else if(ll1.lat() > ll2.lat()) {
            return 1;
        }
        else {
            if(ll1.lng() < ll2.lng()) {
                return -1;
            }
            else if(ll1.lng() > ll2.lng()) {
                return 1;
            }
            else {
                return 0;
            }
        }
    }
}

MarkerSpreader.prototype.spreadMarkers = function(markers) {
    if(markers.length == 0)
        return new Array();
    
    window.spreader = this;
    markers.sort(this.compareMarkers);
    var ret = new Array();

    // create clusters
    var clusters = new Array();
    var lastMarker = markers[0];
    // put the first point in the first cluster
    clusters.push(new Array());
    var clusterIndex = clusters.length - 1;
    clusters[clusterIndex].push(lastMarker);

    for(i = 1; i < markers.length; i++) {
        var actMarker = markers[i];
        if(this.compareMarkers(lastMarker, actMarker) != 0) {
            // create a  new cluster
            clusters.push(new Array());
            clusterIndex = clusters.length - 1;
            lastMarker = actMarker;
        }
        clusters[clusterIndex].push(actMarker);
    }

    var projector = new google.maps.MercatorProjection(20);
    var projectionZoomLevel = 16;
    for(i = 0; i < clusters.length; i++) {
        var cluster = clusters[i];
        if(cluster.length == 1) {
            ret.push(cluster[0]);
        }
        else {
            // spread the cluster's point around the position of the
            // first element
            ret.push(cluster[0]);
            
            var centerLL = cluster[0].getLatLng();
            var radiusLL = this.spreadRadiusFunction(centerLL, cluster.length);
            var firstPointLL = new google.maps.LatLng(centerLL.lat(), centerLL.lng() + radiusLL);

            var center = projector.fromLatLngToPixel(centerLL, projectionZoomLevel);
            var firstPoint = projector.fromLatLngToPixel(firstPointLL, projectionZoomLevel);
            var radius = firstPoint.x - center.x;

            var delta = 2 * Math.PI / (cluster.length - 1);
            for(j = 1; j < cluster.length; j++) {
                var angle = j * delta;
                var point = new google.maps.Point(
                    center.x + radius * Math.cos(angle),
                    center.y + radius * Math.sin(angle)
                );
                cluster[j].setLatLng(projector.fromPixelToLatLng(point, projectionZoomLevel));
                ret.push(cluster[j]);
            }
        }
    }

    return ret;
}