Welcome to EMC Consulting Blogs Sign in | Join | Help

Tim James' Blog

3D maps on WP7

Theres lots of good sources for map tiles on the internet like Bing maps and OpenStreet maps. This short sample will show you how map tiles from these sources on to a 3D globe and will provide a good visualization of the distortion issues caused by the Mercator transform.

There are a few simple steps to get from this…

map

To this…

globe

Each tile in both Bing maps and OpenStreet maps (OSM from now on) has a x, y and z coordinate, where x and y is its position relative to the top left of the map and z is the zoom level. In this sample, were only going to work at a single zoom level, download all the globes tiles for that zoom level and then map them on to a sphere. Ill focus on OSM tiles, but it exactly the same for bing tiles. OSM is easier to deal with because we do not need to get an API key to use it.

Let have a look at out tile class.

 public class Tile
    {
        public int X { get; private set; }
        public int Y { get; private set; }
        public int Zoom { get; private set; }
        public Tile Parent;

        public bool HasChildren { get { return this.Children != null; } }
        public Tile[] Children { get; private set; }

        public Tile(int x, int y, int zoom)
        {
            this.X = x;
            this.Y = y;
            this.Zoom = zoom;
        }

        public static Tile FromLocation(Location location, int zoom)
        {
            double n = Math.Pow(2, zoom);
            double latRad = location.Latitude*Math.PI/180;
            double x = ((location.Longitude + 180)/360)*n;
            double y = ((1 - (Math.Log(Math.Tan(latRad) + 1/Math.Cos(latRad))/Math.PI))/2)*n;
            return new Tile()
                       {
                           X = (int)x,
                           Y = (int)y,
                           Zoom = zoom
                       };
        }

        public static Location GetLocation(int zoom, int x, int y)
        {
            double n = Math.Pow(2, zoom);
            double lon = (x/n)*360 - 180;
            double lat = Math.Atan(Math.Sinh(Math.PI*(1 - 2*y/n)))*180/Math.PI;
            return new Location(lon, lat);
        }

        public Location[] GetCorners()
        {
            var corners = new Location[4];
            corners[0] = GetLocation(Zoom, X + 1, Y + 1);
            corners[1] = GetLocation(Zoom, X, Y + 1);
            corners[2] = GetLocation(Zoom, X + 1, Y);
            corners[3] = GetLocation(Zoom, X, Y);
            return corners;
        }

        public void Split()
        {
            Tile[] subtiles = new Tile[4];

            subtiles[0] = new Tile(2 * X, 2 * Y, Zoom + 1);
            subtiles[1] = new Tile(2 * X + 1, 2 * Y, Zoom + 1);
            subtiles[2] = new Tile(2 * X, 2 * Y + 1, Zoom + 1);
            subtiles[3] = new Tile(2 * X + 1, 2 * Y + 1, Zoom + 1);
            this.Children = subtiles;
        }

        public override string ToString()
        {
            return string.Format("X: {0} Y: {1} Zoom: {2}", X, Y, Zoom);
        }
    }

Theres a fair amount of cool here. First off, lets look at some maths. The GetLocation() method is the cornerstone of this sample. It coverts a tile XYZ too a location, or Latitude Longitude pair. This gives us the real world location of the tiles top left corner. GetCorners() takes this one step further, it returns an array of the locations for all 4 corners on tile. This is all we need to generate geometry!

    public class Geometry
    {
        public List<VertexPositionNormalTexture> Verticies;
        public Texture2D Texture;
        public Tile Tile;
    }

Heres are geometry class, this hold a vertex buffer and a texture needed to draw a single tile. To fill the vertex buffer we need to translate the 4 corner positions of each tile from being latitude longitude pairs to 3D coordinates…. which turns out to be way easier than it sounds! We plug them a angles in to the sphere equation to get there location on the sphere surface. This is handled in the location class.

    public class Location
    {
        public const double EarthRadiusInKm = 6378.137;

        public double Latitude { get; set; }
        public double Longitude { get; set; }

        public Location(double lat, double lon)
        {
            this.Latitude = lat;
            this.Longitude = lon;
        }

        public Vector3 Position()
        {
            var omega = (MathHelper.Pi / 180.0) * this.Longitude;
            var phi = (MathHelper.Pi / 180.0) * this.Latitude;
            var r = EarthRadiusInKm;

            Vector3 v = Vector3.Zero;
            v.X = (float)(r * Math.Cos(phi) * Math.Sin(omega));
            v.Z = (float)(r * Math.Cos(phi) * Math.Cos(omega));
            v.Y = (float)(r * Math.Sin(phi));
            
            return v;
        }

        public override string ToString()
        {
            return string.Format("{0}:{1}, {2}:{3}", this.Latitude > 0 ? "N" : "S", this.Latitude, this.Longitude > 0 ? "E" : "W", this.Longitude);
        }
    }

and now we tie that up into a vertex buffer like so:

private void CreateTiles()
{
    int tilecount = (int)Math.Pow(2, ZoomLevel);
    tiles = new List<Tile>();
    for (int y = 0; y < tilecount; y++)
    {
        for (int x = 0; x < tilecount; x++)
        {
            var t = new Tile()
                        {
                            X = x,
                            Y = y,
                            Zoom = ZoomLevel
                        };
            tiles.Add(t);

            LoadTexture(t);

            CreateGeometory(t);

            Console.WriteLine("loaded z:" + ZoomLevel + " x:" + x + " y:" + y);
        }
    }
}

private void LoadTexture(Tile t)
{
    WebClient webClient = new WebClient();
    byte[] buffer = webClient.DownloadData(source.GetTileUrl(t));
    var stream = new MemoryStream(buffer);

    textures[t] = Texture2D.FromStream(GraphicsDevice, stream);
}

public void CreateGeometory(Tile tile)
{
    Geometry geometry = new Geometry();
    geometry.Tile = tile;
    geometry.Texture = textures[tile];

    var locations = tile.GetCorners();
    geometry.Verticies = new List<VertexPositionNormalTexture>()
                             {
                                 new VertexPositionNormalTexture(locations[0].ToVector3(0), Vector3.Normalize(locations[0].ToVector3(0)), new Vector2(1,1)),
                                 new VertexPositionNormalTexture(locations[1].ToVector3(0), Vector3.Normalize(locations[1].ToVector3(0)), new Vector2(0,1)),
                                 new VertexPositionNormalTexture(locations[2].ToVector3(0), Vector3.Normalize(locations[2].ToVector3(0)), new Vector2(1,0)),
                                 new VertexPositionNormalTexture(locations[3].ToVector3(0), Vector3.Normalize(locations[3].ToVector3(0)), new Vector2(0,0)),
                             };

    this.geoms.Add(geometry);
}

Here we are only using two triangles for each tile so the resulting globe will be really low polly and angular, its easy to sort this out with a bit of sub diving.

And there you have it, a quick and dirty way to download tiles and map them to a sphere. One last thought to leave you with.
This exercise can really give you a good look at how distorted the top and bottom of a world map can get. Its something we have to live with if we want square maps of a sperical object.

globe

in this screenshot each rectangular face on the globe is a single map tile. It quite obvious how little space the tiles at the top of the globe take up hardly any surface space compared to the
equatorial ones.

Helpful links:

Bing maps tilesystem helper: http://msdn.microsoft.com/en-us/library/bb259689.aspx

OSM Tile names: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

Published 06 July 2011 14:02 by tim.james

Comments

No Comments
Anonymous comments are disabled
Powered by Community Server (Personal Edition), by Telligent Systems