Dienstag, 15. Januar 2013

How to use Google Maps in your own Windows Phone App


When Google pulled the plug for it’s maps for all Windows Phone users two weeks ago, a loud fuss went trough the loyal Windows Phone user and developer base. A couple of days later Google found the plug again and since then we’re able to use the web version of Google Maps from our Windows Phones again.
The discussion around the whole map provider thing reminded me about a an implementation I did a while back for Windows Phone 7. I developed an app that displayed various map provider instead of the provided Bing Maps. It changed the base layer and allowed to display basically all base layer that support a standard URL scheme. Google Maps, Bing Maps and OpenStreetMaps are just a few examples. In this post I would like to show you how to use the Maps control in Windows Phone 8 and change the underlying base layer from Bing Maps to a different one.
Unfortunately this explanation has to start with a disclaimer. In Windows Phone 7 the shipped map control was using Bing Maps as a Base Layer and supported the change of the base layer. In Windows Phone 8 the story is a bit different as the Maps Control that ships with WP8 is a complete rewrite of the control from WP7 and does not support the change of the base layer at this point – although Microsoft said in a forum it’ll support it in a future version. That said, you can still use the “old” map control that has Bing Maps as a base layer in your Windows Phone 8 application. Keep in mind though, that the control is deprecated.

Create a new project


First of all create a new Windows Phone 8 project and add the old Maps control as a reference to your project. You can find the assembly in the WP SDK folder here C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v8.0\Libraries.

ReferenceWindow

Add maps control to view


The view, where you want to add the map, needs the following references in the header. Please pay attention to the correct paths as you need to reference the “old” map control and not the new one with the Nokia base layer.

xmlns:Microsoft_Phone_Controls_Maps="clr-namespace:Microsoft.Phone.Controls.Maps;assembly=Microsoft.Phone.Controls.Maps"
xmlns:MSPCMCore="clr-namespace:Microsoft.Phone.Controls.Maps.Core;assembly=Microsoft.Phone.Controls.Maps"
view raw gistfile1.txt hosted with ❤ by GitHub

The next step is to add the actual map control to the view, by adding the following xaml into your view.
<Microsoft_Phone_Controls_Maps:Map
Name="MyMap"
Grid.Column="1"
LogoVisibility="Collapsed"
d:LayoutOverrides="GridBox"
Margin="0,0,0,2">
<Microsoft_Phone_Controls_Maps:Map.Mode>
<MSPCMCore:MercatorMode/>
</Microsoft_Phone_Controls_Maps:Map.Mode>
</Microsoft_Phone_Controls_Maps:Map>
view raw gistfile1.xml hosted with ❤ by GitHub


By default the map control supports all the known features of a map control like navigating the map, zooming and pinching, and displays the Bing Map as a base layer.

Implement custom base layer as TileSource


In order to change the underlying base layer and add a custom one, we need to create a custom TileSource that maps the request from the control to the Google Maps URL Scheme. The Map control cannot load all images for all zoom level for a given position. Therefore the layer is divided into map tiles. The control calculates how many map tiles it needs to display the current map section and requests each map tile individually from the base map provider. In our case GetUri gets called for each map tile with different values for x and y. The zoom level stays constant for a given section of the map as the zoom level is the same.

using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace MapApp
{
public enum GoogleType
{
Street = 'm',
Hybrid = 'y',
Satellite = 's',
Physical = 't',
PhysicalHybrid = 'p',
StreetOverlay = 'h',
WaterOverlay = 'r'
}
public class Google : TileSource
{
public Google()
{
MapType = GoogleType.Street;
UriFormat = @"http://mt{0}.google.com/vt/lyrs={1}&z={2}&x={3}&y={4}";
}
public GoogleType MapType { get; set; }
public override Uri GetUri(int x, int y, int zoomLevel)
{
return new Uri(
string.Format(UriFormat, (x % 2) + (2 * (y % 2)),
(char)MapType, zoomLevel, x, y));
}
}
}
view raw gistfile1.cs hosted with ❤ by GitHub


Special binding for base layer


The last piece is to swap the base map provider from Bing Maps to Google Maps. To accomplish this, I’ve created a special binding class that binds to a TileSource implementation on the ViewModel. This way it is very easy to change the base map provider again, as we only need to implement a new TileSource and change the type of the bound property on the ViewModel to the new implementation.
The Binding Helper is listed below.

