I was working on my kayaking web page and came up with a new feature. I wanted to allow the URL to include a parameter for highlighting a specific route. “Highlighting” the route means the map will be zoomed in and panned to the location of the route and the info window will appear.

Here are some problems with this feature that I encountered:

  • The Google maps fitBounds() function is asynchronous. After it returns, the map may not be panned and zoomed to the specified bounds.
  • The size and placement of the info window cannot be read back in latlng coordinates.
  • The fitBounds() function cannot be use in a way that easily allows for extra room for the info window.
  • The fitBounds() function is not exact and uses the nearest integer zoom level. The area shown on the map may be quite a bit larger than the desired bounds.
  • The infoWindow open() function will pan the map so that the info window is entirely visible.
  • The fitBounds() function makes the map include more area than the bounds specirfied to it.
  • The bounds_changed event is not reliable!

If I get the coordinates of a route and fit the map to those coordinates, I can’t be sure exactly how much extra room is available outside of those coordinates. I then can’t easily decide if I need to zoom out or pan somehow to make room for the info widow I want to display. I also must do any map changes in a callback function since bounds changes are asynchronous.

The Solution

The solution is a bit complicated and looks ugly. The code is not self-explanatory so I’ll explain the important parts after the code:

0     // Callback to ensure no action is taken until the map is ready.
1     google.maps.event.addListenerOnce(map, 'bounds_changed', function() 
2     {
3         // Callback for the first fitBounds() call is made.
4         google.maps.event.addListenerOnce( map, 'idle', function()
5         {
6             // Callback to see if the info window moved the route off the map.
7             google.maps.event.addListenerOnce( map, 'idle', function()
8             {
9                 // Test to see if any route waypoint is outside of the map bounds.
10                newbounds = map.getBounds();
11                var poly = trips[WhichTrip].VisiblePolyline.getPath();
12                var len = poly.getLength();
13                var NeedToZoom = false;
14                for( var Index=0; Index < len; Index++ )
15                {
16                    if( newbounds.contains( poly.getAt( Index ) ) == false )
17                    {
18                        // Darn, need to zoom out!
19                        NeedToZoom = true;
20                        break;
21                    }
22                }
23                if( NeedToZoom )
24                {
25                    // Callback for after the zoom.
26                    google.maps.event.addListenerOnce( map, 'zoom_changed', function()
27                    {
28                        google.maps.event.addListener( map, 'zoom_changed', function() { ZoomHandler(); } );   
29                    } );
30                    
31                    var zoom = map.getZoom();
32                    zoom--;
33                    map.setZoom( zoom );
34                }
35                else
36                    google.maps.event.addListener( map, 'zoom_changed', function() { ZoomHandler(); } );
37            } );
38            
39            ShowNPolylineInfo( WhichTrip );
40        } );
41        
42        bounds = new google.maps.LatLngBounds();
43        var poly = trips[WhichTrip].VisiblePolyline.getPath();
44        var len = poly.getLength();
45        for( var Index=0; Index < len; Index++ )
46        {
47            bounds.extend( poly.getAt( Index ) );
48        }
49    
50        map.fitBounds( bounds );
51    
52    } );

I want to note, for the sake of anyone finding this page via Google, that I am using the V3 version of the Google Maps API.

The first important issue is the nested callbacks. This was just easier than trying to pass variables to the callback functions. Note that it is the code after a callback is defined that causes the callback to get called. Line 42 is the first line that is executed and only after line 50 is executed is the code at line 6 executed.

The operations are as follows:

  1. The waypoints of the desired route are used to create a bounds objects that encompasses the route.
  2. The map bounds are adjusted to be the route bounds.
  3. Google code adds some space around the route bounds when changing the map bounds.
  4. Once the bounds of the map have changed, the next callback is called. This is an idle event callback because the map bounds might not change!
  5. My code to display the info window for the route is called. Google code will pan the map if needed.
  6. The callback after the info window is displayed is called. We wait for an idle event since the map bounds might not change!
  7. The route waypoints are compared to the actual map bounds and any point outside of the map bounds causes the code to do a zoom out.
  8. The code adds a zoom handler for other reasons and is done.

It's a bit ugly because there is no way to know if a change will cause a bounds change. If not, there is no callback for a bounds_changed event. The idle event does seem to work properly but is not called until after the map is redrawn. It is also possible that the Google code is always calling the callback for a bounds_changed event but not at the right time. Again, using the idle callback seems to work more consistently.

Here is a link to the kayaking trips page with one of the routes highlighted in this way:

Map Highlight Image