October 2016

Volume 31 Number 10

[Bing Maps]

Create Interactive Geo-Applications Using Bing Maps 8

By James McCaffrey

Enterprises are generating huge amounts of data, and a lot of that data contains latitude and longitude location information. The Bing Maps version 8 library, released in June 2016, has many new features that allow you to create interactive geo-applications. In this article I present two Web applications that demonstrate some of the most interesting features of the Bing Maps 8 library. The first application highlights some of the features that allow user interaction, including a new drawing control and full event model capabilities. The second application highlights some of the features that allow users to deal with large amounts of data, including a new data clustering module and heat map visualizations.

This article assumes you have basic familiarity with Web appli­cation development but doesn’t assume you know anything about geolocation applications or the Bing Maps 8 library. The two demo Web applications use only standard HTML and standard JavaScript—no ASP.NET, and no JavaScript framework-of-the-month. Each of the two demo Web applications is contained in a single HTML file. The complete source code and the two data files used are available in the accompanying code download.

Take a look at the first Web application in Figure 1. When the Web page loaded, the HTML controls on the left were rendered immediately while the map was being fetched asynchronously. The map object is centered near Portland, Ore., and a default-style purple pushpin marker was placed at the map center.

Pushpins and Polygons Demo
Figure 1 Pushpins and Polygons Demo

I then clicked on the HTML5 File control Browse button and pointed to a text file named LatLonData.txt, stored on my local machine in the C:\Data directory. The file has four data points and each has some associated text. Then I clicked on the button control labeled Place Pushpins and the application read the text file, created four custom-styled small orange pushpins and placed them on the left side of the map.

Next, I clicked on the polygon item in the drawing tools control in the upper-right part of the map and interactively drew a four-sided green polygon just above the city of Vancouver, Wash. I drew a second polygon with three sides, to the right and below the map center. During drawing, the Web application listened for drawing-start and drawing-end events, and printed messages when those events fired.

I clicked on the button control labeled Drawn Shape Info and the Web application retrieved information about the interactively created polygons, and displayed the three vertices of the triangle polygon. Next, I moved my mouse cursor over and then away from the bottom-most orange pushpin. The application code caught the mouseover and mouseout and displayed the location of the events. Although it’s not visible in the image, when I moved my mouse cursor over the pushpin, a popup Infobox object appeared, and when I moved the mouse cursor away, the Infobox automatically disappeared.

I finished my demo session by moving the mouse cursor over the top-most orange pushpin and the application responded by creating a default-style Infobox object that displayed data associated with the pushpin (the text “first data location”) and the location of the pushpin (45.46, -122.90).

To summarize, the first Web application demonstrates asynchronous map loading, dynamic custom pushpins, rich event modeling, interactive shape creation and Infobox objects.

Creating the Pushpins and Polygons Demo Application

Before I started writing the first Web page, I created the source data file using Notepad:

45.46,-122.90,first location data
45.38,-122.90,second location data
45.42,-122.94,third location data
45.42,-122.86,fourth location data

I didn’t hit the <enter> key after the last line of data so my file-reading code wouldn’t try to interpret an empty line of text. I used commas as the field delimiter, but I could have used the tab character. I saved the data file as LatLonData.txt in the C:\Data directory on my local machine. As you’ll see, Bing Maps can work with any kind of data store.

I used the Notepad program to create the demo Web applications. I like Notepad when learning a new technology because it forces me to be careful and there’s no hidden magic to obscure the key ideas.

Because I used only plain vanilla HTML and JavaScript, I didn’t need to do anything special to prepare IIS or my machine. I created a directory named NodeAtlasLight in the C:\inetpub\wwwroot directory on my machine. That name is arbitrary and you can use whatever name you like if you want to run the demo Web applications.

I launched Notepad using the “Run as administrator”option so I’d be able to save my code under the protected C:\inetpub root directory. I named the application PushpinsAndPolygonsDemo.html, but the Bing Maps 8 library has no required naming conventions, so you can use a different filename if you wish.

The overall structure of the Web application is shown in Figure 2. Here’s a highly abbreviated version of the structure:

<html>
  <head>
    <script type=‘text/javascript’>
      // All JavaScript here
    </script>
  </head>
  <body>
    <!-- all HTML here -->
    <script type='text/javascript'
      src='https://www.bing.com/api/maps/mapcontrol?callback=GetMap'
      async defer></script>
  </body>
