Sunday, August 29, 2010

Right-click menu on Google Maps

A right-click menu is an easy, yet extremely useful and intuitive, feature to add to most applications. This holds especially true for mapping applications where location-specific functions are desired, such as adding a destination or zooming.

The first point of business is to create the HTML for the actual menu. In this case, we will create a new DIV object with several links to javascript functions.

contextmenu = document.createElement("div");
contextmenu.className = 'contextmenu';

contextmenu.innerHTML = '<a href="javascript:void(0)" onclick="zoomIn()" id="menu1"><div class="context">  Zoom in  </div></a>'
   + '<a href="javascript:void(0)" onclick="zoomOut()" id="menu2"><div class="context">  Zoom out  </div></a>'
   + '<a href="javascript:void(0)" onclick="zoomInHere()" id="menu3"><div class="context">  Zoom in here  </div></a>'
   + '<a href="javascript:void(0)" onclick="zoomOutHere()" id="menu4"><div class="context">  Zoom out here  </div></a>'
   + '<a href="javascript:void(0)" onclick="centreMapHere()" id="menu5"><div class="context">  Centre map here  </div></a>'
   + '<a href="javascript:void(0)" onclick="addDestinationHere()" id="menu6"><div class="context">  Add destination  </div></a>';

Then we append the new code to the map DIV. We will set the visibility of the DIV to false by default, and then we will set it to true whenever we want the menu to appear.

$(map.getDiv()).append(contextmenu);

So, at this point we have the code for our context menu, and it is now appended to the map DIV. The next step is to make the context menu visible when the user performs a right-click event. We also want to position the menu at the location of the user click. For now we will add a listener for the right-click event, which will call another function with the location (Latitude/Longitude) as an argument.

google.maps.event.addListener(map, "rightclick", function(event) {
   showContextMenu(event.latLng);
});

The showContextMenu() function must do three things: (1) display the context menu, (2) set the location of the context menu to the click location, and (3) store the click location for use by any of the menu functions. The first item is fairly simple, and it consists of setting the visibility to true. And for the third item we store the Latitude/Longitude parameter into a global variable. The second item is a bit trickier, so we'll separate it into another function setMenyXY().

function showContextMenu(currentLatLng) {
   setMenuXY(currentLatLng);
   clickedLoc = currentLatLng;
   contextmenu.style.visibility = "visible";
}

The setMenuXY() function is a bit more code, but the concept is simple. We first store the width and height for the map and menu. We then test the click location to make sure we are never positioned outside the map; if we are, then we adjust the location to within the confines of the map. Then we set the left and right properties for the context menu to the desired location.

function setMenuXY(currentLatLng){
   var mapWidth = $('#map_canvas').width();
   var mapHeight = $('#map_canvas').height();
   var menuWidth = $('.contextmenu').width();
   var menuHeight = $('.contextmenu').height();
   var clickedPosition = getCanvasXY(currentLatLng);
   var x = clickedPosition.x ;
   var y = clickedPosition.y ;

   if((mapWidth - x ) < menuWidth)
      x = x - menuWidth;
   if((mapHeight - y ) < menuHeight)
      y = y - menuHeight;

   $('.contextmenu').css('left',x);
   $('.contextmenu').css('top' ,y);
}
Now, you will notice that there is one small problem with the menu. Most right-click menus will disappear when the user left-clicks elsewhere in the object, and this menu seems to persist until an item is selected on the meny. So, we will add another event handler that will set the visibility of the context menu to false whenever a left-click event is performed.
google.maps.event.addListener(map, "click", function(event) {
   contextmenu.style.visibility="hidden";
});

And that's your right-click menu! My next post, which should be fairly short, will detail the actual code for each function contained in the context menu (zoom, add destination, etc.).

Friday, August 13, 2010

Using more than 8 waypoints in Google maps

If you have ever tried to use the Google Maps API, you will probably have noticed that Google places a limit of 8 waypoints for the free version of the API, and a limit of 23 waypoints for a premiere account. For those of you who want to circumvent this limitation, I have written a class that can be used in place of Google's DirectionsService and DirectionsRenderer classes. Although, my class uses Google's DirectionsService class, it does things a bit differently.

It works by splitting apart a list of waypoints into groups of 10 or less, finding the path between those points, and then appending all the results into a single PolyLine. Since the DirectionsService class returns a DirectionsResult class, we can maintain and amend all the data that DirectionsRenderer has access to, and this can be used to customize the way the information is presented to the user.

