Using maps in a Windows 8 Metro application

October 29, 2011 at 2:46 pm | Posted in HTML/JavaScript, Metro, Windows 8 | Leave a comment

With the release of the Customer Preview of Windows 8, there is now a Bing Maps SDK which can be used with both C# and JavaScript apps.  This functionality makes the technique described in this post unnecessary. (Added 4/10/12)

There are many uses for detailed, interactive maps in mobile and desktop applications.  The basic and advanced mapping functionality can be supplied by Google or Bing map APIs.  This post will show how to integrate these maps into a Windows 8 Developer Preview application.

Currently, there is no map control for Metro development so one solution is to use a JavaScript map API in an element that can be hosted in the application.  Security restrictions make this approach tricky and, as far as I know, it is only possible to do this using the HTML/JavaScript programming environment.

The security issue is that the main application in the local context may not load scripts that are external to the host. Of course, for both Google and Bing map APIs require an external script load.  The solution is to use an iframe element in the body of the main app that refers to a local html page that operates in a web context and is able to load foreign scripts.  The postMessage() method is used to communicate between the two pages.

In the following example a simple application displays a map, a button and a status output.  The Google map API is used but it could easily be converted to use the Bing map API.  The map displays two markers: one on Paris and the other, Rouen, France.  Clicking the button sends a command to the map page.  The command is to zoom to a set of coordinates (for Paris).  If you click on one of the markers on the map, the city name is set back to the parent page (the main app) and is displayed in the status output.

For purposes of this example, the messaging system is very simple.  It would be possible to create a more generalized function that could be used for a variety of such applications.

Here are the main files:

First, the default.html containing the main display.  There is an iframe element which refers to another local file, map.html.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Map Help Example</title>
    <!-- WinJS references -->
    <link rel="stylesheet" href="/winjs/css/ui-dark.css" />
    <script src="/winjs/js/base.js"></script>
    <script src="/winjs/js/ui.js" type="text/javascript"></script>
    <script type="text/javascript" src="winjs/js/binding.js"></script>
    <script src="/winjs/js/animations.js" type="text/javascript"></script>
    <script src="/winjs/js/uicollections.js" type="text/javascript"></script>
    <script src="/winjs/js/wwaapp.js"></script>
    <!-- WinWebApp2 references -->
    <link rel="stylesheet" href="/css/default.css" />
    <script src="/js/default.js"></script>

</head>
<body>
    <div id="appBody">
        <h1 id="head_title" class = "win-title">
            Map Example</h1>
        <div id="status">
            Hello</div>
        <button type="submit" id="paris">Paris</button>
        <div id="mapSection">
            <iframe id="mapIframe" src="ms-wwa-web:///map.html"></iframe>
        </div>
    </div>
</body>
</html>

This is the map.html which holds the container for the actual map.  In addition to the map.js script, it loads the Google map API.

