Skip to content

Instantly share code, notes, and snippets.

@cmdoptesc
Last active March 12, 2016 09:52
Show Gist options
  • Save cmdoptesc/6224455 to your computer and use it in GitHub Desktop.
Save cmdoptesc/6224455 to your computer and use it in GitHub Desktop.
muniNow Quick Demo

muniNow Demo

muniNow aims to be a nicer NextMuni by giving users the ability to see when the soonest Muni bus is arriving at a glance.

Live demo on Bl.ocks.org

Powered by D3, built at Catalyst/Hack Reactor as a personal project. And many thanks to Mike and Adnan.

al lin, aug. 2013

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>muniNow bl.ocks demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.0-rc1/css/bootstrap.min.css" media="screen">
<script src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.1/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.0.0/handlebars.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.2.2/d3.v3.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.0-rc1/js/bootstrap.min.js"></script>
<style type='text/css'>
#svg-muninow {
border: 1px solid rgba(153,153,153, 0.5);
width: 100%;
min-height: 500px;
}
.center-time {
font-family: helvetica, sans-serif;
font-weight: bold;
fill: rgba(112,158,192, 1);
stroke: rgb(102,102,102, 0.75);
stroke-width: 2px;
}
.arc-path, .dest-path {
stroke: rgba(153, 153, 153, 0.10);
stroke-width: 2px;
cursor: pointer;
}
.highlighted {
fill: #fc7d47;
}
.highlighted2 {
fill: #deae8a;
}
.transition {
fill: #deae8a;
}
</style>
</head>
<body>
<div class="container">
<h1>muniNow</h1>
<div class="row">
<form id="RouteForm" class="form-horizontal">
<p>
<select id="RouteSelector" class="form-control input-sm">
<option value="-1">Please select your Muni line..</option>
</select>
</p>
<p>
<select id="DirectionSelector" class="form-control input-sm"></select>
</p>
<p>
<label for="StopSelector">From:</label><select id="StopSelector" class="form-control input-sm"></select>
</p>
<p>
<label for="DestSelector">To:</label><select id="DestSelector" class="form-control input-sm"></select>
</p>
<button type="button" class="btn btn-info btn-mini" id="AddRouteButton">Add Route</button>
</form>
</div>
<div id="AdditionalInfo" class="row"><div id="UrlDiv"></div></div>
<div id="ChartArea" style="max-height: 600"></div>
</div>
<script src="muninow-api.js"></script>
<script src="muninow-forms.js"></script>
<script src="muninow-d3.js"></script>
<script src="muninow-init.js"></script> </body>
</html>
var nextBusser = function(agency) {
var nb = {
routesInfo: {},
// basic nextbus query, check the Nextbus PDF for commands & options
getNextbus: function(query, callback) {
var apiUrl = 'http://webservices.nextbus.com/service/publicXMLFeed';
$.get(apiUrl, query, function(xml){
callback.call(this, xml);
});
},
// needed a custom query function since the query URL reuses the "stops" key
// stopsArray is an array of stop objects in the format: {r: route tag, s: stop tag}
getMultiStops: function(stopsArray, destsArray, callback) {
var baseQuery = 'http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni';
var args = Array.prototype.slice.call(arguments);
if(_.isArray(args[1]) && _.isFunction(args[2])) {
destsArray = args[1];
callback = args[2];
} else if(_.isFunction(args[1])) {
destsArray = undefined;
callback = args[1];
}
var query = nb._buildQuery(baseQuery, stopsArray, destsArray);
$.get(query, function(xml){
callback.call(this, xml);
});
},
// combines all the stops to form a big query -- does not check for duplicate stops
// stopsArray & destsArray are array of stop objects in the format: {r: route tag, s: stop tag}
_buildQuery: function(baseQuery, stopsArray, destsArray) {
for(var i=0; i<stopsArray.length; i++) {
baseQuery += '&stops='+ stopsArray[i].r +'|'+ stopsArray[i].s;
if(destsArray && typeof destsArray[i] !== 'undefined') { baseQuery += '&stops='+ destsArray[i].r +'|'+ destsArray[i].s; }
}
return baseQuery;
},
parseXMLroutes: function(xml, callback) {
var routes = [];
var route = {};
$(xml).find("body > route").each(function(indx, rt){
$rt = $(rt);
route = {
routeTag: $rt.attr('tag'),
routeTitle: $rt.attr('title')
};
routes.push(route);
});
return callback ? callback(routes) : routes;
},
// converting raw xml: http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=J
// into two objects, then passing them to a callback or as an object
parseXMLstops: function(xml, callback) {
var directions = {};
var $dir, dirTag;
$(xml).find('direction').each(function(indx, dir){
var $dir = $(dir);
dirTag = $dir.attr('tag');
directions[dirTag] = {
title : $dir.attr('title'),
name : $dir.attr('name'),
dirTag: dirTag,
stops : []
};
$dir.find('stop').each(function(indx, stop) {
directions[dirTag].stops.push($(stop).attr('tag'));
});
});
var $route = $(xml).find("body > route");
var stopsInfo = {
routeTag: $route.attr('tag'),
title: $route.attr('title'),
color: $route.attr('color'),
oppositeColor: $route.attr('oppositeColor')
};
var $stop, stopTag;
$(xml).find("body route > stop").each(function(indx, stop) {
$stop = $(stop);
stopTag = $stop.attr('tag');
stopsInfo[stopTag] = {
title : $stop.attr('title'),
lat : $stop.attr('lat'),
lon : $stop.attr('lon'),
stopId : $stop.attr('stopId')
};
});
return callback ? callback(stopsInfo, directions) : {stopsInfo:stopsInfo, directions:directions};
},
// parses prediction XML for single stops:
// http://webservices.nextbus.com/service/publicXMLFeed?command=predictions&a=sf-muni&r=5&s=5684
parseXMLtimes: function(xml, callback) {
var predictions = [];
var routeTag = $(xml).find('predictions').attr('routeTag');
var $pr, prediction;
$(xml).find('prediction').each(function(indx, pr) {
$pr = $(pr);
prediction = {
routeTag: routeTag,
seconds: $pr.attr('seconds'),
vehicle: $pr.attr('vehicle'),
dirTag: $pr.attr('dirTag')
};
predictions.push(prediction);
});
return callback ? callback(predictions) : predictions;
},
// parses predictionsForMultiStops:
// http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=5|5684&stops=38|5684&stops=38|5689
// replaced by hashXMLmulti & combinePredictions to make total trip predictions.
// still works fine for parsing multiple stops, but won't discern between stop and destination stop
parseXMLmulti: function(xml, callback) {
var predictions = [];
var $stop, $pr;
var routeTag, stopTag, prediction;
$(xml).find('predictions').each(function(indx, prs) {
$stop = $(prs);
routeTag = $stop.attr('routeTag');
stopTag = $stop.attr('stopTag');
$stop.find('prediction').each(function(indx, pr) {
$pr = $(pr);
prediction = {
routeTag: routeTag,
stopTag: stopTag,
seconds: $pr.attr('seconds'),
vehicle: $pr.attr('vehicle')
};
predictions.push(prediction);
});
});
return callback ? callback(predictions) : predictions;
},
// groups the stop and destination times by vehicle number in orer to make predictions based
// on the difference between two stops. late at night, nextbus might return the vehicle's
// second departure time after its roundtrip run (e.g. arriving here, going to the end,
// arriving here again), so seconds are stored as arrays.
// ** needs to work with combinePredictions to return a similar result as parseXMLmulti **
/*
Hash Hierarchy = {
routeTag: {
vehicle: {
stop: [seconds, seconds]
}
}
}
*/
hashXMLmulti: function(xml, callback) {
var predictions = {};
var routeTag, stopTag, vehicle;
var $stop, $pr;
$(xml).find('predictions').each(function(indx, prs){
$stop = $(prs);
routeTag = $stop.attr('routeTag');
stopTag = $stop.attr('stopTag');
if(typeof predictions[routeTag] === 'undefined') { predictions[routeTag] = {}; }
$stop.find('prediction').each(function(indx, pr){
$pr = $(pr);
vehicle = $pr.attr('vehicle');
if(typeof predictions[routeTag][vehicle] === 'undefined') { predictions[routeTag][vehicle] = {}; }
if(predictions[routeTag][vehicle][stopTag]) { predictions[routeTag][vehicle][stopTag].push($pr.attr('seconds')); }
if(typeof predictions[routeTag][vehicle][stopTag] === 'undefined') { predictions[routeTag][vehicle][stopTag] = [$pr.attr('seconds')]; }
});
});
return callback ? callback(predictions) : predictions;
},
// combines predictions to estimate the time to a destination by taking the difference
// between two stops for a particular vehicle. in conjuction with hashXMLmulti,
// will return a result similar to parseXMLmulti, with added 'to destination' times
// relies on a predictions hash created by hashXMLmulti. stopQueries and destQueries
// are used to determine which stops are stops vs. destinations.
// there are many loops here; at 4am, they seem necessary..
// most loops will be very short: stopQueries is based on the number of routes a user
// wants to track (probably a max of 5) and stopTimes and destTimes will most likely
// be of length 1. edge cases are the all-nighter routes like the 5-Fulton at 3am,
// then the array is of length 2.
combinePredictions: function(prsHash, stopQueries, destQueries) {
var predictions = [];
var query, stopTimes, destTimes;
var pre;
var i, j, k, vehicle;
// sub-routine used to make prediction objects to be pushed into the predictions array
function mkPrediction(routeTag, vehicle, timeInfo) {
var prediction = {
routeTag: routeTag,
vehicle: vehicle
};
_.extend(prediction, timeInfo);
return prediction;
}
for(i=0; i<stopQueries.length; i++) {
query = stopQueries[i];
for(vehicle in prsHash[query.r]) {
if(prsHash[query.r][vehicle][query.s]) {
stopTimes = prsHash[query.r][vehicle][query.s];
for(j=0; j<stopTimes.length; j++) {
pre = mkPrediction(query.r, vehicle, {
stopTag: query.s,
seconds: parseInt(stopTimes[j], 10)
});
if(destQueries[i] && prsHash[query.r][vehicle][destQueries[i].s]) {
destTimes = prsHash[query.r][vehicle][destQueries[i].s];
for(k=j; k<destTimes.length; k++) {
if(parseInt(destTimes[k], 10) > parseInt(pre.seconds, 10)) {
pre.destTag = destQueries[i].s;
pre.secondsTotal = parseInt(destTimes[k], 10);
pre.destSeconds = pre.secondsTotal - pre.seconds;
break;
}
}
}
predictions.push(pre);
}
} else if(destQueries[i] && prsHash[query.r][vehicle][destQueries[i].s]) {
destTimes = prsHash[query.r][vehicle][destQueries[i].s];
for(k=0; k<destTimes.length; k++) {
var destInfo = {
destTag: destQueries[i].s,
destSeconds: parseInt(destTimes[k], 10),
seconds: -1
};
pre = mkPrediction(destQueries[i].r, vehicle, destInfo);
predictions.push(pre);
}
}
}
}
return predictions;
},
init: function(agency, callback) {
var query = {
command:'routeList',
a: agency
};
nb.getNextbus(query, function(xml){
var routes = nb.parseXMLroutes(xml);
_(routes).each(function(rt){
nb.routesInfo[rt.routeTag] = {};
nb.routesInfo[rt.routeTag].title = rt.routeTitle;
});
return callback ? callback(routes) : routes;
});
}
};
if(typeof agency === 'string') {
nb.init(agency, function(){
return nb;
});
}
return nb;
};
// returns a function that stores stops & routes to be used for a multiple stop query
var queryStorageMaker = function() {
var memo = {};
return function(stop, route, del) {
if( stop && route ) {
var query = {
r: route,
s: stop
};
if(checkQuery(query)) {
if(del === 'true') { delete memo[route]; }
else {
memo[route] = query;
}
}
}
var queries = [];
for(var key in memo) {
queries.push(memo[key]);
}
return queries;
};
};
// converts the queries array to a serialised parameter for bookmarking
var serialiseQueries = function(queries) {
var params = {};
for(var i=0; i<queries.length; i++) {
params[i] = queries[i];
}
return $.param(params);
};
// deserialised the parameter and checks them, returning an array of sanitised queries
var deserialiseParams = function(params) {
var deserialised = $.deparam(params);
var i = 0, queries = [];
while(deserialised[i]) {
if(checkQuery(deserialised[i])) { queries.push(deserialised[i]); }
i++;
}
return queries;
};
// checks to see if the query has a valid bus line and a stop number of four digits
// four digits does *NOT* mean the stop number is necessarily valid
var checkQuery = function(pre) {
var regex = /\b\d{4}\b/;
return (typeof nb.routesInfo[pre.r] === 'undefined' || !regex.test(pre.s)) ? false : true;
};
var makeChart = function(stopTag, routeTag) {
var chart = {};
updateChart(stopTag, routeTag, chart);
return chart;
};
var makeChartView = function(chart) {
$chartArea = $("#ChartArea");
$chartArea.html('<h3 class="route-title"></h3>');
$chartArea.append('<div class="chart-div"></div>');
chart.div = $chartArea.children().last();
chart.d3vis = d3.select(chart.div[0]).append("svg:svg").attr('id', 'svg-muninow');
updateChartView(chart);
};
var _queriesToStop = queryStorageMaker();
var _queriesToDest = queryStorageMaker();
var updateChart = function(stopTag, routeTag, chart) {
var destTag = $("#DestSelector").val();
chart.stopQueries = _queriesToStop(stopTag, routeTag);
chart.destQueries = _queriesToDest(destTag, routeTag);
return chart;
};
var updateFormView = function(stopTag, routeTag, callback) {
var $routeSel = $("#RouteSelector");
$routeSel.val(routeTag);
var dirTag;
stopTag += ''; // stopTag should be a string, but if it isn't, convert it
_(nb.routesInfo[routeTag].directions).each(function(dir){
for(var i=0; i<dir.stops.length; i++) {
if(dir.stops[i] === stopTag) {
dirTag = dir.dirTag;
}
}
});
displayDirections(nb.routesInfo[routeTag].stopsInfo, nb.routesInfo[routeTag].directions, dirTag);
var $dirSel = $("#DirectionSelector");
$dirSel.val(dirTag);
$dirSel.change();
var $stopSel = $("#StopSelector");
$stopSel.val(stopTag);
$stopSel.change();
if(callback) { callback(stopTag, routeTag); }
};
// helper function for views
var combineTitles = function(queries) {
var title = '';
for(var i=0; i<queries.length; i++) {
if(i > 0) {
title += ' & ';
}
title += nb.routesInfo[queries[i].r].title;
}
return title;
};
// helper function for views
var updateTitle = function(title) {
return $("#ChartArea .route-title").text(title);
};
// helper function for views
var getSixSoonest = function(times) {
var newtimes = _.sortBy(times, function(time){
return parseInt(time.seconds, 10);
});
if(newtimes.length > 5) {
newtimes = newtimes.splice(0,5);
}
return newtimes;
};
// helper function for views
var sortAndRender = function(predictions, vis) {
var times = getSixSoonest(predictions);
d3methods.render(times, vis);
};
var updateChartView = function(chart) {
if(chart.timer) { window.clearInterval(chart.timer); }
$(chart.d3vis[0]).empty();
(chart.d3vis).append("svg:g").attr("class", 'center-group');
var stopQueries = chart.stopQueries;
destQueries = chart.destQueries;
var bookmarkableUrl = window.location.href.split('?')[0] + '?' + serialiseQueries(chart.stopQueries);
var info = Handlebars.compile('<a class="bookmarkable" href="{{url}}">Bookmarkable URL</a> - Additional Muni lines may be tracked by re-using the form above.');
updateTitle(combineTitles(stopQueries));
$("#AdditionalInfo").html(info({ url: bookmarkableUrl }));
var combined;
nb.getMultiStops(stopQueries, destQueries, function(xml){
combined = nb.combinePredictions(nb.hashXMLmulti(xml), stopQueries, destQueries);
sortAndRender(combined, chart.d3vis);
setTimeout(function(){
d3methods.ripple(chart.d3vis);
}, 500);
});
chart.timer = setInterval(function(){
nb.getMultiStops(chart.stopQueries, chart.destQueries, function(xml){
combined = nb.combinePredictions(nb.hashXMLmulti(xml), stopQueries, destQueries);
sortAndRender(combined, chart.d3vis);
});
}, 14500);
};
var d3methods = {
_highlightColor: '#fc7d47',
_transitionColor: '#deae8a',
_highlight2Color: 'rgb(250,174,135)',
_toMin: function(sec) {
var fuzzy = 5*( 5 - sec/60 );
return ( sec%60 > fuzzy ) ? Math.ceil(sec/60) : Math.floor(sec/60);
},
_secToRadians: function(sec) {
return Math.round(parseFloat((sec/60)*6 * (Math.PI/180)) * 10000)/10000;
},
_arcScaleMaker: function(max) {
return d3.scale.linear()
.domain([0, max])
.range(["rgb(185,218,197)", "rgb(233,237,220)"]);
},
_destScaleMaker: function(max) {
return d3.scale.linear()
.domain([0, max])
.range(["rgb(243,231,214)", "rgb(230,230,213)"]);
},
_resetColors: function(selector) {
d3.selectAll(selector).classed("highlighted", function(d, i){
return false;
});
d3.selectAll(selector).classed("highlighted2", function(d, i){
return false;
});
},
ripple: function(vis) {
var d3arcs = vis.selectAll("path.arc-path");
var d3centerText = vis.selectAll("text.center-time");
var lastIndex = d3arcs[0].length-1;
var maxSeconds = parseInt(d3.select(d3arcs[0][lastIndex]).datum().seconds, 10);
var arcColorScale = d3methods._arcScaleMaker(maxSeconds);
var highlightColor = d3methods._highlightColor;
var transitionColor = d3methods._transitionColor;
d3arcs.transition()
.delay(function(d, i){
return i*400;
})
.duration(800)
.attr("fill", transitionColor)
.each("start", function(d, i){
if(i === 1) { d3methods._resetColors("path.arc-path"); }
d3centerText.transition()
.delay(440)
.text(d3methods._toMin(d.seconds));
})
.each("end", function(d, i) {
var indx = i;
d3.select(this).transition()
.duration(350)
.attr("fill", arcColorScale(d.seconds))
.each("end", function(d, i) {
if(indx === lastIndex) {
d3centerText.transition()
.delay(100)
.text(d3methods._toMin(d3centerText.datum().seconds));
d3.select(d3arcs[0][0]).transition()
.duration(300)
.attr("fill", highlightColor)
.each("end", function(d, i) {
var d3this = d3.select(this);
d3this.classed("highlighted", true);
d3this.attr("fill", arcColorScale(d.seconds));
});
}
});
});
},
render: function(dataset, vis) {
dataset = _.filter(dataset, function(prediction){
return (prediction.seconds > 0) ? 1 : 0;
});
// constants
var w = $(vis[0]).width();
var h = $(vis[0]).height();
h = (h < w) ? h : w;
var cX = Math.round(w/2);
var cY = Math.floor(h/2);
var arcMin = Math.floor(h*0.15);
var arcWidth = Math.floor(arcMin/3.75);
var arcPad = Math.ceil(arcWidth*0.1);
var highlightColor = d3methods._highlightColor;
var highlight2Color = d3methods._highlight2Color;
var transitionColor = d3methods._transitionColor;
var destColor = d3methods._destColor;
var arcColorScale = d3methods._arcScaleMaker( d3.max(dataset, function(d) {
return parseInt(d.seconds, 10);
}) );
var destColorScale = d3methods._destScaleMaker( d3.max(dataset, function(d) {
return parseInt(d.secondsTotal, 10);
}) );
function updateCenter(newData) {
d3centerText.data(newData).text(function(d){
return d3methods._toMin(d.seconds);
})
.style("font-size", Math.floor(arcMin*1.44) + 'px')
.attr("transform", 'translate(0,'+ parseInt(arcMin/2, 10) +')');
}
var transitionTime = 3000;
// main group where objects are located -- saves from declaring multiple transforms
var g = vis.select("g.center-group");
g.attr("transform", 'translate('+ cX +','+ cY +')');
var gArc = g.selectAll("g.arc-group");
var d3centerText = g.selectAll(".center-time");
var key = function(d) {
return d.vehicle;
};
gArc = gArc.data(dataset, key);
gArc.exit().remove();
var centerTextData = [dataset[0]];
// defining arc accessor
var r;
var arc = d3.svg.arc()
.innerRadius(function(d, i) {
r = d.index || i;
return arcMin + r*(arcWidth) + arcPad;
})
.outerRadius(function(d, i) {
r = d.index || i;
return arcMin + (r+1)*(arcWidth);
})
.startAngle(0)
.endAngle(function(d) {
return d3methods._secToRadians(d.seconds);
});
var arcDest = d3.svg.arc()
.innerRadius(function(d, i) {
r = d.index || i;
return arcMin + r*(arcWidth) + arcPad;
})
.outerRadius(function(d, i) {
r = d.index || i;
return arcMin + (r+1)*(arcWidth);
})
.startAngle(function(d) {
if(d.seconds < 0) {
return 0;
} else {
var pad = ( parseInt(d.secondsTotal-d.seconds, 10) > 180 ) ? 15 : 8;
return d3methods._secToRadians(d.seconds + pad);
}
})
.endAngle(function(d) {
return d3methods._secToRadians(d.secondsTotal);
});
function arcTween(a, indx) {
var end = {
index: indx,
seconds: a.seconds
};
var inter = d3.interpolateObject(this._current, end);
this._current = end;
return function(t) {
return arc(inter(t), indx);
};
}
function destTween(a, indx) {
var end = {
index: indx,
seconds: a.seconds,
secondsTotal: a.secondsTotal
};
var inter = d3.interpolateObject(this._current, end);
this._current = end;
return function(t) {
return arcDest(inter(t), indx);
};
}
// update for arcs
// loop below is to see if there is a highlighted arc
var hasHighlight = false;
gArc.select("path.arc-path").each(function(d){
if( this.classList.contains('highlighted') || this.classList.contains('highlighted2') ) {
hasHighlight = true;
centerTextData = [d3.select(this).datum()];
}
});
// re-colors the arcs, if there is no highlighted arc (from above), highlight the first one
gArc.select("path.arc-path").classed("highlighted", function(d, i){
if( this.classList.contains("highlighted") || (!hasHighlight && i === 0)) {
return true;
} else {
return false;
}
});
gArc.select("path.arc-path").transition()
.duration(transitionTime)
.attrTween("d", arcTween);
gArc.select("path.dest-path").transition()
.duration(transitionTime)
.attrTween("d", destTween);
// enter for arcs
var group = gArc.enter().append("svg:g").attr("class", 'arc-group');
group.append("svg:path").attr("class", 'arc-path')
.attr("fill", function(d, i){
return arcColorScale(d.seconds);
})
.classed("highlighted", function(d, i){
if(i === 0) {
return true;
}
})
.attr("d", arc)
.each(function(d, i){
this._current = {
index: i,
seconds: d.seconds
};
if( d.secondsTotal && d.secondsTotal < 60*60 ) {
var indx = i;
d3.select(this.parentNode).append("svg:path").attr("class", 'dest-path')
.attr("fill", function(d){
return destColorScale(d.secondsTotal);
})
.attr("d", function(d, i) {
return arcDest(d, indx);
})
.each(function(d){
this._current = {
index: indx,
seconds: d.seconds,
secondsTotal: d.secondsTotal
};
});
}
});
gArc.selectAll("path.arc-path")
.on("click", function(d, i){
d3methods._resetColors("path.arc-path");
d3methods._resetColors("path.dest-path");
var d3selected = d3.select(this);
d3selected.classed("highlighted", true);
centerTextData = [d];
var busTitle = nb.routesInfo[d.routeTag].title;
var stopTitle = nb.routesInfo[d.routeTag].stopsInfo[centerTextData[0].stopTag].title;
var minTitle = d3methods._toMin(d.seconds);
if(minTitle > 0) {
minTitle = ' in '+ minTitle + ' min';
} else {
minTitle = ' in less than 1 min';
}
updateTitle(stopTitle +': '+ busTitle + minTitle);
updateCenter(centerTextData);
});
gArc.selectAll("path.dest-path")
.on("click", function(d, i){
d3methods._resetColors("path.arc-path");
d3methods._resetColors("path.dest-path");
var d3selected = d3.select(this);
d3selected.classed("highlighted", true);
var d3stopArc = d3.select(this.previousElementSibling);
d3stopArc.classed("highlighted2", true);
centerTextData = [{seconds: d.secondsTotal}];
// var busTitle = routesInfo.routesList[d.routeTag];
var stopTitle = nb.routesInfo[d.routeTag].stopsInfo[d.stopTag].title;
var stop2Title = nb.routesInfo[d.routeTag].stopsInfo[d.destTag].title;
updateTitle(stopTitle + ' to ' + stop2Title);
updateCenter(centerTextData);
});
// enter and update for the center text
d3centerText = d3centerText.data(centerTextData);
d3centerText.enter().append("svg:text")
.attr("class", "center-time")
.attr("text-anchor", 'middle');
updateCenter(centerTextData);
if(!d3.selectAll(".click-circle")[0].length) {
g.append("circle").attr("r", arcMin*0.85).attr("fill", "rgba(255,255,255, 0.01)").attr("class", 'click-circle');
}
g.select(".click-circle").on("click", function(d){
this.__rotate__ = !this.__rotate__;
if(this.__rotate__) {
matchTime();
} else {
resetZero();
}
});
}
};
var matchTime = function() {
var dt = new Date();
var deg = (dt.getMinutes()/60)*360;
d3.selectAll('g.arc-group').transition().duration(300).attr("transform", 'rotate(' + deg + ')');
};
var resetZero = function() {
d3.selectAll('g.arc-group').transition().duration(300).attr("transform", 'rotate(0)');
};
// dirTag is optional. if provided, will set drop-down to the specified direction
var displayDirections = function(stopsInfo, routes, dirTag) {
var $dirSel = $("#DirectionSelector");
$dirSel.empty();
$("#stopSelector").empty();
var routeOption = Handlebars.compile('<option value="{{value}}">{{title}}{{#if from}} from {{from}}{{/if}}</option>');
var stopOption = Handlebars.compile('<option value="{{value}}">{{title}}</option>');
var opt1 = '';
_(routes).each(function(route, key) {
// if a route has more than two origins add a 'from' to clarify
if (route.name === 'Inbound' && Object.keys(routes).length>2) {
route.from = stopsInfo[route.stops[0]].title;
}
route.value = key;
$dirSel.append(routeOption(route));
});
dirTag ? $dirSel.val(dirTag) : dirTag = $dirSel.val();
displayStops(stopsInfo, routes, dirTag);
};
var stopOption = Handlebars.compile('<option value="{{value}}">{{title}}</option>');
// stopTag is optional. if provided will set drop-down to specified stop
var displayStops = function(stopsInfo, routes, dirTag, stopTag) {
var $stopSel = $("#StopSelector");
$stopSel.empty();
_(routes[dirTag].stops).each(function(stopNum) {
$stopSel.append(stopOption({
value: stopNum,
title: stopsInfo[stopNum].title
}));
});
stopTag ? $stopSel.val(stopTag) : stopTag = $stopSel.val();
displayDestinations(stopsInfo, routes, dirTag, stopTag);
};
var displayDestinations = function(stopsInfo, routes, dirTag, selectedStop) {
var $destSel = $("#DestSelector");
$destSel.empty();
var stops = routes[dirTag].stops;
var flag = false;
_(stops).each(function(stopTag) {
if(flag) {
$destSel.append(stopOption({
value: stopTag,
title: stopsInfo[stopTag].title
}));
}
if(stopTag === selectedStop) { flag = true; }
});
};
// stores information of the routes the user has looked up for the session
var nb = nextBusser();
$(function() {
var charts = [];
$("#RouteSelector").change(function(){
var routeTag = $("#RouteSelector").val();
// if the route hasn't been looked up before, look it up and store it in routesInfo
if(nb.routesInfo[routeTag] && typeof nb.routesInfo[routeTag].stopsInfo === 'undefined') {
nb.getNextbus({command: 'routeConfig', a:'sf-muni', r: routeTag}, function(xml) {
_.extend(nb.routesInfo[routeTag], nb.parseXMLstops(xml));
displayDirections(nb.routesInfo[routeTag].stopsInfo, nb.routesInfo[routeTag].directions);
});
} else {
displayDirections(nb.routesInfo[routeTag].stopsInfo, nb.routesInfo[routeTag].directions);
}
});
$("#DirectionSelector").change(function(){
var route = nb.routesInfo[$("#RouteSelector").val()];
displayStops(route.stopsInfo, route.directions, $("#DirectionSelector").val());
});
$("#StopSelector").change(function(){
var route = nb.routesInfo[$("#RouteSelector").val()];
var stopTag = $("#StopSelector").val();
displayDestinations(route.stopsInfo, route.directions, $("#DirectionSelector").val(), stopTag);
});
$("#AddRouteButton").click(function(){
var routeTag = $("#RouteSelector").val();
var stopTag = $("#StopSelector").val();
if(charts[0]) {
updateChart(stopTag, routeTag, charts[0]);
updateChartView(charts[0]);
} else {
charts.push(makeChart(stopTag, routeTag));
makeChartView(charts[0]);
}
});
// list pulled from: http://webservices.nextbus.com/service/publicXMLFeed?command=routeList&a=sf-muni
var routesToInsert = [
["F", "F-Market & Wharves"],
["J", "J-Church"],
["KT", "KT-Ingleside/Third Street"],
["L", "L-Taraval"],
["M", "M-Ocean View"],
["N", "N-Judah"],
["NX", "NX-N Express"],
["1", "1-California"],
["1AX", "1AX-California A Express"],
["1BX", "1BX-California B Express"],
["2", "2-Clement"],
["3", "3-Jackson"],
["5", "5-Fulton"],
["6", "6-Parnassus"],
["8X", "8X-Bayshore Express"],
["8AX", "8AX-Bayshore A Express"],
["8BX", "8BX-Bayshore B Express"],
["9", "9-San Bruno"],
["9L", "9L-San Bruno Limited"],
["10", "10-Townsend"],
["12", "12-Folsom/Pacific"],
["14", "14-Mission"],
["14L", "14L-Mission Limited"],
["14X", "14X-Mission Express"],
["16X", "16X-Noriega Express"],
["17", "17-Parkmerced"],
["18", "18-46th Avenue"],
["19", "19-Polk"],
["21", "21-Hayes"],
["22", "22-Fillmore"],
["23", "23-Monterey"],
["24", "24-Divisadero"],
["27", "27-Bryant"],
["28", "28-19th Avenue"],
["28L", "28L-19th Avenue Limited"],
["29", "29-Sunset"],
["30", "30-Stockton"],
["30X", "30X-Marina Express"],
["31", "31-Balboa"],
["31AX", "31AX-Balboa A Express"],
["31BX", "31BX-Balboa B Express"],
["33", "33-Stanyan"],
["35", "35-Eureka"],
["36", "36-Teresita"],
["37", "37-Corbett"],
["38", "38-Geary"],
["38AX", "38AX-Geary A Express"],
["38BX", "38BX-Geary B Express"],
["38L", "38L-Geary Limited"],
["39", "39-Coit"],
["41", "41-Union"],
["43", "43-Masonic"],
["44", "44-O'Shaughnessy"],
["45", "45-Union/Stockton"],
["47", "47-Van Ness"],
["48", "48-Quintara/24th Street"],
["49", "49-Mission/Van Ness"],
["52", "52-Excelsior"],
["54", "54-Felton"],
["56", "56-Rutland"],
["66", "66-Quintara"],
["67", "67-Bernal Heights"],
["71", "71-Haight/Noriega"],
["71L", "71L-Haight/Noriega Limited"],
["76X", "76X-Marin Headlands Express"],
["81X", "81X-Caltrain Express"],
["82X", "82X-Levi Plaza Express"],
["83X", "83X-Caltrain"],
["88", "88-Bart Shuttle"],
["90", "90-San Bruno Owl"],
["91", "91-Owl"],
["108", "108-Treasure Island"],
["K OWL", "K-Owl"],
["L OWL", "L-Owl"],
["M OWL", "M-Owl"],
["N OWL", "N-Owl"],
["T OWL", "T-Owl"],
["59", "Powell/Mason Cable Car"],
["60", "Powell/Hyde Cable Car"],
["61", "California Cable Car"]];
// insert the routes into the drop down and also create a lookup hash
_(routesToInsert).each(function(route){
$("#RouteSelector").append('<option value="'+ route[0] +'">'+ route[1] +'</option>');
nb.routesInfo[route[0]] = {};
nb.routesInfo[route[0]].title = route[1];
});
var recursiveAsyncGoodness = function(queries, indx) {
indx = indx || 0;
if(queries && queries.length > 0) {
if(indx < queries.length) {
var stopTag = queries[indx].s,
routeTag = queries[indx].r;
nb.getNextbus({command: 'routeConfig', a:'sf-muni', r: routeTag}, function(xml) {
nb.routesInfo[routeTag] = nb.parseXMLstops(xml);
(indx===0) ? charts.push(makeChart(stopTag, routeTag)) : updateChart(stopTag, routeTag, charts[0]);
recursiveAsyncGoodness(queries, indx+1);
});
} else {
makeChartView(charts[0]);
updateFormView(queries[0].s, queries[0].r);
}
}
};
var params = window.location.search;
if(params.length>0) {
var queries = deserialiseParams(params.substr(1));
recursiveAsyncGoodness(queries);
}
});
// deparam - thanks Ben Alman!
// https://gist.github.com/cowboy/1025817
(function($) {
// Creating an internal undef value is safer than using undefined, in case it
// was ever overwritten.
var undef;
// A handy reference.
var decode = decodeURIComponent;
// Document $.deparam.
var deparam = $.deparam = function(text, reviver) {
// The object to be returned.
var result = {};
// Iterate over all key=value pairs.
$.each(text.replace(/\+/g, ' ').split('&'), function(index, pair) {
// The key=value pair.
var kv = pair.split('=');
// The key, URI-decoded.
var key = decode(kv[0]);
// Abort if there's no key.
if ( !key ) { return; }
// The value, URI-decoded. If value is missing, use empty string.
var value = decode(kv[1] || '');
// If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it
// into its component parts.
var keys = key.split('][');
var last = keys.length - 1;
// Used when key is complex.
var i = 0;
var current = result;
// If the first keys part contains [ and the last ends with ], then []
// are correctly balanced.
if ( keys[0].indexOf('[') >= 0 && /\]$/.test(keys[last]) ) {
// Remove the trailing ] from the last keys part.
keys[last] = keys[last].replace(/\]$/, '');
// Split first keys part into two parts on the [ and add them back onto
// the beginning of the keys array.
keys = keys.shift().split('[').concat(keys);
// Since a key part was added, increment last.
last++;
} else {
// Basic 'foo' style key.
last = 0;
}
if ( $.isFunction(reviver) ) {
// If a reviver function was passed, use that function.
value = reviver(key, value);
} else if ( reviver ) {
// If true was passed, use the built-in $.deparam.reviver function.
value = deparam.reviver(key, value);
}
if ( last ) {
// Complex key, like 'a[]' or 'a[b][c]'. At this point, the keys array
// might look like ['a', ''] (array) or ['a', 'b', 'c'] (object).
for ( ; i <= last; i++ ) {
// If the current key part was specified, use that value as the array
// index or object key. If omitted, assume an array and use the
// array's length (effectively an array push).
key = keys[i] !== '' ? keys[i] : current.length;
if ( i < last ) {
// If not the last key part, update the reference to the current
// object/array, creating it if it doesn't already exist AND there's
// a next key. If the next key is non-numeric and not empty string,
// create an object, otherwise create an array.
current = current[key] = current[key] || (isNaN(keys[i + 1]) ? {} : []);
} else {
// If the last key part, set the value.
current[key] = value;
}
}
} else {
// Simple key.
if ( $.isArray(result[key]) ) {
// If the key already exists, and is an array, push the new value onto
// the array.
result[key].push(value);
} else if ( key in result ) {
// If the key already exists, and is NOT an array, turn it into an
// array, pushing the new value onto it.
result[key] = [result[key], value];
} else {
// Otherwise, just set the value.
result[key] = value;
}
}
});
return result;
};
// Default reviver function, used when true is passed as the second argument
// to $.deparam. Don't like it? Pass your own!
deparam.reviver = function(key, value) {
var specials = {
'true': true,
'false': false,
'null': null,
'undefined': undef
};
return (+value + '') === value ? +value // Number
: value in specials ? specials[value] // true, false, null, undefined
: value; // String
};
}(jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment