﻿/**
 * Contains all logic necessary for pushpin clustering
 */
function ClusterManager()
{   
    //attributes
    var mapStyle = null;
    var mapWidth = null;
    var mapHeight = null;
    var gridCells = new Array();
    var that = this;
    var emptyLatLong = new VELatLong(0,0).toString();
    var lastClusteredShapeType = ""; 
    
    //public properties
    this.PoiPresent = false;
    this.ClusterLayer = new VEShapeLayer();
    this.ClusterLayersArray = new Array();
    this.ShapeArrays = new Array();
    this.ShapeTypes = new Array();
    this.MapInstance = null;
    
    /**
     * Sets MapInstance local variable to passed in VE map instance
     *
     * @param mapInstance - Current VE map instance
     */
    this.Initialize = function(mapInstance)
    {
        this.MapInstance = mapInstance;
    }

    /**
     * Redraws the clustering shape layer when the map or pin data changes
     *
     * @param e - Event arguments when used as a callback (optional)
     */
    this.Update = function(e)
    {        
        if(!this.PoiPresent)
        {
            return true;
        }

        var currentStyle = mapManager.Map.GetMapStyle();
        if(mapStyle != currentStyle)
        {
            mapStyle = currentStyle;
            return true;
        }
        
        //if the map has resized then rebuild the grid
        if(mapWidth != this.MapInstance.GetWidth() || mapHeight != this.MapInstance.GetHeight())
        {
            GenerateGrid();
        }
        
        this.ClusterShapes();
        
        if(this.ShapeArrays.length == 0)
        {
            this.PoiPresent = false;
        }
        
        HideLoading();
        return true;
    }
    
    /**
     * Generates the contents of the clustered layer using VEShapeLayer SetClusteringConfiguration method
     *
     * @param type - type of shape to be culstered
     */
    this.ClusterShapes = function(type)
    {        
        // dettach the cluster shape layer to the map
        this.MapInstance.DeleteShapeLayer(this.ClusterLayer);
        //this.DeleteClusterLayer(type);
        
        // set cluster options
        var clusterOptions = new VEClusteringOptions();
        clusterOptions.Icon = new VECustomIconSpecification();
        clusterOptions.Icon.CustomHTML = '<div class="clusterPin"><div class="clusterCount"></div></div>';
    
        // create new cluster shape layer
        this.ClusterLayer.DeleteAllShapes();
        this.ClusterLayer = new VEShapeLayer();
        
        var newClusterLayer = new VEShapeLayer();
        //newClusterLayer.SetClusteringConfiguration(VEClusteringType.Grid,clusterOptions);
		//custom clustering algorithm:
		newClusterLayer.SetClusteringConfiguration(customCluster, clusterOptions);
		//to benchmark in all browsers:
		//newClusterLayer.SetClusteringConfiguration (clusterBenchmark, clusterOptions);
    
        // populate cluster layer with poi shapes
        var shapes;
        for(var i=0; i<this.ShapeArrays.length; i++){
            shapes = new Array();
            shapes = this.ShapeArrays[i];            
            // add each shape of each type to cluster layer
            for(var j=1; j<shapes.length; j++){            
                // shapes[0] is the type of shape
                newClusterLayer.AddShape(shapes[j],shapes[0]);
            }
        }
        
        this.ClusterLayer = newClusterLayer;
        
        // attach the cluster shape layer to the map
        this.MapInstance.AddShapeLayer(this.ClusterLayer);
        
        HideLoading();        
    }
    
   /**
    * Deletes the VEShapeLayer of type  from this.MapInstance
    *
    * @param type - type of shape layer to be deleted
    */
    this.DeleteClusterLayer = function(type){    
        this.MapInstance.DeleteShapeLayer(this.ClusterLayer);
    }
    
   /**
    * Retrieves the VEShapeLayer of type type
    *
    * @param type - type of shape layer to be returned
    */
    this.GetClusterLayer = function(type){
        var clusterLayer = new VEShapeLayer();
        for(var i=0; i<this.ClusterLayers.length; i++){
            return clusterLayer[i];
        }
    }
    
    /**
     *  Callback function that accepts a VEShapeLayer Class object and returns VEClusterSpecification Class objects array
     */     
    function GetClusteredShapes(poiLayer)
    {  
        var clusterShapes = new Array();
        clusterShapes = poiLayer.GetClusteredShapes(VEClusteringType.Grid);

        return clusterShapes;
    }
    
    /**
     * Determines whether or not a shape has the necessary information for clustering
     */
    function ValidateShape(shape)
    {
        var result = true;
        
        //suppress empty shapes, those without lat/longs or those at 0,0
        if(shape == null || shape.Primitives.length < 1 || shape.GetPoints()[0].toString() == emptyLatLong)
        {
            result = false;
        }
        
        return result;
    }
    
    /**
     * Sets all the shapes to be displayed in the cluster layer
     *
     * @param shapes - Array of VE shapes to display
     */
    this.SetShapes = function(shapes)
    {
        this.ClearShapes();
        this.ShapeArrays = new Array();
        
        for(var i = 0; i < shapes.length; i++)
        {
            if(ValidateShape(shapes[i]))
            {
                this.ShapeArrays[0].push(shapes[i]);
            }
        }
    }

    this.ClusterOff = function(type)
    {
        var clusterArray = new Array();
        // loop through all clustered layers
        for (var i = 0; i < this.ClusterLayersArray.length; i++) {
            clusterArray = this.ClusterLayersArray[i];
           
            // Change config for current  cluster layer if it is not of type
            clusterArray[1].SetClusteringConfiguration(VEClusteringType.None);
        }    
    }

    this.ClusterOn = function(type)
    {
        // set options for new cluster layer
        var clusterOptions = new VEClusteringOptions();
        // set pushpin icons
        clusterOptions.Icon = new VECustomIconSpecification();
        clusterOptions.Icon.CustomHTML = '<div class="clusterPin' + type + '"></div>'; 
        this.lastClusteredShapeType = type;
        // set callback function for when clustering changes
        clusterOptions.Callback = this.FormatClusterShapes;

        var clusterArray = new Array();
        // loop through all clustered layers
        for (var i = 0; i < this.ClusterLayersArray.length; i++) {
            clusterArray = this.ClusterLayersArray[i];
           
            // Change config for current  cluster layer if it is not of type
            clusterArray[1].SetClusteringConfiguration(VEClusteringType.Grid, clusterOptions);
        }    
    }
    
    /**
     * Adds a set of shapes to the cluster layer
     *
     * @param shapes - Array of VE shapes to add
     * @param   type - type of pushpin for which shape is being added
     */
    this.AddShapes = function(shapes, type) {
        if (shapes == null || shapes.length == 0 || this.typeDisplayed(type)) {
            return;
        }

        var shapeArray = new Array();
        shapeArray.push(type);

        // set options for new cluster layer
        var clusterOptions = new VEClusteringOptions();
        // set pushpin icons
        clusterOptions.Icon = new VECustomIconSpecification();
        clusterOptions.Icon.CustomHTML = '<div class="clusterPin' + type + '"></div>'; 
        this.lastClusteredShapeType = type;
        // set callback function for when clustering changes
        clusterOptions.Callback = this.FormatClusterShapes;

        // create new cluster layer
        var clusterLayer = new VEShapeLayer();
        //clusterLayer.SetClusteringConfiguration(VEClusteringType.Grid, clusterOptions);

/*        if(mapManager.Map.GetZoomLevel() >= clusterOnOffZoomThreshold) {
            clusterLayer.SetClusteringConfiguration(VEClusteringType.None);
        } else {
            clusterLayer.SetClusteringConfiguration(VEClusteringType.Grid, clusterOptions);
		}
*/
		//custom clustering algorithm:
		clusterLayer.SetClusteringConfiguration(customCluster, clusterOptions);
		//to benchmark in all browsers:
		//clusterLayer.SetClusteringConfiguration (clusterBenchmark, clusterOptions);

        for (var i = 0; i < shapes.length; i++) {
            if (ValidateShape(shapes[i])) {
                shapeArray.push(shapes[i]);
                // add each shape to new cluster layer
                clusterLayer.AddShape(shapes[i]);
                
                // Now that shape added it has a ShapeID and now we can build up lookup hash between g_sResult and ShapeID
	            if(shapes[i]._customIcon != undefined) {
					var curCustomIconClass = shapes[i]._customIcon.match(/class=(["'])?(\w+)\1/)[2];
					if (curCustomIconClass.match(/^pin_wf|pin_v_wf|pin_nonad|pin_v_nonad|mapIcon/i) != null) {
						var keyInSpan = shapes[i].GetDescription();
						
						var imKey = InfoBoxManager.StripSpan(keyInSpan);
						if (InfoBoxManager.IsFormattedKey(imKey)) {
							var fields=imKey.split('||');
							var type = fields[0];
							listingIndex = parseInt(fields[1]);
	                    }
					}
				}
                YPSearchManager.updateListingShapeMap(listingIndex, shapes[i].GetID());
            }
        }
        this.ShapeArrays.push(shapeArray);
        this.ShapeTypes.push(type);

        // push type and new cluster layer onto this.ClusterLayersArray
        clusterLayerArray = new Array();
        clusterLayerArray.push(type);
        clusterLayerArray.push(clusterLayer);
        this.ClusterLayersArray.push(clusterLayerArray);

        // add new cluster layer to map
        this.MapInstance.AddShapeLayer(clusterLayer);
if(debug)console.log("In AddShapes: Done with clustering, just called AddShapeLayer(clusterLayer)");
        HideLoading();
    }
    
    
    /**
     * Formats cluster icons
     *
     * @param clusters - array of VEClusterSpecification Class objects representing clusters
     */
    this.FormatClusterShapes = function(clusters) {
        var clusterShape;
        var clusterShapeSpec;
        var clusterShapeSpecHTML;
        var clusterLatLon;
        var clusterArray = new Array();
        var zoomLevel = mapManager.Map.GetZoomLevel();
        var changeClusterIcon = false;

        if(debug)console.log("In FormatClusterShapes: clusters.length: ", clusters.length);

        // for each cluster
        for (var i = 0; i < clusters.length; i++) {
            // array of VEShapes belonging to cluster
            clusterArray = clusters[i].Shapes;
            // VEShape displayed for cluster
            clusterShape = clusters[i].GetClusterShape();
            // VELatLong of VEShape displayed for cluster
            clusterLatLon = clusters[i].LatLong;

			changeClusterIcon = false;
            // SET CLUSTER PUSHPIN DESCRIPTION            
            // concat description of each VEShape belonging to cluster as description for that cluster
            var descriptionArray = new Array();
            var descriptionKey = clusterArray[0].GetDescription();
            descriptionKey = InfoBoxManager.StripSpan(descriptionKey);        
            var descriptionAddressArray = new Array();
            var descriptionAddress;
            var descriptionImage; // = InfoBoxManager.GetImage(descriptionKey);
            var descriptionDirections;
            var description;
            var clusteredLatLonsArray = new Array();

//            if (zoomLevel != 19) {
                for (var j = 0; j < clusterArray.length; j++) {
                    clusteredLatLonsArray.push('new VELatLong(' + clusterArray[j].GetIconAnchor() + ')');
                }
                var clusteredLatLonsArrayString = "new Array(" + clusteredLatLonsArray.join(',') + ")";
                // for each VEShape belonging to cluster
                for (var j = 0; j < clusterArray.length; j++) {
                    // get the description lookup key
                    descriptionKey = clusterArray[j].GetDescription();
                    descriptionKey = InfoBoxManager.StripSpan(descriptionKey);
            		descriptionImage = InfoBoxManager.GetImage(descriptionKey);
            		if(descriptionImage.match(/Visited/i) != null)
            			changeClusterIcon = true;
                    // lookup the address for this description
                    descriptionAddress = InfoBoxManager.GetAddress(descriptionKey);
                    // remove 'br' tags
                    descriptionAddressArray = descriptionAddress.split('<br>');
                    descriptionAddress = descriptionAddressArray[0];
                    shapeID = clusterArray[j].GetID();
                    descriptionArray.push(shapeID + '\')">' + descriptionImage + '</td><td style="padding-left:7px;"><span>' + InfoBoxManager.GetName(descriptionKey) + '</span> - ' + descriptionAddress + '<br /><span>' + InfoBoxManager.GetPhone(descriptionKey));
                    //This DIV approach has IE issue with overflow: descriptionArray.push(shapeID + '\')">' + descriptionImage + '</div><div class="clusterCol2"><div class="clusterCol2Data">' + InfoBoxManager.GetName(descriptionKey) + ' ' + descriptionAddress);
                }
                // cluster description is concatenation of descriptions of all belonging locations
                // clusterShape.SetDescription('<div class="clusterDescription"><div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td style="cursor:pointer;" onclick="mapManager.Map.SetMapView(' + (descriptionArray.join('</span></td></tr></table></div><br/><div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td style="cursor:pointer;" onclick="mapManager.Map.SetMapView(')) + '</span></td></tr></table></div></div>');
				//clusterShape.SetDescription('<div class="clusterDescription"><div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td class="clusterCol1" style="cursor:pointer;" onclick="YPSearchManager.GetMPData(\'' + (descriptionArray.join('</span></td></tr></table></div><br/><div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td class="clusterCol1" style="cursor:pointer;" onclick="YPSearchManager.GetMPData(\'')) + '</span></td></tr></table></div></div>');
                //var closeLine = '<div class="ypgMMBClose"><a class="ypSpLayerFunctions ypSmClose" href="#" onclick="javascript:activeEro.Hide();return false;" rel="nofollow">&nbsp;</a></div>';
				clusterShape.SetDescription('<div class="clusterDescription"><div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td class="clusterCol1" style="cursor:pointer;" onclick="YPSearchManager.GetMPData(\'' + (descriptionArray.join('</span></td></tr></table></div><div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td class="clusterCol1" style="cursor:pointer;" onclick="YPSearchManager.GetMPData(\'')) + '</span></td></tr></table></div></div>');

				//This DIV approach has IE issue with overflow: clusterShape.SetDescription('<div class="clusterDescription"> <div class="clusterRow" style="font-weight: bold; font: 12px/14px Arial;"><div class="clusterCol1" style="cursor:pointer;" onclick="YPSearchManager.GetMPData(\'' + (descriptionArray.join('</div></div><div class="clearboth"></div></div>  <div class="clusterRow" style="font-weight: bold; font: 12px/14px Arial;"><div class="clusterCol1" style="cursor:pointer;" onclick="YPSearchManager.GetMPData(\'')) + '</div></div><div class="clearboth"></div></div>');
                if(changeClusterIcon)
                    clusterShape.SetCustomIcon('<div class="clusterPin100_v"></div>'); 
                
//            } else {
//                for (var j = 1; j < clusterArray.length; j++) {
//                    descriptionArray.push(clusterArray[j].GetDescription().split('||')[1]);
//                }
//                clusterShape.SetDescription(clusterArray[0].GetDescription()+'||'+(descriptionArray.join('||')));
//            }
            //Stop clusterShape.SetTitle('cluster');
            clusterShape.SetTitle('<div class="clusterPin100ClusterDialog clusterDescriptionTitle">' + resourceClusterHeader + '</div>');
        }
        if(debug)console.log("In FormatClusterShapes: ... finished");
    }

    /**
    * Generate the description for popup
    *
    * @param key - unique ids of clustered pois (\POI\d+||(\d+)+\)
    *
    * @return the expanded description
    */
    this.GenerateDescription = function(key) {
        var divHeight = "170px";
        var topMargin = "8px";

        var keys = key.split('||');

        // concat description of each VEShape belonging to cluster as description for that cluster
        var descriptionArray = new Array();
        var descriptionKey = (keys[0] + '||' + keys[1]);
        var descriptionAddressArray = new Array();
        var descriptionAddress;
        var descriptionImage = InfoBoxManager.GetImage(descriptionKey);
        // for each VEShape belonging to cluster
        for (var j = 0; j < keys.length - 1; j++) {
            //descriptionKey = clusterArray[j].GetDescription().split('</span>')[0].split('>')[1];
            // lookup the address for this description
            descriptionAddress = InfoBoxManager.GetAddress(descriptionKey);
            // remove 'br' tags
            descriptionAddressArray = descriptionAddress.split('<br>');
            descriptionAddress = descriptionAddressArray[0];

            descriptionArray.push('<div style="font-weight: bold; font: 12px/14px Arial;"><table><tr><td>' + descriptionImage + '</td><td style="padding-left:7px;"><span>' + InfoBoxManager.GetName(descriptionKey) + '</span><span>' + descriptionAddress + '</span></td></tr></table></div>');

            // get the next description lookup key
            descriptionKey = (keys[0] + '||' + keys[j + 1]);
        }

        key = descriptionKey;
        var poiImg = descriptionImage;

        var description = '<div style="height: ' + divHeight + ';margin-top:' + topMargin + ';">'   // Overall
                + '<div class="popupTop" style="overflow-y:auto; height:91%">'   // Top
                + '<div class="popupTopLeft">'   // Top Left
                + descriptionArray.join("<br/>")   // names and addresses
                + '</div>'   // top left div
                + '<div class="popupTopRight" style="width:53%">'
                + '<div id=\"AP_ACTION_' + key + '\" class="popupActionArea" style="top:10px;">'  // Action area                
                + '<div id=\"AP_EMPTY_' + key + '\" style="height:130px; width:100px;"></div>'    // Empty (Action)
                + '<div id=\"AP_TO_' + key + '\" style="height:130px;display:none;">'
                + '<div class="popupHeader"><strong>' + resourceDD + ':</strong></div><br/>' // To Here (Action)
                + resourceToHere + ':<br/>'
                + '<input type="text" id="p_toAddress' + key + '" style="width:158px;" onkeydown="ExecuteFindFromInput(\'p_imgTo' + (key) + '\', event);"/><br/>'
                + '<div><img id="p_imgTo' + key + '" src="' + images + 'GetDD.gif" class="popupLink" style="left:100px; float: right;" onclick="POIManager.GDDForPOIPopup(\'TO\', \'' + (key) + '\');" /></div>'
                + '</div>'
                + '<div id=\"AP_FROM_' + key + '\" style="height:130px;display:none;">'
                + '<div class="popupHeader"><strong>' + resourceDD + ': </strong></div><br/>'   // From Here (Action)
                + resourceFromHere + ':<br/>'
                + '<input type="text" id="p_fromAddress' + key + '" style="width:158px;" onkeydown="ExecuteFindFromInput(\'p_imgFrom' + (key) + '\', event);"/><br/>'
                + '<div><img id="p_imgFrom' + (key) + '" src="' + images + 'GetDD.gif" class="popupLink" style="left:100px; float: right;" onclick="POIManager.GDDForPOIPopup(\'FROM\', \'' + (key) + '\');"/></div>'
                + '</div>'
                + '<div id=\"AP_NEAR_' + key + '\" style="height:130px;display:none;"><div class="popupHeader"><strong>' + resourceWhatsNearby + ': </strong></div><br/>'  // What's near (Action)
                + '<div class="popupSubheader">' + resourceSearchForCategory + ':<br/></div>'
                + '<input type="text" id="p_nearby' + key + '" style="width:158px;" onkeydown="ExecuteFindFromInput(\'p_imgNear' + (key) + '\', event);"/><br/>'
                + '<div style="float:left; width:110px">' + resourceWhatsNearbyExample + '</div>'
                + '<div><img id="p_imgNear' + (key) + '" src="' + images + 'GetDD.gif" class="popupLink" style="margin-top:11px; left:100px; float: right;" onclick="POIManager.GNBForPOIPopup(\'' + (key) + '\');"/></div>'
                + '</div>'
                + '<div id=\"AP_MAP_' + key + '\" style="height:130px;display:none;">'    // Save to a Map (Action)
                + '<div id=\"AP_MAP_SAVE_' + key + '\" style="display:none;">'
                + '<div style="float:left;">'
                + '<div class="popupHeader" style="width:110px"><strong>' + resourceSaveToAMap + ': </strong></div><br />'
                + '<div style="width:110px">' + resourceSaveToAMapPrompt + '</div>'
                + '</div>'
                + '<div><img src="Images/' + poiImg + '" class="popupLink" style="margin-right:10px; margin-top:10px; float: right; border: 1px solid black;" /></div>'
                + '<div style="margin-top:10px;"><select id="mapSelect" style="width:158px;"></select><br/></div>'
                + '<div><img id="p_imgAdd' + (key) + '" src="' + images + 'doneButton.gif" class="popupLink" style="left:100px; float: right;" onclick="POIManager.SavePOIToCustomMap(' + (key) + ');"/></div>'
                + '</div>'
                + '<div id=\"AP_MAP_WARN_' + key + '\" style="float:left;display:none">'
                + '<div class="popupHeader" style="width:110px"><strong>' + resourceSaveToAMap + ': </strong></div><br />'
                + '<div style="width:180px">' + resourceLoginNeeded + '</div>'
                + '</div>'
                + '</div>'
                + '</div>'   //Action area
                + '</div>'   // top right
                + '</div>'   // top div                
                + '<div class="popupBottom" style="padding-top:5px">'    // Function bar
                + '<table><td><img src="images/drivingicon.gif" align="left" style="margin-right:3px;margin-left:3px"></img></td>'
                + '<td>' + resourceDD.toUpperCase()
                + '<div></div><span class="popupLink popupDrivingFunctionTo" onclick="YPSearchManager.ShowActionDiv(\'AP_\', \'' + (key) + '\', \'FROM\');"><img src="images/tohereicon.gif"><span>' + resourceToHere.toUpperCase() + '</span></img></span> | '
                + '<span class="popupLink popupDrivingFunctionFrom" onclick="YPSearchManager.ShowActionDiv(\'AP_\', \'' + (key) + '\', \'TO\');"><img src="images/fromhereicon.gif"><span>' + resourceFromHere.toUpperCase() + '</span></img></span>'
                + '</td>'  // DD
                + '<td style="width:60px;margin-right:3px;"><span class="popupLink" onclick="YPSearchManager.ShowActionDiv(\'AP_\', \'' + (key) + '\', \'NEAR\');"><img src="images/nearbyicon.gif" align="left" style="margin-right:3px"></img>'
                + '<span class="popupLink">' + resourceWhatsNearby.toUpperCase() + '</div></span>'
                + '</td>'
                + '<td style="margin-right:3px; width:70px;"><span class="popupLink" onclick="YPSearchManager.ShowActionDiv(\'AP_\', \'' + (key) + '\', \'MAP\');"><img src="images/savemapicon.gif" align="left" style="margin-right:3px;margin-left:3px"></img>'
                + resourceAddToAMap.toUpperCase() + '</span>'
                + '</td>'
                + '</tr></table>'   // Function bar
                + '</div>'   // Bottom div
                + '</div>';  // overall

        return description;
    }
    
    /**
     * Deletes an array shapes from the cluster layer
     *
     * @param type - type of shape to delete
     */
    this.DeleteShapes = function(type) {
        // if type shapes clustered on map
        if (this.typeDisplayed(type)) {
            var newShapeArrays = new Array();
            var shapeArray = new Array();
            for (var i = 0; i < this.ShapeArrays.length; i++) {
                shapeArray = this.ShapeArrays[i];
                if (shapeArray[0] != type) {
                    newShapeArrays.push(shapeArray);
                }
            }
            var newClusterLayersArray = new Array();
            var clusterArray = new Array();
            // loop through all clustered layers
            for (var i = 0; i < this.ClusterLayersArray.length; i++) {
                clusterArray = this.ClusterLayersArray[i];
                // keep current cluster layer if it is not of type
                if (clusterArray[0] != type) {
                    newClusterLayersArray.push(clusterArray);
                }
                // delete current cluster layer and remove it from map if it is of type
                else {
                    this.MapInstance.DeleteShapeLayer(clusterArray[1]);
                }
            }
            // type shapes now deleted
            this.ShapeArrays = newShapeArrays;
            this.ClusterLayersArray = newClusterLayersArray;
        }

        HideLoading();
    }
    
    /**
     * Checks that type is not a shape type already being clustered
     * @param type - shape type being checked
     *
     * returns true if type exists in this.ShapeTypes, false otherwise
     */
     this.typeDisplayed = function(type)
     {
        var shapeArray = new Array();
        // checking each shapeArray
        for(var i=0; i<this.ShapeArrays.length; i++){
            shapeArray = this.ShapeArrays[i];            
            // return true if shapeArray of type found
            if(shapeArray[0] == type){
                return true;
            }
        }
        return false;
     }
    
    /**
     * Removes all shapes from the cluster layer and those stored internally
     */
    this.DeleteAllShapes = function()
    {
        this.ClearShapes();
        this.ShapeArrays = new Array();
        this.PoiPresent = false;
    }
    
    /**
     * Clears out all the shapes from the cluster layer
     */
    this.ClearShapes = function()
    {
        var gridSize = gridCells.length;
        
        this.ClusterLayer.DeleteAllShapes();
        
        //clear out and restore the (now empty) grid
        gridCells = null;
        gridCells = new Array(gridSize)
    }
    
    /**
     * Generates the contents of the clustered layer
     */
    function GenerateLayer(shapes)
    {
        var zoomLevel = that.MapInstance.GetZoomLevel();
        var xCells = parseInt(Math.ceil(mapWidth / gridSize));
        
        that.ClearShapes();

        //iterate through all shapes and populate shape grid
        for(var i = 0; i < shapes.length; i++)
        {
            var shape = shapes[i];
            var latLong = shape.GetPoints()[0];
            var pixel = that.MapInstance.LatLongToPixel(latLong);

            //only display shapes in the map view
            if(pixel.x <= mapWidth && pixel.y <= mapWidth && pixel.x >= 0 && pixel.y >= 0)
            {
                //calculate the shape's grid location
                var gridX = Math.floor(pixel.x / gridSize);
                var gridY = Math.floor(pixel.y / gridSize);
                
                //determine the grid index of this shape
                var index = gridX + gridY * xCells;
                
                if(index < gridCells.length)
                {
                    if(gridCells[index] == null)
                    {
                        gridCells[index] = new Array();
                    }
                    
                    gridCells[index].push(shape);
                }
            }
        }
        
        //iterate through the shape grid determine which shapes are clustered, populate shape layer
        for(var i = 0; i < gridCells.length; i++)
        {
            var cell = gridCells[i];
            var shape = null;
            
            //multiple points - clustered pin
            if(cell != null && cell.length > 1 && zoomLevel < minZoomLimit)
            {
                //extract the lat/longs for the pins in this pin
                var latLongs = '';
                
                //append to the pipe-delimietd list of lat/longs for clustered pins for later use
                for(var shapeIndex = 0; shapeIndex < cell.length; shapeIndex++)
                {
                    latLongs += cell[shapeIndex].GetPoints()[0].toString().replace(' ', '');
                    latLongs += (shapeIndex <= cell.length - 2) ? '|' : '';
                }
                
                shape = new VEShape(VEShapeType.Pushpin, cell[0].GetPoints()[0]);

                shape.SetTitle('cluster');
                shape.SetDescription(latLongs);
                shape.SetCustomIcon('<div class="clusterPin"><div class="clusterCount">' + cell.length + '</div></div>');
                                             
                this.ClusterLayer.AddShape(shape);
            }
            //multiple unclustered points or single pin
            else
            {
                if(cell != null && cell.length > 0)
                {
                    for(var shapeCount = 0; shapeCount < cell.length; shapeCount++)
                    {
                        this.ClusterLayer.AddShape(cell[shapeCount]);
                    }
                }
            }
        }
    }
    
    /**
     * Performs the necessary calculations to generate the shape/cluster grid
     */
    function GenerateGrid()
    {
        mapWidth = that.MapInstance.GetWidth();
        mapHeight = that.MapInstance.GetHeight();
        var xCells = parseInt(Math.ceil(mapWidth / gridSize));
        var yCells = parseInt(Math.ceil(mapHeight / gridSize));
        
        gridCells = new Array(xCells * yCells);
    }

    /**
     * Serializes a POI VE pushpin to a string
     */
    function ShapeToString(shape)
    {
        var points = shape.GetPoints()[0];
        var result = points.Latitude + '|' + points.Longitude + '|' + shape.GetDescription() + '|' + shape.GetTitle + '|' + shape.GetCustomIcon();
        
        return result;
    }
}

/*
* custom clustering algorithm
* msdn documentation:
* http://msdn.microsoft.com/en-us/library/cc966716.aspx
*/
//benchmarking tools
/*
function clusterBenchmark(layer) {
	//note: may add overhead
	var _id = consoleLog("starting cluster algorithm");
	var tmp = customCluster(layer);
	consoleLog("done cluster algorithm",_id);
	return tmp;
}

var _benchmark = new Array();
var _consoleLastId = 0;
function consoleLog(message, id) {
	var now = new Date().getTime();
	if (id == undefined) {
		console.log(message);
		_consoleLastId++;
		_benchmark[_consoleLastId] = now;
		return _consoleLastId;
	} else {
		console.log((now - _benchmark[id]) + "ms, " + message);
	}
}
*/
//end of benchmarking tools
function deleteFromDist(distArray, allShapes, tmpIndex) {
	var tmpx = Math.floor(allShapes[tmpIndex].x / 10);
	var tmpy = Math.floor(allShapes[tmpIndex].y / 10);
	
	for (var j = distArray[tmpx][tmpy].length - 1; j >= 0; j--) {
		if (distArray[tmpx][tmpy][j] == tmpIndex) {
			distArray[tmpx][tmpy].splice(j,1);
			j = -1;
		}
	}
}

//var clusterTurnOff = false;
var totalx = 0;
var totaly = 0;
function customCluster(layer) {
	var pins = new Array();
//	if (clusterTurnOff)
//		return pins;
	var total = layer.GetShapeCount();
	var allShapes = new Array();
	
	var allShapesIndex = 0;

	if(debug)console.log("In customCluster: total Shape in layer: ", total);
		
	totalx = Math.ceil(mapManager.Map.GetWidth() / 10) + 1;
	totaly = Math.ceil(mapManager.Map.GetHeight() / 10) + 1;
	var distArray = new Array(totalx);
	
	for (var i = totalx - 1; i >= 0; i--) {
		distArray[i] = new Array(totaly);
		for (var j = totaly - 1; j >= 0; j--)
			distArray[i][j] = new Array();
	}
	
	//var _benchXY = consoleLog("getting pixel co-ords");
	//var returnAfter = false;
	for (var i = 0; i < total; i++) {
/* Original code added all pins from layer to allShapes array
		allShapes[i] = layer.GetShapeByIndex(i);
		var tmp = mapManager.Map.LatLongToPixel(allShapes[i].GetPoints()[0]);
		allShapes[i].x = tmp.x;
		allShapes[i].y = tmp.y;
*/
		var aShape = layer.GetShapeByIndex(i)
		var tmp = mapManager.Map.LatLongToPixel(aShape.GetPoints()[0]);
		
		var tmpArrayX = Math.floor(tmp.x / 10);
		var tmpArrayY = Math.floor(tmp.y / 10);
		if ((tmpArrayX < 0) || (tmpArrayX >= totalx) || (tmpArrayY < 0) || (tmpArrayY >= totaly)) {
/*
			var tmpPin = new VEClusterSpecification();
			tmpPin.LatLong = new VELatLong(0,0);
			tmpPin.Shapes = new Array();
			while (total--) {
				tmpPin.Shapes[total] = layer.GetShapeByIndex(total);
			}
			pins.push(tmpPin);
*/
			//if(debug)console.log("In customCluster:1st for loop: Discard pin from distArray - grid for visible map: tmpArrayX:%s totalx:%s  tmpArrayY:%s totaly:%s ", tmpArrayX, totalx, tmpArrayY, totaly);
			
			//return pins;
		} else {
			aShape.x = tmp.x;
			aShape.y = tmp.y;
			allShapes.push(aShape);
		
			//distArray[tmpArrayX][tmpArrayY].push(i);
			distArray[tmpArrayX][tmpArrayY].push(allShapesIndex);
			allShapesIndex++;
		}
		//distArray represents a grid
	}
	//consoleLog("done pixel co-ords",_benchXY);
    total = allShapesIndex;
	if(debug)console.log("In customCluster: total after building grid is: ", total);
	if(debug)console.log("In customCluster: allShapes.length is: ", allShapes.length);
	
	/*if (returnAfter) {
		var tmpPin = new VEClusterSpecification();
		tmpPin.LatLong = new VELatLong(0,0);
		tmpPin.Shapes = allShapes;
		pins.push(tmpPin);
		return pins;
	}*/

    var zoomLevel = mapManager.Map.GetZoomLevel();
	var zoomLevelClusterCompareDist = zoomLevelClusterCompareDistMap[zoomLevel];
	if(debug)console.log("In customCluster: zoomLevel: %s  zoomLevelClusterCompareDist: %s", zoomLevel, zoomLevelClusterCompareDist);
	
	var maxValue = 0;
	var maxShapes = new Array();
	var maxIndex = -1;
	var livingIndicesToAllShapes = new Array(total);
	var clusterValues = new Array(total);
	var tmpResult;
	for (var i = allShapes.length - 1; i >= 0; i--) {
		livingIndicesToAllShapes[i] = i;
		tmpResult = singlePinCluster(i, distArray, allShapes, zoomLevelClusterCompareDist);
		//somehow make it not add pins with tmp value of 1
		clusterValues[i] = [tmpResult[0], i, tmpResult[1]];
	}

	if(debug)console.log("In customCluster: clusterValues: ", clusterValues.length);

	while (clusterValues.length > 0) {
		clusterValues.sort(clusterValueSort);
		maxValue = -1;
		maxIndex = 0;
		var i = 0;
		var go = true;
		while (go) {
			if (i >= clusterValues.length)
				go = false;
			else {
				//var tmp = singlePinValue(clusterValues[i][1], distArray, allShapes);
				var tmp = singlePinCache(clusterValues[i][2], livingIndicesToAllShapes);
				if (tmp == 1) {
					clusterValues.splice(i, 1);
				} else {
					if (tmp > maxValue) {
						maxValue = tmp;
						maxIndex = i;
					}
					if (tmp == clusterValues[i][0]) {
						go = false;
					}
					clusterValues[i][0] = tmp;
					i++;
				}
			}
		}
		
		if (maxValue <= 1)
			return pins;
		
		//maxShapes = singlePinCluster(clusterValues[maxIndex][1], distArray, allShapes)[1];
		//maxShapes = singlePinCacheCluster(clusterValues[maxIndex][2], livingIndicesToAllShapes);
		maxShapes = clusterValues[maxIndex][2];
		
		var tmp = new VEClusterSpecification();
		tmp.LatLong = allShapes[clusterValues[maxIndex][1]].GetPoints()[0];
		
		//livingIndicesToAllShapes maps allShapes -> clusterValues
		//toDelete subset of clusterValues
		
		livingIndicesToAllShapes = [];
		var i = clusterValues.length;
		while (i--) {
			livingIndicesToAllShapes[clusterValues[i][1]] = i;
		}
		
		var toDelete = new Array(maxShapes.length);
		i = maxShapes.length;
		while (i--) {
			toDelete[i] = livingIndicesToAllShapes[maxShapes[i]];
		}
		
		toDelete.sort(numSort);
		
		i = toDelete.length;
		while (i--) {
			var tmpIndex = clusterValues[toDelete[i]][1];
			deleteFromDist(distArray, allShapes, tmpIndex);
			
			maxShapes[i] = allShapes[tmpIndex];
			clusterValues.splice(toDelete[i], 1);
			livingIndicesToAllShapes[tmpIndex] = undefined;
		}
		
		tmp.Shapes = maxShapes;
		
		pins.push(tmp);
	}
if(debug)console.log("In customCluster: End: About to return pins: ", pins.length);
	return pins;
}

function clusterValueSort(a,b) { return b[0] - a[0]; }

function numSort(a,b) { return a - b; };

var clusterCompareDist = 1600; //square the distance so Math.sqrt is not necessary in the calculations
function singlePinCluster(index, distArray, allShapes, zoomLevelClusterCompareDist) {
	//heart of the algorithm
	
	//we will look 50 pixels away (always 10 more than distance we wish to compare)
	//so dist = 5 (5 * 10)
	var arrayX = Math.floor(allShapes[index].x / 10);
	var arrayY = Math.floor(allShapes[index].y / 10);
	//(arrayX > (dist - 1))
	var minX = (arrayX > 4) ? arrayX - 4 : 0;
	var minY = (arrayY > 4) ? arrayY - 4 : 0;
	var maxX = (arrayX < (totalx - 4)) ? arrayX + 4 : totalx - 1;
	var maxY = (arrayY < (totaly - 4)) ? arrayY + 4 : totaly - 1;
	var shapes = new Array();
	var value = 0;
	var i = minX - 1;
	while (i++ < maxX) {
//	for (var i = minX; i <= maxX; i++) {
		var j = minY - 1;
		while (j++ < maxY) {
//		for (var j = minY; j <= maxY; j++) {
//##			if (((arrayX - i) * (arrayX - i)) + ((arrayY - j) * (arrayY - j)) <= 8) {
//##				value += distArray[i][j].length;
//##				Array.prototype.push.apply(shapes, distArray[i][j]);
//##			} else {
				for (var k = distArray[i][j].length - 1; k >= 0; k--) {
					if (distBetween(allShapes[index], allShapes[distArray[i][j][k]]) <= zoomLevelClusterCompareDist) {
					//^^ must. increase. speed.
					//if (distBetween(allShapes[index], allShapes[distArray[i][j][k]]) <= clusterCompareDist) {
						value++;
						shapes.push(distArray[i][j][k]);
					}
				}
//##			}
		}
	}
	return [value, shapes];
}

function singlePinCache(cache, livingIndices) {
	var value = 0;
	for (var i = cache.length - 1; i >= 0; i--) {
		if (livingIndices[cache[i]] == undefined) {
			cache.splice(i, 1);
		} else {
			value++;
		}
	}
	return value;
}

function singlePinCacheCluster(cache, livingIndices) {
	var shapes = [];
	for (var i = cache.length - 1; i >= 0; i--)  {
		if (livingIndices[cache[i]] != undefined)
			shapes.push(cache[i]);
	}
	return shapes;
}

function singlePinValue(index, distArray, allShapes) {
	//singlePinCluster but only returns the value, will be faster in IE because "new Array()" has been removed
	var arrayX = Math.floor(allShapes[index].x / 10);
	var arrayY = Math.floor(allShapes[index].y / 10);
	var minX = (arrayX > 4) ? arrayX - 4 : 0;
	var minY = (arrayY > 4) ? arrayY - 4 : 0;
	var maxX = (arrayX < (distArray.length - 4)) ? arrayX + 4 : distArray.length - 1;
	var maxY = (arrayY < (distArray[arrayX].length - 4)) ? arrayY + 4 : distArray[arrayX].length - 1;
	var value = 0;
	for (var i = minX; i <= maxX; i++) {
		for (var j = minY; j <= maxY; j++) {
			for (var k = distArray[i][j].length - 1; k >= 0; k--) {
				if (distBetween(allShapes[index], allShapes[distArray[i][j][k]]) <= clusterCompareDist)
					value++;
			}
		}
	}
	return value;
}

function distBetween(shape1, shape2) {
	//does not return true distance, as it does not do the square root (optimization)
	var dx = shape1.x - shape2.x;
	var dy = shape1.y - shape2.y;
	return Math.floor((dx * dx) + (dy * dy));
}
