Be smooth: Optimize Bing Maps in multipage Windows Store apps (JavaScript)

Bing Maps is the most widely used map control inside of Windows Store apps. Many simple apps consist of a single page user experience while many other consist of multiple pages. Many developers like to use Bing Maps throughout their application, but have found that this results in the map control constantly being loaded/disposed as the user navigates between pages. This doesn’t make for the best user experience, as there is a slight delay due to the loading of the map. One method to create a smoother user experience is to use a single instance of the map control throughout the app. This way only the data and view of the map needs to be loaded rather than the full map control. In this blog post we are going to see how a single instance of the Bing Maps control can be used throughout a multipage Windows Store app to create a smoother user experience.

Full source code for this blog post can be found in the MSDN Code Samples here.

Multipage Windows Store App Templates

In Visual Studio there are several different app templates for creating Window Store apps using JavaScript. Three of these, Hub, Grid and Split app template, are multipage app templates. All three of these templates contain a base page, default.html, in which all the subpages of the app are rendered to. So even though there are multiple pages that loaded/unloaded in these applications, the default.html page is always loaded while the app is loaded. As such, we will be able to use this to our advantage by adding our map to the default.html page and repositioning and resizing it as needed throughout the app. Below is a quick summary of these different templates: Hub, Grid and Split You can find detailed information in the MSDN documentation here.

Hub App Template

The Hub template project creates a three-page Windows Store app that uses a Hub control. It displays content that can be panned horizontally and provides a variety of ways to access content. This template is used by many of the Bing Apps like Bing News, Bing Weather and Bing Travel.

Screenshot: Hub app template

Screenshot: Hub app template

Grid App Template

The Grid app template is good for apps that have a drill down or parent-child style of navigation. This template is designed to show one or more categories on a page called GroupedItems, which is the default start page. If you select a group, details about a group are displayed on a page called GroupDetail. If you select an item, details about the selected item are displayed on a page called ItemDetail. This template works well for many apps that allow you to navigate through a lot of information such as store apps, blog reader apps, photo organizer apps and teaching apps.

Screenshot: Grid app template

Screenshot: Grid app template

Split App Template

The Split app template creates a two-page Windows Store project. Designed for a Parent-child or drill down style of navigation. The difference between the Split and Grid App, is that when you select a group with this template it brings you to a page that is split in two. One half of the page lists all of the items in the group, the other half of the page lists the details of any item you select within the group.

Screenshot: Split app template

Screenshot: Split app template

Creating our multipage app

For this blog post we are going to create a Windows Store app using the Split app template. The goal will be to use a single instance of the Bing Maps control to show a map in the description of each item in the split view page of the app. Later in this blog post we will see how we can use location data for each item to set the location of the map.

To get started open Visual Studio and create a new JavaScript Windows Store app using the Split App Template called OptimizedMultipageMapApp.

Screenshot: Split app template new project

Screenshot: Split app template new project

Once the project is created add a reference to the Bing Maps SDK for Windows Store apps by right clicking on the References folder and press Add Reference. Select WindowsExtensions and then select Bing Maps for JavaScript. If you do not see this option ensure that you have installed the Bing Maps SDK for Windows Store apps.

Screenshot: Reference manager

Screenshot: Reference manager

Next, right click on the js folder and select Add → New Item. Create two new JavaScript files called ReusableMap.js and MapPlaceholder.js. We will use the ReusableMap.js file to load a single Bing Maps instance on the default.html page. Later we will use the MapPlaceholder.js file to create a WinJS control that can be placed inside our HTML to specify where the map should be positioned.

Open the default.html page and in the header of the page add script references to the Bing Maps SDK and the ReusableMap.js file using the following HTML.

<!-- Add Bing Maps and ReusableMap references -->
<script type="text/javascript" src="ms-appx:///Bing.Maps.JavaScript//js/veapicore.js"></script>
<script src="/js/ReusableMap.js"></script>

 

Next, add an absolutely positioned div with an id of resuableMap in the body of the default.html page after the content host div.

<div id="reusableMap" style="position:absolute;"></div>

 

In the ReusableMap.js file we want to load the map and create a global variable reference that can be accessed from anywhere in the application. We will also monitor the navigation of the app and hide the map whenever navigating between pages. This will ensure that the map is not displayed on pages where a map placeholder is not specified. Add the following code to the ResuableMap.js file:

(function () {
    "use strict";    

    function initMapNamespace() {
        Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: initMap });
    }

    function initMap() { // Initialize Bing map
        var mapDiv = document.getElementById("reusableMap");

        window.reusableMap = new Microsoft.Maps.Map(mapDiv, {
            credentials: "YOUR_BING_MAPS_KEY"
        });

        //Hide the map when navigating between pages.
        WinJS.Navigation.addEventListener('beforenavigate', function () {
            mapDiv.style.display = 'none';
        });
    }

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

 

