File Coverage

blib/lib/Geo/METARTAF.pm
Criterion Covered Total %
statement 71 443 16.0
branch 0 240 0.0
condition 0 176 0.0
subroutine 24 59 40.6
pod 9 9 100.0
total 104 927 11.2


line stmt bran cond sub pod time code
1             # A module to decode North American METAR's and TAF's
2             # Based on Geo::TAF (Designed for European METAR's/TAF's)
3             # but updated with information from Transport Canada AIM.
4             # These changes may cause the module to fail with European data.
5             # Copyright (c) 2025 Peter Carter
6             # Version Date Description
7             # ------- ---------- --------------------------------------
8             # 1.00 2025-01-03 First version
9             # 1.01 2025-01-12 Fixed issue when decoding 11th, 12th,
10             # and 13th (they were displayed as 11st,
11             # 12nd, and 13rd).
12             # 1.02 2025-01-13 Fixed two issues: error code display
13             # and vicinity (VC) phenomena display.
14             # 1.03 2025-01-21 Fixed an issue with decoding certain RVR
15             # data and resolved the errors generated in
16             # the Apache log when used by a CGI script.
17              
18             package Geo::METARTAF;
19              
20 1     1   94352 use 5.005;
  1         3  
21 1     1   6 use strict;
  1         1  
  1         30  
22 1     1   5 use vars qw($VERSION);
  1         1  
  1         4071  
