Expanding Clustered Pins in Virtual Earth 6

Article by Robert Mcgrath

Expanding Clustered Pins in Virtual Earth 6

So you have read John O'Brien’s articles on Clustering in Virtual Earth. You’ve even implemented Clustering Virtual Earth with MS AJAX and C#. What more could you possibly do now?

How about expanding a clustered pin to show the individual pins that the clustered pin represents? This article explains one method of doing just that.

For this example, clicking on this clustered pin:

a clustered pin
Figure 1.

Expands it to show the individual pins:

an expanded pin
Figure 2.

That is the concept and here is the basic theory behind the implementation used in this example:

To expand the clustered pin:

  1. Capture the onclick mouse event
  2. Verify that a clustered pin was clicked
  3. Create a new ShapeLayer to house the individual pins
  4. Create (and add to the new ShapeLayer) a new pushpin for each pin that the Clustered pin represents

Then to collapse the expaned pin:

  1. Capture the onclick mouse event
  2. Verify that an expaned pin was clicked
  3. Delete the ShapeLayer that was created when the clustered pin was expanded

With a few missing details, data structures and omitted caveats, that is the algorithm in a nutshell.

With the algorithm outlined only the coding remains.

One of the missing details is data structures. We need a way to track whether pins are clustered or not, the individual pins that a clustered pin represents, if the clustered pin is expanded or not and anything else that you might find pertinent to you implementation. It looks like a bit of information we need to keep tabs on, so how can we do that efficiently?

One way to store this information is to use the JavaScript prototype object to create custom properties for the VEShape object which represents the pushpins. This example uses the following JavaScript code to store the necessary information that we need.
(Normally this code would be included in an initialization function where the VEMap is created such as an onload() function)


    VEShape.prototype.IsClustered = false;  // is a clustered pin flag
    VEShape.prototype.IsExpanded = false;   // is an expanded pin flag
    VEShape.prototype.Pins = null;          // array containing clustered pins
    VEShape.prototype.ExpandedLayer = null; // a Shape layer object containing the expanded pins
    

Capturing the onclick mouse event is implemented with the following JavaScript code:
(Normally this info would be included in an initialization function where the VEMap is created such as an onload() function)


    map.AttachEvent("onclick", MouseClickHandler);

Now to add a clustered pushpin and set its custom properties
(This could be an AJAX call, but for this example the pins are hard coded)


    function AddPins()
    {
        var pin = new VEShape(VEShapeType.Pushpin, new VELatLong(25.629 , -80.3480));
        var pins = new Array("Pin  #1"," Pin  #2"," Pin  #3"," Pin  #5"," Pin  #5");
        pin.Pins = pins;
        pin.SetCustomIcon("<img src='cluster.png' />");
        pin.SetTitle("Clusterd Pin");
        pin.SetDescription("This Clusterd Pin Represents " + pins.length + 
                           " Pins <br /><br />Click The PinTo Expand");
        pin. IsClustered = true;
        pin.IsExpanded = false;
        map.AddShape(pin); 
    }
    

Now that the data structure and mouse events have been taken care of we just need to implement the handler for the onclick mouse event as well as the expand/collapse functions. With the custom pushpin properties defined, the implementation is fairly straight forward.

In the MouseClickHandler, we check if a clustered pushpin was clicked. If the clustered pin is expanded then collapse it back to its original state, otherwise expand the clustered pin to show the individual pins.


    function MouseClickHandler(e)
    {
        if(e.elementID != null)
        {
            var pin = map.GetShapeByID(e.elementID);
            
            // Verify that a clustered Pushpin was clicked
            if(pin.GetType() == "Point" && pin. IsClustered)
            {
                // if pin is expanded - collapse it
                if(pin.IsExpanded)
                {
                    CollapsePin(pin);
         	        return;
                }
             	
                // otherwise expand the pin
                ExpandPin(pin);
            }
        }
    }
    

To expand a clustered pin we need to decide where we are going to place the individual pins that the clustered pin represents. As figure 2 shows, this example expands the pins in an evenly spaced, radial fashion, centered on the clustered pin. To do this takes a little bit of trigonometry. Also to have it fit on the map correctly at different map zoom levels we have to makes use of one more of the missing details this article mentioned earlier. That missing detail is an array of scaling factor constants that help with the trigonometry calculations. These values were determined by trial and error and chosen based on what looked right for the particular zoom level it represents. Here is the array that this example uses.


    // scale factor array
    var scaleFactors = new Array(.0005,     
                                 30, 20, 10, 
                                 5, 2.5, 1, 
                                 .7, .3, .2, 
                                 .1, .05, .03, 
                                 .01, .005, .003, 
                                 .0015, .0007, 
                                 .0004, .0002, .0002);
    

With that missing detail in place, here is the theory behind expanding the clustered pin.

Change the title, description, icon and anything else you may need of the clustered pin. Create a new ShapeLayer for the individual pins. Determine what scale factor to use based on the map zoom level. Then for each pin that the clustered pin represents, calculate the Latitude and Longitude, create a new pushpin and add it to the ShapeLayer. Add the ShapeLayer to the map. Finally, flag the clustered pin as an expanded pin.

Calculating the new latitude and longitude is where the trigonometry comes into play. This article will not go into the trigonometry details suffice to say that it calculates an X and Y offset that is added to the latitude and longitude of the center clustered pin.

