1 ! Copyright (C) 2013 John Benediktsson
2 ! See http://factorcode.org/license.txt for BSD license
4 USING: accessors arrays ascii assocs calendar calendar.format
5 classes.tuple combinators command-line continuations csv
6 formatting grouping http.client io io.encodings.ascii io.files
7 io.styles kernel math math.extras math.functions math.parser
8 namespaces regexp sequences sorting.human splitting strings urls
13 TUPLE: station cccc name state country latitude longitude ;
19 ERROR: bad-location str ;
21 : parse-location ( str -- n )
22 "-" split dup length {
23 { 3 [ first3 [ string>number ] tri@ 60.0 / + 60.0 / + ] }
24 { 2 [ first2 [ string>number ] bi@ 60.0 / + ] }
25 { 1 [ first string>number ] }
29 : string>longitude ( str -- lon/f )
30 dup R/ \d+-\d+(-\d+(\.\d+)?)?[WE]/ matches? [
33 [ CHAR: W = [ neg ] when ] bi*
36 : string>latitude ( str -- lat/f )
37 dup R/ \d+-\d+(-\d+(\.\d+)?)?[NS]/ matches? [
40 [ CHAR: S = [ neg ] when ] bi*
43 : stations-data ( -- seq )
44 URL" http://tgftp.nws.noaa.gov/data/nsd_cccc.txt"
45 http-get nip CHAR: ; [ string>csv ] with-delimiter ;
49 MEMO: all-stations ( -- seq )
56 [ 7 swap nth string>latitude ]
57 [ 8 swap nth string>longitude ]
61 : all-stations. ( -- )
62 all-stations standard-table-style [
68 dup string? [ "%.2f" sprintf ] unless write
76 : find-by-cccc ( cccc -- station )
77 all-stations swap '[ cccc>> _ = ] find nip ;
79 : find-by-country ( country -- stations )
80 all-stations swap '[ country>> _ = ] filter ;
82 : find-by-state ( state -- stations )
83 all-stations swap '[ state>> _ = ] filter ;
87 TUPLE: metar-report type station timestamp modifier wind
88 visibility rvr weather sky-condition temperature dew-point
89 altimeter remarks raw ;
91 CONSTANT: pressure-tendency H{
92 { "0" "increasing then decreasing" }
93 { "1" "increasing more slowly" }
95 { "3" "increasing more quickly" }
97 { "5" "decreasing then increasing" }
98 { "6" "decreasing more slowly" }
100 { "8" "decreasing more quickly" }
103 CONSTANT: lightning H{
104 { "CA" "cloud-air lightning" }
105 { "CC" "cloud-cloud lightning" }
106 { "CG" "cloud-ground lightning" }
107 { "IC" "in-cloud lightning" }
114 { "DR" "low drifting" }
116 { "DU" "widespread dust" }
118 { "FC" "funnel clouds" }
123 { "GS" "small hail and/or snow pellets" }
125 { "IC" "ice crystals" }
127 { "PL" "ice pellets" }
128 { "PO" "well-developed dust/sand whirls" }
134 { "SG" "snow grains" }
139 { "TS" "thuderstorm" }
141 { "VA" "volcanic ash" }
144 MEMO: glossary ( -- assoc )
145 "vocab:metar/glossary.txt" ascii file-lines
146 [ "," split1 ] H{ } map>assoc ;
148 : parse-glossary ( str -- str' )
153 [ glossary ?at drop ] if
157 : parse-timestamp ( str -- str' )
158 [ now [ year>> ] [ month>> ] bi ] dip
159 2 cut 2 cut 2 cut drop [ string>number ] tri@
161 [ drop 0 ] dip 0 instant <timestamp> 1 days time+
163 0 instant <timestamp>
164 ] if timestamp>rfc822 ;
166 CONSTANT: compass-directions H{
186 : direction>compass ( direction -- compass )
187 22.5 round-to-step compass-directions at ;
189 : parse-compass ( str -- str' )
190 string>number [ direction>compass ] keep "%s (%s°)" sprintf ;
192 : parse-direction ( str -- str' )
193 dup "VRB" = [ drop "variable" ] [
194 parse-compass "from %s" sprintf
197 : kt>mph ( kt -- mph ) 1.15077945 * ;
199 : mph>kt ( mph -- kt ) 1.15077945 / ;
201 : parse-speed ( str units -- str'/f )
202 [ string>number ] dip '[
204 [ drop dup kt>mph "%s knots (%.1f mph)" sprintf ]
205 [ "%s %s" sprintf ] if
208 : parse-wind ( str -- str' )
209 dup "00000" head? [ drop "calm" ] [
210 "/" split1 [ 3 cut ] unless*
211 [ parse-direction ] dip {
212 { [ "KT" ?tail ] [ "knots" ] }
213 { [ "MPS" ?tail ] [ "meters per second" ] }
215 } cond [ "G" split1 ] dip '[ _ parse-speed ] bi@
216 [ "%s at %s with gusts to %s " sprintf ]
217 [ "%s at %s" sprintf ] if*
220 : parse-wind-variable ( str -- str' )
221 "V" split1 [ parse-compass ] bi@
222 ", variable from %s to %s" sprintf ;
224 : parse-visibility ( str -- str' )
227 { CHAR: M [ rest "less than " ] }
228 { CHAR: P [ rest "more than " ] }
231 CHAR: \s over index [ " " "+" replace ] when
232 string>number "%s%s statute miles" sprintf
236 { [ dup 800 < ] [ "%dm" sprintf ] }
237 { [ dup 5000 < ] [ 1000 /f "%.1fkm" sprintf ] }
238 { [ dup 9999 < ] [ 1000 /f "%dkm" sprintf ] }
239 [ drop "more than 10km" ]
249 ] { } map-as unclip-last
250 [ "-" join ] dip append " " glue
254 : parse-rvr ( str -- str' )
256 { [ "U" ?tail ] [ " with improvement" ] }
257 { [ "D" ?tail ] [ " with aggravation" ] }
258 { [ "N" ?tail ] [ " with no change" ] }
261 "R" ?head drop "/" split1 "FT" ?tail [
263 [ string>number ] bi@
264 "varying between %s and %s" sprintf
266 string>number "of %s" sprintf
267 ] if* "runway %s visibility %s" sprintf
268 ] dip " ft" " meters" ? append
271 : (parse-weather) ( str -- str' )
272 dup "+FC" = [ drop "tornadoes or waterspouts" ] [
274 { CHAR: + [ rest "heavy " ] }
275 { CHAR: - [ rest "light " ] }
278 2 group dup [ weather key? ] all?
279 [ [ weather at ] map join-words ]
280 [ concat parse-glossary ] if
284 : parse-weather ( str -- str' )
285 dup "VC" subseq-of? [ "VC" "" replace t ] [ f ] if
287 [ [ " in the vicinity" append ] when ] bi* ;
289 : parse-altitude ( str -- str' )
290 string>number " at %s00 ft" sprintf ;
296 { "SCT" "scattered" }
297 { "SKC" "clear sky" }
298 { "CLR" "clear sky" }
299 { "NSC" "clear sky" }
301 { "ACC" "altocumulus castellanus" }
302 { "ACSL" "standing lenticular altocumulus" }
303 { "CCSL" "cirrocumulus standing lenticular cloud" }
305 { "SC" "stratocumulus" }
306 { "SCSL" "stratocumulus standing lenticular cloud" }
307 { "TCU" "towering cumulus" }
310 : parse-sky-condition ( str -- str' )
315 [ sky at [ " (%s)" sprintf ] [ f ] if* ]
319 : F>C ( F -- C ) 32 - 5/9 * ;
321 : C>F ( C -- F ) 9/5 * 32 + ;
323 : parse-temperature ( str -- temp dew-point )
326 "M" ?head [ string>number ] [ [ neg ] when ] bi*
327 dup C>F "%d °C (%.1f °F)" sprintf
331 : parse-altimeter ( str -- str' )
332 unclip [ string>number ] [ CHAR: A = ] bi*
333 [ 100 /f "%.2f Hg" sprintf ] [ "%s hPa" sprintf ] if ;
335 CONSTANT: re-timestamp R/ \d{6}Z/
336 CONSTANT: re-station R/ \w{4}/
337 CONSTANT: re-temperature R/ [M]?\d{2}\/([M]?\d{2})?/
338 CONSTANT: re-wind R/ (VRB|\d{3})(\/\d+|\d{2,3})(G\d{2,3})?(KT|MPS)/
339 CONSTANT: re-wind-variable R/ \d{3}V\d{3}/
340 CONSTANT: re-visibility R/ ((\d+|[MP])?\d+(\/\d+)?SM|\d{4}[NSEW]{0,2})/
341 CONSTANT: re-rvr R/ R\d{2}[RLC]?\/[MP]?\d{4}(V\d{4})?(FT)?[UDN]?/
342 CONSTANT: re-weather R/ [+-]?(VC)?(\w{2}|\w{4})/
343 CONSTANT: re-sky-condition R/ (\w{2,3}\d{3}(\w+)?|\w{3}|CAVOK)/
344 CONSTANT: re-altimeter R/ [AQ]\d{4}/
346 : find-one ( seq quot: ( elt -- ? ) -- seq' elt/f )
347 dupd find [ [ swap remove-nth ] when* ] dip ; inline
349 : find-all ( seq quot: ( elt -- ? ) -- seq elts )
350 [ dupd find drop ] keep '[
352 [ dup ?first _ [ f ] if* ] [ unclip ] produce
356 : fix-visibility ( seq -- seq' )
357 dup [ R/ \d+(\/\d+)?SM/ matches? ] find drop [
358 dup 1 - pick ?nth [ R/ \d+/ matches? ] [ f ] if* [
359 cut [ unclip-last ] [ unclip swap ] bi*
360 [ " " glue 1array ] [ 3append ] bi*
364 : metar-body ( report seq -- report )
365 [ { "METAR" "SPECI" } member? ] find-one
366 [ pick type<< ] when*
368 [ re-station matches? ] find-one
369 [ pick station<< ] when*
371 [ re-timestamp matches? ] find-one
372 [ parse-timestamp pick timestamp<< ] when*
374 [ { "AUTO" "COR" } member? ] find-one
375 [ pick modifier<< ] when*
377 [ re-wind matches? ] find-one
378 [ parse-wind pick wind<< ] when*
380 [ re-wind-variable matches? ] find-one
381 [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
384 [ re-visibility matches? ] find-all
385 [ parse-visibility ] map ", " join pick visibility<<
387 [ re-rvr matches? ] find-all
388 [ parse-rvr ] map ", " join pick rvr<<
390 [ re-weather matches? ] find-all
391 [ parse-weather ] map ", " join pick weather<<
393 [ re-sky-condition matches? ] find-all
394 [ parse-sky-condition ] map ", " join pick sky-condition<<
396 [ re-temperature matches? ] find-one
399 [ pick temperature<< ]
400 [ pick dew-point<< ] bi*
403 [ re-altimeter matches? ] find-one
404 [ parse-altimeter pick altimeter<< ] when*
408 : signed-number ( sign value -- n )
409 [ string>number ] bi@ swap zero? [ neg ] unless 10.0 / ;
411 : single-value ( str -- str' )
412 1 cut signed-number ;
414 : double-value ( str -- m n )
415 1 cut 3 cut [ signed-number ] dip 1 cut signed-number ;
417 : parse-1hr-temp ( str -- str' )
418 "T" ?head drop dup length 4 > [
420 [ dup C>F "%.1f °C (%.1f °F)" sprintf ] bi@
421 "hourly temperature %s and dew point %s" sprintf
424 "hourly temperature %.1f °C (%.1f °F)" sprintf
427 : parse-6hr-max-temp ( str -- str' )
428 "1" ?head drop single-value dup C>F
429 "6-hour maximum temperature %.1f °C (%.1f °F)" sprintf ;
431 : parse-6hr-min-temp ( str -- str' )
432 "2" ?head drop single-value dup C>F
433 "6-hour minimum temperature %.1f °C (%.1f °F)" sprintf ;
435 : parse-24hr-temp ( str -- str' )
436 "4" ?head drop double-value
437 [ dup C>F "%.1f °C (%.1f °F)" sprintf ] bi@
438 "24-hour maximum temperature %s minimum temperature %s"
441 : parse-1hr-pressure ( str -- str' )
442 "5" ?head drop 1 cut single-value [ pressure-tendency at ] dip
443 "hourly pressure %s %s hPa" sprintf ;
445 : parse-snow-depth ( str -- str' )
446 "4/" ?head drop string>number "snow depth %s inches" sprintf ;
448 CONSTANT: low-clouds H{
449 { 1 "cumulus (fair weather)" }
450 { 2 "cumulus (towering)" }
451 { 3 "cumulonimbus (no anvil)" }
452 { 4 "stratocumulus (from cumulus)" }
453 { 5 "stratocumuls (not cumulus)" }
454 { 6 "stratus or Fractostratus (fair)" }
455 { 7 "fractocumulus / fractostratus (bad weather)" }
456 { 8 "cumulus and stratocumulus" }
457 { 9 "cumulonimbus (thunderstorm)" }
461 CONSTANT: mid-clouds H{
462 { 1 "altostratus (thin)" }
463 { 2 "altostratus (thick)" }
464 { 3 "altocumulus (thin)" }
465 { 4 "altocumulus (patchy)" }
466 { 5 "altocumulus (thickening)" }
467 { 6 "altocumulus (from cumulus)" }
468 { 7 "altocumulus (with altocumulus, altostratus, nimbostratus)" }
469 { 8 "altocumulus (with turrets)" }
470 { 9 "altocumulus (chaotic)" }
471 { -1 "above overcast" }
474 CONSTANT: high-clouds H{
475 { 1 "cirrus (filaments)" }
476 { 2 "cirrus (dense)" }
477 { 3 "cirrus (often with cumulonimbus)" }
478 { 4 "cirrus (thickening)" }
479 { 5 "cirrus / cirrostratus (low in sky)" }
480 { 6 "cirrus / cirrostratus (hi in sky)" }
481 { 7 "cirrostratus (entire sky)" }
482 { 8 "cirrostratus (partial)" }
483 { 9 "cirrocumulus or cirrocumulus / cirrus / cirrostratus" }
484 { -1 "above overcast" }
487 : parse-cloud-cover ( str -- str' )
488 "8/" ?head drop first3 [ CHAR: 0 - ] tri@
489 [ [ f ] [ low-clouds at "low clouds are %s" sprintf ] if-zero ]
490 [ [ f ] [ mid-clouds at "middle clouds are %s" sprintf ] if-zero ]
491 [ [ f ] [ high-clouds at "high clouds are %s" sprintf ] if-zero ]
492 tri* 3array join-words ;
494 : parse-inches ( str -- str' )
495 dup [ CHAR: / = ] all? [ drop "unknown" ] [
497 [ "trace" ] [ 100 /f "%.2f inches" sprintf ] if-zero
500 : parse-1hr-precipitation ( str -- str' )
501 "P" ?head drop parse-inches
502 "%s precipitation in last hour" sprintf ;
504 : parse-6hr-precipitation ( str -- str' )
505 "6" ?head drop parse-inches
506 "%s precipitation in last 6 hours" sprintf ;
508 : parse-24hr-precipitation ( str -- str' )
509 "7" ?head drop parse-inches
510 "%s precipitation in last 24 hours" sprintf ;
512 ! XXX: "on the hour" instead of "00 minutes past the hour" ?
514 : parse-recent-time ( str -- str' )
517 [ " minutes past the hour" append ] if ;
519 : parse-peak-wind ( str -- str' )
520 "/" split1 [ parse-wind ] [ parse-recent-time ] bi*
521 "%s occuring at %s" sprintf ;
523 : parse-sea-level-pressure ( str -- str' )
524 "SLP" ?head drop string>number 10.0 /f 1000 +
525 "sea-level pressure is %s hPa" sprintf ;
527 : parse-lightning ( str -- str' )
528 "LTG" ?head drop 2 group [ lightning at ] map join-words ;
530 CONSTANT: re-recent-weather R/ ((\w{2})?[BE]\d{2,4}((\w{2})?[BE]\d{2,4})?)+/
532 : parse-began/ended ( str -- str' )
534 [ CHAR: B = "began" "ended" ? ]
535 [ parse-recent-time ] bi* "%s at %s" sprintf ;
537 : split-recent-weather ( str -- seq )
539 dup [ digit? ] find drop
540 over [ digit? not ] find-from drop
541 [ cut ] [ f ] if* swap
544 : (parse-recent-weather) ( str -- str' )
545 dup [ digit? ] find drop 2 > [
546 2 cut [ weather at " " append ] dip
547 ] [ f swap ] if parse-began/ended "" append-as ;
549 : parse-recent-weather ( str -- str' )
551 [ (parse-recent-weather) ] map join-words ;
553 : parse-varying ( str -- str' )
554 "V" split1 [ string>number ] bi@
555 "varying between %s00 and %s00 ft" sprintf ;
557 : parse-from-to ( str -- str' )
558 "-" split [ parse-glossary ] map " to " join ;
560 : parse-water-equivalent-snow ( str -- str' )
561 "933" ?head drop parse-inches
562 "%s water equivalent of snow on ground" sprintf ;
564 : parse-duration-of-sunshine ( str -- str' )
565 "98" ?head drop string>number
566 [ "no" ] [ "%s minutes of" sprintf ] if-zero
567 "%s sunshine" sprintf ;
569 : parse-6hr-snowfall ( str -- str' )
570 "931" ?head drop parse-inches
571 "%s snowfall in last 6 hours" sprintf ;
573 : parse-probability ( str -- str' )
574 "PROB" ?head drop string>number
575 "probability of %d%%" sprintf ;
577 : parse-remark ( str -- str' )
579 { [ dup glossary key? ] [ glossary at ] }
580 { [ dup R/ 1\d{4}/ matches? ] [ parse-6hr-max-temp ] }
581 { [ dup R/ 2\d{4}/ matches? ] [ parse-6hr-min-temp ] }
582 { [ dup R/ 4\d{8}/ matches? ] [ parse-24hr-temp ] }
583 { [ dup R/ 4\/\d{3}/ matches? ] [ parse-snow-depth ] }
584 { [ dup R/ 5\d{4}/ matches? ] [ parse-1hr-pressure ] }
585 { [ dup R/ 6[\d\/]{4}/ matches? ] [ parse-6hr-precipitation ] }
586 { [ dup R/ 7\d{4}/ matches? ] [ parse-24hr-precipitation ] }
587 { [ dup R/ 8\/\d{3}/ matches? ] [ parse-cloud-cover ] }
588 { [ dup R/ 931\d{3}/ matches? ] [ parse-6hr-snowfall ] }
589 { [ dup R/ 933\d{3}/ matches? ] [ parse-water-equivalent-snow ] }
590 { [ dup R/ 98\d{3}/ matches? ] [ parse-duration-of-sunshine ] }
591 { [ dup R/ T\d{4,8}/ matches? ] [ parse-1hr-temp ] }
592 { [ dup R/ \d{3}\d{2,3}\/\d{2,4}/ matches? ] [ parse-peak-wind ] }
593 { [ dup R/ P\d{4}/ matches? ] [ parse-1hr-precipitation ] }
594 { [ dup R/ SLP\d{3}/ matches? ] [ parse-sea-level-pressure ] }
595 { [ dup R/ LTG\w+/ matches? ] [ parse-lightning ] }
596 { [ dup R/ PROB\d+/ matches? ] [ parse-probability ] }
597 { [ dup R/ \d{3}V\d{3}/ matches? ] [ parse-varying ] }
598 { [ dup R/ [^-]+(-[^-]+)+/ matches? ] [ parse-from-to ] }
599 { [ dup R/ [^\/]+(\/[^\/]+)+/ matches? ] [ ] }
600 { [ dup R/ \d+.\d+/ matches? ] [ ] }
601 { [ dup re-recent-weather matches? ] [ parse-recent-weather ] }
602 { [ dup re-weather matches? ] [ parse-weather ] }
603 { [ dup re-sky-condition matches? ] [ parse-sky-condition ] }
607 : metar-remarks ( report seq -- report )
608 [ parse-remark ] map join-words >>remarks ;
610 : <metar-report> ( metar -- report )
611 [ metar-report new ] dip [ >>raw ] keep
612 [ blank? ] split-when { "RMK" } split1
613 [ metar-body ] [ metar-remarks ] bi* ;
615 : row. ( name quot -- )
617 [ _ write ] with-cell
618 [ @ [ 65 wrap-string write ] when* ] with-cell
621 : calc-humidity ( report -- humidity/f )
622 [ dew-point>> ] [ temperature>> ] bi 2dup and [
623 [ " " split1 drop string>number ] bi@
624 [ [ 17.625 * ] [ 243.04 + ] bi / e^ ] bi@ / 100 *
628 : metar-report. ( report -- )
629 standard-table-style [
631 [ "Station" [ station>> ] row. ]
632 [ "Timestamp" [ timestamp>> ] row. ]
633 [ "Wind" [ wind>> ] row. ]
634 [ "Visibility" [ visibility>> ] row. ]
635 [ "RVR" [ rvr>> ] row. ]
636 [ "Weather" [ weather>> ] row. ]
637 [ "Sky condition" [ sky-condition>> ] row. ]
638 [ "Temperature" [ temperature>> ] row. ]
639 [ "Dew point" [ dew-point>> ] row. ]
640 [ "Altimeter" [ altimeter>> ] row. ]
641 [ "Humidity" [ calc-humidity ] row. ]
642 [ "Remarks" [ remarks>> ] row. ]
643 [ "Raw Text" [ raw>> ] row. ]
645 ] tabular-output nl ;
649 GENERIC: metar ( station -- metar )
651 M: station metar cccc>> metar ;
654 "http://tgftp.nws.noaa.gov/data/observations/metar/stations/%s.TXT"
655 sprintf http-get nip ;
657 GENERIC: metar. ( station -- )
659 M: station metar. cccc>> metar. ;
662 [ metar <metar-report> metar-report. ]
663 [ drop "%s METAR not found\n" printf ] recover ;
667 : parse-wind-shear ( str -- str' )
668 "WS" ?head drop "/" split1
669 [ parse-altitude ] [ parse-wind ] bi* prepend
670 "wind shear " prepend ;
672 CONSTANT: re-from-timestamp R/ FM\d{6}/
674 : parse-from-timestamp ( str -- str' )
675 "FM" ?head drop parse-timestamp ;
677 CONSTANT: re-valid-timestamp R/ \d{4}\/\d{4}/
679 : parse-valid-timestamp ( str -- str' )
680 "/" split1 [ "00" append parse-timestamp ] bi@ " to " glue ;
682 TUPLE: taf-report station timestamp valid-timestamp wind
683 visibility rvr weather sky-condition partials raw ;
685 TUPLE: taf-partial from-timestamp wind visibility rvr weather
688 : taf-body ( report str -- report )
689 [ blank? ] split-when
691 [ "TAF" = ] find-one drop
693 [ { "AMD" "COR" "RTD" } member? ] find-one drop
695 [ re-station matches? ] find-one
696 [ pick station<< ] when*
698 [ re-timestamp matches? ] find-one
699 [ parse-timestamp pick timestamp<< ] when*
701 [ re-valid-timestamp matches? ] find-one
702 [ parse-valid-timestamp pick valid-timestamp<< ] when*
704 [ re-wind matches? ] find-one
705 [ parse-wind pick wind<< ] when*
707 [ re-wind-variable matches? ] find-one
708 [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
710 [ re-visibility matches? ] find-one
711 [ parse-visibility pick visibility<< ] when*
713 [ re-rvr matches? ] find-all join-words
714 [ parse-rvr ] map ", " join pick rvr<<
716 [ re-weather matches? ] find-all
717 [ parse-weather ] map ", " join pick weather<<
719 [ re-sky-condition matches? ] find-all
720 [ parse-sky-condition ] map ", " join pick sky-condition<<
724 : <taf-partial> ( str -- partial )
725 [ taf-partial new ] dip [ blank? ] split-when
727 [ re-from-timestamp matches? ] find-one
728 [ parse-from-timestamp pick from-timestamp<< ] when*
730 [ re-wind matches? ] find-one
731 [ parse-wind pick wind<< ] when*
733 [ re-wind-variable matches? ] find-one
734 [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
736 [ re-visibility matches? ] find-one
737 [ parse-visibility pick visibility<< ] when*
739 [ re-rvr matches? ] find-all join-words
740 [ parse-rvr ] map ", " join pick rvr<<
742 [ re-weather matches? ] find-all
743 [ parse-weather ] map ", " join pick weather<<
745 [ re-sky-condition matches? ] find-all
746 [ parse-sky-condition ] map ", " join pick sky-condition<<
750 : taf-partials ( report seq -- report )
751 [ <taf-partial> ] map >>partials ;
753 : <taf-report> ( taf -- report )
754 [ taf-report new ] dip [ >>raw ] keep
755 split-lines [ [ blank? ] trim ] map
756 rest dup first "TAF" = [ rest ] when
757 harvest unclip swapd taf-body swap taf-partials ;
759 : taf-report. ( report -- )
761 standard-table-style [
763 [ "Station" [ station>> ] row. ]
764 [ "Timestamp" [ timestamp>> ] row. ]
765 [ "Valid From" [ valid-timestamp>> ] row. ]
766 [ "Wind" [ wind>> ] row. ]
767 [ "Visibility" [ visibility>> ] row. ]
768 [ "RVR" [ rvr>> ] row. ]
769 [ "Weather" [ weather>> ] row. ]
770 [ "Sky condition" [ sky-condition>> ] row. ]
771 [ "Raw Text" [ raw>> ] row. ]
776 standard-table-style [
778 [ "From" [ from-timestamp>> ] row. ]
779 [ "Wind" [ wind>> ] row. ]
780 [ "Visibility" [ visibility>> ] row. ]
781 [ "RVR" [ rvr>> ] row. ]
782 [ "Weather" [ weather>> ] row. ]
783 [ "Sky condition" [ sky-condition>> ] row. ]
791 GENERIC: taf ( station -- taf )
793 M: station taf cccc>> taf ;
796 "http://tgftp.nws.noaa.gov/data/forecasts/taf/stations/%s.TXT"
797 sprintf http-get nip ;
799 GENERIC: taf. ( station -- )
801 M: station taf. cccc>> taf. ;
804 [ taf <taf-report> taf-report. ]
805 [ drop "%s TAF not found\n" printf ] recover ;
809 [ metar print ] [ taf print ] bi nl