Beginnings of ETA calc based on actual data
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,4 +10,9 @@
[submodule "js/yepnope"]
path = js/yepnope
url = https://github.com/SlexAxton/yepnope.js.git
-
+[submodule "javascripts/tesseract"]
+ path = javascripts/tesseract
+ url = https://github.com/square/tesseract.git
+[submodule "javascripts/d3"]
+ path = javascripts/d3
+ url = https://github.com/mbostock/d3.git
--- a/include/common-request.inc.php
+++ b/include/common-request.inc.php
@@ -29,6 +29,9 @@
}
if (isset($_REQUEST['nearby'])) {
$nearby = true;
+}
+if (isset($_REQUEST['labs'])) {
+ $labs = true;
}
if (isset($_REQUEST['suburb'])) {
$suburb = $_REQUEST['suburb'];
--- a/include/db/servicealert-dao.inc.php
+++ b/include/db/servicealert-dao.inc.php
@@ -106,7 +106,19 @@
function getFutureAlerts() {
global $conn;
- $query = "SELECT id,extract('epoch' from start) as start, extract('epoch' from \"end\") as \"end\",cause,effect,header,description,url from servicealerts_alerts where NOW() > start or NOW() < \"end\"";
+ $query = "SELECT id,extract('epoch' from start) as start, extract('epoch' from \"end\") as \"end\",cause,effect,header,description,url from servicealerts_alerts where NOW() < \"end\"";
+ // debug($query, "database");
+ $query = $conn->prepare($query);
+ $query->execute();
+ if (!$query) {
+ databaseError($conn->errorInfo());
+ return Array();
+ }
+ return $query->fetchAll();
+}
+function getAllAlerts() {
+ global $conn;
+ $query = "SELECT id,extract('epoch' from start) as start, extract('epoch' from \"end\") as \"end\",cause,effect,header,description,url from servicealerts_alerts";
// debug($query, "database");
$query = $conn->prepare($query);
$query->execute();
--- /dev/null
+++ b/labs/busdelay.csv.php
@@ -1,1 +1,52 @@
+<?php
+setlocale(LC_CTYPE, 'C');
+// source: http://stackoverflow.com/questions/81934/easy-way-to-export-a-sql-table-without-access-to-the-server-or-phpmyadmin#81951
+include ('../include/common.inc.php');
+$query = $conn->prepare('
+SELECT * from myway_timingdeltas'
+ , array(PDO::ATTR_CURSOR => PDO::FETCH_ORI_NEXT));
+$query->execute();
+$errors = $conn->errorInfo();
+if ($errors[2] != "") {
+ die("Export terminated, db error" . print_r($errors, true));
+}
+
+$headers = Array("date", "delay", "distance", "origin", "destination");
+
+$fp = fopen('php://output', 'w');
+if ($fp && $query) {
+ //header('Content-Type: text/csv');
+ header('Pragma: no-cache');
+ header('Expires: 0');
+ fputcsv($fp, $headers);
+ while ($r = $query->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT)) {
+ $row = Array();
+ foreach ($headers as $i => $fieldName) {
+ switch ($fieldName) {
+ case "date":
+ $row[] = date("r",strtotime($r['date']." ".$r['time']));
+ break;
+ case "delay":
+ $row[] = $r['timing_delta'];
+ break;
+ case "distance":
+ $row[] = $r['stop_sequence'];
+ break;
+ case "origin":
+ $row[] = $r['myway_stop'];
+ break;
+ case "destination":
+ $row[] = $r['route_name'];
+ break;
+ default:
+ break;
+ }
+ }
+ fputcsv($fp, array_values($row));
+ }
+ die;
+}
+?>
+
+
--- /dev/null
+++ b/labs/busdelay.php
@@ -1,1 +1,495 @@
-
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tesseract</title>
+<style>
+
+
+#charts {
+ padding: 10px 0;
+}
+
+.chart {
+ display: inline-block;
+ height: 151px;
+ margin-bottom: 20px;
+}
+
+.reset {
+ padding-left: 1em;
+ font-size: smaller;
+ color: #ccc;
+}
+
+.background.bar {
+ fill: #ccc;
+}
+
+.foreground.bar {
+ fill: steelblue;
+}
+
+.axis path, .axis line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+
+.axis text {
+ font: 10px sans-serif;
+}
+
+.brush rect.extent {
+ fill: steelblue;
+ fill-opacity: .125;
+}
+
+.brush .resize path {
+ fill: #eee;
+ stroke: #666;
+}
+
+#hour-chart {
+ width: 260px;
+}
+
+#delay-chart {
+ width: 230px;
+}
+
+#distance-chart {
+ width: 420px;
+}
+
+#date-chart {
+ width: 920px;
+}
+
+#flight-list {
+ min-height: 1024px;
+}
+
+#flight-list .date,
+#flight-list .day {
+ margin-bottom: .4em;
+}
+
+#flight-list .flight {
+ line-height: 1.5em;
+ background: #eee;
+ width: 640px;
+ margin-bottom: 1px;
+}
+
+#flight-list .time {
+ color: #999;
+}
+
+#flight-list .flight div {
+ display: inline-block;
+ width: 100px;
+}
+
+#flight-list div.distance,
+#flight-list div.delay {
+ width: 160px;
+ padding-right: 10px;
+ text-align: right;
+}
+
+#flight-list .early {
+ color: green;
+}
+
+aside {
+ position: absolute;
+ left: 740px;
+ font-size: smaller;
+ width: 220px;
+}
+
+</style>
+
+
+<div id="charts">
+ <div id="hour-chart" class="chart">
+ <div class="title">Time of Day</div>
+ </div>
+ <div id="delay-chart" class="chart">
+ <div class="title">Arrival Delay (min.)</div>
+ </div>
+ <div id="distance-chart" class="chart">
+ <div class="title">Distance (mi.)</div>
+ </div>
+ <div id="date-chart" class="chart">
+ <div class="title">Date</div>
+ </div>
+</div>
+
+<aside id="totals"><span id="active">-</span> of <span id="total">-</span> flights selected.</aside>
+
+<div id="lists">
+ <div id="flight-list" class="list"></div>
+</div>
+
+
+</div>
+
+
+<script src="../js/tesseract/tesseract.min.js"></script>
+<script src="../js/d3/d3.v2.min.js"></script>
+<script>
+
+d3.csv("busdelay.csv.php", function(flights) {
+
+ // Various formatters.
+ var formatNumber = d3.format(",d"),
+ formatChange = d3.format("+,d"),
+ formatDate = d3.time.format("%B %d, %Y"),
+ formatTime = d3.time.format("%I:%M %p");
+
+ // A nest operator, for grouping the flight list.
+ var nestByDate = d3.nest()
+ .key(function(d) { return d3.time.day(d.date); });
+
+ // A little coercion, since the CSV is untyped.
+ flights.forEach(function(d, i) {
+ d.index = i;
+ d.date = parseDate(d.date);
+ d.delay = +d.delay;
+ d.distance = +d.distance;
+ });
+
+ // Create the tesseract and relevant dimensions and groups.
+ flight = tesseract(flights),
+ all = flight.groupAll(),
+ date = flight.dimension(function(d) { return d3.time.day(d.date); }),
+ dates = date.group(),
+ hour = flight.dimension(function(d) { return d.date.getHours() + d.date.getMinutes() / 60; }),
+ hours = hour.group(Math.floor),
+ //delay = flight.dimension(function(d) { return Math.max(-60, Math.min(149, d.delay)); }),
+ delay = flight.dimension(function(d) { return d.delay; }),
+ delays = delay.group(function(d) { return Math.floor(d / 10) * 10; }),
+ distance = flight.dimension(function(d) { return Math.min(60, d.distance); }),
+ distances = distance.group(function(d) { return Math.floor(d / 50) * 50; });
+
+ var charts = [
+
+ barChart()
+ .dimension(hour)
+ .group(hours)
+ .x(d3.scale.linear()
+ .domain([0, 24])
+ .rangeRound([0, 10 * 24])),
+
+ barChart()
+ .dimension(delay)
+ .group(delays)
+ .x(d3.scale.linear()
+ .domain([-650, 650])
+ .rangeRound([0, 10 * 21])),
+
+ barChart()
+ .dimension(distance)
+ .group(distances)
+ .x(d3.scale.linear()
+ .domain([0, 60])
+ .rangeRound([0, 10 * 40])),
+
+ barChart()
+ .dimension(date)
+ .group(dates)
+ .round(d3.time.day.round)
+ .x(d3.time.scale()
+ .domain([new Date(2011, 4, 1), new Date(2012, 1, 4)])
+ .rangeRound([0, 10 * 90]))
+ .filter([new Date(2011, 4, 4), new Date(2012, 4, 4)])
+
+ ];
+
+ // Given our array of charts, which we assume are in the same order as the
+ // .chart elements in the DOM, bind the charts to the DOM and render them.
+ // We also listen to the chart's brush events to update the display.
+ var chart = d3.selectAll(".chart")
+ .data(charts)
+ .each(function(chart) { chart.on("brush", renderAll).on("brushend", renderAll); });
+
+ // Render the initial lists.
+ var list = d3.selectAll(".list")
+ .data([flightList]);
+
+ // Render the total.
+ d3.selectAll("#total")
+ .text(formatNumber(flight.size()));
+
+ renderAll();
+
+ // Renders the specified chart or list.
+ function render(method) {
+ d3.select(this).call(method);
+ }
+
+ // Whenever the brush moves, re-rendering everything.
+ function renderAll() {
+ chart.each(render);
+ list.each(render);
+ d3.select("#active").text(formatNumber(all.value()));
+ }
+
+ // Like d3.time.format, but faster.
+ function parseDate(d) {
+ return new Date(d);
+ }
+
+ window.filter = function(filters) {
+ filters.forEach(function(d, i) { charts[i].filter(d); });
+ renderAll();
+ };
+
+ window.reset = function(i) {
+ charts[i].filter(null);
+ renderAll();
+ };
+
+ function flightList(div) {
+ var flightsByDate = nestByDate.entries(date.top(40));
+
+ div.each(function() {
+ var date = d3.select(this).selectAll(".date")
+ .data(flightsByDate, function(d) { return d.key; });
+
+ date.enter().append("div")
+ .attr("class", "date")
+ .append("div")
+ .attr("class", "day")
+ .text(function(d) { return formatDate(d.values[0].date); });
+
+ date.exit().remove();
+
+ var flight = date.order().selectAll(".flight")
+ .data(function(d) { return d.values; }, function(d) { return d.index; });
+
+ var flightEnter = flight.enter().append("div")
+ .attr("class", "flight");
+
+ flightEnter.append("div")
+ .attr("class", "time")
+ .text(function(d) { return formatTime(d.date); });
+
+ flightEnter.append("div")
+ .attr("class", "origin")
+ .text(function(d) { return d.origin; });
+
+ flightEnter.append("div")
+ .attr("class", "destination")
+ .text(function(d) { return d.destination; });
+
+ flightEnter.append("div")
+ .attr("class", "distance")
+ .text(function(d) { return formatNumber(d.distance) + " mi."; });
+
+ flightEnter.append("div")
+ .attr("class", "delay")
+ .classed("early", function(d) { return d.delay < 0; })
+ .text(function(d) { return formatChange(d.delay) + " min."; });
+
+ flight.exit().remove();
+
+ flight.order();
+ });
+ }
+
+ function barChart() {
+ if (!barChart.id) barChart.id = 0;
+
+ var margin = {top: 10, right: 10, bottom: 20, left: 10},
+ x,
+ y = d3.scale.linear().range([100, 0]),
+ id = barChart.id++,
+ axis = d3.svg.axis().orient("bottom"),
+ brush = d3.svg.brush(),
+ brushDirty,
+ dimension,
+ group,
+ round;
+
+ function chart(div) {
+ var width = x.range()[1],
+ height = y.range()[0];
+
+ y.domain([0, group.top(1)[0].value]);
+
+ div.each(function() {
+ var div = d3.select(this),
+ g = div.select("g");
+
+ // Create the skeletal chart.
+ if (g.empty()) {
+ div.select(".title").append("a")
+ .attr("href", "javascript:reset(" + id + ")")
+ .attr("class", "reset")
+ .text("reset")
+ .style("display", "none");
+
+ g = div.append("svg")
+ .attr("width", width + margin.left + margin.right)
+ .attr("height", height + margin.top + margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+ g.append("clipPath")
+ .attr("id", "clip-" + id)
+ .append("rect")
+ .attr("width", width)
+ .attr("height", height);
+
+ g.selectAll(".bar")
+ .data(["background", "foreground"])
+ .enter().append("path")
+ .attr("class", function(d) { return d + " bar"; })
+ .datum(group.all());
+
+ g.selectAll(".foreground.bar")
+ .attr("clip-path", "url(#clip-" + id + ")");
+
+ g.append("g")
+ .attr("class", "axis")
+ .attr("transform", "translate(0," + height + ")")
+ .call(axis);
+
+ // Initialize the brush component with pretty resize handles.
+ var gBrush = g.append("g").attr("class", "brush").call(brush);
+ gBrush.selectAll("rect").attr("height", height);
+ gBrush.selectAll(".resize").append("path").attr("d", resizePath);
+ }
+
+ // Only redraw the brush if set externally.
+ if (brushDirty) {
+ brushDirty = false;
+ g.selectAll(".brush").call(brush);
+ div.select(".title a").style("display", brush.empty() ? "none" : null);
+ if (brush.empty()) {
+ g.selectAll("#clip-" + id + " rect")
+ .attr("x", 0)
+ .attr("width", width);
+ } else {
+ var extent = brush.extent();
+ g.selectAll("#clip-" + id + " rect")
+ .attr("x", x(extent[0]))
+ .attr("width", x(extent[1]) - x(extent[0]));
+ }
+ }
+
+ g.selectAll(".bar").attr("d", barPath);
+ });
+
+ function barPath(groups) {
+ var path = [],
+ i = -1,
+ n = groups.length,
+ d;
+ while (++i < n) {
+ d = groups[i];
+ path.push("M", x(d.key), ",", height, "V", y(d.value), "h9V", height);
+ }
+ return path.join("");
+ }
+
+ function resizePath(d) {
+ var e = +(d == "e"),
+ x = e ? 1 : -1,
+ y = height / 3;
+ return "M" + (.5 * x) + "," + y
+ + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6)
+ + "V" + (2 * y - 6)
+ + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y)
+ + "Z"
+ + "M" + (2.5 * x) + "," + (y + 8)
+ + "V" + (2 * y - 8)
+ + "M" + (4.5 * x) + "," + (y + 8)
+ + "V" + (2 * y - 8);
+ }
+ }
+
+ brush.on("brushstart.chart", function() {
+ var div = d3.select(this.parentNode.parentNode.parentNode);
+ div.select(".title a").style("display", null);
+ });
+
+ brush.on("brush.chart", function() {
+ var g = d3.select(this.parentNode),
+ extent = brush.extent();
+ if (round) g.select(".brush")
+ .call(brush.extent(extent = extent.map(round)))
+ .selectAll(".resize")
+ .style("display", null);
+ g.select("#clip-" + id + " rect")
+ .attr("x", x(extent[0]))
+ .attr("width", x(extent[1]) - x(extent[0]));
+ dimension.filterRange(extent);
+ });
+
+ brush.on("brushend.chart", function() {
+ if (brush.empty()) {
+ var div = d3.select(this.parentNode.parentNode.parentNode);
+ div.select(".title a").style("display", "none");
+ div.select("#clip-" + id + " rect").attr("x", null).attr("width", "100%");
+ dimension.filterAll();
+ }
+ });
+
+ chart.margin = function(_) {
+ if (!arguments.length) return margin;
+ margin = _;
+ return chart;
+ };
+
+ chart.x = function(_) {
+ if (!arguments.length) return x;
+ x = _;
+ axis.scale(x);
+ brush.x(x);
+ return chart;
+ };
+
+ chart.y = function(_) {
+ if (!arguments.length) return y;
+ y = _;
+ return chart;
+ };
+
+ chart.dimension = function(_) {
+ if (!arguments.length) return dimension;
+ dimension = _;
+ return chart;
+ };
+
+ chart.filter = function(_) {
+ if (_) {
+ brush.extent(_);
+ dimension.filterRange(_);
+ } else {
+ brush.clear();
+ dimension.filterAll();
+ }
+ brushDirty = true;
+ return chart;
+ };
+
+ chart.group = function(_) {
+ if (!arguments.length) return group;
+ group = _;
+ return chart;
+ };
+
+ chart.round = function(_) {
+ if (!arguments.length) return round;
+ round = _;
+ return chart;
+ };
+
+ return d3.rebind(chart, brush, "on");
+ }
+});
+
+</script>
+
--- a/rtpis/servicealert_editor.php
+++ b/rtpis/servicealert_editor.php
@@ -82,7 +82,7 @@
<table>
<?php
foreach (getFutureAlerts() as $alert) {
- echo "<tr><td>" . date("c", $alert['start']) . "</td><td>" . date("c", $alert['end']) . "</td><td>" . substr($alert['description'], 0, 999) . '</td><td><a href="?edit=' . $alert['id'] . '">edit</a></td></tr>';
+ echo "<tr><td>" . date("c", $alert['start']) . "<br>to<br>" . date("c", $alert['end']) . "</td><td><b>" .$alert['header']."</b><br>". $alert['description'] . '</td><td><a href="?edit=' . $alert['id'] . '">edit</a></td></tr>';
}
?>
</table>
--- a/stop.php
+++ b/stop.php
@@ -141,6 +141,30 @@
if (sizeof($trips) == 0) {
echo "<li style='text-align: center;'>No trips in the near future.</li>";
} else {
+ if ($labs) {
+// ETA calculation
+
+ $tripETA = Array();
+ // max/min delay instead of stddev?
+ $query = $query = "select 'lol', avg(timing_delta), stddev(timing_delta), count(*) from myway_timingdeltas where extract(hour from time) between ".date("H", $earlierTime)." and ".date("H", $laterTime);
+ //select 'lol', stop_id,extract(hour from time), avg(timing_delta), stddev(timing_delta), count(*) from myway_timingdeltas where stop_id = '5501' group by stop_id, extract(hour from time) order by extract(hour from time)
+ $query = $conn->prepare($query);
+ $query->execute();
+ if (!$query) {
+ databaseError($conn->errorInfo());
+ return Array();
+ }
+ $ETAparams = Array();
+ foreach ($query->fetchAll() as $row) {
+ $ETAparams[$row[0]] = Array("avg"=> $row[1], "stddev"=>floor($row[2]),"count"=>$row[3]);
+ };
+ //print_r($ETAparams);
+ foreach ($trips as $trip) {
+ $tripETA[$trip['trip_id']] = date("H:i",strtotime($trip['arrival_time']." - ".(floor($ETAparams['lol']['stddev']))." seconds"))." to ".
+ date("H:i",strtotime($trip['arrival_time']." + ".(floor($ETAparams['lol']['stddev']))." seconds"));
+ }
+ //print_r($tripETA);
+}
foreach ($trips as $trip) {
if (
isset($filterHasStop) && (getTripHasStop($trip['trip_id'], $filterHasStop) == 1)
@@ -152,6 +176,9 @@
$destination = getTripDestination($trip['trip_id']);
echo '<a href="trip.php?stopid=' . $stopid . '&tripid=' . $trip['trip_id'] . '"><h3>' . $trip['route_short_name'] . " towards " . $destination['stop_name'] . "</h3><p>";
$viaPoints = viaPointNames($trip['trip_id'], $trip['stop_sequence']);
+if ($labs) {
+ echo '<br><span class="eta">ETA: ' . $tripETA[$trip['trip_id']] . '</span>';
+ }
if ($viaPoints != "")
echo '<br><span class="viaPoints">Via: ' . $viaPoints . '</span>';
if (sizeof($tripStopNumbers) > 0) {