May 2017

Volume 32 Number 5

[Modern Apps]

Deep Dive into Map Control

By Frank La

Frank La VigneIn my column last month, I demonstrated the basic features of the Bing Maps Control for Universal Windows Platform (UWP) apps and how easy it is to add a map to your UWP apps. This month I’ll show you how to leverage some of the more advanced features that will make your apps really stand out and provide great experiences for your users. I’ll also explore how to embed the rich imagery and 3D experience of Bing Maps directly into your apps.

Getting Started

For an in-depth look at how to add the Map control and obtain a MapServiceToken, please refer to my column last month (msdn.com/magazine/mt797655). For the purposes of this column, create a new blank UWP project in Visual Studio by choosing New Project from the File menu. Then expand to Installed Templates | Windows | Blank App (Universal Windows). Name the project DCTourismMap and then click OK. Immediately afterward, a dialog box will appear asking you which version of Windows the app should target. For this project, the default options will be fine, so you can simply click OK. In the MainPage.xaml file, add the following namespace declaration to the Page tag:

xmlns:maps="using:Windows.UI.Xaml.Controls.Maps"

Next, add the Map control to the page, by adding the following XAML in the Grid control on the page (be sure to insert the MapServiceToken value you received from the Bing Maps Portal at bingmapsportal.com):

<maps:MapControl x:Name="mapControl" MapServiceToken="{Insert Key Here}" >
</maps:MapControl>

You may now run the solution and you should see a Map control covering the entire screen of your app. With this control in place, you can now begin to take advantage of all that the Bing Maps control for UWP has to offer.

Custom Map Markers

In my last column, I wrote about adding markers to the map. The markers simply pinpoint a location on a map. However, the UWP Map control also lets developers place customized XAML controls onto a map and, along with XAML controls, comes enhanced interactivity. The goal is to create a map that showcases various landmarks in Washington, D.C. The markers on the map will each have an image of the location and will bring up a Web site about it when you click on it. In the end, the map should look something like Figure 1.

Map with Customized Button Controls Placed at Specified Location
Figure 1 Map with Customized Button Controls Placed at Specified Location

To accomplish this, the Map control will need a MapItemsControl, a DataTemplate, and a few other attributes added. First, modify the Map control XAML in the MainPage.xaml to look like what’s shown in Figure 2.

Figure 2 Modified Map Control XAML in MainPage.xaml

<maps:MapControl x:Name="mapControl" Loaded="mapControl_Loaded"
  MapServiceToken="[Insert Key Here]">
  <maps:MapItemsControl x:Name="sitesMapItemsControl" >
    <maps:MapItemsControl.ItemTemplate>
      <DataTemplate>
        <Button Click="itemButton_Click"
          maps:MapControl.Location="{Binding Location}" >
          <StackPanel>
            <Border Background=
              "{ThemeResource ApplicationPageBackgroundThemeBrush}">
              <TextBlock Text="{Binding Name}"/>
            </Border>
            <Image Source="{Binding ImageUri}" Width="100" Height="50"
              Stretch="UniformToFill">
            </Image>
          </StackPanel>
        </Button>
      </DataTemplate>
    </maps:MapItemsControl.ItemTemplate>
  </maps:MapItemsControl>
</maps:MapControl>

Read the XAML carefully and note that the MapItemsControl DataTemplate binds to an object with several properties. The next step is to create a model that contains those properties. In Solution Explorer, right-click on the project, click on Add, and then click on New Folder in the subsequent menu. Name the folder Models and hit enter. Right-click on the Models folder, click Add, and then choose New Item. In the subsequent dialog box, choose Class from the list of templates. Name the new class file POI.cs and click OK (POI stands for points of interest).

Modify the contents of the POI.cs file to resemble the following code:

using System;
using Windows.Devices.Geolocation;
namespace DCTourismMap.Model
{
  public class POI
  {
    public string Name { get; set; }
    public Geopoint Location { get; set; }
    public Uri ImageUri { get; set; }
    public Uri InformationUri { get; set; }
  }
}

Next, open the MainPage.xaml.cs file and add the event handler shown in Figure 3 to center the map on Washington, D.C., and set a zoom level that focuses on the area around the National Mall. The LoadPointsOfInterest method creates and populates a List<POI>. The last line of the mapControl_Loaded method sets the ItemsSource of the MapItemsControl to this list.

