remove commas
[spider.git] / Geo / TAF / TAF.pm
1 #
2 # A set of routine for decode TAF and METAR a bit better and more comprehensively
3 # than some other products I tried.
4 #
5 # $Id$
6 #
7 # Copyright (c) 2003 Dirk Koopman G1TLH
8 #
9
10 package Geo::TAF;
11
12 use 5.005;
13 use strict;
14 use vars qw($VERSION);
15
16 $VERSION = '1.01';
17
18
19 my %err = (
20                    '1' => "No valid ICAO designator",
21                    '2' => "Length is less than 10 characters",
22                    '3' => "No valid issue time",
23                    '4' => "Expecting METAR or TAF at the beginning", 
24                   );
25
26 my %clt = (
27                    SKC => 1,
28                    CLR => 1,
29                    NSC => 1,
30                    BLU => 1,
31                    WHT => 1,
32                    GRN => 1,
33                    YLO => 1,
34                    AMB => 1,
35                    RED => 1,
36                    BKN => 1,
37                    NIL => 1,
38                   );
39
40 my %ignore = (
41                           AUTO => 1,
42                           COR => 1,
43                          );
44
45                    
46 # Preloaded methods go here.
47
48 sub new
49 {
50         my $pkg = shift;
51         my $self = bless {@_}, $pkg;
52         $self->{chunk_package} ||= "Geo::TAF::EN";
53         return $self;
54 }
55
56 sub metar
57 {
58         my $self = shift;
59         my $l = shift;
60         return 2 unless length $l > 10;
61         $l = 'METAR ' . $l unless $l =~ /^\s*(?:METAR|TAF)\s/i;
62         return $self->decode($l);
63 }
64
65 sub taf
66 {
67         my $self = shift;
68         my $l = shift;
69         return 2 unless length $l > 10;
70         $l = 'TAF ' . $l unless $l =~ /^\s*(?:METAR|TAF)\s/i;
71         return $self->decode($l);
72 }
73
74 sub as_string
75 {
76         my $self = shift;
77         return join ' ', $self->as_strings;
78 }
79
80 sub as_strings
81 {
82         my $self = shift;
83         my @out;
84         for (@{$self->{chunks}}) {
85                 push @out, $_->as_string;
86         }
87         return @out;
88 }
89
90 sub chunks
91 {
92         my $self = shift;
93         return exists $self->{chunks} ? @{$self->{chunks}} : ();
94 }
95
96 sub as_chunk_strings
97 {
98         my $self = shift;
99         my @out;
100         
101         for (@{$self->{chunks}}) {
102                 push @out, $_->as_chunk;
103         }
104         return @out;
105 }
106
107 sub as_chunk_string
108 {
109         my $self = shift;
110         return join ' ', $self->as_chunk_strings;
111 }
112
113 sub raw
114 {
115         return shift->{line};
116 }
117
118 sub is_weather
119 {
120         return $_[0] =~ /^\s*(?:(?:METAR|TAF)\s+)?[A-Z]{4}\s+\d{6}Z?\s+/;
121 }
122
123 sub errorp
124 {
125         my $self = shift;
126         my $code = shift;
127         return $err{"$code"};
128 }
129
130 # basically all metars and tafs are the same, except that a metar is short
131 # and a taf can have many repeated sections for different times of the day
132 sub decode
133 {
134         my $self = shift;
135         my $l = uc shift;
136
137         $l =~ s/=$//;
138         
139         my @tok = split /\s+/, $l;
140
141         $self->{line} = join ' ', @tok;
142         
143         
144         # do we explicitly have a METAR or a TAF
145         my $t = shift @tok;
146         if ($t eq 'TAF') {
147                 $self->{taf} = 1;
148         } elsif ($t eq 'METAR') {
149                 $self->{taf} = 0;
150         } else {
151             return 4;
152         }
153
154         # next token is the ICAO dseignator
155         $t = shift @tok;
156         if ($t =~ /^[A-Z]{4}$/) {
157                 $self->{icao} = $t;
158         } else {
159                 return 1;
160         }
161
162         # next token is an issue time
163         $t = shift @tok;
164         if (my ($day, $time) = $t =~ /^(\d\d)(\d{4})Z?$/) {
165                 $self->{day} = $day;
166                 $self->{time} = _time($time);
167         } else {
168                 return 3;
169         }
170
171         # if it is a TAF then expect a validity (may be missing)
172         if ($self->{taf}) {
173                 if (my ($vd, $vfrom, $vto) = $tok[0] =~ /^(\d\d)(\d\d)(\d\d)$/) {
174                         $self->{valid_day} = $vd;
175                         $self->{valid_from} = _time($vfrom * 100);
176                         $self->{valid_to} = _time($vto * 100);
177                         shift @tok;
178                 } 
179         }
180
181         # we are now into the 'list' of things that can repeat over and over
182
183         my @chunk = (
184                                  $self->_chunk('HEAD', $self->{taf} ? 'TAF' : 'METAR', 
185                                                            $self->{icao}, $self->{day}, $self->{time})
186                                 );
187         
188         push @chunk, $self->_chunk('VALID', $self->{valid_day}, $self->{valid_from}, 
189                                                            $self->{valid_to}) if $self->{valid_day};
190
191         while (@tok) {
192                 $t = shift @tok;
193                 
194                 # temporary 
195                 if ($t eq 'TEMPO' || $t eq 'BECMG') {
196                         
197                         # next token may be a time if it is a taf
198                         my ($from, $to);
199                         if (($from, $to) = $tok[0] =~ /^(\d\d)(\d\d)$/) {
200                                 if ($self->{taf} && $from >= 0 && $from <= 24 && $to >= 0 && $to <= 24) {
201                                         shift @tok;
202                                         $from = _time($from * 100);
203                                         $to = _time($to * 100);
204                                 } else {
205                                         undef $from;
206                                         undef $to;
207                                 }
208                         }
209                         push @chunk, $self->_chunk($t, $from, $to);                     
210
211                 # ignore
212                 } elsif ($ignore{$t}) {
213                         ;
214                         
215         # no sig weather
216                 } elsif ($t eq 'NOSIG' || $t eq 'NSW') {
217                         push @chunk, $self->_chunk('WEATHER', 'NOSIG');
218
219                 # specific broken on its own
220                 } elsif ($t eq 'BKN') {
221                         push @chunk, $self->_chunk('WEATHER', $t);
222                         
223         # other 3 letter codes
224                 } elsif ($clt{$t}) {
225                         push @chunk, $self->_chunk('CLOUD', $t);
226                         
227                 # EU CAVOK viz > 10000m, no cloud, no significant weather
228                 } elsif ($t eq 'CAVOK') {
229                         $self->{viz_dist} ||= ">10000";
230                         $self->{viz_units} ||= 'm';
231                         push @chunk, $self->_chunk('CLOUD', 'CAVOK');
232
233         # RMK group (end for now)
234                 } elsif ($t eq 'RMK') {
235                         last;
236
237         # from
238         } elsif (my ($time) = $t =~ /^FM(\d\d\d\d)$/ ) {
239                         push @chunk, $self->_chunk('FROM', _time($time));
240
241         # Until
242         } elsif (($time) = $t =~ /^TL(\d\d\d\d)$/ ) {
243                         push @chunk, $self->_chunk('TIL', _time($time));
244
245         # probability
246         } elsif (my ($percent) = $t =~ /^PROB(\d\d)$/ ) {
247
248                         # next token may be a time if it is a taf
249                         my ($from, $to);
250                         if (($from, $to) = $tok[0] =~ /^(\d\d)(\d\d)$/) {
251                                 if ($self->{taf} && $from >= 0 && $from <= 24 && $to >= 0 && $to <= 24) {
252                                         shift @tok;
253                                         $from = _time($from * 100);
254                                         $to = _time($to * 100);
255                                 } else {
256                                         undef $from;
257                                         undef $to;
258                                 }
259                         }
260                         push @chunk, $self->_chunk('PROB', $percent, $from, $to);
261
262         # runway
263         } elsif (my ($sort, $dir) = $t =~ /^(RWY?|LDG)(\d\d[RLC]?)$/ ) {
264                         push @chunk, $self->_chunk('RWY', $sort, $dir);
265
266                 # a wind group
267                 } elsif (my ($wdir, $spd, $gust, $unit) = $t =~ /^(\d\d\d|VRB)(\d\d)(?:G(\d\d))?(KT|MPH|MPS|KMH)$/) {
268                         
269                         # the next word might be 'AUTO'
270                         if ($tok[0] eq 'AUTO') {
271                                 shift @tok;
272                         }
273
274                         # it could be variable so look at the next token
275
276                         my ($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/;
277                         shift @tok if defined $fromdir; 
278                         $spd = 0 + $spd;
279                         $gust = 0 + $gust if defined $gust;
280                         $unit = ucfirst lc $unit;
281                         $unit = 'm/sec' if $unit eq 'Mps';
282                         $self->{wind_dir} ||= $wdir;
283                         $self->{wind_speed} ||= $spd;
284                         $self->{wind_gusting} ||= $gust;
285                         $self->{wind_units} ||= $unit;
286                         push @chunk, $self->_chunk('WIND', $wdir, $spd, $gust, $unit, $fromdir, $todir);
287                         
288                 # pressure 
289                 } elsif (my ($u, $p, $punit) = $t =~ /^([QA])(?:NH)?(\d\d\d\d)(INS?)?$/) {
290                         
291                         if ($u eq 'A' || $punit && $punit =~ /^I/) {
292                                 $p = sprintf "%.2f", $p / 100;
293                                 $u = 'in';
294                         } else {
295                                 $u = 'hPa';
296                         }
297                         $self->{pressure} ||= $p;
298                         $self->{pressure_units} ||= $u;
299                         push @chunk, $self->_chunk('PRESS', $p, $u);
300
301                 # viz group in metres
302                 } elsif (my ($viz, $mist) = $t =~ m!^(\d\d\d\d[NSEW]{0,2})([A-Z][A-Z])?$!) {
303                         $viz = $viz eq '9999' ? ">10000" : 0 + $viz;
304                         $self->{viz_dist} ||= $viz;
305                         $self->{viz_units} ||= 'm';
306                         push @chunk, $self->_chunk('VIZ', $viz, 'm');
307                         push @chunk, $self->_chunk('WEATHER', $mist) if $mist;
308
309                 # viz group in KM
310                 } elsif (($viz) = $t =~ m!^(\d+)KM$!) {
311                         $viz = $viz eq '9999' ? ">10000" : 0 + $viz;
312                         $self->{viz_dist} ||= $viz;
313                         $self->{viz_units} ||= 'Km';
314                         push @chunk, $self->_chunk('VIZ', $viz, 'Km');
315
316                 # viz group in miles and faction of a mile with space between
317                 } elsif (my ($m) = $t =~ m!^(\d)$!) {
318                         if (my ($viz) = $tok[0] =~ m!^(\d/\d)SM$!) {
319                                 shift @tok;
320                                 $viz = "$m $viz";
321                                 $self->{viz_dist} ||= $viz;
322                                 $self->{viz_units} ||= 'miles';
323                                 push @chunk, $self->_chunk('VIZ', $viz, 'miles');
324                         }
325                         
326                 # viz group in miles (either in miles or under a mile)
327                 } elsif (my ($lt, $mviz) = $t =~ m!^(M)?(\d+(:?/\d)?)SM$!) {
328                         $mviz = '<' . $mviz if $lt;
329                         $self->{viz_dist} ||= $mviz;
330                         $self->{viz_units} ||= 'Stat. Miles';
331                         push @chunk, $self->_chunk('VIZ', $mviz, 'Miles');
332                         
333
334                 # runway visual range
335                 } elsif (my ($rw, $rlt, $range, $vlt, $var, $runit, $tend) = $t =~ m!^R(\d\d[LRC]?)/([MP])?(\d\d\d\d)(?:V([MP])(\d\d\d\d))?(?:(FT)/?)?([UND])?$!) {
336                         $runit = 'm' unless $runit;
337                         $runit = lc $unit;
338                         $range = "<$range" if $rlt && $rlt eq 'M';
339                         $range = ">$range" if $rlt && $rlt eq 'P';
340                         $var = "<$var" if $vlt && $vlt eq 'M';
341                         $var = ">$var" if $vlt && $vlt eq 'P';
342                         push @chunk, $self->_chunk('RVR', $rw, $range, $var, $runit, $tend);
343                 
344                 # weather
345                 } elsif (my ($deg, $w) = $t =~ /^(\+|\-|VC)?([A-Z][A-Z]{1,4})$/) {
346                         push @chunk, $self->_chunk('WEATHER', $deg, $w =~ /([A-Z][A-Z])/g);
347                          
348         # cloud and stuff 
349                 } elsif (my ($amt, $height, $cb) = $t =~ m!^(FEW|SCT|BKN|OVC|SKC|CLR|VV|///)(\d\d\d|///)(CB|TCU)?$!) {
350                         push @chunk, $self->_chunk('CLOUD', $amt, $height eq '///' ? 0 : $height * 100, $cb) unless $amt eq '///' && $height eq '///';
351
352                 # temp / dew point
353                 } elsif (my ($ms, $t, $n, $d) = $t =~ m!^(M)?(\d\d)/(M)?(\d\d)?$!) {
354                         $t = 0 + $t;
355                         $d = 0 + $d;
356                         $t = -$t if defined $ms;
357                         $d = -$d if defined $d && defined $n;
358                         $self->{temp} ||= $t;
359                         $self->{dewpoint} ||= $d;
360                         push @chunk, $self->_chunk('TEMP', $t, $d);
361                 } 
362                 
363         }                       
364         $self->{chunks} = \@chunk;
365         return undef;   
366 }
367
368 sub _chunk
369 {
370         my $self = shift;
371         my $pkg = shift;
372         no strict 'refs';
373         $pkg = $self->{chunk_package} . '::' . $pkg;
374         return $pkg->new(@_);
375 }
376
377 sub _time
378 {
379         return sprintf "%02d:%02d", unpack "a2a2", sprintf "%04d", shift;
380 }
381
382 # accessors
383 sub AUTOLOAD
384 {
385         no strict;
386         my $name = $AUTOLOAD;
387         return if $name =~ /::DESTROY$/;
388         $name =~ s/^.*:://o;
389
390         *$AUTOLOAD = sub { $_[0]->{$name}};
391     goto &$AUTOLOAD;
392 }
393
394 #
395 # these are the translation packages
396 #
397 # First the factory method
398 #
399
400 package Geo::TAF::EN;
401
402 sub new
403 {
404         my $pkg = shift;
405         return bless [@_], $pkg; 
406 }
407
408 sub as_chunk
409 {
410         my $self = shift;
411         my ($n) = (ref $self) =~ /::(\w+)$/;
412         return '[' . join(' ', $n, map {defined $_ ? $_ : '?'} @$self) . ']';
413 }
414
415 sub as_string
416 {
417         my $self = shift;
418         my ($n) = (ref $self) =~ /::(\w+)$/;
419         return join ' ', ucfirst $n, map {defined $_ ? $_ : ()} @$self;
420 }
421
422 # accessors
423 sub AUTOLOAD
424 {
425         no strict;
426         my $name = $AUTOLOAD;
427         return if $name =~ /::DESTROY$/;
428         $name =~ s/^.*:://o;
429
430         *$AUTOLOAD = sub { $_[0]->{$name}};
431     goto &$AUTOLOAD;
432 }
433
434 package Geo::TAF::EN::HEAD;
435 use vars qw(@ISA);
436 @ISA = qw(Geo::TAF::EN);
437
438 sub as_string
439 {
440         my $self = shift;
441         return "$self->[0] for $self->[1] issued day $self->[2] at $self->[3]";
442 }
443
444 package Geo::TAF::EN::VALID;
445 use vars qw(@ISA);
446 @ISA = qw(Geo::TAF::EN);
447
448 sub as_string
449 {
450         my $self = shift;
451         return "valid day $self->[0] from $self->[1] till $self->[2]";
452 }
453
454
455 package Geo::TAF::EN::WIND;
456 use vars qw(@ISA);
457 @ISA = qw(Geo::TAF::EN);
458
459 # direction, $speed, $gusts, $unit, $fromdir, $todir
460 sub as_string
461 {
462         my $self = shift;
463         my $out = "wind";
464         $out .= $self->[0] eq 'VRB' ? " variable" : " $self->[0]";
465     $out .= " varying between $self->[4] and $self->[5]" if defined $self->[4];
466         $out .= ($self->[0] eq 'VRB' ? '' : " degrees") . " at $self->[1]";
467         $out .= " gusting $self->[2]" if defined $self->[2];
468         $out .= $self->[3];
469         return $out;
470 }
471
472 package Geo::TAF::EN::PRESS;
473 use vars qw(@ISA);
474 @ISA = qw(Geo::TAF::EN);
475
476 # $pressure, $unit
477 sub as_string
478 {
479         my $self = shift;
480         return "QNH $self->[0]$self->[1]";
481 }
482
483 # temperature, dewpoint
484 package Geo::TAF::EN::TEMP;
485 use vars qw(@ISA);
486 @ISA = qw(Geo::TAF::EN);
487
488 sub as_string
489 {
490         my $self = shift;
491         my $out = "temperature $self->[0]C";
492         $out .= " dewpoint $self->[1]C" if defined $self->[1];
493
494         return $out;
495 }
496
497 package Geo::TAF::EN::CLOUD;
498 use vars qw(@ISA);
499 @ISA = qw(Geo::TAF::EN);
500
501 my %st = (
502                   VV => 'vertical visibility',
503                   SKC => "no cloud",
504                   CLR => "no cloud no significant weather",
505                   SCT => "5-7 oktas",
506                   BKN => "3-4 oktas",
507                   FEW => "0-2 oktas",
508                   OVC => "8 oktas overcast",
509                   CAVOK => "no cloud below 5000ft >10Km visibility no significant weather (CAVOK)",
510                   CB => 'thunderstorms',
511           TCU => 'towering cumulus',
512                   NSC => 'no significant cloud',
513                   BLU => '3 oktas at 2500ft 8Km visibility',
514                   WHT => '3 oktas at 1500ft 5Km visibility',
515                   GRN => '3 oktas at 700ft 3700m visibility',
516                   YLO => '3 oktas at 300ft 1600m visibility',
517                   AMB => '3 oktas at 200ft 800m visibility',
518                   RED => '3 oktas at <200ft <800m visibility',
519                   NIL => 'no weather',
520                   '///' => 'some',
521                  );
522
523 sub as_string
524 {
525         my $self = shift;
526         return $st{$self->[0]} if @$self == 1;
527         return $st{$self->[0]} . " $self->[1]ft" if $self->[0] eq 'VV';
528         return $st{$self->[0]} . " cloud at $self->[1]ft" . ((defined $self->[2]) ? " with $st{$self->[2]}" : "");
529 }
530
531 package Geo::TAF::EN::WEATHER;
532 use vars qw(@ISA);
533 @ISA = qw(Geo::TAF::EN);
534
535 my %wt = (
536                   '+' => 'heavy',
537           '-' => 'light',
538           'VC' => 'in the vicinity',
539
540                   MI => 'shallow',
541                   PI => 'partial',
542                   BC => 'patches of',
543                   DR => 'low drifting',
544                   BL => 'blowing',
545                   SH => 'showers',
546                   TS => 'thunderstorms containing',
547                   FZ => 'freezing',
548                   RE => 'recent',
549                   
550                   DZ => 'drizzle',
551                   RA => 'rain',
552                   SN => 'snow',
553                   SG => 'snow grains',
554                   IC => 'ice crystals',
555                   PE => 'ice pellets',
556                   GR => 'hail',
557                   GS => 'small hail/snow pellets',
558                   UP => 'unknown precip',
559                   
560                   BR => 'mist',
561                   FG => 'fog',
562                   FU => 'smoke',
563                   VA => 'volcanic ash',
564                   DU => 'dust',
565                   SA => 'sand',
566                   HZ => 'haze',
567                   PY => 'spray',
568                   
569                   PO => 'dust/sand whirls',
570                   SQ => 'squalls',
571                   FC => 'tornado',
572                   SS => 'sand storm',
573                   DS => 'dust storm',
574                   '+FC' => 'water spouts',
575                   WS => 'wind shear',
576                   'BKN' => 'broken',
577
578                   'NOSIG' => 'no significant weather',
579                   
580                  );
581
582 sub as_string
583 {
584         my $self = shift;
585         my @out;
586
587         my ($vic, $shower);
588         my @in;
589         push @in, @$self;
590         
591         while (@in) {
592                 my $t = shift @in;
593
594                 if (!defined $t) {
595                         next;
596                 } elsif ($t eq 'VC') {
597                         $vic++;
598                         next;
599                 } elsif ($t eq 'SH') {
600                         $shower++;
601                         next;
602                 } elsif ($t eq '+' && $self->[0] eq 'FC') {
603                         push @out, $wt{'+FC'};
604                         shift;
605                         next;
606                 }
607                 
608                 push @out, $wt{$t};
609                 
610                 if (@out && $shower) {
611                         $shower = 0;
612                         push @out, $wt{'SH'};
613                 }
614         }
615         push @out, $wt{'VC'} if $vic;
616
617         return join ' ', @out;
618 }
619
620 package Geo::TAF::EN::RVR;
621 use vars qw(@ISA);
622 @ISA = qw(Geo::TAF::EN);
623
624 sub as_string
625 {
626         my $self = shift;
627         my $out = "visual range on runway $self->[0] is $self->[1]$self->[3]";
628         $out .= " varying to $self->[2]$self->[3]" if defined $self->[2];
629         if (defined $self->[4]) {
630                 $out .= " decreasing" if $self->[4] eq 'D';
631                 $out .= " increasing" if $self->[4] eq 'U';
632         }
633         return $out;
634 }
635
636 package Geo::TAF::EN::RWY;
637 use vars qw(@ISA);
638 @ISA = qw(Geo::TAF::EN);
639
640 sub as_string
641 {
642         my $self = shift;
643         my $out = $self->[0] eq 'LDG' ? "landing " : '';  
644         $out .= "runway $self->[1]";
645         return $out;
646 }
647
648 package Geo::TAF::EN::PROB;
649 use vars qw(@ISA);
650 @ISA = qw(Geo::TAF::EN);
651
652 sub as_string
653 {
654         my $self = shift;
655     
656         my $out = "probability $self->[0]%";
657         $out .= " $self->[1] till $self->[2]" if defined $self->[1];
658         return $out;
659 }
660
661 package Geo::TAF::EN::TEMPO;
662 use vars qw(@ISA);
663 @ISA = qw(Geo::TAF::EN);
664
665 sub as_string
666 {
667         my $self = shift;
668         my $out = "temporarily";
669         $out .= " $self->[0] till $self->[1]" if defined $self->[0];
670
671         return $out;
672 }
673
674 package Geo::TAF::EN::BECMG;
675 use vars qw(@ISA);
676 @ISA = qw(Geo::TAF::EN);
677
678 sub as_string
679 {
680         my $self = shift;
681         my $out = "becoming";
682         $out .= " $self->[0] till $self->[1]" if defined $self->[0];
683
684         return $out;
685 }
686
687 package Geo::TAF::EN::VIZ;
688 use vars qw(@ISA);
689 @ISA = qw(Geo::TAF::EN);
690
691 sub as_string
692 {
693     my $self = shift;
694
695     return "visibility $self->[0]$self->[1]";
696 }
697
698 package Geo::TAF::EN::FROM;
699 use vars qw(@ISA);
700 @ISA = qw(Geo::TAF::EN);
701
702 sub as_string
703 {
704     my $self = shift;
705
706     return "from $self->[0]";
707 }
708
709 package Geo::TAF::EN::TIL;
710 use vars qw(@ISA);
711 @ISA = qw(Geo::TAF::EN);
712
713 sub as_string
714 {
715     my $self = shift;
716
717     return "until $self->[0]";
718 }
719
720
721 # Autoload methods go after =cut, and are processed by the autosplit program.
722
723 1;
724 __END__
725 # Below is stub documentation for your module. You'd better edit it!
726
727 =head1 NAME
728
729 Geo::TAF - Decode METAR and TAF strings
730
731 =head1 SYNOPSIS
732
733   use strict;
734   use Geo::TAF;
735
736   my $t = new Geo::TAF;
737
738   $t->metar("EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
739   or
740   $t->taf("EGSH 311205Z 311322 04010KT 9999 SCT020
741      TEMPO 1319 3000 SHSN BKN008 PROB30
742      TEMPO 1318 0700 +SHSN VV///
743      BECMG 1619 22005KT");
744   or 
745   $t->decode("METAR EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
746   or
747   $t->decode("TAF EGSH 311205Z 311322 04010KT 9999 SCT020
748      TEMPO 1319 3000 SHSN BKN008 PROB30
749      TEMPO 1318 0700 +SHSN VV///
750      BECMG 1619 22005KT");
751
752   foreach my $c ($t->chunks) {
753           print $c->as_string, ' ';
754   }
755   or
756   print $self->as_string;
757
758   foreach my $c ($t->chunks) {
759           print $c->as_chunk, ' ';
760   }
761   or 
762   print $self->as_chunk_string;
763
764   my @out = $self->as_strings;
765   my @out = $self->as_chunk_strings;
766   my $line = $self->raw;
767   print Geo::TAF::is_weather($line) ? 1 : 0;
768
769 =head1 ABSTRACT
770
771 Geo::TAF decodes aviation METAR and TAF weather forecast code 
772 strings into English or, if you sub-class, some other language.
773
774 =head1 DESCRIPTION
775
776 METAR (Routine Aviation weather Report) and TAF (Terminal Area
777 weather Report) are ascii strings containing codes describing
778 the weather at airports and weather bureaus around the world.
779
780 This module attempts to decode these reports into a form of 
781 English that is hopefully more understandable than the reports
782 themselves. 
783
784 It is possible to sub-class the translation routines to enable
785 translation to other langauages.
786
787 =head1 METHODS
788
789 =over
790
791 =item new(%args)
792
793 Constructor for the class. Each weather announcement will need
794 a new constructor. 
795
796 If you sub-class the built-in English translation routines then 
797 you can pick this up by called the constructor thus:-
798  
799   C<my $t = Geo::TAF-E<gt>new(chunk_package =E<gt> 'Geo::TAF::ES');>
800
801 or whatever takes your fancy.
802
803 =item decode($line)
804
805 The main routine that decodes a weather string. It expects a
806 string that begins with either the word C<METAR> or C<TAF>.
807 It creates a decoded form of the weather string in the object.
808
809 There are a number of fixed fields created and also array
810 of chunks L<chunks()> of (as default) C<Geo::TAF::EN>.
811
812 You can decode these manually or use one of the built-in routines.
813
814 This method returns undef if it is successful, a number otherwise.
815 You can use L<errorp($r)> routine to get a stringified
816 version. 
817
818 =item metar($line)
819
820 This simply adds C<METAR> to the front of the string and calls
821 L<decode()>.
822
823 =item taf($line)
824
825 This simply adds C<TAF> to the front of the string and calls
826 L<decode()>.
827
828 It makes very little difference to the decoding process which
829 of these routines you use. It does, however, affect the output
830 in that it will mark it as the appropriate type of report.
831
832 =item as_string()
833
834 Returns the decoded weather report as a human readable string.
835
836 This is probably the simplest and most likely of the output
837 options that you might want to use. See also L<as_strings()>.
838
839 =item as_strings()
840
841 Returns an array of strings without separators. This simply
842 the decoded, human readable, normalised strings presented
843 as an array.
844
845 =item as_chunk_string()
846
847 Returns a human readable version of the internal decoded,
848 normalised form of the weather report. 
849
850 This may be useful if you are doing something special, but
851 see L<chunks()> or L<as_chunk_strings()> for a procedural 
852 approach to accessing the internals.  
853
854 Although you can read the result, it is not, officially,
855 human readable.
856
857 =item as_chunk_strings()
858
859 Returns an array of the stringified versions of the internal
860 normalised form without separators.. This simply
861 the decoded (English as default) normalised strings presented
862 as an array.
863
864 =item chunks()
865
866 Returns a list of (as default) C<Geo::TAF::EN> objects. You 
867 can use C<$c-E<gt>as_string> or C<$c-E<gt>as_chunk> to 
868 translate the internal form into something readable. 
869
870 If you replace the English versions of these objects then you 
871 will need at an L<as_string()> method.
872
873 =item raw()
874
875 Returns the (cleaned up) weather report. It is cleaned up in the
876 sense that all whitespace is reduced to exactly one space 
877 character.
878
879 =item errorp($r)
880
881 Returns a stringified version of any error returned by L<decode()>
882
883 =back
884
885 =head1 ACCESSORS
886
887 =over
888
889 =item taf()
890
891 Returns whether this object is a taf or not.
892
893 =item icao()
894
895 Returns the ICAO code contained in the weather report
896
897 =item day()
898
899 Returns the day of the month of this report
900
901 =item time()
902
903 Returns the issue time of this report
904
905 =item valid_day()
906
907 Returns the day this report is valid for (if there is one).
908
909 =item valid_from()
910
911 Returns the time from which this report is valid for (if there is one).
912
913 =item valid_to()
914
915 Returns the time to which this report is valid for (if there is one).
916
917 =item viz_dist()
918
919 Returns the minimum visibility, if present.
920
921 =item viz_units()
922
923 Returns the units of the visibility information.
924
925 =item wind_dir()
926
927 Returns the wind direction in degrees, if present.
928
929 =item wind_speed()
930
931 Returns the wind speed.
932
933 =item wind_units()
934
935 Returns the units of wind_speed.
936
937 =item wind_gusting()
938
939 Returns any wind gust speed. It is possible to have L<wind_speed()> 
940 without gust information.
941
942 =item pressure()
943
944 Returns the QNH (altimeter setting atmospheric pressure), if present.
945
946 =item pressure_units()
947
948 Returns the units in which L<pressure()> is messured.
949
950 =item temp()
951
952 Returns any temperature present.
953
954 =item dewpoint()
955
956 Returns any dewpoint present.
957
958 =back
959
960 =head1 ROUTINES
961
962 =over
963
964 =item is_weather($line)
965
966 This is a routine that determines, fairly losely, whether the
967 passed string is likely to be a weather report;
968
969 This routine is not exported. You must call it explicitly.
970
971 =back
972
973 =head1 SEE ALSO
974
975 L<Geo::METAR>
976
977 For a example of a weather forecast from the Norwich Weather 
978 Centre (EGSH) see L<http://www.tobit.co.uk>
979
980 For data see <ftp://weather.noaa.gov/data/observations/metar/>
981 L<ftp://weather.noaa.gov/data/forecasts/taf/> and also
982 L<ftp://weather.noaa.gov/data/forecasts/shorttaf/>
983
984 To find an ICAO code for your local airport see
985 L<http://www.ar-group.com/icaoiata.htm>
986
987 =head1 AUTHOR
988
989 Dirk Koopman, L<mailto:djk@tobit.co.uk>
990
991 =head1 COPYRIGHT AND LICENSE
992
993 Copyright (c) 2003 by Dirk Koopman, G1TLH
994
995 This library is free software; you can redistribute it and/or modify
996 it under the same terms as Perl itself. 
997
998 =cut