require 'date' require 'cgi' module Agents class PublicTransportAgent < Agent cannot_receive_events! default_schedule "every_2m" description <<~MD The Public Transport Request Agent generates Events based on NextBus GPS transit predictions. Specify the following user settings: * agency (string) * stops (array) * alert_window_in_minutes (integer) First, select an agency by visiting [http://www.nextbus.com/predictor/adaAgency.jsp](http://www.nextbus.com/predictor/adaAgency.jsp) and finding your transit system. Once you find it, copy the part of the URL after `?a=`. For example, for the San Francisco MUNI system, you would end up on [http://www.nextbus.com/predictor/adaDirection.jsp?a=**sf-muni**](http://www.nextbus.com/predictor/adaDirection.jsp?a=sf-muni) and copy "sf-muni". Put that into this Agent's agency setting. Next, find the stop tags that you care about. Select your destination and lets use the n-judah route. The link should be [http://www.nextbus.com/predictor/adaStop.jsp?a=sf-muni&r=N](http://www.nextbus.com/predictor/adaStop.jsp?a=sf-muni&r=N) Once you find it, copy the part of the URL after `r=`. The link may not work, but we're just trying to get the part after the r=, so even if it gives an error, continue to the next step. To find the tags for the sf-muni system, for the N route, visit this URL: [http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=**N**](http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=N) The tags are listed as tag="1234". Copy that number and add the route before it, separated by a pipe '|' symbol. Once you have one or more tags from that page, add them to this Agent's stop list. E.g, agency: "sf-muni" stops: ["N|5221", "N|5215"] Remember to pick the appropriate stop, which will have different tags for in-bound and out-bound. This Agent will generate predictions by requesting a URL similar to the following: [http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N|5221&stops=N|5215](http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N|5221&stops=N|5215) Finally, set the arrival window that you're interested in. E.g., 5 minutes. Events will be created by the agent anytime a new train or bus comes into that time window. alert_window_in_minutes: 5 MD event_description "Events look like this:\n\n " + Utils.pretty_print({ "routeTitle": "N-Judah", "stopTag": "5215", "prediction": { "epochTime": "1389622846689", "seconds": "3454", "minutes": "57", "isDeparture": "false", "affectedByLayover": "true", "dirTag": "N__OB4KJU", "vehicle": "1489", "block": "9709", "tripTag": "5840086" } }) def check_url query = URI.encode_www_form([ ["command", "predictionsForMultiStops"], ["a", interpolated["agency"]], *interpolated["stops"].map { |a| ["stops", a] } ]) "http://webservices.nextbus.com/service/publicXMLFeed?#{query}" end def stops interpolated["stops"].collect { |a| a.split("|").last } end def check hydra = Typhoeus::Hydra.new request = Typhoeus::Request.new(check_url, followlocation: true) request.on_success do |response| page = Nokogiri::XML response.body predictions = page.css("//prediction") predictions.each do |pr| parent = pr.parent.parent vals = { "routeTitle" => parent["routeTitle"], "stopTag" => parent["stopTag"] } next unless pr["minutes"] && pr["minutes"].to_i < interpolated["alert_window_in_minutes"].to_i vals = vals.merge Hash.from_xml(pr.to_xml) if not_already_in_memory?(vals) create_event(payload: vals) log "creating event..." update_memory(vals) else log "not creating event since already in memory" end end end hydra.queue request hydra.run end def update_memory(vals) add_to_memory(vals) cleanup_old_memory end def cleanup_old_memory self.memory["existing_routes"] ||= [] time = 2.hours.ago self.memory["existing_routes"].reject! { |h| h["currentTime"].to_time <= time } end def add_to_memory(vals) (self.memory["existing_routes"] ||= []) << { "stopTag" => vals["stopTag"], "tripTag" => vals["prediction"]["tripTag"], "epochTime" => vals["prediction"]["epochTime"], "currentTime" => Time.now } end def not_already_in_memory?(vals) m = self.memory["existing_routes"] || [] m.select { |h| h['stopTag'] == vals["stopTag"] && h['tripTag'] == vals["prediction"]["tripTag"] && h['epochTime'] == vals["prediction"]["epochTime"] }.count == 0 end def default_options { agency: "sf-muni", stops: ["N|5221", "N|5215"], alert_window_in_minutes: 5 } end def validate_options errors.add(:base, 'agency is required') unless options['agency'].present? errors.add(:base, 'alert_window_in_minutes is required') unless options['alert_window_in_minutes'].present? errors.add(:base, 'stops are required') unless options['stops'].present? end def working? event_created_within?(2) && !recent_error_logs? end end end