The majority of this functionality is done by this function:

/**
 * Calculate path between destinations.
 *
 * @param {Array} destinations The list of destinations
 *    to visit.
 * @param {google.maps.DirectionsTravelMode} selectedMode The type of traveling: car, bike, or walking
 * @param {bool} hwy Whether to avoid highways
 * @param {bool} toll Whether to avoid tolled roads
 * @param {bool} onlyCurrent If using multiple routes, do we want to show all of them or just the current
 * @param {string} units The distance units to use, either "km" or "mi"
 */
DirectionsRoute.prototype.route = function(destinations, selectedMode, hwy, toll, onlyCurrent, units) {
     this.directionsDisplay.reset();
 
     // Add all destinations as markers.
     var places = new Array();
     for(var i=0; i < destinations.length; i++) {
          this.process_location_(destinations, i, places);
     }
 
     // Determine unit system.
     var unitSystem = google.maps.DirectionsUnitSystem.IMPERIAL;
     if(units == "km")
          unitSystem = google.maps.DirectionsUnitSystem.METRIC;

     // Loop through all destinations in groups of 10, and find route to display.
     for(var idx1=0; idx1 < destinations.length-1; idx1+=9)
     {
          // Setup options.
          var idx2 = Math.min(idx1+9, destinations.length-1);
          var request = {
               avoidHighways: hwy,
               avoidTolls: toll,
               origin: destinations[idx1].location,
               destination: destinations[idx2].location,
               travelMode: google.maps.DirectionsTravelMode[selectedMode],
               unitSystem: unitSystem,
               waypoints: destinations.slice(idx1+1, idx2)
          };
  
          // Determine path and display results.
          this.directionsService.route(request, function (response, status) {
               if (status == google.maps.DirectionsStatus.OK)
                    this.directionsDisplay.parse(response, units);
               });
     }
}
This function uses a few auxiliary functions, but the bulk of the logic is in here. First, the code goes through each destination and places a marker on the map. The, the main for loop goes through each destination in groups of 10 (because we can have a maximum of 8 waypoints, plus the origin and end) and finds the path between each set. For each route that is found, we then pass the DirectionsResults that is returned to a new function to combine and display the results:
/**
 * Generates boxes for a given route and distance
 *
 * @param {google.maps.DirectionsResult} response The result of calculating
 *    directions through the destinations.
 */
DirectionsDisplay.prototype.parse = function (response, units) {
     var routes = response.routes;
 
     // Loop through all routes and append
     for(var rte in routes)
     {
          var legs = routes[rte].legs;
          this.add_leg_(routes[rte].overview_path);
  
          for(var leg in legs)
          {
               var steps = legs[leg].steps;
   
               // Compute overall distance and time for the trip.
               this.overallDistance += legs[leg].distance.value;
               this.overallTime += legs[leg].duration.value;
          }
     }

     // Set zoom and center of map to fit all paths, and display directions.
     this.fit_route_();
     this.create_stepbystep_(response, units);
}
This function simply goes through each route and leg of each DirectionsResult and creates a PolyLine and appends it to an array, this.legs. It also calculates the overall distance/duration of the trip by summing together each piece as it goes. Finally, it fits the display window around all of the routes, and displays the step-by-step directions for the entire trip. We are then given freedom to customize the function create_stepbystep_() to do as we please and incorporate any features we wish. So far, it simply displays each step in a table, but ultimately I plan to add mouse events to interact with the map, and also make things a little more visually appealing. The entire javascript file can be found here. And, a sample page that uses the classes can be found here.

Next post I will outline my process of adding a right-click menu to the map. I will also add some other basic functionality, such as reordering the destinations and centering the map around the routes.

Thursday, August 12, 2010

Google APIs

Okay, after looking through some information on Google APIs, I came across this wonderful resource:

http://code.google.com/apis/ajax/playground/

It is an interactive code development environment that allows you to write web scripts and see the output on the fly! There is also some example code for various Google APIs, and a built-in Firebug Lite debugger. I have been messing around with it for a few days or so, and decided on an initial project that I want to complete. It involves the Google Maps API, and my plan is to allow route calculation with the ability to look up restaurants, fuel stations, hotels, rental car agencies, weather, etc. along the trip path. I can't tell you how many times I've planned a trip and needed to search in various cities along the path for hotels and rental car agencies, not to mention look up the weather and find the cheapest gas stations. This, to me, seems to be a major feature lacking in Google Maps.

