-
Notifications
You must be signed in to change notification settings - Fork 942
Added NJT: New Jersey Transit train finder #891
Changes from all commits
74c9dde
d7de0bc
3b65bcb
71f8f33
7659826
e11119c
0ebc964
7d3acbe
c6d6603
11bfd7a
59a5fb5
28abc4d
88338c9
c3041a8
fcd9c42
bd8a11f
9603fbe
7db992d
4591c9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package DDG::Spice::NJT; | ||
|
||
use DDG::Spice; | ||
|
||
primary_example_queries "next train from Metropark to New York Penn"; | ||
secondary_example_queries "train times to Trenton from Secaucus"; | ||
description "Lookup the next NJ Transit train going your way"; | ||
name "NJT"; | ||
source "NJT"; | ||
code_url "https://github.com/duckduckgo/zeroclickinfo-spice/blob/master/lib/DDG/Spice/NJT.pm"; | ||
topics "everyday"; | ||
category "time_sensitive"; | ||
attribution twitter => 'mattr555', | ||
github => ['https://github.com/mattr555/', 'Matt Ramina']; | ||
|
||
spice to => 'http://njt-api.appspot.com/njt/times/$1'; | ||
spice wrap_jsonp_callback => 1; | ||
spice proxy_cache_valid => "418 1d"; | ||
|
||
#load a list of stops so we don't trigger this if we don't get njt stops | ||
#(the triggers are similar to SEPTA's) | ||
my @stops = share('stops.txt')->slurp; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a comment that describes what this slurps in. |
||
|
||
#check if the stop name is in the list of stops | ||
#(using the same matching algorithm as the backend) | ||
sub is_stop { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a comment for this function. |
||
foreach my $stop (@stops){ | ||
return 1 if index(lc $stop, lc $_[0]) > -1; | ||
} | ||
return; | ||
}; | ||
|
||
triggers any => "next train", "train times", "train schedule", "njt", "nj transit", "new jersey transit"; | ||
|
||
handle remainder => sub { | ||
return unless /(?:from |to )?(.+) (to|from) (.+)/; | ||
my $orig = $1; | ||
my $dest = $3; | ||
if (is_stop($orig) and is_stop($dest)){ | ||
#lowercase the stop names found in the query and change the spaces to dashes | ||
my $orig = join "-", map { lc } split /\s+/, $orig; | ||
my $dest = join "-", map { lc } split /\s+/, $dest; | ||
#if the word between the two stop names is "to", then we're going from the first stop to the second | ||
#if it's "from", then we're going from the second stop to the first | ||
return $2 eq 'to' ? ($orig, $dest) : ($dest, $orig); | ||
} | ||
return; | ||
}; | ||
|
||
1; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
.tile--njt { | ||
text-align: center; | ||
width: 13em !important; | ||
} | ||
|
||
.tile--njt .njt__time { | ||
font-size: 2.3em; | ||
color: #333; | ||
font-weight: 300; | ||
} | ||
|
||
.tile--njt .njt__details { | ||
color: #999; | ||
margin-top: 5px; | ||
margin-bottom: 10px; | ||
} | ||
|
||
.tile--njt .njt__status-sep { | ||
height: 12px; | ||
} | ||
|
||
.tile--njt .njt__sep-line { | ||
background-color: #e1e1e1; | ||
height: 1px; | ||
width: 30px; | ||
margin-bottom: 6px; | ||
margin-left: 5px; | ||
margin-right: 5px; | ||
display: inline-block; | ||
} | ||
|
||
.tile--njt .njt__status { | ||
color: #666; | ||
margin-top: 10px; | ||
margin-bottom: 2px; | ||
} | ||
|
||
.tile--njt .njt__badge { | ||
background-color: #e0e0e0; | ||
height: 12px; | ||
border-radius: 6px; | ||
padding: 0 6px; | ||
display: inline-block; | ||
} | ||
|
||
.tile--njt .njt__delayed { | ||
background-color: #eea43e; | ||
} | ||
|
||
.tile--njt .njt__boarding { | ||
background-color: #60ae50; | ||
} | ||
|
||
.tile--njt .njt__cancelled { | ||
background-color: #e34c2d; | ||
} | ||
|
||
.tile--njt .njt__cancelled-tile { | ||
background-color: #fafafa; | ||
border-color: #fafafa; | ||
} | ||
|
||
.tile--njt .njt__cancelled-tile .njt__time, | ||
.tile--njt .njt__cancelled-tile .njt__details, | ||
.tile--njt .njt__cancelled-tile .njt__status { | ||
color: #bbb; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
(function (env){ | ||
"use strict"; | ||
env.ddg_spice_njt = function(api_result) { | ||
if (!api_result || api_result.failed){ | ||
return Spice.failed('njt'); | ||
} | ||
|
||
var now = timeInMins(api_result.now); | ||
|
||
Spice.add({ | ||
id: 'njt', | ||
name: 'NJ Transit', | ||
data: api_result.routes, | ||
meta: { | ||
heading: api_result.origin + " to " + api_result.destination, | ||
sourceUrl: api_result.url, | ||
sourceName: "NJ Transit" | ||
}, | ||
templates: { | ||
group: 'base', | ||
detail: false, | ||
item_detail: false, | ||
options: { | ||
content: Spice.njt.train_item | ||
} | ||
}, | ||
normalize: function(item) { | ||
var classes = { | ||
"Cancelled": "njt__cancelled", | ||
"Delayed": "njt__delayed", | ||
"All Aboard": "njt__boarding", | ||
"Boarding": "njt__boarding", | ||
"Stand By": "njt__boarding" | ||
}; | ||
|
||
if (!item.status) item.status = "On Time"; | ||
return { | ||
departure_time: format_time(actualDepartureTime(item, now)), | ||
arrival_time: format_time(actualArrivalTime(item, now)), | ||
status: actualStatus(item, now), | ||
line: item.line.replace('Line', ''), | ||
cancelled: item.status === "Cancelled", | ||
status_class: delayedMins(item, now) > 0 ? "njt__delayed" : classes[item.status], | ||
url: 'http://dv.njtransit.com/mobile/train_stops.aspx?train=' + item.train | ||
}; | ||
}, | ||
sort_fields: { | ||
time: function(a, b) { | ||
var c = actualDepartureTime(a, now), | ||
d = actualDepartureTime(b, now); | ||
c = (c + 5 < now) ? c + 24*60 : c; //push times from the next day to the end of the list | ||
d = (d + 5 < now) ? d + 24*60 : d; | ||
return c - d; | ||
} | ||
}, | ||
sort_default: 'time', | ||
onShow: function(){ | ||
$('.njt__cancelled').parents('.tile__body').addClass('njt__cancelled-tile'); | ||
} | ||
}); | ||
}; | ||
|
||
var delayed_regex = new RegExp(/In (\d+) Min/); | ||
|
||
function padZeros(n, len) { | ||
var s = n.toString(); | ||
while (s.length < len) { | ||
s = '0' + s; | ||
} | ||
return s; | ||
} | ||
|
||
//takes a time in minutes (integer) and converts it to human-readable (string like 2:05 PM) | ||
function format_time(t) { | ||
var hour = Math.floor(t / 60), | ||
minute = t % 60, | ||
ampm = (hour >= 24) ? 'AM' : (hour >= 12) ? 'PM' : 'AM'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, if a train leaves at 11:30 PM and arrives at 1:00 AM, the API will report those times as |
||
if (hour > 24) { | ||
hour -= 24; | ||
} else if (hour > 12) { | ||
hour -= 12; | ||
} | ||
if (hour === 0){ | ||
hour = 12; | ||
} | ||
return hour + ':' + padZeros(minute, 2) + ' ' + ampm; | ||
} | ||
|
||
//converts a time string (like 14:05 or 2:05 PM) to integer minutes | ||
function timeInMins(t) { | ||
var hour = parseInt(t.split(':')[0]), | ||
minute = parseInt(t.split(':')[1]), | ||
ampm = 0; | ||
if (t.indexOf('PM') > -1 && hour !== 12) { | ||
ampm = 60*12; | ||
} else if (t.indexOf('AM') > -1 && hour === 12) { | ||
ampm = -60*12; | ||
} | ||
return hour*60 + minute + ampm; | ||
} | ||
|
||
//find how many minutes the train is delayed | ||
function delayedMins(item, now) { | ||
var match = delayed_regex.exec(item.status); | ||
if (match && match.length > 1){ | ||
var mins = parseInt(match[1]); | ||
return (now + mins) - timeInMins(item.departure_time); | ||
} | ||
return 0; | ||
} | ||
|
||
//departure time adjusted for delay | ||
function actualDepartureTime(item, now) { | ||
return timeInMins(item.departure_time) + delayedMins(item, now); | ||
} | ||
|
||
//arrival time adjusted for delay | ||
function actualArrivalTime(item, now) { | ||
return timeInMins(item.arrival_time) + delayedMins(item, now); | ||
} | ||
|
||
//status adjusted for delay | ||
function actualStatus(item, now) { | ||
if (delayedMins(item, now) > 0) { //if it's delayed, tell the user the scheduled departure time | ||
return 'Delayed from ' + format_time(timeInMins(item.departure_time)); | ||
} | ||
if (item.status === "Boarding") { | ||
return 'Now Boarding'; | ||
} | ||
var match = delayed_regex.exec(item.status); | ||
if (match && match.length > 1) { //if we know when it departs, tell the user how many minutes | ||
return 'Departs in ' + match[1] + 'm'; | ||
} | ||
return item.status; | ||
} | ||
}(this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should namespace NJT and SEPTA under "DDG::Spice::Transport" or similar?
@jagtalon @russellholt any opinions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@moollaza additionally, I have data for the Port Authority Trans-Hudson (PATH), Long Island Railroad, and Metro-North Railroad. If we make spices for these, their designs will be similar to this one, so we could use the same CSS and Handlebars files. Would namespacing allow us to reuse these files instead of copying and pasting the same code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently @mattr555 that isn't how it works (sharing common CSS/HB) though it's something we have investigated before (I had it working in DuckPAN) and this might be a good usecase to see if we can get it working again in production
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks @moollaza. Might be a good idea from a DRY standpoint.