November 2012

Volume 27 Number 11

Touch and Go - Assembling Bing Map Tiles on Windows Phone

By Charles Petzold | ovember 2012

Charles PetzoldThe Motion sensor in Windows Phone  consolidates information from the phone’s compass and accelerometer to create a rotation matrix that describes the orientation of the phone in 3D space.

Recently I began pondering how the phone’s orientation could be used in combination with Bing Maps. I anticipated a quickie mashup, but the job turned out to be rather more complex.

If you’ve experimented with the standard Maps program on your Windows Phone, you know that the display is usually aligned so that north is toward the top of the phone. (The only exception is when you’re using the map for directions to a location, in which case the map is oriented to indicate the direction you’re travelling.) Up for north is the convention for maps, of course, but in some cases you might want the phone’s map to rotate relative to the phone so that north on the map is actually pointing north.

It seems so simple, right?

Map Control Limitations

In my quest to implement a rotating map on the phone, I started with the Bing Maps Silverlight Control for Windows Phone, the center of which is a control named simply Map in the Microsoft.Phone.Controls.Maps namespace.

To get started with the Map control (or anything involving accessing Bing Maps programmatically) you need to register at the Bing Maps Account Center at bingmapsportal.com. It’s a straight­forward process to obtain a credentials key that gives your program access to Bing Maps.

In the Windows Phone project that uses the Map control, you’ll need a reference to the Microsoft.Phone.Controls.Maps assembly, and you’ll probably want an XML namespace declaration in the XAML file (in one line):

xmlns:maps="clr-namespace:Microsoft.Phone.Controls.Maps;
  assembly=Microsoft.Phone.Controls.Maps"

Instantiating a basic map is then trivial:

<maps:Map CredentialsProvider="credentials-key" />

Insert the actual credentials key you’ve obtained from the Bing Maps Account Center.

Yes, getting a map on the screen is easy, but when I searched out ways to rotate it, I came up dry. For sure, the Map class inherits a Heading property from the MapBase class, but apparently this property is relevant only for “bird’s eye” maps, and the Map control supports only the standard road and aerial views.

Of course, it’s fairly trivial to rotate the Map control by setting a RotateTransform object to its RenderTransform property, but I wasn’t happy at all with that solution. For one thing, it tended to obscure the copyright notice at the bottom of the map, and doing that seemed to be in violation of the conditions I agreed to when obtaining a credentials key.

I decided to abandon the Maps control and instead try my luck on a much lower level by accessing the Bing Maps SOAP Services. This set of Web services allows a program to obtain the actual bitmap tiles from which larger maps are constructed.

Accessing the SOAP Service

In a Web service built around Simple Object Access Protocol (SOAP), information is transferred between your program and the server using XML documents. Very often these documents involve rather complex data structures, so instead of handling the XML directly, a much easier approach is to have Visual Studio create a proxy class for you. This allows your program to access the Web service with normal (albeit asynchronous) C# classes and method calls.

The Bing Maps SOAP Services interface is documented at bit.ly/S3R4lG, and consists of four separate services:

  • Geocode Service: Match addresses with longitude and latitude.
  • Imagery Service: Obtain maps and tiles.
  • Route Service: Get directions.
  • Search Service: Locate people and businesses.

I was only interested in the Imagery Service.

The downloadable code for this article is a single project named RotatingMapTiles. I added a proxy for the Imagery Service to this project by choosing Add Service Reference from the Project menu. In the Address field of the Add Service Reference dialog, I copied the URL listed in the Bing Maps SOAP Services Addresses section of the documentation and pressed Go. When the service was located, I gave it a name of ImageryService in the Namespace field.

The C# code generated for this service has a namespace that’s the normal project namespace, followed by the namespace you specify when creating the service reference, so the MainPage.xaml.cs file in the RotatingMapTile program contains the following using directive:

using RotatingMapTiles.ImageryService;

The Imagery Service supports two types of requests involving the function calls GetMapUriAsync and GetImageryMetadataAsync. The first call allows you to obtain static maps of a particular location, size and zoom level, while the second works at a lower level. Among the metadata this second call returns is a URI template that lets you access the actual bitmap tiles that are used to assemble all the Bing maps.

Figure 1 shows the Loaded handler for the MainPage class in the RotatingMapTiles project making two calls to the Imagery Web service to obtain metadata for the MapStyle.Road and MapStyle.Aerial styles. (MapStyle.Birdseye is also available but it’s rather more complex to use.)