I have also been looking through the Google Maps API:

http://code.google.com/apis/maps/documentation/javascript/reference.html

to try to figure out where to start since I've never used this particular API before. After looking through the samples provided on the page, I finally figured out how to create a simple web application to show a map and allow me to search for directions from one place to another. This is actually pretty simple using the following code (hosted here):

var map;
var directionsDisplay = new google.maps.DirectionsRenderer();
var directionsService = new google.maps.DirectionsService();
var waypts = new Array();

function initialize() {
   // Setup the map.
   var chicago = new google.maps.LatLng(41.850033, -87.6500523);
   var myOptions = {
        zoom: 7,
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        center: chicago
   }
   map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
 
   // Initialize the directions renderer.
   directionsDisplay.setMap(map);
   directionsDisplay.setPanel(document.getElementById("directionsPanel"));

   // Place our initial locations into the waypoints list, and calculate route.
   placeMarker("Chicago, IL");
   placeMarker("St. Louis, MO");

   // Setup listener to add a new location for each map click.
   google.maps.event.addListener(map, 'click', function(event) {placeMarker(event.latLng);});
}

This initialize() function is used to setup the page elements. It initializes the map and directions services, and then adds a couple default destinations (Chicago and St. Louis) to our list of destinations before calculating the route. It then registers a callback to append a new destination to the end of the list when the map is clicked. The "directionsPanel" div is an empty div on the bottom-right of the page used to print out the step-by-step directions.

The callback code to add a new destination is as follows:

// Add a new waypoint at the specified location, and recalculate path.
function placeMarker(location) {
   waypts.push({
        location:location,
        stopover:true});
    
   calcRoute();
}

It is fairly simple in its operation. The one thing that should be noted is that Google uses the DirectionsWaypoint class for waypoints, and so we must add an object containing a location and whether it is a stopover (whether it is an actual destination between the origin and ending point).

Then, finally, the code to plot the directions is:

// Calculate directions between a set of waypoints.
function calcRoute() {
   // Get which method of travel is to be used.
   var selectedMode = document.getElementById("mode").value
   // Calculate directions and display results.
   var request = {
        origin:waypts[0].location,
        destination:waypts[waypts.length-1].location,
        travelMode: google.maps.DirectionsTravelMode[selectedMode],
        waypoints: waypts.slice(1,waypts.length-1)
   };
   directionsService.route(request, function(response, status) {
        if (status == google.maps.DirectionsStatus.OK) {
             directionsDisplay.setDirections(response);
        }
   });

   // Print waypoint information into the waypoint panel in the top-right.
   var waypointPanel  = document.getElementById("waypointsPanel");
   waypointPanel.innerHTML = "";
   for(var x=0; x < waypts.length; x++) {
        waypointPanel.innerHTML += "<b> WP" + (x+1) + ": </b>" + waypts[x].location + "<br>";
   }
}

This code sends a request to the Google directions service to find a path between the waypoints, prints the results to the map and the "directionsPanel" div, and then prints out each waypoint to see on the "waypointsPanel" div.

However, I quickly discovered that Google limits the free API to only allow 8 waypoints to be sent to the DirectionsService (which is Google's class for finding a path through a set of waypoints) for route fitting. Premiere customers can have up to 23 waypoints, but either way I didn't like the limitation and wanted to find the path between an unlimited number of waypoints. I thought of many possible solutions, but the most effective seemed to be to rewrite a couple of Google's classes to break apart the waypoints into groups of 8, send the query to Google's DirectionsService, and then append them all into a single PolyLine.

Of course, after rewriting this code, I will probably have to rewrite the code for DirectionsRenderer (which is Google's class for displaying the step-by-step directions for a given route) because I am unsure of where the limitations are placed. My guess would be that the DirectionsService contains the limitation because that is where most of the work is being done, but I didn't want to waste too much time trying to link together all of the DirectionsResult's that are returned from DirectionsService (and if they both contain the limitation, then I would have wasted a lot of time). Mainly, I didn't know what fields I would have to update in order to allow the DirectionsRenderer to not yell at me when it tried to do its work. And, besides, if I write my own version of the class, I can then customize the output to however I want. All of the pertinent information is contained in the DirectionsResult's that will be returned from each query to the DirectionsService, so it should be a simple matter of displaying the results in a visually-appealing way.

Anyway, my next post will detail my progress.