A graphical timetable

if 0 {Richard Suchenwirth 2007-09-16 - Graphical timetables visualize operations on a transit line, typically a railway. The X axis represents time of day, the Y axis the sequence of stations in distance scale. Lines indicate the movement of trains, with the slope indicating direction and speed, horizontal being stand-still.

WikiDbImage gratt.jpg

The public API is just an overloaded canvas named gratt, which accepts some extra configuration parameters, and has the extra add method. A global variable named like the widget is created, and upvar-ed to the "anonymous array" in all functions. }

 proc gratt {w args} {
    upvar #0 $w ""
    set (from)   0
    set (to)     24
    set (offset) 80
    canvas $w
    foreach {key value} $args {
        switch -- $key {
            -line   {set line $value}
            -from   {set (from)   $value}
            -to     {set (to)     $value}
            -offset {set (offset) $value}
            default {$w configure $key $value}
        }
    }
    gratt'draw $w $line
    rename $w _$w
    proc $w {cmd args} {
        set self [lindex [info level 0] 0]
        switch -- $cmd {
            add     {eval gratt'add $self $args}
            default {eval _$self $cmd $args}
        }
    }
    return $w
 }

#-- Internal routines: draw grid, label stations and hours

 proc gratt'draw {w line} {
    upvar #0 $w ""
    set x0 $(offset)
    set width [$w cget -width]
    set (dx) [expr {($width - $x0) / ($(to) - $(from))}]
    set height [$w cget -height]
    for {set h $(from); set x $x0} {$h <= $(to)} {incr h; incr x $(dx)} {
        $w create text $x 8 -text $h
        $w create line $x 16 $x [expr {$height-8}] -fill gray
    }
    incr x -$(dx)
    set yfac [expr {double($height-24)/[lindex $line end]}]
    foreach {name distance} $line {
        set ($name) [expr {$distance*$yfac+16}]
        $w create text 5 $($name) -text "$name $distance" -anchor w
        $w create line $x0 $($name) $x $($name)
    }
 }

#-- Add a train

 proc gratt'add {w number schedule args} {
    upvar #0 $w ""
    set coords {}
    foreach {name times} $schedule {
        foreach time $times {
            lappend coords [gratt'x $w $time] $($name)
        }
    }
    eval $w create line $coords $args
 }

#-- convert a "military" time (HHMM) to x coordinate

 proc gratt'x {w time} {
    upvar #0 $w ""
    scan $time %d time ;# strip off leading zero
    set mins [expr {($time/100-$(from))*60+$time%100}]
    expr {double($(offset)) + $mins * $(dx)/60}
 }

if 0 {Here's a testing example: the fictional "Mason - Dixon line". Assume it is single-tracked, and trains can only cross in Mason-East and Springfield. We have regular trains (blue) and expresses (red). First, to declare the line with mileposts:}

 set md {Mason 0 Mason-East 4 Springfield 12 Dixon-West 18 Dixon 20}

 pack [gratt .g -line $md -from 5 -to 19 -bg white -width 720 -height 240]

#-- And now for the timetable. Westbound trains:

 .g add 1201 {
    Mason 0550 Mason-East {0556 0558} Springfield {0612 0618} Dixon 0639
 } -fill blue
 .g add 1203 {
    Mason 0655 Mason-East {0701 0703} Springfield {0714 0720}
    Dixon-West {0728 0730} Dixon 0741
 } -fill blue
 .g add X303 {Mason 0730 Springfield {0748 0750} Dixon 0811} -fill red
 .g add 1205 {
    Mason 0800 Mason-East {0806 0810} Springfield {0824 0830} Dixon 0853
 } -fill blue
 .g add 1207 {
    Mason 1000 Mason-East {1006 1008} Springfield {1022 1026} Dixon 1051
 } -fill blue
 .g add 1401 {Springfield 1200 Dixon-West {1210 1212} Dixon 1221} -fill blue
 .g add 1209 {
    Mason 1200 Mason-East {1206 1208} Springfield 1222 Dixon 1251
 } -fill blue
 .g add X305 {Mason 1330 Springfield {1348 1350} Dixon 1411} -fill red
 .g add 1211 {
    Mason 1500 Mason-East {1506 1508} Springfield {1522 1524} Dixon 1553
 } -fill blue
 .g add 1213 {
    Mason 1700 Mason-East {1706 1708} Springfield {1722 1724} Dixon 1753
 } -fill blue

#---------------------- Eastbound trains

 .g add 1200 {
    Dixon 0550 Dixon-West {0556 0558} Springfield {0612 0619}
    Mason-East {0628 0630} Mason 0643
 } -fill blue
 .g add 1202 {
    Dixon 0650 Dixon-West {0656 0658} Springfield {0712 0719}
    Mason-East {0728 0740} Mason 0753
 } -fill blue
 .g add 1204 {
    Dixon 0900 Dixon-West {0906 0908} Springfield {0922 0929}
    Mason-East {0936 0938} Mason 0951
 } -fill blue
 .g add 1206 {
    Dixon 1000 Dixon-West {1006 1008} Springfield {1022 1029}
    Mason-East {1036 1038} Mason 1051
 } -fill blue
 .g add 1408 {Dixon 1130 Dixon-West {1136 1138} Springfield 1146} -fill blue
 .g add X306 {Dixon 1100 Springfield {1122 1125} Mason 1143} -fill red
 .g add 1208 {
    Dixon 1330 Dixon-West {1336 1338} Springfield {1342 1353}
    Mason-East {1408 1410} Mason 1423
 } -fill blue
 .g add 1210 {
    Dixon 1600 Dixon-West {1606 1608} Springfield {1622 1629}
    Mason-East {1636 1638} Mason 1651
 } -fill blue
 .g add X308 {Dixon 1730 Springfield {1752 1755} Mason 1813} -fill red
 .g add 1212 {
    Dixon 1800 Dixon-West {1806 1808} Springfield {1822 1829}
    Mason-East {1836 1838} Mason 1851
 } -fill blue

if 0 {Looking at the diagram, one can deduce that at least three trainsets are needed: a regular one that stays in Dixon overnight (which does two full round-trips and the short Springfield runs around noon), and a regular and an express (if they differ in type) stationed in Mason. It's an interesting pastime to modify the timetable and see how the diagram changes ... :^)


}