VE Imagery Service and Custom Icons

Currently the VE Imagery service has the ability to display 26 different icons on the map. Oddly enough the default pushpin icon from the JavaScript version of Virtual Earth is not one of these icons. Unfortunately there is currently no built in method to display custom icons on the map image. There is also currently a limitation of 10 pushpins being passed to the imagery service. This article shows how to add any number of custom icons to the map image in the correct location. The default pushpin icon from the JavaScript version of Virtual Earth will be used as the custom icon. The high level steps involved to accomplish this are as follows:

  1. Calculate the best map view for an array of Location objects.
  2. Request map image from the imagery service that matches the best map view calculated in step 1.
  3. Calculate the pixel coordinate of each location and display custom icon on map image.
Calculate the Best Map View

The best map view is the zoom level and center point of a map image that contains all locations with the closest zoom level. The best map view will be stored using a structure.

/// <summary>
/// Structure to define a BestView object which defines a map view (centerpoint and zoom level)
/// </summary>
struct BestView
{
public Location center;
public int zoom;

//Constructor public BestView(Location _center, int _zoom)
{
center = _center;
zoom = _zoom;
}
}

Listing 1Structure to define a BestView object

The following method calculates the best map view for an array of locations. This results in a map image similar to what is displayed if the VEMap.SetMapView function is used in the JavaScript version of Virtual Earth.

/// <summary>
/// Calculates best map view for an array of points. Similar to VEMap.SetMapView method
/// </summary>
/// <param name="points">Array of Location objects</param>
/// <returns></returns>
private BestView CalculateMapView(Location[] points)
{
//Calculate bounding rectangle double maxLat = -90, minLat = 90, maxLon = -180, minLon = 180;

//default zoom scales in km/pixel from http://msdn2.microsoft.com/en-us/library/aa940990.aspx double[] defaultScales = { 78.27152, 39.13576, 19.56788, 9.78394, 4.89197, 2.44598, 1.22299,
0.61150, 0.30575, 0.15287, .07644, 0.03822, 0.01911, 0.00955, 0.00478, 0.00239, 0.00119, 0.0006, 0.0003 };

//Calculate bounding box for array of locations for (int i = 0; i < points.Length; i++)
{
if (points[i].Latitude > maxLat)
maxLat = points[i].Latitude;

if (points[i].Latitude < minLat)
minLat = points[i].Latitude;

if (points[i].Longitude > maxLon)
maxLon = points[i].Longitude;

if (points[i].Longitude < minLon)
minLon = points[i].Longitude;
}

//calculate center coordinate of bounding box double centerLat = (maxLat + minLat) / 2;
double centerLong = (maxLon + minLon) / 2;

//create a Location object for the center point Location centerPoint = new Location();
centerPoint.Latitude = centerLat;
centerPoint.Longitude = centerLong;

//want to calculate the distance in km along the center latitude between the two longitudes double meanDistanceX = HaversineDistance(centerLat, minLon, centerLat, maxLon);

//want to calculate the distance in km along the center longitude between the two latitudes double meanDistanceY = HaversineDistance(maxLat, centerLong, minLat, centerLong) * 2;

//calculates the x and y scales var meanScaleValueX = meanDistanceX / mapWidth; var meanScaleValueY = meanDistanceY / mapHeight; double meanScale;

//gets the largest scale value to work with if (meanScaleValueX > meanScaleValueY)
meanScale = meanScaleValueX;
else meanScale = meanScaleValueY; //intialize zoom level variable int zoom = 1;

//calculate zoom level for (var i = 1; i < 19; i++)
{
if (meanScale >= defaultScales[i])
{
zoom = i;
break;
}
}

//return a BestView object with the center point and zoom level to use return new BestView(centerPoint, zoom);
}

Listing 2 Method to calculate the best map view for an array of Locations

The method that calculates the best map view uses a method called HaversineDistance. This method calculates the distance in kilometers between two coordinates using the Haversine formula. The code for this method is as follows:

/// <summary>
/// Calculate the distance in kilometers between two coordinates
/// </summary>
/// <param name="lat1"></param>
/// <param name="lon1"></param>
/// <param name="lat2"></param>
/// <param name="lon2"></param>
/// <returns></returns>
private double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
{
double earthRadius = 6371;
double factor = Math.PI / 180;
double dLat = (lat2 - lat1) * factor;
double dLon = (lon2 - lon1) * factor;
double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(lat1 * factor) * Math.Cos(lat2 * factor)
* Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));

return earthRadius * c;
}

Listing 3 Haversine distance method

Request Map Image from the Imagery Service

By specifying a specific zoom level and center point for a map, this ensures that we have at least one point of reference between pixel coordinates and a latitude/longitude coordinate. The following method retrieves an image URL of a map for a specific map view. The image is then retrieves using a stream.

/// <summary>
/// Gets map from VE Imagery Web Service
/// </summary>
/// <param name="view">Map view</param>
/// <returns></returns>
private Image GetMapImage(BestView view)
{
//Map uri request object is created MapUriRequest mapUriRequest = new MapUriRequest();

// Credentials are set using a valid Virtual Earth Token mapUriRequest.Credentials = new VEImageryService.Credentials();
mapUriRequest.Credentials.Token = clientToken;

//Pass centerpoint of map mapUriRequest.Center = view.center; // Map style and zoom level are set MapUriOptions mapUriOptions = new MapUriOptions();
mapUriOptions.Style = MapStyle.Road;

//Set zoom level of map mapUriOptions.ZoomLevel = view.zoom; // Size of the requested image in pixels is set mapUriOptions.ImageSize = new VEImageryService.SizeOfint();
mapUriOptions.ImageSize.Height = mapHeight;
mapUriOptions.ImageSize.Width = mapWidth;

//Map options are added to the request mapUriRequest.Options = mapUriOptions; //ImageryServiceClient object created ImageryServiceClient imageryService = new ImageryServiceClient();

//A request for a map uri is made MapUriResponse mapUriResponse = imageryService.GetMapUri(mapUriRequest); //Get image from URI System.Net.WebRequest request =
System.Net.WebRequest.Create(mapUriResponse.Uri.Replace("{token}", clientToken));
System.Net.WebResponse response = request.GetResponse(); System.IO.Stream responseStream = response.GetResponseStream(); //return image return Image.FromStream(responseStream);
}

Listing 4 Method to retrieve map image from the VE Web Services for best map view

Calculate Pixel Coordinate for a Location

Virtual Earth tiles are positioned using quad key tile positioning. In Virtual Earth a map tile is 256 pixels by 256 pixels. For zoom level one, there are 4 map tiles displayed, making the total map width and height to display the whole world 512 pixels by 512 pixels. If the zoom level is 2, there are 16 map tiles being displayed, making the total map width and height to display the whole world 1024 pixels by 1024 pixels. Additional information the Virtual Earth tiling system can be found here: http://msdn.microsoft.com/en-us/library/bb259689.aspx

With the Virtual Earth tiling system in mind, all pixel coordinates for a set of latitude/longitude coordinates can be calculated. Using the center point of the map image the top left hand corner of the map image that is returned by the Virtual Earth imagery service can be calculated as a pixel location on a map that displays the whole world. Relative pixel coordinates can be then calculated for all location latitude/longitude coordinates.

The following method takes in a Location object and a BestView object and returns a Point object which refers to the pixel location of a latitude/longitude coordinate on the map image.

/// <summary>
/// Converts a latlitude longitude coordinate to a pixel coordinate of a map based on the map view
/// </summary>
/// <param name="latlong"></param>
/// <param name="view"></param>
/// <returns></returns>
private Point LatLongToPixel(Location latlong, BestView view)
{
//Formulas based on following article: //http://msdn.microsoft.com/en-us/library/bb259689.aspx //calcuate pixel coordinates of center point of map double sinLatitudeCenter = Math.Sin(view.center.Latitude * Math.PI / 180);
double pixelXCenter = ((view.center.Longitude + 180) / 360) * 256 * Math.Pow(2, view.zoom);
double pixelYCenter = (0.5 - Math.Log((1 + sinLatitudeCenter) / (1 - sinLatitudeCenter)) / (4 * Math.PI))
* 256 * Math.Pow(2, view.zoom);

//calculate pixel coordinate of location double sinLatitude = Math.Sin(latlong.Latitude * Math.PI / 180);
double pixelX = ((latlong.Longitude + 180) / 360) * 256 * Math.Pow(2, view.zoom);
double pixelY = (0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI))
* 256 * Math.Pow(2, view.zoom);

//calculate top left corner pixel coordiates of map image double topLeftPixelX = pixelXCenter - (mapWidth / 2);
double topLeftPixelY = pixelYCenter - (mapHeight / 2);

//calculate relative pixel cooridnates of location double x = pixelX - topLeftPixelX;
double y = pixelY - topLeftPixelY;

return new Point((int)Math.Floor(x), (int)Math.Floor(y));
}

Listing 5 LatLong to Pixel coordinate method

Putting it all together

In order for these methods to work in a WinForm application the following references will have to be made:

using System.Drawing.Design;
using System.Drawing.Drawing2D;
using System.IO;

Listing 6 Required References

A reference to the Virtual Earth Imagery Service and the Virtual Earth token service will also be required.

The following global variables will also be needed:

string clientToken;
int mapWidth = 600;
int mapHeight = 400;

