--- a/origin-src/transitfeed-1.2.6/gtfsscheduleviewer/marey_graph.py +++ b/origin-src/transitfeed-1.2.6/gtfsscheduleviewer/marey_graph.py @@ -1,1 +1,470 @@ - +#!/usr/bin/python2.5 +# +# Copyright (C) 2007 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Output svg/xml data for a marey graph + +Marey graphs are a visualization form typically used for timetables. Time +is on the x-axis and position on the y-axis. This module reads data from a +transitfeed.Schedule and creates a marey graph in svg/xml format. The graph +shows the speed between stops for each trip of a route. + +TODO: This module was taken from an internal Google tool. It works but is not +well intergrated into transitfeed and schedule_viewer. Also, it has lots of +ugly hacks to compensate set canvas size and so on which could be cleaned up. + +For a little more information see (I didn't make this URL ;-) +http://transliteracies.english.ucsb.edu/post/research-project/research-clearinghouse-individual/research-reports/the-indexical-imagination-marey%e2%80%99s-graphic-method-and-the-technological-transformation-of-writing-in-the-nineteenth-century + + MareyGraph: Class, keeps cache of graph data and graph properties + and draws marey graphs in svg/xml format on request. + +""" + +import itertools +import transitfeed + + +class MareyGraph: + """Produces and caches marey graph from transit feed data.""" + + _MAX_ZOOM = 5.0 # change docstring of ChangeScaleFactor if this changes + _DUMMY_SEPARATOR = 10 #pixel + + def __init__(self): + # Timetablerelated state + self._cache = str() + self._stoplist = [] + self._tlist = [] + self._stations = [] + self._decorators = [] + + # TODO: Initialize default values via constructor parameters + # or via a class constants + + # Graph properties + self._tspan = 30 # number of hours to display + self._offset = 0 # starting hour + self._hour_grid = 60 # number of pixels for an hour + self._min_grid = 5 # number of pixels between subhour lines + + # Canvas properties + self._zoomfactor = 0.9 # svg Scaling factor + self._xoffset = 0 # move graph horizontally + self._yoffset = 0 # move graph veritcally + self._bgcolor = "lightgrey" + + # height/width of graph canvas before transform + self._gwidth = self._tspan * self._hour_grid + + def Draw(self, stoplist=None, triplist=None, height=520): + """Main interface for drawing the marey graph. + + If called without arguments, the data generated in the previous call + will be used. New decorators can be added between calls. + + Args: + # Class Stop is defined in transitfeed.py + stoplist: [Stop, Stop, ...] + # Class Trip is defined in transitfeed.py + triplist: [Trip, Trip, ...] + + Returns: + # A string that contain a svg/xml web-page with a marey graph. + " <svg width="1440" height="520" version="1.1" ... " + """ + output = str() + if not triplist: + triplist = [] + if not stoplist: + stoplist = [] + + if not self._cache or triplist or stoplist: + self._gheight = height + self._tlist=triplist + self._slist=stoplist + self._decorators = [] + self._stations = self._BuildStations(stoplist) + self._cache = "%s %s %s %s" % (self._DrawBox(), + self._DrawHours(), + self._DrawStations(), + self._DrawTrips(triplist)) + + + + output = "%s %s %s %s" % (self._DrawHeader(), + self._cache, + self._DrawDecorators(), + self._DrawFooter()) + return output + + def _DrawHeader(self): + svg_header = """ + <svg width="%s" height="%s" version="1.1" + xmlns="http://www.w3.org/2000/svg"> + <script type="text/ecmascript"><![CDATA[ + function init(evt) { + if ( window.svgDocument == null ) + svgDocument = evt.target.ownerDocument; + } + var oldLine = 0; + var oldStroke = 0; + var hoffset= %s; // Data from python + + function parseLinePoints(pointnode){ + var wordlist = pointnode.split(" "); + var xlist = new Array(); + var h; + var m; + // TODO: add linebreaks as appropriate + var xstr = " Stop Times :"; + for (i=0;i<wordlist.length;i=i+2){ + var coord = wordlist[i].split(","); + h = Math.floor(parseInt((coord[0])-20)/60); + m = parseInt((coord[0]-20))%%60; + xstr = xstr +" "+ (hoffset+h) +":"+m; + } + + return xstr; + } + + function LineClick(tripid, x) { + var line = document.getElementById(tripid); + if (oldLine) + oldLine.setAttribute("stroke",oldStroke); + oldLine = line; + oldStroke = line.getAttribute("stroke"); + + line.setAttribute("stroke","#fff"); + + var dynTxt = document.getElementById("dynamicText"); + var tripIdTxt = document.createTextNode(x); + while (dynTxt.hasChildNodes()){ + dynTxt.removeChild(dynTxt.firstChild); + } + dynTxt.appendChild(tripIdTxt); + } + ]]> </script> + <style type="text/css"><![CDATA[ + .T { fill:none; stroke-width:1.5 } + .TB { fill:none; stroke:#e20; stroke-width:2 } + .Station { fill:none; stroke-width:1 } + .Dec { fill:none; stroke-width:1.5 } + .FullHour { fill:none; stroke:#eee; stroke-width:1 } + .SubHour { fill:none; stroke:#ddd; stroke-width:1 } + .Label { fill:#aaa; font-family:Helvetica,Arial,sans; + text-anchor:middle } + .Info { fill:#111; font-family:Helvetica,Arial,sans; + text-anchor:start; } + ]]></style> + <text class="Info" id="dynamicText" x="0" y="%d"></text> + <g id="mcanvas" transform="translate(%s,%s)"> + <g id="zcanvas" transform="scale(%s)"> + + """ % (self._gwidth + self._xoffset + 20, self._gheight + 15, + self._offset, self._gheight + 10, + self._xoffset, self._yoffset, self._zoomfactor) + + return svg_header + + def _DrawFooter(self): + return "</g></g></svg>" + + def _DrawDecorators(self): + """Used to draw fancy overlays on trip graphs.""" + return " ".join(self._decorators) + + def _DrawBox(self): + tmpstr = """<rect x="%s" y="%s" width="%s" height="%s" + fill="lightgrey" stroke="%s" stroke-width="2" /> + """ % (0, 0, self._gwidth + 20, self._gheight, self._bgcolor) + return tmpstr + + def _BuildStations(self, stoplist): + """Dispatches the best algorithm for calculating station line position. + + Args: + # Class Stop is defined in transitfeed.py + stoplist: [Stop, Stop, ...] + # Class Trip is defined in transitfeed.py + triplist: [Trip, Trip, ...] + + Returns: + # One integer y-coordinate for each station normalized between + # 0 and X, where X is the height of the graph in pixels + [0, 33, 140, ... , X] + """ + stations = [] + dists = self._EuclidianDistances(stoplist) + stations = self._CalculateYLines(dists) + return stations + + def _EuclidianDistances(self,slist): + """Calculate euclidian distances between stops. + + Uses the stoplists long/lats to approximate distances + between stations and build a list with y-coordinates for the + horizontal lines in the graph. + + Args: + # Class Stop is defined in transitfeed.py + stoplist: [Stop, Stop, ...] + + Returns: + # One integer for each pair of stations + # indicating the approximate distance + [0,33,140, ... ,X] + """ + e_dists2 = [transitfeed.ApproximateDistanceBetweenStops(stop, tail) for + (stop,tail) in itertools.izip(slist, slist[1:])] + + return e_dists2 + + def _CalculateYLines(self, dists): + """Builds a list with y-coordinates for the horizontal lines in the graph. + + Args: + # One integer for each pair of stations + # indicating the approximate distance + dists: [0,33,140, ... ,X] + + Returns: + # One integer y-coordinate for each station normalized between + # 0 and X, where X is the height of the graph in pixels + [0, 33, 140, ... , X] + """ + tot_dist = sum(dists) + if tot_dist > 0: + pixel_dist = [float(d * (self._gheight-20))/tot_dist for d in dists] + pixel_grid = [0]+[int(pd + sum(pixel_dist[0:i])) for i,pd in + enumerate(pixel_dist)] + else: + pixel_grid = [] + + return pixel_grid + + def _TravelTimes(self,triplist,index=0): + """ Calculate distances and plot stops. + + Uses a timetable to approximate distances + between stations + + Args: + # Class Trip is defined in transitfeed.py + triplist: [Trip, Trip, ...] + # (Optional) Index of Triplist prefered for timetable Calculation + index: 3 + + Returns: + # One integer for each pair of stations + # indicating the approximate distance + [0,33,140, ... ,X] + """ + + def DistanceInTravelTime(dep_secs, arr_secs): + t_dist = arr_secs-dep_secs + if t_dist<0: + t_dist = self._DUMMY_SEPARATOR # min separation + return t_dist + + if not triplist: + return [] + + if 0 < index < len(triplist): + trip = triplist[index] + else: + trip = triplist[0] + + t_dists2 = [DistanceInTravelTime(stop[3],tail[2]) for (stop,tail) + in itertools.izip(trip.GetTimeStops(),trip.GetTimeStops()[1:])] + return t_dists2 + + def _AddWarning(self, str): + print str + + def _DrawTrips(self,triplist,colpar=""): + """Generates svg polylines for each transit trip. + + Args: + # Class Trip is defined in transitfeed.py + [Trip, Trip, ...] + + Returns: + # A string containing a polyline tag for each trip + ' <polyline class="T" stroke="#336633" points="433,0 ...' + """ + + stations = [] + if not self._stations and triplist: + self._stations = self._CalculateYLines(self._TravelTimes(triplist)) + if not self._stations: + self._AddWarning("Failed to use traveltimes for graph") + self._stations = self._CalculateYLines(self._Uniform(triplist)) + if not self._stations: + self._AddWarning("Failed to calculate station distances") + return + + stations = self._stations + tmpstrs = [] + servlist = [] + for t in triplist: + if not colpar: + if t.service_id not in servlist: + servlist.append(t.service_id) + shade = int(servlist.index(t.service_id) * (200/len(servlist))+55) + color = "#00%s00" % hex(shade)[2:4] + else: + color=colpar + + start_offsets = [0] + first_stop = t.GetTimeStops()[0] + + for j,freq_offset in enumerate(start_offsets): + if j>0 and not colpar: + color="purple" + scriptcall = 'onmouseover="LineClick(\'%s\',\'Trip %s starting %s\')"' % (t.trip_id, + t.trip_id, transitfeed.FormatSecondsSinceMidnight(t.GetStartTime())) + tmpstrhead = '<polyline class="T" id="%s" stroke="%s" %s points="' % \ + (str(t.trip_id),color, scriptcall) + tmpstrs.append(tmpstrhead) + + for i, s in enumerate(t.GetTimeStops()): + arr_t = s[0] + dep_t = s[1] + if arr_t is None or dep_t is None: + continue + arr_x = int(arr_t/3600.0 * self._hour_grid) - self._hour_grid * self._offset + dep_x = int(dep_t/3600.0 * self._hour_grid) - self._hour_grid * self._offset + tmpstrs.append("%s,%s " % (int(arr_x+20), int(stations[i]+20))) + tmpstrs.append("%s,%s " % (int(dep_x+20), int(stations[i]+20))) + tmpstrs.append('" />') + return "".join(tmpstrs) + + def _Uniform(self, triplist): + """Fallback to assuming uniform distance between stations""" + # This should not be neseccary, but we are in fallback mode + longest = max([len(t.GetTimeStops()) for t in triplist]) + return [100] * longest + + def _DrawStations(self, color="#aaa"): + """Generates svg with a horizontal line for each station/stop. + + Args: + # Class Stop is defined in transitfeed.py + stations: [Stop, Stop, ...] + + Returns: + # A string containing a polyline tag for each stop + " <polyline class="Station" stroke="#336633" points="20,0 ..." + """ + stations=self._stations + tmpstrs = [] + for y in stations: + tmpstrs.append(' <polyline class="Station" stroke="%s" \ + points="%s,%s, %s,%s" />' %(color,20,20+y+.5,self._gwidth+20,20+y+.5)) + return "".join(tmpstrs) + + def _DrawHours(self): + """Generates svg to show a vertical hour and sub-hour grid + + Returns: + # A string containing a polyline tag for each grid line + " <polyline class="FullHour" points="20,0 ..." + """ + tmpstrs = [] + for i in range(0, self._gwidth, self._min_grid): + if i % self._hour_grid == 0: + tmpstrs.append('<polyline class="FullHour" points="%d,%d, %d,%d" />' \ + % (i + .5 + 20, 20, i + .5 + 20, self._gheight)) + tmpstrs.append('<text class="Label" x="%d" y="%d">%d</text>' + % (i + 20, 20, + (i / self._hour_grid + self._offset) % 24)) + else: + tmpstrs.append('<polyline class="SubHour" points="%d,%d,%d,%d" />' \ + % (i + .5 + 20, 20, i + .5 + 20, self._gheight)) + return "".join(tmpstrs) + + def AddStationDecoration(self, index, color="#f00"): + """Flushes existing decorations and highlights the given station-line. + + Args: + # Integer, index of stop to be highlighted. + index: 4 + # An optional string with a html color code + color: "#fff" + """ + tmpstr = str() + num_stations = len(self._stations) + ind = int(index) + if self._stations: + if 0<ind<num_stations: + y = self._stations[ind] + tmpstr = '<polyline class="Dec" stroke="%s" points="%s,%s,%s,%s" />' \ + % (color, 20, 20+y+.5, self._gwidth+20, 20+y+.5) + self._decorators.append(tmpstr) + + def AddTripDecoration(self, triplist, color="#f00"): + """Flushes existing decorations and highlights the given trips. + + Args: + # Class Trip is defined in transitfeed.py + triplist: [Trip, Trip, ...] + # An optional string with a html color code + color: "#fff" + """ + tmpstr = self._DrawTrips(triplist,color) + self._decorators.append(tmpstr) + + def ChangeScaleFactor(self, newfactor): + """Changes the zoom of the graph manually. + + 1.0 is the original canvas size. + + Args: + # float value between 0.0 and 5.0 + newfactor: 0.7 + """ + if float(newfactor) > 0 and float(newfactor) < self._MAX_ZOOM: + self._zoomfactor = newfactor + + def ScaleLarger(self): + """Increases the zoom of the graph one step (0.1 units).""" + newfactor = self._zoomfactor + 0.1 + if float(newfactor) > 0 and float(newfactor) < self._MAX_ZOOM: + self._zoomfactor = newfactor + + def ScaleSmaller(self): + """Decreases the zoom of the graph one step(0.1 units).""" + newfactor = self._zoomfactor - 0.1 + if float(newfactor) > 0 and float(newfactor) < self._MAX_ZOOM: + self._zoomfactor = newfactor + + def ClearDecorators(self): + """Removes all the current decorators. + """ + self._decorators = [] + + def AddTextStripDecoration(self,txtstr): + tmpstr = '<text class="Info" x="%d" y="%d">%s</text>' % (0, + 20 + self._gheight, txtstr) + self._decorators.append(tmpstr) + + def SetSpan(self, first_arr, last_arr, mint=5 ,maxt=30): + s_hour = (first_arr / 3600) - 1 + e_hour = (last_arr / 3600) + 1 + self._offset = max(min(s_hour, 23), 0) + self._tspan = max(min(e_hour - s_hour, maxt), mint) + self._gwidth = self._tspan * self._hour_grid +