The maps control that comes with the Windows Phone SDK is quite nice and very easy to use as a developer. This is true as long as your app is always online. There are use case, though, that demand offline map usage. A perfect example is Tourschall which brings mobile audio guides to your smartphone. Users can download a guide and use it later on while traveling without the need of being online and therefore without potential roaming costs. This article shows how to tweak the Windows Phone map control to use offline map material.
Introduction
A Tourschall audio guide is self-contained in a way that it bundles all audio material as well as map data for a specific region. As a result, no internet connection is needed while using after you downloaded the guide. Map data is stored as a number if images, called tiles, directly on the phone. The default map control in Windows Phone works the same way, except that it downloads the tiles directly from the Bing Maps server while the user is pinching and zooming through the map.
Custom tile sources in Windows Phone 7
In Windows Phone 7 it was possible already to replace the default Bing Maps tile source with a custom one. Please have a look at Joost van Schaik’s great article explaining how to do that. The basic idea is to add another tiles source which downloads tiles from e.g. Openstreet Map. As a result, you can provide custom tiles to your app users. Unfortunately, the tile source still had to be online. Local url’s are not resolved by the map control. This is true for embedded resources in the XAP file as well as resources in the isolated storage.
The only workaround back than was to manually attach the local tile images to their specific position on the map which also demands for additional tile loading code and tile zooming with respect to zoom-gestures of the user.
Server Sockets in Windows Phone 8
Luckily, Microsoft introduced TCP server sockets within WP8 . This gives you another tool to realize offline maps. The basic idea here is to use a server socket to implement a local web server in your app. The web server will act as a local tile source which loads the tile images either directly from the XAP or from isolated storage. The map control, in turn, will request the tile data via a URL pointing to localhost. This can be achieved by introducing a corresponding custom tile source.
Stop talking! Show me the code!
The following steps are required in order to convince the map control to work with offline tiles.
1) Implement a local tile server
You can instantiate a TCP socket server by using a StreamSockerListener
. Moreover, you need to register a callback for received connections. You start the server by binding it to a port of your choice (in this case I used 33321).
this.server = new StreamSocketListener(); this.server.ConnectionReceived += ConnectionReceivedCallback; this.server.BindServiceNameAsync(33321);
The callback parses the HTTP request and triggers the preparation of the HTTP response. The given example code only checks if there has been a HTTP GET request. Otherwise, the server does not send a response at all.
async void ConnectionReceivedCallback(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) { DataReader reader = new DataReader(args.Socket.InputStream); reader.InputStreamOptions = InputStreamOptions.Partial; uint numStrBytes = await reader.LoadAsync(512); string request= reader.ReadString(numStrBytes); using (IOutputStream output = args.Socket.OutputStream) { string requestMethod = request.Split('\n')[0]; string[] requestParts = requestMethod.Split(' '); if (requestParts[0] == "GET") await SendResponse(requestParts[1], output); } }
The HTTP response method checks if the requested resource is available in isolated storage. If this is the case a corresponding response is created. Otherwise, the server replies with HTTP 404. Please be warned that for readability reasons the code given here, does not handle any exceptional cases in a way it should be in productive code.
private async Task SendResponse(string path, IOutputStream os) { using (IsolatedStorageFile myIsolatedStorage = IsolatedStorageFile.GetUserStoreForApplication()) { using (Stream resp = os.AsStreamForWrite()) { if (!myIsolatedStorage.FileExists(path)) { byte[] headerArray = Encoding.UTF8.GetBytes( "HTTP/1.1 404 Not Found\r\n" + "Content-Length:0\r\n" + "Connection: close\r\n\r\n"); await resp.WriteAsync(headerArray, 0, headerArray.Length); } else { using (IsolatedStorageFileStream fs = myIsolatedStorage.OpenFile(path, FileMode.Open, FileAccess.Read)) { string header = String.Format("HTTP/1.1 200 OK\r\n" + "Content-Length: {0}\r\n" + "Content-Type: image/png\r\n" + "Connection: close\r\n" + "\r\n", fs.Length); byte[] headerArray = Encoding.UTF8.GetBytes(header); await resp.WriteAsync(headerArray, 0, headerArray.Length); await fs.CopyToAsync(resp); } } await resp.FlushAsync(); } } }
Now, you should have a running TCP server socket emulating a very simple local HTTP server.
2) Copy the map tiles to isolated storage
First of all you need to download the tiles for the map area you are interested in. There are many tools out there which help you here. Please make sure that you do not violate any legal obligations. As a result you should have a number of tile images.
For uploading the tiles to isolated storage you can use the ISETool that is shipped with the Windows Phone SDK. You can also modify the server socket code to read the tiles from the XAP of course.
3) Implement a custom tile source
Again, I would like to reference the article of Joost van Schaik how to do that in general. In this case, though, the tile source should not point to a remote server but to your local tile server.
public override Uri GetUri(int x, int y, int zoomLevel) { return new Uri("http://127.0.0.1:33321/Map/" + zoomLevel + "/" + x + "/" + y + ".png ", UriKind.Absolute); }
The article assumes you are using the Silverlight maps control (Microsoft.Phone.Controls.Maps
). In WP8 this control is marked deprecated. As a replacement, there is a map control in Microsoft.Phone.Maps.Controls
which is quite similar. I am sure you will find out how to register your custom tile source with that map. The only hick up with this control is that there seems to be no way to disable the default Bing Maps tile source. As a result, you will see the default tiles and your new tiles loaded on top. If you know how to bypass this please let me know.
Conclusion
After following the presented steps and gluing together the given code, you should have a map using your local tiles and still offering the great Bing Maps experience with all the cross-fading and zooming you know and love.