The graphic that we want to move with the mouse over the map is similar to the mask used in Chapter 3 to turn the rectangular video clip into a circular video clip. Both masks can be described as resembling a rectangular donut: a rectangle with a round hole. We draw the graphics for the shadow/spotlight using two paths, just like the mask for the video in the previous chapter. There are two distinct differences, however, between the two situations:
• The exact shape of this mask varies. The outer boundary is the whole canvas, and the location of the hole is aligned with the current position of the mouse. The hole moves around.
• The color of the mask is not solid paint, but a transparent gray.
The canvas starts out on top of the Google Map. I accomplish this by writing style directives that set the z-index values:
canvas {position:absolute; top: 165px; left:0px; z-index:100;}
#place {position:absolute; top: 165px; left: 0px; z-index:1;}
The first directive refers to all canvas elements. There is only one in this HTML document. Recall that the z-axis comes out of the screen toward the viewer, so higher values are on top of lower values.
Note also that we use zIndex in the JavaScript code and z-index in the CSS. The JavaScript parser would treat the – sign as a minus operator, so the change to zIndex is necessary. I will need to write code that changes the zIndex to get the event handling that I want for this project.
Figure 4-16 shows one example of the shadow mask drawn on the canvas. The canvas is over the map in terms of the z-index, and the mask is drawn with a gray color that is transparent so the map underneath is visible.
Figure 4-16. Shadow/spotlight on one place on the map
Figure 4-17. Shadow mask over another position on the map
Several topics are interlinked here. Let’s assume that the variables mx and my hold the position of the mouse cursor on the canvas. I will explain how later in this chapter. The function drawshadowmask will draw the shadow mask. The transparent gray that is the color of the mask is defined in a variable I named grayshadow and constructed using the built-in function rgba. The rgba stands for red-green-blue-alpha. The alpha refers to the transparency/opacity. A value of 1 for alpha means that the color is fully opaque: solid. A value of 0 means that it is fully transparent—the color is not visible. Recall also that the red, green, and blue values go from 0 to 255, and the combination of 255, 255, and 255 would be white.
This is a time for experimentation. I decided on the following setting for the gray/grayish/ghostlike shadow:
var grayshadow = "rgba(250,250,250,.8)";
The function drawshadowmask makes use of several variables that are constants—they never change.
A schematic indicating the values is shown in Figure 4-18.
Figure 4-18. Schematic with variable values indicated for mask
The mask is drawn in two parts as was done for the mask for the bouncing video. You may look back to Figure 3-8 and Figure 3-9. The coding is similar:
function drawshadowmask(mx,my) { ctx.clearRect(0,0,600,400);
ctx.fillStyle = grayshadow;
ctx.beginPath();
ctx.moveTo(canvasAx,canvasAy);
ctx.lineTo(canvasBx,canvasBy);
ctx.lineTo(canvasBx,my);
ctx.lineTo(mx+holerad,my);
ctx.arc(mx,my,holerad,0,Math.PI,true);
ctx.lineTo(canvasAx,my);
ctx.lineTo(canvasAx,canvasAy);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(canvasAx,my);
ctx.lineTo(canvasDx,canvasDy);
ctx.lineTo(canvasCx,canvasCy);
ctx.lineTo(canvasBx,my);
ctx.lineTo(mx+holerad,my);
ctx.arc(mx,my,holerad,0,Math.PI,false);
ctx.fill();
}
Now we move on to the red lightbulb.
Cursor
The cursor—the small graphic that moves on the screen when you move the mouse—can be set in the style element or in JavaScript. There are several built-in choices for the graphic (e.g., crosshair, pointer), and we also can refer to our own designs for a custom-made cursor, which is what I demonstrate in this project. I included the statement
can.onmousedown = function () { return false; } ;
in the init function to prevent a change to the default cursor when pressing down on the mouse. This may not be necessary since the default may not be triggered.
To change the cursor for moving the mouse to something that conveyed a spotlight, I created a picture of a red compact fluorescent lightbulb and saved it in the file light.gif. I then used the following statement in the function showshadow. The showshadow function has been set as the event handler for mousemove:
can.style.cursor = "url('light.gif'), pointer";
to indicate that JavaScript should use that address for the image for the cursor when on top of the can element. Furthermore, if the file 'light.gif' is not available, the statement directs JavaScript to use the built-in pointer icon. This is similar to the way that fonts can be specified with a priority listing of choices. The variable can has been set to reference the canvas element. The cursor will not be used when the canvas has been pushed under the Google Map, as will be discussed in the next section.
Events
The handling of events—namely mouse events, but also events for changing the slider on the Google Map or clicking the radio buttons—seemed the most daunting when I started work on this project.
However, the actual implementation turned out to be straightforward. In the init function, I write code to set up event handling for movement of the mouse, mouse button down, and mouse button up, all regarding the canvas element:
can.onmousedown = function () { return false; } ; can.addEventListener('mousemove',showshadow,true);
can.addEventListener('mousedown',pushcanvasunder,true);
can.addEventListener("mouseout",clearshadow,true);
The true value for the third parameter indicates that this event is to bubble, meaning that it is to signal other listeners. However, more work was needed to achieve the event handling I wanted for this project. I will explain the three functions and then go on to describe one more event.
The showshadow function, as indicated previously, calls the drawshadowmask function. I could have combined these two functions, but dividing tasks into smaller tasks generally is a good practice. The showshadow function determines the mouse position, makes an adjustment so the lightbulb base is at the center of the spotlight, and then makes the call to drawshadowmask:
function showshadow(ev) {
can.style.cursor = "url('light.gif'), pointer";
mx = mx+10;
my = my + 12;
drawshadowmask(mx,my);
}
Now I needed to think what I wanted to do when the user pressed down on the mouse. I decided that I wanted the shadow to go away and the map to be displayed in its full brightness. In addition to the appearance of things, I also wanted the Google Maps API to resume control. A critical reason for wanting the Google Maps API to take over is that I wanted to place a marker on the map, as opposed to the canvas, to mark a location. This is because I wanted the marker to move with the map, and that would be very difficult to do by drawing on the canvas. I would need to synchronize the marker on the canvas with panning and zooming of the map. Instead, the API does all this for me. In addition, I needed the Google Maps API to produce latitude and longitude values for the location.
The way to put Google Maps back in control, so to speak, was to “push the canvas under.” The function is
function pushcanvasunder(ev) { can.style.zIndex = 1;
pl.style.zIndex = 100;
}
The operation of pushing the canvas under or bringing it back on top is not instantaneous. I am open to suggestions on (1) how to define the interface and (2) how to implement what you have defined.
There is room for improvement here.
One more situation to take care of is what I want to occur if and when the user moves the mouse off from the canvas? The mouseout event is available as something to be listened for, so I wrote the code setting up the event (see the can.addEventListener statements shown above) to be handled by the clearshadow function. The clearshadow function accomplishes just that: clearing the whole canvas, including the shadow:
function clearshadow(ev) {
ctx.clearRect(0,0,600,400);
}
In the function that brings in the Google Map, I set up an event handler for mouseup for maps.
listener = google.maps.event.addListener(map, 'mouseup', function(event) { checkit(event.latLng);
});
event object as a parameter. As you can guess, event.latLng is the latitude and longitude values at the position of the mouse when the mouse button was released on the map object. The checkit function will use those values to calculate the distance from the base location and to print out the values along with the distance on the screen. The code invokes a function I wrote that rounds the values. I did this to avoid displaying a value with many significant digits, more than is appropriate for this project. The Google Maps API marker method provides a way to use an image of my choosing for the marker, this time a black ,hand-drawn x, and to include a title with the marker. The title is recommended to make applications accessible for people using screen readers, though I cannot claim that this project would satisfy anyone in terms of accessibility. It is possible to produce the screen shown in Figure 4-19.
Figure 4-19. Title indicating distance shown on map
The checkit function, called with a parameter holding the latitude and longitude value, follows:
function checkit(clatlng) {
var distance = dist(clatlng,blatlng);
distance = round(distance,2);
var distanceString = String(distance)+" km";
marker = new google.maps.Marker({
position: clatlng, title: distanceString, icon: bxmarker,
document.getElementById("answer").innerHTML =
"The distance from base to most recent marker ("+clat+", "+clng+") is "+String(distance)
+" km.";
can.style.zIndex = 100;
pl.style.zIndex = 1;
}
Notice that the last thing that the function does is put the canvas back on top of the map.
The CHANGE button and the radio buttons are implemented using standard HTML and JavaScript.
The form is produced using the following HTML coding:
<form name="f" onSubmit=" return changebase();">
<input type="radio" name="loc" /> friends of ED, NYC<br/>
<input type="radio" name="loc" /> Purchase College<br/>
<input type="radio" name="loc" /> Illinois Institute of Technology<br/>
<input type="submit" value="CHANGE">
</form>
The function changebase is invoked when the submit button, labeled CHANGE, is clicked. The changebase function determines which of the radio buttons was checked and uses the Locations table to pick up the latitude and longitude values. It then makes a call to makemap using these values for
parameters. This way of organizing data is called parallel structures: the locations array elements correspond to the radio buttons. The last statement sets the innerHTML of the header element to display text, including the name of the selected base location.
function changebase() {
Google Maps, as many of us know, provides information on distances and even distinguishes between walking and driving. For this application, I needed more control on specifying the two locations for which I wanted the distance calculated, so I decided to develop a function in JavaScript. Determining the distance between two points, each representing latitude and longitude values, is done using the
spherical law of cosines. My source was www.movable-type.co.uk/scripts/latlong.html. Here is the code:
function dist(point1, point2) { var R = 6371; // km
// var R = 3959; // miles
var lat1 = point1.lat()*Math.PI/180;
var lat2 = point2.lat()*Math.PI/180 ; var lon1 = point1.lng()*Math.PI/180;
var lon2 = point2.lng()*Math.PI/180;
var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) + Math.cos(lat1)*Math.cos(lat2) *
Math.cos(lon2-lon1)) * R;
return d;
}
■ Caution I don’t include many comments in the code because I include the tables with each line annotated.
However, comments are important. I strongly recommend leaving the comments on
kmand
milesin the
distfunction so you can adjust your program as appropriate. Alternatively, you could display both values or give the user a choice.
The last function is for rounding values. When a quantity is dependent on a person moving a mouse, you shouldn’t display a value to a great number of decimal places. However, we should keep in mind that latitude and longitude represent big units. I decided I wanted the distances to be shown with two decimal places and the latitude and longitude with four.
The function I wrote is quite general. It takes two parameters, one the number num and the other places, indicating how many decimal places to take the value. You can use it in other circumstances. It rounds up or down, as appropriate, by adding in the value I call the increment and then calculating the biggest integer not bigger than the value. So
• round(9.147,2) will produce 9.15; and
• round(9.143, 2) will produce 9.14.
The way the code works is first to determine what I term the factor, 10 raised to the desired number of places. For 2, this will be 100. I then calculate the increment. For two places, this will be 5 / 100 * 10, which is 5 / 1,000, which is .005. My code does the following:
1. Adds the increment to the original number 2. Multiplies the result by the factor
3. Calculates the largest whole number not bigger than the result (this is called the floor)—producing a whole number
4. Divides the result by the factor The code follows:
function round (num,places) {
var factor = Math.pow(10,places);
var increment = 5/(factor*10);
return Math.floor((num+increment)*factor)/factor;
}
I use the round function to round off distances to two decimal places and latitude and longitude to four decimal places.
■ Tip
JavaScript has a method called
toFixedthat essentially performs the task of my round. If
numholds a number—say, 51.5621—then
num.toFixed()will produce 51 and
num.toFixed(2)will produce 51.56. I’ve read that there can be inaccuracies with this method, so I chose to create my own function. You may be happy to go with
toFixed()in your own applications, though.
With the explanation of the relevant HTML5 and Google Maps API features, we can now put it all together.
Building the Application and Making It Your Own
The map spotlight application sets up the combination of Google Maps functionality with HTML5 coding. A quick summary of the application is the following:
1. init: Initialization, including bringing in the map (makemap) and setting up mouse events with handlers: showshadow, pushcanvasunder, clearshadow 2. makemap: Brings in a map and sets up event handling, including the call to
checkit
3. showshadow: Invokes drawshadowmask 4. pushcanvasunder: Enables events the on map
5. checkit: Calculates distance, adds a custom marker, and displays distance and rounded latitude and longitude
The function table describing the invoked/called by and calling relationships (Table 4-1) is the same for all the applications.
Function Invoked/Called By Calls init Invoked by action of the onLoad attribute in the <body> tag makemap
pushcanvasunder Invoked by action of addEventListener called in init
clearshadow Invoked by action of addEventListener called in init
showshadow Invoked by action of addEventListener called in init drawshadowmask drawshadowmask Called by showshadow
makemap Called by init
checkit Called by action of addEventListener called in makemap round, dist
round Called by checkit (three times)
dist Called by checkit
changebase Called by action of onSubmit in <form> makemap
Table 4-2 shows the code for the Map Maker application, named mapspotlight.html.
Table 4-2. Complete Code for the mapspotlight.html Application
Code Line Description
<!DOCTYPE html> Header
<html> Opening html tag
<head> Opening head tag
<title>Spotlight </title> Complete title
<meta charset="UTF-8"> Meta tag
<style> Opening of style element
header {font-family:Georgia,"Times New Roman",serif; Set fonts for the heading
font-size:16px; Font size
Code Line Description
display:block; } Line breaks before and after
canvas {position:absolute; top: 165px; left:0px; Style directive for the single canvas element: position slightly down the page
z-index:100;} Initial setting for canvas is
on top of map
#place {position:absolute; top: 165px; left: 0px; Style directive for the div holding the Google Map;
position exactly the same as the canvas
z-index:1;} Initial setting to be under
canvas
</style> Close style element
<script type="text/javascript" charset="UTF-8"
src="http://maps.google.com/maps/api/js?sensor=false"></script>
Bring in the external script element holding the Google Maps API; no attempt to use sensing for geolocation
<script type="text/javascript" charset="UTF-8"> Opening script tag
var locations = [ Define set of base locations
[40.725592,-74.00495, "Friends of ED, NYC"], Latitude, longitude name for
var candiv; Used to hold div holding the
canvas
var can; Reference canvas element
var ctx; Reference context of canvas;
used for all drawing
var pl; Reference the div holding
the Google Map
function init() { Function header for init
var mylat; Will hold latitude value
var mylong; Will hold longitude value
candiv = document.createElement("div"); Create a div
candiv.innerHTML = ("<canvas id='canvas' width='600' height='400'>No canvas </canvas>");
Set its contents to be a canvas element
document.body.appendChild(candiv); Add to the body
can = document.getElementById("canvas"); Set reference to the canvas
pl = document.getElementById("place"); Set reference to the div holding the Google Map
ctx = can.getContext("2d"); Set the context
can.onmousedown = function () { return false; } ; Prevents change of cursor to default
can.addEventListener('mousemove',showshadow,true); Set event handling for mouse moving
can.addEventListener('mousedown',pushcanvasunder,true); Set event handling for pushing down on mouse button
can.addEventListener("mouseout",clearshadow,true); Set event handling for moving mouse off of the canvas
mylat = locations[0][0]; Set the latitude to be the
latitude of the 0th location
Code Line Description
mylong = locations[0][1]; Set the longitude to be the
longitude of the 0th location
makemap(mylat,mylong); Invoke function to make a
map (bring in Google Maps at specified location)
} Close init function
function pushcanvasunder(ev) { Header for pushcanvas
function, called with
function clearshadow(ev) { Header for clearshadow
function, called with parameter referencing the event
ctx.clearRect(0,0,600,400); Clear canvas (erase shadow
mask)
} Close clearshadow function
function showshadow(ev) { Header for showshadow
function, called with
mx = ev.layerX; If so, use it to set mx . . .
my = ev.layerY; . . . and my
} else if (ev.offsetX || ev.offsetX == 0) { Try offsetX
mx = ev.offsetX; If so, use it to set mx . . .
my = ev.offsetY; . . . and my
} Close clause
can.style.cursor = "url('light.gif'), pointer"; Set up cursor to be light.gif if available, otherwise pointer
mx = mx+10; Make rough correction to
make center of light at base of lightbulb horizontally and . . .
my = my + 12; . . . vertically
drawshadowmask(mx,my); Invoke drawshadowmask
function at the modified (mx,my)
} Close showshadow function
var canvasAx = 0; Constant for mask:
Upper-left x
var canvasAy = 0; Upper-left y
var canvasBx = 600; Upper-right x
var canvasBy = 0; Upper-right y
var canvasCx = 600; Lower-right x
var canvasCy = 400; Lower-right y
var canvasDx = 0; Lower-left x
Code Line Description
var canvasDy = 400; Lower-left y
var holerad = 50; Constant radius for hole in
shadow (radius of spotlight)
var grayshadow = "rgba(250,250,250,.8)"; Color for faint shadow; note alpha of .8
function drawshadowmask(mx,my) { Header for drawshadowmask
function; parameters hold center of donut hole
ctx.clearRect(0,0,600,400); Erase whole canvas
ctx.fillStyle = grayshadow; Set color
ctx.beginPath(); Start first (top) path
ctx.moveTo(canvasAx,canvasAy); Move to upper-left corner
ctx.lineTo(canvasBx,canvasBy); Draw over to upper-right
corner
ctx.lineTo(canvasBx,my); Draw to vertical point
specified by my parameter
ctx.lineTo(mx+holerad,my); Draw over to the left to edge
of hole
ctx.arc(mx,my,holerad,0,Math.PI,true); Draw semicircular arc
ctx.lineTo(canvasAx,my); Draw to left side
ctx.lineTo(canvasAx,canvasAy); Draw back to start
ctx.closePath(); Close path
ctx.fill(); Fill in
ctx.beginPath(); Start of second (lower) path
ctx.moveTo(canvasAx,my); Start at point on left side
indicated by my parameter
ctx.lineTo(canvasDx,canvasDy); Draw to lower-left corner
ctx.lineTo(canvasCx,canvasCy); Draw to lower-right corner
ctx.lineTo(canvasBx,my); Draw to point on right edge
ctx.lineTo(mx+holerad,my); Draw to left to edge of hole
ctx.arc(mx,my,holerad,0,Math.PI,false); Draw semicircular arc
ctx.lineTo(canvasAx,my); Draw to right edge
ctx.closePath(); Close path
ctx.fill(); Fill in
} Close drawshadowmask
function
var listener; Variable set by addListener
var listener; Variable set by addListener