Here is the code that this example uses.
(When creating the individual pins, an AJAX call could be made to get dynamic information for each pin, but for this example that information is hard coded)


    function ExpandPin(pin)
    {
        pin.SetDescription("Click pin  to collapse");
        pin.SetCustomIcon("<img src='expanded.png' />");

        // create a new shape layer
        var expandLayer = new VEShapeLayer();
        pin.ExpandedLayer = expandLayer;
        
         // get count of pins
        var pinCount = pin.Pins.length;
        var zoom = map.GetZoomLevel();
        var centerPoint = pin.GetPoints();
        var scalefactor = scaleFactors[zoom];
        var ms = map.GetMapStyle();
        
        if( ms == 'o' )  //adjust scaleFactor if oblique view
        {
            scalefactor = scaleFactors[0];
            if( zoom == 2 )
            {
                scalefactor = scaleFactors[20];
            }
        }

        var deg = 360 / pinCount;
        var totalDegree = 0;

        // loop to create the individual pins
        for(var i = 0; i < pinCount; i++)
        {
            var y = Math.cos(totalDegree * Math.PI/180);
            var x = Math.sin(totalDegree * Math.PI/180 );
            totalDegree += deg;
     
            var newLatLon = new VELatLong(centerPoint[0].Latitude + ( x * scalefactor), centerPoint[0].Longitude + ( y * scalefactor));
            var newPin = new VEShape(VEShapeType.Pushpin, newLatLon );
            var newpoly = new VEShape(VEShapeType.Polyline, [new VELatLong(centerPoint[0].Latitude, centerPoint[0].Longitude),newLatLon]);

            newpoly.HideIcon();
            newPin.SetTitle("Single Pin");
            newPin.SetDescription(pin.Pins[i]);
            newPin.SetCustomIcon("single.png");       
            expandLayer.AddShape(newPin);	
            expandLayer.AddShape(newpoly);  				
        }
        
        map.AddShapeLayer(expandLayer);
        
        pin.IsExpanded = true;
    }
    

The last step to make everything functional is the collapse function. It is quite straight forward. Set the clustered pins title, description and any other information you need to, and delete the ShapeLayer that holds the individual pins.


    function CollapsePin(pin)
    {
        // get the expanded layer and remove/delete it
        map.DeleteShapeLayer(pin.ExpandedLayer);
        
        pin.SetDescription("This Clusterd Pin Represents " + pin.Pins.length + 
		               " Pins <br /><br />Click the pin to expand");
        pin.SetCustomIcon("<img src='cluster.png' />");
        pin.ExpandedLayer = null;
        pin.IsExpanded = false;
    }
    

That is all that is needed to get the basic functionality of expanding/collapsing a clustered pin.

Now a little bit on some possible caveats.

The scaling factors:

As mentioned in the article, the scaling factors that this example uses are chosen by trial and error and can be changed to suit your needs. However if high values are used then the pins will end up outside of the map. This can also happen if the map size is really small.

Zooming in/out:

This example does not address the issue of recalculating the expanded pin locations when the map is zoomed in or out. So similar to the above issue, if you expand a pin while the map is zoomed out and then zoom the map in, the expanded pins will eventually be fall outside of the map. This could be remedied by attaching a handler to the “onendzoom” event and recalculate new positions for expanded pins, or just collapseing the expanded pins. Also, due to the default map projection, when the map is zoomed far out and a pin is expanded, the resulting individual pin locations will not be symmetrical. This could be remedied with a more complex and robust calculation based on the zoom factor, but for this example, the current results should work fine.

Pin locations:

Because the pin locations are calculated in a 360 degree circle around the center pin, if a clustered pin only has 2 pins, when it is expanded it looks like this:

two expanded pins

Personally I don’t find that aesthetically pleasing. So here is a bit of hack code that can be added to the expand function to adjust for this case and what the result looks like:

a better example of two expanded pins


    function ExpandPin(pin)
    {
	    .
	    .
        var deg = 360 / pinCount;
        var totalDegree = 0;
        
        // hack code to adjust for 2 pins
        if( deg > 120 )
        {
            deg = 120;
            totalDegree = -150;
         }

     // loop to create the individual pins
     for(var i = 0; i < pinCount; i++)
	    .
	    .
    

PNG custom pins and 3D mode:

This example uses custom icon files in PNG format and as such there are a few issues with 3D mode and older versions of Internet Explorer. This means that the custom icons may not render correctly unless a workaround is added to get the PNG files to render properly. Also, the scaling factors may need to be adjusted when creating latitude and longitude while in 3D mode. The simplest fix for this may be to add a second scalingFactor array, checking the map mode in the expand function and using this second scalingFactor array when in 3d mode.

Conclusion:

Because of the above issues, this example may not be ready for professional use. However it is a place to start, and with a little bit of rework, you can easily add this new feature to your Virtual Earth toolbox.

If you have any ideas for improvement, or any other method of expanding a clustered pin, please let me know about it so that I may have a chance to check it out.

Copyright 2009. Sponsored by nsquared.   |  Terms Of Use  |  Privacy Statement
Content on this site is generated from the developer community and shared freely for your enjoyment and benefit. This site is run independently of Microsoft and does not express Microsoft's views in any way.