Clustering with the Bing Maps Silverlight Control – Part 1

Introduction

There may be very good reasons why you don’t want to cluster your points of interest on a map e.g. if you want to show the density of points in a certain area. An example for this is the Bing Maps World Tour which has been developed by our partner Earthware and which visualizes the highlights of the latest Bing Maps data updates.

image

The downside of this is though that the map looks cluttered and you can’t access the points at the bottom of the pile anymore. In addition the number of points that you render on a map also affects the performance. The performance aspect is certainly much more relevant for the AJAX control since the Silverlight performance in general is much better but after a few thousand points you might reach a level where the user experience suffers. The Bing Maps AJAX control has client-side clustering build into the API and in a previous post I have compared the performance of client- and server-side clustering for the AJAX control.

The Bing Maps Silverlight control does not provide build-in clustering and hence we will have a look mainly at the clustering but we will also:

In this example we will build a photomap. On low zoom-levels, i.e. when we look at the whole world, all the photos for let’s say Sardinia are represented by a single pushpin…

image

…but we still have access to all the photos that are represented by this pin:

image

On higher zoom-levels we break up the cluster and show accurate positions for the photos. In the screenshot below you also see, that we extended the build-in navigation control with some custom elements to show and hide the photo-layer, toggle the mini-map and toggle full-screen mode.

image

You can see the application here and download the sample code here

Note: You will have to enter your own Bing Maps Key in the MainPage.xaml and modify the connection string in the web.config

 

What we need

 

The Concept

Before we actually start coding let’s hold on for a sec and think about the concept. The Bing Maps Silverlight control supports a variety of events. Since we don’t want to load all the photos but only those that are in the current map view we use these events to determine the bounding box and a couple of other parameters for our database query whenever we finish zooming or panning the map. At a first glance it might appear that the event TargetViewChanged might be appropriate but that would be a mistake since the target view changes with the zoom-level – so far so good – but also with every frame when you pan the map. That would create quite a lot of database queries and we certainly want to avoid that. More appropriate is the ViewChangeEnd event. We will add a handler for this event to the map and call a web service asynchronously. The web service will query our database, create a cluster of points and return a List-object that we can then process in our Silverlight application.

image

In order to cluster the photos (or points of interest) we will create in the web service a grid – let’s say with a width of 40 pixel – and determine the photos that are within each grid-cell. For each grid-cell that has one or more photo in it we will return a pin and tag it with some ID’s that identify the individual photos.

image

Once the Silverlight application receives the response we will process the data by adding pins to the map and dynamically creating child-elements for our photo-viewer. In this example we will do it by adding thumbnail images to a StackPanel in the ChildWindow. OK, let’s do it.

 

The Local Database

The database will be quite simple. We just have one table where me maintain the meta-data for our photos. The script for the table is listed below.

CREATE TABLE [Photos](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [Lat] [float] NULL,
    [Lon] [float] NULL,
    [Title] [varchar](250) NULL,
    [Name] [varchar](50) NULL,
    [Date] [date] NULL,
    [Loc1] [varchar](250) NULL,
    [Loc2] [varchar](250) NULL,
    [Loc3] [varchar](250) NULL,
 CONSTRAINT [PK_Photos] PRIMARY KEY CLUSTERED 
(
    [id] ASC
) ON [PRIMARY]
) ON [PRIMARY]

We need columns for the latitudes and longitudes of the locations where the photo was taken, the name of the file (the part before the extension .jpg) and optionally some more information that describe the location.

image

The photos themselves are accessible through a web server and for starters we’re going to use our local Internet Information Service to create a virtual directory that points to the location of the photos.

 

The Web Service

Let’s first create the web service that accesses the database. In our Visual Studio we create a new Silverlight application:

image

And choose to host the application in a new website.

image

In the web-project we create a new Silverlight-enabled WCF Service. Let’s call it svcPhotos.

image

If you consider using Windows Azure you must be aware of a bug that really puzzled me for a while. Everything worked well when I had my stand-alone application but once I wanted to use it within a Windows Azure project – no matter if it as in the development fabric or in the live environment – I received a weird error message: “cannot be processed at the receiver, due to an AddressFilter mismatch at the EndpointDispatcher. Check that the sender and receiver’s EndpointAddresses agree.”. Since I was actually re-writing the endpoint in my code I was pretty sure that the address was correct and finally I found out that the error can be resolved by adding the following ServiceBehaviour:

image

In our service we create a DataContract that describes the list-items in the list-object we want to return as well as an OperationsContract that queries the database, clusters the points and returns the list-object. The OperationsContract will take as input-parameters

  • latitudes and longitudes of the bounding box of the current map view
  • The width and height of the map in the browser
  • and the zoom-level

For the clustering we will also nee some helper functions that allow us to convert latitudes and longitudes into pixel coordinates. These functions are explained in more detail here. The full code of the web service is listed below:

Imports System.ServiceModel
Imports System.ServiceModel.Activation
Imports System.Data.SqlClient
Imports System.Runtime.Serialization
Imports System.Globalization

