Bing Maps Tile System: A bit of theory and an app to test it in Windows Phone 7

A few weeks back my good friend, colleague and Microsoft trainer par excellence, Elias Markelis asked me to do a joint webinar on Windows Phone and Bing Maps, part of Microsoft’s Live Virtual Academy series. The idea being that since I am the ‘GIS guy’ I will talk a bit more about the spatial / map aspects and not just the code.

(BTW, you can also find the full webinar at http://www.techdays.gr/videos/4056.html but take note: The slides are in English but seminar was delivered in Greek.)

Elias has now written a 3-part blog post to cover most of the things we talked about in the webinar: You can find the links here:

These are very detailed step-by-step guides on how to develop your first Windows phone map-based app, utilizing the Geocoding and Routing Services, GPS, the emulator and lots of other goodies to get you started.

But this is the code only. In this post, which can be thought as an ‘addendum’ to Elias’ series, I would talk about how Bing Maps work, their tile system, scales and projection systems. The post is mainly geared towards Silverlight and Windows Phone developers who never worked with GIS and mapping systems before. GIS gurus, feel free to skip directly to the code!

Should I care? Just show me some code!

Well, you should care really. Understanding the tile system and projections would make a huge difference in real-world applications where you would invariably need to integrate with other spatial data in different formats. For example you may have to display geometry data from a SQL Server Database, or add a WMS layer. In any of these cases, you would need to understand terms like ‘projection system’, ‘geographic coordinates’ and ‘map scales’. And also understand how these apply in Bing Maps.

But fear not, its not going to be ALL theory. I would also show you how to develop a Bing Maps app which will help you visualize some of the theory and maths behind Bing Maps.

The Basics – Bing Maps tile system

Now, before I start, I should point out that there is an excellent article in MSDN which explains the Bing Maps Tile System in great detail, including all the maths behind it. It even includes all the C# code that implements it. In actual fact, I would be using bits of this code in my own example. But here, I will just give you the basics to get you started in the form of a list of simple, clear statements. So here it goes:

  • Bing Maps uses a single map projection system called Mercator. The projection is necessary since Bing Maps displays the whole earth, the earth, well, is round, so in order to see it a screen (which, you know, is flat) we have to ‘project’ it. The image below depicts this concept. (This is not the Mercator projection BTW, just a peeled orange ).
image
The Earth is round, a computer screen ain
  • To accurately locate a point or any other feature on the map, the WGS84 Spatial Reference System (SRID) is used which uses geographic coordinates (Latitude and Longitude denoted also as φ,λ). This is the same SRID used by the GPS devices (see the link here?)
  • Map scale is another feature of any map. It denotes the ratio between a real-world distance and the map distance at a specified unit. For example a map at a scale of 1:100,000 with map units in meters, means that a meter measured on the map (or you screen) is a 100,000m in the real world. One of the things to point out here is that scale only makes sense and applies on maps on a screen or on paper. Data itself does not have scale.For example, the question of what is the scale of a point at coordinates 40,2345, 22.87361 does not make sense!
  • In very simple terms Bing Maps maps the world using satellite images stitched together. As you can appreciate, these are terrabytes of data we are talking about.To optimize performance these images are divided into tiles of 256×256 pixels each.
  • Bing Maps has the concept of zoom levels. There are 23 zoom levels where 1 displays the whole earth and 23 zooms at street level. When you zoom in or out at a different zoom level, different tiles (and hence satellite images) are displayed.
  • Tiles have their own coordinate system (a “Tile XY coordinate”) which can also be denoted as a single string called a quadkey. As explained in the Bing Maps Tile System article:

To optimize the indexing and storage of tiles, the two-dimensional tile XY coordinates are combined into one-dimensional strings called quadtree keys, or “quadkeys” for short. Each quadkey uniquely identifies a single tile at a particular level of detail…

The example below displays the quadkeys for Levels 1-3 (image from the same article):

image
Quadkeys for zoom levels 1 to 3

Some code (at last)

Just to ‘visualize’ what we talked about, we will create a Windows Phone application with Bing Maps which will accomplish a few simple tasks:

  • Double-tapping the map will display the quadkey value of the current tile in a message box.
  • Panning the map, will display the latitude, longitude coordinates in a textblock.
  • The map will also draw the tiles used in the current map extent and display their quadkey value

To start off with, start a new Windows Phone 7 project in Visual Studio 2010 and add the maps control I won’t go into any details on how to do this, there are a lot of articles abound. You can try Elias’ Windows Phone location awareness– the basics or one of mine Sterling Database for Windows Phone 7, Silverlight and Bing Maps

We will now add 3 event handlers to the map control. One for double-tapping on the map, one for mouse move and one for the ViewChangeEnd which fires when the map has finished zooming or panning. Our XAML file should now look like this:

Code Snippet
  1. <phone:PhoneApplicationPage
  2.     x:Class=”BingMapsBasics.MainPage”
  3.     xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;
  4.     xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml&#8221;
  5.     xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
  6.     xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
  7.     xmlns:d=”http://schemas.microsoft.com/expression/blend/2008&#8243;
  8.     xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006&#8243;
  9.     xmlns:my=”clr-namespace:Microsoft.Phone.Controls.Maps;assembly=Microsoft.Phone.Controls.Maps”
  10.     mc:Ignorable=”d” d:DesignWidth=”480″ d:DesignHeight=”768″
  11.     FontFamily=”{StaticResource PhoneFontFamilyNormal}
  12.     FontSize=”{StaticResource PhoneFontSizeNormal}
  13.     Foreground=”{StaticResource PhoneForegroundBrush}
  14.     SupportedOrientations=”Portrait” Orientation=”Portrait”
  15.     shell:SystemTray.IsVisible=”True”>
  16.     <!–LayoutRoot is the root grid where all page content is placed–>
  17.     <Grid x:Name=”LayoutRoot” Background=”Transparent”>
  18.         <Grid.RowDefinitions>
  19.             <RowDefinition Height=”Auto”/>
  20.             <RowDefinition Height=”*”/>
  21.         </Grid.RowDefinitions>
  22.         <!–TitlePanel contains the name of the application and page title–>
  23.         <StackPanel x:Name=”TitlePanel” Grid.Row=”0″ Margin=”12,17,0,28″>
  24.             <TextBlock x:Name=”ApplicationTitle” Text=”Bing Maps Demo” Style=”{StaticResource PhoneTextNormalStyle}“/>
  25.             <TextBlock x:Name=”PageTitle” Text=”Map Basics” Margin=”9,-7,0,0″ Style=”{StaticResource PhoneTextTitle1Style}“/>
  26.         </StackPanel>
  27.         <Grid x:Name=”ContentPanel” Grid.Row=”1″ Margin=”12,0,12,0″>
  28.             <Grid.RowDefinitions>
  29.                 <RowDefinition Height=”50″/>
  30.                 <RowDefinition Height=”*”/>
  31.             </Grid.RowDefinitions>
  32.             <my:Map Grid.Row=”1″ x:Name=”demoMap” Margin=”0,0,0,0″
  33.                     CredentialsProvider=”YOURBINGMAPSCODEHERE”
  34.                     DoubleTap=”demoMap_DoubleTap”
  35.                     MouseMove=”demoMap_MouseMove” ZoomBarVisibility=”Visible”
  36.                    ViewChangeEnd=”demoMap_ViewChangeEnd”>
  37.                 <my:MapLayer x:Name=”quadkeys”></my:MapLayer>
  38.             </my:Map>
  39.             <TextBlock Grid.Row=”0″ x:Name=”txbCoords” VerticalAlignment=”Top” HorizontalAlignment=”Left” Foreground=”White” FontWeight=”Bold” Text=”Coordinates:” />
  40.         </Grid>
  41.     </Grid>
  42. </phone:PhoneApplicationPage>

The MouseMove event handler is listed below:

Code Snippet
  1. private void demoMap_MouseMove(object sender, MouseEventArgs e)
  2.         {
  3.             // Point in pixels
  4.             Point pnt = e.GetPosition(this.demoMap);
  5.             // Geographic point
  6.             GeoCoordinate geoPnt = new GeoCoordinate();
  7.             // Get current point in Lat/Lon
  8.             geoPnt = demoMap.ViewportPointToLocation(pnt);
  9.             //Update the textblock with the current Lat/Lon
  10.             txbCoords.Text = “Lat: “ + geoPnt.Latitude.ToString() +” Lon: “ + geoPnt.Longitude.ToString();
  11.         }

Our DoubleTap handler follows. It basically picks up the Latitude, Longitude location the user tapped on the map, converts them to pixel coordinates, then to Tile coordinates and then to the quadkey value. All the conversion functions can be found in the (yes, you guessed it) MSDN article mentioned above.

Code Snippet
  1. private void demoMap_DoubleTap(object sender, GestureEventArgs e)
  2.         {
  3.             int pX; int pY;
  4.             int tX; int tY;
  5.             Point p = e.GetPosition(this.demoMap);
  6.             GeoCoordinate geo = new GeoCoordinate();
  7.             // Get current point in Lat/Lon
  8.             geo = demoMap.ViewportPointToLocation(p);
  9.             // Convert to pixel coordinates
  10.             LatLongToPixelXY(geo.Latitude, geo.Longitude, Convert.ToInt32(demoMap.ZoomLevel), out pX, out pY);
  11.             //Convert to tile coordinates
  12.             PixelXYToTileXY(pX, pY, out tX, out tY);
  13.             //Get the Quadkey
  14.             string quadKey = TileXYToQuadKey(tX, tY, Convert.ToInt32(demoMap.ZoomLevel));
  15.             MessageBox.Show(“QuadKey: “ + quadKey + “Lat: “ + geo.Latitude.ToString() +” Lon: “ + geo.Longitude.ToString());
  16.         }

Okimage, for the time being leave our last event handler (ViewChangeEnd) empty and compile and run the project. Zoom and pan the map to an area you are interested in. Notice that while panning the geographic coordinates are getting updated in the text block.

Now double click/tap at different areas of the screen. A message box should appear displaying the quadkey of the current tile

That’s ok. Useful-ish. But you have to \guess’ where the different tiles are. What we will do next is display the actual tiles on the map as rectangles and also display the corresponding quadkeys. And we will wire this functionality in the ViewChangEnd event, so it runs only when the map has finished rendering.

The logic behind the drawing of the tiles is pretty simple: Find the extent of the current map (min & max coordinates) and then ‘loop’ or ‘sweep’ the map, 256pixels at a time to get to the next tile, calculating the rectangle (tile) at each iteration and draw this rectangle on the map.

I implemented this ‘sweeping’ using three loops, one by latitude, one by longitude and one ‘diagonal’ where both latitude and longitude were iterated by 256 pixels. Now, I am pretty sure there is a by far smarter way of achieving the same result but its getting late and this was the easiest way I could come up with.

The relevant code is listed below:

Code Snippet
  1.  private void demoMap_ViewChangeEnd(object sender, MapEventArgs e)
  2.  {
  3.      CalcQuadkeys();
  4.  }
  5.  ///<summary>
  6.  /// Calculates the quadkeys
  7.  ///</summary>
  8.  public void CalcQuadkeys()
  9.  {
  10.      MapLayer l = (MapLayer)demoMap.FindName(“quadkeys”);
  11.      l.Children.Clear();
  12.      LocationRect extents = demoMap.BoundingRectangle;
  13.      double minLat = extents.Southwest.Latitude;
  14.      double minLon = extents.Southwest.Longitude;
  15.      double maxLat = extents.Northeast.Latitude;
  16.      double maxLon = extents.Northeast.Longitude;
  17.      double curLat = minLat;
  18.      double curLon = minLon;
  19.      // ‘Scan’ the map in the Latitude direction increasing the Longitude value in each step
  20.      while (curLat <= maxLat)
  21.      {
  22.          //Starting from the ll
  23.          int nextLatInPixels; int nextLonInPixels;
  24.          int curLatInPixels; int curLonInPixels;
  25.          int curTileX; int curTileY;
  26.          double nextLat; double nextLon;
  27.          LatLongToPixelXY(curLat, curLon, Convert.ToInt32(demoMap.ZoomLevel), out curLatInPixels, out curLonInPixels);
  28.          nextLatInPixels = curLatInPixels + 256;
  29.          nextLonInPixels = curLonInPixels – 256;
  30.          // Now convert them back to WGS84 coords
  31.          PixelXYToLatLong(nextLatInPixels, nextLonInPixels, Convert.ToInt32(demoMap.ZoomLevel), out nextLat, out nextLon);
  32.          PixelXYToTileXY(curLatInPixels, curLonInPixels, out curTileX, out curTileY);
  33.          string quadKey = TileXYToQuadKey(curTileX, curTileY, Convert.ToInt32(demoMap.ZoomLevel));
  34.          DrawQuadkeys(quadKey, curLat, curLon, nextLat, nextLon);
  35.          curLat = nextLat;
  36.      }
  37.      //Reset values
  38.      curLat = minLat;
  39.      curLon = minLon;
  40.      // ‘Scan’ the map in the Longitude direction increasing the Longitude value in each step
  41.      while (curLon <= maxLon)
  42.      {
  43.          int nextLatInPixels; int nextLonInPixels;
  44.          int curLatInPixels; int curLonInPixels;
  45.          int curTileX; int curTileY;
  46.          double nextLat; double nextLon;
  47.          LatLongToPixelXY(curLat, curLon, Convert.ToInt32(demoMap.ZoomLevel), out curLatInPixels, out curLonInPixels);
  48.          nextLatInPixels = curLatInPixels + 256;
  49.          nextLonInPixels = curLonInPixels – 256;
  50.          // Now convert them back to WGS84 coords
  51.          PixelXYToLatLong(nextLatInPixels, nextLonInPixels, Convert.ToInt32(demoMap.ZoomLevel), out nextLat, out nextLon);
  52.          PixelXYToTileXY(curLatInPixels, curLonInPixels, out curTileX, out curTileY);
  53.          string quadKey = TileXYToQuadKey(curTileX, curTileY, Convert.ToInt32(demoMap.ZoomLevel));
  54.          DrawQuadkeys(quadKey, curLat, curLon, nextLat, nextLon);
  55.          curLon = nextLon;
  56.      }
  57.      //Reset values
  58.      curLat = minLat;
  59.      curLon = minLon;
  60.      // ‘Scan’ the map diagonally increasing both Lat & Lon in each step
  61.      while (curLon <= maxLon && curLat <= maxLat)
  62.      {
  63.          int nextLatInPixels; int nextLonInPixels;
  64.          int curLatInPixels; int curLonInPixels;
  65.          int curTileX; int curTileY;
  66.          double nextLat; double nextLon;
  67.          LatLongToPixelXY(curLat, curLon, Convert.ToInt32(demoMap.ZoomLevel), out curLatInPixels, out curLonInPixels);
  68.          nextLatInPixels = curLatInPixels + 256;
  69.          nextLonInPixels = curLonInPixels – 256;
  70.          // Now convert them back to WGS84 coords
  71.          PixelXYToLatLong(nextLatInPixels, nextLonInPixels, Convert.ToInt32(demoMap.ZoomLevel), out nextLat, out nextLon);
  72.          PixelXYToTileXY(curLatInPixels, curLonInPixels, out curTileX, out curTileY);
  73.          string quadKey = TileXYToQuadKey(curTileX, curTileY, Convert.ToInt32(demoMap.ZoomLevel));
  74.          DrawQuadkeys(quadKey, curLat, curLon, nextLat, nextLon);
  75.          curLat = nextLat;
  76.          curLon = nextLon;
  77.      }
  78.  }
  79.  ///<summary>
  80.  /// Draws the Quadkeys as rectangles on the map
  81.  ///</summary>
  82.  ///<param name=”quadkey”></param>
  83.  ///<param name=”minlat”></param>
  84.  ///<param name=”minlon”></param>
  85.  ///<param name=”maxlat”></param>
  86.  ///<param name=”maxlon”></param>
  87.  public void DrawQuadkeys(string quadkey, double minlat, double minlon, double maxlat, double maxlon)
  88.  {
  89.      //Get the Quadkeys layer
  90.      MapLayer l = (MapLayer)demoMap.FindName(“quadkeys”);
  91.      //Create polygon
  92.      LocationCollection locs = new LocationCollection();
  93.      locs.Add(new GeoCoordinate(minlat, minlon));
  94.      locs.Add(new GeoCoordinate(maxlat, minlon));
  95.      locs.Add(new GeoCoordinate(maxlat, maxlon));
  96.      locs.Add(new GeoCoordinate(minlat, maxlon));
  97.      MapPolygon mp = new MapPolygon();
  98.      mp.Locations = locs;
  99.      mp.Stroke = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
  100.      mp.StrokeThickness = 2;
  101.      // Add polygon
  102.      l.Children.Add(mp);
  103.      var avgLat = mp.Locations.Select(q => q.Latitude).Average();
  104.      var avgLon = mp.Locations.Select(q => q.Longitude).Average();
  105.      // Add quadkey label
  106.      TextBox qLabel = new TextBox();
  107.      qLabel.Text = quadkey;
  108.      l.AddChild(qLabel, new GeoCoordinate(avgLat, avgLon));
  109.  }

Now compile and run the app again. This time you should be able to see the tiles appearing as red rectangles every time you pan and zoom the map with the quadkey displaying inside as in the image below.

image

Hope this example clears up a few things about Bing Maps and how they work. You can download the full application from http://www.box.com/s/ppovoyfbuiqf304qod8b

Enjoy coding! – PV

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