--- /dev/null
+#
+# A set of routine for decode TAF and METAR a bit better and more comprehensively
+# than some other products I tried.
+#
+# $Id$
+#
+# Copyright (c) 2003 Dirk Koopman G1TLH
+#
+
+package Geo::TAF;
+
+use 5.005;
+use strict;
+use vars qw($VERSION);
+
+$VERSION = '1.01';
+
+
+my %err = (
+ '1' => "No valid ICAO designator",
+ '2' => "Length is less than 10 characters",
+ '3' => "No valid issue time",
+ '4' => "Expecting METAR or TAF at the beginning",
+ );
+
+my %clt = (
+ SKC => 1,
+ CLR => 1,
+ NSC => 1,
+ BLU => 1,
+ WHT => 1,
+ GRN => 1,
+ YLO => 1,
+ AMB => 1,
+ RED => 1,
+ BKN => 1,
+ NIL => 1,
+ );
+
+my %ignore = (
+ AUTO => 1,
+ COR => 1,
+ );
+
+
+# Preloaded methods go here.
+
+sub new
+{
+ my $pkg = shift;
+ my $self = bless {@_}, $pkg;
+ $self->{chunk_package} ||= "Geo::TAF::EN";
+ return $self;
+}
+
+sub metar
+{
+ my $self = shift;
+ my $l = shift;
+ return 2 unless length $l > 10;
+ $l = 'METAR ' . $l unless $l =~ /^\s*(?:METAR|TAF)\s/i;
+ return $self->decode($l);
+}
+
+sub taf
+{
+ my $self = shift;
+ my $l = shift;
+ return 2 unless length $l > 10;
+ $l = 'TAF ' . $l unless $l =~ /^\s*(?:METAR|TAF)\s/i;
+ return $self->decode($l);
+}
+
+sub as_string
+{
+ my $self = shift;
+ my $out;
+
+ for (@{$self->{chunks}}) {
+ $out .= $_->as_string . ' ' if $_->can('as_string');
+ }
+ chop $out;
+ chop $out;
+ return $out;
+}
+
+sub as_strings
+{
+ my $self = shift;
+ my @out;
+ for (@{$self->{chunks}}) {
+ push @out, $_->as_string;
+ }
+ return @out;
+}
+
+sub chunks
+{
+ my $self = shift;
+ return exists $self->{chunks} ? @{$self->{chunks}} : ();
+}
+
+sub as_chunk_strings
+{
+ my $self = shift;
+ my @out;
+
+ for (@{$self->{chunks}}) {
+ push @out, $_->as_chunk;
+ }
+ return @out;
+}
+
+sub as_chunk_string
+{
+ my $self = shift;
+ return join ' ', $self->as_chunk_strings;
+}
+
+sub raw
+{
+ return shift->{line};
+}
+
+sub is_weather
+{
+ return $_[0] =~ /^\s*(?:(?:METAR|TAF)\s+)?[A-Z]{4}\s+\d{6}Z?\s+/;
+}
+
+sub errorp
+{
+ my $self = shift;
+ my $code = shift;
+ return $err{"$code"};
+}
+
+# basically all metars and tafs are the same, except that a metar is short
+# and a taf can have many repeated sections for different times of the day
+sub decode
+{
+ my $self = shift;
+ my $l = uc shift;
+
+ $l =~ s/=$//;
+
+ my @tok = split /\s+/, $l;
+
+ $self->{line} = join ' ', @tok;
+
+
+ # do we explicitly have a METAR or a TAF
+ my $t = shift @tok;
+ if ($t eq 'TAF') {
+ $self->{taf} = 1;
+ } elsif ($t eq 'METAR') {
+ $self->{taf} = 0;
+ } else {
+ return 4;
+ }
+
+ # next token is the ICAO dseignator
+ $t = shift @tok;
+ if ($t =~ /^[A-Z]{4}$/) {
+ $self->{icao} = $t;
+ } else {
+ return 1;
+ }
+
+ # next token is an issue time
+ $t = shift @tok;
+ if (my ($day, $time) = $t =~ /^(\d\d)(\d{4})Z?$/) {
+ $self->{day} = $day;
+ $self->{time} = _time($time);
+ } else {
+ return 3;
+ }
+
+ # if it is a TAF then expect a validity (may be missing)
+ if ($self->{taf}) {
+ if (my ($vd, $vfrom, $vto) = $tok[0] =~ /^(\d\d)(\d\d)(\d\d)$/) {
+ $self->{valid_day} = $vd;
+ $self->{valid_from} = _time($vfrom * 100);
+ $self->{valid_to} = _time($vto * 100);
+ shift @tok;
+ }
+ }
+
+ # we are now into the 'list' of things that can repeat over and over
+
+ my @chunk = (
+ $self->_chunk('HEAD', $self->{taf} ? 'TAF' : 'METAR',
+ $self->{icao}, $self->{day}, $self->{time})
+ );
+
+ push @chunk, $self->_chunk('VALID', $self->{valid_day}, $self->{valid_from},
+ $self->{valid_to}) if $self->{valid_day};
+
+ while (@tok) {
+ $t = shift @tok;
+
+ # temporary
+ if ($t eq 'TEMPO' || $t eq 'BECMG') {
+
+ # next token may be a time if it is a taf
+ my ($from, $to);
+ if (($from, $to) = $tok[0] =~ /^(\d\d)(\d\d)$/) {
+ if ($self->{taf} && $from >= 0 && $from <= 24 && $to >= 0 && $to <= 24) {
+ shift @tok;
+ $from = _time($from * 100);
+ $to = _time($to * 100);
+ } else {
+ undef $from;
+ undef $to;
+ }
+ }
+ push @chunk, $self->_chunk($t, $from, $to);
+
+ # ignore
+ } elsif ($ignore{$t}) {
+ ;
+
+ # no sig weather
+ } elsif ($t eq 'NOSIG' || $t eq 'NSW') {
+ push @chunk, $self->_chunk('WEATHER', 'NOSIG');
+
+ # specific broken on its own
+ } elsif ($t eq 'BKN') {
+ push @chunk, $self->_chunk('WEATHER', $t);
+
+ # other 3 letter codes
+ } elsif ($clt{$t}) {
+ push @chunk, $self->_chunk('CLOUD', $t);
+
+ # EU CAVOK viz > 10000m, no cloud, no significant weather
+ } elsif ($t eq 'CAVOK') {
+ $self->{viz_dist} ||= ">10000";
+ $self->{viz_units} ||= 'm';
+ push @chunk, $self->_chunk('CLOUD', 'CAVOK');
+
+ # RMK group (end for now)
+ } elsif ($t eq 'RMK') {
+ last;
+
+ # from
+ } elsif (my ($time) = $t =~ /^FM(\d\d\d\d)$/ ) {
+ push @chunk, $self->_chunk('FROM', _time($time));
+
+ # Until
+ } elsif (($time) = $t =~ /^TL(\d\d\d\d)$/ ) {
+ push @chunk, $self->_chunk('TIL', _time($time));
+
+ # probability
+ } elsif (my ($percent) = $t =~ /^PROB(\d\d)$/ ) {
+
+ # next token may be a time if it is a taf
+ my ($from, $to);
+ if (($from, $to) = $tok[0] =~ /^(\d\d)(\d\d)$/) {
+ if ($self->{taf} && $from >= 0 && $from <= 24 && $to >= 0 && $to <= 24) {
+ shift @tok;
+ $from = _time($from * 100);
+ $to = _time($to * 100);
+ } else {
+ undef $from;
+ undef $to;
+ }
+ }
+ push @chunk, $self->_chunk('PROB', $percent, $from, $to);
+
+ # runway
+ } elsif (my ($sort, $dir) = $t =~ /^(RWY?|LDG)(\d\d[RLC]?)$/ ) {
+ push @chunk, $self->_chunk('RWY', $sort, $dir);
+
+ # a wind group
+ } elsif (my ($wdir, $spd, $gust, $unit) = $t =~ /^(\d\d\d|VRB)(\d\d)(?:G(\d\d))?(KT|MPH|MPS|KMH)$/) {
+
+ # the next word might be 'AUTO'
+ if ($tok[0] eq 'AUTO') {
+ shift @tok;
+ }
+
+ # it could be variable so look at the next token
+
+ my ($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/;
+ shift @tok if defined $fromdir;
+ $spd = 0 + $spd;
+ $gust = 0 + $gust if defined $gust;
+ $unit = ucfirst lc $unit;
+ $unit = 'm/sec' if $unit eq 'Mps';
+ $self->{wind_dir} ||= $wdir;
+ $self->{wind_speed} ||= $spd;
+ $self->{wind_gusting} ||= $gust;
+ $self->{wind_units} ||= $unit;
+ push @chunk, $self->_chunk('WIND', $wdir, $spd, $gust, $unit, $fromdir, $todir);
+
+ # pressure
+ } elsif (my ($u, $p, $punit) = $t =~ /^([QA])(?:NH)?(\d\d\d\d)(INS?)?$/) {
+
+ if ($u eq 'A' || $punit && $punit =~ /^I/) {
+ $p = sprintf "%.2f", $p / 100;
+ $u = 'in';
+ } else {
+ $u = 'hPa';
+ }
+ $self->{pressure} ||= $p;
+ $self->{pressure_units} ||= $u;
+ push @chunk, $self->_chunk('PRESS', $p, $u);
+
+ # viz group in metres
+ } elsif (my ($viz, $mist) = $t =~ m!^(\d\d\d\d[NSEW]{0,2})([A-Z][A-Z])?$!) {
+ $viz = $viz eq '9999' ? ">10000" : 0 + $viz;
+ $self->{viz_dist} ||= $viz;
+ $self->{viz_units} ||= 'm';
+ push @chunk, $self->_chunk('VIZ', $viz, 'm');
+ push @chunk, $self->_chunk('WEATHER', $mist) if $mist;
+
+ # viz group in KM
+ } elsif (($viz) = $t =~ m!^(\d+)KM$!) {
+ $viz = $viz eq '9999' ? ">10000" : 0 + $viz;
+ $self->{viz_dist} ||= $viz;
+ $self->{viz_units} ||= 'Km';
+ push @chunk, $self->_chunk('VIZ', $viz, 'Km');
+
+ # viz group in miles and faction of a mile with space between
+ } elsif (my ($m) = $t =~ m!^(\d)$!) {
+ if (my ($viz) = $tok[0] =~ m!^(\d/\d)SM$!) {
+ shift @tok;
+ $viz = "$m $viz";
+ $self->{viz_dist} ||= $viz;
+ $self->{viz_units} ||= 'miles';
+ push @chunk, $self->_chunk('VIZ', $viz, 'miles');
+ }
+
+ # viz group in miles (either in miles or under a mile)
+ } elsif (my ($lt, $mviz) = $t =~ m!^(M)?(\d+(:?/\d)?)SM$!) {
+ $mviz = '<' . $mviz if $lt;
+ $self->{viz_dist} ||= $mviz;
+ $self->{viz_units} ||= 'Stat. Miles';
+ push @chunk, $self->_chunk('VIZ', $mviz, 'Miles');
+
+
+ # runway visual range
+ } 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])?$!) {
+ $runit = 'm' unless $runit;
+ $runit = lc $unit;
+ $range = "<$range" if $rlt && $rlt eq 'M';
+ $range = ">$range" if $rlt && $rlt eq 'P';
+ $var = "<$var" if $vlt && $vlt eq 'M';
+ $var = ">$var" if $vlt && $vlt eq 'P';
+ push @chunk, $self->_chunk('RVR', $rw, $range, $var, $runit, $tend);
+
+ # weather
+ } elsif (my ($deg, $w) = $t =~ /^(\+|\-|VC)?([A-Z][A-Z]{1,4})$/) {
+ push @chunk, $self->_chunk('WEATHER', $deg, $w =~ /([A-Z][A-Z])/g);
+
+ # cloud and stuff
+ } elsif (my ($amt, $height, $cb) = $t =~ m!^(FEW|SCT|BKN|OVC|SKC|CLR|VV|///)(\d\d\d|///)(CB|TCU)?$!) {
+ push @chunk, $self->_chunk('CLOUD', $amt, $height eq '///' ? 0 : $height * 100, $cb) unless $amt eq '///' && $height eq '///';
+
+ # temp / dew point
+ } elsif (my ($ms, $t, $n, $d) = $t =~ m!^(M)?(\d\d)/(M)?(\d\d)?$!) {
+ $t = 0 + $t;
+ $d = 0 + $d;
+ $t = -$t if defined $ms;
+ $d = -$d if defined $d && defined $n;
+ $self->{temp} ||= $t;
+ $self->{dewpoint} ||= $d;
+ push @chunk, $self->_chunk('TEMP', $t, $d);
+ }
+
+ }
+ $self->{chunks} = \@chunk;
+ return undef;
+}
+
+sub _chunk
+{
+ my $self = shift;
+ my $pkg = shift;
+ no strict 'refs';
+ $pkg = $self->{chunk_package} . '::' . $pkg;
+ return $pkg->new(@_);
+}
+
+sub _time
+{
+ return sprintf "%02d:%02d", unpack "a2a2", sprintf "%04d", shift;
+}
+
+# accessors
+sub AUTOLOAD
+{
+ no strict;
+ my $name = $AUTOLOAD;
+ return if $name =~ /::DESTROY$/;
+ $name =~ s/^.*:://o;
+
+ *$AUTOLOAD = sub { $_[0]->{$name}};
+ goto &$AUTOLOAD;
+}
+
+#
+# these are the translation packages
+#
+# First the factory method
+#
+
+package Geo::TAF::EN;
+
+sub new
+{
+ my $pkg = shift;
+ return bless [@_], $pkg;
+}
+
+sub as_chunk
+{
+ my $self = shift;
+ my ($n) = (ref $self) =~ /::(\w+)$/;
+ return '[' . join(' ', $n, map {defined $_ ? $_ : '?'} @$self) . ']';
+}
+
+sub as_string
+{
+ my $self = shift;
+ my ($n) = (ref $self) =~ /::(\w+)$/;
+ return join ' ', ucfirst $n, map {defined $_ ? $_ : ()} @$self;
+}
+
+# accessors
+sub AUTOLOAD
+{
+ no strict;
+ my $name = $AUTOLOAD;
+ return if $name =~ /::DESTROY$/;
+ $name =~ s/^.*:://o;
+
+ *$AUTOLOAD = sub { $_[0]->{$name}};
+ goto &$AUTOLOAD;
+}
+
+package Geo::TAF::EN::HEAD;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ return "$self->[0] for $self->[1] issued day $self->[2] at $self->[3]";
+}
+
+package Geo::TAF::EN::VALID;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ return "valid day $self->[0] from $self->[1] till $self->[2]";
+}
+
+
+package Geo::TAF::EN::WIND;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+# direction, $speed, $gusts, $unit, $fromdir, $todir
+sub as_string
+{
+ my $self = shift;
+ my $out = "wind";
+ $out .= $self->[0] eq 'VRB' ? " variable" : " $self->[0]";
+ $out .= " varying between $self->[4] and $self->[5]" if defined $self->[4];
+ $out .= ($self->[0] eq 'VRB' ? '' : " degrees") . " at $self->[1]";
+ $out .= " gusting $self->[2]" if defined $self->[2];
+ $out .= $self->[3];
+ return $out;
+}
+
+package Geo::TAF::EN::PRESS;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+# $pressure, $unit
+sub as_string
+{
+ my $self = shift;
+ return "QNH $self->[0]$self->[1]";
+}
+
+# temperature, dewpoint
+package Geo::TAF::EN::TEMP;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ my $out = "temperature $self->[0]C";
+ $out .= ", dewpoint $self->[1]C" if defined $self->[1];
+
+ return $out;
+}
+
+package Geo::TAF::EN::CLOUD;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+my %st = (
+ VV => 'vertical visibility',
+ SKC => "no cloud",
+ CLR => "no cloud, no significant weather",
+ SCT => "5-7 oktas",
+ BKN => "3-4 oktas",
+ FEW => "0-2 oktas",
+ OVC => "8 oktas, overcast",
+ CAVOK => "no cloud below 5000ft, >10Km visibility, no significant weather (CAVOK)",
+ CB => 'thunderstorms',
+ TCU => 'towering cumulus',
+ NSC => 'no significant cloud',
+ BLU => '3 oktas at 2500ft, 8Km visibility',
+ WHT => '3 oktas at 1500ft, 5Km visibility',
+ GRN => '3 oktas at 700ft, 3700m visibility',
+ YLO => '3 oktas at 300ft, 1600m visibility',
+ AMB => '3 oktas at 200ft, 800m visibility',
+ RED => '3 oktas at <200ft, <800m visibility',
+ NIL => 'no weather',
+ '///' => 'some',
+ );
+
+sub as_string
+{
+ my $self = shift;
+ return $st{$self->[0]} if @$self == 1;
+ return $st{$self->[0]} . " $self->[1]ft" if $self->[0] eq 'VV';
+ return $st{$self->[0]} . " cloud at $self->[1]ft" . ((defined $self->[2]) ? " with $st{$self->[2]}" : "");
+}
+
+package Geo::TAF::EN::WEATHER;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+my %wt = (
+ '+' => 'heavy',
+ '-' => 'light',
+ 'VC' => 'in the vicinity',
+
+ MI => 'shallow',
+ PI => 'partial',
+ BC => 'patches of',
+ DR => 'low drifting',
+ BL => 'blowing',
+ SH => 'showers',
+ TS => 'thunderstorms containing',
+ FZ => 'freezing',
+ RE => 'recent',
+
+ DZ => 'drizzle',
+ RA => 'rain',
+ SN => 'snow',
+ SG => 'snow grains',
+ IC => 'ice crystals',
+ PE => 'ice pellets',
+ GR => 'hail',
+ GS => 'small hail/snow pellets',
+ UP => 'unknown precip',
+
+ BR => 'mist',
+ FG => 'fog',
+ FU => 'smoke',
+ VA => 'volcanic ash',
+ DU => 'dust',
+ SA => 'sand',
+ HZ => 'haze',
+ PY => 'spray',
+
+ PO => 'dust/sand whirls',
+ SQ => 'squalls',
+ FC => 'tornado',
+ SS => 'sand storm',
+ DS => 'dust storm',
+ '+FC' => 'water spouts',
+ WS => 'wind shear',
+ 'BKN' => 'broken',
+
+ 'NOSIG' => 'no significant weather',
+
+ );
+
+sub as_string
+{
+ my $self = shift;
+ my @out;
+
+ my ($vic, $shower);
+ my @in;
+ push @in, @$self;
+
+ while (@in) {
+ my $t = shift @in;
+
+ if (!defined $t) {
+ next;
+ } elsif ($t eq 'VC') {
+ $vic++;
+ next;
+ } elsif ($t eq 'SH') {
+ $shower++;
+ next;
+ } elsif ($t eq '+' && $self->[0] eq 'FC') {
+ push @out, $wt{'+FC'};
+ shift;
+ next;
+ }
+
+ push @out, $wt{$t};
+
+ if (@out && $shower) {
+ $shower = 0;
+ push @out, $wt{'SH'};
+ }
+ }
+ push @out, $wt{'VC'} if $vic;
+
+ return join ' ', @out;
+}
+
+package Geo::TAF::EN::RVR;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ my $out = "visual range on runway $self->[0] is $self->[1]$self->[3]";
+ $out .= " varying to $self->[2]$self->[3]" if defined $self->[2];
+ if (defined $self->[4]) {
+ $out .= " decreasing" if $self->[4] eq 'D';
+ $out .= " increasing" if $self->[4] eq 'U';
+ }
+ return $out;
+}
+
+package Geo::TAF::EN::RWY;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ my $out = $self->[0] eq 'LDG' ? "landing " : '';
+ $out .= "runway $self->[1]";
+ return $out;
+}
+
+package Geo::TAF::EN::PROB;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+
+ my $out = "probability $self->[0]%";
+ $out .= " $self->[1] till $self->[2]" if defined $self->[1];
+ return $out;
+}
+
+package Geo::TAF::EN::TEMPO;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ my $out = "temporarily";
+ $out .= " $self->[0] till $self->[1]" if defined $self->[0];
+
+ return $out;
+}
+
+package Geo::TAF::EN::BECMG;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+ my $out = "becoming";
+ $out .= " $self->[0] till $self->[1]" if defined $self->[0];
+
+ return $out;
+}
+
+package Geo::TAF::EN::VIZ;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+
+ return "visibility $self->[0]$self->[1]";
+}
+
+package Geo::TAF::EN::FROM;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+
+ return "from $self->[0]";
+}
+
+package Geo::TAF::EN::TIL;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+ my $self = shift;
+
+ return "until $self->[0]";
+}
+
+
+# Autoload methods go after =cut, and are processed by the autosplit program.
+
+1;
+__END__
+# Below is stub documentation for your module. You'd better edit it!
+
+=head1 NAME
+
+Geo::TAF - Decode METAR and TAF strings
+
+=head1 SYNOPSIS
+
+ use strict;
+ use Geo::TAF;
+
+ my $t = new Geo::TAF;
+
+ $t->metar("EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
+ or
+ $t->taf("EGSH 311205Z 311322 04010KT 9999 SCT020
+ TEMPO 1319 3000 SHSN BKN008 PROB30
+ TEMPO 1318 0700 +SHSN VV///
+ BECMG 1619 22005KT");
+ or
+ $t->decode("METAR EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
+ or
+ $t->decode("TAF EGSH 311205Z 311322 04010KT 9999 SCT020
+ TEMPO 1319 3000 SHSN BKN008 PROB30
+ TEMPO 1318 0700 +SHSN VV///
+ BECMG 1619 22005KT");
+
+ foreach my $c ($t->chunks) {
+ print $c->as_string, ' ';
+ }
+ or
+ print $self->as_string;
+
+ foreach my $c ($t->chunks) {
+ print $c->as_chunk, ' ';
+ }
+ or
+ print $self->as_chunk_string;
+
+ my @out = $self->as_strings;
+ my @out = $self->as_chunk_strings;
+ my $line = $self->raw;
+ print Geo::TAF::is_weather($line) ? 1 : 0;
+
+=head1 ABSTRACT
+
+Geo::TAF decodes aviation METAR and TAF weather forecast code
+strings into English or, if you sub-class, some other language.
+
+=head1 DESCRIPTION
+
+METAR (Routine Aviation weather Report) and TAF (Terminal Area
+weather Report) are ascii strings containing codes describing
+the weather at airports and weather bureaus around the world.
+
+This module attempts to decode these reports into a form of
+English that is hopefully more understandable than the reports
+themselves.
+
+It is possible to sub-class the translation routines to enable
+translation to other langauages.
+
+=head1 METHODS
+
+=over
+
+=item new(%args)
+
+Constructor for the class. Each weather announcement will need
+a new constructor.
+
+If you sub-class the built-in English translation routines then
+you can pick this up by called the constructor thus:-
+
+ C<my $t = Geo::TAF-E<gt>new(chunk_package =E<gt> 'Geo::TAF::ES');>
+
+or whatever takes your fancy.
+
+=item decode($line)
+
+The main routine that decodes a weather string. It expects a
+string that begins with either the word C<METAR> or C<TAF>.
+It creates a decoded form of the weather string in the object.
+
+There are a number of fixed fields created and also array
+of chunks L<chunks()> of (as default) C<Geo::TAF::EN>.
+
+You can decode these manually or use one of the built-in routines.
+
+This method returns undef if it is successful, a number otherwise.
+You can use L<errorp($r)> routine to get a stringified
+version.
+
+=item metar($line)
+
+This simply adds C<METAR> to the front of the string and calls
+L<decode()>.
+
+=item taf($line)
+
+This simply adds C<TAF> to the front of the string and calls
+L<decode()>.
+
+It makes very little difference to the decoding process which
+of these routines you use. It does, however, affect the output
+in that it will mark it as the appropriate type of report.
+
+=item as_string()
+
+Returns the decoded weather report as a human readable string.
+
+This is probably the simplest and most likely of the output
+options that you might want to use. See also L<as_strings()>.
+
+=item as_strings()
+
+Returns an array of strings without separators. This simply
+the decoded, human readable, normalised strings presented
+as an array.
+
+=item as_chunk_string()
+
+Returns a human readable version of the internal decoded,
+normalised form of the weather report.
+
+This may be useful if you are doing something special, but
+see L<chunks()> or L<as_chunk_strings()> for a procedural
+approach to accessing the internals.
+
+Although you can read the result, it is not, officially,
+human readable.
+
+=item as_chunk_strings()
+
+Returns an array of the stringified versions of the internal
+normalised form without separators.. This simply
+the decoded (English as default) normalised strings presented
+as an array.
+
+=item chunks()
+
+Returns a list of (as default) C<Geo::TAF::EN> objects. You
+can use C<$c-E<gt>as_string> or C<$c-E<gt>as_chunk> to
+translate the internal form into something readable.
+
+If you replace the English versions of these objects then you
+will need at an L<as_string()> method.
+
+=item raw()
+
+Returns the (cleaned up) weather report. It is cleaned up in the
+sense that all whitespace is reduced to exactly one space
+character.
+
+=item errorp($r)
+
+Returns a stringified version of any error returned by L<decode()>
+
+=back
+
+=head1 ACCESSORS
+
+=over
+
+=item taf()
+
+Returns whether this object is a taf or not.
+
+=item icao()
+
+Returns the ICAO code contained in the weather report
+
+=item day()
+
+Returns the day of the month of this report
+
+=item time()
+
+Returns the issue time of this report
+
+=item valid_day()
+
+Returns the day this report is valid for (if there is one).
+
+=item valid_from()
+
+Returns the time from which this report is valid for (if there is one).
+
+=item valid_to()
+
+Returns the time to which this report is valid for (if there is one).
+
+=item viz_dist()
+
+Returns the minimum visibility, if present.
+
+=item viz_units()
+
+Returns the units of the visibility information.
+
+=item wind_dir()
+
+Returns the wind direction in degrees, if present.
+
+=item wind_speed()
+
+Returns the wind speed.
+
+=item wind_units()
+
+Returns the units of wind_speed.
+
+=item wind_gusting()
+
+Returns any wind gust speed. It is possible to have L<wind_speed()>
+without gust information.
+
+=item pressure()
+
+Returns the QNH (altimeter setting atmospheric pressure), if present.
+
+=item pressure_units()
+
+Returns the units in which L<pressure()> is messured.
+
+=item temp()
+
+Returns any temperature present.
+
+=item dewpoint()
+
+Returns any dewpoint present.
+
+=back
+
+=head1 ROUTINES
+
+=over
+
+=item is_weather($line)
+
+This is a routine that determines, fairly losely, whether the
+passed string is likely to be a weather report;
+
+This routine is not exported. You must call it explicitly.
+
+=back
+
+=head1 SEE ALSO
+
+L<Geo::METAR>
+
+For a example of a weather forecast from the Norwich Weather
+Centre (EGSH) see L<http://www.tobit.co.uk>
+
+For data see <ftp://weather.noaa.gov/data/observations/metar/>
+L<ftp://weather.noaa.gov/data/forecasts/taf/> and also
+L<ftp://weather.noaa.gov/data/forecasts/shorttaf/>
+
+To find an ICAO code for your local airport see
+L<http://www.ar-group.com/icaoiata.htm>
+
+=head1 AUTHOR
+
+Dirk Koopman, L<mailto:djk@tobit.co.uk>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2003 by Dirk Koopman, G1TLH
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut