public_transport_agent.rb 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. require 'date'
  2. require 'cgi'
  3. module Agents
  4. class PublicTransportAgent < Agent
  5. cannot_receive_events!
  6. description <<-MD
  7. The Public Transport Agent monitors if any bus is expected to arrive at a particular stop in 5 minutes or less.
  8. You must specify 5 things for it too work correctly. Your state, city, route, stop and destination. All these things
  9. should be in the language that nextbus understands. For details check out http://www.nextbus.com/predictor/stopSelector.jsp?a=sf-muni and http://www.apihub.com/nextbus/api/nextbus-api/docs/reference.
  10. Specify the following user settings:
  11. - stops (array)
  12. - agency (string)
  13. - alert_window_in_minutes (integer)
  14. This Agent generates Events based on NextBus GPS transit predictions. First, select an agency by visiting http://www.nextbus.com/predictor/agencySelector.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/stopSelector.jsp?a=sf-muni and copy "sf-muni". Put that into this Agent's agency setting.
  15. Next, find the stop tags that you care about. To find the tags for the sf-muni system, for the N route, visit this URL:
  16. http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=N
  17. 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,
  18. agency: "sf-muni"
  19. stops: ["N|5221", "N|5215"]
  20. This Agent will generate predictions by requesting a URL similar to the following:
  21. http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N|5221&stops=N|5215
  22. 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.
  23. alert_window_in_minutes: 5
  24. having the agent's default check period be every minute, and creating an Event in #check whenever a new tripTag (supplied by the predictionsForMultiStops API) shows up within alert_window_in_minutes from the stop. Do not create events for the same tripTag more than once per stop. I'd do this by keeping a list of [stop tag, tripTag, timestamp] tuples in memory and checking to make sure one doesn't already exist before making a new Event. This memory should get cleaned up when timestamp is older than an hour (or something) so that it doesn't fill up all of the Agent's memory.
  25. The NextBusAgent doesn't need to receive Events.
  26. It needs to fetch XML from one URL, store a list of timestamps in memory, and make Events.
  27. MD
  28. default_schedule "every_2m"
  29. event_description <<-MD
  30. Events look like this:
  31. { "routeTitle":"N-Judah",
  32. "stopTag":"5215",
  33. "prediction":
  34. {"epochTime":"1389622846689",
  35. "seconds":"3454","minutes":"57","isDeparture":"false",
  36. "affectedByLayover":"true","dirTag":"N__OB4KJU","vehicle":"1489",
  37. "block":"9709","tripTag":"5840086"
  38. }
  39. }
  40. MD
  41. def session
  42. @session ||= Patron::Session.new
  43. @session.connect_timeout = 10
  44. @session.timeout = 60
  45. @session.headers['Accept-Language'] = 'en-us,en;q=0.5'
  46. @session.headers['User-Agent'] = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.584.0 Safari/534.12"
  47. @session
  48. end
  49. def check_url
  50. stop_query = URI.encode(options["stops"].collect{|a| "&stops=#{a}"}.join)
  51. "http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=#{options["agency"]}#{stop_query}"
  52. end
  53. def stops
  54. options["stops"].collect{|a| a.split("|").last}
  55. end
  56. def check
  57. page = session.get(check_url)
  58. page = Nokogiri::XML page.body
  59. predictions = page.css("//prediction")
  60. predictions.each do |pr|
  61. parent = pr.parent.parent
  62. vals = {routeTitle: parent["routeTitle"], stopTag: parent["stopTag"]}
  63. if pr["minutes"] && pr["minutes"].to_i < options["alert_window_in_minutes"].to_i
  64. vals = vals.merge Hash.from_xml(pr.to_xml)
  65. if not_already_in_memory?(vals)
  66. create_event(:payload => vals)
  67. add_to_memory(vals)
  68. else
  69. end
  70. end
  71. end
  72. end
  73. def add_to_memory(vals)
  74. self.memory["existing_routes"] ||= []
  75. self.memory["existing_routes"] << {stopTag: vals["stopTag"], tripTag: vals["prediction"]["tripTag"], epochTime: vals["prediction"]["epochTime"], currentTime: Time.now}
  76. end
  77. def not_already_in_memory?(vals)
  78. m = self.memory["existing_routes"]
  79. m.select{|h| h[:stopTag] == vals["stopTag"] &&
  80. h[:tripTag] == vals["prediction"]["tripTag"] &&
  81. h[:epochTime] == vals["prediction"]["epochTime"]
  82. }.count == 0
  83. end
  84. def default_options
  85. {
  86. agency: "sf-muni",
  87. stops: ["N|5221", "N|5215"],
  88. alert_window_in_minutes: 5
  89. }
  90. end
  91. def validate_options
  92. errors.add(:base, 'agency is required') unless options['agency'].present?
  93. errors.add(:base, 'alert_window_in_minutes is required') unless options['alert_window_in_minutes'].present?
  94. errors.add(:base, 'stops are required') unless options['stops'].present?
  95. end
  96. def working?
  97. event_created_within?(2) && !recent_error_logs?
  98. end
  99. end
  100. end