<!--This file is used to render the map API and will be included in an iFrame of the main html.-->
<!DOCTYPE html>
<html>
<head>

    <title>Map</title>
    <link href="winjs/css/ui-light.css" rel="stylesheet" type="text/css" />
    <!-- We can include remote scripts because it is rendered in the web context (ms-wwa-web:///map.html)  -->
    <script src="http://maps.googleapis.com/maps/api/js?sensor=false" type="text/javascript"></script>
    <script src="js/map.js" type="text/javascript"></script>
    <style type="text/css">

        #map
        {
          position:absolute;
          width:100%;
          height:100%;
        }

    </style>
</head>
<body>
    <div id="map"></div>
</body>
</html>

Here is the default.js which manages the main program UI. Note that the command is an object that is serialized to a string so that it can be transmitted to the map using postMessage(). There is also a function to receive a simple message from the map page to display on the status line.


(function () {
    'use strict';

    Debug.enableFirstChanceException(true);

    function id(elementId) {
        return document.getElementById(elementId);
    }

    WinJS.Application.onmainwindowactivated = function (e) {
        if (e.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            WinJS.UI.processAll();

            //Button event listener
            paris.addEventListener("click", changeLocation, false);
        }
    }

    WinJS.Application.start();

    //Button event handler which sends a command to the map to zoom to Paris.
    function changeLocation(e) {

        id("status").innerText = "Zooming to Paris";

        //Construct a message to send
        var msg = {
            command: 'zoomTo',
            latitude: 48.863811,
            longitude: 2.351761,
            zoom: 8
        };

        //Convert message object to string and send to the map control.
        var msgS = JSON.stringify(msg);
        document.frames['mapIframe'].postMessage(msgS, "ms-wwa-web://" + document.location.host);
    }

    //Receive message from map and displays to status output
    window.addEventListener("message", receiveMessage, false);
    function receiveMessage(event) {
        if (event.origin === "ms-wwa-web://" + document.location.host) {
            id("status").innerText = "Hello from " + event.data;
        }
    }
})();

Here is the map.js which controls the map. It receives the string message and converts it back to an object where it is interpreted and operated on. Map events such as the clicking of a marker are handled by sending a message back to the parent page.


//This script handles all of the controls to the map API
(function () {
    'use strict';

    var map;

    //Process messages from main script
    window.addEventListener("message", receiveMessage, false);
    function receiveMessage(event) {
        if (event.origin === "ms-wwa://" + document.location.host) {

            //Return the message string to an object
            var messageObject = JSON.parse(event.data);

            //If message is to zoom, change the location and zoom level
            if (messageObject.command == "zoomTo") {
                var newCenter = new google.maps.LatLng(messageObject.latitude, messageObject.longitude);
                var newOptions = {
                    zoom: messageObject.zoom,
                    center: newCenter,
                    mapTypeId: google.maps.MapTypeId.ROADMAP
                };
                map.setOptions(newOptions);
            }
        }
    }

    //This function sends a message back to the parent window.
    function sendMessageBack(city) {
        window.parent.postMessage(city, "ms-wwa://" + document.location.host);
    }

    function initialize() {
        //initialize the map
        var latlng = new google.maps.LatLng(38.96, -96.78);
        var myOptions = {
            zoom: 4,
            center: latlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };
        map = new google.maps.Map(document.getElementById("map"),
        myOptions);

        //Add a couple of pushpins with event handlers
        var parisLocation = new google.maps.LatLng(48.863811, 2.351761);
        var marker1 = new google.maps.Marker({
            position: parisLocation,
            map: map,
            title: "This is Paris"
        });
        google.maps.event.addListener(marker1, 'click', function () {
            sendMessageBack("Paris");
        });

        var rouenLocation = new google.maps.LatLng(49.4467, 1.085889);
        var marker2 = new google.maps.Marker({
            position: rouenLocation,
            map: map,
            title: "This is Rouen"
        });
        google.maps.event.addListener(marker2, 'click', function () {
            sendMessageBack("Rouen");
        });
    }

    document.addEventListener("DOMContentLoaded", initialize, false);
})();

Finally, here is the default.css file which defines the layout of the main application elements.


#appBody
{
    width: 100%;
    height: 100%;
    display: -ms-grid;
    -ms-grid-rows: 100px 85px 520fr 60px;
    -ms-grid-columns: 42px 405px 9px 846fr 64px;
}

#head_title
{
    margin-bottom: -8px;
    margin-top: 0px;
    -ms-grid-column: 2;
    -ms-grid-row-align: end;
    -ms-grid-column-span: 5;
}

#mapSection
{
    -ms-grid-column: 4;
    -ms-grid-row: 1;
    -ms-grid-row-span: 4;
    -ms-grid-column-span: 2;
}

#mapIframe
{
    width: 100%;
    height: 100%;
}

#status
{
    -ms-grid-column: 2;
    -ms-grid-row: 2;
    -ms-grid-row-align: center;
}

#paris
{
    -ms-grid-column: 2;
    -ms-grid-row: 3;
    width: 59px;
    height: 35px;
}
Advertisements

Create a free website or blog at WordPress.com.
Entries and comments feeds.