Next, we will create a MapPlaceholder class. This class will wrap a div element and use its position and size to position and resize the map. Since there are no events for when the position or size of a div element changes, we will make use of the requestAnimationFrame function and have it update these values of the map continuously like an animation. Add the following code to the MapPlaceholder.js file:

(function () {
    "use strict";

    var mapPlaceholderControl = WinJS.Class.define(
            function (element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                var self = this;

                this._map = options.map;

                var repositionMap = function () {
                    if (self.map) {
                        var mapDiv = self.map.getRootElement().parentElement;

                        //Give the map the same visibility as the map placeholder
                        mapDiv.style.display = self.element.style.display;

                        var p = WinJS.Utilities.getPosition(self.element);
                        mapDiv.style.left = p.left + 'px';
                        mapDiv.style.top = p.top + 'px';
                        mapDiv.style.width = p.width + 'px';
                        mapDiv.style.height = p.height + 'px';

                        requestAnimationFrame(repositionMap);
                    }
                };

                requestAnimationFrame(repositionMap);
            },
            {
                map: {
                    get: function () {
                        return this._map;
                    },
                    set: function (value) {
                        var oldValue = this._map;
                        this._map = value;
                        this.notify("map", value, oldValue);
                    }
                }
            });

    //Make the map placeholder control bindable control.  
    WinJS.Class.mix(mapPlaceholderControl, WinJS.Binding.observableMixin);

    //Add the map placeholder control and to a common Namespace called ReusableMap. 
    WinJS.Namespace.define("ReusableMap", {
        MapPlaceholderControl: mapPlaceholderControl
    });
})();

 

Then we can add the map placeholder to the different pages in the application were we want the map to be rendered. The following HTML shows how to create an instance of the MapPlaceholderControl class and set the map property to point to the global reusable map we created earlier.

<div data-win-control="ReusableMap.MapPlaceholderControl" data-win-options="{map:window.reusableMap}"></div>

 

For this blog post we will add the placeholder to the split page of the application between the header and content of an item. First we have to add the following script reference to the MapPlaceholder.js file in the head section of the split.html page.

<script src="/js/MapPlaceholder.js"></script>

 

For this app we will let the map stretch the full width of the article section of the page and give it a height of 250 pixels. To do this, add an instance of the MapPlaceholderControl class between the header and the article-content div in the articlesection div as shown below:

<div class="articlesection" aria-atomic="true" aria-label="Item detail column" aria-live="assertive">
    <article>
        <header class="header">
            <div class="text">
                <h2 class="article-title win-type-ellipsis" data-win-bind="textContent: title"></h2>
                <h4 class="article-subtitle" data-win-bind="textContent: subtitle"></h4>
            </div>
            <img class="article-image" src="#" data-win-bind="src: backgroundImage; alt: title" />
        </header>
                
        <!-- Add Map Placeholder between header and content. -->
        <div data-win-control="ReusableMap.MapPlaceholderControl" data-win-options="{map:window.reusableMap}" style="width:100%;height:250px;"></div> 

        <div class="article-content" data-win-bind="innerHTML: content"></div>
    </article>
</div>

 

If you now run the application and select a group to view individual items on the split page, you will see a map displayed in the application. If you put a break point in the ReuableMap.js file where the map loads, you will find that as you navigate through the app this break point will only be hit when the application initially loads.

Example: View individual items on the split page

Example: View individual items on the split page

Also notice that as you scroll the content of the item that the map will move smoothly to stay correctly positioned. If you select the back button, the map will disappear until you view an individual item again.

Loading the map to an items location

Being able to reuse a single map control across a multipage application is great, but you likely want to show different map views that are related to the item that is being viewed. In this section of the blog post we are going to bring our app to life by using more interesting data to power the app. What we will do is create two groups in our app: Cities and Landmarks. The item information will then be one of these two types of information. For each group we will add a new property called zoom, which will be the preferred zoom level to set the map to for each group of items. For each item we will add a latitude and longitude property which will be used for centering the map. To do this, open up the data.js file and update the sampleGroups and the sampleItems values with the following:

var sampleGroups = [
    { key: "group1", title: "Cities", subtitle: "Major Cities", zoom: 11, backgroundImage: darkGray, description: groupDescription },
    { key: "group2", title: "Landmarks", subtitle: "Interesting Landmarks", zoom: 17, backgroundImage: lightGray, description: groupDescription }
];

