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.
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);
});
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);
});
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.