</html>

The key code is the <script> tag located at the bottom of the <body> section. You can loosely interpret this to mean, “Load the basic Bing Maps 8 library asynchronously while the HTML is rendering. When the library has loaded, transfer control to a JavaScript function named GetMap.” It’s possible to load the Bing Maps 8 library synchronously, but an asynchronous load gives a better UX in situations where the library is slow to load.

Figure 2 Pushpins and Polygons Demo Web Page Structure

<!DOCTYPE html>
<!-- PushpinsAndPolygonsDemo.html -->
<html>
  <head>
    <title>Bing Maps 8 Pushpins with Infoboxes</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <script type="text/javascript">
    var map = null;
    var pushpins = [];
    var infobox = null;  // Shared infobox for all pushpins
    var ppLayer = null;  // Pushpin layer
    var drawingManager = null;
    var drawnShapes = null;  // an array
    function GetMap() { . . }
    function AddDrawControlEvents(manager) { . . }
    function WriteLn(txt) { . . }
    function LatLonStr(loc) { . . }
    function Button1_Click() { . . }
    function Button2_Click() { . . }
    function ShowInfobox(e) { . . }
    function HideInfobox(e) { . . }
    function CreateCvsDot(radius, clr) { . . }
    </script>
  </head>
  <body style="background-color:wheat">
    <div id='controlPanel' style="float:left; width:262px; height:580px;
      border:1px solid green; padding:10px; background-color: beige">
      <input type="file" id="file1" size="24"></input>
      <span style="display:block; height:10px"></span>
      <input id="button1" type='button' style="width:125px;"
        value='Place Pushpins' onclick="Button1_Click();"></input>
      <div style="width:2px; display:inline-block"></div>
      <input id="textbox1" type='text' size='15' value=' (not used)'></input><br/>
      <span style="display:block; height:10px"></span>
      <input id="button2" type='button' style="width:125px;"
        value='Drawn Shape Info' onclick="Button2_Click();"></input>
      <div style="width:2px; display:inline-block"></div>
      <input id="textbox2" type='text' size='15' value=' (not used)'></input><br/>
      <span style="display:block; height:10px"></span>
      <textarea id='msgArea' rows="34" cols="36"
        style="font-family:Consolas; font-size:12px"></textarea>
    </div>
    <div style="float:left; width:10px; height:600px">
    <div id='mapDiv' style="float:left; width:700px; height:600px;
      border:1px solid red;"></div>
    <br style="clear: left;" />  <!-- magic formatting -->
    <script type='text/javascript'
      src='https://www.bing.com/api/maps/mapcontrol?callback=GetMap'
      async defer></script>
  </body>
</html>

The overall layout of the Web page consists of two side-by-side floating <div> areas. The left-side <div> holds the HTML controls. The right-side <div> holds the Map object:

<div id='mapDiv' style="float:left; width:700px; height:600px;
  border:1px solid red;">
</div>

It’s possible to have multiple Map objects for specialized scenarios. Instead of specifying the map width and height using pixel units, you can also use the CSS3 viewport units, vw and vh. For simplicity, I embed all HTML styling directly rather than using a separate CSS file, at the minor expense of a bit of messiness.

To summarize, a Bing Maps 8 map object is created using a program-defined JavaScript function, and is placed in an HTML <div> area that specifies the size of the map. You can load a map synchronously or asynchronously.

Initializing the Map Object

The Web application sets up six global script-scope objects:

var map = null;
var pushpins = [];
var infobox = null;
var ppLayer = null;
var drawingManager = null;
var drawnShapes = null;

When I create a mapping application, I tend to think of the architecture as similar to a large C# or Java class, and so the script-scope JavaScript objects are typically those that are used by two or more functions. However, because of the JavaScript language’s quirks and heavy use of callback functions and closures, I’ll sometimes place objects that only need function-scope into the script-scope area.

The object named map is the Map object and although that name isn’t required, it’s more or less standard. The pushpins object is an array that will hold all the pushpins. I initialize the object to an empty array here, as opposed to setting it to null, mostly to indicate that the object is an array. The infobox object is a single instance of the Infobox class that will be shared by all pushpins.