using Microsoft.Phone.Controls.Maps;
using System.Windows;
namespace MapApp
{
public static class BindingHelpers
{
//Used for binding a single TileSource object to a Bing Maps control
#region TileSourceProperty
// Name, Property type, type of object that hosts the property, method to call when anything changes
public static readonly DependencyProperty TileSourceProperty =
DependencyProperty.RegisterAttached("TileSource", typeof(TileSource),
typeof(BindingHelpers), new PropertyMetadata(SetTileSourceCallback));
// Called when TileSource is retrieved
public static TileSource GetTileSource(DependencyObject obj)
{
return obj.GetValue(TileSourceProperty) as TileSource;
}
// Called when TileSource is set
public static void SetTileSource(DependencyObject obj, TileSource value)
{
obj.SetValue(TileSourceProperty, value);
}
//Called when TileSource is set
private static void SetTileSourceCallback(object sender, DependencyPropertyChangedEventArgs args)
{
var map = sender as Map;
var newSource = args.NewValue as TileSource;
if (newSource == null || map == null) return;
// Remove existing layer(s)
for (var i = map.Children.Count - 1; i >= 0; i--)
{
var tileLayer = map.Children[i] as MapTileLayer;
if (tileLayer != null)
{
map.Children.RemoveAt(i);
}
}
var newLayer = new MapTileLayer();
newLayer.TileSources.Add(newSource);
map.Children.Add(newLayer);
}
#endregion
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

To bind to the map, please change the xaml in the view to the following.

<Microsoft_Phone_Controls_Maps:Map
Name="MyMap"
Grid.Column="1"
LogoVisibility="Collapsed"
d:LayoutOverrides="GridBox"
MapAppScope:BindingHelpers.TileSource="{Binding GoogleMap}"
Margin="0,0,0,2">
<Microsoft_Phone_Controls_Maps:Map.Mode>
<MSPCMCore:MercatorMode/>
</Microsoft_Phone_Controls_Maps:Map.Mode>
</Microsoft_Phone_Controls_Maps:Map>
view raw gistfile1.cs hosted with ❤ by GitHub

Make sure you create a ViewModel, bind that to your View and have a property called GoogleMap in your ViewModel that is of type Google – the type of the custom TileSource implementation.
In our implementation we choose Google Street as map type. Google offers Satellite, Hybrid and others as well. You can change those by swapping the map type in the constructor of the Google Map TileSource implementation.

Google_Screenshot1Google_Screenshot2Google_Screenshot3

Adding another base layer


Google Maps is not the only publisher of Web Mapping Service (WMS). Openstreetmap offers a service as well. To integrate openstreetmap as a base layer, use the implementation below and change the used TileSource in the ViewModel.

using Microsoft.Phone.Controls.Maps;
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace MapApp
{
public class Mapnik : TileSource
{
public Mapnik()
{
UriFormat = "http://{0}.tile.openstreetmap.org/{1}/{2}/{3}.png";
}
private readonly static string[] TilePathPrefixes = new[] { "a", "b", "c" };
public override Uri GetUri(int x, int y, int zoomLevel)
{
if (zoomLevel > 0)
{
var url = string.Format(UriFormat, TilePathPrefixes[y % 3], zoomLevel, x, y);
return new Uri(url);
}
return null;
}
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

The map control now displays openstreetmap tiles.

Mapnik1Mapnik2Mapnik3

6 Kommentare:

  1. Do you know if it is possible to use your own images from isostorage within Windows 8 map control TileSource? It was not possible on WP7.
    The idea is to provide offline functionalities. Who knows... maybe in-app map purchasing ;)

    AntwortenLöschen
    Antworten
    1. Hi Pablo, since the control I am using in this post is the 'old' control from Bing Maps Control from 7.1 you can't load images from isolated storage.

      The new maps control that ships with WP8 does not allow to use a custom TileSource unfortunately.

      Löschen
  2. I would be interested to hear about loading from isostore as well.

    AntwortenLöschen
    Antworten
    1. Hi Steve, since the control I am using in this post is the 'old' control from Bing Maps Control from 7.1 you can't load images from isolated storage as this control doesn't allow to load from isostore.

      The new maps control that ships with WP8 does not allow to use a custom TileSource unfortunately.

      Löschen
  3. This article may be of interest:
    http://kodira.de/2013/01/offline-maps-with-windows-phone-8/

    AntwortenLöschen