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:

Figure 1.
Expands it to show the individual pins:

Figure 2.
That is the concept and here is the basic theory behind the implementation used in this example:
To expand the clustered pin:
- Capture the onclick mouse event
- Verify that a clustered pin was clicked
- Create a new ShapeLayer to house the individual pins
- Create (and add to the new ShapeLayer) a new pushpin for each pin that the Clustered pin represents
Then to collapse the expaned pin:
- Capture the onclick mouse event
- Verify that an expaned pin was clicked
- 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:

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:

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.