
function MarkerClusterer(map, markers, clusterDetailsCvs, clusterDetailsGrd, markerInfoCvs, markerType, minZoomLvlShow, maxZoomLvlShow) {
   this.clusterDetailsCvs_ = clusterDetailsCvs;
   this.clusterDetailsGrd_ = clusterDetailsGrd;
   this.clusters_ = [];
   this.dataInited_ = false;
   this.gridSize_;
   this.map_ = map;
   this.markerInfoCvs_ = markerInfoCvs;
   this.markers_ = markers;
   this.markerType_ = markerType;
   this.maxZoomCluster_ = 12;
   this.maxZoomLvlShow_ = maxZoomLvlShow;
   this.minZoomLvlShow_ = minZoomLvlShow;
   this.triggerBounds_;
   this.mcfn_ = null;

   this.curZoom_ = this.map_.getZoom();
  
   if (this.curZoom_ >= this.maxZoomCluster_ || this.markerType_ == _constMarkerTypeZIP3 || this.markerType_ == _constMarkerTypeState) {
      this.gridSize_ = parseInt(0);
   } else {
      this.gridSize_ = parseInt(60);
   }
   triggerBounds_ = this.getTriggerBounds();

   if (this.curZoom_ >= this.minZoomLvlShow_ && this.curZoom_ <= this.maxZoomLvlShow_) {
      // initialize
      if (markers && markers.length) {
         this.addMarkers();
         this.dataInited_ = true;
      }
   }

   var that = this;
   this.mcfn_ = google.maps.event.addListener(this.map_, "idle", function() {
      that.onMapMoveEnd();
   });
}

    MarkerClusterer.prototype.onMapMoveEnd = function() {
      this.resetViewport();
      if (((this.isTriggered() && this.markerType_ == _constMarkerTypeZIP) || !this.dataInited_) &&
        this.curZoom_ >= this.minZoomLvlShow_ && this.curZoom_ <= this.maxZoomLvlShow_) {
        getCountsForOccupationMore();
      }
    };

    MarkerClusterer.prototype.getTriggerBounds = function() {
      var dataLoadBounds = this.map_.getBounds();
      var latTop = dataLoadBounds.getNorthEast().lat() + (Math.abs(dataLoadBounds.getSouthWest().lat() - dataLoadBounds.getNorthEast().lat()) * (_triggerPct / 100));
      var latBottom = dataLoadBounds.getSouthWest().lat() - (Math.abs(dataLoadBounds.getSouthWest().lat() - dataLoadBounds.getNorthEast().lat()) * (_triggerPct / 100));
      var lngLeft = dataLoadBounds.getSouthWest().lng() - (Math.abs(dataLoadBounds.getNorthEast().lng() - dataLoadBounds.getSouthWest().lng()) * (_triggerPct / 100));
      var lngRight = dataLoadBounds.getNorthEast().lng() + (Math.abs(dataLoadBounds.getNorthEast().lng() - dataLoadBounds.getSouthWest().lng()) * (_triggerPct / 100));
      return new google.maps.LatLngBounds(new google.maps.LatLng(latBottom, lngLeft), new google.maps.LatLng(latTop, lngRight));
    };
    
    MarkerClusterer.prototype.isTriggered = function() {
      var mapBounds = this.map_.getBounds();
      if (mapBounds.getNorthEast().lat() > triggerBounds_.getNorthEast().lat() ||
        mapBounds.getSouthWest().lat() < triggerBounds_.getSouthWest().lat() ||
        mapBounds.getSouthWest().lng() < triggerBounds_.getSouthWest().lng() ||
        mapBounds.getNorthEast().lng() > triggerBounds_.getNorthEast().lng()) {
        return true;
      } else {
        return false;
      }
    };

    MarkerClusterer.prototype.clearMarkers = function() {
      for (var i = 0; i < this.clusters_.length; ++i) {
        if (typeof this.clusters_[i] !== "undefined" && this.clusters_[i] !== null) {
          this.clusters_[i].clearMarkers();
        }
      }
      this.clusters_ = [];
      google.maps.event.removeListener(this.mcfn_);
    };
    
    MarkerClusterer.prototype.addMarker = function(idx) {
      var projection = projectionOverlay_.getProjection();
      var marker = this.markers_[idx];
      var pos = projection.fromLatLngToDivPixel(new google.maps.LatLng(marker.lat_, marker.lng_));
      
      // No cluster contain the marker, create a new cluster.
      var cluster = new Cluster(this, this.markerType_, this.clusterDetailsCvs_, this.clusterDetailsGrd_, this.markerInfoCvs_);
      cluster.addMarker(marker);
      
      var centerPt;
      for (var i = idx+1; i < this.markers_.length; i++) {
        marker = this.markers_[i];
        centerPt = projection.fromLatLngToDivPixel(new google.maps.LatLng(marker.lat_, marker.lng_));
      
        // Found a cluster which contains the marker.
        if (pos.x >= centerPt.x - this.gridSize_ && pos.x <= centerPt.x + this.gridSize_ &&
            pos.y >= centerPt.y - this.gridSize_ && pos.y <= centerPt.y + this.gridSize_) {
          cluster.addMarker(marker);
          this.markers_.splice(i, 1);
          i--;
        } else if (pos.y > centerPt.y + this.gridSize_) {
          break;
        }
      }
      
      // Add this cluster both in clusters provided and clusters_
      this.clusters_.push(cluster);
    };
    
    MarkerClusterer.prototype.redraw = function() {
      var clusters = this.getClustersInViewport();
      for (var i = 0; i < clusters.length; ++i) {
        clusters[i].redraw();
      }
      clusters = this.getClustersOutViewport();
      for (var j = 0; j < clusters.length; ++j) {
        if (clusters[j].clusterIcon_ !== null) {
          clusters[j].clusterIcon_.hide();
        }
      }
    };
    
    MarkerClusterer.prototype.getClustersInViewport = function() {
      var clusters = [];
      for (var i = 0; i < this.clusters_.length; i++) {
        if (this.clusters_[i].isInBounds()) {
          clusters.push(this.clusters_[i]);
        }
      }
      return clusters;
    };
    
    MarkerClusterer.prototype.getClustersOutViewport = function() {
      var clusters = [];
      for (var i = 0; i < this.clusters_.length; i ++) {
        if (!this.clusters_[i].isInBounds()) {
          clusters.push(this.clusters_[i]);
        }
      }
      return clusters;
    };
    
    MarkerClusterer.prototype.getMap = function() {
      return this.map_;
    };
    
    MarkerClusterer.prototype.resetViewport = function() {
      var newZoom = this.map_.getZoom();
      if (this.curZoom_ !== newZoom && (this.curZoom_ < this.maxZoomCluster_ || newZoom < this.maxZoomCluster_)) {
        if (newZoom >= this.maxZoomCluster_ || this.markerType_ == _constMarkerTypeZIP3 || this.markerType_ == _constMarkerTypeState) {
          this.gridSize_ = parseInt(0);
        } else {
          this.gridSize_ = parseInt(60);
        }
          
        var clustersLen = this.clusters_.length;
        var mks;
        for (var i = 0; i < clustersLen; ++i) {
          mks = this.clusters_[i].getMarkers();
          for (var j = 0; j < mks.length; ++j) {
            this.markers_.push(mks[j]);
          }
          this.clusters_[i].clearMarkers();
        }
        this.clusters_ = [];
      }
      this.curZoom_ = newZoom;
      
      if (this.curZoom_ >= this.minZoomLvlShow_ && this.curZoom_ <= this.maxZoomLvlShow_) {
        this.addMarkers();
        this.redraw();
      }
    };

    MarkerClusterer.prototype.addMarkers = function() {
      this.markers_.sort(function(a,b){return a.lat_ - b.lat_});
      for (var i = 0; i < this.markers_.length; i++) {
        this.addMarker(i);
      }
      this.markers_ = [];
      this.redraw();
    };
    
