With the release of the Bing Maps REST Elevations service I started looking into cool and interesting things that can be done with the service. While doing some searching around, I stumbled across an interesting blog post titled Examining 3D Terrain of Bing Maps Tiles with SQL Server 2008 and WPF by one of our Bing Maps MVP’s which inspired me to see if I could make something similar using this new Elevations service. So with that, I’ve put together this blog posts which demonstrates how to create a tool for generating a 3D model of elevation data and then overlay static imagery over the top. As a teaser, here is a screenshot of a 3D model of Niagara Falls created using this code.
Setting up the Visual Studio Project
To start, we will create a WPF Application project in Visual Studios called BingMaps3DModel_WPF. Once this is done we will want to add references to the following libraries:
- System.Runtime.Serialization
- Microsoft.Maps.MapControl.WPF
Adding support for the REST based Elevation service
Since we will be accessing the Bing Maps REST Elevation service from .NET code we will need to add in a library to parse the responses from the service. Rather than writing these from scratch I’ll be making use of some code I helped put together in a previous MSDN article on using Bing Maps REST Service with .NET Libraries. To include this library into the project we will right click on the project and select Add -> New Item. Add a class file called BingMapsRESTServices.cs. Remove any content that’s in this file and copy and paste in the complete code from the bottom of the previous blog post. At this point your project should look something like this:
Creating the User Interface
For this application we will want to have two tabs. The first tab will have a map that the user will be able to use to select what area the 3D model should be created for. Once the user has selected the area they are interested in they will be able to press a button to generate the 3D model. Once the model is created the user will be taken to the second tab which will allow the user to view and interact with the 3D model. To make things a bit cleaner we will create a separate user control for the 3D models tab. To do this, right click on the project and select Add -> New Item. Select “User Control (WPF)” and call it ViewerPanel3D.xaml.
With this, we can create the markup for the MainWindow.xaml file. The XAML should look like this.
<Windowx:Class="BingMaps3DModel_WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
xmlns:local="clr-namespace:BingMaps3DModel_WPF"
Title="3D Map Generator"Height="700"Width="800">
<Grid>
<TabControlName="MyTabs">
<TabItemHeader="Map">
<Grid>
<m:MapName="MyMap"CredentialsProvider="YOUR_BING_MAPS_KEY"Mode="AerialWithLabels"/>
<ButtonContent="Generate 3D Map Model"Click="Generate3DModel_Click"
Width="150"Height="25"Margin="10"
HorizontalAlignment="Right"VerticalAlignment="Top"/>
</Grid>
</TabItem>
<TabItemHeader="3D Model">
<local:ViewerPanel3Dx:Name="viewerPanel"/>
</TabItem>
</TabControl>
</Grid>
</Window>
If you run the application now, it should result in an application that looks like this:
Note: you may get an error if you haven’t created an instance of the click event for the button. Simply right click on the event name and press “Navigate to Event Handler” to generate the click event code that is needed.
The ViewerPanel3D User Control
We can now turn our attention to the ViewerPanel3D user control that we created and add the needed markup to that. When a model is loaded into this control we will want to give the user several sliders which they can use to rotate and move the model around with. I came across a really nice example in this blog post that I thought I would use as a starting point. The nice thing about this example is that it binds the sliders to the rotation transform which means there is no code needed in the background for this functionality. In addition to the sliders for rotating the model we will add a set of sliders for translating (moving in different directions) the model. We will also add a mouse wheel event for zooming in and out of the model. Putting this all together the markup for the ViewerPanel3D.xaml file should look like this:
<UserControlx:Class="BingMaps3DModel_WPF.ViewerPanel3D"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300"d:DesignWidth="300">
<UserControl.Resources>
<StyleTargetType="{x:Type TextBlock}">
<SetterProperty="HorizontalAlignment"Value="Center"/>
<SetterProperty="VerticalAlignment"Value="Center"/>
</Style>
<Stylex:Key="slider">
<SetterProperty="Slider.Orientation"Value="Vertical"/>
<SetterProperty="Slider.Height"Value="130.0"/>
<SetterProperty="Slider.HorizontalAlignment"Value="Center"/>
<SetterProperty="Slider.VerticalAlignment"Value="Center"/>
</Style>
</UserControl.Resources>
<GridBackground="Gray"MouseWheel="OnViewportMouseWheel">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinitionWidth="100"/>
</Grid.ColumnDefinitions>
<Viewport3DName="viewport"Grid.Row="0"Grid.Column="0">
<Viewport3D.Camera>
<PerspectiveCamerax:Name="camera"FarPlaneDistance="50"
NearPlaneDistance="0"LookDirection="0,0,-10"UpDirection="0,1,0"
Position="0,0,5"FieldOfView="45">
<PerspectiveCamera.Transform>
<Transform3DGroup>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Axis="1.0, 0.0, 0.0"
Angle="{Binding ElementName=sliderX, Path=Value}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Axis="0.0, 1.0, 0.0"
Angle="{Binding ElementName=sliderY, Path=Value}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Axis="0.0, 0.0, 1.0"
Angle="{Binding ElementName=sliderZ, Path=Value}"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D
OffsetX="{Binding ElementName=transSliderX, Path=Value}"
OffsetY="{Binding ElementName=transSliderY, Path=Value}"
OffsetZ="{Binding ElementName=transSliderZ, Path=Value}"/>
<ScaleTransform3D
ScaleX="{Binding ElementName=sliderZoom, Path=Value}"
ScaleY="{Binding ElementName=sliderZoom, Path=Value}"
ScaleZ="{Binding ElementName=sliderZoom, Path=Value}"/>
</Transform3DGroup>
</PerspectiveCamera.Transform>
</PerspectiveCamera>
</Viewport3D.Camera>
<ModelVisual3D>
</ModelVisual3D>
<ModelVisual3Dx:Name="model">
<ModelVisual3D.Content>
<Model3DGroupx:Name="group">
<AmbientLightColor="DarkGray"/>
<DirectionalLightColor="DarkGray"Direction="10,10,5"/>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
<StackPanelGrid.Column="1"Width="100"Background="LightGray">
<GroupBoxHeader="Rotation"Margin="4.0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlockText="X"Grid.Column="0"Grid.Row="0"/>
<TextBlockText="Y"Grid.Column="1"Grid.Row="0"/>
<TextBlockText="Z"Grid.Column="2"Grid.Row="0"/>
<Sliderx:Name="sliderX"Grid.Column="0"Grid.Row="1"Minimum="0.0"Maximum="360.0"Value="230"Style="{StaticResource slider}">
<Slider.ToolTip>
<TextBlockText="Rotate around X-Axis"/>
</Slider.ToolTip>
</Slider>
<Sliderx:Name="sliderY"Grid.Column="1"Grid.Row="1"Minimum="-180.0"Maximum="180.0"Style="{StaticResource slider}">
<Slider.ToolTip>
<TextBlockText="Rotate around Y-Axis"/>
</Slider.ToolTip>
</Slider>
<Sliderx:Name="sliderZ"Grid.Column="2"Grid.Row="1"Minimum="-180.0"Maximum="180.0"Style="{StaticResource slider}">
<Slider.ToolTip>
<TextBlockText="Rotate around Z-Axis"/>
</Slider.ToolTip>
</Slider>
</Grid>
</GroupBox>
<GroupBoxHeader="Translate"Margin="4.0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlockText="X"Grid.Column="0"Grid.Row="0"/>
<TextBlockText="Y"Grid.Column="1"Grid.Row="0"/>
<TextBlockText="Z"Grid.Column="2"Grid.Row="0"/>
<Sliderx:Name="transSliderX"Grid.Column="0"Grid.Row="1"Minimum="-10"Maximum="10"Style="{StaticResource slider}">
<Slider.ToolTip>
<TextBlockText="Translate along the X-Axis"/>
</Slider.ToolTip>
</Slider>
<Sliderx:Name="transSliderY"Grid.Column="1"Grid.Row="1"Minimum="-10"Maximum="10"Style="{StaticResource slider}">
<Slider.ToolTip>
<TextBlockText="Translate along the Y-Axis"/>
</Slider.ToolTip>
</Slider>
<Sliderx:Name="transSliderZ"Grid.Column="2"Grid.Row="1"Minimum="-10"Maximum="10"Style="{StaticResource slider}">
<Slider.ToolTip>
<TextBlockText="Translate along the Z-Axis"/>
</Slider.ToolTip>
</Slider>
</Grid>
</GroupBox>
<GroupBoxHeader="Zoom"Margin="4.0">
<Sliderx:Name="sliderZoom"IsDirectionReversed="True"Minimum="0.01"Maximum="1"Value="0.8"Style="{StaticResource slider}"/>
</GroupBox>
</StackPanel>
</Grid>
</UserControl>
using System.Windows.Controls;
using System.Windows.Input;
namespace BingMaps3DModel_WPF
{
/// <summary>
/// Interaction logic for ViewerPanel3D.xaml
/// </summary>
publicpartialclass ViewerPanel3D : UserControl
{
public ViewerPanel3D()
{
InitializeComponent();
}
privatevoid OnViewportMouseWheel(object sender, MouseWheelEventArgs e)
{
sliderZoom.Value -= (double)e.Delta / 1000;
}
}
}
Generating the Model
Having a nice panel for viewing the model is a good start but doesn’t really do us much good without having a 3D model to view. To create the 3D model we will need to do the following:
(1) Get a static map image for the based on the center point and zoom level of the map. To keep things easy, we will make keep the width and height of the image equal to 800 pixels.
(2) Based on the center point, zoom level and imagery size we will then need to calculate the bounding box of the image as we will need it to request the elevation data.
(3) Make a request for the elevation data for the bounding box we created. Again, to keep things simple we will specify that the data points be evenly distributed over 30 rows and columns. This will result in 900 elevation data points being returned which is under the 1000 elevation data point limit.
(4) We need to loop through all the elevation data, calculate the relative coordinate, and then convert this coordinate to a pixel coordinate. We will also need to convert the elevation into a pixel length and then scale these values down relative to the size of the map.
(5) Now we can create a 3D Mesh Geometry out of the data. To do this, we will need to specify all the data points as 3 dimensional coordinates, and then specify the texture coordinates used to map the static map image to the mesh. We will also need to specify the point indices used to create the triangles needed for the mesh.
(6) As a final step, we will create a Geometry Model out of the 3D Mesh and set the static image as the material to be overlaid on top of it. This model can then be passed into our ViewerPanel3D user control.
Most of the math used to work with the pixel coordinates are based off of these two articles: Bing Maps Tile System, and VE Imagery Service and Custom Icons. Putting all the above tasks together and adding them to the MainWindow.xaml.cs file you should end up with a code for the MainWindow.xaml file that looks like this:
using System;
using System.IO;
using System.Net;
using System.Runtime.Serialization.Json;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using BingMapsRESTService.Common.JSON;
namespace BingMaps3DModel_WPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
publicpartialclass MainWindow : Window
{
#region Private Properties
privatestring sessionBingMapsKey;
private GeometryModel3D mGeometry;
privateconstdouble mapSize = 800;
privatedouble topLeftX;
privatedouble topLeftY;
privatedouble zoom;
#endregion
#region Constructor
public MainWindow()
{
InitializeComponent();
MyMap.CredentialsProvider.GetCredentials((x) =>
{
sessionBingMapsKey = x.ApplicationId;
});
}
#endregion
#region Button Event Handler
privatevoid Generate3DModel_Click(object sender, RoutedEventArgs e)
{
double cLat = MyMap.Center.Latitude;
double cLon = MyMap.Center.Longitude;
//Round off the zoom level to the nearest integer as an integer zoom level is needed for the REST Imagery service
zoom = Math.Round(MyMap.ZoomLevel);
//Only generate models when the user is zoomed in at a decent zoom level, otherwise the model will just be a flat sheet.
if (zoom < 8)
{
MessageBox.Show("This zoom level is not supported. Please zoom in closer (>8).");
return;
}
//Clear current model from the viewer panel
if (mGeometry != null)
{
viewerPanel.group.Children.Remove(mGeometry);
}
//Open up the 3D model tab
MyTabs.SelectedIndex = 1;
//Calculate bounding box of image for specified zoom level and center point for map dimensions
//Retrieve image of map dimensions for the specified zoom level and center point.
string imgUrl = string.Format("http://dev.virtualearth.net/REST/v1/Imagery/Map/Aerial/{0},{1}/{2}?mapSize={3},{4}&key={5}",
cLat,
cLon,
zoom,
mapSize,
mapSize,
sessionBingMapsKey);
ImageBrush imgBrush = new ImageBrush();
imgBrush.ImageSource = new BitmapImage(new Uri(imgUrl));
DiffuseMaterial material = new DiffuseMaterial(imgBrush);
//calcuate pixel coordinates of center point of map
double sinLatitudeCenter = Math.Sin(cLat * Math.PI / 180);
double pixelXCenter = ((cLon + 180) / 360) * 256 * Math.Pow(2, zoom);
double pixelYCenter = (0.5 - Math.Log((1 + sinLatitudeCenter) / (1 - sinLatitudeCenter)) / (4 * Math.PI)) * 256 * Math.Pow(2, zoom);
//calculate top left corner pixel coordiates of map image
topLeftX = pixelXCenter - (mapSize / 2);
topLeftY = pixelYCenter - (mapSize / 2);
//Calculate bounding coordinates of map view
double brLongitude, brLatitude, tlLongitude, tlLatitude;
PixelToLatLong(new System.Windows.Point(900, 800), out brLatitude, out brLongitude);
PixelToLatLong(new System.Windows.Point(0, 0), out tlLatitude, out tlLongitude);
//Retrieve elevation data for the specified bounding box
//Rows * Cols <= 1000 -> Let R = C = 30
string elevationUrl = string.Format("http://dev.virtualearth.net/REST/v1/Elevation/Bounds?bounds={0},{1},{2},{3}&rows=30&cols=30&key={4}",
brLatitude,
tlLongitude,
tlLatitude,
brLongitude,
sessionBingMapsKey);
WebClient wc = new WebClient();
wc.OpenReadCompleted += (s, a) =>
{
using (Stream stream = a.Result)
{
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Response));
Response response = ser.ReadObject(stream) as Response;
if (response != null&&
response.ResourceSets != null&&
response.ResourceSets.Length > 0 &&
response.ResourceSets[0] != null&&
response.ResourceSets[0].Resources != null&&
response.ResourceSets[0].Resources.Length > 0)
{
ElevationData elevationData = response.ResourceSets[0].Resources[0] as ElevationData;
//Map elevation data to 3D Mesh
MeshGeometry3D mesh = new MeshGeometry3D();
double dLat = Math.Abs(tlLatitude - brLatitude) / 30;
double dLon = Math.Abs(tlLongitude - brLongitude) / 30;
double x, y, m2p;
for (int r = 0; r < 30; r++)
{
y = tlLatitude + (dLat * r);
for (int c = 0; c < 30; c++)
{
int idx = r * 30 + c;
x = tlLongitude + (dLon * c);
double z = -elevationData.Elevations[idx];
m2p = 156543.04 * Math.Cos(y * Math.PI / 180) / Math.Pow(2, zoom);
System.Windows.Point p = LatLongToPixel(y, x);
p.X = (p.X - 400) / mapSize;
p.Y = (p.Y + 400) / mapSize;
mesh.Positions.Add(new Point3D(p.X, p.Y, z / mapSize / m2p));
mesh.TextureCoordinates.Add(p);
//Create triangles for model
if (r < 29 && c < 29)
{
mesh.TriangleIndices.Add(idx);
mesh.TriangleIndices.Add(idx + 1);
mesh.TriangleIndices.Add(idx + 30);
mesh.TriangleIndices.Add(idx + 1);
mesh.TriangleIndices.Add(idx + 31);
mesh.TriangleIndices.Add(idx + 30);
}
}
}
//Add 3D mesh to view panel
mGeometry = new GeometryModel3D(mesh, material);
mGeometry.Transform = new Transform3DGroup();
viewerPanel.group.Children.Add(mGeometry);
}
}
};
wc.OpenReadAsync(new Uri(elevationUrl));
}
#endregion
#region Helper Methods
private System.Windows.Point LatLongToPixel(double latitude, double longitude)
{
//Formulas based on following article:
//http://msdn.microsoft.com/en-us/library/bb259689.aspx
//calculate pixel coordinate of location
double sinLatitude = Math.Sin(latitude * Math.PI / 180);
double pixelX = ((longitude + 180) / 360) * 256 * Math.Pow(2, zoom);
double pixelY = (0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI)) * 256 * Math.Pow(2, zoom);
//calculate relative pixel cooridnates of location
double x = pixelX - topLeftX;
double y = pixelY - topLeftY;
returnnew System.Windows.Point((int)Math.Floor(x), (int)Math.Floor(y));
}
privatevoid PixelToLatLong(System.Windows.Point pixel, outdouble lat, outdouble lon)
{
double x = topLeftX + pixel.X;
double y = topLeftY + pixel.Y;
lon = (x * 360) / (256 * Math.Pow(2, zoom)) - 180;
double efactor = Math.Exp((0.5 - y / 256 / Math.Pow(2, zoom)) * 4 * Math.PI);
lat = Math.Asin((efactor - 1) / (efactor + 1)) * 180 / Math.PI;
}
#endregion
}
}
Here is a screenshot of my finished application with the generated 3D model of a section of the Grand Canyon. Nice, right?
- Ricky Brundritt, EMEA Bing Maps Technology Solution Professional