Hack 41. Plot Points on an Interactive Map Using DHTML

Adding JavaScript to interactive maps on the Web can make them more responsive by eliminating the need to hit the network for updates.

Suppose we want to make a little web application that puts a star on a map of the world, at a given latitude and longitude. We might imagine building a simple form interface with some HTML that looks like this:

 

Latitude: Longitude:

 

In a web browser, the page might look something like Figure 4-8. We have two text input boxes, one called lat and the other lon, and a Go button. The map shown is the 1024x512 pixel AVHRR Pathfinder map from Dave Pape's Earth Images collection (http://www.evl.uic.edu/pape/data/Earth/), scaled down to 800 pixels wide.

Figure 4-8. A simple web-based map interface

 

Obviously, one way to turn this interface into an application would be to point the form action at a CGI script, do all of the work on the web server, and return a dynamically generated version of the world map. The main reason not to do this is, quite frankly, that it's slow. The time that a user spends waiting for their web browser to go out over the network, request the page, have the image rendered server-side, download everything, and then render the web page itself is time that they're not spending getting information out of our map. If we can do all of our calculations in the browser itself, and overlay the star on top of the map dynamically, then the usability of our map goes way up.

Fortunately, if the map projection is simple enough, we can do this by hacking together a bit of JavaScript with some Cascading Style Sheet (CSS) properties. (When JavaScript is used to push bits of HTML and CSS around, the result is called Dynamic HTML, or DHTML.) As it happens, the Pathfinder map is a Plate Carrée, or an Equidistant Cylindrical, projection, so the math required to turn a latitude and longitude into a point on the map image is quite simple, as discussed in [Hack #29], and well within the means of JavaScript. We'll step through the process of building this form so that a user can enter points and have them displayed on the map, without having to hit the server each time.

4.8.1. Adding the Star to the Interface

First, we'll need to add the star to the HTML layout. We want the star to be rendered within the same container as the base map, but we don't want it to be visible until the user enters a set of coordinates. To accomplish this, we set its CSS visibility property to hidden.

 

 

Now we're ready to add some JavaScript code to make the star appear in the right place and time.

4.8.2. The Code

The following block of JavaScript code makes the star appear on the map image. The code goes in the same file as our HTML interface.

 

 

We start by defining the boundaries, in degrees, of our world map, which will be used shortly. Next, we introduce a function, moveStar(), which will be called when the user clicks Go. The object representing our input form is passed as an argument to the function. In lines 9-16, we extract the latitude and longitude values from the form, check to see if they're within the boundaries of our map, and kick out an error pop-up if they're not.

Then we come to the heart of the matter. In line 18, we call the Document Object Model API to find the image element containing our base map, in order to query its width and height. These figures are used on lines 19 and 20 to calculate the scale of the map, or the number of pixels per degree, in the fashion described in [Hack #29] . Calculating the scale on the fly means that the map can change and our code will still do the right thing.

In similar fashion to [Hack #29], we calculate the number of degrees from our point to the left edge of the map on line 22, and then multiply that by our scale value to get the number of pixels. Then we do the same for the vertical axis on line 23.

Now, here's an added wrinkle: absolute positioning in CSS is reckoned from the edge of the browser window, not the edge of the enclosing HTML element. However, our base map image might not be rendered at the exact top or left edges of the document. What's worse, the DOM API will only tell us how far the map image rests from the edge of its immediate parent element, rather than from the edge of the window. So, in lines 25-28, we walk upward through the chain of nested HTML elements, and add up each horizontal and vertical offset, until we get to the very top of the document.

Next, we deal with one final hitch in lines 30-32, which is that we really want the star centered over the point in question, not placed on its corner. To achieve this result, we have to find the width and height of the star and subtract half of each from our x- and y-coordinates, so that the star appears centered over our point. Again, we could hardcode the width and height of the star, but doing it this way allows us to use any image in place of our little star without having to touch the code.

At last, we're ready to plot the star on top of the map, which we do in lines 34 through 37, by setting the style attribute of the star to make it visible at the calculated location.

4.8.3. Running the Code

Now that we can plot the star on the map, we need to trigger the star's placement when the user presses Go. There are a number of ways to accomplish this, but we'll settle for setting a JavaScript action on our original form element, which triggers the JavaScript function moveStar(this) as its only argument:

 

Latitude:

 

Now we can load our web page into a browser, enter some coordinates, and have the star plotted in the right place. You can try out this example for yourself at http://mappinghacks.com/code/dhtml/. Figure 4-9 shows our working interface, with the star positioned more or less over New York City.

Figure 4-9. The same web-based map interface, powered by JavaScript

 

4.8.4. Hacking the Hack

Now, in and of itself, this demo application isn't very interesting, because people don't usually think of places in terms of latitude and longitude, per se. If we wanted to use this application to show the world's 50 largest cities, we might have something a little more interesting, but we'd still have to hit the network to look up the location of a selected city in a database somewhereunless we happened to put that list in JavaScript somewhere, as well. The following is an excerpt from cities.js, which just happens to be such a list:

var cities = [ [ "Mumbai, India [12.4M]", 18.96, 72.82 ], [ "Buenos Aires, Argentina [12.1M]", -34.61, -58.37 ], [ "Karu0101chi, Pakistan [10.5M]", 24.86, 67.01 ], [ "Manila, Philippines [10.2M]", 14.62, 120.97 ], // ... and so on, for 46 more lines ... ];

 

This data structure is just a list of lists, in which each inner list contains the name and population of the city, as well as its latitude and longitude. You can find a copy of the full cities.js in http://mappinghacks.com/code/dhtml/, which was generated using the list of cities found at http://mappinghacks.com/data/cities.txt. We can pull it into our HTML interface, by adding an external script reference to it, like so:

 

 

You may be wondering about the u0101 in Karu0101chi. It's called a Unicode escape sequence. Your web browser's JavaScript engine is supposed to interpret this as "the Unicode character with hexadecimal code 0101" (or decimal 257), which is the letter a with a bar on top of it.

 

We'll need to add a drop-down menu to our HTML interface, to allow the user to select a city to display. We'll also need to add an onLoad event to dynamically populate the drop-down box from the list of cities in cities.js. Here are the changes to the HTML:

 

onLoad="populateCityList( )">

Latitude: Longitude:

 

You'll notice that the drop-down menu is initially empty. That's all right; as soon as the document loads, the browser calls populateCityList(), which creates the drop-down options by iterating through the list of cities. Also, the drop-down box has an onChange event that is called when a user selects a city to display. This event calls selectCity(), which updates the latitude and longitude fields in the form and refreshes the map. Here's the code for that:

function populateCityList ( ) { for (i = 0; i < cities.length; i++) document.forms["location"].city.options[i] = new Option(cities[i][0], i); } function selectCity (item) { item.form["lat"].value = cities[item.selectedIndex][1]; item.form["lon"].value = cities[item.selectedIndex][2]; item.form.submit( ); }

 

So, with only a little extra work, we're now using JavaScript data structures and UI events to power an application that maps the world's largest cities. You can see a working demo of this at http://mappinghacks.com/code/dhtml/cities.html. It may not sound like much, but try it! Odds are, unless you're all but the very biggest geography buff, you will run across at least one enormous metropolis that you've never heard of, or never knew the actual location of. For such a simple web application, this quickly turns into a fascinating geographic exploration! Figure 4-10 shows the enhanced interface, with Buenos Aires selected.

Figure 4-10. An interactive world city browser, in JavaScript

 

4.8.5. Hacking Other Projections

"This is all well and good," you may be thinking, "but what if I want to plot points in JavaScript on maps that aren't in a rectangular projection?" Well, that's a little bit more of a challenge. For some projections, like the sinusoidal projection, the math still isn't that hard, and JavaScript can pull it off. For others, even seemingly simple projections like the Mercator, the amount of time you'll spend writing the conversion, or the amount of time the client-side will spend performing it, may outweigh the benefits of doing such a calculation in the web browser.

Consider this, however: if your server-side scripts permit you to populate a JavaScript data structure with the latitude and longitudes of a constrained list of places you're interested in, why not precalculate the projected coordinates on the server-side, and get the best of both worlds, as it were?

For another way to plot points on a world map, see [Hack #29]

 

Категории