info343/labs/9/solution/busmap.js

var ROUTES_FOR_LOCATION = '//api.onebusaway.org/api/where/routes-for-location.json';
var TRIPS_FOR_ROUTE = '//api.onebusaway.org/api/where/trips-for-route/%ROUTEID%.json';
var TRIP_FOR_VEHICLE = '//api.onebusaway.org/api/where/trip-for-vehicle/%VEHICLEID%.json';
var PROXY = '//localhost/proxy/proxy.php';
var API_KEY = '78911936-facb-4db5-9feb-5788590ded87';
var MAP, BOUNDS, FRAME, SPAN;
var CENTER = new google.maps.LatLng(47.654952, -122.308058);
var FRAME_PADDING = .25;
var ROUTE_IDS, VEHICLE_IDS;
var VEHICLE_QUEUE, VEHICLE_TRIP_DATA;
var RESIZE_TIMEOUT, REFRESH_TRIP_LIST, VEHICLE_UPDATE_CYCLE;
var MARKERS = {};

$.ajaxSetup({
   data: { key: API_KEY },
   error: ajaxError
});

$(document).ready(function() {
   createMap();
   // $.ajax('')
});

function fetchRoutesInBounds() {
   RESIZE_TIMEOUT = null;
   
   // Clear previous interval timers which fetched trip lists / individual trips
   // for previous bounds.
   stopTimer('REFRESH_TRIP_LIST');
   stopTimer('VEHICLE_UPDATE_CYCLE');
   
   // Set new bounds, center, and span.
   BOUNDS = MAP.getBounds();
   CENTER = BOUNDS.getCenter();
   SPAN = BOUNDS.toSpan();
   
   // Calculate bounds for "frame," with extra padding on all sides.
   var sw = BOUNDS.getSouthWest();
   var ne = BOUNDS.getNorthEast();
   var vert_padding = SPAN.lng() * FRAME_PADDING;
   var horz_padding = SPAN.lat() * FRAME_PADDING;
   var new_sw = new google.maps.LatLng(sw.lat() - horz_padding, sw.lng() - vert_padding);
   var new_ne = new google.maps.LatLng(ne.lat() + horz_padding, ne.lng() + vert_padding);
   FRAME = new google.maps.LatLngBounds(new_sw, new_ne);
   
   // Fetch routes for these bounds.
   $.ajax(ROUTES_FOR_LOCATION, {
      data: {
         lat: CENTER.lat(),
         lon: CENTER.lng(),
         latSpan: SPAN.lat(),
         lonSpan: SPAN.lng(),
         maxCount: 200,
      },
      dataType: 'jsonp',
      success: gotRoutesInBounds,
   });
}

// 
function gotRoutesInBounds(data) {
   // Update route list for these bounds.
   ROUTE_IDS = [];
   var routes = data.data.routes;
   for (var i = 0; i < routes.length; i++) {
      ROUTE_IDS.push(routes[i].id);
   }
   
   updateTripsForAllRoutes();
   
   // Fetch new list of trips for each route every 10 minutes.
   REFRESH_TRIP_LIST = setInterval(updateTripsForAllRoutes, 10 * 60 * 1000);
}

function updateTripsForAllRoutes() {
   // pauseLiveVehicleUpdates();
   VEHICLE_IDS = [];
   VEHICLE_QUEUE = [];
   VEHICLE_TRIP_DATA = {};
   throttle(fetchTripsForRoute, ROUTE_IDS.slice(), 50, beginLiveVehicleUpdates);
}

function fetchTripsForRoute(routeid) {
   $.get(TRIPS_FOR_ROUTE.replace(/%ROUTEID%/, routeid), {
      includeStatus: true
   }, function(data) {
      gotTripsForRoute(data, routeid);
   }, 'jsonp');
}

