   // Globals -- ugly, I know. But I've yet to find a way to pass 
   // a GMap object to a function using setInterval() or setTimeout().
   
   var map = null;             // The GMap2 object.
   var marker = new Array();
   var first_request = true;
   var online = true;
   var map_control = null;
   var track_lock = true;
   var track_list = new Array();
   var history_timer = null;
   var points_history_array = new Array();
   var h = 0;
   var playing = false;
   

   function load() { 

   }


   function current_time() {

      clearTimeout(tick)  // only need this once

      var currentTime = new Date();
      currentTime     = format_time(currentTime);      
      
      document.getElementById('clock').innerHTML = currentTime;
      
   }


   function format_time(date_object) {
      
      // A horrifying kludge. Javascript is really lacking
      // in the date/time formatting department. In PHP this
      // would be ONE line of code:
      //
      //    $timestamp = date("g:i:s a", $timestamp);
      //
      // Sheesh.
      
      var hours   = date_object.getHours();
      var minutes = date_object.getMinutes();
      var seconds = date_object.getSeconds();
      
      if (minutes < 10) { minutes = "0" + minutes; }
      if (seconds < 10) { seconds = "0" + seconds; }
      
      if (hours > 11) {
         meridien = "pm";
      } else {
         meridien = "am";
      }
      
      if (hours > 12) { hours = hours - 12; } 
      if (hours == 0) { hours = 12; }
      
      formatted_time = hours + ":" + minutes + ":" + seconds + " " + meridien;
      
      return formatted_time;
   }  


   function format_date(date_object) {
      
      return (date_object.getMonth() + 1) + "/" + date_object.getDate() + "/" + date_object.getFullYear();
      
   }
   
   
   function time_zone(date_object) {
      
      time_zone = date_object.toString().substr(34, 3);
      return time_zone;

   }


   function online(server_response) {
      
      //if (server_response.indexOf("Tracking") > -1) { return true; } else { return false; }
      //if (server_response.indexOf("Tracking") > -1) { return true; } else { return false; }
      
   }

   
   function reset_timer(seconds, scale_factor) { // The get_status() timer

      clearTimeout(status_timer);                                                // Clear current timer.
      status_timer = setTimeout("get_status()", 1000 * seconds * scale_factor);  // Set timer for x milliseconds.

   }
   
   
   function get_history() {
   
      history_date = document.dummy_form.history_select.options[document.dummy_form.history_select.selectedIndex].value

      if (history_date == "Today") { 
         
         h = 0;
         document.getElementById('play').className = "hidden";
         clearTimeout(history_timer);
         get_status(); 
         
      } else {
      
         h = 0;
         document.getElementById('play').className = "";
         clearTimeout(status_timer);
         get_points_history("Dummy");
      
      }
   
   }

   function toggle_playback() {
      
      //document.getElementById('play').className = "hidden";
      
      if (document.getElementById('play').value == "Pause") {
          document.getElementById('play').value = "Play";
          playing = false;
          animate_marker();
      } else {
          document.getElementById('play').value = "Pause";
          playing = true;
          animate_marker();
      }
   }

   function get_status() { // Request some sort of status info from the server.

      //clearTimeout(status_timer);                                               // Clear current timer.
      //status_timer = setTimeout("get_status()", 10000);  // Set timer for x milliseconds.

      reset_timer(5, 1);  // Reset timer for 5 seconds (active tracking mode).
      
      new Ajax.Request('code/getstatus.php', { // Calls a PHP script for some sort of status info.
                                              // Ajax.Request is a method of the Prototype.js framework.
         method: 'post',  // Could also use 'get' here.
         
         parameters: phone_names,  // Request status of one or more phones.
         onSuccess:  function(request) { 

            var response  = request.responseText; // The output of getstatus.php.
            //window.online = online(response);
            //dummy = online(response);
            var status    = $('status');          // Assumes you have a div or span id="status". A Prototype.js shortcut.

            //status.update(response);        // Replace the contents of 'status' with latest server response.
                                            // More Prototype.js at work -- a shortcut around innerHTML.


            switch(true) { // Bring all decisions on update time under one switch() statement.

               case (response.indexOf('Tracking') > -1):  // Active tracking mode.
   
                     reset_timer(5, 1);                   // Reset timer for 5 seconds.
                     window.online = true;
                     break;
               
               case (response.indexOf('minutes') > -1):  // Measuring offline time in minutes.
   
                     reset_timer(60, 1);                 // Reset time for 1 minute (60 secs * 1).
                     window.online = false;
                     break;
               
               default: reset_timer(60, 10);             // Reset for 10 minutes (60 secs * 10). "Deep offline" mode.
                        window.online = false;
               
            }
            
            //if (window.online || window.first_request) { // Request GPS data ONLY if we're tracking or initializing map. 
               
               //clearTimeout(status_timer);                        // Clear current timer.
               //status_timer = setTimeout("get_status()", 10000);  // Reset timer for 10 seconds.

               get_point(response);  // Now the real work begins: Get our GPS data.

            //}

         },

         onFailure: function() { 
            
            $('status').update('Could not retrieve status information. Please wait...'); 
            // Status timer is still running, so we'll try again every ten seconds.
         
         }

      });
      
   }


   function get_point(status_block) {
      
      new Ajax.Request('code/getlastpoint.php', { // getlastpoint.php returns the GPS data we want.
         
         method: 'post',
         parameters: phone_names,  // Request data for one or more phones.
         onSuccess:  function(request) { 

            var points = request.responseText;  // Output of getlastpoint.php -- a tab-delimited string
                                                // containing all GPS data for the current point(s).

            points_array = points.split('\n');  // Produces an array of points plus an empty final element.
            var junk = points_array.pop();      // Remove empty element.

            if (window.first_request) { load_map(points_array); }

            for (i=0; i < points_array.length; i++) {
            
               var point = extract_point(points_array, i);
            
               move_marker(point,i);
               status_block = set_speed_direction(points_array, i, status_block);
                     
             } // for loop

             $('status').update(status_block);
             reframe(points_array);
         },
         
         onFailure:  function() { /* $('status').update('Could not retrieve GPS data.'); */ }
         
      });
              
   }


   function get_points_history(status_block) {
      
      new Ajax.Request('code/getallpoints.php', { // getlastpoint.php returns the GPS data we want.
         
         method: 'post',
         parameters: phone_names + "&date=" + history_date, // Request data for one or more phones.
         onSuccess:  function(request) { 

            var points_history = request.responseText;  // Output of getlastpoint.php -- a tab-delimited string
                                                        // containing all GPS data for the current point(s).

            points_history_array = points_history.split('\n');  // Produces an array of points plus an empty final element.
            var junk = points_history_array.pop();              // Remove empty element.

            document.getElementById('play').value = "Play";
            playing = false;
            
            history_timer = setTimeout("animate_marker();", 1); // Move to first point

         },
         
         onFailure:  function() { $('status').update('Could not retrieve GPS data.'); }
         
      });
              
   }


   function set_speed_direction(points_array, index, status_block) {
   
      var gps_array = points_array[index].split('\t');    // Create an array of GPS data for this point.

      var speed = Math.round(gps_array[4] * 0.621371192); // kph to mph
      
      var heading = gps_array[3];
      var compass = "";
     
      var gps_timestamp = eval(gps_array[9]);        // Convert string to integer.
      var date_object   = new Date(gps_timestamp);   // Format_time() needs a js Date object.
      var gps_time      = format_time(date_object);
   // var time_zone     = time_zone(date_object);
      var gps_date      = format_date(date_object);
      
      var status_time   = new Date();
      status_time       = format_time(status_time);
   
      switch(true) {
      
         case (heading >=      0 && heading <=  11.25): compass = "N";  break;
         
         case (heading >   11.25 && heading <   33.75): compass = "NNE";  break;  //  ~22.5
         case (heading >=  33.75 && heading <=  56.25): compass = "NE";   break;  //  ~45.0
         case (heading >   56.25 && heading <   78.75): compass = "ENE";  break;  //  ~67.5
      
         case (heading >=  78.75 && heading <= 101.25): compass = "E";    break;  //  ~90.0
         
         case (heading >  101.25 && heading <  123.75): compass = "ESE";  break;  // ~112.5
         case (heading >= 123.75 && heading <= 146.25): compass = "SE";   break;  // ~135.0
         case (heading >  146.25 && heading <  168.75): compass = "SSE";  break;  // ~157.5
      
         case (heading >= 168.75 && heading <= 191.25): compass = "S";    break;  // ~180.0
      
         case (heading >  191.25 && heading <  213.75): compass = "SSW";  break;  // ~202.5
         case (heading >= 213.75 && heading <= 236.25): compass = "SW";   break;  // ~225.0
         case (heading >  236.25 && heading <  258.75): compass = "WSW";  break;  // ~247.5
      
         case (heading >= 258.75 && heading <= 281.25): compass = "W";    break;  // ~270.0
      
         case (heading >  281.25 && heading <  303.75): compass = "WNW";  break;  // ~292.5
         case (heading >= 303.75 && heading <= 326.25): compass = "NW";   break;  // ~315.0
         case (heading >  326.25 && heading <  348.75): compass = "NNW";  break;  // ~337.5
   
         case (heading >= 348.75):                      compass = "N";  break;                
         
         default: compass = ""; // -1 means we're not moving.
      }
      
      if (window.online) { speed_o_meter = speed.toString() + " mph"; } else { speed_o_meter = ""; compass = ""; }
      if (speed == -1) { speed_o_meter = " (No GPS signal)"; }
      
      this_speed   = "speed"    + index;
      this_compass = "heading"  + index;
      this_gps     = "gps_time" + index; 
      
      status_block = status_block.replace(this_speed, speed_o_meter);
      status_block = status_block.replace(this_compass, compass);
      status_block = status_block.replace(this_gps, gps_time);
      //status_block = status_block.replace(this_gps, gps_time + " (last update " + status_time +")");
      
      if (track_list[index] == false) {
      
         status_block = status_block.replace((index +' label lit'), 'label unlit');
      
      }
      
      return status_block;
   
   }


   function create_marker(point, index) {
   
      // Create a base icon for all of our markers that specifies the
      // shadow, icon dimensions, etc.
      var baseIcon = new GIcon();
      baseIcon.shadow = "http://www.google.com/mapfiles/shadow50.png";
      baseIcon.iconSize = new GSize(20, 34);
      baseIcon.shadowSize = new GSize(37, 34);
      baseIcon.iconAnchor = new GPoint(9, 34);
      baseIcon.infoWindowAnchor = new GPoint(9, 2);
      baseIcon.infoShadowAnchor = new GPoint(18, 25);
      
      // Label markers A-Z.   
      var letter = String.fromCharCode("A".charCodeAt(0) + index);
      var icon = new GIcon(baseIcon);
      icon.image = "http://www.google.com/mapfiles/marker" + letter + ".png";
      var new_marker = new GMarker(point, icon);
      
      return new_marker;
   }


   function extract_point(points_array, index) {

      var gps_array = points_array[index].split('\t');  // Create an array of GPS data for this point.
      
      // My GPS data is ordered according to mologogo.wikispaces.com/Alternate+URL+feature,
      // which is why longitude comes first. ;)
      
      var lng = gps_array[0];  // Longitude
      var lat = gps_array[1];  // Latitude
      
      //if (index == 2) { lng = lng - .02; }
      
      point = new GLatLng(lat, lng);  // Awright! We got ourselves put on the map.
      
      return point;

   }


   function load_map(points_array) { // Initialize and display map with current marker position.
   
      first_point = extract_point(points_array, 0);  // A quickie shortcut to load position data.
      var routeKML = new GGeoXml("http://gps.mcmn.org/CRFK2007.kml");  // Route markers.

      if (GBrowserIsCompatible()) {

         var map_types = new Array;
         map_types[0] = "G_NORMAL_MAP";
         map_types[1] = "G_SATELLITE_MAP";

         window.map = new GMap2(document.getElementById("map"),{mapTypes:[G_NORMAL_MAP, G_SATELLITE_MAP]});

         window.map.setCenter(first_point, 11);       // default zoom leval of map
         //window.map.disableDragging();              // Automatic tracking mode -- no user interaction.
         window.map.enableDoubleClickZoom();          // Only when dragging is enabled.
         window.map.enableContinuousZoom();           // Works in IE.
         window.map.addControl(new GScaleControl());
         window.map.addControl(new GMapTypeControl());
         //window.map_control = new GLargeMapControl();  // Restore Google's pan/zoom controls.
         //window.map.addControl(window.map_control);
         window.map.addOverlay(routeKML);             // Overlay route markers.
         //window.map.addOverlay(new GTrafficOverlay);
         
         window.first_request = false;
         
         place_markers(points_array);  // Create and place a marker for each phone, 
                                       // then reframe map to fit them all on screen.

      }
   
   }


   function place_markers(points_array) { // Set map size and place markers.
      
      var bounds = new GLatLngBounds();   // The rectangle defined by marker positions.
      
      for (i=0; i < points_array.length; i++) { // Points array is current data for all phones.
         
         point = extract_point(points_array, i);     // Position data for phone[i].
         bounds.extend(point);                       // Add it to GLatLngBounds object.
         window.marker[i] = create_marker(point, i); // Create marker[i].
         window.map.addOverlay(window.marker[i]);    // Place it on the map.
         track_list[i] = true;
      }
      
      window.map.setZoom(window.map.getBoundsZoomLevel(bounds)-1); // Set zoom level to fit all phones on screen.
      window.map.setCenter(bounds.getCenter());                    // Center assembled phones on map.
   }


   function move_marker(new_point, index) { window.marker[index].setPoint(new_point); }  // Move the marker. Index here is phone, not point.

   
   function animate_marker() {
      
      clearTimeout(history_timer);
      
      //$('status').update(h + ": " + points_history_array[h]);
      $('status').update(window.map.getZoom());
      
      limit = points_history_array.length - 1
      
      if (h < limit) {
         
         var point = extract_point(points_history_array, h);
         move_marker(point,0);
         reframe_history(points_history_array, limit);
         //status_block = set_speed_direction(points_history_array, h, status_block);
         h++;
         
         if (playing) { history_timer = setTimeout("animate_marker()", 1000); }
            
      } else { 
         
         h = 0;
         clearTimeout(history_timer);
         
      } // if h

   }
   
   function reframe_history(points_history_array, limit) {
      
      this_point = extract_point(points_history_array, h);
      
      if ((h > 0) && (h < limit)) {
         last_point = extract_point(points_history_array, h - 1);
         next_point = extract_point(points_history_array, h + 1);
         
         var bounds = new GLatLngBounds();
         
         bounds.extend(last_point);
         bounds.extend(this_point);
         bounds.extend(next_point);
         
         //window.map.panTo(this_point);                          // Center oon current point.
         new_center = bounds.getCenter();
         new_zoom = window.map.getBoundsZoomLevel(bounds);  // Zoom level that includes last and next.
         
//         if ((new_zoom < window.map.getZoom()) || ((Math.abs(new_zoom - window.map.getZoom()) > 1) && (new_zoom < 17))) { 
         if ((new_zoom < window.map.getZoom()) || ((new_zoom - window.map.getZoom() > 1) && (new_zoom < 17))) { 
            window.map.setZoom(new_zoom);  // Zoom in/out if current zoom isn't ideal.
         }
         window.map.panTo(new_center);
      } else { // if h > 0
         window.map.panTo(this_point);
      }
   }

   
   function reframe(points_array) { 
      
      if (track_lock) { // Don't reframe in Manual mode.
         
         var bounds = new GLatLngBounds();  // Get current boundaries, etc.
         
         phone_count = points_array.length;
         
         for (i=0; i < phone_count; i++) {
            
            point = extract_point(points_array, i);
            
            if (track_list[i]) { bounds.extend(point); }
            
            if (i == phone_count -1) { // Wait until we have last phone's data. Zero-based array.
               
               new_center = bounds.getCenter()                          // Center of current phone array.
               new_zoom   = window.map.getBoundsZoomLevel(bounds) - 1;  // Zoom level required to fit all.
               
//               if (new_zoom != window.map.getZoom()) { 
//                 window.map.setZoom(new_zoom);  // Zoom in/out if current zoom isn't ideal.
//               } 
               if ((new_zoom > window.map.getZoom()) || ((Math.abs(new_zoom - window.map.getZoom()) > 1) && (new_zoom < 17))) { 
                  window.map.setZoom(new_zoom);  // Zoom in/out if current zoom isn't ideal.
               }
               
               // Kludge to work around disappearing tile issue. Still broken in Safari.
               //window.map.enableDragging();
               window.map.panTo(new_center);  // Pan map to new_center.
               //window.map.disableDragging();
               //window.map.panTo(new_center);

             } // if i == phone_count - 1
         }     // for loop

//         This stuff SHOULD be outside the for loop, but it doesn't work.         
//         window.map.setZoom(window.map.getBoundsZoomLevel(bounds)-1);
//         window.map.setCenter(bounds.getCenter());
//         window.map.panTo(bounds.getCenter());
         
      }
      
   }

   
   function toggle_lock(toggle) { // Toggle controls for (un)locking map. Default is Automatic. 
   
      switch (true) {

         //case (toggle == "on" && track_lock == false): // Go to Automatic mode.
         case (track_lock == false): // Go to Automatic mode.
         
            document.getElementById('track-on').className = "label lit";    // Flip state of toggle buttons.
            document.getElementById('track-off').className = "label unlit"; 
            //window.map.disableDragging();                                   // Lock map.
            window.map.removeControl(window.map_control);  // Remove Google's pan/zoom controls.
            track_lock = true;
            get_status();       // Trigger an immediate update.
            break;
               
         //case (toggle == "off" && track_lock == true): // Go to Manual mode.
         case (track_lock == true): // Go to Manual mode.
   
            document.getElementById('track-off').className = "label lit";  // Flip state of toggle buttons.
            document.getElementById('track-on').className = "label unlit"; 
            //window.map.enableDragging();                                   // Unlock map.
            window.map_control = new GLargeMapControl();  // Restore Google's pan/zoom controls.
            window.map.addControl(window.map_control);
            track_lock = false;
            break;
         
      }
   
   }

   
   function toggle_phone(element) {
   
      index = element.id.charCodeAt(0) - 65;
   
      if (element.className.indexOf('unlit') > -1) { 
         element.className = "label lit";
         track_list[index] = true;
      } else {
         element.className = "label unlit";
         track_list[index] = false;
      }
      get_status();
   }
   
   