<ServiceContract(Namespace:="svcPhotos")>
<ServiceBehavior(AddressFilterMode:=AddressFilterMode.Any)>
<AspNetCompatibilityRequirements(RequirementsMode:=AspNetCompatibilityRequirementsMode.Allowed)>
Public Class svcPhotos
    'Constants for the Clustering
    '(addressable area in Bing Maps)
    Private Const MinLatitude As Double = -85.05112878
    Private Const MaxLatitude As Double = 85.05112878
    Private Const MinLongitude As Double = -180
    Private Const MaxLongitude As Double = 180

    'Grid-Size
    Private Const GridSize As Integer = 40

    'Clips a number to the specified minimum and maximum values
    Private Shared Function Clip(ByVal n As Double, ByVal minValue As Double, _
ByVal maxValue As Double) As Double Return Math.Min(Math.Max(n, minValue), maxValue) End Function 'Determine the offset off the map Public Shared Function Offset(ByVal lvl As Integer) As UInt32 Return 256 << lvl End Function 'Convert Latitude and Longitude to VEPixel Public Shared Sub LatLongToPixel(ByVal latitude As Double, ByVal longitude As Double, _
ByVal lvl As Integer, ByRef pixelX As Integer, ByRef pixelY As Integer) latitude = Clip(latitude, MinLatitude, MaxLatitude) longitude = Clip(longitude, MinLongitude, MaxLongitude) Dim x As Double = (longitude + 180) / 360 Dim sinLatitude As Double = Math.Sin(latitude * Math.PI / 180) Dim y As Double = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI) Dim mapSize As UInt32 = Offset(lvl) pixelX = CType(Clip(x * mapSize + 0.5, 0, mapSize - 1), Integer) pixelY = CType(Clip(y * mapSize + 0.5, 0, mapSize - 1), Integer) End Sub <OperationContract()> _ Public Function GetClusterInView(ByVal NWlat As Double, ByVal NWlon As Double, _
ByVal SElat As Double, ByVal SELon As Double, ByVal lvl As Integer, _
ByVal mapWidth As Integer, ByVal mapHeight As Integer) As List(Of ClusterPoint) 'set culture to en-UK to avoid potential problems with decimal-separators System.Threading.Thread.CurrentThread.CurrentCulture = _
System.Globalization.CultureInfo.CreateSpecificCulture("en-UK") 'Set up a grid for the Clustering Dim numXCells = CInt(Math.Ceiling(mapWidth / GridSize)) Dim numYCells = CInt(Math.Ceiling(mapHeight / GridSize)) Dim numCells As Integer = numXCells * numYCells - 1 Dim gridCells()() As Object = New Object(numCells)() {} 'Determine PixelX and PixelY of upper left corner Dim ulTotalX As Integer Dim ulTotalY As Integer LatLongToPixel(NWlat, NWlon, lvl, ulTotalX, ulTotalY) 'Query database(s) and create JavaScript Dim poiTotalX As Integer Dim poiTotalY As Integer Dim poiMapX As Integer Dim poiMapY As Integer Dim settings As ConnectionStringSettings 'Connection String is set in web.config

settings = ConfigurationManager.ConnectionStrings("TalkingDonkeyLocal") Dim myConn As New SqlConnection(settings.ConnectionString) myConn.Open() Dim myQuery As String = "SELECT Lat, Lon, Name, Loc1, Loc2, Loc3, Date “ _
+ ”FROM Photos WHERE (Lat BETWEEN "
+ SElat.ToString + " AND " _
+ NWlat.ToString + ") AND (Lon BETWEEN " + NWlon.ToString _
+ " AND " + SELon.ToString + ")" Dim myCMD As New SqlCommand(myQuery, myConn) Dim myReader As SqlDataReader = myCMD.ExecuteReader() While myReader.Read() 'Determine PixelX and PixelY of POI LatLongToPixel(myReader(0), myReader(1), lvl, poiTotalX, poiTotalY) poiMapX = poiTotalX - ulTotalX poiMapY = poiTotalY - ulTotalY 'Populate the array with clustered pins For x = 0 To numXCells - 1 If (x * GridSize <= poiMapX) And (poiMapX < (x + 1) * GridSize) Then For y = 0 To numYCells - 1 If (y * GridSize <= poiMapY) And (poiMapY < (y + 1) * GridSize) Then Dim myClusteredPin(4) As Object If gridCells(x * y) Is Nothing Then myClusteredPin(0) = 1 myClusteredPin(1) = myReader(0) myClusteredPin(2) = myReader(1) myClusteredPin(3) = myReader(2).ToString + "," _
+ myReader(3).ToString + "|" + myReader(4).ToString _
+ "|" + myReader(5).ToString + "|" _
+ CDate(myReader(6)).ToString("d", New CultureInfo("en-GB")) Else myClusteredPin = gridCells(x * y) myClusteredPin(0) = myClusteredPin(0) + 1 myClusteredPin(3) = myClusteredPin(3) + "," _
+ myReader(2).ToString + "," + myReader(3).ToString _
+ "|" + myReader(4).ToString + "|" + myReader(5).ToString + "|" _
+ CDate(myReader(6)).ToString("d", New CultureInfo("en-GB")) End If gridCells(x * y) = myClusteredPin End If Next End If Next End While myReader.Close() myConn.Close() 'Create the pins Dim myPins As New List(Of ClusterPoint) For i = 0 To numCells If gridCells(i) IsNot Nothing Then Dim myClusteredPin = gridCells(i) Dim myPin As New ClusterPoint(myClusteredPin(1), myClusteredPin(2), _
myClusteredPin(3)) myPins.Add(myPin) End If Next Return myPins End Function End Class <DataContract()> _ Public Class ClusterPoint <DataMember()> _ Private _Lat As Double Public Property Lat() As Double Get Return _Lat End Get Set(ByVal value As Double) _Lat = value End Set End Property <DataMember()> _ Private _Lon As Double Public Property Lon() As Double Get Return _Lon End Get Set(ByVal value As Double) _Lon = value End Set End Property <DataMember()> _ Private _Names As String Public Property Names() As String Get Return _Names End Get Set(ByVal value As String) _Names = value End Set End Property Public Sub New(ByVal _Lat As Double, ByVal _Lon As Double, ByVal _Names As String) Lat = _Lat Lon = _Lon Names = _Names End Sub End Class

 

Advertisements
This entry was posted in Bing Maps. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s