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 (@tok && (($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 (@tok && (($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 my ($fromdir, $todir);
271 if (@tok && (($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/)) {
275 # it could be variable so look at the next token
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);
288 } elsif (my ($u, $p, $punit) = $t =~ /^([QA])(?:NH)?(\d\d\d\d)(INS?)?$/) {
290 if ($u eq 'A' || $punit && $punit =~ /^I/) {
291 $p = sprintf "%.2f", $p / 100;
296 $self->{pressure} ||= $p;
297 $self->{pressure_units} ||= $u;
298 push @chunk, $self->_chunk('PRESS', $p, $u);
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;
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');
315 # viz group in miles and faction of a mile with space between
316 } elsif (my ($m) = $t =~ m!^(\d)$!) {
318 if (@tok && (($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;
422 package Geo::TAF::EN::HEAD;
424 @ISA = qw(Geo::TAF::EN);
429 return "$self->[0] for $self->[1] issued day $self->[2] at $self->[3]";
432 package Geo::TAF::EN::VALID;
434 @ISA = qw(Geo::TAF::EN);
439 return "valid day $self->[0] from $self->[1] till $self->[2]";
443 package Geo::TAF::EN::WIND;
445 @ISA = qw(Geo::TAF::EN);
447 # direction, $speed, $gusts, $unit, $fromdir, $todir
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];
460 package Geo::TAF::EN::PRESS;
462 @ISA = qw(Geo::TAF::EN);
468 return "QNH $self->[0]$self->[1]";
471 # temperature, dewpoint
472 package Geo::TAF::EN::TEMP;
474 @ISA = qw(Geo::TAF::EN);
479 my $out = "temperature $self->[0]C";
480 $out .= " dewpoint $self->[1]C" if defined $self->[1];
485 package Geo::TAF::EN::CLOUD;
487 @ISA = qw(Geo::TAF::EN);
490 VV => 'vertical visibility',
492 CLR => "no cloud no significant weather",
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',
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]}" : "");
519 package Geo::TAF::EN::WEATHER;
521 @ISA = qw(Geo::TAF::EN);
526 'VC' => 'in the vicinity',
531 DR => 'low drifting',
534 TS => 'thunderstorms containing',
542 IC => 'ice crystals',
545 GS => 'small hail/snow pellets',
546 UP => 'unknown precip',
551 VA => 'volcanic ash',
557 PO => 'dust/sand whirls',
562 '+FC' => 'water spouts',
566 'NOSIG' => 'no significant weather',
584 } elsif ($t eq 'VC') {
587 } elsif ($t eq 'SH') {
590 } elsif ($t eq '+' && $self->[0] eq 'FC') {
591 push @out, $wt{'+FC'};
598 if (@out && $shower) {
600 push @out, $wt{'SH'};
603 push @out, $wt{'VC'} if $vic;
605 return join ' ', @out;
608 package Geo::TAF::EN::RVR;
610 @ISA = qw(Geo::TAF::EN);
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';
624 package Geo::TAF::EN::RWY;
626 @ISA = qw(Geo::TAF::EN);
631 my $out = $self->[0] eq 'LDG' ? "landing " : '';
632 $out .= "runway $self->[1]";
636 package Geo::TAF::EN::PROB;
638 @ISA = qw(Geo::TAF::EN);
644 my $out = "probability $self->[0]%";
645 $out .= " $self->[1] till $self->[2]" if defined $self->[1];
649 package Geo::TAF::EN::TEMPO;
651 @ISA = qw(Geo::TAF::EN);
656 my $out = "temporarily";
657 $out .= " $self->[0] till $self->[1]" if defined $self->[0];
662 package Geo::TAF::EN::BECMG;
664 @ISA = qw(Geo::TAF::EN);
669 my $out = "becoming";
670 $out .= " $self->[0] till $self->[1]" if defined $self->[0];
675 package Geo::TAF::EN::VIZ;
677 @ISA = qw(Geo::TAF::EN);
683 return "visibility $self->[0]$self->[1]";
686 package Geo::TAF::EN::FROM;
688 @ISA = qw(Geo::TAF::EN);
694 return "from $self->[0]";
697 package Geo::TAF::EN::TIL;
699 @ISA = qw(Geo::TAF::EN);
705 return "until $self->[0]";
709 # Autoload methods go after =cut, and are processed by the autosplit program.
713 # Below is stub documentation for your module. You'd better edit it!
717 Geo::TAF - Decode METAR and TAF strings
724 my $t = new Geo::TAF;
726 $t->metar("EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
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");
733 $t->decode("METAR EGSH 311420Z 29010KT 1600 SHSN SCT004 BKN006 01/M00 Q1021");
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");
740 foreach my $c ($t->chunks) {
741 print $c->as_string, ' ';
744 print $self->as_string;
746 foreach my $c ($t->chunks) {
747 print $c->as_chunk, ' ';
750 print $self->as_chunk_string;
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;
759 Geo::TAF decodes aviation METAR and TAF weather forecast code
760 strings into English or, if you sub-class, some other language.
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.
768 This module attempts to decode these reports into a form of
769 English that is hopefully more understandable than the reports
772 It is possible to sub-class the translation routines to enable
773 translation to other langauages.
781 Constructor for the class. Each weather announcement will need
784 If you sub-class the built-in English translation routines then
785 you can pick this up by called the constructor thus:-
787 C<my $t = Geo::TAF-E<gt>new(chunk_package =E<gt> 'Geo::TAF::ES');>
789 or whatever takes your fancy.
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.
797 There are a number of fixed fields created and also array
798 of chunks L<chunks()> of (as default) C<Geo::TAF::EN>.
800 You can decode these manually or use one of the built-in routines.
802 This method returns undef if it is successful, a number otherwise.
803 You can use L<errorp($r)> routine to get a stringified
808 This simply adds C<METAR> to the front of the string and calls
813 This simply adds C<TAF> to the front of the string and calls
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.
822 Returns the decoded weather report as a human readable string.
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()>.
829 Returns an array of strings without separators. This simply
830 the decoded, human readable, normalised strings presented
833 =item as_chunk_string()
835 Returns a human readable version of the internal decoded,
836 normalised form of the weather report.
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.
842 Although you can read the result, it is not, officially,
845 =item as_chunk_strings()
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
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.
858 If you replace the English versions of these objects then you
859 will need at an L<as_string()> method.
863 Returns the (cleaned up) weather report. It is cleaned up in the
864 sense that all whitespace is reduced to exactly one space
869 Returns a stringified version of any error returned by L<decode()>
879 Returns whether this object is a taf or not.
883 Returns the ICAO code contained in the weather report
887 Returns the day of the month of this report
891 Returns the issue time of this report
895 Returns the day this report is valid for (if there is one).
899 Returns the time from which this report is valid for (if there is one).
903 Returns the time to which this report is valid for (if there is one).
907 Returns the minimum visibility, if present.
911 Returns the units of the visibility information.
915 Returns the wind direction in degrees, if present.
919 Returns the wind speed.
923 Returns the units of wind_speed.
927 Returns any wind gust speed. It is possible to have L<wind_speed()>
928 without gust information.
932 Returns the QNH (altimeter setting atmospheric pressure), if present.
934 =item pressure_units()
936 Returns the units in which L<pressure()> is messured.
940 Returns any temperature present.
944 Returns any dewpoint present.
952 =item is_weather($line)
954 This is a routine that determines, fairly losely, whether the
955 passed string is likely to be a weather report;
957 This routine is not exported. You must call it explicitly.
965 For a example of a weather forecast from the Norwich Weather
966 Centre (EGSH) see L<http://www.tobit.co.uk>
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/>
972 To find an ICAO code for your local airport see
973 L<http://www.ar-group.com/icaoiata.htm>
977 Dirk Koopman, L<mailto:djk@tobit.co.uk>
979 =head1 COPYRIGHT AND LICENSE
981 Copyright (c) 2003 by Dirk Koopman, G1TLH
983 This library is free software; you can redistribute it and/or modify
984 it under the same terms as Perl itself.