Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Creating a 3D Topographical Map in HoloLens/Windows MR With Bing Maps Elevation API

DZone's Guide to

Creating a 3D Topographical Map in HoloLens/Windows MR With Bing Maps Elevation API

Want to learn how to make a 3D topographical map for your virtual reality device? Here's how in this tutorial on the Bing Maps Elevation API. Enjoy!

· IoT Zone ·
Free Resource

Introduction

All right, my friends, it's time for the real deal, the blog post I have been anticipating to write since I first published the 3D version of Walk the World two months ago. Unfortunately, it took me a while to extract comprehensible code from my rather convoluted app. Everyone who has ever embarked on a trip of 'exploratory programming' (meaning you have an idea of what you want to do, but only some vague clues about how) knows how you end up with a repo (and code) full of failed experiments, side tracks, etc. So, I had to clean that up a little first. Also, this app does a lot more than just show the map. As an added bonus, after creating this blog post, I finally began to understand, myself, how and, most importantly, why the app works.

So, without further ado, I am going to show you how to display a 3D map in your HoloLens or Windows MR headset.  I will build upon my previous post where I showed you how to make a flat, slippy map. This time, we are going 3D.

The General Idea

As you can read in my previous post, I 'paste' the actual map tiles — mere images — on a Unity3D Plane. A plane is a so-called mesh that exists out of a grid of 11x11 points and forms the vertices. If I somehow was able to ascertain the actual elevation on those locations, I would move those points up and down and actually get a 3D map. The tile itself will be stretched up and down. The idea of manipulating the insides of a mesh is actually very simple. It can be explained in greater depth by the awesome Rick Bazarra in the first episode of his must-see "Unity Strikes Back" explanatory video series on YouTube. This is a follow-up to his Creative Coding with Unity series on Channel 9. I consider this video to be a great starting point for everyone who wants to get off the ground with Unity3D.

So, where do we get those elevations? Enter the Microsoft service called the Bing Maps Elevation API. It seems to be built-to-order for this task. Your first order of business is to get yourself a Basic Bing Maps key.

Adding Some Geo-Intelligence to the Tile

The Bing Maps Elevation API documentation describes an endpoint GetElevations that allows you to get altitudes in several ways. One of them is a grid of altitudes in a bounding box. That is what we want because our tiles are square. The documentation says the bounding box should be specified as follows: "A bounding box defined as a set of WGS84 latitudes and longitudes in the following order: south latitude, west longitude, north latitude, east longitude."

If you envision a tile positioned so that north is up, we are required to calculate the geographical location of the top-right and bottom-left of the tile. The Open Street Maps Wiki provides code for the north-west corner of the tile, i.e. top left. I translated the code to C#...

//http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#C.23
private WorldCoordinate GetNorthWestLocation(int tileX, int tileY, int zoomLevel)
{
    var p = new WorldCoordinate();
    var n = Math.Pow(2.0, zoomLevel);
    p.Lon = (float)(tileX / n * 360.0 - 180.0);
    var latRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * tileY / n)));
    p.Lat = (float) (latRad * 180.0 / Math.PI);
    return p;
}


It works fine, but we need the south-west and northeast points. If you consider how the tiles are stacked, you can easily see how the northeast point is the north-west point of the tile right of our current tile, and the south-west point is the north-west point of the tile below our tile. Therefore, we can use the north-west points of those adjacent tiles to find the values we actually need, like this:

public WorldCoordinate GetNorthEast()
{
    return GetNorthWestLocation(X+1, Y, ZoomLevel);
}

public WorldCoordinate GetSouthWest()
{
    return GetNorthWestLocation(X, Y+1, ZoomLevel);
}


That was easy, right?

Size Matters

Although Microsoft is a USA company, fortunately, it has an international orientation so that the Bing Maps Elevation API returns no yards, feet, inches, miles, furlongs, stadia, or any other deprecated distance unit. It returns plain old meters. This is very fortunate, as the Windows Mixed Reality distance unit is — oh joy — meters too. But, it returns elevation in real-world values, and, while it might be fun to show Kilimanjaro in real height, it will be a bit too big to fit in my room (or any room, for that matter). Open Street Map is shown at a definite scale per zoom level. As a GIS guy, I like to be the height correctly scaled to gain a real-life feeling. Once again, referring to the Open Street Map Wiki, there is a nice table that shows how many meters a pixel is at any given zoom level. We will need the size per tile (which is 256 pixels, as I explained in the previous post), so we add the following code that will give you a scale factor for the available zoom levels for Open Street Map:

