GEOG5870/1M: Web-based GIS A course on web-based mapping

Adding information to the markers

In the previous section, we tidied up the code of our Google Maps example. In this section, we'll make the markers more useful, by adding additional information to the marker. We wish to add a pop-up information window to markers, so that they can tell the user appropriate information. Let's start by doing this for one marker.

Single marker with an infowindow

As noted in the introduction to JavaScript, JavaScript has events that can be used to trigger various actions (such as acting when a marker has been clicked). There are various event listener names defined generically in JavaScript; we have already used one of these: the onload listener name, that we have attached to the BODY tag of HTML code. In that case, the BODY generates the onload event, and the browser looks for the onload listener name to pass the event to; the event handling function attached to that name then runs. Various additional listener names are also provided by the Google Maps API.

While building handlers into HTML this way works for most things, what if we don't write the associated event generating object in HTML? With a marker, the code isn't in the HTML, it's in the JavaScript. For this, we need a slightly different approach. The code below shows how we can add a listener/handler for a click on a marker built within the JavaScript, rather than HTML (and this would be the case even if the JavaScript was embedded inside the HTML). We're going to use this click to run a function that opens an information window.

The code shows a revised map setup file; it should be loaded by an HTML file similar to those shown previously (but, obviously, with the script src parameter changed appropriately).

var map; // The map object
var myCentreLat = 53.807767;
var myCentreLng = -1.557428;
var initialZoom = 12;

function initialize() {
   var latlng = new google.maps.LatLng(myCentreLat,myCentreLng);
   var myOptions = {
      zoom: initialZoom,
      center: latlng,
      mapTypeId: google.maps.MapTypeId.ROADMAP
   };
   map =
      new google.maps.Map(document.getElementById("map_canvas"),
      myOptions);

   var marker = new google.maps.Marker({
      position: latlng,
      map: map,
      title:"Hello World!"
   });

   var marker = new google.maps.Marker({
      position: new google.maps.LatLng(53.81,-1.56),
      map: map,
      title:"And another point"
   });

   /*
   * Create a new infowindow object
   */
   var infowindow = new google.maps.InfoWindow({
      content: "The first marker"
   });

   // Attach it to the marker we've just added
   google.maps.event.addListener(marker, 'click', function() {
      infowindow.open(map,marker);
   });

}

Here's the code for download.

In this example, we have returned to using a single marker (as we shall soon see, adding multiple infowindows to multiple markers requires a slightly revised process...). This is defined and created exactly as before. Two additional statements are added at the end of the script. Firstly, we declare a new variable infowindow, as an instance of the object google.maps.InfoWindow(). When we construct this we can supply a number of properties, but for this example we use only the 'content' property. This statement has created an infowindow object, but as yet it is not associated with a marker (or any events):

   var infowindow = new google.maps.InfoWindow({
      content: "The first marker"
   });

The second new statement uses the method google.maps.event.addListener() to run a function when the marker is clicked. This function will open the infowindow. This is quite a complicated set of nested code elements, so let's break it down.

   google.maps.event.addListener(marker, 'click', function() {
      infowindow.open(map,marker);
   });

