Hack 12. How Far Is That? Go Beyond Driving Directions
Draw routes and calculate distances on your own Google Maps.
How far is it? That's a basic question we often ask of maps. Google Maps' driving directions answer that question, but driving directions are not (yet) accessible to the developer's API. More importantly, they simply give driving distances assuming the optimal route, where optimal is defined as getting there as quickly as possible in an automobile. They are not optimized for "scenic drive" or "safest bicycle route" or "quiet stroll" or "jog around the park."
There are at least two sites that allow you to create routes and calculate distances by clicking on maps. The Gmaps Pedometer at http://www.sueandpaul.com/gmapPedometer/ shown in Figure 2-6 estimates cumulative distanceand even includes a calorie counter.
Use the standard map controls to zoom into your area of interest, and then click Start Recording. When you double-click a point on the map, it will recenter to that spot and add a marker there. The second time you click, the map will recenter to your new point, the marker will be moved, and a line will be drawn from the last point clicked to this one. Each time you do this, the Total Distance and Last Leg Distance fields will be updated.
Figure 2-6. Sue and Paul's Gmaps Pedometer
Handling double clicks is a bit awkward and browser dependent. Sue and Paul are doing browser detection and then setting the appropriate event handler based on the browser:
if (navigator.appName == 'Microsoft Internet Explorer'){ document.ondblclick = handleDblClick; bIsIE = true; } else { window.ondblclick = handleDblClick; bIsIE = false; }
Do you see the difference between the two ondblclick events? It is a difference in how they implement the Document Object Model. Internet Explorer handles double-clicks at the document level, hence document.ondblclick and everyone else (well, everyone else according to this code) uses the window object, so window.ondblclick. In both cases when there is a double-click the variable bDoubleClickHappened is set to true.
This will become important in a bit. You can't add a double-click listener with the Google Maps API, so the map does not directly capture the double-click event, but rather the moveend event, which according to the API documentation is "Triggered at the end of a discrete or continuous map movement. This event is triggered once at the end of a continuous pan."
This means that when there is a double-click event, Sue and Paul's handleDblClick function is called to set the bDoubleClickHappened variable. Next the Google Maps equivalent of handleDblClick is called.
Once Google Maps has finished the move or pan, the moveend event is triggered, the function set to listen for moveend events is called, and the anonymous function set in this code is called:
GEvent.addListener(map, "moveend", function() { if (bDoubleClickHappened){ addLeg(map.getCenterLatLng().x, map.getCenterLatLng().y); drawPolyLine(gPointArray); } bDoubleClickHappened = false; });
I love this code! It is an example of not always getting what you want, but finding a way to get what you need. We don't want to add a leg to our route on every move, just the ones that were initiated by a double-click. When the double-click handler was called, the bDoubleClickHappened variable was set. This code is called any time the map is moved, and if the map was double-clicked the addLeg and drawPolyLine functions are called.
Walk Jog Run, shown in Figure 2-7, works in similar ways, but it captures single clicks. If you click on the map it asks if you want to start a new route. If you say yes, it captures each click, adds a marker, and draws a line connecting all of the points you've clicked.
Figure 2-7. Walk Jog Run
If this is the first click, the route will be empty, so startRoute() is called; otherwise, this is the continuation of a route, so this point is added to the list.
GEvent.addListener(map, 'click', function(overlay, point) { if (overlay) { /* do nothing */ } else if (point) { if (route.length == 0) { startRoute(point); } else if (route[route.length-1].x != point.x || route[route.length-1].y != point.y) { route.push(point); currentRouteId = null; drawRoute(route); } } });
The prototype for a click event handler accepts an overlay and point parameter. The overlay parameter is set when the user clicks on an overlay (that is, a line or marker). Most of the time we handle click events on markers by setting a listener when the marker is created. The drawRoute method is called when a new point is added and then goes through the list of points in the route, setting up the text for the marker overlays and calling this createMarker code to draw them, and then drawing the polyline of the route.
function createMarker(point, mtext, icon) { var marker = new GMarker(point,icon); var html = "Route Information " + mtext; GEvent.addListener(marker, "click", function() { currentPoint = marker.point; map.centerAndZoom(currentPoint, map.getZoomLevel()); marker.openInfoWindowHtml(html); }); return marker; }
This illustrates the creation of a marker with embedded text. If you want your markers to open, add a listener for the click event of the marker. The standard choice when clicking a marker is the openInfoWindowHTML() method for the marker. This pops up the standard HTML-enabled info window, but you can do anything when a marker is called.
|
Walk Jog Run aims to be the http://del.icio.us of maps for the running community. It lets you save your own routes as well as search and comment on the shared list of routes. You can look at the information relevant to any of the intermediate points on the route. Walk Jog Run shows you an estimated time for the total route and for each segment and allows you to delete any points from a route, unlike Sue and Paul's, which will only let you undo the last point.
Lines or Points? Both services let you add points by clicking. GMaps Pedometer shows a marker for your start and most recent position and hides the markers for the intermediate points. The result is a clean path overlaid on the map. Walk Jog Run leaves the markers on the map, which lets you view statistics for each segment of a completed route. They each have advantages.
Both GMaps Pedometer and Walk Jog Run aim to be full-featured sites. As a result, the code has a lot of detail that might make it hard to understand what is going on. Our page at http://mappinghacks.com/projects/gmaps/lines.html has another example of adding markers and lines, and then calculating distances in response to click events, as shown in Figure 2-8.
Figure 2-8. Adding markers and lines
When the user clicks on the Start Recording button, the code sets up a listener to process click events and the recording_flag is set in the JavaScript. If it is set the current position is added to the arrays that hold our x and y positions (where, you'll recall x equals longitude, and y equals latitude), and the drawRoute function is called. Finally, the current latitude and longitude are shown in the form elements click_lat and click_long. Capturing clicks is described in more detail in "Where Did the User Click?" [Hack #11].
GEvent.addListener(map, 'click', function(overlay, point) { if (point) { if (recording_flag > 0) { addPoint(point.y, point.x, keepPoint); x_array.push(point.x); y_array.push(point.y); drawRoute(); document.getElementById('click_lat').value = point.y; document.getElementById('click_long').value = point.x; } } } ); // end of GEvent.addListener
The drawRoute( ) function is a bit longer, but hopefully straightforward. The first trick when updating markers is to clear all the existing markers by calling clearOverlays( ). Next the code walks the array of longitudes, x_array. Distances are calculated for the segment and the running distance of the route up to this point. The segment_distance and total_distance form elements are updated to show the distances.
The point is then created and a marker added. The created point is added to the array points. Once all of the elements in the x and y arrays have been processed, the array of points is added as a new GPolyLine.
function drawRoute() { map.clearOverlays(); var points = []; for (i = 0; i < x_array.length; i++) { if (i>0) { segment_distance_array[i] = calcDist(x_array[i-1], y_array[i-1], x_array[i], y_array[1]); total_distance_array[i] = total_distance_array[i-1] + segment_distance_array[i]; document.getElementById('segment_distance').value = segment_distance_array[i]; document.getElementById('total_distance').value = total_distance_array[i]; } else { // initialize the first element distances to 0 document.getElementById('segment_distance').value = 0; document.getElementById('total_distance').value = 0; total_distance_array[0] = 0; segment_distance_array[0] = 0; } var point = new GPoint(x_array[i], y_array[i]); points.push(point); var marker = new GMarker(point); // define the text that appears in the marker var html = "location " + y_array[i] + ', ' + x_array[i] + ""; GEvent.addListener(marker, "click", function() { marker.openInfoWindowHtml(html); }); map.addOverlay(marker); } map.addOverlay(new GPolyline(points)); }
This is not the only, or even best, way to do this! There is more than one way to do it!
Now, let's move on to calculating distances. Walk Jog Run and GMaps Pedometer use similar functions to calculate distance. I used the one from Walk Jog Run in my demo because it specifically had a Creative Commons Attribution-NonCommercial-ShareAlike license.
/* calcDist() function is from Adam Howitt Copyright Adam Howitt 2005 Email: adamhowitt@gmail.com This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License. http://creativecommons.org/licenses/by-nc-sa/2.5/ */ function calcDist(lon1,lat1,lon2,lat2) { var r = 3963.0; var multiplier = 1; // var multiplier = currentUnit == "miles" ? 1 : MILESASKM; return multiplier * r * Math.acos(Math.sin(lat1/57.2958) * Math.sin(lat2/57.2958) + Math.cos(lat1/57.2958) * Math.cos(lat2/57.2958) * Math.cos(lon2/57.2958 - lon1/57.2958)); }:
Note that the variable multiplier has been commented out. In this code, I'm displaying the values only in miles. The multiplier represents the conversion factor from miles to whatever units you need. MILESASKM is Miles as Kilometers, the number of kilometers in one mile, or 1.609344. The multiplier is set using the ternary operator. If the current unit equals miles then the multiplier is 1 (as 1 mile equals 1 mile); otherwise, it is set to the number of kilometers in a mile. You don't need to understand this formula, but if you want to learn more, see "How to calculate distance in miles from latitude and longitude" at http://www.meridianworlddata.com/Distance-Calculation.asp.
The constant 3,963 is close enough to the radius of the earth in statute miles. 57.2958 is the number of statute miles (5,280 feet, as opposed to nautical miles) in one degree of latitude anywhere, or one degree of longitude at the equator. A nautical mile is defined as 1 minute of latitude (or 1 minute of longitude at the equator).
With the multiplier code commented out, you can copy this function into your own code and calculate distances between anything. Go distance crazy!