Archive for December, 2010

December 26, 2010

Examining 3D Terrain of Bing Maps Tiles with SQL Server 2008 and WPF (Part 2)

In the last post, I downloaded some elevation data from the GTOPO digital elevation model and loaded it into a SQL Server table. In this post, I’m now going to select a subset of that data in order to create a terrain map of a particular Bing Maps tile.

The Bing Maps Tile System

As you probably already know, Bing Maps are constructed from a set of raster “tiles” – prerendered 256px x 256px gif, jpg, or png images that form the background of each map. There are different sets of tiles that correspond to the different styles of map.

Here’s a few examples:

r120200223312

Road Map Tile

h120200223312

Aerial Map Tile

r120200223312

Ordnance Survey Map Tile

Every tile is numbered with a quadkey – a unique identifier that exactly describes the position and zoom level of where that tile should be placed. I’m not going to explain the quadkey system here, because there’s already an excellent article on MSDN.

The important fact is that, by knowing only the quadkey of a tile, it is possible to determine the exact coordinate boundaries of every corner of that tile. In SQL Server, that means we can construct a POLYGON that represents the extent of that tile, and retrieve any data that lies on the tile (or, in this example, the elevation data that corresponds to the terrain of that tile). The following method demonstrates how to return a SqlGeography polygon from a Bing Maps quadkey:

public static SqlGeography GeographyPolygonFromQuadKey(string quadKey) {

      int tilex = 0, tiley = 0;

      int tilewidth = 256, tileheight = 256;

      int zoomLevel = quadKey.Length;

      // Work out the x and y (grid) position of this tile

      for (int i = zoomLevel; i > 0; i--)

      {

        int mask = 1 << (i - 1);

        switch (quadKey[zoomLevel - i])

        {

                case '0':

                    break;

                case '1':

                    tilex |= mask;

                    break;

                case '2':

                    tiley |= mask;

                    break;

                case '3':

                    tilex |= mask;

                    tiley |= mask;

                    break;

                default:

                    throw new ArgumentException("Invalid QuadKey digit sequence.");

            }

        }

 

        // From the grid position and zoom, work out the min and max Latitude / Longitude values of this tile

        float minLongitude = (float)(tilex * tilewidth) * 360 / (float)(tilewidth * Math.Pow(2, zoomLevel)) - 180;

        float maxLatitude = (float)Math.Asin((Math.Exp((0.5 - (tiley * tileheight) / (tileheight) / Math.Pow(2, zoomLevel)) * 4 * Math.PI) - 1) / (Math.Exp((0.5 - (tiley * tileheight) / 256 / Math.Pow(2, zoomLevel)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;

        float maxLongitude = (float)((tilex + 1) * tilewidth) * 360 / (float)(tilewidth * Math.Pow(2, zoomLevel)) - 180;

        float minLatitude = (float)Math.Asin((Math.Exp((0.5 - ((tiley + 1) * tileheight) / (tileheight) / Math.Pow(2, zoomLevel)) * 4 * Math.PI) - 1) / (Math.Exp((0.5 - ((tiley + 1) * tileheight) / 256 / Math.Pow(2, zoomLevel)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;

 

        // Create a new SqlGeography instance representing the extent of this tile

        SqlGeographyBuilder gb = new SqlGeographyBuilder();

        gb.SetSrid(4326);

        gb.BeginGeography(OpenGisGeographyType.Polygon);

        gb.BeginFigure(minLongitude, maxLatitude);

        gb.AddLine(minLongitude, minLatitude);

        gb.AddLine(maxLongitude, minLatitude);

        gb.AddLine(maxLongitude, maxLatitude);

        gb.AddLine(minLongitude, maxLatitude);

        gb.EndFigure();

        gb.EndGeography();

 

      return gb.ConstructedGeography;

    }

 

Now, I just need to choose a particular tile whose terrain to examine…. I’ve chosen the tile at zoom level 6 that portrays most of Scotland – my ancestral home and also home to some interesting geographic features (i.e. mountains and lochs!). Here’s the tile viewed using Bing Maps’ road map style:

r031133

The quadkey for this tile is 031133 and, using the method above, I can calculate the corresponding SqlGeography as:

POLYGON((-5.625 55.7765730186677, 0 55.7765730186677, 0 58.8137417157078, –5.625 58.8137417157078, -5.625 55.7765730186677))

Therefore, we can retrieve the relevant GTOPO30 elevation data for this tile from SQL Server using the following query:

SELECT * FROM W020N90 WHERE geog4326.STIntersects(geography::STPolyFromText(‘POLYGON((-5.625 55.7765730186677, 0 55.7765730186677, 0 58.8137417157078, –5.625 58.8137417157078, -5.625 55.7765730186677))’, 4326) = 1;

Here’s the results:

image

In the next post, I’ll use this elevation data to create a 3d surface onto which to overlay the Bing Maps tile image.

Tags: , , ,
December 17, 2010

Examining 3D Terrain of Bing Maps Tiles with SQL Server 2008 and WPF (Part 1)

Following on from my last last post, I thought I’d continue to explore the topic of altitude (or elevation) and how it relates to Bing Maps.

A map is, by definition, two-dimensional – it is an image that has been projected onto a flat surface. However, it is still possible, and on many occasions very important, to portray three-dimensional properties of features on that map. One way of doing this is by presenting an oblique, “birdseye” view rather than a pure top-down view. This is certainly an effective way to present the height of individual distinct features such as buildings, as in the default Bing Maps control. But what about displaying variations in height of the underlying terrain? In this first of a set of posts I’ll look at a way of combining altitude data, SQL Server, and WPF to create a “3D” view of the elevation of terrain on a Bing Maps tile.

Obtaining Altitude Data

Bing Maps does not expose altitude data, so the first step is to acquire this data from somewhere else. There have been various scientific surveys of global elevation data, produced by government agencies and scientific organisations such as NASA. Three common sets of data are as follows:

  • GTOPO30 – global elevation grid at resolution of 30 arcseconds (approx 1km).
  • SRTM – elevation grid at resolution 3 arcseconds (approx 90m) between latitudes –56 and +60.
  • ASTER – elevation grid with resolution 1 arcsecond (approx 30m) between latitudes –83 and 83.

Elevation Webservices

The data from various elevation surveys is exposed via webservices provided by the USGS, Geonames and Google (amongst others). For example, to determine the altitude of the point located at (53.06839056444848, -4.076281785964964), you could call one of the following URLs

These services are very useful if you want to retrieve the elevation of only one, or a small number of points. However, for this example I’m going to use several thousand elevation points spread out across the surface of a Bing Maps tile, so these services aren’t really suitable.

Elevation Datasets

Each dataset is also available to download in full. I’ll be using the GTOP30 dataset, which you can download from http://eros.usgs.gov/#/Find_Data/Products_and_Data_Available/gtopo30_info. It’s split into several smaller tiles, so if you only want to download a particular area that’s fine. Tiles are named according to the coordinates at the top left corner. I’m going to use the W020N90 tile, which starts at a longitude of –20 and latitude of 90, and covers most of Western Europe, as highlighted below:

image

The W020N90 data is provided as a gzip archive, which is about 8.6Mb in size.

Converting and Loading into SQL Server

The GTOPO data is supplied as a DEM image, so the next step is to convert it into a more useable format. I’m going to use the excellent free MicroDEM program to crop a section of the W020N90 DEM file and save it as ASCII text file.

Converting the DEM Binary file to ASCII XYZ

  1. Load the DEM data into MicroDEM by selected File —> Open, and selecting the HDR header file from the GTOPO download.
  2. Once the data has loaded, use the Subset & zoom tool from the MicroDEM menu bar to select the part of the map to crop (if desired).
  3. Select File –> Save DEM –> Current subset, MD DEM.
  4. Close the current file and re-open the newly saved cropped file.
  5. Save the cropped file in text format by selecting File –> Save DEM –> ASCII XYZ.

image

Tidying Up the ASCII XYZ file

Once saved out as a ASCII XYZ file, the elevation data is stored as a very simple dataset with three columns as follows:

LAT        LONG        ELEVATION

52.837501  -8.479166   55
53.670834  -8.479166   77

The only slightly annoying thing about the XYZ export from MicroDEM is that there isn’t a consistent separator used to delimit the columns – it seems that any length of space is used as a separator. To fix this, I then opened up Visual Studio and performed a quick replace (Ctrl + H) using regular expressions:

  • First, to remove leading spaces on some of the lines, search for ^[ \t]+ and replace with an empty string.
  • Then, to replace the variable-length spaces between columns, search for [ ][ ]* and replace with ,

We’ve now got a nice comma-separated CSV file that is easy to import into SQL Server:

LAT,LONG,ELEVATION

52.837501,-8.479166,55
53.670834,-8.479166,77

Importing into SQL Server

Once you’ve imported the CSV file into a new table in SQL Server (Use the import/export wizard), we can add a geography column to that table, and populate it with a Point in each row. Since we want our Points to contain Z values, we have to use one of the WKT static methods (the geography Point() method, the xxxFromWKB() and GeomFromGml() methods can only be used to create two-dimensional coordinates):

ALTER TABLE W020N90

ADD GEOG4326 geography;

 

UPDATE W020N90

SET GEOG4326 = geography::STPointFromText('POINT(' + LONG + ' ' + LAT + ' ' + ELEVATION + ')', 4326);

(note that WKT requires longitude/latitude/altitude coordinate ordering, not latitude/longitude/altitude as you might expect).

At this stage, you can check the GTOPO elevation point data (well, the first 5,000 rows of it, anyway) by executing a SELECT query from the SQL Server table and checking the Spatial Results tab in SQL Server Management Studio, as shown below:

image

In the next post, I’ll examine how you can convert this table of points into a set of polygonal faces that can be used as the basis for a 3D terrain model for any particular geographic area.

Tags: , , ,
December 12, 2010

Pushpin Altitude and the Birdseye Map Style (Bing Maps v7)

For the inaugural post in my new blog, I thought I’d cover one of the (many) issues that seems to have been generating some confusion following the release of the new v7 AJAX API – namely how to set altitude on a pushpin location, and the effects of doing so.

Before I get into any code, it’s worth making a couple of important observations about altitude:

- Although it may seem obvious, changing altitude will have no effect on the display of a pushpin if you’re using a two-dimensional, top-down map style.

- In contrast, when displaying maps in three-dimensional view (including oblique “birdseye” modes), considering altitude becomes essential in order to correctly position a pushpin in the display.

Under the old v6.x map control, you only generally needed to worry about setting the altitude of your pushpins if you were explicitly using the birdseye or (now deprecated) 3d mode. If you didn’t use those modes, you could set the EnableBirdseyeMode and showSwitch map options to false to prevent your users from accessing the birdseye and 3d map styles respectively, and then use only the road and aerial “top-down” styles, comfortably ignoring any implications of altitude. Every pushpin therefore needed only a latitude and longitude coordinate.

In version 7, the situation is slightly different for two reasons: firstly, there does not currently appear to be any way of limiting the range of map styles available – any style can be chosen from the main menu bar by the end user, including birdseye. Not only that, but the default Automatic map style is now a hybrid style that smoothly transitions from two-dimensional top-down road and aerial imagery at distant zoom levels into “tilted” three-dimensional birdseye at close zoom levels. The effect of these changes is that all Bing Maps developers should now consider what effect altitude has on the way their pushpins are displayed using the different map styles in v7.

Now, for an example: the following code listing creates two pushpins. The first, whose location is specified using only latitude and longitude coordinates, is positioned at the default ground level, at the base of Nelson’s column in Trafalgar Square. The second is positioned exactly 50m above the first, and is specified using both the altitude and the altitudereference of the location.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

  <title></title>

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

  <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>

  <script type="text/javascript">

    function GetMap() {

      var map = new Microsoft.Maps.Map(document.getElementById("mapDiv"),

        { credentials: "BINGMAPSKEY",

          center: new Microsoft.Maps.Location(51.5077007450027, -0.12793064117432637 ),

          zoom: 19

        });

      var NelsonsColumnBottom = new Microsoft.Maps.Pushpin(

        new Microsoft.Maps.Location(51.50772, -0.1279303)

      );

      var NelsonsColumnTop = new Microsoft.Maps.Pushpin(

        new Microsoft.Maps.Location(51.50772, -0.1279303, 50, Microsoft.Maps.AltitudeReference.ground)

      );

      map.entities.push(NelsonsColumnBottom);

      map.entities.push(NelsonsColumnTop);

    }

  </script>

</head>

<body onload="GetMap();">

  <div id='mapDiv' style="position: relative; width: 1024px; height: 768px;">

  </div>

</body>

</html>

 

When viewing this map at zoom level 16, the Automatic map style chooses the flat 2d road map view, so the two pushpins are completely superimposed:

automatic_16

However, zoom in one level and the Automatic map style changes to enhanced birdseye view, so the two pushpins diverge to show the bottom and top of the column:

automatic_17

Since we are viewing at an oblique angle from the south, while the bottom pushpin (placed on the ground) appears to have remained in the same place the “top” pushpin appears to have shifted north up the map (whereas, in fact, it is directly above the bottom pushpin). Notice that the image above is taken at zoom level 17, which, in v6.x would still only be capable of showing a flat aerial map (the old “birdseye” mode only being available at the very highest level of zoom).

The enhanced birdseye mode in v7 that replaces the old “aerial” mode still demonstrates perceivable 3d and distinction between these two pushpin locations even when significantly zoomed out. The following image demonstrates that there is still a slight perceivable difference between the location of the pushpins at the top and bottom of Nelson’s column when viewed in birdseye mode at zoom level 14:

automatic_14

Personally, I think the enhanced birdseye style is great since you get the best of both worlds: the traditional aerial imagery view combined with an added sense of depth perception from the slightly oblique angle. However, many developers used to dealing only with the purely top-down “aerial” view of 6.x might not have thought of some of the implications that the new map styles create on the clarity of pushpin location. The Bing Maps default, if no altitude is specified, is to place a pushpin at terrain height of that location (i.e. on the ground). However, there appear to be cases where the underlying terrain elevation data is incorrect, in which case pushpins are incorrectly placed above or below the correct ground height. When viewed obliquely, this can make it appear that they are then misplaced laterally…. to demonstrate what I mean by this, consider the following image:

nelsoncolumn_downingst

At first glance, this map might appear to be highlighting two street locations; one in Trafalgar Square and 10 Downing Street. However, it’s actually showing the same points as used earlier in this post – both placed at exactly the same latitude/longitude. The only difference is that this time the top point has been raised to an altitude of 500m. When viewed from this orientation, and combined with the expectation that both points lie on the same level, may lead a viewer to erroneously conclude that the two pushpins lie at different positions on the ground. So, if your pushpins appear to be displayed in the wrong place, be sure to check not only the latitude/longitude values, but also that you’ve specified the correct altitude.

Note: There is currently an error on the MSDN documentation concerning the location class. http://msdn.microsoft.com/en-us/library/gg427612.aspx should describe the property that determines the basis for stating an altitude of a location as altitudeReference, not altitudeMode.

December 11, 2010

Blog v.Next

I’ve come to the conclusion that I’m just not a natural blogger. Like many things in life, I start enthusiastically, keep it up for a few months, and then lose interest and move onto the next project. And then every now again something comes along that makes me want to start again.

In this case, it was yesterday’s Microsoft UK MVP open day. The bunch of guys and gals I spent the day with yesterday are not only some of the best technical brains in the country, but they’re great teachers too – they’re generous with their time, patient and clear in their explanations, and generally a very nice bunch. I’m honoured and slightly humbled to be able to say that I’m a member of this group of dedicated individuals, and I feel I ought to do my bit to “earn” that title.

So, this blog will gather various thoughts, ramblings, code samples etc. about those technical subjects that interest me and where I think I’ve got something of value to share back with the development community – probably focussed around SQL Server and Bing Maps, but may include smatterings of Wikipedia / Open Street Map / Google Earth / C# / PHP / Drupal and whatever else takes my fancy at the time.

Here’s hoping this version lasts longer than my previous blog attempts….

Follow

Get every new post delivered to your Inbox.

Join 53 other followers