23              
24             $VERSION = '1.03';
25              
26             my %err = (
27             '0' => "",
28             '1' => "No valid ICAO designator",
29             '2' => "Length is less than 15 characters",
30             '3' => "No valid issue time",
31             '4' => "Expecting METAR, SPECI, or TAF at the beginning",
32             );
33              
34             my %clt = (
35             SKC => 1,
36             CLR => 1,
37             NSC => 1,
38             BLU => 1,
39             WHT => 1,
40             GRN => 1,
41             YLO => 1,
42             AMB => 1,
43             RED => 1,
44             BKN => 1,
45             NIL => 1,
46             );
47              
48             my %ignore = (
49             AUTO => 1,
50             COR => 1,
51             );
52              
53             my $report_type = '';
54            
55             # Module methods
56              
57             # Create a new object
58             sub new
59             {
60 0     0 1   my $pkg = shift;
61 0           my $self = bless {@_}, $pkg;
62 0   0       $self->{decode_language} ||= "Geo::METARTAF::EN";
63 0           return $self;
64             }
65              
66             # Precede input data with 'METAR' and decode
67             sub metar
68             {
69 0     0 1   my $self = shift;
70 0           my $l = shift;
71 0 0         $l = 'METAR ' . $l unless $l =~ /^\s*(?:METAR|TAF|SPECI)\s/i;
72 0           return $self->decode($l);
73             }
74              
75             # Precede input data with 'SPECI' and decode
76             sub speci
77             {
78 0     0 1   my $self = shift;
79 0           my $l = shift;
80 0 0         $l = 'SPECI ' . $l unless $l =~ /^\s*(?:METAR|TAF|SPECI)\s/i;
81 0           return $self->decode($l);
82             }
83              
84             # Precede input data with 'TAF' and decode
85             sub taf
86             {
87 0     0 1   my $self = shift;
88 0           my $l = shift;
89 0 0         $l = 'TAF ' . $l unless $l =~ /^\s*(?:METAR|TAF|SPECI)\s/i;
90 0           return $self->decode($l);
91             }
92              
93             # Format and print decoded data as a string
94             sub as_string
95             {
96 0     0 1   my $self = shift;
97 0           my $decoded_data = '';
98 0           my $subsection = '';
99 0           for (@{$self->{sections}}) {
  0            
100 0           my $line = $_->as_string;
101 0 0         if ($line =~ /^~/) {
102 0           my ($section_name, $section_data) = $line =~ /^~(.+?:)(.+)/;
103 0 0         if ($subsection eq $section_name) {
104 0           $decoded_data .= " $section_data\n";
105             }
106             else {
107 0           $decoded_data .= " $section_name\n";
108 0           $decoded_data .= " $section_data\n";
109 0           $subsection = $section_name;
110             }
111             }
112             else {
113 0           $decoded_data .= "$line\n";
114 0           $subsection = '';
115             }
116             }
117 0           return $decoded_data;
118             }
119              
120             # Format and print decoded data as an HTML table
121             sub as_html
122             {
123 0     0 1   my $self = shift;
124 0           my $decoded_data = '' . "\n"; \n\n" if ($endTr); \n"; ' . "\n"; \n\n"; \n" . '\n\n"; \n" . '\n\n"; \n" . '\n\n"; \n" . '\n\n"; \n" . '\n\n"; \n" . '\n\n"; \n" . '\n\n"; \n" . '\n\n"; \n\n" if ($endTr);
125 0           my $subsection = '';
126 0           my $endTr = 0;
127 0           for (@{$self->{sections}}) {
  0            
128 0           my $line = $_->as_string;
129 0           chomp $line;
130 0 0         if ($line =~ /^~/) {
131 0           my ($section_name, $section_data) = $line =~ /^~(.+?:)(.+)/;
132 0 0         if ($subsection eq $section_name) {
133 0           $decoded_data .= '
' . $section_data;
134             }
135             else {
136 0 0         $decoded_data .= "
137 0           $decoded_data .= "
138 0           $decoded_data .= ' ' . $section_name . '
139 0           $decoded_data .= ' ' . $section_data;
140 0           $subsection = $section_name;
141 0           $endTr = 1;
142             }
143             }
144             else {
145 0 0         if ($endTr) {
146 0           $decoded_data .= "
147 0           $endTr = 0;
148             }
149 0 0         if ($line =~ /METAR|SPECI|TAF/) {
    0          
    0          
    0          
    0          
    0          
    0          
150 0           $decoded_data .= "
' . $line . "
151             }
152             elsif ($line =~ /^Valid/) {
153 0           $decoded_data .= "
' . $line . "
154             }
155             elsif ($line =~ /^Temporarily/) {
156 0           $decoded_data .= "
' . $line . "
157             }
158             elsif ($line =~ /^Probability/) {
159 0           $decoded_data .= "
' . $line . "
160             }
161             elsif ($line =~ /^Becoming/) {
162 0           $decoded_data .= "
' . $line . "
163             }
164             elsif ($line =~ /^From/) {
165 0           $decoded_data .= "
' . $line . "
166             }
167             elsif ($line =~ /^Until/) {
168 0           $decoded_data .= "
' . $line . "
169             }
170             else {
171 0           $decoded_data .="
' . $line . "
172             }
173 0           $subsection = '';
174             }
175             }
176 0 0         $decoded_data .= "
177 0           $decoded_data .= "
";
178 0 0         $decoded_data =~ s/°/°/g if $decoded_data =~ /°/;
179 0           return $decoded_data;
180             }
181              
182             # Return input data with excess spaces removed
183             sub minimal
184             {
185 0     0 1   return shift->{line};
186             }
187              
188             # Return error codes
189             sub error
190             {
191 0     0 1   my $self = shift;
192 0           return $err{$self->error_code} . "\n";
193             }
194              
195             # Decode input data
196             sub decode
197             {
198 0     0 1   my $self = shift;
199 0           my $l = uc shift;
200              
201 0           $self->{amendedOrCorrected} = '';
202              
203 0           $l =~ s/=$//;
204              
205 0 0         unless (length $l > 15) {
206 0           $self->{error_code} = 2;
207 0           return;
208             }
209            
210 0           my @tok = split /\s+/, $l;
211              
212 0           $self->{line} = join ' ', @tok;
213            
214             # Do we explicitly have a METAR, SPECI, or a TAF
215 0           my $t = shift @tok;
216 0 0 0       if ($t eq 'TAF') {
    0          
217 0           $self->{taf} = 1;
218 0           $self->{report} = $t;
219             } elsif ($t eq 'METAR' || $t eq 'SPECI') {
220 0           $self->{taf} = 0;
221 0           $self->{report} = $t;
222             } else {
223 0           $self->{error_code} = 4;
224 0           return;
225             }
226              
227             # The next token may be "AMD" (amended) if it is a TAF
228 0 0 0       if ($self->{taf} && $tok[0] eq 'AMD') {
229 0           $self->{amendedOrCorrected} = '(Amended/Corrected)';
230 0           shift @tok;
231             }
232              
233             # The next token is the ICAO designator
234 0           $t = shift @tok;
235 0 0         if ($t =~ /^[A-Z]{4}$/) {
236 0           $self->{icao} = $t;
237             } else {
238 0           $self->{error_code} = 1;
239 0           return;
240             }
241              
242             # The next token is the issue / observation date and time
243 0           $t = shift @tok;
244 0 0         if (my ($day, $time) = $t =~ /^(\d\d)(\d{4})Z?$/) {
245 0           $self->{day} = $day;
246 0           $self->{time} = _time($time) . ' UTC';
247             } else {
248 0           $self->{error_code} = 3;
249 0           return;
250             }
251              
252             # The next token may be "CCx" (corrected) and/or AUTO (AWOS) if it is a METAR/SPECI
253 0   0       while (!$self->{taf} && $tok[0] =~ /^AUTO|CC[A-Z]$/) {
254 0 0         if ($tok[0] eq 'AUTO') {
255 0           $self->{amendedOrCorrected} .= 'reported by AWOS ';
256 0           shift @tok;
257             }
258             else {
259 0           my ($revLetter) = $tok[0] =~ /CC([A-Z])/;
260 0           my $revNumber = ord($revLetter) - 64;
261 0           $self->{amendedOrCorrected} .= "(Correction # $revNumber) ";
262 0           shift @tok;
263             }
264             }
265              
266             # If it is a TAF then expect a validity period
267 0 0         if ($self->{taf}) {
268 0 0         if (my ($v_from_day, $v_from_hour, $v_to_day, $v_to_hour) = $tok[0] =~ /^(\d{2})(\d{2})\/(\d{2})(\d{2})$/) {
269 0           $self->{valid_from_day} = $v_from_day;
270 0           $self->{valid_from_hour} = _time($v_from_hour * 100) . ' UTC';
271 0           $self->{valid_to_day} = $v_to_day;
272 0           $self->{valid_to_hour} = _time($v_to_hour * 100) . ' UTC';
273 0           $self->{valid_from} = "$v_from_hour:00 UTC on $v_from_day";
274 0           $self->{valid_to} = "$v_to_hour:00 UTC on $v_to_day";
275 0           shift @tok;
276             }
277             }
278              
279             # Next is the 'list' of things that can repeat over and over
280              
281 0           my $ceiling = 100000;
282              
283             my @section = (
284             $self->_section('HEAD', $self->{report}, $self->{icao}, $self->{day}, $self->{time}, $self->{amendedOrCorrected})
285 0           );
286            
287 0 0         push @section, $self->_section('VALID', $self->{valid_from_day}, $self->{valid_from_hour}, $self->{valid_to_day}, $self->{valid_to_hour}) if $self->{valid_from_day};
288              
289 0           while (@tok) {
290 0           $t = shift @tok;
291            
292             # Temporary or Becoming
293 0 0 0       if ($t eq 'TEMPO' || $t eq 'BECMG') {
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
294 0 0         push @section, $self->_section('CEIL', $ceiling) if ($ceiling < 100000);
295 0           $ceiling = 100000;
296             # Next token should be a time if it is a TAF
297 0           my ($from_day, $from_hour, $to_day, $to_hour);
298 0 0 0       if (@tok && (($from_day, $from_hour, $to_day, $to_hour) = $tok[0] =~ /^(\d\d)(\d\d)\/(\d\d)(\d\d)$/)) {
299 0 0 0       if ($self->{taf} && $from_hour >= 0 && $from_hour <= 24 && $to_hour >= 0 && $to_hour <= 24) {
      0        
      0        
      0        
300 0           shift @tok;
301 0           $from_hour = _time($from_hour * 100);
302 0           $to_hour = _time($to_hour * 100);
303             } else {
304 0           undef $from_day;
305 0           undef $from_hour;
306 0           undef $from_day;
307 0           undef $to_hour;
308             }
309             }
310 0           push @section, $self->_section($t, $from_day, $from_hour, $to_day, $to_hour);
311              
312             # Ignore
313             } elsif ($ignore{$t}) {
314             ;
315            
316             # No significant weather
317             } elsif ($t eq 'NOSIG' || $t eq 'NSW') {
318 0           push @section, $self->_section('WEATHER', 'NOSIG');
319              
320             # Specify broken on its own
321             } elsif ($t eq 'BKN') {
322 0           push @section, $self->_section('WEATHER', $t);
323            
324             # Other 3 letter codes
325             } elsif ($clt{$t}) {
326 0           push @section, $self->_section('CLOUD', $t);
327            
328             # EU CAVOK visibility > 10000m, no cloud, no significant weather
329             } elsif ($t eq 'CAVOK') {
330 0   0       $self->{visibility_dist} ||= ">10000";
331 0   0       $self->{visibility_units} ||= 'm';
332 0           push @section, $self->_section('CLOUD', 'CAVOK');
333              
334             # RMK group (Display ceiling, if one exists, and end)
335             } elsif ($t eq 'RMK') {
336 0 0         push @section, $self->_section('CEIL', $ceiling) if ($ceiling < 100000);
337 0           last;
338              
339             # From
340             } elsif (my ($fromDay, $fromTime) = $t =~ /^FM(\d{2})(\d{4})$/ ) {
341 0 0         push @section, $self->_section('CEIL', $ceiling) if ($ceiling < 100000);
342 0           $ceiling = 100000;
343 0           push @section, $self->_section('FROM', "$fromDay-" . _time($fromTime));
344              
345             # Until
346             } elsif (my ($tilDay, $tilTime) = $t =~ /^TL(\d{2})(\d{4})$/ ) {
347 0 0         push @section, $self->_section('CEIL', $ceiling) if ($ceiling < 100000);
348 0           $ceiling = 100000;
349 0           push @section, $self->_section('TIL', "$tilDay-" . _time($tilTime));
350              
351             # Probability
352             } elsif (my ($percent) = $t =~ /^PROB(\d\d)$/ ) {
353 0 0         push @section, $self->_section('CEIL', $ceiling) if ($ceiling < 100000);
354 0           $ceiling = 100000;
355             # Next token may be a time if it is a TAF
356 0           my ($from_day, $from_hour, $to_day, $to_hour);
357 0 0 0       if (@tok && (($from_day, $from_hour, $to_day, $to_hour) = $tok[0] =~ /^(\d\d)(\d\d)\/(\d\d)(\d\d)$/)) {
358 0 0 0       if ($self->{taf} && $from_hour >= 0 && $from_hour <= 24 && $to_hour >= 0 && $to_hour <= 24) {
      0        
      0        
      0        
359 0           shift @tok;
360 0           $from_hour = _time($from_hour * 100);
361 0           $to_hour = _time($to_hour * 100);
362             } else {
363 0           undef $from_day;
364 0           undef $from_hour;
365 0           undef $from_day;
366 0           undef $to_hour;
367             }
368             }
369 0           push @section, $self->_section('PROB', $percent, $from_day, $from_hour, $to_day, $to_hour);
370              
371             # Runway
372             } elsif (my ($sort, $dir) = $t =~ /^(RWY?|LDG)(\d\d[RLC]?)$/ ) {
373 0           push @section, $self->_section('RWY', $sort, $dir);
374              
375             # Wind
376             } elsif (my ($wdir, $spd, $gust, $unit) = $t =~ /^(\d\d\d|VRB)(\d\d)(?:G(\d\d))?(KT|MPH|MPS|KMH)$/) {
377            
378 0           my ($fromdir, $todir);
379            
380 0 0 0       if (@tok && (($fromdir, $todir) = $tok[0] =~ /^(\d\d\d)V(\d\d\d)$/)) {
381 0           shift @tok;
382             }
383            
384             # It could be variable so look at the next token
385              
386 0           $spd = 0 + $spd;
387 0 0         $gust = 0 + $gust if defined $gust;
388 0           $unit = ucfirst lc $unit;
389 0 0         $unit = 'm/sec' if $unit eq 'Mps';
390 0 0         $unit = 'knots' if $unit eq 'Kt';
391 0   0       $self->{wind_dir} ||= $wdir;
392 0   0       $self->{wind_speed} ||= $spd;
393 0   0       $self->{wind_gusting} ||= $gust;
394 0   0       $self->{wind_units} ||= $unit;
395 0           push @section, $self->_section('WIND', $wdir, $spd, $gust, $unit, $fromdir, $todir);
396            
397             # Altimeter setting
398             } elsif (my ($u, $p, $punit) = $t =~ /^([QA])(?:NH)?(\d\d\d\d)(INS?)?$/) {
399              
400 0           $p = 0 + $p;
401 0 0 0       if ($u eq 'A' || $punit && $punit =~ /^I/) {
      0        
402 0           $p = sprintf "%.2f", $p / 100;
403 0           $u = 'inHg';
404             } else {
405 0           $u = 'hPa';
406             }
407 0   0       $self->{pressure} ||= $p;
408 0   0       $self->{pressure_units} ||= $u;
409 0           push @section, $self->_section('PRESS', $p, $u);
410              
411             # Current (METAR) wind shear
412             } elsif ($t eq 'WS') {
413 0           my $runway = '';
414 0 0 0       if ($tok[0] eq 'ALL' && $tok[1] eq 'RWY') {
    0 0        
415 0           $runway = 'all runways';
416 0           shift @tok; shift @tok;
  0            
417             }
418             elsif ($tok[0] eq 'RWY' && $tok[1] =~ /\d\d[LRC]?/) {
419 0           $runway = "Runway $tok[1]";
420 0           shift @tok; shift @tok;
  0            
421             }
422 0           push @section, $self->_section('CURRENTSHEAR', $runway);
423              
424             # Forecast (TAF) wind shear
425             } elsif (my ($top, $direction, $speed) = $t =~ m!^WS(\d{3})\/(\d{3})(\d+)KT$!) {
426 0           push @section, $self->_section('FORECASTSHEAR', $top * 100, $direction, $speed);
427              
428             # Visibility group in metres
429             } elsif (my ($visibility, $mist) = $t =~ m!^(\d\d\d\d[NSEW]{0,2})([A-Z][A-Z])?$!) {
430 0 0         $visibility = $visibility eq '9999' ? ">10000" : 0 + $visibility;
431 0   0       $self->{visibility_dist} ||= $visibility;
432 0   0       $self->{visibility_units} ||= 'metres';
433 0           push @section, $self->_section('VISIBILITY', $visibility, 'm');
434 0 0         push @section, $self->_section('WEATHER', $mist) if $mist;
435              
436             # Visibility group in kilometres
437             } elsif (($visibility) = $t =~ m!^(\d+)KM$!) {
438 0 0         $visibility = $visibility eq '9999' ? ">10000" : 0 + $visibility;
439 0   0       $self->{visibility_dist} ||= $visibility;
440 0   0       $self->{visibility_units} ||= 'kilometres';
441 0           push @section, $self->_section('VISIBILITY', $visibility, 'Km');
442              
443             # Visibility group in miles and fraction of a mile with space between them
444             } elsif (my ($m) = $t =~ m!^(\d)$!) {
445 0           my $visibility;
446 0 0 0       if (@tok && (($visibility) = $tok[0] =~ m!^(\d/\d)SM$!)) {
447 0           shift @tok;
448 0           $visibility = "$m $visibility";
449 0   0       $self->{visibility_dist} ||= $visibility;
450 0   0       $self->{visibility_units} ||= 'Statute Miles';
451 0           push @section, $self->_section('VISIBILITY', $visibility, 'miles');
452             }
453            
454             # Visibility group in miles (either in miles or under a mile)
455             } elsif (my ($lt, $mvisibility) = $t =~ m!^([MP])?(\d+(:?/\d)?)SM$!) {
456 0 0 0       $mvisibility = 'Less than ' . $mvisibility if (defined($lt) && $lt eq 'M');
457 0 0 0       $mvisibility = 'Greater than ' . $mvisibility if (defined($lt) && $lt eq 'P');
458 0   0       $self->{visibility_dist} ||= $mvisibility;
459 0   0       $self->{visibility_units} ||= 'Statute Miles';
460 0           my $units = 'miles';
461 0 0 0       $units = 'mile' if ($mvisibility =~ /M|\// || $mvisibility =~ /^1$/);
462 0           push @section, $self->_section('VISIBILITY', $mvisibility, $units);
463            
464             # Runway Visual Range
465             } elsif (my ($rw, $rlt, $range, $vlt, $var, $runit, $tend) = $t =~ m!^R(\d\d[LRC]?)/([MP])?(\d\d\d\d)(?:([VMP]+)(\d\d\d\d))?(?:(FT)/?)?([UND])?$!) {
466 0 0         $runit = 'm' unless $runit;
467 0           $runit = lc $runit;
468 0 0         $runit = 'feet' if $runit eq 'ft';
469 0 0 0       $range = "<$range" if $rlt && $rlt eq 'M';
470 0 0 0       $range = ">$range" if $rlt && $rlt eq 'P';
471 0 0 0       $var = "<$var" if $vlt && $vlt =~ /M/;
472 0 0 0       $var = ">$var" if $vlt && $vlt =~ /P/;
473 0           push @section, $self->_section('RVR', $rw, $range, $var, $runit, $tend);
474            
475             # Weather
476             # The symbol used for "light" descriptor appears to vary; included both a
477             # hyphen and an en dash.
478             } elsif (my ($deg, $w) = $t =~ /^(−|-|\+|VC)?((?:SH)?\S{0,4})$/) {
479             # Replace +FC (tornado) with module specific 'ZZ' code
480 0 0 0       if (defined($deg) && "$deg$w" eq '+FC') {
    0          
481 0           $deg = '';
482 0           $w = 'ZZ';
483             } elsif ($w eq '+FC') {
484 0           $w = 'ZZ';
485             }
486             # Differentiate between VC in TAF and METAR/SPECI; use VT for TAF
487 0 0 0       $deg = 'VT' if (defined($deg) && $deg eq 'VC' && $self->{taf});
      0        
488              
489 0           push @section, $self->_section('WEATHER', $deg, $w =~ /([A-Z][A-Z])/g);
490              
491             # Sky conditions
492             } elsif (my ($amt, $height, $cb) = $t =~ m!^(FEW|SCT|BKN|OVC|SKC|CLR|VV|///)(\d\d\d|///)(CB|TCU)?$!) {
493 0 0 0       push @section, $self->_section('CLOUD', $amt, $height eq '///' ? 0 : $height * 100, $cb) unless $amt eq '///' && $height eq '///';
    0          
494 0 0         if ($amt =~ /BKN|OVC|VV/) {
495 0 0         $ceiling = $height * 100 if ($height * 100 < $ceiling);
496             }
497              
498             # Temperature / Dew Point (only appears in METAR/SPECI)
499             } elsif (my ($ms, $t, $n, $d) = $t =~ m/^(M)?(\d\d)\/(M)?(\d\d)?$/) {
500 0           $t = 0 + $t;
501 0           $d = 0 + $d;
502 0 0         $t = -$t if defined $ms;
503 0 0 0       $d = -$d if defined $d && defined $n;
504 0   0       $self->{temp} ||= $t;
505 0   0       $self->{dewpoint} ||= $d;
506 0           push @section, $self->_section('TEMP', $t);
507 0           push @section, $self->_section('DWPT', $d);
508             }
509            
510             }
511 0           $self->{sections} = \@section;
512 0           $self->{error_code} = 0;
513 0           return undef;
514             }
515              
516             sub _section
517             {
518 0     0     my $self = shift;
519 0           my $pkg = shift;
520 1     1   7 no strict 'refs';
  1         2  
  1         107  
521 0           $pkg = $self->{decode_language} . '::' . $pkg;
522 0           return $pkg->new(@_);
523             }
524              
525             sub _time
526             {
527 0     0     return sprintf "%02d:%02d", unpack "a2a2", sprintf "%04d", shift;
528             }
529              
530             # Accessors
531             sub AUTOLOAD
532             {
533 1     1   4 no strict;
  1         1  
  1         518  
534 0     0     my ($package, $name) = $AUTOLOAD =~ /^(.*)::(\w+)$/;
535 0 0         return if $name eq 'DESTROY';
536              
537 0     0     *$AUTOLOAD = sub {return $_[0]->{$name}};
  0            
538 0           goto &$AUTOLOAD;
539             }
540              
541             #
542             # These are the translation packages
543             #
544             # First the factory method
545             #
546              
547             package Geo::METARTAF::EN;
548              
549             sub new
550             {
551 0     0     my $pkg = shift;
552 0           return bless [@_], $pkg;
553             }
554              
555             sub as_string
556             {
557 0     0     my $self = shift;
558 0           my ($n) = (ref $self) =~ /::(\w+)$/;
559 0 0         return join ' ', ucfirst $n, map {defined $_ ? $_ : ()} @$self;
  0            
560             }
561              
562             sub day
563             {
564 0     0     my $pkg = shift;
565 0 0         my $d = sprintf "%d", ref($pkg) ? shift : $pkg;
566 0 0 0       if ($d == 1 || $d == 21 || $d == 31) {
    0 0        
    0 0        
      0        
567 0           return "${d}st";
568             } elsif ($d == 2 || $d == 22) {
569 0           return "${d}nd";
570             } elsif ($d == 3 || $d == 23) {
571 0           return "${d}rd";
572             }
573 0           return "${d}th";
574             }
575              
576             # Report header (type, location, issue time, and date)
577             package Geo::METARTAF::EN::HEAD;
578 1     1   7 use vars qw(@ISA);
  1         2  
  1         104  
579             @ISA = qw(Geo::METARTAF::EN);
580              
581             sub as_string
582             {
583 0     0     my $self = shift;
584 0           return "$self->[0] for $self->[1] issued at $self->[3] on the " . $self->day($self->[2]) . " $self->[4]\n";
585             }
586              
587             # TAF valid period
588             package Geo::METARTAF::EN::VALID;
589 1     1   3 use vars qw(@ISA);
  1         2  
  1         110  
590             @ISA = qw(Geo::METARTAF::EN);
591              
592             sub as_string
593             {
594 0     0     my $self = shift;
595 0           my $out = "Valid from $self->[1]";
596             # If the day is the same for both times
597 0 0         if ($self->[0] == $self->[2]) {
598 0           $out .= " to $self->[3] on the " . $self->day($self->[0]);
599             }
600             else {
601 0           $out .= " on the " . $self->day($self->[0]) . " to $self->[3] on the " . $self->day($self->[2]);
602             }
603 0           return $out;
604             }
605              
606             # Wind Info: direction, $speed, $gusts, $unit, $fromdir, $todir
607             package Geo::METARTAF::EN::WIND;
608 1     1   6 use vars qw(@ISA);
  1         1  
  1         183  
609             @ISA = qw(Geo::METARTAF::EN);
610              
611             sub as_string
612             {
613 0     0     my $self = shift;
614 0           my $out = "~Wind Conditions: ";
615 0 0 0       if ($self->[0] ne 'VRB' && $self->[0] == 0 && $self->[1] == 0) {
      0        
616 0           $out .= 'Calm';
617             }
618             else {
619 0 0         $out .= $self->[0] eq 'VRB' ? "Variable" : "$self->[0]";
620 0 0         $out .= "°T varying between $self->[4]°T and $self->[5]" if defined $self->[4];
621 0 0         $out .= ($self->[0] eq 'VRB' ? '' : "°T") . " at $self->[1] $self->[3]";
622 0 0         $out .= " gusting to $self->[2] $self->[3]" if defined $self->[2];
623             }
624 0           return $out;
625             }
626              
627             # Altimeter setting
628             package Geo::METARTAF::EN::PRESS;
629 1     1   5 use vars qw(@ISA);
  1         1  
  1         72  
630             @ISA = qw(Geo::METARTAF::EN);
631              
632             sub as_string
633             {
634 0     0     my $self = shift;
635 0           return "~Altimeter Setting: $self->[0] $self->[1]";
636             }
637              
638             # Low-level wind shear in METAR
639             package Geo::METARTAF::EN::CURRENTSHEAR;
640 1     1   4 use vars qw(@ISA);
  1         1  
  1         76  
641             @ISA = qw(Geo::METARTAF::EN);
642              
643             sub as_string
644             {
645 0     0     my $self = shift;
646 0           return "~Low-Level Wind Shear: Within 1500 feet AGL along the take-off or approach path of $self->[0]";
647             }
648              
649             # Low-level wind shear in TAF
650             package Geo::METARTAF::EN::FORECASTSHEAR;
651 1     1   3 use vars qw(@ISA);
  1         1  
  1         120  
652             @ISA = qw(Geo::METARTAF::EN);
653              
654             sub as_string
655             {
656 0     0     my $self = shift;
657 0           return "~Low-Level Wind Shear: $self->[1]°T at $self->[2] knots with top layer at $self->[0] feet AGL";
658             }
659              
660             # Temperature
661             package Geo::METARTAF::EN::TEMP;
662 1     1   5 use vars qw(@ISA);
  1         1  
  1         109  
663             @ISA = qw(Geo::METARTAF::EN);
664              
665             sub as_string
666             {
667 0     0     my $self = shift;
668 0           my $out = "~Temperature: $self->[0]°C";
669 0           return $out;
670             }
671              
672             # Dew Point
673             package Geo::METARTAF::EN::DWPT;
674 1     1   4 use vars qw(@ISA);
  1         2  
  1         76  
675             @ISA = qw(Geo::METARTAF::EN);
676              
677             sub as_string
678             {
679 0     0     my $self = shift;
680 0           my $out = "~Dew Point: $self->[0]°C";
681 0           return $out;
682             }
683              
684             # Cloud coverage
685             package Geo::METARTAF::EN::CLOUD;
686 1     1   4 use vars qw(@ISA);
  1         1  
  1         186  
687             @ISA = qw(Geo::METARTAF::EN);
688              
689             my %st = (
690             VV => "Obscured at",
691             SKC => "Clear - No cloud",
692             CLR => "Clear - No cloud and no significant weather",
693             SCT => "Scattered clouds (3-4 oktas) at",
694             BKN => "Broken clouds (5-7 oktas) at",
695             FEW => "Few clouds (0-2 oktas) at",
696             OVC => "Overcast clouds (8 oktas) at",
697             CAVOK => "No cloud below 5000 feet, >= 10 km visibility, and no significant weather (CAVOK)",
698             CB => "Cumulonimbus clouds present",
699             TCU => "Towering Cumulus clouds present",
700             NSC => "No significant cloud",
701             BLU => "3 oktas at 2500ft 8Km visibility",
702             WHT => "3 oktas at 1500ft 5Km visibility",
703             GRN => "3 oktas at 700ft 3700m visibility",
704             YLO => "3 oktas at 300ft 1600m visibility",
705             AMB => "3 oktas at 200ft 800m visibility",
706             RED => "3 oktas at <200ft <800m visibility",
707             NIL => "No weather",
708             '///' => "some",
709             );
710              
711             sub as_string
712             {
713 0     0     my $self = shift;
714 0 0         return "~Sky Conditions: " . $st{$self->[0]} if @$self == 1;
715 0 0         return "~Sky Conditions: " . $st{$self->[0]} . " $self->[1] feet AGL" if $self->[0] eq 'VV';
716 0 0         return "~Sky Conditions: " . $st{$self->[0]} . " $self->[1] feet AGL" . ((defined $self->[2]) ? " with $st{$self->[2]}" : "");
717             }
718              
719             # Local ceiling
720             package Geo::METARTAF::EN::CEIL;
721 1     1   14 use vars qw(@ISA);
  1         1  
  1         76  
722             @ISA = qw(Geo::METARTAF::EN);
723              
724             sub as_string
725             {
726 0     0     my $self = shift;
727 0           my $out = "~Ceiling: $self->[0] feet AGL";
728 0           return $out;
729             }
730              
731             # Weather phenomena
732             package Geo::METARTAF::EN::WEATHER;
733 1     1   4 use vars qw(@ISA);
  1         1  
  1         302  
734             @ISA = qw(Geo::METARTAF::EN);
735              
736             my %wt = (
737             '+' => "Heavy",
738             '−' => "Light",
739             '-' => "Light",
740             'VC' => "within 5 SM of the aerodrome (but not at the aerodrome)",
741             'VT' => "within 5-10 NM of the aerodrome (but not at the aerodrome)",
742              
743             MI => "Shallow",
744             PR => "Partial",
745             BC => "Patches of",
746             DR => "Drifting",
747             BL => "Blowing",
748             SH => "Showers",
749             TS => "Thunderstorm",
750             FZ => "Freezing",
751             RE => "Recent",
752            
753             DZ => "Drizzle",
754             RA => "Rain",
755             SN => "Snow",
756             SG => "Snow Grains",
757             IC => "Ice Crystals (Vis <= 6 SM)",
758             PL => "Ice Pellets",
759             GR => "Hail",
760             GS => "Snow Pellets",
761             UP => "Unknown precipitation",
762            
763             BR => "Mist (Vis >= 5/8 SM)",
764             FG => "Fog (Vis < 5/8 SM)",
765             FU => "Smoke (Vis <= 6 SM)",
766             VA => "Volcanic Ash",
767             DU => "Dust (Vis <= 6 SM)",
768             SA => "Sand (Vis <= 6 SM)",
769             HZ => "Haze (Vis <= 6 SM)",
770             PY => "Spray",
771            
772             PO => "Dust/Sand Whirls (Dust Devils)",
773             SQ => "Squalls",
774             FC => "Funnel Cloud",
775             SS => "Sandstorm (Vis < 5/8 SM)",
776             DS => "Dust Storm (Vis < 5/8 SM)",
777             WS => "Wind Shear",
778             ZZ => "Tornado or Waterspout", # Only for this module (actual code is '+FC')
779              
780             'BKN' => "Broken",
781             'NOSIG' => "No significant weather",
782             );
783              
784             sub as_string
785             {
786 0     0     my $self = shift;
787 0           my @out;
788 0           my $report_vic = '';
789              
790 0           my ($vic, $shower);
791 0           my @in;
792 0           push @in, @$self;
793            
794 0           while (@in) {
795 0           my $t = shift @in;
796              
797 0 0 0       if (!defined $t) {
    0          
    0          
798 0           next;
799             } elsif ($t eq 'VC' || $t eq 'VT') {
800 0           $report_vic = $t;
801 0           $vic++;
802 0           next;
803             } elsif ($t eq 'SH') {
804 0 0         if ($vic) {
805 0           push @out, $wt{'SH'};
806             } else {
807 0           $shower++;
808             }
809 0           next;
810             }
811            
812             # Display singular of phenomena when associated with showers
813             # (e.g. - 'Ice Pellet Showers' instead of 'Ice Pellets Showers')
814 0 0 0       if ($shower && $wt{$t} =~ /s$/) {
815 0           push @out, substr($wt{$t}, 0, -1);
816             } else {
817 0           push @out, $wt{$t};
818             }
819              
820             }
821            
822 0 0 0       if (@out && $shower) {
823 0           $shower = 0;
824 0           push @out, $wt{'SH'};
825             }
826 0 0         push @out, $wt{$report_vic} if $vic;
827              
828 0           return "~Weather Phenomena: " . join ' ', @out;
829             }
830              
831             # Runway Visual Range
832             package Geo::METARTAF::EN::RVR;
833 1     1   5 use vars qw(@ISA);
  1         1  
  1         137  
834             @ISA = qw(Geo::METARTAF::EN);
835              
836             sub as_string
837             {
838 0     0     my $self = shift;
839 0           my $out = "~Runway Visual Range: $self->[1] $self->[3]";
840 0 0         $out .= " varying to $self->[2] $self->[3]" if defined $self->[2];
841 0 0         if (defined $self->[4]) {
842 0 0         $out .= " and decreasing" if $self->[4] eq 'D';
843 0 0         $out .= " and increasing" if $self->[4] eq 'U';
844             }
845 0           $out .= " on Runway $self->[0]";
846 0           return $out;
847             }
848              
849             package Geo::METARTAF::EN::RWY;
850 1     1   6 use vars qw(@ISA);
  1         10  
  1         94  
851             @ISA = qw(Geo::METARTAF::EN);
852              
853             sub as_string
854             {
855 0     0     my $self = shift;
856 0 0         my $out = $self->[0] eq 'LDG' ? "landing " : '';
857 0           $out .= "runway $self->[1]";
858 0           return $out;
859             }
860              
861             # Visibility
862             package Geo::METARTAF::EN::VISIBILITY;
863 1     1   5 use vars qw(@ISA);
  1         1  
  1         107  
864             @ISA = qw(Geo::METARTAF::EN);
865              
866             sub as_string
867             {
868 0     0     my $self = shift;
869 0           return "~Visibility: $self->[0] $self->[1]";
870             }
871              
872             # Probability
873             package Geo::METARTAF::EN::PROB;
874 1     1   4 use vars qw(@ISA);
  1         1  
  1         164  
875             @ISA = qw(Geo::METARTAF::EN);
876              
877             sub as_string
878             {
879 0     0     my $self = shift;
880 0           my $out = "Probability $self->[0]%";
881 0 0         if (defined $self->[1]) {
882 0           $out .= " between $self->[2]";
883             # If the day is the same for both times
884 0 0         if ($self->[1] == $self->[3]) {
885 0           $out .= " and $self->[4] on the " . $self->day($self->[1]);
886             }
887             else {
888 0           $out .= " on the " . $self->day($self->[1]) . " and $self->[4] on the " . $self->day($self->[3]);
889             }
890             }
891 0           return $out;
892             }
893              
894             # Temporary
895             package Geo::METARTAF::EN::TEMPO;
896 1     1   4 use vars qw(@ISA);
  1         1  
  1         140  
897             @ISA = qw(Geo::METARTAF::EN);
898              
899             sub as_string
900             {
901 0     0     my $self = shift;
902 0           my $out = "Temporarily";
903 0           $out .= " between $self->[1]";
904             # If the day is the same for both times
905 0 0         if ($self->[0] == $self->[2]) {
906 0           $out .= " and $self->[3] on the " . $self->day($self->[0]);
907             }
908             else {
909 0           $out .= " on the " . $self->day($self->[0]) . " and $self->[3] on the " . $self->day($self->[2]);
910             }
911 0           return $out;
912             }
913              
914             # Becoming
915             package Geo::METARTAF::EN::BECMG;
916 1     1   4 use vars qw(@ISA);
  1         2  
  1         112  
917             @ISA = qw(Geo::METARTAF::EN);
918              
919             sub as_string
920             {
921 0     0     my $self = shift;
922 0           my $out = "Becoming";
923 0           $out .= " between $self->[1]";
924             # If the day is the same for both times
925 0 0         if ($self->[0] == $self->[2]) {
926 0           $out .= " and $self->[3] on the " . $self->day($self->[0]);
927             }
928             else {
929 0           $out .= " on the " . $self->day($self->[0]) . " and $self->[3] on the " . $self->day($self->[2]);
930             }
931 0           return $out;
932             }
933              
934             # From
935             package Geo::METARTAF::EN::FROM;
936 1     1   5 use vars qw(@ISA);
  1         1  
  1         110  
937             @ISA = qw(Geo::METARTAF::EN);
938              
939             sub as_string
940             {
941 0     0     my $self = shift;
942 0           my ($theDay, $theHour) = split(/-/, $self->[0]);
943 0           return "From $theHour on the " . $self->day($theDay);
944             }
945              
946             # Until
947             package Geo::METARTAF::EN::TIL;
948 1     1   4 use vars qw(@ISA);
  1         0  
  1         94  
949             @ISA = qw(Geo::METARTAF::EN);
950              
951             sub as_string
952             {
953 0     0     my $self = shift;
954 0           return "Until $self->[0]";
955             }
956              
957             # Autoload methods go after =cut, and are processed by the autosplit program.
958              
959             1;
960             __END__