add these back as well
authorminima <minima>
Thu, 2 Nov 2006 14:08:29 +0000 (14:08 +0000)
committerminima <minima>
Thu, 2 Nov 2006 14:08:29 +0000 (14:08 +0000)
Geo/TAF/Changes [new file with mode: 0644]
Geo/TAF/MANIFEST [new file with mode: 0644]
Geo/TAF/Makefile.PL [new file with mode: 0644]
Geo/TAF/README [new file with mode: 0644]
Geo/TAF/TAF.pm [new file with mode: 0644]

diff --git a/Geo/TAF/Changes b/Geo/TAF/Changes
new file mode 100644 (file)
index 0000000..bf41101
--- /dev/null
@@ -0,0 +1,18 @@
+Revision history for Perl extension Geo::TAF.
+
+1.01  Fri Jan 31 15:08:17 2003
+       - original version; created by h2xs 1.22 with options
+               --skip-exporter --skip-autoloader -Xb 5.5.0 -n Geo::TAF
+
+1.02  Mon Feb  3 01:28:00 2003
+       - fixed some uninitialised data errors in TAF.pm
+       - included example scgi_weather.pl
+
+1.03  Mon Feb  3 17:00:00 2003
+       - swapped BKN with SCT
+       - added dayend function in Geo::TAF::EN
+       - detail changes to example/scgi_weather.pl
+       - added some tests
+
+1.04  Mon Feb  3 17:40:00 2003
+       - fix typo in scgi_weather.pl
diff --git a/Geo/TAF/MANIFEST b/Geo/TAF/MANIFEST
new file mode 100644 (file)
index 0000000..30444d4
--- /dev/null
@@ -0,0 +1,11 @@
+Changes
+Makefile.PL
+MANIFEST
+README
+TAF.pm
+example/cmd_chunks.pl
+example/cmd_taf.pl
+example/fetch_weather.pl
+example/cgi_weather.pl
+example/scgi_weather.pl
+t/1.t
diff --git a/Geo/TAF/Makefile.PL b/Geo/TAF/Makefile.PL
new file mode 100644 (file)
index 0000000..48e01cb
--- /dev/null
@@ -0,0 +1,12 @@
+use 5.005;
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    'NAME'             => 'Geo::TAF',
+    'VERSION_FROM'     => 'TAF.pm', # finds $VERSION
+    'PREREQ_PM'                => {}, # e.g., Module::Name => 1.1
+    ($] >= 5.005 ?    ## Add these new keywords supported since 5.005
+      (ABSTRACT_FROM => 'TAF.pm', # retrieve abstract from module
+       AUTHOR     => 'Dirk Koopman <djk@localdomain>') : ()),
+);
diff --git a/Geo/TAF/README b/Geo/TAF/README
new file mode 100644 (file)
index 0000000..a12b490
--- /dev/null
@@ -0,0 +1,34 @@
+Geo/TAF version 1.01
+====================
+
+Geo::TAF decodes aviation METAR and TAF weather forecast code 
+strings into English or, if you sub-class, some other language.
+
+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. 
+
+INSTALLATION
+
+To install this module type the following:
+
+   perl Makefile.PL
+   make
+   make test
+   make install
+
+DEPENDENCIES
+
+   None
+
+COPYRIGHT AND LICENCE
+
+Copyright (C) 2003 Dirk Koopman G1TLH
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself. 
+
diff --git a/Geo/TAF/TAF.pm b/Geo/TAF/TAF.pm
new file mode 100644 (file)
index 0000000..d394f58
--- /dev/null
@@ -0,0 +1,1003 @@
+#
+# 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.04';
+
+
+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;
+       return join ' ', $self->as_strings;
+}
+
+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 (@tok && (($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 (@tok && (($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)$/) {
+                       
+                       my ($fromdir, $todir);
+                       
+                       if      (@tok && (($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/)) {
+                               shift @tok;
+                       }
+                       
+                       # it could be variable so look at the next token
+
+                       $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?)?$/) {
+
+                       $p = 0 + $p;
+                       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)$!) {
+                       my $viz;
+                       if (@tok && (($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 ($package, $name) = $AUTOLOAD =~ /^(.*)::(\w+)$/;
+       return if $name eq 'DESTROY';
+
+       *$AUTOLOAD = sub {return $_[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;
+}
+
+sub day
+{
+       my $pkg = shift;
+       my $d = sprintf "%d", ref($pkg) ? shift : $pkg;
+       if ($d =~ /1$/) {
+               return "${d}st";
+       } elsif ($d =~ /2$/) {
+               return "${d}nd";
+       } elsif ($d =~ /3$/) {
+               return "${d}rd";
+       }
+       return "${d}th";
+}
+
+
+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 at $self->[3] on " . $self->day($self->[2]);
+}
+
+package Geo::TAF::EN::VALID;
+use vars qw(@ISA);
+@ISA = qw(Geo::TAF::EN);
+
+sub as_string
+{
+       my $self = shift;
+       return "valid from $self->[1] to $self->[2] on " . $self->day($self->[0]);
+}
+
+
+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 => "3-4 oktas",
+                 BKN => "5-7 oktas",
+                 FEW => "0-2 oktas",
+                 OVC => "8 oktas overcast",
+                 CAVOK => "no cloud below 5000ft >10Km visibility no significant weather (CAVOK)",
+                 CB => 'thunder clouds',
+          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] to $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] to $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] to $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:-
+  my $t = Geo::TAF->new(chunk_package => '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. There
+is also a routine (C<$c-E<gt>day>)to turn a day number into 
+things like "1st", "2nd" and "24th". 
+
+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 L<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