Updated! Pin Differential is no more!
Previous to Version 6 of the API, I implemented a pin differential algorithm that looped over the current data and added only the new pins, removing only the pins now out of view. In the last few days I've been work on the next generation of the solution. As part of this I have been testing the performance of the new bulk "addshape" method and determined its performance renders this older logic obsolete. Download the new JavaScript code below.
This article is part four in a series that explores the next generation clustering techniques for Virtual Earth. In part one Clustering a million points on Virtual Earth using AJAX and .Net we explored the general concept and made it work with ajax and vb.net. In part two Clustering Virtual Earth with MS AJAX and C# we refined the server side code with a complete rewrite in C# and we introduced ASP.NET AJAX web services. Part three looked at upgrading to Virtual Earth 5 and using the latest Object Orientated approach to Javascript from ASP.NET AJAX and also introduce a shape differential to only add and remove shapes as required to the map. In this article we upgrade to Virtual Earth 6, making use of the bulk addition of shapes, introduce some throttling logic to improve performance and consider PNG icons.
*Note: The server side code, and as such the core clustering logic, has not changed. The changes are purely client side in the javascript files.
In the very first article we learnt why we need clustering for large numbers of points and also for smaller numbers of point in the close proximity.
(Impossible to select an individual pin, large data size to client)
(Pins represent all pins under them, small data size to client)
Virtual Earth Version 6
Version 6 builds on the strong improvement made in V5 while making no breaking changes, simple by changing the URL of the API the application will run without change.
<script type="text/javascript"
src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6"></script>
Several key improvements from V6 we will utilise today including the bulk addition of shapes, bounds of birdseye scenes and css layout. The performance benefits and the support for Safari are great reasons to upgrade your application.
As usual the snippets I show are from the full source code that can be downloaded from the link at the end.
Bulk Shapes Addition
Shapes of type Pushpin can now be added in bulk, the change to the API in minimal, AddShape now accepts an array of shapes.
this._layer.AddShape([item1, item2, item3, ....., itemN]);
So we can modify our function that determines what needs to be added to the map and add to an array instead of directly to map. Then with a single call add all the shapes. I measured in the order of 5x the performance utilising this feature under normal conditions. Most interesting was measuring the time to add extremely large data examples to map, where adding 1500 shapes used to take over 2 min it now happens in a few seconds. The code changes:
_OnMapDataSucceeded: function(results) {
/// <summary>
/// Receive data for map.
/// </summary>
/// <param name="result">The webservice result object - Optomised CSV string</param>
//decode pins
var result=results.split(",")
var locs = Utility.decodeLine(result[0]);
var newShapes = new Array();
//clear existing pins
this._layer.DeleteAllShapes();
//add new pins
for(x = 0; x < locs.length; x++) {
var loc = locs[x];
var bounds = result[x+1];
var newShape = new VEShape(VEShapeType.Pushpin, loc);
newShape.Bounds = bounds;
//set custom png pin, IE6 will require a PNG fix.
if(Sys.Browser.agent==Sys.Browser.InternetExplorer&&Sys.Browser.version==6){
newShape.SetCustomIcon("<div class='myPushpinIE6'></div>");
}else {
newShape.SetCustomIcon("<div class='myPushpin'></div>");
}
newShapes.push(newShape);
}
this._layer.AddShape(newShapes);
},
Birdseye Bounds
When we upgraded to version 5 of VE we came across a problem, it was no longer possible to get the latitude and longitude bounds of the current scene in order to determine what data we needed from the server for the current view. The issue was one of license agreements concerning accurate reverse lookups of latitude and longitude on the birdseye scene. Although an encoded version is accessible for drawing and the like we had to resort to saving the centre of the view and approximating the area. Version 6 now gives us a new method that returns the approximate bounds, oversized to include at least the current scene. This gives us exactly what we need under the imposed license restrictions.
var rect = be.GetBoundingRectangle();
This means we can remove our previous hack and get our bounds when in birdseye mode like so:
if (this._map.GetMapStyle() == VEMapStyle.Birdseye) {
//set zoomlevel
zoom = 19;
var be = this._map.GetBirdseyeScene();
var rect = be.GetBoundingRectangle();
points.push(rect.TopLeftLatLong);
points.push(rect.BottomRightLatLong);
}else {
Throttling the web service calls
Using this code every time you move the map it calls the webservice to get the data for the current view. This is very powerful and allows us to stream just the information the user requires on demand. The issue is if you move the map many times in a short period of time a request is made for every move and this data must be processed. By default only two requests can be executed against the same domain so we must wait. The effect is the map data seems to lag, mimicking your previous movements as they load.
The solution is to only make, and process, one request at a time. Utilising the throttling concept from Omar Al Zabir we override the web service call and build a buffer of requests only allowing the most recent map data request to be made. If the server request for map data took one second to return and the map was moved 10 times within that one second, where previously we would see a staggered 10 sets of data be rendered over the 5-10 seconds instead only 2 calls are ever made, the initial move and the most recent, the others are discarded. The user sees the current views data 1 sec after they stop moving the map, the most ideal result.
var GlobalCallQueue = {
_callQueue : [], // Maintains the list of webmethods to call
_callInProgressNames : [], // Maintains webmethods names in progress by browser
_callInProgress : 0, // Number of calls currently in progress by browser
_maxConcurrentCall : 2, // Max number of calls to execute at a time
call : function(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout)
{
var queuedCall = new QueuedCall(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout);
//if method name is already waiting then remove from queue then add the new call
for (var x=0;x<GlobalCallQueue._callQueue.length;x++)
{
if (GlobalCallQueue._callQueue[x]._methodName==queuedCall._methodName)
{
Array.removeAt( GlobalCallQueue._callQueue, x);
break;
}
}
Array.add(GlobalCallQueue._callQueue,queuedCall);
GlobalCallQueue.run();
},
run : function()
{
/// Execute a call from the call queue
if (GlobalCallQueue._callInProgress < GlobalCallQueue._maxConcurrentCall)
{
if( 0 == GlobalCallQueue._callQueue.length )
{
return;
}
GlobalCallQueue._callInProgress ++;
//get first index that is not already in progess
var runIndex = -1;
for (var x=0;x<GlobalCallQueue._callQueue.length;x++)
{
var found=false;
for (var y=0;y<GlobalCallQueue._callInProgressNames.length;y++)
{
if (GlobalCallQueue._callQueue[x]._methodName==GlobalCallQueue._callInProgressNames[y])
{
found=true;
break;
}
}
if (!found)
{
runIndex = x;
break;
}
}
if (runIndex!=-1)
{
var queuedCall = GlobalCallQueue._callQueue[runIndex];
Array.removeAt( GlobalCallQueue._callQueue, runIndex );
//set name in progress
Array.add(GlobalCallQueue._callInProgressNames,queuedCall._methodName);
// Call the web method
queuedCall.execute();
}else
{
GlobalCallQueue._callInProgress --;
}
}
},
callComplete : function(methodName)
{
GlobalCallQueue._callInProgress --;
//remove name in progress
for (var x=0;x<GlobalCallQueue._callInProgressNames.length;x++)
{
if (GlobalCallQueue._callInProgressNames[x]==methodName)
{
Array.removeAt( GlobalCallQueue._callInProgressNames, x );
break;
}
}
GlobalCallQueue.run();
}
};
QueuedCall = function( servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout )
{
this._servicePath = servicePath;
this._methodName = methodName;
this._useGet = useGet;
this._params = params;
this._onSuccess = onSuccess;
this._onFailure = onFailure;
this._userContext = userContext;
this._timeout = timeout;
}
QueuedCall.prototype =
{
execute : function()
{
Sys.Net.WebServiceProxy.original_invoke(
this._servicePath, this._methodName, this._useGet, this._params,
Function.createDelegate(this, this.onSuccess), // Handle call complete
Function.createDelegate(this, this.onFailure), // Handle call complete
this._userContext, this._timeout );
},
onSuccess : function(result, userContext, methodName)
{
this._onSuccess(result, userContext, methodName);
GlobalCallQueue.callComplete(methodName);
},
onFailure : function(result, userContext, methodName)
{
this._onFailure(result, userContext, methodName);
GlobalCallQueue.callComplete(methodName);
}
};
//override invoke
Sys.Net.WebServiceProxy.original_invoke = Sys.Net.WebServiceProxy.invoke;
Sys.Net.WebServiceProxy.invoke =
function Sys$Net$WebServiceProxy$invoke(servicePath, methodName,
useGet, params, onSuccess, onFailure, userContext, timeout)
{
GlobalCallQueue.call(servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout);
}
Using PNG icons
Shapes have an icon, usually an image. As the shapes are simply HTML elements the type of image that can be used is limited to the browser. The PNG format supports a full alpha channel, that is 256 levels of transparency for every pixel. This differs from the GIF format that only support fully transparent pixels. For map icons the format allows us to use drop shadows, smooth edges and effectively more eye-popping icons that stand out from the map.
The issue we face is that IE6 does not support the PNG transparency format without some help. The most effective solution is to use an IE filter in your CSS to load the PNG image. Now the trick is you need to detect IE6 and only perform this trick for that browser and version. The AJAX library has just the functions we need, in this code I use a <div> for my icon and set a slightly different css class based on the browser detection.
if(Sys.Browser.agent==Sys.Browser.InternetExplorer&&Sys.Browser.version==6){
newShape.SetCustomIcon("<div class='myPushpinIE6'></div>");
}else {
newShape.SetCustomIcon("<div class='myPushpin'></div>");
}
CSS
.myPushpin {
margin-left: 4px;
margin-top: -24px;
background:url(pin.png);
height:40px;
width:35px;}
.myPushpinIE6 {
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='pin.png', sizingMethod='image');
height:40px;
width:35px;}
Sample Source Code Link Below
You must download and install MS ASP.NET AJAX to run this code.
The sample code for this article gets it data from an XML file of Australian postcodes. I have cached this generated data in memory for performance, for small sets of static data this maybe an option for you. For larger sets of data I recommend storing the data in a database allowing you to query based on the map bounds.
Conclusion:
Version 6 of Virtual Earth is very much welcomed by this code, we improve the performance and remove a "hack" required previously. Additionally I shared with you the concept of throttling data calls to increase the effective response time of the system and the use of PNG graphics.
Spatial support in SQL Server 2008 is just around the corner. The ability to move much of the server-side cluster logic to the data tier promises to increase the performance of this concept significantly. I look forward to this database technology providing many improvements for working with large sets of spatial data with Virtual Earth.
Have a comment or used this code on your site? Why don't you tell me about it at my blog