Figure 1 Code to Access the Bing Maps Imagery Web Service

void OnMainPageLoaded(object sender, RoutedEventArgs e)
{
  // Initialize the Bing Maps imagery service
  ImageryServiceClient imageryServiceClient =
    new ImageryServiceClient("BasicHttpBinding_IImageryService");
    imageryServiceClient.GetImageryMetadataCompleted +=
      GetImageryMetadataCompleted;
  // Make a request for the road metadata
  ImageryMetadataRequest request = new ImageryMetadataRequest
  {
    Credentials = new Credentials
    {
      ApplicationId = "credentials-key"
    },
    Style = MapStyle.Road
  };
  imageryServiceClient.GetImageryMetadataAsync(request, "road");
  // Make a request for the aerial metadata
  request.Style = MapStyle.Aerial;
  imageryServiceClient.GetImageryMetadataAsync(request, "aerial");
}

You’ll need your own Bing Maps credential key to substitute for the placeholder.

Figure 2 shows the handler for the Completed event of the asynchronous call. The information includes a URI for a bitmap with the Bing logo, so it’s easy to credit Bing Maps on the program’s screen.

Figure 2 The Completed Handler for the Bing Maps Web Service

void GetImageryMetadataCompleted(object sender,
   GetImageryMetadataCompletedEventArgs args)
{
  if (!args.Cancelled && args.Error == null)
  {
    // Get the "powered by" bitmap
    poweredByBitmap.UriSource = args.Result.BrandLogoUri;
    poweredByDisplay.Visibility = Visibility.Visible;
    // Get the range of map levels available
    ImageryMetadataResult result = args.Result.Results[0];
    minimumLevel = result.ZoomRange.From;
    maximumLevel = result.ZoomRange.To;
    // Get the URI and make some substitutions
    string uri = result.ImageUri;
    uri = uri.Replace("{subdomain}", result.ImageUriSubdomains[0]);
    uri = uri.Replace("&token={token}", "");
    uri = uri.Replace("{culture}", "en-US");
    if (args.UserState as string == "road")
      roadUriTemplate = uri;
    else
      aerialUriTemplate = uri;
    if (roadUriTemplate != null && aerialUriTemplate != null)
      RefreshDisplay();
  }
  else
  {
    errorTextBlock.Text =
      "Cannot access Bing Maps: " + args.Error.Message;
  }
}

The other URI is the one you’ll use to access the map tiles. There are separate URIs for the road and aerial views, and the URI contains a placeholder for a number that identifies precisely the tile you want.

Maps and Tiles

The tiles that form the basis of Bing Maps are bitmaps that are always 256 pixels square. Each tile is associated with a particular longitude, latitude and zoom level, and contains an image of a square area on the surface of the Earth flattened using the common Mercator projection.

The most extreme zoomed-out view is known as Level 1, and only four tiles are required to cover the entire world—or at least the part of the world with latitudes between positive and negative 85.05°—as shown in Figure 3.

The Four Level 1 Tiles
Figure 3 The Four Level 1 Tiles

I’ll explain the numbers on the tiles in a moment. Because the tiles are 256 pixels square, at the equator each pixel is equivalent to about 49 miles.

Level 2 is more granular, and now 16 tiles cover the Earth, as shown in Figure 4.

The 16 Level 2 Tiles
Figure 4 The 16 Level 2 Tiles

The tiles in Figure 4 are also 256 pixels square, so at the equator each pixel is about 24 miles. Notice that each tile in Level 1 covers the same area as 4 tiles in Level 2.

This scheme continues: Level 3 has 64 tiles, Level 4 has 256 tiles, and up and up and up to Level 21, which covers the Earth with a total of more than 4 trillion tiles—2 million horizontally and 2 million vertically for a resolution (at the equator) of 3 inches per pixel.

Numbering the Tiles

Because at least a few of these trillions of tiles must be individually referenced by a program that wishes to use them, they must be identified in a clear and consistent manner. There are three dimensions involved—longitude, latitude and zoom level—and a practical consideration as well: To minimize disk accesses on the server, tiles associated with the same area should be stored near each other, which implies a single numbering system that encompasses all three dimensions in some very clever manner.

The clever numbering system used for these map tiles is called a “quadkey.” The MSDN Library article, “Bing Maps Tile System,” by Joe Schwartz (bit.ly/SxVojI) is a really good explanation of the system (including helpful code) but I’ll take a somewhat different approach here.

