2 # A set of routine for decode TAF and METAR a bit better and more comprehensively
3 # than some other products I tried.
7 # Copyright (c) 2003 Dirk Koopman G1TLH
14 use vars qw($VERSION);
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",
46 # Preloaded methods go here.
51 my $self = bless {@_}, $pkg;
52 $self->{chunk_package} ||= "Geo::TAF::EN";
60 return 2 unless length $l > 10;
61 $l = 'METAR ' . $l unless $l =~ /^\s*(?:METAR|TAF)\s/i;
62 return $self->decode($l);
69 return 2 unless length $l > 10;
70 $l = 'TAF ' . $l unless $l =~ /^\s*(?:METAR|TAF)\s/i;
71 return $self->decode($l);
77 return join ' ', $self->as_strings;
84 for (@{$self->{chunks}}) {
85 push @out, $_->as_string;
93 return exists $self->{chunks} ? @{$self->{chunks}} : ();
101 for (@{$self->{chunks}}) {
102 push @out, $_->as_chunk;
110 return join ' ', $self->as_chunk_strings;
115 return shift->{line};
120 return $_[0] =~ /^\s*(?:(?:METAR|TAF)\s+)?[A-Z]{4}\s+\d{6}Z?\s+/;
127 return $err{"$code"};
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
139 my @tok = split /\s+/, $l;
141 $self->{line} = join ' ', @tok;
144 # do we explicitly have a METAR or a TAF
148 } elsif ($t eq 'METAR') {
154 # next token is the ICAO dseignator
156 if ($t =~ /^[A-Z]{4}$/) {
162 # next token is an issue time
164 if (my ($day, $time) = $t =~ /^(\d\d)(\d{4})Z?$/) {
166 $self->{time} = _time($time);
171 # if it is a TAF then expect a validity (may be missing)
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);
181 # we are now into the 'list' of things that can repeat over and over
184 $self->_chunk('HEAD', $self->{taf} ? 'TAF' : 'METAR',
185 $self->{icao}, $self->{day}, $self->{time})
188 push @chunk, $self->_chunk('VALID', $self->{valid_day}, $self->{valid_from},
189 $self->{valid_to}) if $self->{valid_day};
195 if ($t eq 'TEMPO' || $t eq 'BECMG') {
197 # next token may be a time if it is a taf
199 if (($from, $to) = $tok[0] =~ /^(\d\d)(\d\d)$/) {
200 if ($self->{taf} && $from >= 0 && $from <= 24 && $to >= 0 && $to <= 24) {
202 $from = _time($from * 100);
203 $to = _time($to * 100);
209 push @chunk, $self->_chunk($t, $from, $to);
212 } elsif ($ignore{$t}) {
216 } elsif ($t eq 'NOSIG' || $t eq 'NSW') {
217 push @chunk, $self->_chunk('WEATHER', 'NOSIG');
219 # specific broken on its own
220 } elsif ($t eq 'BKN') {
221 push @chunk, $self->_chunk('WEATHER', $t);
223 # other 3 letter codes
225 push @chunk, $self->_chunk('CLOUD', $t);
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');
233 # RMK group (end for now)
234 } elsif ($t eq 'RMK') {
238 } elsif (my ($time) = $t =~ /^FM(\d\d\d\d)$/ ) {
239 push @chunk, $self->_chunk('FROM', _time($time));
242 } elsif (($time) = $t =~ /^TL(\d\d\d\d)$/ ) {
243 push @chunk, $self->_chunk('TIL', _time($time));
246 } elsif (my ($percent) = $t =~ /^PROB(\d\d)$/ ) {
248 # next token may be a time if it is a taf
250 if (($from, $to) = $tok[0] =~ /^(\d\d)(\d\d)$/) {
251 if ($self->{taf} && $from >= 0 && $from <= 24 && $to >= 0 && $to <= 24) {
253 $from = _time($from * 100);
254 $to = _time($to * 100);
260 push @chunk, $self->_chunk('PROB', $percent, $from, $to);
263 } elsif (my ($sort, $dir) = $t =~ /^(RWY?|LDG)(\d\d[RLC]?)$/ ) {
264 push @chunk, $self->_chunk('RWY', $sort, $dir);
267 } elsif (my ($wdir, $spd, $gust, $unit) = $t =~ /^(\d\d\d|VRB)(\d\d)(?:G(\d\d))?(KT|MPH|MPS|KMH)$/) {
269 # the next word might be 'AUTO'
270 if ($tok[0] eq 'AUTO') {
274 # it could be variable so look at the next token
276 my ($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/;
277 shift @tok if defined $fromdir;
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);
289 } elsif (my ($u, $p, $punit) = $t =~ /^([QA])(?:NH)?(\d\d\d\d)(INS?)?$/) {
291 if ($u eq 'A' || $punit && $punit =~ /^I/) {
292 $p = sprintf "%.2f", $p / 100;
297 $self->{pressure} ||= $p;
298 $self->{pressure_units} ||= $u;
299 push @chunk, $self->_chunk('PRESS', $p, $u);
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;
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');
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$!) {
321 $self->{viz_dist} ||= $viz;
322 $self->{viz_units} ||= 'miles';
323 push @chunk, $self->_chunk('VIZ', $viz, 'miles');
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');
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;
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);
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);
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 '///';
353 } elsif (my ($ms, $t, $n, $d) = $t =~ m!^(M)?(\d\d)/(M)?(\d\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);
364 $self->{chunks} = \@chunk;
373 $pkg = $self->{chunk_package} . '::' . $pkg;
374 return $pkg->new(@_);
379 return sprintf "%02d:%02d", unpack "a2a2", sprintf "%04d", shift;
386 my $name = $AUTOLOAD;
387 return if $name =~ /::DESTROY$/;
390 *$AUTOLOAD = sub { $_[0]->{$name}};
395 # these are the translation packages
397 # First the factory method
400 package Geo::TAF::EN;
405 return bless [@_], $pkg;
411 my ($n) = (ref $self) =~ /::(\w+)$/;
412 return '[' . join(' ', $n, map {defined $_ ? $_ : '?'} @$self) . ']';
418 my ($n) = (ref $self) =~ /::(\w+)$/;
419 return join ' ', ucfirst $n, map {defined $_ ? $_ : ()} @$self;
426 my $name = $AUTOLOAD;
427 return if $name =~ /::DESTROY$/;
430 *$AUTOLOAD = sub { $_[0]->{$name}};
434 package Geo::TAF::EN::HEAD;
436 @ISA = qw(Geo::TAF::EN);
441 return "$self->[0] for $self->[1] issued day $self->[2] at $self->[3]";
444 package Geo::TAF::EN::VALID;
446 @ISA = qw(Geo::TAF::EN);
451 return "valid day $self->[0] from $self->[1] till $self->[2]";
455 package Geo::TAF::EN::WIND;
457 @ISA = qw(Geo::TAF::EN);
459 # direction, $speed, $gusts, $unit, $fromdir, $todir
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];
472 package Geo::TAF::EN::PRESS;
474 @ISA = qw(Geo::TAF::EN);
480 return "QNH $self->[0]$self->[1]";
483 # temperature, dewpoint
484 package Geo::TAF::EN::TEMP;
486 @ISA = qw(Geo::TAF::EN);
491 my $out = "temperature $self->[0]C";
492 $out .= " dewpoint $self->[1]C" if defined $self->[1];
497 package Geo::TAF::EN::CLOUD;
499 @ISA = qw(Geo::TAF::EN);
502 VV => 'vertical visibility',
504 CLR => "no cloud no significant weather",
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',
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]}" : "");
531 package Geo::TAF::EN::WEATHER;
533 @ISA = qw(Geo::TAF::EN);
538 'VC' => 'in the vicinity',
543 DR => 'low drifting',
546 TS => 'thunderstorms containing',
554 IC => 'ice crystals',
557 GS => 'small hail/snow pellets',
558 UP => 'unknown precip',
563 VA => 'volcanic ash',
569 PO => 'dust/sand whirls',
574 '+FC' => 'water spouts',
578 'NOSIG' => 'no significant weather',
596 } elsif ($t eq 'VC') {
599 } elsif ($t eq 'SH') {
602 } elsif ($t eq '+' && $self->[0] eq 'FC') {
603 push @out, $wt{'+FC'};
610 if (@out && $shower) {
612 push @out, $wt{'SH'};
615 push @out, $wt{'VC'} if $vic;
617 return join ' ', @out;
620 package Geo::TAF::EN::RVR;
622 @ISA = qw(Geo::TAF::EN);
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';
636 package Geo::TAF::EN::RWY;
638 @ISA = qw(Geo::TAF::EN);
643 my $out = $self->[0] eq 'LDG' ? "landing " : '';
644 $out .= "runway $self->[1]";
648 package Geo::TAF::EN::PROB;
650 @ISA = qw(Geo::TAF::EN);
656 my $out = "probability $self->[0]%";
657 $out .= " $self->[1] till $self->[2]" if defined $self->[1];
661 package Geo::TAF::EN::TEMPO;
663 @ISA = qw(Geo::TAF::EN);
668 my $out = "temporarily";
669 $out .= " $self->[0] till $self->[1]" if defined $self->[0];
674 package Geo::TAF::EN::BECMG;
676 @ISA = qw(Geo::TAF::EN);
681 my $out = "becoming";
682 $out .= " $self->[0] till $self->[1]" if defined $self->[0];
687 package Geo::TAF::EN::VIZ;
689 @ISA = qw(Geo::TAF::EN);
695 return "visibility $self->[0]$self->[1]";
698 package Geo::TAF::EN::FROM;
700 @ISA = qw(Geo::TAF::EN);
706 return "from $self->[0]";
709 package Geo::TAF::EN::TIL;
711 @ISA = qw(Geo::TAF::EN);
717 return "until $self->[0]";
721 # Autoload methods go after =cut, and are processed by the autosplit program.
725 # Below is stub documentation for your module. You'd better edit it!
729 Geo::TAF - Decode METAR and TAF strings
736 my $t = new Geo::TAF;
738 $t->metar("EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
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");
745 $t->decode("METAR EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
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");
752 foreach my $c ($t->chunks) {
753 print $c->as_string, ' ';
756 print $self->as_string;
758 foreach my $c ($t->chunks) {
759 print $c->as_chunk, ' ';
762 print $self->as_chunk_string;
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;
771 Geo::TAF decodes aviation METAR and TAF weather forecast code
772 strings into English or, if you sub-class, some other language.
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.
780 This module attempts to decode these reports into a form of
781 English that is hopefully more understandable than the reports
784 It is possible to sub-class the translation routines to enable
785 translation to other langauages.
793 Constructor for the class. Each weather announcement will need
796 If you sub-class the built-in English translation routines then
797 you can pick this up by called the constructor thus:-
799 C<my $t = Geo::TAF-E<gt>new(chunk_package =E<gt> 'Geo::TAF::ES');>
801 or whatever takes your fancy.
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.
809 There are a number of fixed fields created and also array
810 of chunks L<chunks()> of (as default) C<Geo::TAF::EN>.
812 You can decode these manually or use one of the built-in routines.
814 This method returns undef if it is successful, a number otherwise.
815 You can use L<errorp($r)> routine to get a stringified
820 This simply adds C<METAR> to the front of the string and calls
825 This simply adds C<TAF> to the front of the string and calls
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.
834 Returns the decoded weather report as a human readable string.
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()>.
841 Returns an array of strings without separators. This simply
842 the decoded, human readable, normalised strings presented
845 =item as_chunk_string()
847 Returns a human readable version of the internal decoded,
848 normalised form of the weather report.
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.
854 Although you can read the result, it is not, officially,
857 =item as_chunk_strings()
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
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.
870 If you replace the English versions of these objects then you
871 will need at an L<as_string()> method.
875 Returns the (cleaned up) weather report. It is cleaned up in the
876 sense that all whitespace is reduced to exactly one space
881 Returns a stringified version of any error returned by L<decode()>
891 Returns whether this object is a taf or not.
895 Returns the ICAO code contained in the weather report
899 Returns the day of the month of this report
903 Returns the issue time of this report
907 Returns the day this report is valid for (if there is one).
911 Returns the time from which this report is valid for (if there is one).
915 Returns the time to which this report is valid for (if there is one).
919 Returns the minimum visibility, if present.
923 Returns the units of the visibility information.
927 Returns the wind direction in degrees, if present.
931 Returns the wind speed.
935 Returns the units of wind_speed.
939 Returns any wind gust speed. It is possible to have L<wind_speed()>
940 without gust information.
944 Returns the QNH (altimeter setting atmospheric pressure), if present.
946 =item pressure_units()
948 Returns the units in which L<pressure()> is messured.
952 Returns any temperature present.
956 Returns any dewpoint present.
964 =item is_weather($line)
966 This is a routine that determines, fairly losely, whether the
967 passed string is likely to be a weather report;
969 This routine is not exported. You must call it explicitly.
977 For a example of a weather forecast from the Norwich Weather
978 Centre (EGSH) see L<http://www.tobit.co.uk>
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/>
984 To find an ICAO code for your local airport see
985 L<http://www.ar-group.com/icaoiata.htm>
989 Dirk Koopman, L<mailto:djk@tobit.co.uk>
991 =head1 COPYRIGHT AND LICENSE
993 Copyright (c) 2003 by Dirk Koopman, G1TLH
995 This library is free software; you can redistribute it and/or modify
996 it under the same terms as Perl itself.