/*
   function toggle_sim_phone() {
      if (phone_names.indexOf("Sim") > 0) {
         phone_names = phone_names.replace("&phone3=Sim", "");
         window.marker[2].hide()
      } else {
         phone_names = phone_names + "&phone3=Sim";
         window.marker[2].show()
      }
      get_status();
   }
*/   

   function unload() { // cleanup time
   
      // Does this avoid memeory leaks? I have no idea.
   
      GUnload();
      clearInterval(tock);
      clearTimeout(status_timer);
      tick = null;
      tock = null;
      status_timer = null;
      window.map = null;
      window.marker = null;

   }


   function resize_map() {
   
      var primary_height = document.getElementById("primary").offsetHeight;
   
      var legend_height = document.getElementById("legend").offsetHeight + 24;
      var footer_height = document.getElementById("footer").offsetHeight + 24;
      
      map_height = primary_height - 79 - 89;
      //map_height = primary_height - legend_height - footer_height;
      
      //document.write("<div id=\"map\" style=\"height:" + map_height + "px;\"></div>");
      
      //document.getElementById('map').style.top = legend_height;
      //document.getElementById('map').style.bottom = footer_height;
      document.getElementById('map').style.height = map_height;
      get_status();
   
   }
      

   function addEvent(obj, evType, fn) { // By Scott Andrew
      if (obj.addEventListener) {
         obj.addEventListener(evType, fn, true);
         return true;
      }  else if (obj.attachEvent) {
         var r = obj.attachEvent("on"+evType, fn);
         return r;
      }  else {
         return false;
      }
   }

   addEvent(window, 'resize', resize_map);
   addEvent(window, 'load', load);         // Use event handlers instead of <body onload="" onunload="">.
   addEvent(window, 'unload', unload);     // Supposed to be a good thing, I guess:
                                           // www.digital-web.com/articles/separating_behavior_and_structure_2/

   var tick = setTimeout("current_time()", 1000);         // Start the clock. Not sure why calling current_time() directly doesn't work.
   var tock = setInterval("current_time()", 1000);     // Keep it running.

   var status_timer = setTimeout("get_status();", 1);  // Let it roll. :)
