• 沒有找到結果。

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

km

and

miles

in the

dist

function 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

toFixed

that essentially performs the task of my round. If

num

holds 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