// This file contains the Javascript for a NextBus google map
//
// All contents are copyright of NextBus Inc 2006.
// May not be reproduced or reengineered in any manner.
//
// Parameters:
//   agencyTag    -- required - specifies the agency to show
//   routeTags[]  -- required - specifies which routes to initially show
//   dirTag       -- optional - for specifying stop to initially show 
//   stopTag      -- optional - for specifying stop to initially show
//   zoomLevel    -- optional - for specifying zoom level
//   centerPoint  -- optional - for specifying lat,lon of center of map
//

//////////////////// Internationalization ////////////////////////////////////

function Querystring(qs) { // optionally pass a querystring to parse
    this.params = {};

    if (qs == null) qs = location.search.substring(1, location.search.length);
    if (qs.length == 0) return;

    qs = qs.replace(/\+/g, ' ');
    var args = qs.split('&'); // parse out name/value pairs separated via &

    // split out each name=value pair
    for (var i = 0; i < args.length; i++) {
        var pair = args[i].split('=');
        var name = decodeURIComponent(pair[0]);

        var value = (pair.length==2)
            ? decodeURIComponent(pair[1])
            : name;

        this.params[name] = value;
    }
}

Querystring.prototype.get = function(key, default_) {
    var value = this.params[key];
    return (value != null) ? value : default_;
}

// Determine the language to use. If the variable browserLanguage is set
// then use it. Otherwise check query string for the "lang" element.
// Need to use browserLanguage because Javascript doesn't have a way
// of directly determining brower language, but the server does. So
// need to use a JSP element in the .jsp file to determine if the user has
// configured their browser for a specific language.
// Use "en" as default if language not specified.
var qs = new Querystring();
var language = qs.get("lang");
// If language not set in query str then use browserLanguage if it is set
if (language == null && 
    typeof(browserLanguage) != "undefined" &&
    browserLanguage != null) {
  language = browserLanguage;
}
// If language not set by browser or query string then use "en" as default
if (language == null) {
  language = "en";
}

// For storing all translation tables
var languageTranslationTables = new Array();

// Returns the string in the proper language. If the language
// is the default language of English ("en") then the string
// is returned.
function getI18NStr(str) {
  // If English then simply return the string
  if (language == "en")
    return str;

  // Look up the translation table based on the language
  var currentLanguage = languageTranslationTables[language];
  if (currentLanguage == null) {
    alert("The text " + str + " is not defined for language=" + language);
    return str;
  }

  // Look up text in a table by language
  var result = currentLanguage[str];
  return result;
} 

//////////////////// translation table for French ///////////////////////////

var french = new Array();
french["Next Tracked Vehicles:"] = "Les prochains passages :";
french["Route:"]     = "Ligne :";
french["Direction:"] = "Direction :";
french["Stop:"]      = "Arr&ecirc;t :";
french["Stop: "]     = "Arret : ";  // Separate one needed because tooltips can't handle the accent
french["Arrivals:"]  = "Arriv&eacute;es :";
french["Departures:"]= "En partance :";
french["Message:"]   = "Message";
french["Departing"]  = "En partance";
french["Arriving"]   = "D&ucirc;";
french["No current prediction"] = "Aucunes pr&eacute;visions disponibles";

// Add French to the languageTranslationTables table
languageTranslationTables["fr"] = french;

///////////////////// Global Variables ////////////////////////////////  
  // Make sure the parameters exist
  var zoomLevel = null;
  var centerPoint = null;
  var dirTag;

  // The actual Google map
  var map;
  // Array of routes that have been accessed.
  var routes = new Array();
  // Stops array associated with current predictions info window
  var globalStops;
  // Timeout associated with predictions info window. Made a global
  // so that timeout can be removed when info window closed.
  var globalInfoWindowTimeout; 
  // For keeping track of whether the window displaying predictions is already open
  var globalPredictionInfoWindowOpen = false;

  // For when predictions less than a minute. Some agencies want to
  // use a different word like "Due".
  var departingWord = "Departing";
  var arrivingWord  = "Arriving";
  // For moving vehicle icons smoothly
  var INTERPOLATION_CNT = 8; 
  var currentInterpolationCnt = 0;
  var TIMEOUT_BETWEEN_SCOOTCHING = 40;
  var vehiclesBeingScootched = new Array();
 
//////////////////// Helper Functions ////////////////////////////////////////

// For resizing map portion when size of browser window changes.
function getWindowSize(){
    var e = new Object();
    if(window.self && self.innerWidth){
        e.width = self.innerWidth;
        e.height = self.innerHeight;
    }else if(document.documentElement && document.documentElement.clientHeight){
        e.width = document.documentElement.clientWidth;
        e.height = document.documentElement.clientHeight;
    }else{
        e.width = document.body.clientWidth;
        e.height = document.body.clientHeight;
    }
    return e
}