Figure 3 Event Handler to Center Map on Washington, D.C., and Create List to Populate Points of Interest

private void mapControl_Loaded(object sender, RoutedEventArgs e)
{
  this.mapControl.Center = new Geopoint(
    new BasicGeoposition() { Latitude = 38.889906, Longitude = -77.028634 });
  this.mapControl.ZoomLevel = 16;
  sitesMapItemsControl.ItemsSource = LoadPointsOfInterest();
}
private List<POI> LoadPointsOfInterest()
{
  List<POI> pointsOfInterest = new List<POI>();
  pointsOfInterest.Add(new POI()
  {
    Name ="Washington Monument",
    ImageUri= new Uri(
      "https://upload.wikimedia.org/wikipedia/commons/e/ea/
      Washington_October_2016-6_%28cropped%29.jpg"),
    InformationUri = new Uri("https://www.nps.gov/wamo/index.htm"),
    Location = new Geopoint(
      new BasicGeoposition() { Latitude = 38.8895, Longitude = -77.0353 })
  });
pointsOfInterest.Add(new POI()
{
  Name = "White House",
  ImageUri = new Uri(
    "https://www.publicdomainpictures.net/pictures/20000/nahled/white-house.jpg"),
  InformationUri = new Uri("https://www.whitehouse.gov"),
  Location = new Geopoint(
    new BasicGeoposition() { Latitude = 38.897734, Longitude = -77.036535 })
});
pointsOfInterest.Add(new POI()
{
  Name = "The US Capitol",
  ImageUri = new Uri(
    "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/US_
    Capitol_west_side.JPG/320px-US_Capitol_west_side.JPG"),
  InformationUri = new Uri("https://www.capitol.gov/"),
  Location = new Geopoint(
    new BasicGeoposition() { Latitude = 38.889892, Longitude = -77.009341 })
});
return pointsOfInterest;
}

Adding Interactivity

Now, add the following event handler code for the button control specified in the DataTemplate:

private void itemButton_Click(object sender, RoutedEventArgs e)
{
var buttonSender = sender as Button;
POI poi = buttonSender.DataContext as POI;
Launcher.LaunchUriAsync(poi.InformationUri);
}

The event handler retrieves the POI object associated with the control that triggered the event and hands off the InformationURI property to the LaunchURIAsync method of the Launcher class. For URIs of Web sites, this will launch the default browser on the system and load the URI sent to it. The Launcher class resides in the Windows.System namespace. You have the option of adding the namespace manually with a using statement or letting Visual Studio handle that detail. The Launcher class can do much more than simply open a browser. You can read more about the Launcher class at bit.ly/2n4Zx0F.

Run the solution now and it should look like Figure 1. Clicking on any of the markers will bring up a Web site about the landmark.

Maps in Three Dimensions

One of the more appealing aspects of the Bing Maps UWP control is its ability to render 3D images of many locations around the world. To get started, create a new project using the steps detailed in the Getting Started section. This time, however, name the solution 3DMaps. Be sure to add the namespace declaration to the MainPage.xaml file to include the maps namespace. Insert the XAML shown in Figure 4 in the Page node of MainPage.xaml to the Map control and a series of buttons to control the 3D view of the map.

Figure 4 XAML to Create the Interface for the 3D Map App

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid.RowDefinitions>
    <RowDefinition Height="30*"/>
    <RowDefinition Height="80*"/>
    <RowDefinition Height="549*"/>
  </Grid.RowDefinitions>
  <StackPanel Orientation="Horizontal"  Grid.Row="0">
    <Button Name="btn3DView" Content="3D View" Click="btn3DView_Click" />
  </StackPanel>
  <StackPanel Orientation="Vertical" Grid.Row="1">
    <TextBlock FontWeight="Bold">Heading: </TextBlock>
    <TextBlock Text="{Binding ElementName=mapControl,Path=Heading,
      Mode=OneWay}"></TextBlock>
    <TextBlock FontWeight="Bold">Pitch: </TextBlock>
    <TextBlock Text="{Binding ElementName=mapControl,Path=ActualCamera.Pitch,
      Mode=OneWay}"></TextBlock>
  </StackPanel>
  <maps:MapControl x:Name="mapControl" Grid.Row="2"
    MapServiceToken="[Insert Token Here]">
  </maps:MapControl>
</Grid>

Manipulating 3D Maps

Viewing a map in 3D is simple and I touched on that briefly in my column last month. However, all the sample app from that column did was switch to 3D view. Now, let’s go over how to programmatically manipulate a 3D map. Add the event handler for the btn3Dview button, as shown in Figure 5.

Figure 5 Code to Switch to a 3D Map View Focused on Lower Manhattan

private async void btn3DView_Click(object sender, RoutedEventArgs e)
{
  if (this.mapControl.Is3DSupported)
  {
    this.mapControl.Style = MapStyle.Aerial3DWithRoads;
    BasicGeoposition nycDowntownLatLong = new BasicGeoposition()
    {
      Latitude = 40.712929,
      Longitude = -74.018291
    };
    double radius = 550;
    double heading = 90;
    double pitch = 80;
    MapScene nycDowntownScene = MapScene.CreateFromLocationAndRadius(
      new Geopoint(nycDowntownLatLong), radius, heading, pitch );
    await mapControl.TrySetSceneAsync(nycDowntownScene);
  }
}

Immediately, you’ll notice the code to switch the Map control Style property to Aerial3DWithRoads. Beyond that, I want to be able to control specific details about the view. In this example, I want to show the skyline of Lower Manhattan. In order to do this, I need to “frame a shot” very much in the same way a photographer would by picking a location in 3D space to set up a camera and point in a certain position and specified angle. The diagram in Figure 6 provides an overview.

Conceptual Diagram of a MapScene
Figure 6 Conceptual Diagram of a MapScene

To set up the desired picture, start with a latitude and a longitude to define the location. Then define variables for radius, heading and pitch to control the view of the camera. Next, create a MapScene using the MapScene.CreateFromLocationAndRadius method. This creates a scene centered on the specified latitude and longitude from a certain distance away in meters (radius). Heading refers to the direction in which the camera faces. This value can be anywhere from zero to 360, representing the compass points. For reference, a value of 90 represents East. A value of zero is North, 180 is South, and 270 is West. Pitch refers to the angle at which the camera is facing. Zero would be a top-down view and 90 would be perpendicular.

Once the MapScene object is created, it’s applied to the Map control by using the TrySetSceneAsync method. Run the project now. Click on 3D View and your app should show a 3D view of lower Manhattan. Note that you can control the map via mouse and keyboard. If your device supports touch, you may also use touch gestures to control the heading, zoom level and pitch of the Map control.

Although having the mouse, keyboard and gesture control built right into the Map control is good, it would be nice to manipulate the 3D viewport of the map programmatically.

Close the app and return to the code in the MainPage.xaml file in Visual Studio. Add the following XAML into the StackPanel control after the btn3DView button:

<Button Name="btnRotateCameraLeft" Content="Rotate Left"
  Click="btnRotateCameraLeft_Click" />
<Button Name="btnRotateCameraRight" Content="Rotate Right"
  Click="btnRotateCameraRight_Click" />
<Button Name="btnTiltCameraDown" Content="Tilt Down"
  Click="btnTiltCameraDown_Click" />
<Button Name="btnTiltCameraUp" Content="Tilt Up"
  Click="btnTiltCameraUp_Click" />

This XAML will create four buttons to control the viewport of the map. Two are for controlling heading: one to turn right and the other to turn left. Two are for controlling the pitch: one to tilt up and the other to tilt down. Now, add the event handlers, as shown in Figure 7, to the MainPage.xaml.cs file.

Figure 7 Event Handlers for UI Buttons that Control the MapScene Camera

private async void btnRotateCameraLeft_Click(object sender, RoutedEventArgs e)
{
  await mapControl.TryRotateAsync(-30);
}
private async void btnRotateCameraRight_Click(object sender, RoutedEventArgs e)
{
  await mapControl.TryRotateAsync(30);
}
private async void btnTiltCameraUp_Click(object sender, RoutedEventArgs e)
{
  await mapControl.TryTiltAsync(15);
}
private async void btnTiltCameraDown_Click(object sender, RoutedEventArgs e)
{
  await mapControl.TryTiltAsync(-15);
}

The methods TryRotateAsync and TryTiltAsync both take a parameter of type double that represents the angle to rotate or tilt, respectively. Negative values rotate left and tilt down. Positive values rotate right and tilt up. Run the app now. Click on 3D View and try out the control you now have over the map through the buttons. Your app should look something like Figure 8.

The App After Clicking Rotate Right Several Times
Figure 8 The App After Clicking Rotate Right Several Times

StreetSide View

One of the other powerful features of Bing Maps is the StreetSide View, which lets users explore a location through a series of interactive panoramas. With the UWP Map control, your apps can now have this feature embedded into them.

If the app is already running, stop it and return to Visual Studio. Add the following XAML in the MainPage.xaml file to add a button for enabling StreetSide View:

<Button Name="btnStreetSide" Content="StreetSide" Click="btnStreetSide_Click" />

Now add the event handler in Figure 9 to enable StreetSide View.

Figure 9 Setup and Display the StreetSide View of Lower Manhattan

if (mapControl.IsStreetSideSupported)
{
  BasicGeoposition nycDowntownLatLong = new BasicGeoposition()
  {
    Latitude = 40.712929,
    Longitude = -74.018291
  };
  Geopoint nycDowntownPoint = new Geopoint(nycDowntownLatLong);
  StreetSidePanorama panoramaNearDowntownNYC =
    await StreetSidePanorama.FindNearbyAsync(nycDowntownPoint);
  if (panoramaNearDowntownNYC != null)
  {
    var nycSSE = new StreetSideExperience(panoramaNearDowntownNYC);
    mapControl.CustomExperience = nycSSE;
  }
  }
}

The first step is to check whether the app is running on a device that supports StreetSide View, as not every device is capable of supporting it. The next step is to create a GeoPoint from a latitiude/longitude coordinate. Now, pass that GeoPoint to the FindNearbyAsync method to find a nearby location that has StreetSide imagery available. If no imagery near that location exists, the method will return null.

All that’s left to do is to create a StreetSide Experience by passing the StreetSide Panorama object to the StreetSide Experience constructor. To switch the Map control over to StreetSide View, set the Map control Custom Experience property to the newly created StreetSide Experience object.

Run the solution now and click on the StreetSide button. Click on Tilt Up and Tilt Down and the app should show the StreetSide View of the World Financial Center and Freedom Tower. Notice that the control responds to keyboard, mouse and touch. The control will even let you zoom in and explore the environment. Below the StreetSide View, there’s an overview map that lets users explore the area even further. In the process of using the app, you might have noticed that the Turn Left and Turn Right buttons no longer work.

Close the app and return to Visual Studio and modify the event handlers for the turn buttons. In order to change the heading of the StreetSide view, the Heading property of the Map control needs to be changed directly. To detect whether StreetSide View is enabled, check to see if the Custom Experience property isn’t null. Modify the event handlers for the Turn Left and Turn Right buttons so that they look like the code in Figure 10.

Figure 10 Updated Rotate Event Handlers to Support StreetSide View

private async void btnRotateCameraLeft_Click(object sender, RoutedEventArgs e)
{
  await mapControl.TryRotateAsync(-30);
  if (mapControl.CustomExperience != null)
  {
    mapControl.Heading = mapControl.Heading - 30;
  }
}
private async void btnRotateCameraRight_Click(object sender, RoutedEventArgs e)
{
  await mapControl.TryRotateAsync(30);
  if (mapControl.CustomExperience != null)
  {
    mapControl.Heading = mapControl.Heading + 30;
  }
}

Run the solution again and click on the StreetSide button to enable StreetSide View. The Turn Left and Turn Right buttons now work. It’s worth noting that, in 3D View, setting the Heading property of the Map control does, indeed, rotate the view of the camera. However, it does so without animating the points in between, making for a rough transition.

Wrapping Up

As mentioned in my column last month, maps are truly one of the more indispensable features of mobile devices. With the transition to digital maps, maps can become not just customizable, but interactive and immersive. In this column, you saw how the Map control included with the UWP provides these rich interactive features and 3D imagery. In most cases, access to the mapping services and imagery come at no cost to you, the developer.


Frank La Vigne is chief evangelist at DataLeader.io, where he helps customers leverage data science in order to seek actionable insights. He blogs regularly at FranksWorld.com and has a YouTube channel called Frank’s World TV (FranksWorld.TV).

Thanks to the following Microsoft technical expert for reviewing this article: Rachel Appel


Discuss this article in the MSDN Magazine forum