There are three parameters that must be set in addListener(): the object that the listener is linked to (in this example, the marker variable), the event that is listened for (in this case, the 'click' event), and a function to call when the event is triggered. In this case, the function is defined within the addListener statement (remembering from the JavaScript introduction that we can pass functions into other functions as parameters, and that we don't even need to give them a name). The fragment:

function () {infowindow.open(map,marker);}

is included as the third parameter, and it uses the function () syntax we saw in the JavaScript intro to define an unamed function that we wish to pass in as a parameter. The source code for the function is placed within curly brackets, and it consists of a single statement:

infowindow.open(map,marker);

You will recall that the variable infowindow (created in the first of the additional statements) is an instance of the InfoWindow object that is defined as part of the Google Maps API. This object has a number of defined methods, including the method open(). This method requires the name of a Map object and, optionally the name of an object that the infowindow will be placed next to (usually a Marker object). Note that the first parameter of the addListener() method identifies the object that needs to be clicked for the function to be triggered, whilst the second parameter of InfoWindow.open() affects the placement of the infowindow. Normally these will be the same object – you want the infowindow to pop up next to the marker that you have clicked.

If we nest all these elements inside each other, which is a common practice in scripting languages, we get our statement:

   google.maps.event.addListener(marker, 'click', function() {
      infowindow.open(map,marker);
   });

Multiple markers with infowindows

Clearly, we often want to add more than one marker and more than one infowindow to a map. In order to do so, we have to use an additional step when creating the markers. We're going to break this down a little, concentrating on two markers on this page, and more on the next. This is so we can show the new code with just two markers here, and then add in more generic code for multiple markers afterwards. The following code shows the map setup code for a map with two markers, each with attached infowindows.

var map; // The map object
var myCentreLat = 53.807767;
var myCentreLng = -1.557428;
var initialZoom = 12;

function infoCallback(infowindow, marker) {
   return function() { infowindow.open(map, marker); };
}

function initialize() {
   var latlng = new google.maps.LatLng(myCentreLat,myCentreLng);
   var myOptions = {
      zoom: initialZoom,
      center: latlng,
      mapTypeId: google.maps.MapTypeId.ROADMAP
   };
   map =
      new google.maps.Map(document.getElementById("map_canvas"),
      myOptions);

   // First marker
   var marker = new google.maps.Marker({
      position: new google.maps.LatLng(53.7996388,-1.5491221),
      map: map,
      title:"Leeds"
   });

   // First infowindow
   var infowindow = new google.maps.InfoWindow({
      content: "<div class=infowindow><h1>Leeds</h1><p>Population: 715,402</p></div>"
   });

   // Attach it to the marker we've just added
   google.maps.event.addListener(marker, 'click', infoCallback(infowindow, marker));

   // Second marker
   var marker = new google.maps.Marker({
      position: new google.maps.LatLng(53.7938530,-1.7524422),
      map: map,
      title:"Bradford"
   });

   // Second infowindow
   var infowindow = new google.maps.InfoWindow({
      content: "<div class=infowindow>
      <h1>Bradford</h1><p>Population: 467,665</p></div>"
   });

   google.maps.event.addListener(marker,
      'click', infoCallback(infowindow, marker));

}

Here's the code for downloading.

The first thing to notice is that we make two markers and two infowindows, but we use the same variable names. This is so that later (on the next page), we can build a function that takes in some data and sets up a marker for each set of data, internally repeatedly using the same variable name – something a little like this (but more complicated):

function makeMarker (a,b,c) {
   var marker = new Marker(a,b,c);
   // do stuff with marker
}

The problem, as we'll see, is that using the same name repeatedly in JavaScript can become problematic, even if we're redeclaring the variable with var. Specifically, markers will mysteriously pick up only the last infowindow, all gaining exactly the same information appearing in exactly the same place. We could solve these problems for a two-marker version by just calling them "marker1", "marker2" and "infowindow1", "infowindow2", but that doesn't help us with the multiple-marker situation, so let's stick with using the same names, and see if we can identify and solve the issue with just two markers. The simplest (though perhaps not the most intuitive) way to understand this, is to look at the solution, then see the issue it is trying to solve.

The first part of the solution is that we have added an additional function, infoCallback(). This is a function whose job it is to create and return a new function. Again, this is quite complicated, so let's break it down.

First, what is a "callback" function? A "callback" is a function that is passed into another with the expection that it will be run ("called back"), usually at some later point in time. We've already seen a very simple example, when we passed a nameless function into our addListener code in the single marker example. Callbacks are quite popular in web-based programming where we might want to set up code when a page is loaded, but only enact it at some later point in time. You can find an example that advances on the event handling example on Wikipedia. Here, we are, again, passing the callback into addListener:

   google.maps.event.addListener(marker,
      'click', infoCallback(infowindow, marker));

The only additional complexity being that the callback function, when run, is going to return a function that opens the infowindow passed in:

function infoCallback(infowindow, marker) {
   return function() { infowindow.open(map, marker); };
}

The function that is created and returned is identical to that used to open the infowindow earlier, so why the additional step? To understand why, consider the original callback from the single marker example:

   google.maps.event.addListener(marker, 'click', function() {
      infowindow.open(map,marker);
   });

We could run this multiple times, but the problem is that the callback function only runs when the click occurs. At that point, the browser runs off to find out which infowindow to run. Trouble is, if we've set this up multiple times, the infowindow variable has moved on, and is now another infowindow. When we click, that's therefore the infowindow we get. This horrorstory is a result of the variable infowindow and the code immediately above being all in the same scope.

The solution is to step outside this scope to create the function that opens the infowindow. Here we do this by getting another function (infoCallback), external to this scope, to generate the function we want to run. This forces the generation of a new copy of the variable, made at the time the function is first discussed. This new copy of the variable doesn't change when the old one does, as it is in a different scope.

The issue is mainly caused by the fact that we can pass functions as parameters. This has the potential to blur the boundaries of scoping. The solution adopted by JavaScript is something called closures, a record of which environment (a set of states and variables) a variable sits in or has access to – stepping outside the scope as described above, generates a new closure. If you're interested, you can read more about it here (see, especially, the section Creating closures in loops: A common mistake). Suffice it to say, this is just an issue to be aware of, and a pattern to solve it if you see it: watch out for the last thing you set up in a list of things coming up for all of them.

Anyhow, back to the code. This code will make a map with two markers. The process for creating each one is the same: a new marker is made, with a position, a map object and a title as before. In this example, we are providing new positions (not the same as the map centre) for each marker.

   var marker = new google.maps.Marker({
      position: new google.maps.LatLng(53.7996388,-1.5491221),
      map: map,
      title:"Leeds"
   });

We then create an infowindow as before.

   var infowindow = new google.maps.InfoWindow({
      content: "<div class=infowindow>
      <h1>Leeds</h1><p>Population: 715,402</p></div>"
   });

In these examples, the content includes embedded HTML tags, to start to improve the formatting of the infowindow. Note that we have created a division for each window (using the DIV tag) and given this the class 'infowindow'. This will allow us to define styles in CSS that are only used for these windows. At the moment, we have not yet defined any such class in the CSS file, so the window is displayed with the default styling. We have also started to add some information to these windows – namely the populations, although we have not properly cited the source! *cough*

The final stage for each infowindow is to attach it to a marker using the addListener(). This is done using the same method as previously, with the difference that we indicate our new infoCallback() function is to be used, rather than directly defining a new function.

   google.maps.event.addListener(marker,
      'click', infoCallback(infowindow, marker));

Whilst we use exactly the same function name for each marker, the crucial difference is that each new function created via infoCallback() will inherit the program environment that existed at the time of its creation, including the 'right' instance of the marker variable.

This is all fairly complicated stuff, so, even if you don't entirely understand it at the moment, look through it and get a feel for how the code is constructed and what it is trying to achieve, so that you'll recognise this form of solution for this form of problem when you come across it.


[ Next: Code Efficiency ]
[Course Index]