Creating a Windows 8 Metro Slippy Map Application

So (along with half the world, it seems), I grabbed myself a copy of the Windows 8 Developer Preview earlier in the week and have been trying to get to grips with it, both as a user and also with the interest of developing some applications.

I’m not going to go into detail about the (fairly radical) changes made in Windows 8, partly because many other people have already written about these, and partly because I’m very unqualified to do so – I’m finding things out myself as I go along. Instead, seeing as Windows 8 is all about the user interface and aimed at supporting touch-enabled devices, the main thing I was interested in was seeing what’s involved in developing a simple “Metro” application with a slippy map interface. This post is a summary of my initial investigations.

What’s Metro, anyway?

Metro is the (current) name of the new, touch-centric style that is exposed as the default interface in Windows 8. It’s certainly a radical departure from traditional Windows UI, and more resembles the interface you’d get on a mobile phone handset or other portable device. (This is not coincidence – the big growth market in the PC world at the moment lies not in desktops or laptops, but with tablet PCs, and it’s clear that Microsoft is designing Windows 8 to be run on devices that will “compete” against Apple’s iPad and Android tablets). Apart from the touch interface, there are other similarities as well – Metro applications are typically small in size, relatively simple, have a full-screen UI, are responsive (due to asynchronous method calls), and they can be packaged up and distributed via the “Windows Application Store” – a model much like Chrome’s web store, Android’s marketplace or Apple’s App Store.

image

So, I thought it would be nice to create a simple little Metro app that displayed a slippy map interface, possibly showed points of interest near to you (using the built-in geolocation), allowed you to plot routes etc. – fairly regular stuff. Since this is Microsoft Windows, after all, you’d think that this would be relatively easy to do with the Bing Maps API, right? Well….

Developing for Metro – Choose your Weapon