Each tile has a unique quadkey. The tile URIs obtained from the Web service contain a placeholder string “{quadkey}”. Before you use one of the URIs to access a tile, you must replace this placeholder with an actual quadkey.

Figure 3and Figure 4 show the quadkeys for zoom Levels 1 and 2.  Leading zeros are important in quadkeys. (Indeed, you might want to think of the quadkey as a string rather than a number.) The number of digits in a quadkey is always equal to the zoom level of the tile. The tiles in Level 21 are identified with 21-digit quadkeys.

The individual digits of a quadkey are always 0, 1, 2 or 3. Thus, the quadkey is really a base-4 number. Look at these four digits in binary (00, 01, 10, 11) and how they appear in a group of four tiles. In each base-4 digit, the second bit is really a horizontal coordinate and the first bit is a vertical coordinate. The bits correspond to a longitude and latitude, which are effectively interleaved in the quadkey.

Each tile in Level 1 covers the same area as a group of four tiles in Level 2. You can think of a tile in Level 1 as a “parent” to four “children” in Level 2. The quadkey of a child tile always begins with the same digits as its parent, and then adds another digit (0, 1, 2 or 3) depending on its location within the area of its parent. Going from parent to child is a zoom up. Zooming down is similar: For any child quadkey, you obtain the parent quadkey simply by lopping off the last digit.

Here’s how to derive a quadkey from an actual geographic longitude and latitude.

Longitude ranges from -180° at the Inter­national Date Line, and then increases going east to 180° at the International Data Line again. For any longitude, first calculate a relative longitude that ranges from 0 to 1 with 0.5 representing the prime meridian:

double relativeLongitude = (180 + longitude) / 360;

Now convert that to an integer of a fixed number of bits:

int integerLongitude =
  (int)(relativeLongitude * (1 << BITRES));

In my program I’ve set BITRES to 29 for the 21 zoom levels plus 8 bits for the pixel size of the tile. Thus, this integer identifies a longitude precise to the nearest pixel of a tile at the highest zoom level.

The calculation of integerLatitude is a little more complex because the Mercator map projection compresses latitudes as you get further from the equator:

double sinTerm = Math.Sin(Math.PI * latitude / 180);
double relativeLatitude =
  0.5 - Math.Log((1 + sinTerm) / (1 - sinTerm)) 
    / (4 * Math.PI);
int integerLatitude = (int)(relativeLatitude * (1 << BITRES));

The integerLatitude ranges from 0 at 85.05° north of the equator to the maximum value at 85.05° south of the equator.

The center of Central Park in New York City has a longitude of -73.965368° and a latitude of 40.783271°. The relative values are (to just a few decimal places) 0.29454 and 0.37572. The 29-bit integer longitude and latitude values (shown in binary and grouped for easier readability) are:

0 1001 0110 1100 1110 0000 1000 0000
0 1100 0000 0101 1110 1011 0000 0000  

Suppose you want a tile that shows the center of Central Park in a Level 12 zoom. Take the top 12 bits of the integer longitudes and latitudes (watch out—the following digits are grouped a little differently than the 29-bit versions):

0100 1011 0110
0110 0000 0010

These are two binary numbers but we need to combine them to form a base-4 number. There’s no way to do this in code using simple arithmetical operators. You need a little routine that actually goes through the individual bits and constructs a longer integer or a string. For illustrative purposes, you can simply double all the bits in the latitude and add the two values as if they were base-4 values:

0100 1011 0110
0220 0000 0020
0320 1011 0130

The result is the 12-digit quadkey you’ll need to substitute for the “{quadkey}” placeholder in the URI you get from the Web service to access the map tile.

Figure 5 shows a routine to construct a quadkey from the truncated integer longitudes and latitudes. For clarity, I’ve separated the logic into sections that generate a quadkey long integer and a quadkey string.

Figure 5 A Routine to Calculate a Quadkey

string ToQuadKey(int longitude, int latitude, int level)
{
  long quadkey = 0;
  int mask = 1 << (level - 1);
  for (int i = 0; i < level; i++)
  {
    quadkey <<= 2;
    if ((longitude & mask) != 0)
      quadkey |= 1;
    if ((latitude & mask) != 0)
      quadkey |= 2;
    mask >>= 1;
  }
  strBuilder.Clear();
  for (int i = 0; i < level; i++)
  {
    strBuilder.Insert(0, (quadkey & 3).ToString());
    quadkey >>= 2;
  }
  return strBuilder.ToString();
}

