"use strict"; function TrainDiagram(ctx, container) { var route_start, route_end, train_data = {features: []}; var train_num_lookup = {}; var next_scroll_top, next_scroll_bottom; container = d3.select(container); var svg = container.append('svg') .attr('width', 900) .attr('height', 500); var hl = svg.append('rect').attr('id', 'hl'); ctx.on('refresh-train-data', function() { container.append('span') .attr('class', 'spinner') .append('img') .attr('src', 'spinner.gif'); }); ctx.on('train-data', function() { container.select('.spinner') .remove(); }); ctx.on('set-route-start', function(stn) { route_start = stn; next_scroll_top = stn; update(); }); ctx.on('set-route-end', function(stn) { route_end = stn; next_scroll_bottom = stn; update(); }); ctx.on('train-data', function(v) { train_data = v; train_num_lookup = {}; train_data.features.forEach(function(train) { train_num_lookup[train.properties.TrainNum] = train; }); window.train_num_lookup = train_num_lookup; update(); }); function update() { var train_list = findTrains(), stop_info = findVisitedStops(train_list); drawStops(stop_info); drawTrains(train_list, stop_info); if (train_list.length > 0) {console.log("Train List:", train_list);} } function findTrains() { if (!train_data) {return [];} var train_list = []; train_data.features.forEach(function(train) { var stop_codes = train.properties.stops.map(function(s) {return s.code;}), start_idx = stop_codes.indexOf(route_start), end_idx = stop_codes.indexOf(route_end); if (end_idx > start_idx && start_idx > -1) { train_list.push(train.properties.TrainNum); } }); return train_list; } function findVisitedStops(train_list) { // for each train in train_list, determine the stops visited by the train and // the scheduled time as a fraction of the time between route_start and // route_end. Return the average of these fractions for each stop. var stop_list = []; var min_ts = Infinity, offset; train_list.forEach(function(train_num) { var train = train_num_lookup[train_num], start_ts, end_ts, scale; train.properties.stops.forEach(function(stop) { if (stop.code == route_start) {start_ts = new Date(stop.schdep);} if (stop.code == route_end) {end_ts = stop.scharr ? new Date(stop.scharr) : new Date(stop.schdep);} }); scale = end_ts - start_ts; train.properties.stops.forEach(function(stop) { stop_list.push({ code: stop.code, arr: stop.scharr ? (new Date(stop.scharr) - start_ts) / scale : undefined, dep: stop.schdep ? (new Date(stop.schdep) - start_ts) / scale : undefined, }); }); }); var stop_lookup = {}; stop_list.forEach(function(s) { if (!stop_lookup[s.code]) {stop_lookup[s.code] = [];} stop_lookup[s.code].push(s); }); var stop_info = []; for (var scode in stop_lookup) { var arrs = 0, deps = 0, arrsum = 0, depsum = 0; for (var i = 0; i < stop_lookup[scode].length; i++) { var s = stop_lookup[scode][i]; if (typeof s.arr != 'undefined') {arrs += 1; arrsum += s.arr;} if (typeof s.dep != 'undefined') {deps += 1; depsum += s.dep;} } stop_info.push({code: scode, arr: arrsum / arrs, dep: depsum / deps}); } stop_info.sort(function(a, b) {return (a.dep || a.arr) - (b.dep || b.arr);}); return stop_info; } var stnPos = function() {return 0;}; function drawStops(stop_info) { var buffer = 11, scale = 250; var stop_lookup = {}; stop_info.forEach(function(s, i) {s.idx = i; stop_lookup[s.code] = s;}); var ymin = 0, ymax = 500, n = stop_info.length; if (n > 0) { ymin = stop_info[0].arr * scale || stop_info[0].dep * scale; ymax = stop_info[n-1].dep * scale || stop_info[n-1].arr * scale; svg.attr('height', ymax - ymin + 2 * buffer); } stnPos = function(code) { if (!(stop_lookup[code])) {return {};} var s = stop_lookup[code], offset = buffer * (s.idx + 1), arr = (s.arr || s.dep) * scale + offset, dep = (s.dep === 0 || s.dep) ? s.dep * scale + offset : arr; if (dep < arr) {dep = arr;} return { arr: arr - ymin, dep: dep - ymin, mid: (arr + dep) / 2 - ymin, }; }; if (stop_lookup[route_start] && stop_lookup[route_end]) { hl.attr('x', 60) .attr('width', 55) .attr('y', stnPos(route_start).arr - buffer) .attr('height', stnPos(route_end).dep - stnPos(route_start).arr + buffer * 2); } var stops = svg.selectAll('g.stop').data(stop_info); var new_stops = stops.enter() .append('g') .attr('class', 'stop'); new_stops.append('g').attr('class', 'stop-marker'); new_stops.select('.stop-marker').append('rect').attr('class', 'under'); new_stops.select('.stop-marker').append('circle').attr('class', 'arr'); new_stops.select('.stop-marker').append('circle').attr('class', 'dep'); new_stops.select('.stop-marker').append('rect').attr('class', 'layover'); new_stops.append('text'); new_stops.on('mouseover', stopMouseover); stops.exit() .remove(); stops.selectAll('.stop-marker') .call(createStopMarker); stops.selectAll('text') .attr('x', 93) .attr('y', function(d) {return stnPos(d.code).mid + 3;}) .text(function(d) {return d.code;}); function createStopMarker(m) { var x = 100, r = 3; m.select('.arr') .attr('cx', x) .attr('cy', function(d) {return stnPos(d.code).arr;}) .attr('r', r); m.select('.dep') .attr('cx', x) .attr('cy', function(d) {return stnPos(d.code).dep;}) .attr('r', r); m.selectAll('.under') .attr('x', x - r) .attr('width', 2 * r) .attr('y', function(d) {return stnPos(d.code).arr;}) .attr('height', function(d) {var v = stnPos(d.code); return v.dep - v.arr;}); m.selectAll('.layover') .attr('x', x - r + 0.5) .attr('width', 2 * r - 1) .attr('y', function(d) {return stnPos(d.code).arr;}) .attr('height', function(d) {var v = stnPos(d.code); return v.dep - v.arr;}); } } function drawTrains(train_list, stop_info) { var padding = 10; // set y-value for each train // adjust y-values to avoid overlaps // draw labels train_list.forEach(function(train_num) { var train = train_num_lookup[train_num], // find stop for next event next = findNextStopForTrain(train); // find stop for previous event // estimate completed fraction of segment between the two // compute initial y value //console.log(train.properties.TrainNum, train.properties.EventCode, train.properties.EventT, next, stnPos(next)); if (!next) {console.log("No next! train:", train_num);} train.y = stnPos(next); }); var data = train_data.features .filter(function(t) {return train_list.indexOf(t.properties.TrainNum) > -1;}) .filter(function(t) {return typeof t.y != 'undefined';}); // compute adjusted y-values (avoiding overlap) var trains = svg.selectAll('g.train').data(data, function(t) { return t.properties.ID; }); trains.exit().remove(); var new_trains = trains.enter() .append('g') .attr('class', 'train'); new_trains.append('rect').attr('class', 'train-bg'); new_trains.append('circle').attr('class', 'train-marker'); new_trains.append('text').attr('class', 'train-info'); trains.selectAll('.train-bg') .transition() .attr('x', 120 - padding) .attr('y', function(d) {return d.y.dep - padding;}) .attr('width', 180 + padding) .attr('height', padding * 2) .attr('rx', padding / 2) .attr('ry', padding / 2); trains.selectAll('.train-marker') .transition() .attr('cx', 120) .attr('cy', function(d) {return d.y.dep;}) .attr('r', 4); trains.selectAll('.train-info') .transition() .attr('x', 120 + padding) .attr('y', function(d) {return d.y.dep;}) .attr('transform', 'translate(0 5)') .text(trainInfoText); trains.on('mouseover', trainMouseover); } var tfmt = (function() { var fmt = d3.time.format('%I:%M%p'); return function(t) { var tstr = fmt(new Date(t)); return tstr[0] == '0' ? tstr.substr(1) : tstr; }; })(); function trainInfoText(d) { var eta = findTrainStop(d, route_end)[0]; return d.properties.TrainNum + ' ETA: ' + tfmt(eta.estarr || eta.scharr || eta.estdep || eta.schdep); } function stopMouseover(d) { console.log(d); } function trainMouseover(d) { console.log(d); showTrainDetails(d); } function findTrainStop(train, stop) { return train.properties.stops.filter(function(s) {return s.code == stop;}); } function findNextStopForTrain(d) { return (d.properties.EventCode ? d.properties.EventCode : d.properties.TrainState == 'Predeparture' ? d.properties.OrigCode : undefined); } var details = container.append('div').attr('class', 'details'); function detailsTop() {return d3.select('body').property('scrollTop') + 60;} function showTrainDetails(t) { details.selectAll('*').remove(); details.transition().style('top', detailsTop() + 'px'); details.datum(t); var title = details.append('div').attr('class', 'details-title'), current = details.append('div').attr('class', 'details-current'), table = details.append('table').attr('class', 'details-table'); title.append('span').attr('class', 'train-num') .text(t.properties.TrainNum); title.append('span').attr('class', 'route-name') .text(t.properties.RouteName + ' (' + t.properties.OrigCode + ' - ' + t.properties.DestCode + ')'); current.append('span').attr('class', 'speed') .text('Heading: ' + t.properties.Heading + ' at ' + Math.round(+t.properties.Velocity, 1) + ' mph'); var rows = table.selectAll('tr.stop-details').data(t.properties.stops) .enter().append('tr') .attr('class', 'stop-details'); rows.append('td') .text(function(d) {return d.code;}); rows.append('td') .text(function(d) {return d.postcmnt || d.estarrcmnt;}); } }