First things first – the core system component that powers Metro Apps is something called WinRT. WinRT contains APIs for handling devices, graphics, communications etc. that are exposed via COM interfaces. You can write Metro applications that reference these WinRT assemblies using a variety of languages – managed code (C#/VB) or unmanaged code (C/C++) with a XAML front-end, or using Javascript with an HTML/CSS front-end. The Windows 8 technology stack is summarised in this slide shown at the recent BUILD conference:

image

Bing Maps comes in lots of different flavours too, including (amongst others) a Silverlight control, a WPF control, and a Javascript control. Since WPF and Silverlight both make use of XAML for UI and managed code-behind, you might think that it would be relatively easy to port an application that uses the Bing Maps Silverlight/WPF control to Metro XAML and c# code-behind, but actually I’ve not heard of anybody successfully do this yet. Also, considering that the WPF Bing Maps control is still itself only in beta, and the Silverlight control has apparently been in stasis for several years, I certainly wasn’t about to commit any effort to making these work on Windows 8 when I’m not convinced about their future longevity….

Instead, I thought I’d opt for the HTML/Javascript approach – which are technologies I’m more familiar with anyway, and have a more mature associated Bing Maps AJAX control. However, even then, I’ve been surprised at the number of changes required to port a regular HTML/Javascript Bing Maps web application and get it to work in a Metro app.

HTML/Javascript in Metro – it’s all about Context

Most people think of HTML/Javascript as being exclusively used on the web – the two technologies being responsible for structure/client-side code of webpages respectively, and executed only within the context of a web browser. This, in itself, has historically made the Javascript execution environment somewhat sandboxed. Even though projects such as nodejs have shown that Javascript can also be used in other contexts, it has not been widely used for creating desktop applications before. (In a Windows context, Javascript is used for creating sidebar gadgets in Windows Vista/7,  but these have generally been little more than mini-webpages pinned to the desktop, and have little access to system resources)

In Metro, however, a Javascript application can be used to access core parts of the operating system (including the file system, networking, and other devices) by calling into the WinRT APIs. This sounds kind of dangerous, doesn’t it? Javascript, with its <script> injection, dynamic DOM manipulation et al. having access to your system resources? To minimise the risk of malicious code in this scenario, Windows 8 introduces different security contexts for Metro apps.

The main HTML landing page for a Metro app runs in the local context. It has access to the WinRT APIs, and can also do some other things not possible using Javascript running in a regular webbrowser, such as making cross-domain XmlHttpRequests. However, there are two main restrictions placed on pages running within the local context:

  • You can only load locally-packaged scripts. i.e. you can’t include a <script> tag whose src attribute points to a file on a remote server.
  • Any methods that attempt to change the DOM (i.e. by setting an element’s innerHTML or calling the document.write method) are sanitised.

Unfortunately, that means using the Bing Maps AJAX API is not possible from within a Metro app running within the local context, because the Bing Maps control needs to be referenced from a remote URL (in other words, you need to include the library using <script type=”text/javascript” src=”http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0></script&gt;), which is not allowed within the local context. Even if you created an offline copy of the Bing Maps AJAX library (which is not supported), it would still not work because of the way in which it manipulates the DOM of the HTML page.

So, what options do you have?

Option 1: Embed the Map Control in a iFrame using the Web Context

Although the main HTML page of a Metro app must run in the local context, you can have additional HTML pages that are loaded within the web context. When running in the web context, your HTML/JS code behaves almost exactly as it would do if loaded within a regular Internet Explorer browser – in other words, you lose the ability to tap into the WinRT APIs and other new Metro features, but you can include remote script references again as normal. For a full comparison of what’s possible in the local context and in the web context, refer to http://msdn.microsoft.com/en-us/library/windows/apps/hh465373%28v=VS.85%29.aspx

However, I wanted to embed a slippy map as the main frontend UI on my Metro App – I don’t want the user to have to navigate to a separate HTML page in the web context just to load the map. So, is there a way to get the best of both worlds – having elements loaded from within the web context displayed within the main page in the local context? One approach is to create a regular Bing Maps application in a separate HTML page in the web context, and then embed that in our main application in a separate iframe.

To do so, create an iframe in the main page in which the src element points to your Bing Map page, prefixed by  ms-wwa-web:/// to show that the content of the iframe is to be loaded in the web context. So, here’s a simple default.html page for a Metro App:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=1024, height=768" />
    <title>WinWebApp1</title>
    <!-- WinJS references -->
    <link rel="stylesheet" href="/winjs/css/ui-dark.css" />
    <script src="/winjs/js/base.js"></script>
    <script src="/winjs/js/wwaapp.js"></script>
    <script src="/winjs/js/ui.js"></script>
    <script src="/winjs/js/controls.js"></script>
    <!-- WinWebApp1 references -->
    <link rel="stylesheet" href="/css/default.css" />
    <script src="/js/default.js"></script>
</head>
<body>
    <header role="banner" aria-label="Header content">
        <div class="titleArea">
            <h1 class="pageTitle win-title" role="button" aria-label="Groups" tabindex="0">
                Metro Map App</h1>
        </div>
    </header>
    <div>
      <iframe id="mapIframe" src="ms-wwa-web:///map.html" width="1024px" height="400px"></iframe>
    </div>
</body>
</html>

and here’s the map.html page that is loaded within the web context of the iframe:

<!DOCTYPE html>
<html>
<head>
    <title>Map</title>
    <!-- We can include remote script because this page will be loaded in the web context (ms-wwa-web:///map.html)  -->
    <script src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0" type="text/javascript"></script>
    <script type="text/javascript">
        var map = null;

        function initialize() {
            var mapOptions = {
                credentials: "AkeAFl99ZABCDEFG7Kb2D12345678lRQm8vnZMpfMV7HsfNqwertyuiopd",
                center: new Microsoft.Maps.Location(52, 0),
                mapTypeId: Microsoft.Maps.MapTypeId.road,
                zoom: 7,
                showLogo: false,
                showDashboard: false
            };
            map = new Microsoft.Maps.Map(document.getElementById("map"), mapOptions);
        }
        document.addEventListener("DOMContentLoaded", initialize, false);
    </script>
    <style type="text/css">
        body, html
        {
            margin:0;
        }
        
        #map
        {
          position:absolute;
          width:100%;
          height:100%;
        }
    </style>
</head>
<body>
    <div id="map"></div>
</body>
</html>

Hit F5 to debug the app and you’ll get something like this – a regular slippy map interface using Bing Maps, contained in an iframe that seamlessly integrates into the container of your Metro App.

image

So far so good. But the problem with this approach is that, although visually it may appear integrated, the content of the iframe is essentially isolated from the main application (that, after all, is the whole reason why the iframe is allowed to be loaded in the web context – if it could directly access the WinRT APIs available to the parent application you’d be exposing the security risks that the whole local context scenario is meant to avoid). So how do we go about creating interface elements in the parent container page that update (or are updated by) elements in the map iframe?

You can communicate between the host application running in the local context and the map iframe running in the web context by using the HTML5 postMessage method. If you search the internet, you’ll find plenty of examples of postMessage, but they’re very simplistic – most of the examples involve sending a single text string from one window to another and, when it’s received, simply alerting that message to the user. But when you actually want to try to expose entire interfaces between iframe windows, you’ll quickly create a mess of code – the postMessage method is, after all, designed for…. posting messages… so any data passing between iframes must be passed as text. You then need to setup a messageHandler on the receiving window that will act appropriately based on the text data received.

For example, I set up a simple row of button controls in my container HTML page to pan the map and zoom in/out.

<button onclick="Pan(-1,0);">Pan Left</button>
<button onclick="Pan(1,0);">Pan Right</button>
<button onclick="Pan(0,1);">Pan Up</button>
<button onclick="Pan(0,-1);">Pan Down</button>
<button onclick="ZoomOut();">Zoom Out</button>
<button onclick="ZoomIn();">Zoom In</button>

The methods attached to these buttons do not pan or zoom the map directly. Rather, they create an instruction that will be sent via postMessage to the map iframe. Fortunately, the postMessage API implemented in Internet Explorer 10 (which is the engine in which Metro Apps running in the web context are executed) accepts postMessages either as single strings or as JSON objects. So,  the “instruction” passed to the map is contained in a JSON object in which method is the action to perform on the map, and args contains any arguments to pass to that method. Notice that I specify that the target domain of the postMessage call lies in the ms-ww-web web context:

function Pan(dx, dy) {
    var xMsg = { method: 'pan', args: {x: dx, y: dy} };
    mapIframe.postMessage(xMsg, "ms-wwa-web://" + document.location.host);
}

function ZoomIn() {
    var xMsg = { method: 'zoomin' };
    mapIframe.postMessage(xMsg, "ms-wwa-web://" + document.location.host);
}

function ZoomOut(dx, dy) {
    var xMsg = { method: 'zoomout' };
    mapIframe.postMessage(xMsg, "ms-wwa-web://" + document.location.host);
}

Then, in the map iframe, I need to set up a listener to any postMessages received:

window.addEventListener("message", receiveMessage, false);

and, in the receiveMessage() callback, unravel and handle the requested JSON instruction (which is contained in the event.data parameter):

function receiveMessage(event) {
  switch (event.data.method) {
    case 'pan':
      var dx = event.data.args.x;
      var dy = event.data.args.y;
      var pos = map.tryPixelToLocation(new Microsoft.Maps.Point(map.getWidth()/2 + dx, map.getHeight()/2 + dy, Microsoft.Maps.PixelReference.viewport));
      map.setView({ center: pos });
      break;

    case 'zoomin':
      var currentZoom = map.getZoom();
      map.setView({ zoom: currentZoom + 1 });
      break;

    case 'zoomout':
      var currentZoom = map.getZoom();
      map.setView({ zoom: currentZoom - 1 });
      break;
  }
}

Here’s a diagram of what’s going on:

image

Bear in mind that all I’m doing is adding the simplest of interactivity here – just panning and zooming the map. The more functionality you want to add, the more complicated the data structures you have to pass in the postMessage call (consider things like the set of points returned in a route requested from a routing service).

Also, notice that at the moment I’m only doing one-way communication from the parent container to the map iframe. But there are plenty of situations in which you’ll also want to pass messages the other way – to notify the container app when a certain action has happened on the map for example. Thus you’ll also need to setup an event listener on the container page to listen to incoming messages from the map iframe. Using the nested iframe approach and postMessage calls to create two-way interfaces like this quickly becomes unmanageable…

Option 2: Use an AJAX Map Control that works within the Local Context

In order to be executed directly from an HTML page in the local context, a Javascript library must be loaded locally. So how about we ditch the Bing Maps control altogether and use an alternative control that can be run from a local script. How about Leaflet, say?

If you don’t know, Leaflet is a new open source AJAX map control from CloudMade – a company with very close associations to the Open Street Map project. In the past, developers looking for an open source slippy map control have tended to opt for OpenLayers – but Leaflet looks set to provide a more modern, lightweight alternative. In fact, even though it’s only a few months old, Leaflet already offers many features not found in commercial map controls from the likes of Bing and Google (including support for WMS services, <canvas> tiles, and extensible map objects). But, for the purposes of developing a Metro application, Leaflet’s best feature is that it’s a very small download, consisting of just a single .js file, one .css file, and a handful of images that you can include in your own project.

Fortunately, the Leaflet API also follows very similar syntax to Bing Maps / Google Maps and will seem familiar to anyone who’s used to the idea of pushpins, infoboxes, polylines and polygons. Within a few minutes, I was therefore able to rewrite my Bing Maps app to use Leaflet instead. And, because I was loading the Leaflet library from a local <script> resource I didn’t have to worry about embedding an iframe or any of that postMessage nonsense – I could incorporate the map directly in my main HTML page in the local context. Creating a basic Leaflet map involves the following:

var map = new L.Map('divMap');
map.setView(new L.LatLng(52,0), 7);

Then, with a few calls to the Bing Maps REST service I created a new L.TileLayer using Bing Maps road style tiles to match the look and feel of my previous attempt (by default, Leaflet uses Open Street Map tiles). The result looked like this:

image

Visually, not much difference, but the pushpin popup says it all.

Summary

I don’t know the performance difference between these approaches – I guess there must be an overhead in having to marshal postMessage calls between iframes but I don’t know if its significant or not. Anecdotally, using Leaflet in the local context seems to be more responsive than the Bing Maps iframe approach.

Better performing or not, the main thing to note is that it’s a lot easier to write manageable code using the second approach. And there’s the irony – until (if?) a native WinRT maps control comes out (or unless you can figure a better way to handle the local/web context dilemma than me), if you’re looking to develop a slippy map interface for a Windows Metro application it’s actually much easier to do so using a third-party map API such as Leaflet than it is to use Microsoft’s own Bing Maps API…

This entry was posted in Bing Maps, Spatial and tagged , , , , . Bookmark the permalink.

14 Responses to Creating a Windows 8 Metro Slippy Map Application

  1. Jay Borseth says:

    This is a wonderfully helpful post, thank you! Do you happen to know if Leaflet more closely follows Google or Bing APIs?

    • alastaira says:

      Glad you found it helpful!
      To be honest, Leaflet is probably somewhere in the middle of them…. it doesn’t have some features you might have come to expect (I’m not sure if it supports the full range of events when certain actions happen on the map, for example), and it doesn’t have native access to Google/Microsoft’s imagery layers, obviously, but it makes up for it in other ways – supporting geometry collections and polygons with interior rings, and properly-stylable CSS3 infoboxes, for example…. the documentation’s still a bit thin on the ground compared to Microsoft/Google, so I might have to write another post in the future testing a more thorough example with Leaflet and see if I get tripped up further down the line.

  2. vengri says:

    Very good, but there is not an option for XAML BIngs maps?
    Do you you thing a XAML/C# Bings Maps will perform better or will be easier to use?

    • alastaira says:

      Currently there’s no XAML option – it’s javascript or nothing. A XAML control would offer an alternative, but a native WinRT API could be used from either XAML or Javascript, so I imagine that would be better still.
      But there’s no indication from Microsoft that they’re looking to develop either a Metro XAML or WinRT control…

      • Staeff says:

        I tried to include Bing Maps in an XAML WebView. Including the website is no Problem, however using the API the map is acting strange (the center is not correct und it’s not dragable – just in a strange manner).

        Does anyone has got this working?

        Regards.

  3. James Boddie says:

    Thank you for this article. I hope we will see a proper control very soon.

    (BTW, you should probably not show your Bing Maps credentials in the example.)

    • alastaira says:

      Glad you found the article helpful. That’s not my real Bing Maps key, BTW – look carefully and you might notice it contains “ABCDEF”, “123456”, and “qwertyuiop”… which would be quite a coincidence!

  4. Pingback: Bing Maps in Windows 8 | Bing Maps Tips

  5. voodoo says:

    Thanks Alastair… However, I can’t seem to call postMessage on the iframe – I get an error: Object doesn’t support property or method ‘postMessage’
    What are you calling postMessage on?

  6. Andy says:

    Hey there! Thanks for the article – there seems to be so little information on making slippy maps on Win8/WinRT at the moment. I tried implementing a test map using Leaflet, but I couldn’t get it to work – the div map displays, along with the zoom in/out controls, but no tiles display – I’ve tried a few different methods, and even adding my own custom tile layer, to no avail. Would you be able/willing to provide source examples on how you got things working with Leaflet?

    Thanks a ton for your help!

  7. grgrssll says:

    Voodoo, you have to do like this. assume the id of the iframe is mapIframe
    var mapIframe = document.getElementById(‘mapIframe’).contentWindow;
    mapIframe.postMessage(…)

  8. Tim says:

    Not being able to embed youtube videos or anything of that nature is really pissing me off and my eyes and brain are hurting from trying to figure this out. I have a variable that is website content including text, images, and a youtube video. The text and images load fine but I get this error on the iframe:

    “Exception is about to be caught by JavaScript library code at line 14, column 13 in ms-appx://a73a16ae-d0b8-4ebc-a4f2-6aef46e928aa/pages/itemDetail/itemDetail.js
    0x800c001c – JavaScript runtime error: Unable to add dynamic content. A script attempted to inject dynamic content, or elements previously modified dynamically, that might be unsafe. For example, using the innerHTML property to add script or malformed HTML will generate this exception. Use the toStaticHTML method to filter dynamic content, or explicitly create elements and attributes with a method such as createElement.”

    How the heck am I supposed to do it then? I tried the toStaticHTML and I no longer get an error but the video doesn’t load because of the change which pretty much defeats the purpose😛. Most of the posts I’ve been able to find on this aren’t taking data from a JS variable and writing it as content for that particular item’s page. Apparently, it’s possible to embed the youtube videos on the HTML pages, but again that defeats the purpose of trying to do things dynamically.

    Any ideas would be most heartily appreciated!🙂 I’m new to coding and I don’t really know where to start with a problem like this :((.

  9. Tim says:

    Update: So if I take the Youtube embed code and stick it on an HTML page… it works. But if I use JavaScript to write that along with other text/HTML to dynamic pages, it’s suddenly forbidden. Is that not essentially doing the same thing, just creating the html pages dynamically? I really don’t want to have to manually create and link a static HTML page for each article just to be able to embed a Youtube video, it should not be this hard MS :((.

    I started a grid project in Visual Studio and it gave me a bunch of JS files and such. Playing around with the generateSampleData function, I replaced the lorum ipsem text with my own text and images to try and mock up an app. The data.js file has a function in it like this:

    function generateSampleData() {
    var itemContent
    var itemDescription
    var groupDescription

    and so on. The itemContent var has text and images from a review for an item which show up fine. But if it I try to put a iframe embed code in that var string, the program errors out with that dynamic content not allowed error. Is it just not possible to get the JS to write the embed code included in the variable string as HTML onto the item’s webpage when it’s created and rendered by the app??. I don’t see why not, because if I put the iframe HTML directly onto the item page it works but if I let the JS put it there it fails…. but I’m giving up for the night, it’s only been about 4 hours 0.0.

    Sorry for the double post and I like the WP site theme here, very clean🙂.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s