One of the new features of Bing Maps 8 is the Layer class. Instead of placing all visual entities into one monolithic collection, it’s now possible to organize visual objects into layers. The drawing­Manager object is a reference to the DrawingTools control. The drawnShapes object is an array that will hold the Polygon object shapes drawn by a user.

The map is initialized by function GetMap. The definition begins with:

function GetMap()
{
  var options = {
    credentials: "AmUck2_xxxx_jSCm",
    center: new Microsoft.Maps.Location(45.50, -122.50),
    mapTypeId: Microsoft.Maps.MapTypeId.road,
    zoom: 10, enableClickableLogo: false, showCopyright: false
  };
...

Note that I like to capitalize the names of my program-defined functions to distinguish them from library functions or built-in JavaScript functions. The code here defines some of the initial map settings. All of these are optional, even the credentials item, which is essentially a Bing Maps key. If you don’t have a key, you can use any string there and your map will still load and be functional, but there’ll be a thin strip across your map with a message, “The specified credentials are invalid. You can sign up for a free developer account at https://www.bingmapsportal.com.” Creating an account to get a key is relatively painless, but if you’re impatient like me and you want to get started right away, you can sign up later.

The map’s center property is set using a Location object, which accepts a latitude, followed by a longitude, followed optionally by two values related to altitude. If you’re new to geo-applications, you have to be a bit careful. With a normal geometry point (x, y) the x value is the “left-right” value, but in geo-applications the latitude is the “up-down” value.

Next, the map is displayed and the master Infobox object is prepared:

var mapDiv = document.getElementById("mapDiv");
map = new Microsoft.Maps.Map(mapDiv, options);
infobox = new Microsoft.Maps.Infobox(new Microsoft.Maps.Location(0, 0),
  { visible: false, offset: new Microsoft.Maps.Point(0,0) });
infobox.setMap(map);

One of the nice things about Bing Maps is that the API set uses variable and parameter names that are, for the most part, quite understandable. The Infobox is placed at Location (0, 0), which is just a dummy location because the visible property is set to false. The offset property controls the positioning of the small triangular pointer at the bottom of the Infobox object. The default value is (0, 0), so I could’ve omitted it.

Next, the pushpins are prepared:

ppLayer = new Microsoft.Maps.Layer();
var cpp= new Microsoft.Maps.Pushpin(map.getCenter(), null);
ppLayer.add(cpp);
map.layers.insert(ppLayer);

The ppLayer (“pushpin layer”) object defines a visual layer where all the pushpins will be stored. The cpp (“center pushpin”) is added to the Layer and then the Layer is added into the map making the pushpin visible. The second parameter to the Pushpin constructor, which is null here, can be a PushpinOptions object, which will be explained shortly. Passing a value of null gives you a default Pushpin object, which is purple and has a radius of about 10 pixels.

Bing Maps 8 supports the older mechanism of placing all visual objects into one global collection. The code would look like:

map.entities.push(cpp);

The GetMap function finishes by creating the DrawingTools control and placing it onto the map:

...
    Microsoft.Maps.loadModule('Microsoft.Maps.DrawingTools', function() {
    var tools = new Microsoft.Maps.DrawingTools(map);
    tools.showDrawingManager(AddDrawControlEvents);
  });
}

A big architecture change for Bing Maps 8 is that the library is now organized into 11 modules. This allows you to load only those modules you need, which can significantly improve performance. Representative other modules include Search, SpatialMath and HeatMap.

The loadModule function accepts the name of the module to load, plus a callback function definition that contains code to execute after the module has loaded. It can take a while to become comfortable with callback functions, but like anything else, after a few examples you get the hang of using them.

The showDrawingManager function also accepts a callback function, this time using a name (AddDrawControlEvents) rather than an anonymous function. Function AddDrawControlEvents is defined as:

function AddDrawControlEvents(manager)
{
  Microsoft.Maps.Events.addHandler(manager, 'drawingStarted',
    function(e) { WriteLn('Drawing has started'); });
  Microsoft.Maps.Events.addHandler(manager, 'drawingEnded',
    function(e) { WriteLn(‘Drawing has ended \n’); });
  drawingManager = manager;
}