Listing 7 Global Variables

The following method will calculate the best map view for an array of locations, retrieve a map image from the imagery service, add custom icons to the map image and then display the map image in a picture box.

/// <summary>
/// Generate map image with custom icons
/// </summary>
/// <param name="points">Array of Location objects</param>
private void GenerateImage(Location[] points)
{
//Get Best Map View BestView mapView = CalculateMapView(points); //Generate default map for best view Image map = GetMapImage(mapView); //add custom icons to map image AddCustomIcon(mapView, map, points); //display map MapImageBox.Image = map; }

Listing 8 Method to Generate map image with custom icons

The following method can be used to add the custom icons to a map image:

/// <summary>
/// Adds custom icon image to map image
/// </summary>
/// <param name="view"></param>
/// <param name="map"></param>
/// <param name="points"></param>
private void AddCustomIcon(BestView view, Image map, Location[] points)
{
//define the map as a gaphics object Graphics graphics = Graphics.FromImage(map); //Define custom icon Image customIcon = Image.FromFile("../../VEDefaultPin.gif");

//custom icon offset with respect to the top left corner of icon Point imageOffset = new Point(12, 12);

//Add custom icons to map image for (int i = 0; i < points.Length; i++)
{
Point point = LatLongToPixel(points[i], view);
int x = point.X - imageOffset.X;
int y = point.Y - imageOffset.Y;
graphics.DrawImageUnscaled(customIcon, x, y);
}
}

Listing 9 Method to add custom icons to a map image

This method can add a custom icon image and specify an image offset similar to the VECustomIconSpecification.

Here is an example of five locations plotted onto a map using Virtual Earth:

clip_image002

Figure 2 Points plotted using the JavaScript version of Virtual Earth

Here is an example of the same points being plotted onto a map image that is generated from the Virtual Earth Imagery Service using the methods in this article.

clip_image004

Figure 3 Custom icons on a VE Imagery Service image

Conclusion

Combine these methods with a valid client token and you will be able to add your custom icons to a map image. Complete sample code can be found here:
http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/CustomIconVEService.zip

This article was written by Richard Brundritt. Richard works for Infusion Development.

11 thoughts on “VE Imagery Service and Custom Icons

  1. Hi this article is very nice. is there any article to set custom icons for Driving Direction static map?

  2. Richard,

    This post has been very helpful. Can you please tell me if this is still the way to go with the new Bing API (v7) that came out? I am trying to get my custom pin images to show inside the bing map image, but I don’t have any other resource to achieve this other than the option that you have mentioned here. Since my map will contain a lot of pins, the conversion to pixel will be time consuming! Thanks!

    • In version 7 of Bing Maps you can add a custom icon to the map by using the Pushpin class and setting the pushpin options icon property to the url of where your custom pushpin is.

      • Richard,

        Thanks for your response. Currently I am using “ProdImagery.PushPin” class which does not have url property associated with it. It has iconstyle property associated with it and that is not helpful! When you said Pushpin class, is it inside the imagery service that Bing maps has or something else?

        Thanks

  3. Ricky,

    I have a question/comment. You’re setting the offset:
    Point imageOffset = new Point(12, 12);

    and then:
    int x = point.X – imageOffset.X;
    int y = point.Y – imageOffset.Y;

    That particular icon has dimensions of 25 x 30

    Won’t this offset place that bottom point a few pixels off? When I tested vs the AJAX control that was a little difference between the two. For an icon with a point on the bottom shouldn’t the offset be 1/2 the x and the full y?

    int x = point.X – 12;
    int y = point.Y – 30;

    for an icon with dimension 25 x 30

    Steven

    • In v6.3 the pin is not tied to the bottom center of the image. In the above code I was trying to recreate what you see when rendering the same data in Bing Maps v6.3 as that was my base case. The offset I used is the default offset in v6.3.

  4. Hi. My objective is to use a static Bing Map and make it interactive by plotting polygons and pushpins on it.
    However when i try to plot a pushpin on it, using the code given, the pushpin is not plotted at the exact point as desired, and is shifted a bit.

    Any help/suggestion on this would be greatly appreciated.
    Thank You.

    • I suspect that the image you are using for your pushpin is a different size from the one I used. Modify the pushpin offset accordingly. This is the line of code you want to modify: Point imageOffset = new Point(12, 12);

  5. Wow! Of all the articles I’ve read this 2008 article was the one. Your latlong to pixel coordinates method is exactly what I needed in replicating front-end dot placements on a map, to the geometry data type in SQL Server back-end. I’m no GIS developer and found the other examples made me go code blind in one eye and math blind in the other. Thanks for this article, drink on me if you’re ever in London.

Leave a comment