function gotTripsForRoute(data, routeid) {
   if (data.code == 401) {
      setTimeout(fetchTripsForRoute, 50, routeid);
      return;
   }
   
   // Update vehicle/trip list for these routes' trips.
   var trips = {};
   console.log("Recording trip data for route ", routeid, ': ', data);
   $.each(data.data.references.trips, function(i, trip) {
      trips[trip.id] = {
         direction: trip.directionId,
         headsign: trip.tripHeadsign,
         route: trip.routeId
      }
   });
   
   $.each(data.data.list, function(i, trip) {
      var tripid = trip.tripId;
      var vehicleid = trip.status.vehicleId;
      var position = trip.status.position;
      var loc = new google.maps.LatLng(position.lat, position.lon);
      
      // Remember this trip's data if it's within FRAME.
      if (FRAME.contains(loc)) {
         // Add this vehicle (uniquely) to VEHICLE_IDS.
         if (!(vehicleid in VEHICLE_TRIP_DATA)) {
            VEHICLE_TRIP_DATA[vehicleid] = {};
            VEHICLE_IDS.push(vehicleid);
            VEHICLE_QUEUE.push(vehicleid);
         }
         
         // Remember the trip data.
         if (!(tripid in VEHICLE_TRIP_DATA[vehicleid])) {
            trips[tripid].position = loc;
            trips[tripid].orientation = trip.status.orientation;
            trips[tripid].next_stop = trip.status.nextStop;
            VEHICLE_TRIP_DATA[vehicleid][tripid] = trips[tripid];
         }
         
         var tooltip = routeid + " #" + vehicleid + ": " + position.lat + ',' + position.lon;
         // Add or update the marker.
         if (!(vehicleid in MARKERS)) {
            MARKERS[vehicleid] = marker(loc, '', tooltip);
         } else {
            MARKERS[vehicleid].setOptions({
               position: loc,
               title: tooltip
            });
         }
         console.log("Vechicle ", vehicleid, " of route ", routeid, " (trip ", tripid, ") in bounds: " + loc);
      } else {
         if (vehicleid in MARKERS) {
            MARKERS[vehicleid].setMap(null); // Remove marker that's moved out of bounds.
         }
         console.log("Vechicle ", vehicleid, " of route ", routeid, " (trip ", tripid, ") out of bounds: " + loc);
      }
   });
}

function pauseLiveVehicleUpdates() {
   stopTimer('VEHICLE_UPDATE_CYCLE');
}

function beginLiveVehicleUpdates() {
   console.log("beginLiveVehicleUpdates:");
   console.log("VEHICLE_IDS: ", VEHICLE_IDS);
   console.log("VEHICLE_QUEUE: ", VEHICLE_QUEUE);
   console.log("VEHICLE_TRIP_DATA: ", VEHICLE_TRIP_DATA);
   // INDIV_TRIP_INT_TIMER = setInterval(function() {
   //    var vehicleid = VEHICLE_QUEUE.shift();
   //    VEHICLE_QUEUE.push(vehicleid);
      // console.log("Would update vehicle ", vehicleid, " now.");
//       $.get(TRIP_FOR_VEHICLE.replace(/%VEHICLEID%/, vehicleid), function(data) {
//          var pos = data.entry.status.position;
//          var loc = new google.maps.LatLng(pos.lat, pos.lon);
//          if (vehicleid in MARKERS) {
//             MARKERS[vehicleid].setPosition(loc);
//          } else {
//             MARKERS[vehicleid] = marker(loc, '', vehiclid);
//          }
//       });
   // }, 10);
}

// Iteratively passes each member of list to fn, with the specified delay
// between function calls.
function throttle(fn, list, delay, postfn) {
   if (list.length) {
      fn(list.shift());
      setTimeout(throttle, delay, fn, list, delay, postfn);
   } else {
      postfn();
   }
}

function stopTimer(name) {
   clearTimeout(window[name]);
   window[name] = null;
}

function createMap() {
   // an object that we'll use to specify options
   var mapOptions = {
      center: CENTER,
      zoom: 16,
      mapTypeId: google.maps.MapTypeId.ROADMAP
   };
   
   // the DOM object we're going to put the map into
   var mapElement = document.getElementById("busmap");
   
   // create the map inside mapElement with options specified by mapOptions
   MAP = new google.maps.Map(mapElement, mapOptions);
   
   google.maps.event.addListener(MAP, 'bounds_changed', function() {
      // 1 second after the bounds of the map has changed, repaint all stops &
      // trips
      if (RESIZE_TIMEOUT) {
         clearTimeout(RESIZE_TIMEOUT);
      }
      RESIZE_TIMEOUT = setTimeout(fetchRoutesInBounds, 500);
   });
}

function marker(location, info, tooltip) {
   var marker = new google.maps.Marker({
      position: location,
      map: MAP,
      title: tooltip
   });
   
   var infowindow = new google.maps.InfoWindow({
      content: info
   });
   
   google.maps.event.addListener(marker, 'click', function() {
      infowindow.open(MAP, marker);
   });
   return marker;
}

// Provided Ajax error handler function. (Displays useful debugging message.)
function ajaxError(jqxhr, type, error) {
   var msg = "An Ajax error occurred!\n\n";
   if (type == 'error') {
      if (jqxhr.readyState == 0) {
         // Request was never made - security block?
         msg += "Looks like the browser security-blocked the request.";
      } else {
         // Probably an HTTP error.
         msg += 'Error code: ' + jqxhr.status + "\n" + 
                'Error text: ' + error + "\n" + 
                'Full content of response: \n\n' + jqxhr.responseText;
      }
   } else {
      msg += 'Error type: ' + type;
      if (error != "") {
         msg += "\nError text: " + error;
      }
   }
   alert(msg);
}