add new short form cgi program
[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.02';
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 (@tok && (($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 (@tok && (($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                         my ($fromdir, $todir);
270                         
271                         if      (@tok && (($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/)) {
272                                 shift @tok;
273                         }
274                         
275                         # it could be variable so look at the next token
276
277                         $spd = 0 + $spd;
278                         $gust = 0 + $gust if defined $gust;
279                         $unit = ucfirst lc $unit;
280                         $unit = 'm/sec' if $unit eq 'Mps';
281                         $self->{wind_dir} ||= $wdir;
282                         $self->{wind_speed} ||= $spd;
283                         $self->{wind_gusting} ||= $gust;
284                         $self->{wind_units} ||= $unit;
285                         push @chunk, $self->_chunk('WIND', $wdir, $spd, $gust, $unit, $fromdir, $todir);
286                         
287                 # pressure 
288                 } elsif (my ($u, $p, $punit) = $t =~ /^([QA])(?:NH)?(\d\d\d\d)(INS?)?$/) {
289                         
290                         if ($u eq 'A' || $punit && $punit =~ /^I/) {
291                                 $p = sprintf "%.2f", $p / 100;
292                                 $u = 'in';
293                         } else {
294                                 $u = 'hPa';
295                         }
296                         $self->{pressure} ||= $p;
297                         $self->{pressure_units} ||= $u;
298                         push @chunk, $self->_chunk('PRESS', $p, $u);
299
300                 # viz group in metres
301                 } elsif (my ($viz, $mist) = $t =~ m!^(\d\d\d\d[NSEW]{0,2})([A-Z][A-Z])?$!) {
302                         $viz = $viz eq '9999' ? ">10000" : 0 + $viz;
303                         $self->{viz_dist} ||= $viz;
304                         $self->{viz_units} ||= 'm';
305                         push @chunk, $self->_chunk('VIZ', $viz, 'm');
306                         push @chunk, $self->_chunk('WEATHER', $mist) if $mist;
307
308                 # viz group in KM
309                 } elsif (($viz) = $t =~ m!^(\d+)KM$!) {
310                         $viz = $viz eq '9999' ? ">10000" : 0 + $viz;
311                         $self->{viz_dist} ||= $viz;
312                         $self->{viz_units} ||= 'Km';
313                         push @chunk, $self->_chunk('VIZ', $viz, 'Km');
314
315                 # viz group in miles and faction of a mile with space between
316                 } elsif (my ($m) = $t =~ m!^(\d)$!) {
317                         my $viz;
318                         if (@tok && (($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 package Geo::TAF::EN::HEAD;
423 use vars qw(@ISA);
424 @ISA = qw(Geo::TAF::EN);
425
426 sub as_string
427 {
428         my $self = shift;
429         return "$self->[0] for $self->[1] issued day $self->[2] at $self->[3]";
430 }
431
432 package Geo::TAF::EN::VALID;
433 use vars qw(@ISA);
434 @ISA = qw(Geo::TAF::EN);
435
436 sub as_string
437 {
438         my $self = shift;
439         return "valid day $self->[0] from $self->[1] till $self->[2]";
440 }
441
442
443 package Geo::TAF::EN::WIND;
444 use vars qw(@ISA);
445 @ISA = qw(Geo::TAF::EN);
446
447 # direction, $speed, $gusts, $unit, $fromdir, $todir
448 sub as_string
449 {
450         my $self = shift;
451         my $out = "wind";
452         $out .= $self->[0] eq 'VRB' ? " variable" : " $self->[0]";
453     $out .= " varying between $self->[4] and $self->[5]" if defined $self->[4];
454         $out .= ($self->[0] eq 'VRB' ? '' : " degrees") . " at $self->[1]";
455         $out .= " gusting $self->[2]" if defined $self->[2];
456         $out .= $self->[3];
457         return $out;
458 }
459
460 package Geo::TAF::EN::PRESS;
461 use vars qw(@ISA);
462 @ISA = qw(Geo::TAF::EN);
463
464 # $pressure, $unit
465 sub as_string
466 {
467         my $self = shift;
468         return "QNH $self->[0]$self->[1]";
469 }
470
471 # temperature, dewpoint
472 package Geo::TAF::EN::TEMP;
473 use vars qw(@ISA);
474 @ISA = qw(Geo::TAF::EN);
475
476 sub as_string
477 {
478         my $self = shift;
479         my $out = "temperature $self->[0]C";
480         $out .= " dewpoint $self->[1]C" if defined $self->[1];
481
482         return $out;
483 }
484
485 package Geo::TAF::EN::CLOUD;
486 use vars qw(@ISA);
487 @ISA = qw(Geo::TAF::EN);
488
489 my %st = (
490                   VV => 'vertical visibility',
491                   SKC => "no cloud",
492                   CLR => "no cloud no significant weather",
493                   SCT => "5-7 oktas",
494                   BKN => "3-4 oktas",
495                   FEW => "0-2 oktas",
496                   OVC => "8 oktas overcast",
497                   CAVOK => "no cloud below 5000ft >10Km visibility no significant weather (CAVOK)",
498                   CB => 'thunderstorms',
499           TCU => 'towering cumulus',
500                   NSC => 'no significant cloud',
501                   BLU => '3 oktas at 2500ft 8Km visibility',
502                   WHT => '3 oktas at 1500ft 5Km visibility',
503                   GRN => '3 oktas at 700ft 3700m visibility',
504                   YLO => '3 oktas at 300ft 1600m visibility',
505                   AMB => '3 oktas at 200ft 800m visibility',
506                   RED => '3 oktas at <200ft <800m visibility',
507                   NIL => 'no weather',
508                   '///' => 'some',
509                  );
510
511 sub as_string
512 {
513         my $self = shift;
514         return $st{$self->[0]} if @$self == 1;
515         return $st{$self->[0]} . " $self->[1]ft" if $self->[0] eq 'VV';
516         return $st{$self->[0]} . " cloud at $self->[1]ft" . ((defined $self->[2]) ? " with $st{$self->[2]}" : "");
517 }
518
519 package Geo::TAF::EN::WEATHER;
520 use vars qw(@ISA);
521 @ISA = qw(Geo::TAF::EN);
522
523 my %wt = (
524                   '+' => 'heavy',
525           '-' => 'light',
526           'VC' => 'in the vicinity',
527
528                   MI => 'shallow',
529                   PI => 'partial',
530                   BC => 'patches of',
531                   DR => 'low drifting',
532                   BL => 'blowing',
533                   SH => 'showers',
534                   TS => 'thunderstorms containing',
535                   FZ => 'freezing',
536                   RE => 'recent',
537                   
538                   DZ => 'drizzle',
539                   RA => 'rain',
540                   SN => 'snow',
541                   SG => 'snow grains',
542                   IC => 'ice crystals',
543                   PE => 'ice pellets',
544                   GR => 'hail',
545                   GS => 'small hail/snow pellets',
546                   UP => 'unknown precip',
547                   
548                   BR => 'mist',
549                   FG => 'fog',
550                   FU => 'smoke',
551                   VA => 'volcanic ash',
552                   DU => 'dust',
553                   SA => 'sand',
554                   HZ => 'haze',
555                   PY => 'spray',
556                   
557                   PO => 'dust/sand whirls',
558                   SQ => 'squalls',
559                   FC => 'tornado',
560                   SS => 'sand storm',
561                   DS => 'dust storm',
562                   '+FC' => 'water spouts',
563                   WS => 'wind shear',
564                   'BKN' => 'broken',
565
566                   'NOSIG' => 'no significant weather',
567                   
568                  );
569
570 sub as_string
571 {
572         my $self = shift;
573         my @out;
574
575         my ($vic, $shower);
576         my @in;
577         push @in, @$self;
578         
579         while (@in) {
580                 my $t = shift @in;
581
582                 if (!defined $t) {
583                         next;
584                 } elsif ($t eq 'VC') {
585                         $vic++;
586                         next;
587                 } elsif ($t eq 'SH') {
588                         $shower++;
589                         next;
590                 } elsif ($t eq '+' && $self->[0] eq 'FC') {
591                         push @out, $wt{'+FC'};
592                         shift;
593                         next;
594                 }
595                 
596                 push @out, $wt{$t};
597                 
598                 if (@out && $shower) {
599                         $shower = 0;
600                         push @out, $wt{'SH'};
601                 }
602         }
603         push @out, $wt{'VC'} if $vic;
604
605         return join ' ', @out;
606 }
607
608 package Geo::TAF::EN::RVR;
609 use vars qw(@ISA);
610 @ISA = qw(Geo::TAF::EN);
611
612 sub as_string
613 {
614         my $self = shift;
615         my $out = "visual range on runway $self->[0] is $self->[1]$self->[3]";
616         $out .= " varying to $self->[2]$self->[3]" if defined $self->[2];
617         if (defined $self->[4]) {
618                 $out .= " decreasing" if $self->[4] eq 'D';
619                 $out .= " increasing" if $self->[4] eq 'U';
620         }
621         return $out;
622 }
623
624 package Geo::TAF::EN::RWY;
625 use vars qw(@ISA);
626 @ISA = qw(Geo::TAF::EN);
627
628 sub as_string
629 {
630         my $self = shift;
631         my $out = $self->[0] eq 'LDG' ? "landing " : '';  
632         $out .= "runway $self->[1]";
633         return $out;
634 }
635
636 package Geo::TAF::EN::PROB;
637 use vars qw(@ISA);
638 @ISA = qw(Geo::TAF::EN);
639
640 sub as_string
641 {
642         my $self = shift;
643     
644         my $out = "probability $self->[0]%";
645         $out .= " $self->[1] till $self->[2]" if defined $self->[1];
646         return $out;
647 }
648
649 package Geo::TAF::EN::TEMPO;
650 use vars qw(@ISA);
651 @ISA = qw(Geo::TAF::EN);
652
653 sub as_string
654 {
655         my $self = shift;
656         my $out = "temporarily";
657         $out .= " $self->[0] till $self->[1]" if defined $self->[0];
658
659         return $out;
660 }
661
662 package Geo::TAF::EN::BECMG;
663 use vars qw(@ISA);
664 @ISA = qw(Geo::TAF::EN);
665
666 sub as_string
667 {
668         my $self = shift;
669         my $out = "becoming";
670         $out .= " $self->[0] till $self->[1]" if defined $self->[0];
671
672         return $out;
673 }
674
675 package Geo::TAF::EN::VIZ;
676 use vars qw(@ISA);
677 @ISA = qw(Geo::TAF::EN);
678
679 sub as_string
680 {
681     my $self = shift;
682
683     return "visibility $self->[0]$self->[1]";
684 }
685
686 package Geo::TAF::EN::FROM;
687 use vars qw(@ISA);
688 @ISA = qw(Geo::TAF::EN);
689
690 sub as_string
691 {
692     my $self = shift;
693
694     return "from $self->[0]";
695 }
696
697 package Geo::TAF::EN::TIL;
698 use vars qw(@ISA);
699 @ISA = qw(Geo::TAF::EN);
700
701 sub as_string
702 {
703     my $self = shift;
704
705     return "until $self->[0]";
706 }
707
708
709 # Autoload methods go after =cut, and are processed by the autosplit program.
710
711 1;
712 __END__
713 # Below is stub documentation for your module. You'd better edit it!
714
715 =head1 NAME
716
717 Geo::TAF - Decode METAR and TAF strings
718
719 =head1 SYNOPSIS
720
721   use strict;
722   use Geo::TAF;
723
724   my $t = new Geo::TAF;
725
726   $t->metar("EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
727   or
728   $t->taf("EGSH 311205Z 311322 04010KT 9999 SCT020
729      TEMPO 1319 3000 SHSN BKN008 PROB30
730      TEMPO 1318 0700 +SHSN VV///
731      BECMG 1619 22005KT");
732   or 
733   $t->decode("METAR EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
734   or
735   $t->decode("TAF EGSH 311205Z 311322 04010KT 9999 SCT020
736      TEMPO 1319 3000 SHSN BKN008 PROB30
737      TEMPO 1318 0700 +SHSN VV///
738      BECMG 1619 22005KT");
739
740   foreach my $c ($t->chunks) {
741           print $c->as_string, ' ';
742   }
743   or
744   print $self->as_string;
745
746   foreach my $c ($t->chunks) {
747           print $c->as_chunk, ' ';
748   }
749   or 
750   print $self->as_chunk_string;
751
752   my @out = $self->as_strings;
753   my @out = $self->as_chunk_strings;
754   my $line = $self->raw;
755   print Geo::TAF::is_weather($line) ? 1 : 0;
756
757 =head1 ABSTRACT
758
759 Geo::TAF decodes aviation METAR and TAF weather forecast code 
760 strings into English or, if you sub-class, some other language.
761
762 =head1 DESCRIPTION
763
764 METAR (Routine Aviation weather Report) and TAF (Terminal Area
765 weather Report) are ascii strings containing codes describing
766 the weather at airports and weather bureaus around the world.
767
768 This module attempts to decode these reports into a form of 
769 English that is hopefully more understandable than the reports
770 themselves. 
771
772 It is possible to sub-class the translation routines to enable
773 translation to other langauages.
774
775 =head1 METHODS
776
777 =over
778
779 =item new(%args)
780
781 Constructor for the class. Each weather announcement will need
782 a new constructor. 
783
784 If you sub-class the built-in English translation routines then 
785 you can pick this up by called the constructor thus:-
786  
787   C<my $t = Geo::TAF-E<gt>new(chunk_package =E<gt> 'Geo::TAF::ES');>
788
789 or whatever takes your fancy.
790
791 =item decode($line)
792
793 The main routine that decodes a weather string. It expects a
794 string that begins with either the word C<METAR> or C<TAF>.
795 It creates a decoded form of the weather string in the object.
796
797 There are a number of fixed fields created and also array
798 of chunks L<chunks()> of (as default) C<Geo::TAF::EN>.
799
800 You can decode these manually or use one of the built-in routines.
801
802 This method returns undef if it is successful, a number otherwise.
803 You can use L<errorp($r)> routine to get a stringified
804 version. 
805
806 =item metar($line)
807
808 This simply adds C<METAR> to the front of the string and calls
809 L<decode()>.
810
811 =item taf($line)
812
813 This simply adds C<TAF> to the front of the string and calls
814 L<decode()>.
815
816 It makes very little difference to the decoding process which
817 of these routines you use. It does, however, affect the output
818 in that it will mark it as the appropriate type of report.
819
820 =item as_string()
821
822 Returns the decoded weather report as a human readable string.
823
824 This is probably the simplest and most likely of the output
825 options that you might want to use. See also L<as_strings()>.
826
827 =item as_strings()
828
829 Returns an array of strings without separators. This simply
830 the decoded, human readable, normalised strings presented
831 as an array.
832
833 =item as_chunk_string()
834
835 Returns a human readable version of the internal decoded,
836 normalised form of the weather report. 
837
838 This may be useful if you are doing something special, but
839 see L<chunks()> or L<as_chunk_strings()> for a procedural 
840 approach to accessing the internals.  
841
842 Although you can read the result, it is not, officially,
843 human readable.
844
845 =item as_chunk_strings()
846
847 Returns an array of the stringified versions of the internal
848 normalised form without separators.. This simply
849 the decoded (English as default) normalised strings presented
850 as an array.
851
852 =item chunks()
853
854 Returns a list of (as default) C<Geo::TAF::EN> objects. You 
855 can use C<$c-E<gt>as_string> or C<$c-E<gt>as_chunk> to 
856 translate the internal form into something readable. 
857
858 If you replace the English versions of these objects then you 
859 will need at an L<as_string()> method.
860
861 =item raw()
862
863 Returns the (cleaned up) weather report. It is cleaned up in the
864 sense that all whitespace is reduced to exactly one space 
865 character.
866
867 =item errorp($r)
868
869 Returns a stringified version of any error returned by L<decode()>
870
871 =back
872
873 =head1 ACCESSORS
874
875 =over
876
877 =item taf()
878
879 Returns whether this object is a taf or not.
880
881 =item icao()
882
883 Returns the ICAO code contained in the weather report
884
885 =item day()
886
887 Returns the day of the month of this report
888
889 =item time()
890
891 Returns the issue time of this report
892
893 =item valid_day()
894
895 Returns the day this report is valid for (if there is one).
896
897 =item valid_from()
898
899 Returns the time from which this report is valid for (if there is one).
900
901 =item valid_to()
902
903 Returns the time to which this report is valid for (if there is one).
904
905 =item viz_dist()
906
907 Returns the minimum visibility, if present.
908
909 =item viz_units()
910
911 Returns the units of the visibility information.
912
913 =item wind_dir()
914
915 Returns the wind direction in degrees, if present.
916
917 =item wind_speed()
918
919 Returns the wind speed.
920
921 =item wind_units()
922
923 Returns the units of wind_speed.
924
925 =item wind_gusting()
926
927 Returns any wind gust speed. It is possible to have L<wind_speed()> 
928 without gust information.
929
930 =item pressure()
931
932 Returns the QNH (altimeter setting atmospheric pressure), if present.
933
934 =item pressure_units()
935
936 Returns the units in which L<pressure()> is messured.
937
938 =item temp()
939
940 Returns any temperature present.
941
942 =item dewpoint()
943
944 Returns any dewpoint present.
945
946 =back
947
948 =head1 ROUTINES
949
950 =over
951
952 =item is_weather($line)
953
954 This is a routine that determines, fairly losely, whether the
955 passed string is likely to be a weather report;
956
957 This routine is not exported. You must call it explicitly.
958
959 =back
960
961 =head1 SEE ALSO
962
963 L<Geo::METAR>
964
965 For a example of a weather forecast from the Norwich Weather 
966 Centre (EGSH) see L<http://www.tobit.co.uk>
967
968 For data see <ftp://weather.noaa.gov/data/observations/metar/>
969 L<ftp://weather.noaa.gov/data/forecasts/taf/> and also
970 L<ftp://weather.noaa.gov/data/forecasts/shorttaf/>
971
972 To find an ICAO code for your local airport see
973 L<http://www.ar-group.com/icaoiata.htm>
974
975 =head1 AUTHOR
976
977 Dirk Koopman, L<mailto:djk@tobit.co.uk>
978
979 =head1 COPYRIGHT AND LICENSE
980
981 Copyright (c) 2003 by Dirk Koopman, G1TLH
982
983 This library is free software; you can redistribute it and/or modify
984 it under the same terms as Perl itself. 
985
986 =cut