Updated 2017-12-21 23:24:47 by kpv

GWL 2017-12-20 : The code is not working because NOAA has moved the service and changed the WSDL. This would be a nice app to show case using TclWS and I or someone else may update it to use it.

KPV 2017-12-21 : Fixed: I updated this almost a year ago but forgot to upload the new version.

Keith Vetter 2007-01-23 : The National Oceanic and Atmospheric Administration (NOAA) has a some nice web services providing current weather conditions and forecasts. For details on some of those services, check out [1] and [2].

Here's a little program that gets the weather forecast for a given latitude and longitude. It parses the SOAP reply and displays the result.

KPV 2007-02-03 : Added some more features including graphing predicted temperatures (using tklib's PlotChart), a few built-in cities and more robust XML handling.

KPV 2011-09-29 : Updated NOAA's url

PDH 2007-02-15 Corrected tempeture to temperature on line 44, and (pedantically) Januayr to January on line 4. This is an impressive app that really needs a screenshot to show it off. That said, I don't understand why the forecast days scroll horizontally instead of vertically, but that's easily corrected. Replace lines 359-362 with these:
      if {[incr row] > 1} {
           incr col
           set row 0

This is the app I would have written had I the proper mojo.

KPV 2009-08-27 -- vertical is better but you then have to figure out the correct row to start in because day 1 may have only one entry.

S_M 2007-07-04 I also like and use this application, at first I did not understand the temperature ranges for the day and night. Replacing the line 350 (set txt "$WEATHER($key3,$id2,temp,minimum)\xB0 -...) with:
       if {[regexp -nocase "night" $WEATHER($key,$id,name)]} {
            set txt "Low $WEATHER($key3,$id2,temp,minimum)\xB0"
       } else {
            set txt "High $WEATHER($key2,$id2,temp,maximum)\xB0"

will make it more similar to the forecast on the NOAA page.

spacecowboy - 2009-08-21 00:20:47

I have messed with this ndfdXML.htm file for HOURS... I finally had to drop a copy of nusoap.php in the same folder as the aforementioned file and the ndfdXMLclient.php file... the error "Parse error: syntax error, unexpected T_REQUIRE_ONCE in C:\Inetpub\vhosts\worldnewsvine.com\httpdocs\nws\ndfdSOAPclientByDay.php on line 55" finally disappeared by simply changing the runonce statement to 'nusoap.php'

Now I am back to another error I was getting which is:

Warning: Cannot modify header information - headers already sent by (output started at C:\Inetpub\vhosts\worldnewsvine.com\httpdocs\nws\ndfdXMLclient.php:1) in C:\Inetpub\vhosts\worldnewsvine.com\httpdocs\nws\ndfdXMLclient.php on line 111
 (which is this line of code: header("Content-Type: text/xml");... what is it supposed to be? // Send the appropriate mime type for XML isn't text/xml correct?

And to finish that all off, the error continues with:

ERROR HTTP Error: Unsupported HTTP response status 404 Not Found (soapclient->response has contents of the response)

Okay.. I am not professing to be a programmer but one would think that this would be easier to figure-out than this...

Right now I am just using simplepie to fetch the rss feed however, I would love to get this mapping function/application running...

Please any help, in plain ole english.... thanks

KPV 2009-08-27 - huh? what are ndfdXML.htm, nusoap.php and ndfdXMLclient.php? Are you really running this app or some other php one?

KPV 2009-08-27 - while trying to figure out the above error, I decided to replace everything with the more current code on my machine. Some of the changes include caching icon images (turned off for demoing); noon markers on temperature graph; the two suggestions from above; etc.
# noaa.tcl -- Displays weather forecast from NOAA
# by Keith Vetter, January 2007
# 2017-02-22: https protocol

package require Tk
package require http
package require tdom
package require Img
package require Plotchart
package require tile
namespace import -force ::ttk::button
package require tooltip
package require tls
http::register https 443 [list ::tls::socket -tls1 1]   ;# "-tls1 1" is required since [POODLE]

set S(noCache) 0
set S(mustFetch) 1
set S(iconDir) ~/bin/noaaIcons
set S(x,axisStep) 24
set S(box,size) 3

# see http://www.nws.noaa.gov/xml/
# http://www.weather.gov/forecasts/xml/SOAP_server/ndfdSOAPByDay.htm
set S(url,forecast) https://graphical.weather.gov/xml/SOAP_server/ndfdSOAPclientByDay.php
set S(url,temp)     https://graphical.weather.gov/xml/SOAP_server/ndfdXMLclient.php
set S(url,temp,parameters) {?lat=${LAT}&lon=${LON}&product=time-series&begin=${BEGINDATE}T00%3A00%3A00&end=${ENDDATE}T00%3A00%3A00&temp=temp&Submit=Submit}

# Both forecast and current conditions
# http://forecast.weather.gov/MapClick.php?lat=37.4411&lon=-122.1203&unit=0&lg=english&FcstType=dwml

set S(format) 12+hourly
set S(days) 8
set COLORS {lightblue violet}                   ;# Temperature day's columns
set COLORS {\#82eeee \#ee82ee \#eeee82 \#8282ee \#82ee82 \#ee8282}
set COLORS {lightblue}

array set CITIES {
    "Boston, MA"  "42.35 -71.066666"
    "Boulder, CO" "40.27 -105.252"
    "Chicago, IL"  "41.8675 -87.6243"
    "Denver, CO" "39.75 -104.98"
    "Granville, OH" "40.068088 -82.517967"
    "Honolulu, HI" "21.31 -157.83"
    "Leland, MI" "45.024361 -85.762431"
    "Los Angeles, CA"  "34.054 -118.245"
    "Mt View, CA" "37.392778 -122.041944"
    "New York, NY"  "40.7563 -73.9865"
    "Palmer, AK" "61.6019 -149.1172"
    "Providence, RI" "41.82355 -71.422132"
    "San Francisco, CA" "37.77 -122.43"
    "Washington, DC"  "38.9136 -77.0132"
    "Woods Hole, MA"  "41.52645 -70.6545"

proc Submit {who} {
    set ll [PrettyLat $::S(lat) $::S(lon)]
    if {$who eq "temperature"} {
        set ::S(msg) "Fetching NOAA temperature forecast"
        set n [GetNOAATemp $::S(lat) $::S(lon)]
        if {$n} {
            set ::S(msg) "NOAA temperature forecast for $ll"
        } else { set ::S(msg) "error fetching NOAA temperature forecast" }
    } else {
        set ::S(msg) "Fetching NOAA weather forecast"
        set n [GetNOAA $::S(lat) $::S(lon)]
        if {$n} {
            set ::S(msg) "NOAA weather forecast for $ll"
        } else { set ::S(msg) "error fetching NOAA weather forecast" }


proc GetNOAA {lat lon {XML ""}} {
    global doc root xml

    if {$XML ne ""} {
        set xml $XML
    } else {
        set xml [GetForecastXML $lat $lon]

    set n [catch {dom parse $xml doc}]
    if {$n} {
        tk_messageBox -icon error -message "Bad reply from NOAA"
        return 0
    set root [$doc documentElement]
    ReadTimeLayouts $root
    GetIcons $root
    GetTemperatures $root
    GetPrecipitation $root
    GetWeather $root

    unset doc
    return 1
proc GetForecastXML {lat lon} {
    set startdate [clock format [clock scan now] -format "%Y-%m-%d"]
    set url $::S(url,forecast)
    append url "?lat=$lat&lon=$lon&format=$::S(format)&startDate=$startdate"
    append url "&numDays=$::S(days)&Submit=Submit"
    set ::URL $url

    set token [::http::geturl $url]
    set ncode [::http::ncode $token]
    set xml [::http::data $token] ; list
    ::http::cleanup $token

    return $xml
proc GetNOAATemp {lat lon {XML ""}} {
    global doc root xml

    if {$XML ne ""} {
        set xml $XML
    } else {
        set xml [GetTempForecastXML $lat $lon]

    set n [catch {dom parse $xml doc}]
    if {$n} {
        tk_messageBox -icon error -message "Bad reply from NOAA"
        return 0
    set root [$doc documentElement]
    ReadTimeLayouts $root
    GetTemperatures $root

    unset doc
    return 1
proc GetTempForecastXML {lat lon} {
    global S url

    if {! [string is double $lat] || ! [string is double $lon]} {
        error "Bad latitude or longitude ($lat,$lon)"

    set LAT $lat
    set LON $lon
    set BEGINDATE [clock format [clock scan now] -format "%Y-%m-%d"]
    set ENDDATE [clock format [clock scan "now + $S(days) days"] \
                     -format "%Y-%m-%d"]

    set params [subst -nobackslashes -nocommands $S(url,temp,parameters)]
    set url "$S(url,temp)$params"

    set token [::http::geturl $url]
    ::http::wait $token
    set xml [::http::data $token] ; list
    ::http::cleanup $token

    return $xml

proc ReadTimeLayouts {root} {
    global WEATHER

    unset -nocomplain WEATHER

    # <time-layout summarization='12hourly'>
    #   <layout-key>KEY</layout-key>
    #   <start-valid-time period-name='NAME'>...</start-valid-time>
    #   <end-valid-time>...</end-valid-time>
    #   <start-valid-time>...</start-valid-time>
    #   <end-valid-time>...</end-valid-time>
    set nodes [$root selectNodes /dwml/data/time-layout]
    foreach node $nodes {
        set key [[$node selectNodes layout-key/text()] data]
        set WEATHER($key,summary) [$node getAttribute summarization "???"]

        set starts [$node selectNodes start-valid-time]
        set ends [$node selectNodes end-valid-time]
        set cnt -1
        foreach start $starts end $ends {
            incr cnt

            set name ""
            set etime ""
            if {[$start hasAttribute period-name]} {
                set name [$start getAttribute period-name "???"]
            set stime [[$start firstChild] data]
            if {$end ne ""} {
                set etime [[$end firstChild] data]

            set WEATHER($key,$cnt,name) $name
            set WEATHER($key,$cnt,start) $stime
            set WEATHER($key,$cnt,end) $etime

proc GetIcons {root} {
    set node [$root selectNodes /dwml/data/parameters/conditions-icon]
    set key [$node getAttribute time-layout]
    set ::WEATHER(icon,key) $key

    set nodes [$node selectNodes icon-link]
    for {set cnt 0} {$cnt < [llength $nodes]} {incr cnt} {
        set url ""
        set inode [lindex $nodes $cnt]
        if {[$inode hasChildNodes]} {
            set url [[$inode firstChild] data]
        set ::WEATHER($key,$cnt,icon) $url

proc GetTemperatures {root} {
    global WEATHER

    array unset WEATHER *temp*
    set nodes [$root selectNodes /dwml/data/parameters/temperature]
    foreach node $nodes {
        set type [$node getAttribute type]
        set units [$node getAttribute units]
        set key [$node getAttribute time-layout]
        set WEATHER(temp,$type,key) $key
        set WEATHER(temp,$type,units) $units

        set vnodes [$node selectNodes value]
        for {set cnt 0} {$cnt < [llength $vnodes]} {incr cnt} {
            set vnode [lindex $vnodes $cnt]
            set temp "?"
            if {[$vnode hasChildNodes]} {
                set temp [[$vnode firstChild] data]
            set WEATHER($key,$cnt,temp,$type) $temp
proc GetPrecipitation {root} {
    global WEATHER

    array unset WEATHER *rain*
    set node [$root selectNodes \

    set units [$node getAttribute units]
    set key [$node getAttribute time-layout]
    set WEATHER(rain,key) $key
    set WEATHER(rain,units) $units

    set vnodes [$node selectNodes value]
    for {set cnt 0} {$cnt < [llength $vnodes]} {incr cnt} {
        set vnode [lindex $vnodes $cnt]
        set rain "?"
        if {[$vnode hasChildNodes]} {
            set rain [[$vnode firstChild] data]
        set WEATHER($key,$cnt,rain) $rain
proc GetWeather {root} {
    global WEATHER

    array unset WEATHER *weather*
    set node [$root selectNodes /dwml/data/parameters/weather]

    set key [$node getAttribute time-layout]
    set WEATHER(weather,key) $key

    set cnt -1
    foreach value [$node selectNodes weather-conditions] {
        incr cnt
        set WEATHER($key,$cnt,weather,summary) \
            [$value getAttribute weather-summary "?"]

proc DoDisplay {} {
    wm title . "NOAA Weather Forecast"

    bind all <F2> {console show}
    frame .w -bd 2 -relief ridge
    frame .ctrl -bd 2 -relief ridge -pady 5 -padx 30
    label .msg -bd 2 -relief ridge -padx 30 -textvariable S(msg)

    pack .msg -side bottom -fill x
    pack .ctrl -side bottom -fill x
    pack .w -side top -fill both -expand 1

    set cities [lsort [array names ::CITIES]]
    ::ttk::combobox .ctrl.cb -values $cities -state readonly \
        -textvariable ::S(city) -validatecommand {SetCity %P} -validate all

    label .ctrl.llat -text "Latitude" -anchor w
    entry .ctrl.elat -textvariable ::S(lat) -width 12 \
        -validate key -vcmd {string is double %P}
    label .ctrl.llon -text "Longitude" -anchor w
    entry .ctrl.elon -textvariable ::S(lon) -width 12 \
        -validate key -vcmd {string is double %P}
    label .ctrl.ldays -text "Days" -anchor w
    spinbox .ctrl.sbox -from 1 -to 10 -textvariable ::S(days) -width 7 \
        -justify c -state readonly
    .ctrl.sbox config -readonlybackground [.ctrl.sbox cget -bg]
    frame .buttons
    button .forecast -text "Forecast" -command {Submit forecast}
    button .temp -text "Temperatures" -command {Submit temperature}

    grid x .ctrl.cb - x .buttons -pady {0 5} -sticky news
    grid x .ctrl.llat .ctrl.elat x ^ -sticky ew
    grid x .ctrl.llon .ctrl.elon x ^ -sticky ew
    grid x .ctrl.ldays .ctrl.sbox x ^ -sticky ew -pady {5 0}
    grid columnconfigure .ctrl 3 -minsize 30
    grid columnconfigure .ctrl 0 -weight 1
    grid columnconfigure .ctrl 100 -weight 1

    grid .forecast -in .buttons -sticky ew
    grid .temp -in .buttons -sticky ew
    grid rowconfigure .buttons {0 1} -weight 1

    eval destroy [winfo child .w]
    label .w.icon -image ::img::noaa
    label .w.title1 -text "NOAA" -font {Times 32 bold}
    label .w.title2 -text "Weather Forecast" -font {Times 28 bold}

    #grid .w.icon .w.title1
    #grid .w.title2 - -sticky ew -padx 10
    #grid config .w.icon -padx {30 0}
    #grid columnconfigure .w 1 -weight 1
    pack .w.title2 -side bottom -padx 10
    pack .w.icon -side left -padx {30 0}
    pack .w.title1 -side left -expand 1
proc SetCity {where} {
    global CITIES S

    foreach {S(lat) S(lon)} $CITIES($where) break
    return 1

image create photo ::img::noaa -data {
image create photo ::img::noaaLogo -data {

proc DisplayWeather {} {
    global WEATHER

    wm geom . {}                                ;# Reset main window geometry
    wm iconphoto . -default ::img::noaaLogo
    set W .w
    label $W.tmp
    set font "[font actual [$W.tmp cget -font]] -weight bold"
    eval destroy [winfo child $W]
    pack [frame $W.f] -side left -fill both -expand 1
    set W $W.f

    set keyWeather $WEATHER(weather,key)
    set keyMaxTemp $WEATHER(temp,maximum,key)
    set keyMinTemp $WEATHER(temp,minimum,key)
    set keyRain $WEATHER(rain,key)
    set keyIcon $WEATHER(icon,key)
    set row 0
    set col 0
    foreach arr [lsort -dictionary \
                     [array names WEATHER $keyWeather,*,weather,summary]] {
        set id [lindex [split $arr ","] 1]
        set id2 [expr {$id/2}]

        set WF $W.col$id
        frame $WF -bd 2 -relief ridge
        label $WF.name -text $WEATHER($keyWeather,$id,name) -font $font
        label $WF.icon -image [DownloadIcon $WEATHER($keyIcon,$id,icon)] \
            -relief ridge

        if {[regexp -nocase "night" $WEATHER($keyWeather,$id,name)]} {
            set txt "Low $WEATHER($keyMinTemp,$id2,temp,minimum)\xB0"
            if {$row == 0 && $col == 0} { incr row}
        } else {
            set txt "High $WEATHER($keyMaxTemp,$id2,temp,maximum)\xB0"
        append txt "\n$WEATHER($keyRain,$id,rain)%"
        append txt "\n$WEATHER($keyWeather,$id,weather,summary)"
        label $WF.txt -text $txt -wraplength 100

        grid $WF -row $row -column $col -sticky news
        grid columnconfigure $W $col -uniform a
        eval pack [winfo child $WF] -side top
        if {[incr row] > 1} {
            incr col
            set row 0
proc DownloadIcon {url} {
    if {$url eq ""} {return ::img::noaa}
    set cacheName [file join $::S(iconDir) [file tail $url]]
    set sname [file rootname [file tail $url]]
    set iname ::img::$sname
    if {[lsearch [image names] $iname] == -1} {
        image create photo $iname -width 55 -height 58
        if {! $::S(mustFetch) && [file exists $cacheName]} {
            $iname config -file $cacheName
        } else {
            $iname copy ::img::noaa
            set start [clock milliseconds]
            lappend ::ALL [list $url $iname $cacheName]
            after idle [list ::http::geturl $url \
                                -command [list DownloadIcon_Callback $iname $cacheName]]
            lappend ::TIMES [expr {[clock milliseconds] - $start}]

    return $iname
proc DownloadIcon_Callback {iname cacheName token} {
    set ncode [::http::ncode $token]

    if {[::http::ncode $token] != 200} {
        error "bad http ncode for $iname"
    } else {
        set data [::http::data $token] ; list
        $iname config -data [::http::data $token]
        if {! $::S(noCache)} {
            catch {
                set fout [open $cacheName wb]
                puts -nonewline $fout $data
                close $fout
    ::http::cleanup $token
proc ScanTime {when} {
    set ticks [clock scan "[string range $when 0 9] [string range $when 11 18]"]
    return $ticks
proc PrettyLat {lat lon} {
    set lat [int2lat $lat]
    set lon [int2lat $lon]
    foreach {lat1 lat2 lat3} $lat break
    foreach {lon1 lon2 lon3} $lon break
    set lat "$lat1\xB0 $lat2' $lat3\x22N"
    set lon "$lon1\xB0 $lon2' $lon3\x22W"

    return "$lat   $lon"
proc GetTempTime {hourOffset} {
    set seconds [expr {$::PLOT(basetime) + $hourOffset*60*60}]
    return [clock format $seconds -format "%a %l:%M %P"]
proc int2lat {int} {
    set int [expr {abs($int) * 3600}]

    if {[string is integer -strict $int]} {
        set sec [expr {$int % 60}]
    } else {
        #set fra [expr {$int - int($int)}]
        #set fra [expr {round($fra * 10) / 10.0}]
        #set int [expr {int($int)}]
        #set sec [expr {$int % 60 + $fra}]

        set v [expr {$int + .05}]               ;# Round to 1 decimal place
        foreach {int fra} [split $v "."] break  ;# Use string representation
        set fra [string range $fra 0 0]         ;# 1 decimal place only

        set sec [expr {$int % 60}]
        if {$fra ne {0}} { append sec ".$fra"}
    set int [expr {$int / 60}]
    set min [expr {$int % 60}]
    set deg [expr {$int / 60}]

    return [list $deg $min $sec]

proc PlotTemp {} {
    global PLOT s

    wm geom . {}                                ;# Reset main window geometry
    set W .w.c
    set PLOT(W) $W
    if {[winfo exists $W]} {
        $W config -width [winfo width $W] -height [winfo height $W]
        $W delete all
        bind $W <Configure> {}
    } else {
        eval destroy [winfo child .w]
        canvas $W -width 700
        pack $W -fill both -expand 1
    # Bug in plotchart
    ::Plotchart::clearcanvas $W
    array unset ::Plotchart::scaling *$W*
    array unset ::Plotchart::data_series *$W*
    array unset ::Plotchart::config *$W*

    set s [::Plotchart::createXYPlot $W $PLOT(XS) $PLOT(YS)]
    foreach x $PLOT(X) y $PLOT(Y) {
        $s plot series1 $x $y
        set xy [::Plotchart::coordsToPixel $W $x $y]
        set xy [Box $xy $::S(box,size)]
        set id [$W create oval $xy -tag oval -fill red -outline red]

        set when [GetTempTime $x]
        ::tooltip::tooltip $W -items $id "$y\xB0\n$when"

    $s grid $PLOT(Xgrid) $PLOT(Ygrid)
    $s title "Temperature Forecast"
    $s ytext $PLOT(YText)
    $W raise oval
    if {[bind $W <Configure>] eq ""} { bind $W <Configure> PlotTemp }
proc XAxis {} {
    global PLOT
    set W $PLOT(W)

    $W delete xaxis
    set Xticks [lindex $PLOT(Xgrid) 0]
    set Ymin [lindex $PLOT(YS) 0]
    for {set i 0} {$i < [llength $Xticks]} {incr i} {
        set x [lindex $Xticks $i]               ;# Hours from starting
        set day [expr {$x / 24.}]
        if {int($day) != $day} continue

        set ticks [expr {$PLOT(basetime) + int($day)*60*60*24}]
        set day [clock format $ticks -format "%a"]
        set xy [::Plotchart::coordsToPixel $W $x $Ymin]
        $W create text $xy -tag xaxis -anchor n -text $day
proc Freezing {} {
    global PLOT

    set W $PLOT(W)
    foreach {xmin xmax} $PLOT(XS) break
    foreach {ymin ymax} $PLOT(YS) break
    foreach val {32 0} {
        if {$ymin < $val && $ymax > $val} {
            set xy0 [::Plotchart::coordsToPixel $W $xmin $val]
            set xy1 [::Plotchart::coordsToPixel $W $xmax $val]
            $W create line [concat $xy0 $xy1] -fill red -dash 1 -width 2
proc Noons {} {
    global PLOT COLORS

    set W $PLOT(W)
    set xticks [lindex $PLOT(Xgrid) 0]
    foreach {ymin ymax} $PLOT(YS) break

    set x1 [lindex $xticks 0]
    for {set i 1} {$i < [llength $xticks]} {incr i} {
        set x0 $x1
        set x1 [lindex $xticks $i]
        set x [expr {($x0 + $x1)/2}]
        set xy0 [::Plotchart::coordsToPixel $W $x $ymin]
        set xy1 [::Plotchart::coordsToPixel $W $x $ymax]
        $W create line [concat $xy0 $xy1] -fill black -dash 1 -width 1

proc Colorize {} {
    global PLOT COLORS

    set W $PLOT(W)
    set xticks [lindex $PLOT(Xgrid) 0]
    foreach {ymin ymax} $PLOT(YS) break

    set x1 [lindex $xticks 0]
    for {set i 1} {$i < [llength $xticks]} {incr i} {
        set x0 $x1
        set clr [lindex $COLORS [expr {$i % [llength $COLORS]}]]
        set x1 [lindex $xticks $i]
        set xy0 [::Plotchart::coordsToPixel $W $x0 $ymin]
        set xy1 [::Plotchart::coordsToPixel $W $x1 $ymax]

        $W create rect [concat $xy0 $xy1] -fill $clr -tag bg

    $W lower bg
proc lat2int {lat1 lat2 lat3} {
    scan "$lat1 $lat2 $lat3" "%g %g %g" lat1 lat2 lat3
    set lat [expr {abs($lat1) + $lat2 / 60.0 + $lat3 / 3600.0}]
    return $lat

proc GetPlotData {} {
    global PLOT WEATHER

    unset -nocomplain PLOT
    set key $WEATHER(temp,hourly,key)
    set basetime 0

    set X {}
    set Y {}
    foreach arr [lsort -dictionary [array names WEATHER $key,*,temp,hourly]] {
        set idx [lindex [split $arr ","] 1]
        set ticks [ScanTime $WEATHER($key,$idx,start)]
        if {$idx == 0} {
            # Get start of the day for first time range
            set basetime [ScanTime [string range $WEATHER($key,$idx,start) 0 9]]
        lappend X [expr {($ticks - $basetime)/60/60}] ;# Hours from basetime
        lappend Y $WEATHER($key,$idx,temp,hourly)
    ;# Compute Y axis
    set y_sort [lsort -real $Y]
    set ys [::Plotchart::determineScale [lindex $y_sort 0] [lindex $y_sort end]]
    set ys [MakeInt $ys]

    set min 0
    set max [lindex $X end]
    set delta [expr {$max - $min}]
    if {$delta/24 != $delta/24.0} { set max [expr {$min + 24*(1+$delta/24)}]}
    set xs [list $min $max 24]
    set xs [list $min $max $::S(x,axisStep)]

    set Xticks {}
    foreach {a b c} $xs break
    while {$a <= $b} {
        lappend Xticks $a
        incr a $c
    set Yticks {}
    foreach {a b c} $ys break
    while {$a <= $b} {
        lappend Yticks $a
        incr a $c

    set Xgrid {}
    foreach . $Yticks { lappend Xgrid $Xticks }
    set Ygrid {}
    set cnt [llength $Xticks]
    foreach tick $Yticks { lappend Ygrid [string repeat "$tick " $cnt] }

    set PLOT(X) $X
    set PLOT(Y) $Y
    set PLOT(XS) $xs
    set PLOT(YS) $ys
    set PLOT(Xgrid) $Xgrid
    set PLOT(Ygrid) $Ygrid
    set PLOT(YText) $WEATHER(temp,hourly,units)
    set PLOT(basetime) $basetime
proc Box {xy r} {
    foreach {x y} $xy break
    return [list [expr {$x-$r}] [expr {$y-$r}] [expr {$x+$r}] [expr {$y+$r}]]
proc MakeInt {nlist} {
    set ilist {}
    foreach num $nlist { lappend ilist [expr {int($num)}]}
    return $ilist
if {! $S(noCache)} {catch {file mkdir $S(iconDir)}}
set S(city) "Woods Hole, MA"
set S(city) "Granville, OH"
set S(city) "Mt View, CA"
SetCity $S(city)
if {! $tcl_interactive} {
    Submit forecast