//http://wiki.openstreetmap.org/wiki/Zoom_levels
private static readonly float[] _zoomScales =
{
    156412f, 78206f, 39103f, 19551f, 9776f, 4888f, 2444f,
    1222f, 610.984f, 305.492f, 152.746f, 76.373f, 38.187f,
    19.093f, 9.547f, 4.773f, 2.387f, 1.193f, 0.596f, 0.298f
};

private const int MapPixelSize = 256;

public float ScaleFactor
{
    get { return _zoomScales[ZoomLevel] * MapPixelSize; }
}

Creating the Request

Now, we move to MapTile. We add the following code to download the Bing Maps Elevation API values:

private string _mapToken = "your-map-token-here";

public bool IsDownloading { get; private set; }

private WWW _downloader;

private void StartLoadElevationDataFromWeb()
{
    if (_tileData == null)
    {
        return;
    }
    var northEast = _tileData.GetNorthEast();
    var southWest = _tileData.GetSouthWest();

    var urlData = string.Format(
    "http://dev.virtualearth.net/REST/v1/Elevation/Bounds?bounds={0},{1},{2},{3}&rows=11&cols=11&key={4}",
     southWest.Lat, southWest.Lon, northEast.Lat, northEast.Lon, _mapToken);
    _downloader = new WWW(urlData);
    IsDownloading = true;
}


This simply queries the TileInfo structure for the new methods that we have just created. Notice it builds the URL, containing the bounds, the hard-coded 11x11 points that are in a Unity Plane, and the key. Then, it calls a piece of Unity3D code called "WWW" which is a sort of HttpClient. And, that's it. We add a call to the existing SetTileData  method like this:

public void SetTileData(TileInfo tiledata, bool forceReload = false)
{
    if (_tileData == null || !_tileData.Equals(tiledata) || forceReload)
    {
        TileData = tiledata;
StartLoadElevationDataFromWeb();  } }


This is so that whenever a tile data is supplied, it does not only initiate the downloading of the tile, but it initiates the downloading of the 3D data.

Processing the 3D Data

Next, we will look at a method ProcessElevationDataFromWeb that is called from Update (so about 60 times a second). In this method, we check if the MapTile is downloading, and if it's ready, we process the data,:

protected override void OnUpdate()
{
    ProcessElevationDataFromWeb();
}

private void ProcessElevationDataFromWeb()
{
    if (TileData == null || _downloader == null)
    {
        return;
    }

    if (IsDownloading && _downloader.isDone)
    {
        IsDownloading = false;
        var elevationData = JsonUtility.FromJson<ElevationResult>(_downloader.text);
        if (elevationData == null)
        {
            return;
        }

        ApplyElevationData(elevationData);
    }
}


An ElevationResult is a class to deserialize a result from a call to the Bing Maps Elevation API. I entered the result of a manual call in Json2CSharp and got a class structure back. However, I changed all properties into public fields so that the Json2CSharp can handle it. This is because the rather limited Unity JsonUtility does not seem to understand the concept of properties. I also initialized lists in the objects from the constructors. It's not very interesting, but if you want a look, look at this demo project.

Applying the 3D Data

Now, it's time to actually move the mesh points up and down. This is done using mostly code that I stole from Rick Bazarra, with a few adaptions:

private void ApplyElevationData(ElevationResult elevationData)
{
    var threeDScale = TileData.ScaleFactor;

    var resource = elevationData.resourceSets[0].resources[0];

    var verts = new List<Vector3>();
    var mesh = GetComponent<MeshFilter>().mesh;
    for (var i = 0; i < mesh.vertexCount; i++)
    {
        var newPos = mesh.vertices[i];
        newPos.y = resource.elevations[i] / threeDScale;
        verts.Add(newPos);
    }
    RebuildMesh(mesh, verts);
}

private void RebuildMesh(Mesh mesh, List<Vector3> verts)
{
    mesh.SetVertices(verts);
    mesh.RecalculateNormals();
    mesh.RecalculateBounds();
    DestroyImmediate(gameObject.GetComponent<MeshCollider>());
    var meshCollider = gameObject.AddComponent<MeshCollider>();
    meshCollider.sharedMesh = mesh;
}


First, we get the scale factor that's simply the value by which elevation data must be divided to make it match the current zoom level. Next, we get the elevation data itself, which is two levels down in the elevationData .

Then, we need to modify the elevation of the mesh points to match those with the elevation we got. The points will come in at exactly the right in order for Unity to process in the mesh. This is why I said it looks like the Bing Maps Elevation API and why it needs to be built-to-order for this task.