This code is short but rather subtle. In words, “When a user starts drawing a shape using the DrawingTools control and the drawingStarted event automatically fires, place a message using a program-defined function named WriteLn.” The Events.add­Handler function accepts an event-firing object and a callback function. The event argument, e, isn’t used in the demo but it represents the drawn shape.

Program-defined function WriteLn is defined as:

function WriteLn(txt)
{
  var existing = msgArea.value;
  msgArea.value = existing + txt + "\n";
}

The msgArea object is an HTML textarea tag on the left side of the Web page. The approach used here of grabbing the existing content and then replacing it with appended text is rather crude but works well as long as the amount of text doesn’t get huge.

Creating and Displaying Custom Pushpins

When a user clicks on the button control labeled Place Pushpins, control is passed to the Button1_Click function. The structure of the function is:

function Button1_Click()
{
  var f = file1.files[0];
  var reader = new FileReader();
  reader.onload = function(e) {
    // Parse each line of result
    // Create pushpins
    // Add event handlers for pushpins
    // Display pushpins
  }
  reader.readAsText(f);
}

Local object f has information about the physical file pointed to by the browsing control of the HTML file1 object. Because the HTML File API allows multiple files to be selected, the first file is accessed as files[0]. The FileReader object will load a file asynchronously so the Web page will remain responsive. The onload event will fire when the file has been read into memory. Notice that you define what to do after the file is read and then you call the readAsText function to actually start reading the file.

The anonymous function that executes when the onload event fires begins with:

WriteLn("Source data = \n");
var lines = reader.result.split('\n');
for (var i = 0; i < lines.length; ++i) {
  var line = lines[i];
...

The file contents are stored in the reader.result object as one giant string with embedded ‘\n’ characters. The String.split function is used to extract each line into an array. Then the lines are iterated through using a for-loop with the length property. Next:

var tokens = line.split(',');
WriteLn(tokens[0] + " " + tokens[1] + " " + tokens[2]);
var loc = new Microsoft.Maps.Location(tokens[0], tokens[1]);

Recall that a line of the data file looks like:

45.46,-122.90,first location data

Each line is split on the comma delimiter and the three results are stored into an array named tokens, so the latitude is at tokens[0] and the longitude is at tokens[1]. Because a lot can go wrong when reading a text file, in a production system you’d likely wrap the attempt to create a Location object in a JavaScript try-catch block.

Next, a custom pushpin is created for the data of the current line of text:

var ppOptions = { icon: CreateCvsDot(6, "orangered"),
  anchor: new Microsoft.Maps.Point(6,6), subTitle: tokens[2] };
var pp = new Microsoft.Maps.Pushpin(loc, ppOptions);
pushpins[i] = pp;

Custom pushpins are created by passing information to the icon property of a PushpinOptions object. Here, a custom orange-red color icon with a radius of 6 pixels is created by calling a program-defined function named CreateCvsDot. I also set the subTitle property of the current pushpin to the text from the data file that follows the lat-lon fields. After the pushpin is created, it’s added to the global pushpins array.

The anonymous function code finishes with:

...
  Microsoft.Maps.Events.addHandler(pushpins[i], 'mouseover', ShowInfobox);
  Microsoft.Maps.Events.addHandler(pushpins[i], 'mouseout', HideInfobox);
}
ppLayer.add(pushpins);
map.layers.insert(ppLayer);
WriteLn("");

Each pushpin has its mouseover and mouseout events modified using program-defined functions ShowInfobox and HideInfobox. After all pushpins have been created, the array holding them is adding to the pushpin Layer, which is then inserted into the map, which makes the pushpins visible.

Function CreateCvsDot (“create HTML canvas dot”) is defined as:

function CreateCvsDot(radius, clr)  {
  var c = document.createElement('canvas');
  c.width = 2 * radius; c.height = 2 * radius;
  var ctx = c.getContext("2d");
  ctx.beginPath();
  ctx.arc(radius, radius, radius, 0, 2 * Math.PI);
  ctx.fillStyle = clr; ctx.fill();
  return(c.toDataURL());
}

The function accepts a radius and a color and returns an HTML5 canvas object. There are four ways to create a custom pushpin icon. You can use a static image such as a .png file; you can use a static image encoded using Base64 format; you can create a dynamic HTML canvas object; or you can create a dynamic scalable vector graphics (SVG) object.