//////////////////// Object Definitions //////////////////////////////////////

  // Defines the RouteObject class
  function RouteObject(tag) {
    this.tag = tag;
	this.title = null;
	this.shortTitle = null;
	this.color = null;
	this.oppositeColor = null;
	this.stops = new Array();
    this.stopMarkers = new Array();
	this.vehicles = new Array();
	this.pathPolylines = new Array();
	this.minLat = 999.99;
	this.minLon = 999.99;
	this.maxLat = -999.99;
	this.maxLon = -999.99;
    this.loading = false;  // Have requested XML data and no error encountered
	this.loaded = false;   // XML data has been successfully read
	this.visible = false;
	debug("New route=" + tag + " created with lastTime=0");
	this.lastTime = 0; // Last time vehicle positions updated for route
	
	// Add this new route to the array of routes.
	routes.push(this);
  }

  RouteObject.prototype.removeVehiclesForRoute = function() {
	// Remove the vehicles if they were being shown
	if (shouldShowVehicles()) {
      for (var vehicleId in this.vehicles) {
	    vehicle = this.vehicles[vehicleId];
	    if ((typeof vehicle)=="object" && vehicle.marker != null) {
          map.removeOverlay(vehicle.marker);
	      vehicle.marker = null;
		}
	  }
    }
  }
  
  
  
  // Defines the StopObject class
  function StopObject(tag, title, dirTag, point, marker, route) {
    this.tag = tag;
	this.title = title;
    this.dirTag = dirTag;
	this.point = point;
	this.marker = marker;
	this.route = route;
  }
  
  
  // Defines predictions for a stop
  function PredictionsForStopObject(routeTitle, stopTitle, dirTitle, predictions, message) {
    this.routeTitle = routeTitle;
	this.stopTitle = stopTitle;
	this.dirTitle = dirTitle;
	// An array of Prediction objects
	this.predictions = predictions;

    this.message = message;
  }
  
  // Defines an individual prediction
  function Prediction(predictionStr, isDeparture) {
    this.predictionStr = predictionStr;
    this.isDeparture = (isDeparture!=null && isDeparture=='true');
  }
  
  // Use a special small marker for the stops so that the markers
  // do not overlap as much.
  var stopMarkerIcon = new GIcon();
  stopMarkerIcon.image = "/googleMap/images/stopMarkerRed.gif";
  stopMarkerIcon.iconSize = new GSize(7, 7);
  stopMarkerIcon.iconAnchor = new GPoint(4, 4);
  stopMarkerIcon.infoWindowAnchor = new GPoint(4, 4);


  // Defines a vehicle, draws it on the map, and adds the vehicle
  // object to the vehicle list for the route.
  function VehicleObject(id, previousLatLng, latLng, 
                         heading, speed, job, predictable,
                         lateSec, clockWhenReported, routeTag) {
    this.id = id;
    this.previousLatLng = previousLatLng;
    this.latLng = latLng;
    this.heading = heading;
    this.speedMetersPerSecond = parseInt(speed)/100.0;
    this.job = job;
    this.predictable = predictable;
    this.lateSec = lateSec;
    this.clockWhenReported = clockWhenReported;
    var route = getRoute(routeTag);
    this.route = route;
	
    // Add this new vehicle to the route's vehicle associative array
    route.vehicles[id] = this;
  }

  // This function should be overridden if a vehicle icon is to
  // be displayed as part of the label.
  var unitransDoubleDeckers = ['742', '2819', '3123', '4735', '8185', '8186',/* following for debugging '4092', '4002', '2819' */];

  function getVehicleIconHTML(agencyTag, vehicleId) {
    // If double decker bus then show icon
    if (agencyTag == "unitrans") {
      for (var i=0; i<unitransDoubleDeckers.length; ++i) {
        var id = unitransDoubleDeckers[i];
        if (id == vehicleId) {
          return '<img src="/googleMap/images/unitransDblDecker.gif" width="19" height="36" align="absmiddle">';
        }
      }
    }

    return '';
  }
  
  VehicleObject.prototype.addInterpolatedMarker = function(currentCnt, totalCnt) {
    var lat = this.previousLatLng.lat() + (this.latLng.lat()-this.previousLatLng.lat())*currentCnt/totalCnt;
    var lng = this.previousLatLng.lng() + (this.latLng.lng()-this.previousLatLng.lng())*currentCnt/totalCnt;
    var latLng = new GLatLng(lat, lng);
    debug("creating interpolated marker for vehicle=" + this.id + 
          " to lat=" + lat + " lon=" + lng + " and cnt=" + currentCnt);
    this.addMarker(latLng);
  }

  // Creates a marker for the vehicle and adds it to the map. The
  // marker will depend on whether the vehicle is predictable,
  // stale, has a block assignment, and the heading.
  VehicleObject.prototype.addMarker = function(latLng) {
    // If vehicle is stale and should not be showing stale vehicles, then don't show it
    if (!shouldShowStaleVehicles() && this.isStale()) 
      return;

    // If a vehicle is not predictable then don't draw it
    if (!agencyMap && !this.predictable)
      return;

    // So can output debugging info to the vehicle label
    var labelTextForDebugging = '';

    // Create an actual marker
    var imageFileName;
    this.drawnAsStale = false;  // For keeping track if already drawn as a stale vehicle

    // Determine colors to be used for drawing vehicle
    var color;
    var oppositeColor = '#000000';
    var oppositeColorName = 'black';
    if (this.isStale()) {
      // vehicle is stale so use special color 
      color = '#660000';
      oppositeColor = '#FFFFFF';
      oppositeColorName = 'white';
      this.drawnAsStale = true;
    } else if (this.predictable) {
      // vehicle is predictable so use route color
      color = '#' + this.route.color;
      if (this.route.oppositeColor == 'ffffff') {
        oppositeColor = '#FFFFFF';
        oppositeColorName = 'white';
      }
    } else if (this.job != null && this.job != "") {
      // vehicle has job but is not predictable (white vehicle). This is only for agencyMap mode. 
      color = '#FFFFFF';
      oppositeColor = '#000000';
      oppositeColorName = 'black';
    } else {
      // doesn't have job (grey vehicle). This is only for agencyMap mode.
      color = '#AAAAAA';
      oppositeColor = '#000000';
      oppositeColorName = 'black';
    }
    
    // Determine the direction icon to be used 
    var directionImage = "";
    if (this.heading >= 0) {
      var roundedHeading = Math.round(parseInt(this.heading)/15.0) * 15;
      if (roundedHeading == 360)
        roundedHeading = 0;
      // Got tired of creating arrows so just use black ones.
      var arrowColor = oppositeColorName;
      //var directionImageName = '/googleMap/images/' + arrowColor + 'Arrow_' + roundedHeading + '.gif';
      var directionImageName = '/googleMap/images/' + arrowColor + 'Arrow_' + roundedHeading + '.png';
      directionImage = '<img src="' + directionImageName + '" width="15" height="15" align="absmiddle">';
    }

    // Some agencies might require an icon to be displayed
    // as part of the label.
    var vehicleIcon = getVehicleIconHTML(agencyTag, this.id);

    // Want markers to appear on proper side of road, depending
    // on direction of travel. This way they don't overlap too
    // much.
    var padding;
    var pointerOrientation;
    var pointerImageFile;
    var positionBelow = false;
    var positionLeft = false;
    var intHeading = parseInt(this.heading);
    if (intHeading < 0)
      intHeading += 360;
    if (       intHeading >=   0 && intHeading <  90) {
      padding = 'padding: 7px 0px 0px 7px;';
      pointerOrientation = 'top left';
      pointerImageFile = '/googleMap/images/upperLeftPointer.gif';
      positionBelow = true;
    } else if (intHeading >=  90 && intHeading < 180) {
      padding = 'padding: 7px 7px 0px 0px;';
      pointerOrientation = 'top right';
      pointerImageFile = '/googleMap/images/upperRightPointer.gif';
      positionBelow = true;
      positionLeft = true;
    } else if (intHeading >= 180 && intHeading < 270) {
      padding = 'padding: 0px 7px 7px 0px;';
      pointerOrientation = 'bottom right';
      pointerImageFile = '/googleMap/images/lowerRightPointer.gif';
      positionLeft = true;
    } else if (intHeading >= 270 && intHeading < 360) {
      padding = 'padding: 0px 0px 7px 7px;';
      pointerOrientation = 'bottom left';
      pointerImageFile = '/googleMap/images/lowerLeftPointer.gif';
    }

    // For loyola and potential other places, identify handicapped
    // accessible vehicles (ones whose vehicle id ends with the char
    // 'H'.
    var vehicleLabel = this.route.shortTitle;
    if (this.id.charAt(this.id.length-1) == 'H')
      vehicleLabel += "H/C";

    // Uncomment for debugging
    //labelTextForDebugging += ' (id=' + this.id + ' h=' + parseInt(this.heading) + ')';

    // Create the label to mark where the vehicle is
    var labelHtml =    '<div style="' + padding +
                       ' background: url(' +
                       pointerImageFile +
                       ') no-repeat ' +
                       pointerOrientation +
                       ';"><div class="vehicleStyle" style="background-color:' +
                       color +
                       '; color:' +
                       oppositeColor +
                       '; opacity: 0.90; ' +
                       'border-color:' +
                       "black" + // was oppositeColor but always using black looks better
                       '"><nobr>' +
                       vehicleIcon + 
                       '&nbsp;' +
                       vehicleLabel + labelTextForDebugging +'&nbsp;' +
                       directionImage +
                       '</nobr><\/div><\/div>';
    var label = new ELabel(latLng, labelHtml, null, null, positionBelow, positionLeft, 100);
    map.addOverlay(label);

    this.marker = label;
  }
  
  
  // Removes the vehicle from the map
  VehicleObject.prototype.hideVehicle = function() {
    if (this.marker != null) {
  	  map.removeOverlay(this.marker); 
	  this.marker = null;
	}
  }
  
  // Returns number of seconds since the vehicle last reported
  VehicleObject.prototype.getSecsSinceReport = function() {
    return Math.round(((new Date()).getTime() - this.clockWhenReported)/1000);
  }
  
  VehicleObject.prototype.isStale = function() {
    return this.getSecsSinceReport() > STALE_VEHICLE_TIME_IN_SECS;
  }
  
  
  VehicleObject.prototype.redrawAsStaleVehicleIfStale = function() {
    // If vehicle is now stale but it has not yet been drawn as such...
    if (this.isStale() && !this.drawnAsStale) {
	  this.hideVehicle();
	  drawnAsStale = true;
	}
  }
  
/////////////////////////// NextBus Application ///////////////////////////

  function onResize(){
    var mapDiv = document.getElementById("map");
	mapDiv.style.position = 'absolute';
	mapDiv.style.top = TOP + 'px';
	mapDiv.style.left = LEFT + 'px';
	var windowSize = getWindowSize();
	mapDiv.style.width=(windowSize.width - LEFT - RIGHT - 2*BORDER) + 'px';
	mapDiv.style.height=(windowSize.height - TOP - BOTTOM - 2*BORDER) + 'px';
    mapDiv.style.border=BORDER + 'px ' + BORDER_TYPE;
  };

  // Sets up the map with the initial route(s) 
  function onLoad() {
    // Make the window the proper size
    onResize();
	
    // First, create the map. Use the default cursor, which is the pointer,
    // instead of the standard one so that users can know where the hotspot
    // is and move over a stop to get the stop name via the tooltip.
    map = new GMap2(document.getElementById("map"),
                    {draggableCursor: 'default', draggingCursor: 'crosshair'});

    // Turn on some google map features from v2 of the API
    map.enableDoubleClickZoom();
    map.enableContinuousZoom();

	// Read in data for the specified routes
	for (var i=0; i<routeTags.length; ++i) {
  	  loadRouteData(routeTags[i]);
	}
	setTimeout("finishOnLoadAfterRouteReadIn()", 50);

    // Add the controls for zooming and panning
    addControls();

    // Handle mouse clicks on map. Used for popping up
	// predictions.
    GEvent.addListener(map, "click", handleMouseClickOnMap);
	
	// Add zoom by rectangle feature
    initZoomByRectangleFeature(map);
  }
 
  function addControls() { 
	// Add controls for panning and zooming
    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl());
	map.addControl(new GScaleControl());
  }

  // Once the route XML data is read in, this function is called
  // to set the map parameters.
  function finishOnLoadAfterRouteReadIn() {
    // If routes not loaded yet, then wait some more
    var route;	
    for (var i=0; i<routeTags.length; ++i) {
      route = getRoute(routeTags[i]);
      if (route==null || !route.loaded) {
  	//debug("Still waiting for route=" + routeTags[i] + " to be loaded");
        setTimeout("finishOnLoadAfterRouteReadIn()", 50);
	return;
      }
    }
    debug("Routes loaded in for finishOnLoadAfterRouteReadIn()");

    // Determine range of map and zoom levels
    var validStopSpecified = false;
    if (dirTag != null && stopTag != null) {
      // A specific stop was specified so get the stop object
      var routeTag = routeTags[0];
      debug("Stop specified in url is routeTag=" + routeTag + " dirTag=" + dirTag + " stopTag=" + stopTag);
      route = getRoute(routeTag);
      for (var j=0; j<route.stops.length; ++j) {
        var aStop = route.stops[j]; 
        //debug("  Checking stop dir=" + aStop.dirTag + " stop=" + aStop.tag);
        if ((aStop.dirTag == null || aStop.dirTag == "null" || aStop.dirTag == dirTag) && aStop.tag == stopTag) {
          debug("Found the stop specified in the url");
          stopLatLon = new GLatLng(aStop.point.y, aStop.point.x);
          validStopSpecified = true;
          break;
        }
      }
      // The specified stop is valid so center on it and show predictions.
      if (validStopSpecified) {
        if (zoomLevel == null) {
          var sw = new GLatLng(route.minLat, route.maxLon);
          var ne = new GLatLng(route.maxLat, route.minLon);
          var bounds = new GLatLngBounds(sw, ne);
          zoomLevel = map.getBoundsZoomLevel(bounds) +1;
        }
        map.setCenter(stopLatLon, zoomLevel);
  
        // Show predictions for specified stop
        var matchedStops = new Array();
        matchedStops.push(aStop);
        popupPredictionWindow(matchedStops);
      }
    } 
    if (!validStopSpecified) {
      // No stop was specified so set the zoom level for the map so 
      // all routes are visible.
      // Note: need to use abs() because at least Firefox has a javascript
      // bug when comparing two negative values. For northern hemisphere
      // the longitudes are negative which causes maxLon and minLon to be switched.
      var maxLon = -999.99;
      var maxLat = -999.99;
      var minLon = 999.99;
      var minLat = 999.99;
      for (var i=0; i<routeTags.length; ++i) {
        route = getRoute(routeTags[i]);
        if (maxLon < route.maxLon)
          maxLon = route.maxLon;
        if (minLon > route.minLon)
          minLon = route.minLon;
        if (maxLat < route.maxLat)
          maxLat = route.maxLat;
        if (minLat > route.minLat)
          minLat = route.minLat;
      }
  	
      // NOTE: have to switch max & min longitudes to get proper bounds.
      if (zoomLevel == null) {
        var sw = new GLatLng(minLat, maxLon);
        var ne = new GLatLng(maxLat, minLon);
        var bounds = new GLatLngBounds(sw, ne);
        zoomLevel = map.getBoundsZoomLevel(bounds);
      }

      // Have to do screwy thing with minus signs because otherwise the
      // lats and lons are treated as strings instead of floats!
      if (centerPoint == null) {
        centerPoint = new GLatLng(-(-maxLat-minLat)/2.0, -(-maxLon-minLon)/2.0);
      }
      map.setCenter(centerPoint, zoomLevel);
    }

    // Make the initial route visible. This will also
    // display the vehicles associated with the route.
    for (var i=0; i<routeTags.length; ++i) {
      route = getRoute(routeTags[i]);
      showRoute(route.tag);
    }

    // Some kiosk displays need something like an arrow drawn to 
    // show which stop the predictions are for. To do this
    // call handleAdditionalKioskFeatures(), which can be
    // overriden by a kiosk page if something special needs to
    // done.
    handleAdditionalKioskFeatures();

    // Hide the box telling user that system is initializing. 
    // Otherwise it can be temporarily visible when popping up
    // and InfoWindow or such.
    var initializingBox = document.getElementById('initializingBox');
    initializingBox.style.visibility = 'hidden';
	
    // And update the position of vehicles every TIMEOUT_BETWEEN_UPDATING_VEHICLES
    setInterval("updateVehiclePositions(false)", TIMEOUT_BETWEEN_UPDATING_VEHICLES);
  }

  function handleAdditionalKioskFeatures() {
  }

  // Returns the route object associated with the route tag
  function getRoute(routeTag) {
    for (var i=0; i<routes.length; ++i) {
      if (routes[i].tag == routeTag)
	return routes[i];
      }
    return null;
  }
  
  
  // Returns true if the route is visible
  function isRouteVisible(routeTag) {
    var route = getRoute(routeTag);
	if (route == null) {
	  return false;
    } else {
	  return route.visible;
	}
  }
	  
	   
  // If haven't read in route before, loads route data from XML file
  // and creates the corresponding path polylines and stop markers. 
  // Returns the route.
  function loadRouteData(routeTag) {
    debug("In loadRouteData(). Loading route data for route " + routeTag);
	
    // The route object to be returned. If one already created,
    // then return it.
    var route = getRoute(routeTag);
    if (route != null) {
      if (!route.loading) {
        debug("In loadRouteData(). Route " + routeTag + " already exists so returning it");
        return route;
      }
    } else {
      route = new RouteObject(routeTag);
    }
	
    // Read in route configuration XML
    route.loading = true;
    var request = GXmlHttp.create();
    var url = "/service/googleMapXMLFeed?command=routeConfig&a="+agencyTag+"&r="+routeTag + "&key=" + keyForNextTime +
				           (agencyMap?"&agencyMap=true":"");
    debug("url=" + url + "");						
    request.open("GET", url, true);
    request.onreadystatechange = function() {
        if (request.readyState == 4) {
        // If error returned by server, display error message and return.
        if (request.status != 200) {
          error("loadRouteData(): returned status was " + request.status + ": " + request.statusText);
          return;
        }
		  
        debug("Reading in XML for route " + routeTag);
        var xmlDoc = request.responseXML;
        if (xmlDoc == null) {
          debug("In loadRouteData(). the xmlDoc was null so trying again");
          setTimeout('loadRouteData("' + routeTag + '")', 1000);
          return;
        }
		  
        // If there was an error message display it and return
        var errors = xmlDoc.documentElement.getElementsByTagName("Error");
        if (errors.length > 0) {
          var shouldRetry = errors[0].getAttribute("shouldRetry");
          if (shouldRetry != null && shouldRetry.match("true")) {
            debug("In loadRouteData(). Agency not yet initted so setting timeout to load data again for route=" + routeTag);
            // The agency wasn't inited yet so retry again in a second
            setTimeout('loadRouteData("' + routeTag + '")', 1000);
          } else {
            error(errors[0].firstChild.data);
          }
            return;
          }
	 
          // Get the key to be used next time XML feed is called.
          var keysForNextTime = xmlDoc.documentElement.getElementsByTagName("keyForNextTime");
          keyForNextTime = keysForNextTime[0].getAttribute("value");				
          // If route object doesn't yet exist, create it.
          // This also adds it to the global routes[] array.
          // The loaded class param will be set to false to
          // indicate that the object is not ready to use yet.
          var route = getRoute(routeTag);
          if (route == null)
            route = new RouteObject(routeTag);

          // Get route configuration information from XML document
          var routes = xmlDoc.documentElement.getElementsByTagName("route");
          for (var i = 0; i < routes.length; i++) {
            // Store the other route elements
            route.title = routes[i].getAttribute("title");
            route.shortTitle = routes[i].getAttribute("shortTitle");
            if (route.shortTitle == null)
              route.shortTitle = route.title;
            route.color = routes[i].getAttribute("color");
            route.oppositeColor = routes[i].getAttribute("oppositeColor");
			
            // Read in the stop info
            var stops = routes[i].getElementsByTagName("stop");
            for (var j=0; j<stops.length; ++j) {
              var lat = stops[j].getAttribute("lat");
              // For some reason getElementsByTagName() returns all stop tags
              // in the route tag, even if they are nested below another tag.
              // Since we now supply a list of stops for each direction,
              // the XML parsing gets confused. Therefore ignore the stops
              // that don't have a longitude because those are just
              // part of the direction description.
              if (lat==null)
                continue;
              var dirTag = stops[j].getAttribute("dirTag");
              var lon = stops[j].getAttribute("lon");
              var tag = stops[j].getAttribute("tag");
              var title = stops[j].getAttribute("title");
              var point = new GPoint(parseFloat(lon), parseFloat(lat));
			 
              var tooltipText = getI18NStr("Stop: ") + title; 
              var marker = new GMarker(point, {icon: stopMarkerIcon, title: tooltipText, clickable: false});

              // Keep track of the stop markers so that they can be turned on and off
              route.stopMarkers.push(marker);
			  
              // Keep track of the stops so can show predictions when they are 
              // clicked on or near.
              var aStop = new StopObject(tag, title, dirTag, point, marker, route);
              route.stops.push(aStop);
            }
			
            // Read in the path info
            var paths = routes[i].getElementsByTagName("path");
            for (var j=0; j<paths.length; ++j) {
              var pointsForPath = [];
              var points = paths[j].getElementsByTagName("point");
              for (var k=0; k<points.length; ++k) {
                var lat = points[k].getAttribute("lat");
                var lon = points[k].getAttribute("lon");
                pointsForPath.push(new GLatLng(parseFloat(lat), parseFloat(lon)));
			 
                // Update min and max lat and lon
                if (lon < route.minLon) {
                  route.minLon = lon;
                }
                if (lon > route.maxLon) {
                  route.maxLon = lon;
                }
                if (lat < route.minLat)
                  route.minLat = lat;
                if (lat > route.maxLat)
                  route.maxLat = lat;				  				 

                // Seems that as of 5/11/2006 GPolyline can only
                // handle a certain number of points. It works fine
                // for 418 points but when tried wmata route 13F
                // with 755 points cpu usage went to 100% and the
                // paths were never drawn. Therefore limit size
                // of polylines to 300 points.
                if (pointsForPath.length == 300) {
                  //debug("=== adding 300 point path");
                  var polyLine = new GPolyline(pointsForPath, 
			                       '#' + routes[i].getAttribute("color"), 
				  	       4,    // weight
					       1.0); // opacity
                  route.pathPolylines.push(polyLine);
                  // Add event handler because as of google release 2.88
                  // the polylines handle events separately.
                  GEvent.addListener(polyLine, "click", handleMouseClickOnPolyline);
                  pointsForPath = [];
                }
              }  // End of for each point in a path
			
              // Draw the path
              //debug("=== adding " + pointsForPath.length + " point path");
              var polyLine = new GPolyline(pointsForPath, 
                                           '#' + routes[i].getAttribute("color"), 
                                           4,    // weight
                                           1.0); // opacity
								   
              route.pathPolylines.push(polyLine);

              // Add event handler because as of google release 2.88
              // the polylines handle events separately.
              GEvent.addListener(polyLine, "click", handleMouseClickOnPolyline);
            } // End of for each path
          } // End of for each route	  
		  
          route.loading = false;
          route.loaded = true;
        }  // End of if (request.readyState == 4)
    }  // End of onreadystatechange function
    request.send(null);
  }
  
  // For some reason IE is caching the results of the vehicleLocation
  // request so the the location of the bus is not being updated. Therefore
  // change the request each time so that a cached value is not used. Do
  // this by appending an incremented count to each request using the cnt
  // variable.
  var cnt=0;
 
  // Reads from the server a XML doc with the information associated
  // with vehicles that are on the route specified. If the
  // readAllVehicles parameter is true, then all vehicles associated
  // with route read in. This is important to do when turning on a
  // route or making vehicles visible. If not true, then will
  // only read in vehicles that have had a new report since
  // the route.lastTime. This reduces the amount of data plus it
  // prevents icons from flashing if the vehicles haven't moved.
  function updateVehiclePositionsForRoute(route, readAllVehicles) {
      // If route visible then read in associated vehicles
      if (shouldShowVehicles()&& route.visible) {
        // Read in route configuration XML
        var request = GXmlHttp.create();
		var lastTime = 0;
		if (readAllVehicles) {
		  // Since reading in all vehicles, should remove all
		  // the old ones. This is important for when stale vehicle
		  // are to be shown.
		  route.removeVehiclesForRoute();
		  route.vehicles = new Array();
		} else {
		  lastTime = route.lastTime;
		}
		
		// Go through vehicles for route and if any are stale redisplay them as a
		// stale vehicle.
		for (var vehicleId in route.vehicles) {
	      vehicle = route.vehicles[vehicleId];
		  if ((typeof vehicle)=="object")
		    vehicle.redrawAsStaleVehicleIfStale();
		}
		
		 
        debug("updateVehiclePositionsForRoute() - " +
              "<b>Requesting vehicle info XML for route=" + route.tag + "</b> for lastTime=" + lastTime);		
		var url = "/service/googleMapXMLFeed?command=vehicleLocations&a="+
					    agencyTag+"&r="+route.tag + "&t=" + lastTime + 
                        "&key=" + keyForNextTime + (agencyMap?"&agencyMap=true":"") + "&cnt=" + cnt++;
        debug("url=" + url + "");						
        request.open("GET", url, true);
        request.onreadystatechange = function() {
              if (request.readyState == 4) {
		        debug("updateVehiclePositionsForRoute() - " +
                      "Reading in vehicle location XML for route " + route.tag);

		        // If error returned by server, display error message and return.
		        if (request.status != 200) {
		          error("updateVehiclePositionsForRoute(): returned status was " + 
                         request.status + ": " + request.statusText);
		          return;
		        }
		  
                var xmlDoc = request.responseXML;
 	            if (xmlDoc == null)
		          return;
 
   		        // If there was an error message and it wasn't a should retry, display error and return
		        var errors = xmlDoc.documentElement.getElementsByTagName("Error");
		        if (errors.length > 0) {
		          var shouldRetry = errors[0].getAttribute("shouldRetry");
			      if (shouldRetry == null && !shouldRetry.match("true")) {
		            error("in updateVehiclePositionsForRoute(): " + errors[0].firstChild.data);
			      }
			      return;
		        }

                // Determine the last time for the next time this function
				// is called to read in vehicles for this route.				  
                var lastTimes = xmlDoc.documentElement.getElementsByTagName("lastTime");
 			    route.lastTime = lastTimes[0].getAttribute("time");
				
				// Get the key to be used next time XML feed is called.
				var keysForNextTime = xmlDoc.documentElement.getElementsByTagName("keyForNextTime");
				keyForNextTime = keysForNextTime[0].getAttribute("value");
				
				// Read in position and other information for vehicles
				var vehicles = xmlDoc.documentElement.getElementsByTagName("vehicle");
				// For each vehicle get the params and then draw it
                for (var j=0; j<vehicles.length; ++j) {
				  var id = vehicles[j].getAttribute("id");
				  var routeTag = vehicles[j].getAttribute("routeTag");
				  if (routeTag == "null")
				    routeTag = "";
				  var lat = vehicles[j].getAttribute("lat");
				  var lon = vehicles[j].getAttribute("lon");
				  var latLon = new GLatLng(parseFloat(lat), parseFloat(lon));
                  var lateSec = vehicles[j].getAttribute("lateSec");
				  var job = vehicles[j].getAttribute("job");
				  if (job == "null")
				    job = "";
				  var predictable = ("true" == vehicles[j].getAttribute("predictable"));
				  var heading = vehicles[j].getAttribute("heading");
				  var speedCMSec = vehicles[j].getAttribute("speedCMSec");
				  var secsSinceReport = vehicles[j].getAttribute("secsSinceReport");
				  var clockWhenReported = (new Date()).getTime() - parseInt(secsSinceReport)*1000;
                  debug("updateVehiclePositionsForRoute() - read in vehicle id=" + id + 
                        " routeTag=" + routeTag + 
                        " lat=" + lat + " lon=" + lon + 
                        " lateSec=" + lateSec + 
                        " job=" + job + " predictable=" + predictable + 
                        " heading=" + heading + " speedCMSec=" + speedCMSec + 
                        " secsSinceReport=" + secsSinceReport);

                  // Draw the vehicle by first erasing the old marker if there is one.
                  // Also, store the previous latLon so can move vehicles smoothly.
                  var previousLatLon = latLon;
				  if (route.vehicles[id] != null && route.vehicles[id].marker != null) {
				    debug("updateVehiclePositionsForRoute() - erasing vehicle=" + id);
                    var vehicle = route.vehicles[id];
				    vehicle.hideVehicle();

                    previousLatLon = vehicle.latLng;
				  }

                  // If the route tag returned is not for the route specified, then it
				  // means that the vehicle changed routes. For this case should not
				  // draw the vehicle. Therefore only draw the vehicle if the route
				  // tags match.
				  if (route.tag == routeTag) {
				    // Create a new vehicle object. The constructor automatically
				    // adds it to the route and adds marker to the map.				
				    var vehicle = new VehicleObject(id, previousLatLon, latLon, 
                                                    heading, speedCMSec, 
                                                    job, predictable, lateSec, 
								         	        clockWhenReported, route.tag);

                    // If the vehicle has moved (instead of just being new) then
                    // remember that this vehicle marker needs to be interpolated.
                    if (!previousLatLon.equals(latLon)) {
                      debug("Vehicle=" + id + " has moved so adding it to vehiclesBeingScootched");
                      vehiclesBeingScootched.push(vehicle);	
                    }

                    // Add marker at first interpolated position.
                    // The timeout will update the vehicle at the remaining
                    // interpolated positions.
                    vehicle.addInterpolatedMarker(1, INTERPOLATION_CNT, heading);  
				  }  // End of draw marker if of proper route.
                }  // End of for each vehicle
		      }

              // Scootch the vehicles using a timeout
              if (vehiclesBeingScootched.length > 0) {
                currentInterpolationCnt = 2;
                setTimeout("scootchVehicles()", TIMEOUT_BETWEEN_SCOOTCHING);
              }
            }; // End of onreadystatechange function
			
	    // Finish sending the XML request
        request.send(null);
	  }  // End of if route visible
    }
  
  
  // Draws vehicles for routes that visible. Called
  // every few seconds via an Interval.
  function updateVehiclePositions(readAllVehicles) {
    // For each visible route
    for (var i=0; i<routes.length; ++i) {
      var route = routes[i];
      updateVehiclePositionsForRoute(route, readAllVehicles);
    }  // End of for each route
  }

  // Draws vehicles in interpolated position
  function scootchVehicles() {
    debug("In scootchVehicles() and currentInterpolationCnt=" + currentInterpolationCnt);

    // If done just clean up and return
    if (currentInterpolationCnt > INTERPOLATION_CNT) {
      debug("Should not be here!");
      vehiclesBeingScootched = new Array();
      return;
    }

    // Draw the vehicle markers in their new interpolated locations
    for (var i=0; i<vehiclesBeingScootched.length; ++i) {
      var vehicle = vehiclesBeingScootched[i];

      // Hide old marker
      vehicle.hideVehicle();

      // Draw new marker if route still visible
      if (vehicle.route.visible) {
        vehicle.addInterpolatedMarker(currentInterpolationCnt, INTERPOLATION_CNT);
      }
    }
 
    // Set the next timeout if need to continue
    if (currentInterpolationCnt < INTERPOLATION_CNT) {
      ++currentInterpolationCnt;
      setTimeout("scootchVehicles()", TIMEOUT_BETWEEN_SCOOTCHING);
    } else {
      // Done scootching these vehicles so reset the array
      debug("vehiclesBeingScootched array reset");
      vehiclesBeingScootched = new Array();
    }
  }
  
  
  // First makes sure that route object exists and has been
  // loaded. Then displays the route
  function showRoute(routeTag) {
    debug("In showRoute() for route " + routeTag);
    var route = getRoute(routeTag);
    if (route == null) {
      // Read in route xml and then call this function again
      debug("showRoute() - Route " + routeTag + 
            " not loaded yet so loading it now and will call showRoute() again in a bit.");
      loadRouteData(routeTag);
      setTimeout("showRoute('" + routeTag + "')", 50);
      return;
    }
	
    // If route XML configuration not fully loaded yet
    // then wait a bit and simply call this function again.
    if (!route.loaded) {
      debug("showRoute() - Route " + routeTag + " not fully loaded yet so checking again in a bit");
      setTimeout("showRoute('" + routeTag + "')", 50);
      return;
    }
	
    route.visible = true;

    // Make the route stops visible
    if (shouldShowStops()) {
      debug("showRoute() - Route " + routeTag + " making the stops visible");
      for (var i=0; i<route.stopMarkers.length; ++i) {
        map.addOverlay(route.stopMarkers[i]);
      }
    }
	
    // Get new positions of vehicles. Read back to the beginning of time
    // since the locations that we had are mostly likely old by now.
    debug("showRoute() - Route " + routeTag + " getting vehicle positions for route");
    updateVehiclePositionsForRoute(route, true);
	
    // Make route path visible
    debug("showRoute() - Route " + routeTag + " making paths visible");
    for (var i=0; i<route.pathPolylines.length; ++i) {
      map.addOverlay(route.pathPolylines[i]);
    }
    debug("showRoute() - finished making the paths visible");
  }
  
  
  // Hides the path and stops associated with a route
  function hideRoute(routeTag) {
    debug("hideRoute() - Route" + routeTag);
    var route = getRoute(routeTag);
    if (route == null)
      return;

    // Mark route as invisible	  
    route.visible = false;
	  
    // Remove the stop markers
    if (shouldShowStops()) {
      debug("hideRoute() - Hiding stops for route " + routeTag);
      for (var i=0; i<route.stopMarkers.length; ++i)
        map.removeOverlay(route.stopMarkers[i]);
    }
	
    // Remove the vehicles if they were being shown
    route.removeVehiclesForRoute();
	  
    // Remove the route polylines
    debug("Removing polylines for route " + routeTag);
    for (var i=0; i<route.pathPolylines.length; ++i)
      map.removeOverlay(route.pathPolylines[i]);
  }
  
  
  function shouldShowVehicles() {
    return document.theForm.showVehiclesCheckbox.checked;
  }

  
  function shouldShowStops() {
    return document.theForm.showStopsCheckbox.checked;
  }
  
  function shouldShowStaleVehicles() {
    // If vehicles check box doesn't exist, return false.
    if (document.theForm.showStaleVehiclesCheckbox == null)
	  return false;
	  
    return document.theForm.showStaleVehiclesCheckbox.checked;
  }
  
  function showOrHideNoBlockVehicles() {
    // Get or create the route object for vehicles with no route
    var route = getRoute("");
	if (route == null) {
	  route = new RouteObject("");
	}
	
    if (document.theForm.showNoBlockVehiclesCheckbox.checked) {
	  route.visible = true;
	  updateVehiclePositionsForRoute(route, true);
	} else {
	  route.visible = false;
      route.removeVehiclesForRoute();
	}
  }

  // Called when user clicks on the show stale vehicles checkbox.
  // Redisplays the vehicles for all visible routes which in
  // turn takes care of displaying or not displaying stale vehicles.
  function showOrHideStaleVehicles() {
      // For each route that is visible, redisplay the vehicles
      for (var i=0; i<routes.length; ++i) {
        var route = routes[i];

      // If route not visible then don't need to do anything anyways
      if (!route.visible) 
	    continue;
		
	  // The route is visible. Show or hide the stale vehicles.
	  if (shouldShowStaleVehicles()) {
	    updateVehiclePositionsForRoute(route, true);
	  } else {
	    // For each vehicle, if a vehicle is stale hide it
		for (var vehicleId in route.vehicles) {
	      vehicle = route.vehicles[vehicleId];
		  if ((typeof vehicle)=="object" && vehicle.isStale())
 		    vehicle.hideVehicle();
	    }
	  }
	}
  }

  // Called when user clicks on the Show Vehicles checkbox.
  // Shows or hides all vehicles depending on whether
  // document.theForm.showStopsCheckbox is checked
  function showOrHideVehicles() {
    for (var i=0; i<routes.length; ++i) {
      var route = routes[i];

      // If route not visible then don't need to do anything anyways
      if (!route.visible) 
        continue;
		
      // If should show vehicles
      if (shouldShowVehicles()) {
        // make the vehicles associated with the route visible
        updateVehiclePositionsForRoute(route, true);
      } else {  // should hide vehicles
        // For each vehicle for the route
        for (var vehicleId in route.vehicles) {
          vehicle = route.vehicles[vehicleId];
          if ((typeof vehicle)=="object")
            vehicle.hideVehicle();
        }
      }
    }
  }
  
  	  
  // Called when user clicks on the Show Stops checkbox.
  // Shows or hides all stops depending on whether
  // document.theForm.showStopsCheckbox is checked
  function showOrHideStops() {
	for (var i=0; i<routes.length; ++i) {
	  var route = routes[i];

      // If route not visible then don't need to do anything anyways
      if (!route.visible) 
	    continue;
	  
	  // If should show stops
  	  if (shouldShowStops()) {
	    // make the stops associated with the route visible
		for (var j=0; j<route.stopMarkers.length; ++j) {
 	      map.addOverlay(route.stopMarkers[j]);
		}
	  } else {
  	    // For each stop in route
        for (var j=0; j<route.stopMarkers.length; ++j) {
		  // Hide the stops
          map.removeOverlay(route.stopMarkers[j]);
		}
      }
    }
  }
     
  // Called when the map or marker is clicked on. 
  function handleMouseClickOnMap(overlay, point) {
    // Determine where user clicked. If the overlay param is set
    // then the user has clicked on the info window to close it
    // so ignore.
	if (overlay != null)
	    return;

    lowLevelHandleMouseClick(point);
  }

  // Called when a polyline is clicked on. 
  function handleMouseClickOnPolyline(point) {
    lowLevelHandleMouseClick(point);
  }


  // Called when the map or a polyline is clicked on. 
  function lowLevelHandleMouseClick(point) {
	// Determine latitude and longitude distance that corresponds to the 
	// RADIUS_IN_PIXELS_FOR_MATCHING_CLICK_TO_STOPS
    var zeroZeroLatLon = map.fromDivPixelToLatLng(new GPoint(0, 0));
    var radiusLatLon = map.fromDivPixelToLatLng(new GPoint(RADIUS_IN_PIXELS_FOR_MATCHING_CLICK_TO_STOPS,
                                                           RADIUS_IN_PIXELS_FOR_MATCHING_CLICK_TO_STOPS));
    var radiusInLatLon = new GPoint(Math.abs(radiusLatLon.lat() - zeroZeroLatLon.lat()),
                                    Math.abs(radiusLatLon.lng() - zeroZeroLatLon.lng()));

	// Now find all visible stops within the number of pixels of the click
	var matchedStops = new Array();
	for (var i=0; i<routes.length; ++i) {
	  var route = routes[i];
	  // Only check routes that are visible
	  if (route.visible) {
		// For each stop
		for (var j=0; j<route.stops.length; ++j) {
		  var aStop = route.stops[j]; 
		  var deltaLon = Math.abs(point.x - aStop.point.x);
		  var deltaLat = Math.abs(point.y - aStop.point.y);

		  if (deltaLat < radiusInLatLon.x && deltaLon< radiusInLatLon.y) {
            // XML feed provides predictions for all directions for stop
            // so that it works with the complicated unitrans situation.
            // So only need to query a route/stop once, even if it is
            // on multiple directions.
            var alreadyAddedStop = false;
            for (var k=0; k<matchedStops.length; ++k) {
              if (matchedStops[k].route.tag == route.tag && matchedStops[k].tag == aStop.tag) {
                alreadyAddedStop = true;
                break;
              }
            }
            if (!alreadyAddedStop) {
		      matchedStops.push(aStop);
  		      debug("*** found match with route=" + route.tag + " stop=" + aStop.title + " stopTag=" + aStop.tag + " point=" + aStop.point);
            } // End of if don't have stop already
		  } // End of found a stop close to where clicked
		}  // End of for each stop
	  }  // End of if route is visible
	}  // End of for each route
	
	// Popup info window
	if (matchedStops.length >= 1)
	  popupPredictionWindow(matchedStops);
  }  // End of lowLevelHandleMouseClick()


  // When timeout occurs, then the predictions should be updated.
  // Only do so though if the popup prediction window has not
  // been closed by the user.
  function timeOccurredSoPopupPredictionWindow(stops) {
    if (globalPredictionInfoWindowOpen)
      popupPredictionWindow(stops);
  }


  // Gets predictions associated with the stops passed in andn
  // pops up a info window displaying them.
  function popupPredictionWindow(stops) {
    var stopTags = "";
	for (var i=0; i<stops.length; ++i) {
	  var aStop = stops[i];
	  stopTags += "&stops=" + aStop.route.tag + 
	              "|" +   // The tag divider
                  aStop.dirTag +
	              "|" +   // The tag divider
				  aStop.tag;
	}
	
	// Get the predictions for the stops from the server
	var request = GXmlHttp.create();
	debug("XML command=/service/googleMapXMLFeed?command=predictionsForMultiStops&a="+agencyTag+stopTags);
    request.open("GET", 
	             "/service/googleMapXMLFeed?command=predictionsForMultiStops&a="+agencyTag+stopTags + "&key=" + keyForNextTime +
				      (agencyMap?"&agencyMap=true":""), 
				 true);
    request.onreadystatechange = function() {
	      // If not done with resquest, return
          if (request.readyState != 4) 
		    return;
		 
		  // If error returned by server, display error message and return.
		  if (request.status != 200) {
		    error("popupPredictionWindow(): returned status was " + request.status + ": " + request.statusText);
		    return;
		  }

		  // Read in the XML
          var xmlDoc = request.responseXML;
          if (xmlDoc == null)
		    return;
			
		  // If there was an error, handle it
		  var errors = xmlDoc.documentElement.getElementsByTagName("Error");
		  if (errors.length > 0) {
		    debug("Error in popupPredictionWindow(): " + errors[0].firstChild.data);
		    return;
	  	  }		
				  
		  // Get the key to be used next time XML feed is called.
		  var keysForNextTime = xmlDoc.documentElement.getElementsByTagName("keyForNextTime");
		  keyForNextTime = keysForNextTime[0].getAttribute("value");
				
		  // Get stop prediction information from XML document
		  // and put it into the stopPredictions array
          var stopPredictions = new Array();
          var stopXMLs = xmlDoc.documentElement.getElementsByTagName("predictions");
		  // For each stop...
          for (var j=0; j<stopXMLs.length; j++) {
		    // Store the other route elements
			var routeTitle = stopXMLs[j].getAttribute("routeTitle");
			var stopTitle = stopXMLs[j].getAttribute("stopTitle");
			var directionXMLs = stopXMLs[j].getElementsByTagName("direction");
            var messageXMLs = stopXMLs[j].getElementsByTagName("message");
            var message = null;
            if (messageXMLs.length > 0) {
              message = "";
              for (var k=0; k<messageXMLs.length; ++k) {
                if (k>0)
                  message += "<br>";
                message += messageXMLs[k].getAttribute("text");
              }
            }
			// For each direction...
			if (directionXMLs.length == 0) {
			  // If there are no directions for this stop, there are no predictions. 
			  // Still need to create a PredictionsForStopObject though so can display
			  // route and stop titles and such.
			  var dirTitleBecauseNoPredictions = stopXMLs[j].getAttribute("dirTitleBecauseNoPredictions");
              var stopPrediction = new PredictionsForStopObject(routeTitle, stopTitle, dirTitleBecauseNoPredictions, null);
	  		  stopPredictions.push(stopPrediction);
			} else {
  			  for (var k=0; k<directionXMLs.length; ++k) {
  			    var dirTitle = directionXMLs[k].getAttribute("title");
			    var predictionXMLs = directionXMLs[k].getElementsByTagName("prediction");
			    // For each prediction...
			    var predictions = new Array();
 			    for (var kk=0; kk<predictionXMLs.length && kk<3; ++kk) {
			      var predictionInSecondsStr = predictionXMLs[kk].getAttribute("seconds");
  			      var isDeparture = predictionXMLs[kk].getAttribute("isDeparture");
			      var prediction = new Prediction(predictionInSecondsStr, isDeparture);
                  predictions.push(prediction);

                  debug("Read prediction for route=" + routeTitle + 
                        " direction=" + dirTitle + 
                        " stopTitle=" + stopTitle +
                        " predictionInSecondsStr=" + predictionInSecondsStr);
			    }
                var stopPrediction = new PredictionsForStopObject(routeTitle, stopTitle, dirTitle, predictions, message);
	  		    stopPredictions.push(stopPrediction);
			  }
			}
		  }		  

		  // Now for each of the stops, display it along with the predictions
	      var html = "<b>" + getI18NStr("Next Tracked Vehicles:") + 
                          "</b><br/><hr width='240'/>";

          // Add all the predictions (if there are any) to the html
		  for (var j=0; j<stopPredictions.length; ++j) {
		    // If not first stop in list, add space so that stops not bunched too closely
            if (j != 0)
			  html += "<br/><br/>";
			  	
			var stopPrediction = stopPredictions[j];
			
			// Determine if there are another stops in list with the same
			// route and stop name. This means that only the direction
			// differs. If so, the predictions for the stop should be shown together
			// to make things more readable
			var perdictionsForSameStop = new Array();
			if (stopPredictions.length > 1) {
              var k=j+1;
  			  while (k<stopPredictions.length) {
			    if (stopPrediction.routeTitle == stopPredictions[k].routeTitle &&
				    stopPrediction.stopTitle == stopPredictions[k].stopTitle) {
				  // Match found so remember the second associated stop
				  perdictionsForSameStop.push(stopPredictions[k]);
				  
				  // Remove the second stop from the stop list
				  for (var jj=k; jj<stopPredictions.length-1; ++jj) {
				    stopPredictions[jj] = stopPredictions[jj + 1];
				  }
				  stopPredictions.pop();
				} else {
                  // route and stop didn't match so check the next prediction
                  ++k;
                }
			  }
			}
			
			// Determine the proper string "Predictions", "Departures", or "Arrivals"
			// to display in front of the predictions.
			var arrivalDepartureStr;
			if (stopPrediction.predictions == null) {
			  // Don't know if it is arrival or departure so use something neutral.
			  arrivalDepartureStr = "Predictions";
			} else {			
		      var isDeparture = stopPrediction.predictions[0].isDeparture;
			  arrivalDepartureStr = isDeparture ? 
                getI18NStr("Departures:") : getI18NStr("Arrivals:");
			}
			if (perdictionsForSameStop.length == 0) {
			  // There was no second prediction for same route and stop but different
			  // direction so display normally.				  
	          html += "<div style='padding-right: 8px; width: 22em;'>";  // Note: need to specify a width in order for cells in table to be proper
	          html += "<style>.lc {color:grey;padding-right:3px; text-align: right}</style>"
	          html += "<table style='font-size: 80%; overflow: auto;'>";
              html += "<tr><td class='lc'>" + getI18NStr("Route:") + "</td><td>" + stopPrediction.routeTitle + "</td></tr>";
              html += "<tr><td class='lc'>" + getI18NStr("Direction:") + "</td><td>" + stopPrediction.dirTitle + "</td></tr>";
              html += "<tr><td class='lc'>" + getI18NStr("Stop:") + "</td><td>" + stopPrediction.stopTitle + "</td></tr>";
              html += "<tr><td class='lc'>";
		      html += "<nobr>" + arrivalDepartureStr + "</nobr>";
			  html += "</td><td>" + getPredictionHtml(stopPrediction.predictions) + "</td></tr>";			  
              if (stopPrediction.message != null) {
                html += "<tr><td class='lc'>" + getI18NStr("Message:") + "</td><td bgcolor='#FFCCCC'>" + stopPrediction.message + "</td></tr>";
              }
	          html += "</table>";
	          html += "</div>";

			} else {
			  // There were at least two predictions for same stop and route so combine display of them
	          html += "<div style='padding-right: 8px; width: 20em;'>";  // Note: need to specify a width in order for cells in table to be proper
	          html += "<style>.lc {color:grey;padding-right:3px; text-align: right}</style>"
	          html += "<table cellpadding='0' style='font-size: 80%; overflow: auto;'>";
              html += "<tr><td class='lc'>" + getI18NStr("Route:") + "</td><td>" + stopPrediction.routeTitle + "</td></tr>";
              html += "<tr><td class='lc'>" + getI18NStr("Stop:") + "</td><td>" + stopPrediction.stopTitle + "</td></tr>";
              html += "<tr><td class='lc'><img src='../images/blank.gif' width='0' height='16' border='0'>Direction:</td><td valign='bottom'>" + stopPrediction.dirTitle + "</td></tr>";
              html += "<tr><td class='lc'>";
		      html += "<nobr>" + arrivalDepartureStr + "</nobr>";
			  html += "</td><td>" + getPredictionHtml(stopPrediction.predictions) + "</td></tr>";
			  for (kk=0; kk<perdictionsForSameStop.length; ++kk) { 
			    var associatedStopPrediction = perdictionsForSameStop[kk];
                html += "<tr><td class='lc'><img src='../images/blank.gif' width='0' height='16' border='0'>Direction:</td><td valign='bottom'>" + associatedStopPrediction.dirTitle + "</td></tr>";
                html += "<tr><td class='lc'>";
		        html += "<nobr>" + arrivalDepartureStr + "</nobr>";
				html += "</td><td>" + getPredictionHtml(associatedStopPrediction.predictions) + "</td></tr>";
                if (stopPrediction.message != null) {
                  html += "<tr><td class='lc'>" + getI18NStr("Message:") + "</td><td bgcolor='#FFCCCC'>" + stopPrediction.message + "</td></tr>";
                }
			  }
	          html += "</table>";
	          html += "</div>";
			}
		}  // End of displaying each stop

            // Before open up the new info window, clear old listeners
            // so that new ones and old ones don't interfere with each other. This
            // is important because the infowindow close event happens asynchronously.
            GEvent.clearListeners(stops[0].marker, "infowindowopen");
            GEvent.clearListeners(stops[0].marker, "infowindowclose");

            // Add listeners so we can keep track of whether prediction window is open
	    GEvent.addListener(stops[0].marker, 
                               "infowindowopen", 
                               function() { globalPredictionInfoWindowOpen = true; });  
	    GEvent.addListener(stops[0].marker, 
                               "infowindowclose", 
                               function() { globalPredictionInfoWindowOpen = false; });  

	    // Open up the info window and associate it with the first marker on the list.
	    // Can do this because that marker only specifies where the info window 
	    // should be located.
	    debug("HTML=" + html);
	    stops[0].marker.openInfoWindowHtml(html);

	    // Store the stops array in a global so that it can
	    // be accessed when timeout causes predictions to be updated.
	    globalStops = stops;

	    // Call this function again after timeout thas elapsed
            clearTimeout(globalInfoWindowTimeout);
	    globalInfoWindowTimeout = setTimeout("timeOccurredSoPopupPredictionWindow(globalStops)", 
	                                         TIMEOUT_BETWEEN_UPDATING_PREDICTIONS);
	  }  // End of embedded function onreadystatechange
	
	// Finish the sending of the XML request
    request.send(null);
  }  // End of function popupPredictionWindow()
  
  
  // Takes an array of predictons in seconds as strings and 
  // returns corresponding html.
  // Takes in Array of Prediction objects.
  function getPredictionHtml(predictions) {
    var html = "<b>";
	if (predictions == null || predictions.length == 0)
	  html += getI18NStr("No current prediction");
	else {
  	  for (var i=0; i<predictions.length && i<3; ++i) {
		var predictionInSecondsStr = predictions[i].predictionStr;
		var predictionsInSeconds = parseInt(predictionInSecondsStr);
		var predictionInMinutes = Math.round(predictionsInSeconds/60 - 0.5);
		if (i == 1) {
		  if (predictions.length >=3)
			html += ", ";
		  else
			html += " & ";
		} else if (i==2) {
		  html += ", & ";
		}
		if (predictionInMinutes == 0) {
          if (predictions[i].isDeparture) {
		    html += "<blink>" + getI18NStr(departingWord) + "</blink>";
          } else {
		    html += "<blink>" + getI18NStr(arrivingWord) + "</blink>";
          }
		} else {
		  html += predictionInMinutes + "min";
		}
	  }  // End of for each prediction
	}
	
	html += "</b>";
	return html;
  }
  

  // Have to use this kind of indirect function call so that 
  // can have access to parameters.
  function getHandleVehicleMarkerClickFunction(vehicle) {
    return function() { handleVehicleMarkerClick(vehicle); };
  }
  
  
  // Opens up an info window that displays information about the 
  // vehicle clicked on. This only works for agency map since the
  // information provided is only useful to supervisors.
  function handleVehicleMarkerClick(vehicle) {
    if (agencyMap) { 
      var html = "<b>Vehicle Information:</b><br/><hr width='160'/>";
	  html += "<div style='padding-right: 8px; width: 11em;'>";  // Note: need to specify a width in order for cells in table to be proper
  	  html += "<style>.lc {color:grey;padding-right:3px; text-align: right}</style>"
	  html += "<table style='font-size: 80%; overflow: auto;'>";
      html += "<tr><td class='lc'>Vehicle:</td><td>" + vehicle.id + "</td></tr>";
      html += "<tr><td class='lc'>Route:</td><td>" + vehicle.route.title + "</td></tr>";
      html += "<tr><td class='lc'>Heading:</td><td>" + vehicle.heading + "</td></tr>";
      html += "<tr><td class='lc'>Speed:</td><td>" + vehicle.speedMetersPerSecond + " m/sec</td></tr>";
      html += "<tr><td class='lc'>Block:</td><td>" + vehicle.job + "</td></tr>";
      html += "<tr><td class='lc'>Predictable:</td><td>" + vehicle.predictable + "</td></tr>";
      html += "<tr><td class='lc'>Lateness:</td><td>" + vehicle.lateSec + " secs</td></tr>";
      html += "<tr><td class='lc'>Since report:</td><td>" +  vehicle.getSecsSinceReport() + " secs</td></tr>";
	  html += "</table>";
	  html += "</div>";
      vehicle.marker.openInfoWindowHtml(html);
	} 
  }


  // Called by the routeSelector.jsp page when a user clicks on
  // a route. This function either shows or hides the corresponding
  // route.
  function routeSelected(routeTag, checked) {
    if (checked) {
	  debug("routeSelected() - about to show route " + routeTag);
      showRoute(routeTag);
    } else
      hideRoute(routeTag);
  }