As I learned from Rick, you cannot modify the points of a mesh; you have to replace them. So, we loop through the mesh points and fill a list with points that have their y or vertical direction changed to a scaled value of the elevation. Then, we call  RebuildMesh that simply replaces the entire mesh with new vertices. It does some recalculation and rebuilds the collider. Now, your gaze cursor will actually play nice with the new mesh. I also noticed that if you don't do the recalculate stuff, you will end up looking partly through tiles. I am sure people with a deeper understanding of Unity3D will understand why. I just found out that it needs to be done, myself.

Don't press play, yet! There a few tiny things left to do so that the final result looks good.

Setting the Right Location and Material

First of all, the map is kind of shiny, which was more or less okay-ish for the flat map, but, if you turn the map into 3D, you will get a over-bright effect. So, open up the project in Unity, create a new material called " MapMaterial ," and apply the properties as displayed here below. The color of the material should be #BABABAFF (see top image.) When that is done, drag it on top of the MapTile (see bottom image). You can click on each image for improved resolution.


Now, the app is still looking at Redmond. While that's an awesome place, there isn't anything spectacular to see as far as geography is concerned. So, we mosey over to the MapBuilder script. There, we change the zoom level to 14, the Latitude to 46.78403, and the Longitude to -121.7543

It's a little east and quite a bit more south from Redmond. In fact, when you press play, you will see a landmark that is very familiar to the Seattle area, if you have ever lived or visited:

It looks like the famous Mount Rainier, a volcano sitting about 100 km from Seattle and very prominently visible from aircraft, with the weather permitting. To get this view, I had to fiddle with the view control keys a little bit after pressing play. If you press play, initially, you will see Rainier from much closer up.

And that, my friends, is how you make a 3D map in your HoloLens of almost any place in the world. Want to see Kilimanjaro? Change the Latitude to -3.21508 and Longitude to 37.37316. Then, press play.


Want to see Niagra falls? Latitude 43.07306, Longitude -79.07561, and change zoom level to 17. Rotate the view forward a little with your mouse and pull back. You have to look down. But, then, here you go.


GIS is cool, but 3D GIS is ultra-cool! All that is left is to generate the UWP app and deploy it into your HoloLens or Windows Mixed Reality device.

Caveat Emptor

Awesome, right? Now, there are a few things to consider. In my previous post, I said this app was a bandwidth hog since it downloads 169 tiles per map. In addition, it now also fires 169 requests per map to the Bing Maps Elevation API. And, this is for every single map, every time. Apart from the bandwidth and power consequences, there's another thing to consider. If you go to this page and click "Basic Key", you will see something like this:

What it boils down to is if your app is anywhere near successful, it will eat your allotted request limit very fast. Because of this, you will get a mail from the Bing Team, kindly informing you of this (been there, got that). Then, suddenly, you will have a 2D app again. You will have to buy an enterprise key, but those are not cheap. So, I advise you to do some caching. In both the app and, if possible, on an Azure service, I employed a Redis cache to that extent.

Furthermore, I calculated the northeast and southwest points of a tile using the north-west points of the tiles left of and below the current tile. If those tiles are not present, you are at the edge of the map. I have no idea what will happen, but, presumably, it won't work as expected. You can run into this when you are zoomed out in the very south or east of the map. But, then you are either at the International Date Line (that runs from the North Pole to the South Pole exactly on the side of the earth that is opposite of the Greenwich Meridian) or Antarctica. On the first spot, there's mostly ocean (why else do you think they've put it there?) and thus no (visible) geography to speak of.  As far as Antarctica goes, you'll hit another limitation, for it clearly says in the Bing Maps Elevation API documentation, "there is a limitation in that latitude coordinates outside of the range of -85 and 85 are not supported."

So, beware and stay away from the earth's edges. Your app might fall off!

Conclusion

Once you know the basics, it's actually pretty easy to create a 3D scaled map of about anywhere in the world, that is, anywhere where the Bing Maps Elevation API is supported. The 3D stuff is actually the easy part. However, knowing how to calculate tiles and build a slippy map is harder. But, in the end, it is always easy when you know what to do. Like I said, I think GIS is a premier field in which Mixed Reality will shine. I am ready for it, and I hope you are too. 

Get the demo project and get inspired!

Credits

Thanks to René Schulte, the wise man who pointed me to the Bing Maps Elevation API. And, thanks, of course, to Rick Bazarra, who inspired me and actually provided crucial code in his YouTube training series.

Topics:
iot ,hololens ,windows mr ,mixed reality ,ar ,augmented reality ,api ,unity 3d ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}