Skip to content
This repository has been archived by the owner on Oct 15, 2022. It is now read-only.

Added NJT: New Jersey Transit train finder #891

Merged
merged 19 commits into from
Aug 19, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions lib/DDG/Spice/NJT.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package DDG::Spice::NJT;
Copy link
Member

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?

Copy link
Collaborator Author

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?

Copy link
Member

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

Copy link
Collaborator Author

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.


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;
Copy link
Member

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

The 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;
67 changes: 67 additions & 0 deletions share/spice/njt/njt.css
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;
}
136 changes: 136 additions & 0 deletions share/spice/njt/njt.js
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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does hour go beyond 24? Why not just (hour >= 12) ? 'PM' : 'AM'?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 23:30 and 25:00 respectively. (if that sounds familiar, I've said it before :P)

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));
Loading