The ability to create a pushpin icon on the fly gives you a lot of flexibility. For example, you could create different color and size icons depending on the density of pushpins in an area of your map, or depending on the zoom level of the map.

Event-handler function ShowInfobox is defined as:

function ShowInfobox(e)
{
  var loc = e.target.getLocation();
  WriteLn('\n mouseover at ' + loc);
  infobox.setLocation(loc);
  infobox.setOptions( { visible: true, title: e.target.getSubTitle(),
    description: LatLonStr(loc) });
}

When the user moves the mouse cursor over a pushpin, the pushpin’s mouseover event will fire and control will transfer to ShowInfobox. The function gets the Location of the event/pushpin and uses it to place the pushpin. Recall that the subTitle property of each pushpin holds text such as “first data location.” This text is used as the Infobox title.

The description property of the Infobox is set to the location of the pushpin, formatted to two decimal places using the program-defined helper function LatLonStr:

function LatLonStr(loc)
{
  var s = "(" + Number(loc.latitude).toFixed(2) + ", " +
    Number(loc.longitude).toFixed(2) + ")";
  return s;
}

The HideInfobox function is:

function HideInfobox(e)
{
  WriteLn(' mouseout at ' + e.target.getLocation());
  infobox.setOptions({ visible: false });
}

When the user moves the mouse cursor away from a pushpin, the pushpin’s mouseout event will fire and control will transfer to HideInfobox. The visible property is set to false so the Infobox isn’t visible but is still in the map.

Retrieving Interactive Shapes

When a user clicks on the button control labeled Drawn Shapes Info, control is transferred to the Button2_Click function. The function is defined as:

function Button2_Click()
{
  drawnShapes = drawingManager.getPrimitives();
  var numShapes = drawnShapes.length;
  var mostRecent = drawnShapes[numShapes-1];  // Polygon
  var vertices = mostRecent.getLocations();
  WriteLn("There are " + numShapes + " drawn shapes");
  WriteLn("Vertices of most recent drawn shape: ");
  for (var i = 0; i < vertices.length; ++i) {
    WriteLn(LatLonStr(vertices[i]));
  }
}

The global drawingManager object was created when the Drawing­Tools control was placed on the map. It’s used to fetch an array containing all shapes drawn by the drawing control. The last shape drawn will be the last item in the array. The code assumes the drawn shapes are type Polygon, but the DrawingTool control can create different types of objects. You could check the shape type with code like:

var isPoly = mostRecent instanceof Microsoft.Maps.Polygon;

The function finishes by fetching the vertices of the last drawn shape using the getLocations function, and iterating through the vertices to display them.

The Heat Map Demo

When I work with geo-applications, I mentally categorize them according to the number of data points with which I’m dealing. Working with a large number of locations can be challenging. The Bing Maps 8 library has two very nice ways to work with a large number of locations—clustered pushpins and heat maps. Take a look at the demo heat map in Figure 3.

Heat Map Demo
Figure 3 Heat Map Demo

There are several kinds of heat maps, but one common type displays combined data points using a color gradient where different colors represent different data densities. The demo Web application initially loads a map centered at (37.50, -118.00) and places a default large purple pushpin at center.

First, I clicked on the HTML5 File Browse button and pointed to a local file named NV_Cities.txt containing city data. Next, I clicked on the first button control, which loaded and displayed a heat map for city density in the state of Nevada. Then I cleared that heat map using the second button control.

Next, I clicked on the Browse button control again and pointed to a tab-separated text file named CA_Cities.txt. That data file contains a list of 1,522 cities in California and their corresponding latitude-longitude information. Then I clicked on the Show Heat Map button control, which read the text file, parsed out the lat-lon data and stored that data into an array. The lat-lon data was then displayed as a heat map, generating a city density visualization.

The structure of the heat map demo application is almost exactly like the structure of the pushpins and polygons demo. The global script-scope objects are:

var map = null;
var ppLayer = null;
var hmLayer = null;
var reader = null;   // FileReader object
var locs = [];
var cGrad = { '0.0': 'black', '0.2': 'purple', '0.4': 'blue', '0.6': 'green', 
  '0.8': 'yellow', '0.9': 'orange', '1.0': 'red' };
var hmOptions = { intensity: 0.65, radius: 7, colorGradient: cGrad };