Figure 6 shows both the road and aerial tiles for this quadkey. The center of Central Park is actually way down at the bottom of these images, a bit to the left of center. This is predictable from the integer longitudes and latitudes. Look at the next 8 bits of the integer longitude after the first 12 bits: The bits are 0111 0000 or 112. The next 8 bits of the latitude are 1111 0101 or 245. This means the center of Central Park is the 112th pixel from the left and the 245th pixel down in those tiles.

The Tiles for Quadkey “032010110130”

Figure 6 The Tiles for Quadkey “032010110130”

Tiling the Tiles

Once you’ve truncated an integer longitude and latitude to a certain number of bits corresponding to a particular zoom level, obtaining adjacent tiles is a snap: Simply increment and decrement the longitude and latitude integers and form new quadkeys.

You’ve already seen three methods from the MainPage class of the RotatingMapTiles project. The program uses a GeoCoordinateWatcher to obtain the longitude and latitude of the phone, and converts the coordinates to integer values as shown earlier. The application bar has three buttons: to toggle between road and aerial views and to increase and decrease the zoom level.

The program has no other touch interface besides the buttons. It always displays the location obtained from the GeoCoordinateWatcher in the center of the screen and constructs the total map with 25 Image elements in a 5x5 array, a configuration that always fills the 480x800 pixel screen, even with rotation. The MainPage class creates these 25 Image elements and BitmapImage objects in its constructor.

Whenever the GeoCoordinateWatcher comes up with a new location, or the zoom level or map style changes, the RefreshDisplay method in Figure 7 is called. This method shows how the new URIs are obtained and simply set to the existing BitmapImage objects.

Figure 7 The RefreshDisplay Method in RotatingMapTiles

void RefreshDisplay()
{
  if (roadUriTemplate == null || aerialUriTemplate == null)
    return;
  if (integerLongitude == -1 || integerLatitude == -1)
    return;
  // Get coordinates and pixel offsets based on current zoom level
  int croppedLongitude = integerLongitude >> BITRES - zoomLevel;
  int croppedLatitude = integerLatitude >> BITRES - zoomLevel;
  int xPixelOffset = (integerLongitude >> BITRES - zoomLevel - 8) % 256;
  int yPixelOffset = (integerLatitude >> BITRES - zoomLevel - 8) % 256;
  // Prepare for the loop
  string uriTemplate = mapStyle ==
    MapStyle.Road ? roadUriTemplate : aerialUriTemplate;
  int index = 0;
  int maxValue = (1 << zoomLevel) - 1;
  // Loop through the 5x5 array of Image elements
  for (int row = -2; row <= 2; row++)
    for (int col = -2; col <= 2; col++)
    {
      // Get the Image and BitmapImage
      Image image = imageCanvas.Children[index] as Image;
      BitmapImage bitmap = image.Source as BitmapImage;
      index++;
      // Check if you've gone beyond the bounds
      if (croppedLongitude + col < 0 ||
        croppedLongitude + col > maxValue ||
        croppedLatitude + row < 0 ||
        croppedLatitude + row > maxValue)
      {
        bitmap.UriSource = null;
      }
      else
      {
        // Calculate a quadkey and set URI to bitmap
        int longitude = croppedLongitude + col;
        int latitude = croppedLatitude + row;
        string strQuadkey =
          ToQuadKey(longitude, latitude, zoomLevel);
        string uri = uriTemplate.Replace("{quadkey}", strQuadkey);
        bitmap.UriSource = new Uri(uri);
      }
      // Position the Image element
      Canvas.SetLeft(image, col * 256 - xPixelOffset);
      Canvas.SetTop(image, row * 256 - yPixelOffset);
    }
}

To keep this program reasonably simple, it doesn’t attempt to smooth over the transitions between views and zoom levels. Often the whole screen goes blank as new tiles are being loaded.

But the program does rotate the map. The rotation logic is based on the Motion sensor and a RotateTransform and is pretty much independent of the rest of the program. Figure 8 shows me taking my Windows Phone (or perhaps the Windows Phone emulator) for a walk across the Brooklyn Bridge. The top of the phone is pointed in the direction I’m walking, and the little arrow in the upper-left corner indicates north.

The RotatingMapTiles Display
Figure 8 The RotatingMapTiles Display


Charles Petzold is a longtime contributor to MSDN Magazine, and the author of “Programming Windows, 6th edition” (O’Reilly Media, 2012), a book about writing applications for Windows 8. His Web site is charlespetzold.com.

Thanks to the following technical expert for reviewing this article: Thomas Petchel