var sampleItems = [
    { group: sampleGroups[0], title: "New York", subtitle: "The Big Apple", latitude: 40.7595, longitude: -73.9678, description: itemDescription, content: itemContent, backgroundImage: lightGray },
    { group: sampleGroups[0], title: "Redmond", subtitle: "Home of Microsoft", latitude: 47.6786, longitude: -122.1316, description: itemDescription, content: itemContent, backgroundImage: mediumGray },
    { group: sampleGroups[0], title: "Chicago", subtitle: "The Windy City", latitude: 41.8843, longitude: -87.6324, description: itemDescription, content: itemContent, backgroundImage: mediumGray },
    { group: sampleGroups[0], title: "Unknown Location", subtitle: "No coordinates specified", description: itemDescription, content: itemContent, backgroundImage: lightGray },

    { group: sampleGroups[1], title: "Eiffel Tower", subtitle: "Paris, France", latitude: 48.85822, longitude: 2.2945, description: itemDescription, content: itemContent, backgroundImage: darkGray},
    { group: sampleGroups[1], title: "Statue of Liberty", subtitle: "Liberty Island, Manhattan, NY", latitude: 40.6892, longitude: -74.0444, description: itemDescription, content: itemContent, backgroundImage: mediumGray },
    { group: sampleGroups[1], title: "Unknown Location", subtitle: "No coordinates specified", description: itemDescription, content: itemContent, backgroundImage: lightGray }
];

 

To make use of this new location data when a new item is selected, we will check to see if the selected item has location information or not. If it doesn’t then we will hide the map placeholder. If it does then we will show the map placeholder and use the location information and the zoom level for the group to set the view of the map. To do this update the _selectionChanged function in the split.js file with the following code:

_selectionChanged: function (args) {
    var listView = args.currentTarget.winControl;
    var details;
    // By default, the selection is restriced to a single item.
    listView.selection.getItems().done(function updateDetails(items) {
        if (items.length > 0) {
            this._itemSelectionIndex = items[0].index;
            if (this._isSingleColumn()) {
                // If snapped or portrait, navigate to a new page containing the
                // selected item's details.
                setImmediate(function () {
                    nav.navigate("/pages/split/split.html", { groupKey: this._group.key, selectedIndex: this._itemSelectionIndex });
                }.bind(this));
            } else {
                // If fullscreen or filled, update the details column with new data.
                details = document.querySelector(".articlesection");
                binding.processAll(details, items[0].data);
                details.scrollTop = 0;
            }

            //Get a reference to the map placeholder.
            var mapPlaceholder = document.getElementById('MapPlaceholder');

            if (items[0].data.latitude != null && items[0].data.longitude != null) {
                //Display the map
                mapPlaceholder.style.display = '';

                //Center the map on these coordinates
                var loc = new Microsoft.Maps.Location(items[0].data.latitude, items[0].data.longitude);
                mapPlaceholder.winControl.map.setView({ center: loc, zoom: items[0].data.group.zoom });
            } else {
                //Hide the map
                mapPlaceholder.style.display = 'none';
            }
        }
    }.bind(this));
},

 

If you now run the application and select the Cities group and the Chicago item, you will see the map appear and centered over the city of Chicago.

Example:  Cities group and Chicago item selection

Example: Cities group and Chicago item selection

If you then go back to the main page and select the Landmarks group and then select the Statue of Liberty item, you will see the map zoom in to a bird’s eye image of this location. A bird’s eye image is shown because the map mode is set to automatic and the zoom level is close enough for the map to switch into bird’s eye mode.

Example: Selection of Landmarks group and Statue of Liberty item

Example: Selection of Landmarks group and Statue of Liberty item

Optimizations using session keys

In your application you may find it useful to access the Bing Maps REST Services or Bing Spatial Data Services to access things like boundary or elevation data, traffic information, static map images or one of the many other features in these services. A Bing Maps key is required to access these services. However, if you use your normal Bing Maps key, all calls to these services will be marked as billable transactions. In the Bing Maps map control there is a function called getCredentials that generates a special Bing Maps key called a session key. You can use the session key with these services, within the context of the map session. By doing this the transactions will be linked to the user session and marked as non-billable. If you want to create a session key in this application, add the following code to the ReusableMap.js file right after the line of code that loads the map.

window.reusableMap.getCredentials(function (c) {
    window.bingMapsSessionKey = c;
});

 

As an additional tip, if you have a data source stored in the Bing Spatial Data Services, use your query key to load the map. The session key that will be generated by the map will create non-billable transactions and also be able to query your data source. For more information about transactions in Bing Maps, check out the MSDN documentation here.

– Ricky Brundritt, Bing Maps Program Manager