The hmLayer object is a Layer for the heat map. The locs array holds the Location objects that define the heat map. The cGrad object defines a custom color gradient for the heat map options. The hmOptions define the options for the heat map. Using a custom HeatMapOptions object is optional, but in most situations you’ll want to use the options to control the appearance of your heat map.

Here’s the code in function Button1_Click that reads and parses the source data file:

var lines = reader.result.split('\n');
for (var i = 0; i < lines.length; ++i) {  // Each line
  var line = lines[i];
  var tokens = line.split('\t');  // Split on tabs
  var loc = new Microsoft.Maps.Location(tokens[12], tokens[13]);
  locs[i] = loc;
}

The source data files look like:

CA  602000  2409704  Anaheim city    ( . . )  33.855497  -117.760071
CA  602028  2628706  Anchor Bay CDP  ( . . )  38.812653  -123.570267
...

Each line has 14 tab-delimited values. The first value is the state abbreviation. The next two fields are IDs. The fourth field is the place name, which can be a city, a town or a census-designated place (CDP). Then there are eight fields that include information such as U.S. census population count and land area. The last two fields are the latitude and longitude. I got the data from the U.S. Census Web site at bit.ly/29SETIU.

The code that creates and displays the heat map is:

Microsoft.Maps.loadModule('Microsoft.Maps.HeatMap', function() {
  hmLayer = new Microsoft.Maps.HeatMapLayer(locs, hmOptions);
  map.layers.insert(hmLayer);
});

The hmLayer Layer is created using the global array of Location objects and the hmOptions object that contains the custom color gradient. Very nice!

The code for function Button2_Click removes the current heat map:

function Button2_Click()
{
  WriteLn('Clearing heat map' + "\n");
  hmLayer.clear();
  reader = null;
  locs = [];
}

This code illustrates one of the advantages of working with Layer objects. Instead of having to iterate through every object in the Map.entities collection, you can directly access objects in a particular layer.

Pushpin Clustering

One of my favorite new features in Bing Maps 8 is pushpin clustering. The idea is best explained visually. In Figure 4, the Web page named ClusteredPushpinsDemo.html loads a map with an initial zoom level of 10, centered near Portland, Ore. When I clicked on the button control labeled Generate Pins, the application used the getLocations function in the Maps.TestDataGenerator to create 6,000 random locations. Then the application code created clustered pushpins and displayed them. Red circles indicate there are 100 or more pushpins in the associated map area, and blue pushpins indicate 10 to 99 pushpins.

Pushpin Clustering with Zoom Level 10
Figure 4 Pushpin Clustering with Zoom Level 10

Next, I zoomed in three levels. Clustering automatically occurs at each zoom change. At zoom level 13 (see Figure 5) the individual pushpins become visible as small red dots, and green circles indicate there are two to nine pushpins at that location.

Pushpin Clustering with Zoom Level 13
Figure 5 Pushpin Clustering with Zoom Level 13

Both heat maps and pushpins clustering enable you to manage a large number of location items. But using pushpin clustering allows users to access individual items.

Wrapping Up

The demo Web applications presented here should give you a good idea of what the new Big Maps 8 library is like. There are many additional new features that I didn’t cover, including Infobox customization, tile layers, geo-search and spatial math functions. If you want to learn more about Bing Maps 8, I recommend going to the interactive SDK Web site at binged.it/29SFytX. It presents approximately 137 very short but complete Web pages that illustrate many key features of Bing Maps 8. You’ll also find that the official documentation at aka.ms/BingMapsV8Docs is very well-written and useful.

I’ve used two of the main alternatives to the Bing Maps 8 library, the Google Maps library, and the open source Leaflet.js library. All three libraries are excellent, but I really like Bing Maps 8. Some technologies just have a “right feel” to them and for me, at least, Bing Maps 8 is now my preferred library for geo-applications.


Dr. James McCaffrey works for Microsoft Research in Redmond, Wash. He has worked on several Microsoft products including Internet Explorer and Bing. He can be reached at <jammc@microsoft.com.>

Thanks to the following Microsoft technical experts who reviewed this article: Ricky Brundritt (Bing Maps) and John Krumm (Microsoft Research)


Discuss this article in the MSDN Magazine forum