A couple years ago I wrote a tutorial on how to work on Mircrosoft Virtual Earth. As an update I will create a simple Windows 8 food service locator Bing Map app in this post, which includes below features:
- Search by location or by name
- Show different pushpins based on service types
- Dynamically update data when moving around the map
- Show service detail when clicking on the pushpin
The custom pushpins shown on the map are copied courtesy of the free map icons online.
Environment Setup
First you need a Windows 8 or Windows 8.1 environment, VM or physical machine, and install Visual Studio 2012 or 2013 in the machine. Inside Visual Studio toolbar menu, select TOOLS=>Extension and Updates, then search and install "Bing Maps SDK for JavaScript".
You also need to setup a Bing Maps account from Microsoft. The detail of Bing Maps Account setup is described in MSDN.
Create Visual Studio Project
Ope Visual Studio, create a Blank App from JavaScript|Windows Store template, add a reference for "Bing Maps for JavaScript", and update default.html, default.js and default.css as following:
default.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>BingMap Test</title> <!-- WinJS references --> <link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" /> <script src="//Microsoft.WinJS.1.0/js/base.js"></script> <script src="//Microsoft.WinJS.1.0/js/ui.js"></script> <!-- Bing Maps reference --> <script type="text/javascript" src="ms-appx:///Bing.Maps.JavaScript//js/veapicore.js"></script> <!-- BingMapExample references --> <link href="/css/default.css" rel="stylesheet" /> <script src="/js/default.js"></script> </head> <body> <div class="mapPage"> <div class="header"> <!-- Segoe UI current location symbol --> <button id="btnCurrent"></button> <input id="txtSearch" type="text" /> <progress id="progressDiv"></progress> <!-- Segoe UI search symbol --> <button id="btnSearch"></button> </div> <div id="mapDiv"></div> </div> </body> </html>The Segoe UI symbols are listed in MSDN.
default.js:
(function () { "use strict"; var map; // Bing Map ojbect document.addEventListener("DOMContentLoaded", initialize, false); function initialize() { Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: initMap }); Microsoft.Maps.Events.addHandler(map, 'viewchangeend', mapViewChanged); btnCurrent.addEventListener("click", currentLocationClick); btnSearch.addEventListener("click", searchLocationClick); } function initMap() { // Initialize Bing map try { var mapDiv = document.getElementById("mapDiv"); var mapOptions = { credentials: "xxxxxxx", //Read from your Bing Maps Account center: new Microsoft.Maps.Location(43.813, -79.344), //TORONTO zoom: 13, }; map = new Microsoft.Maps.Map(mapDiv, mapOptions); } catch (err) { showError("Init Map error", err); } } // Repopulate map when it's moved function mapViewChanged() { // TODO } // Search by current location function currentLocationClick() { // TODO } // Search servcie by name function searchLocationClick() { // TODO } })();
default.css:
.mapPage { width: 100%; height: 100%; display: -ms-grid; -ms-grid-columns: 120px 1fr 120px; -ms-grid-rows: 20px 40px 1fr 20px; position: absolute; } .mapPage .header { -ms-grid-column: 2; -ms-grid-row: 2; display: -ms-grid; -ms-grid-columns: auto 5px 1fr 5px auto; } .mapPage .header #btnCurrent { -ms-grid-column: 1; -ms-grid-row-align: center; font-family: "Segoe UI Symbol"; } .mapPage .header #txtSearch { -ms-grid-column: 3; -ms-grid-row-align: center; width: 100%; border: none; } .mapPage .header #progressDiv { -ms-grid-column: 3; -ms-grid-column-align: end; -ms-grid-row-align: center; } .mapPage .header #btnSearch { -ms-grid-column: 5; -ms-grid-row-align: center; font-family: "Segoe UI Symbol"; } .mapPage #mapDiv { -ms-grid-column: 2; -ms-grid-row: 3; position: relative; height: 100%; width: 100%; }The CSS3 grid-layout and fractional units are used to layout the page so it can auto-stretch to different screen size. One catch is that the map would occupy the whole screen unless you set its container to relative position.
Now the structure of the page is defined and you will see the map showing properly when debugging the app. Next we will work on the data model and mock data service.
Data Model and Mock Data Service
Our service location entity includes attributes of Geo location, service type, name, address, description, and service type. The Geo location pins the map location as a pushpin and the custom pushpin images will differ based on the service type. The name, address and description will be displayed in an infobox popup.
Mock service will return a list of service locations as well as the center location for the map. To generate locations in valid for a map, mock service needs to know the map boundary. Our prototype has two search types, by Geo location or by name query. For the Geo location search scenario the Geo location is also the map center in the mock service return:
(function () { "use strict"; WinJS.Namespace.define("mockLocationService", { search: search // Only expose one search method }); /* * Proivde mock data for testing * Note: the caculation is based on North America and may not work in other location */ function search(parameters) { var response = {}; response.serviceLocations = []; if (parameters.latMin && parameters.latMax && parameters.longMin && parameters.longMax) { var latMin = parseFloat(parameters.latMin); var latMax = parseFloat(parameters.latMax); var longMin = parseFloat(parameters.longMin); var longMax = parseFloat(parameters.longMax); // recenter the map if search by location if (parameters.lat && parameters.long) { latMin = parameters.lat - (latMax - latMin) / 2; latMax = parameters.lat + (latMax - latMin) / 2; longMin = parameters.long - (longMax - longMin) / 2; longMax = parameters.long + (longMax - longMin) / 2; } // generate 10-20 mock service loacations var count = 10 + Math.floor(Math.random() * 10); var mockData = generateMockData(latMin, latMax, longMin, longMax, count); for (var i = 0; i < mockData.length; i++) { var serviceLocation = mockData[i]; response.serviceLocations.push(serviceLocation) } // also return a center location for mapping response.center = { lat: ((latMax + latMin) / 2), long: ((longMin + longMax) / 2) }; } return response; } function generateMockData(latMin, latMax, longMin, longMax, count) { var services = []; for (var i = 1; i <= count; i++) { var lat = Math.random() * (latMax - latMin) + latMin; var long = Math.random() * (longMax - longMin) + longMin; var name = "Service location " + i; var address = i + " Queen street ..."; var rand = Math.floor(Math.random() * 5) + 1; var type, address, description; switch (rand) { case 1: type = MapService.ServcieType.Restaurant; description = "Come and taste our famous dishes ..."; break; case 2: type = MapService.ServcieType.Vegetarian; description = "Looking for healthy vete food? ..."; break; case 3: type = MapService.ServcieType.Buffet; description = "No idea what to eat? ..."; break; case 4: type = MapService.ServcieType.Bar; description = "Have a few beers and other drinks here ..."; break; case 5: type = MapService.ServcieType.Foodtruck; description = "Call us and we will bring your our nice food ..."; break; } var servicePoint = { name: name, lat: lat, long: long, type: type, address: address, description: description }; services.push(servicePoint); } return services; } })();
Map Service Module
Map service module handles a few things: get current location, invoke location services, parse response data and create object to serve map pushpins and infobox popup:
(function () { "use strict"; // Service types for mock service var ServcieType = { Restaurant: 1, Vegetarian: 2, Buffet: 3, Bar: 4, Foodtruck: 5 }; function invokeLocationService(searchParameter, callBack, errorHandler, mapBounds) { if (!mapBounds) { // Get data from web service var searchUrl = "http://mylocationserver.com/search"; WinJS.xhr({ url: searchUrl, data: searchParameter, responseType: "json" }).done( function (result) { // Service call completed if (callBack) { var data = JSON.parse(result.responseText); callBack(data); } }, function (err) { // Service call error if (errorHandler) { errorHandler(err); } }); } else { // Get data from mock service if (callBack) { try { // mapBounds values in north America, maynot work in other location var latMin = mapBounds.getSouth(); var latMax = mapBounds.getNorth(); var longMin = mapBounds.getWest(); var longMax = mapBounds.getEast(); var parameters = { latMin: latMin, latMax: latMax, longMin: longMin, longMax: longMax }; if (searchParameter.lat && searchParameter.long) { // Search by location parameters.lat = searchParameter.lat; parameters.long = searchParameter.long; } else { // Search by name parameters.query = searchParameter.query; } var serviceLocations = mockLocationService.search(parameters); callBack(serviceLocations); } catch (ex) { if (errorHandler) { errorHandler(ex); } } } } } // parse result JSON and return an array of service locaiton objects function parseServiceLocations(json) { var serviceLocations = []; for (var i = 0; i < json.serviceLocations.length; i++) { var service = json.serviceLocations[i]; var newLocation = {}; newLocation.location = new Microsoft.Maps.Location(service.lat, service.long); newLocation.name = service.name; newLocation.summary = service.description + "<br /><br /><b>Address: </b>" + service.address; newLocation.type = service.type; populateImageUrls(newLocation); serviceLocations.push(newLocation); } return serviceLocations; } // parse center location from result JSON function parseSearchCenterLocation(data) { return new Microsoft.Maps.Location(data.center.lat, data.center.long); } // Populate pushpin and location image urls function populateImageUrls(serviceLocation) { switch (serviceLocation.type) { case ServcieType.Restaurant: serviceLocation.pushpinUrl = "images/locationIcons/restaurant.png"; break; case ServcieType.Vegetarian: serviceLocation.pushpinUrl = "images/locationIcons/vegetarian.png"; break; case ServcieType.Buffet: serviceLocation.pushpinUrl = "images/locationIcons/buffet.png"; break; case ServcieType.Bar: serviceLocation.pushpinUrl = "images/locationIcons/bar.png"; break; case ServcieType.Foodtruck: serviceLocation.pushpinUrl = "images/locationIcons/foodtruck.png"; break; } } // create a pushpin for a service location object function getServicePointPushpin(serviceLocation) { var pushpin = new Microsoft.Maps.Pushpin(serviceLocation.location, { icon: serviceLocation.pushpinUrl, width: 32, height: 37, anchor: new Microsoft.Maps.Point(16, 18) }); pushpin.name = serviceLocation.name; pushpin.summary = serviceLocation.summary; return pushpin; } // create a pushpin for current location function getCurrentLocationPushpin (location) { var pushpin = new Microsoft.Maps.Pushpin(location, { icon: "/images/locationIcons/current.png", width: 32, height: 37, anchor: new Microsoft.Maps.Point(16, 18) }); pushpin.name = "Your current location"; pushpin.summary = "latitude: " + location.latitude + " longitude:" + location.longitude; return pushpin; } // Location service helper var CurrentLocation = WinJS.Class.define( // Constructor function (disableCache) { this.doCache = !disableCache; }, // Instance members { getCurrentLocationAsync: function () { var self = this; return new WinJS.Promise(function (c, e, p) { if (CurrentLocation.geoLocator == null) { CurrentLocation.geoLocator = new Windows.Devices.Geolocation.Geolocator(); } if (CurrentLocation.geoLocator) { if (self.doCache && CurrentLocation.location) { c(CurrentLocation.location); // complete the call back with cached data } else { return CurrentLocation.geoLocator.getGeopositionAsync().then( function (pos) { // get position data handler var loc = new Microsoft.Maps.Location(pos.coordinate.latitude, pos.coordinate.longitude); CurrentLocation.location = loc; // Cache local position c(loc); }.bind(this), function (ex) { // location service error e(ex); }.bind(this)); } } }); } }, // Static members { geoLocator: null, location: null } ); // Expose MapService WinJS.Namespace.define("MapService", { ServcieType: ServcieType, CurrentLocation: CurrentLocation, invokeLocationService: invokeLocationService, parseServiceLocations: parseServiceLocations, parseSearchCenterLocation: parseSearchCenterLocation, getServicePointPushpin: getServicePointPushpin, getCurrentLocationPushpin: getCurrentLocationPushpin, }); })();A couple of notes about the MapService module:
- The mapbounds parameter is unnecessary for real service calls, it's only used by mock service so that it has a coordinate to generate valid service points inside the map.
- Locaiton service uses native Geolocator to retrieve current location, and caches the data for future usage to improve the performace. The cache feature can turn off when instantiating the CurrentLocation object if that's desired.
- The Bing Maps' default pushpin is a 25x35 size image pining at the middle bottom of the image. We need to use pushpin's anchor property to adjust the pin point if using custom images with different size or different pining location.
- Infobox popup supports some HTML tags inside the message, e.g. <br /> line break and <b> bold fonts for "Address" are used in our example
UI Interaction
Now we are ready to refine the UI and implement the user interactions. First add mock service and map service javascript references in the default.html. Following is the screen-shot of the final solution inside Visual Studio:
In general there are three user actions: click current location button to search services around; move map to search nearby locations; type some keyword and click search button to do search name. The first two cases are basically the same as they both search by location. The updated default.js is listed below and it's quite self explanatory:
<script type="text/javascript"> (function () { "use strict"; var map; // Bing Map ojbect var currentLocPin; // Pushpin for current location var locationServie; // Location serverice instance var pushpinLayer; // A layer to contain all pushpins var infoboxLayer; // A layer to contain the infobox var resetMapCenter; // Reset the map center location if is true document.addEventListener("DOMContentLoaded", initialize, false); function initialize() { Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: initMap }); Microsoft.Maps.Events.addHandler(map, 'viewchangeend', mapViewChanged); btnCurrent.addEventListener("click", currentLocationClick); btnSearch.addEventListener("click", searchLocationClick); showProgress(); } function initMap() { // Initialize Bing map try { var mapDiv = document.getElementById("mapDiv"); var mapOptions = { credentials: "xxxxxxx", //Read from your Bing Maps Account center: new Microsoft.Maps.Location(43.813, -79.344), //TORONTO zoom: 13, }; map = new Microsoft.Maps.Map(mapDiv, mapOptions); // layers to maintain the pushpins and infobox popup pushpinLayer = new Microsoft.Maps.EntityCollection(); infoboxLayer = new Microsoft.Maps.EntityCollection(); map.entities.push(pushpinLayer); map.entities.push(infoboxLayer); } catch (err) { showError("Init Map error", err); } } // Repopulate map when its moved function mapViewChanged() { resetMapCenter = false; if (map.getZoom() > 10) { var mapCenter = map.getCenter(); var parameters = { lat: mapCenter.latitude, long: mapCenter.longitude }; searchLocationService(parameters); } } // Search by current location function currentLocationClick() { resetMapCenter = true; if (!locationServie) { locationServie = new MapService.CurrentLocation(); } locationServie.getCurrentLocationAsync().then( function (loc) { currentLocPin = MapService.getCurrentLocationPushpin(loc); searchLocationService({ lat: loc.latitude, long: loc.longitude }); }, function (err) { showError("Location service error", err); }); } // Search servcie by name function searchLocationClick() { resetMapCenter = true; var searchInput = txtSearch.value; if (searchInput) { var parameters = { query: escape(searchInput) }; searchLocationService(parameters); } } // Invoke map service by MapService module function searchLocationService(searchParameter) { showProgress(); try { var mapBounds = map.getBounds(); // Map Bounds are only used by mock service MapService.invokeLocationService(searchParameter, handleSearchResult, handleSearchError, mapBounds); } catch (err) { showError("Error occurs", err); } } // Show error message dialog function showError(title, err) { var msg = (err && err.message) ? err.message : err; var msgBox = new Windows.UI.Popups.MessageDialog(msg, title); msgBox.showAsync(); } // Handle search error function handleSearchError(err) { showError("Map Service error", err); hideProgress(); } // Search Result handling call back function handleSearchResult(data) { var serviceLocations = MapService.parseServiceLocations(data); if (resetMapCenter) { var searchCenter = MapService.parseSearchCenterLocation(data); var locations = []; for (var i = 0; i < serviceLocations.length; i++) { locations.push(serviceLocations[i].location); } var origCenter = map.getCenter(); var origBound = map.getBounds(); var mapBound = Microsoft.Maps.LocationRect.fromLocations(locations); map.setView({ center: searchCenter, bounds: mapBound }); } updatePushpins(serviceLocations); hideProgress(); } // Shwo progress function showProgress() { progressDiv.style.display = "block"; } // Hide progress function hideProgress() { progressDiv.style.display = "none"; } // Update pushpins on the map function updatePushpins(serviceLocations) { infoboxLayer.clear(); pushpinLayer.clear(); for (var i = 0; i < serviceLocations.length; i++) { var loc = serviceLocations[i]; var pin = MapService.getServicePointPushpin(loc); pin.name = loc.name; pin.summary = loc.summary; pushpinLayer.push(pin); Microsoft.Maps.Events.addHandler(pin, 'click', showInfobox); } if (currentLocPin) { pushpinLayer.push(currentLocPin); } } // Display infobox when pushpin is clicked function showInfobox(e) { if (e.targetType == "pushpin") { infoboxLayer.clear(); var pin = e.target; var pinLocation = pin.getLocation(); var infoboxOptions = { showCloseButton: true, //offset: new Microsoft.Maps.Point(0, 5), showPointer: true, zIndex: 10, title: pin.name, description: pin.summary, }; var infobox = new Microsoft.Maps.Infobox(pinLocation, infoboxOptions); infoboxLayer.push(infobox); } } })(); </script>
The whole project can be downloaded from GitHub.