File Coverage

blib/lib/Geo/WeatherNWS.pm
Criterion Covered Total %
statement 344 466 73.8
branch 119 190 62.6
condition 74 125 59.2
subroutine 20 28 71.4
pod 20 20 100.0
total 577 829 69.6


line stmt bran cond sub pod time code
1             package Geo::WeatherNWS;
2              
3             #------------------------------------------------------------------------------
4             #
5             # Package Name: Get Observations from NWS (Geo::WeatherNWS)
6             #
7             # Last Modified: 18 December 2001 - Prepared for CPAN - Marc Slagle
8             # 24 February 2002 - Adding server/error code - Marc
9             # 10 August 2011 - changed FTP server name, added tests,
10             # docs, some restructuring. - Bob Ernst
11             # 14 November 2012 - removed unneeded /d after tr,
12             # make network tests optional,
13             # check status of opens - Bob
14             # 26 November 2012 - Address bug 14632 (METAR Decoding) from dstroma
15             # Address bug 27513 (Geo-WeatherNWS returns wrong station code)
16             # from Guenter Knauf
17             # Fix issues with undefined values,
18             # Change some conversion constants,
19             # Round instead of truncate results,
20             # Only calculate windchill for proper range,
21             # "ptemerature" is now spelled "ptemperature"
22             # Fixed handling of condition text
23             # Relax ICAO naming rules
24             # Change ICAO website
25             # Change http web site from weather.noaa.gov
26             # to www.aviationweather.gov, and change parsing to match.
27             # Add report_date and report_time items.
28             # - Bob
29             # 27 November 2012 - Add POD documentation for new functions.
30             # 1 January 2017 - Switched from POSIX module to File::Temp
31             # 23 February 2026 - changed to use updated http web site,
32             # limit heat index calculation range,
33             # use blanks instead of undef values,
34             # remove leading zero from Celsius
35             # Reformat using perltidy
36             #
37             #
38             #------------------------------------------------------------------------------
39              
40             #------------------------------------------------------------------------------
41             # We need these
42             #------------------------------------------------------------------------------
43              
44             require 5.005_62;
45 6     6   990433 use strict;
  6         15  
  6         327  
46 6     6   38 use warnings;
  6         13  
  6         476  
47 6     6   69 use Carp;
  6         10  
  6         544  
48 6     6   5966 use File::Temp;
  6         151858  
  6         520  
49 6     6   2987 use HTTP::Request;
  6         144463  
  6         255  
50 6     6   53 use IO::Handle;
  6         9  
  6         359  
51 6     6   4518 use LWP::UserAgent;
  6         243885  
  6         353  
52 6     6   4434 use Net::FTP;
  6         802921  
  6         36645  
53              
54             #------------------------------------------------------------------------------
55             # Version
56             #------------------------------------------------------------------------------
57              
58             our $VERSION = '1.06';
59              
60             #------------------------------------------------------------------------------
61             # Round function
62             # Using Math::Round would add another dependency
63             #------------------------------------------------------------------------------
64              
65             sub round {
66 163     163 1 286490 my $float = shift;
67 163         230 my $rounded = "";
68              
69 163 100 100     829 if ( defined $float && $float ne "" ) {
70 127         338 $rounded = sprintf "%.0f", $float;
71             }
72 163         415 return $rounded;
73             }
74              
75             #------------------------------------------------------------------------------
76             # Temperature conversion
77             # If the temperature we are converting from is undefined,
78             # then the temperature we are converting to is also undefined.
79             #------------------------------------------------------------------------------
80              
81             sub convert_f_to_c {
82 22     22 1 37 my $fahrenheit = shift;
83 22         43 my $celsius = "";
84              
85 22 100 100     89 if ( defined $fahrenheit && $fahrenheit ne "" ) {
86 5         14 $celsius = ( 5.0 / 9.0 ) * ( $fahrenheit - 32.0 );
87             }
88 22         56 return $celsius;
89             }
90              
91             sub convert_c_to_f {
92 22     22 1 40 my $celsius = shift;
93 22         34 my $fahrenheit = "";
94              
95 22 100 100     103 if ( defined $celsius && $celsius ne "" ) {
96 20         44 $fahrenheit = ( ( 9.0 / 5.0 ) * $celsius ) + 32.0;
97             }
98 22         54 return $fahrenheit;
99             }
100              
101             #------------------------------------------------------------------------------
102             # Windchill
103             #------------------------------------------------------------------------------
104              
105             sub windchill {
106 15     15 1 33 my $F = shift;
107 15         26 my $wind_speed_mph = shift;
108 15         31 my $windchill = "";
109              
110             # This is the North American wind chill index.
111             # Windchill temperature is only defined for:
112             # * temperatures at or below 50 F
113             # * wind speed above 3 mph
114             # Bright sunshine may increase the wind chill temperature by
115             # 10 to 18 degress F.
116              
117 15 50 66     133 if ( defined $F
      66        
      66        
118             && defined $wind_speed_mph
119             && $F ne ""
120             && $wind_speed_mph ne "" )
121             {
122             # Old Formula
123             # my $Windc=int(
124             # 0.0817*
125             # (3.71*$Self->{windspeedmph}**0.5 + 5.81 - 0.25*$Self->{windspeedmph})*
126             # ($F - 91.4) + 91.4);
127              
128             # New Formula
129 14 100 100     55 if ( $F <= 50 && $wind_speed_mph > 3 ) {
130 4         43 $windchill
131             = 35.74 + ( 0.6215 * $F )
132             - ( 35.75 * ( $wind_speed_mph**0.16 ) )
133             + ( ( 0.4275 * $F ) * ( $wind_speed_mph**0.16 ) );
134             }
135             }
136 15         51 return $windchill;
137             }
138              
139             #------------------------------------------------------------------------------
140             # Heat Index
141             #------------------------------------------------------------------------------
142              
143             sub heat_index {
144 12     12 1 23 my $F = shift;
145 12         24 my $rh = shift;
146 12         25 my $heat_index = "";
147              
148 12 50 33     111 if ( defined $F
      66        
      66        
      66        
149             && $F ne ""
150             && $F >= 80
151             && defined $rh
152             && $rh ne "" )
153             {
154 3         28 $heat_index
155             = -42.379 + 2.04901523 * $F
156             + 10.14333127 * $rh
157             - 0.22475541 * $F * $rh
158             - 6.83783e-03 * $F**2
159             - 5.481717e-02 * $rh**2
160             + 1.22874e-03 * $F**2 * $rh
161             + 8.5282e-04 * $F * $rh**2
162             - 1.99e-06 * $F**2 * $rh**2;
163             }
164 12         37 return $heat_index;
165             }
166              
167             #------------------------------------------------------------------------------
168             # Convert wind speed from nautical miles per hour to miles per hour
169             #------------------------------------------------------------------------------
170              
171             sub convert_kts_to_mph {
172 20     20 1 32 my $knots = shift;
173 20         30 my $mph = "";
174              
175 20 50 33     87 if ( defined $knots && $knots ne "" ) {
176 20         48 $mph = $knots * 1.150779;
177             }
178 20         70 return $mph;
179             }
180              
181             #------------------------------------------------------------------------------
182             # Convert wind speed from nautical miles per hour to kilometers per hour
183             #------------------------------------------------------------------------------
184              
185             sub convert_kts_to_kmh {
186 19     19 1 31 my $knots = shift;
187 19         26 my $kmh = "";
188              
189 19 50 33     60 if ( defined $knots && $knots ne "" ) {
190 19         39 $kmh = $knots * 1.852;
191             }
192 19         41 return $kmh;
193             }
194              
195             #------------------------------------------------------------------------------
196             # Convert miles to kilometers
197             #------------------------------------------------------------------------------
198              
199             sub convert_miles_to_km {
200 8     8 1 13 my $miles = shift;
201 8         15 my $km = "";
202              
203 8 50 33     51 if ( defined $miles && $miles ne "" ) {
204 8         22 $km = $miles * 1.609344;
205             }
206 8         19 return $km;
207             }
208              
209             #------------------------------------------------------------------------------
210             # Translate Weather into readable Conditions Text
211             #
212             # Reference is WMO Code Table 4678
213             #------------------------------------------------------------------------------
214              
215             sub translate_weather {
216 10     10 1 13333 my $coded = shift;
217 10         20 my $old_conditionstext = shift;
218 10         14 my $old_conditions1 = shift;
219 10         18 my $old_conditions2 = shift;
220 10         18 my ( $conditionstext, $conditions1, $conditions2, $intensity );
221              
222             # We use %Converter to translate 2-letter codes into text
223              
224 10         261 my %Converter = (
225             BR => 'Mist',
226             TS => 'Thunderstorm',
227             MI => 'Shallow',
228             PR => 'Partial',
229             BC => 'Patches',
230             DR => 'Low Drifting',
231             BL => 'Blowing',
232             SH => 'Shower',
233             FZ => 'Freezing',
234             DZ => 'Drizzle',
235             RA => 'Rain',
236             SN => 'Snow',
237             SG => 'Snow Grains',
238             IC => 'Ice Crystals',
239             PE => 'Ice Pellets',
240             PL => 'Ice Pellets',
241             GR => 'Hail',
242             GS => 'Small Hail/Snow',
243             UP => 'Unknown Precipitation',
244             FG => 'Fog',
245             FU => 'Smoke',
246             VA => 'Volcanic Ash',
247             DU => 'Widespread Dust',
248             SA => 'Sand',
249             HZ => 'Haze',
250             PY => 'Spray',
251             PO => 'Dust Devils',
252             SQ => 'Squalls',
253             FC => 'Tornado',
254             SS => 'Sandstorm'
255             );
256              
257 10         22 my $Block1 = "";
258 10         32 my $Block2 = "";
259 10 100       42 if ( $coded =~ /^[-+]/ ) {
260              
261             # Heavy(+) or Light(-) condition
262              
263 6 50       20 if ( !$old_conditions1 ) {
264 6         13 my $Modifier = substr( $coded, 0, 1 ); # +/-
265 6         45 my $Block1t = substr( $coded, 1, 2 ); # e.g. TS
266 6         13 my $Block2t = substr( $coded, 3, 4 ); # e.g. RA
267              
268 6 50       46 if ( defined( $Converter{$Block1t} ) ) {
269 6         14 $Block1 = $Converter{$Block1t}; # e.g. Thunderstorm
270             }
271             else {
272 0         0 $Block1 = "";
273             }
274 6         12 $conditions1 = $Block1; # e.g. Thunderstorm
275              
276 6 50       17 if ($Block2t) {
277 6 50       14 if ( defined( $Converter{$Block2t} ) ) {
278 6         11 $Block2 = $Converter{$Block2t}; # e.g. Rain
279             }
280             else {
281 0         0 $Block2 = "";
282             }
283 6         12 $conditions2 = $Block2; # e.g. Rain
284             }
285              
286 6 100       24 if ( $Modifier =~ /^\-/ ) {
    50          
287 3         10 $Block1 = "Light $Block1"; # e.g. Light Thunderstorm
288 3         9 $intensity = "Light";
289             }
290             elsif ( $Modifier =~ /^\+/ ) {
291 3         8 $Block1 = "Heavy $Block1"; # e.g. Heavy Thunderstorm
292 3         38 $intensity = "Heavy";
293             }
294              
295 6 50       18 if ($Block2) {
296 6         12 $Block1 = "$Block1 $Block2"; # e.g. Light Thunderstorm Rain
297             }
298              
299 6 50       15 if ($old_conditionstext) {
300 0 0       0 if ( $Block1 eq "SH" ) {
301 0         0 $conditionstext = "$Block2 of $Block1";
302 0         0 $conditions1 = "Showers of";
303             }
304             else {
305 0         0 $conditionstext = "$old_conditionstext and $Block1";
306             }
307             }
308             else {
309 6         14 $conditionstext = $Block1;
310             }
311             }
312             }
313             else {
314             # Moderate condition
315              
316 4         10 my $Block1 = "";
317 4         7 my $Block2 = "";
318 4 50       16 if ( !$old_conditions1 ) {
319 4         10 my $Block1t = substr( $coded, 0, 2 ); # e.g. TS
320 4         13 my $Block2t = substr( $coded, 2, 4 ); # e.g. RA
321              
322 4 50       14 if ( defined( $Converter{$Block1t} ) ) {
323 4         10 $Block1 = $Converter{$Block1t}; # e.g. Thunderstorm
324             }
325             else {
326 0         0 $Block1 = "";
327             }
328              
329 4         8 $conditions1 = $Block1; # e.g. Thunderstorm
330              
331 4 50       14 if ($Block2t) {
332 4 50       14 if ( defined( $Converter{$Block2t} ) ) {
333 4         9 $Block2 = $Converter{$Block2t};
334             }
335             else {
336 0         0 $Block2 = "";
337             }
338 4         6 $conditions2 = $Block2;
339 4         15 $Block1 = "$Block1 $Block2";
340             }
341              
342 4 50       10 if ($old_conditionstext) {
343 0 0       0 if ( $Block1 eq "SH" ) {
344 0         0 $conditionstext = "$Block2 of $Block1";
345 0         0 $conditions1 = "Showers of";
346             }
347             else {
348 0         0 $conditionstext = "$old_conditionstext and $Block1";
349             }
350             }
351             else {
352 4         13 $conditionstext = $Block1;
353             }
354             }
355             }
356 10         188 return ( $conditionstext, $conditions1, $conditions2, $intensity );
357             }
358              
359             #------------------------------------------------------------------------------
360             # Lets create a new self
361             #------------------------------------------------------------------------------
362              
363             sub new {
364 10     10 1 431895 my $Proto = shift;
365 10   100     88 my $Class = ref($Proto) || $Proto || __PACKAGE__;
366 10         19 my $Self = {};
367              
368 10         48 $Self->{servername} = "tgftp.nws.noaa.gov";
369 10         20 $Self->{username} = "anonymous";
370 10         20 $Self->{password} = 'weather@cpan.org';
371 10         22 $Self->{directory} = "/data/observations/metar/stations";
372 10         20 $Self->{timeout} = 120;
373 10         21 $Self->{obs} = "";
374              
375 10         23 bless $Self, $Class;
376 10         26 return $Self;
377             }
378              
379             #------------------------------------------------------------------------------
380             # Adding ability to edit server/user/directory at runtime...
381             #------------------------------------------------------------------------------
382              
383             sub setservername {
384 0     0 1 0 my $Self = shift;
385 0         0 my $Servername = shift;
386 0         0 $Self->{servername} = $Servername;
387 0         0 return $Self;
388             }
389              
390             sub setusername {
391 0     0 1 0 my $Self = shift;
392 0         0 my $User = shift;
393 0         0 $Self->{username} = $User;
394 0         0 return $Self;
395             }
396              
397             sub setpassword {
398 0     0 1 0 my $Self = shift;
399 0         0 my $Pass = shift;
400 0         0 $Self->{password} = $Pass;
401 0         0 return $Self;
402             }
403              
404             sub setdirectory {
405 0     0 1 0 my $Self = shift;
406 0         0 my $Dir = shift;
407 0         0 $Self->{directory} = $Dir;
408 0         0 return $Self;
409             }
410              
411             sub settemplatefile {
412 0     0 1 0 my $Self = shift;
413 0         0 my $Tfile = shift;
414 0         0 $Self->{tfile} = $Tfile;
415 0         0 return $Self;
416             }
417              
418             sub settimeout {
419 0     0 1 0 my $Self = shift;
420 0         0 my $Seconds = shift;
421 0         0 $Self->{timeout} = $Seconds;
422 0         0 return $Self;
423             }
424              
425             #------------------------------------------------------------------------------
426             # Here we get to FTP to the NWS and get the data
427             #------------------------------------------------------------------------------
428              
429             sub getreporthttp {
430 0     0 1 0 my $Self = shift;
431 0         0 my $Code = shift;
432              
433             # The old site was: http://weather.noaa.gov/cgi-bin/mgetmetar.pl?cccc=$Code
434             # Also outdated:
435             # 'http://www.aviationweather.gov/adds/metars/?station_ids=' . $Code . '&chk_metars=on&hoursStr=most+recent+only';
436              
437             # We'll keep using the raw METAR report to minimize changes for now.
438             # The new API could parse out the elements for us.
439             $Self->{http}
440 0         0 = 'https://aviationweather.gov/api/data/metar?ids='
441             . $Code
442             . '&format=raw&taf=false&hours=0';
443 0         0 my $Ret = &getreport( $Self, $Code );
444 0         0 return $Ret;
445             }
446              
447             sub getreport {
448 0     0 1 0 my $Self = shift;
449 0         0 my $Station = shift;
450              
451 0         0 $Self->{error} = "0";
452              
453 0         0 my $Tmphandle = File::Temp->new();
454 0         0 my $Tmpfile = $Tmphandle->filename;
455 0         0 close $Tmphandle;
456              
457 0         0 my $Code = uc($Station);
458              
459 0 0       0 if ( !$Code ) {
460 0         0 $Self->{error} = "1";
461 0         0 $Self->{errortext} = "No Station Code Entered\n";
462 0         0 return $Self;
463             }
464              
465 0 0 0     0 if ( $Self->{http} && $Self->{http} ne "" ) {
466 0         0 my $Ua = LWP::UserAgent->new();
467 0         0 $Ua->agent("Geo::WeatherNWS $VERSION");
468              
469 0         0 my $Req = HTTP::Request->new( GET => $Self->{http} );
470 0         0 $Req->content_type('application/x-www-form-urlencoded');
471 0         0 my $Res = $Ua->request($Req);
472              
473 0 0       0 if ( $Res->is_success ) {
474 0         0 $Self->{obs} = $Res->content;
475 0         0 $Self->{obs} =~ s/^METAR //;
476             }
477             }
478             else {
479              
480             # Some users needed this for firewalls...
481             my $Ftp = Net::FTP->new(
482             $Self->{servername},
483             Debug => 0,
484             Passive => 1,
485             Timeout => $Self->{timeout}
486 0         0 );
487              
488             # my $Ftp=Net::FTP->new($Self->{servername}, Debug => 0);
489 0 0       0 if ( !defined $Ftp ) {
490 0         0 $Self->{error} = 1;
491 0         0 $Self->{errortext} = "Cannot connect to $Self->{servername}: $@";
492 0         0 return $Self;
493             }
494 0         0 $Ftp->login( $Self->{username}, $Self->{password} );
495 0         0 my $Rcode = $Ftp->code();
496 0         0 my $Message = $Ftp->message();
497              
498 0 0       0 if ( $Rcode =~ /^[45]/ ) {
499 0         0 $Self->{error} = $Rcode;
500 0         0 $Self->{errortext} = $Message;
501 0         0 return $Self;
502             }
503              
504 0         0 $Ftp->cwd( $Self->{directory} );
505 0         0 $Rcode = $Ftp->code();
506 0         0 $Message = $Ftp->message();
507              
508 0 0       0 if ( $Rcode =~ /^[45]/ ) {
509 0         0 $Self->{error} = $Rcode;
510 0         0 $Self->{errortext} = $Message;
511 0         0 return $Self;
512             }
513              
514 0         0 $Rcode = $Ftp->get( "$Code.TXT", $Tmpfile );
515 0         0 $Rcode = $Ftp->code();
516 0         0 $Message = $Ftp->message();
517 0         0 $Ftp->quit;
518              
519 0 0       0 if ( $Rcode =~ /^[45]/ ) {
520 0         0 $Self->{error} = $Rcode;
521 0         0 $Self->{errortext} = $Message;
522 0         0 return $Self;
523             }
524              
525 0         0 local $/; # enable slurp mode
526 0 0       0 open my $F, '<', $Tmpfile
527             or croak "error opening temp input $Tmpfile: $!";
528 0         0 my $Data = <$F>;
529 0         0 close($F);
530 0         0 unlink($Tmpfile);
531              
532 0         0 $Data =~ tr/\n/ /;
533 0         0 $Self->{obs} = $Data;
534             }
535              
536 0         0 $Self->decode();
537 0         0 return $Self;
538             }
539              
540             #------------------------------------------------------------------------------
541             # Decodeobs takes the obs in a string format and decodes them
542             #------------------------------------------------------------------------------
543              
544             sub decodeobs {
545 9     9 1 1118 my $Self = shift;
546 9         13 my $Obs = shift;
547 9         21 $Self->{obs} = $Obs;
548 9         27 $Self->decode();
549 9         34 return $Self;
550             }
551              
552             #------------------------------------------------------------------------------
553             # Decode does the work, and is only called internally
554             #------------------------------------------------------------------------------
555              
556             sub decode {
557 9     9 1 17 my $Self = shift;
558 9         12 my @Cloudlevels;
559              
560 9         49 my @Splitter = split( /\s+/, $Self->{obs} );
561              
562             #------------------------------------------------------------------------------
563             # Break the METAR observations down and decode
564             #------------------------------------------------------------------------------
565              
566 9         32 my $have_icao_code = 0;
567 9         14 my $column = 0;
568              
569 9         27 $Self->{cloudcover} = "";
570 9         17 $Self->{conditions1} = "";
571 9         19 $Self->{conditions2} = "";
572 9         17 $Self->{conditionstext} = "";
573 9         22 $Self->{dewpoint_c} = "";
574 9         17 $Self->{dewpoint_f} = "";
575 9 50       33 $Self->{error} = "" if !defined $Self->{error};
576 9 50       28 $Self->{errortext} = "" if !defined $Self->{errortext};
577 9         18 $Self->{eventbe} = "";
578 9         15 $Self->{heat_index_c} = "";
579 9         14 $Self->{heat_index_f} = "";
580 9 50       26 $Self->{http} = "" if !defined $Self->{http};
581 9         29 $Self->{intensity} = "";
582 9         16 $Self->{maintenance} = "";
583 9         15 $Self->{precipslr} = "";
584 9         25 $Self->{pressure_inhg} = "";
585 9         15 $Self->{pressure_kgcm} = "";
586 9         35 $Self->{pressure_lbin} = "";
587 9         13 $Self->{pressure_mb} = "";
588 9         13 $Self->{pressure_mmhg} = "";
589 9         13 $Self->{ptemperature} = "";
590 9         15 $Self->{relative_humidity} = "";
591 9         16 $Self->{slp_inhg} = "";
592 9         15 $Self->{slp_kgcm} = "";
593 9         15 $Self->{slp_lbin} = "";
594 9         17 $Self->{slp_mb} = "";
595 9         15 $Self->{slp_mmhg} = "";
596 9         14 $Self->{storm} = "";
597 9         16 $Self->{temperature_c} = "";
598 9         13 $Self->{temperature_f} = "";
599 9 50       23 $Self->{templateout} = "" if !defined $Self->{templateout};
600 9 50       28 $Self->{tfile} = "" if !defined $Self->{tfile};
601 9         16 $Self->{thpressure} = "";
602 9         15 $Self->{thpressure} = "";
603 9         16 $Self->{visibility_km} = "";
604 9         15 $Self->{visibility_mi} = "";
605 9         13 $Self->{windchill_c} = "";
606 9         16 $Self->{windchill_f} = "";
607 9         13 $Self->{winddir} = "";
608 9         13 $Self->{winddirtext} = "";
609 9         53 $Self->{windgustkmh} = "";
610 9         16 $Self->{windgustkts} = "";
611 9         16 $Self->{windgustmph} = "";
612 9         13 $Self->{windspeedkmh} = "";
613 9         13 $Self->{windspeedkts} = "";
614 9         15 $Self->{windspeedmph} = "";
615              
616 9         20 foreach my $Line (@Splitter) {
617 110         144 $column++;
618              
619             #------------------------------------------------------------------------------
620             # Report date and time
621             # These aren't always present (for example, from the http interface)
622             #------------------------------------------------------------------------------
623              
624 110 100 66     293 if ( $column == 1 && $Line =~ /^\d{4}\/\d{2}\/\d{2}$/ ) {
625 9         21 $Self->{report_date} = $Line;
626             }
627              
628 110 100 66     270 if ( $column == 2 && $Line =~ /^\d{2}:\d{2}$/ ) {
629 9         20 $Self->{report_time} = $Line;
630             }
631              
632             #------------------------------------------------------------------------------
633             # ICAO station code
634             #------------------------------------------------------------------------------
635              
636 110 100 100     1908 if ( ( $Line =~ /^([A-Z][A-Z0-9]{3})/ )
    100 66        
    100 66        
    100 33        
    100 33        
    100 66        
    100 100        
    100 100        
    100 100        
    100 100        
      66        
      66        
637             && ( !$have_icao_code ) )
638             {
639             # Use the first value that looks like the ICAO code.
640             # This should either be the first item, or
641             # the third item if there is a leading date and time.
642             # (Before we checked have_icao_code, we'd get values
643             # like TSRA or FZFG later in the observation being treated
644             # as the ICAO code.)
645             # We also allow the last three characters to be digits.
646              
647             # There was a check for "AUTO" above before, for now
648             # we'll add an extra check for that value. (AUTO should
649             # show up in the fifth column.)
650 9 50       26 croak "Unexpected value AUTO for ICAO code" if $Line eq "AUTO";
651              
652 9         19 $Self->{code} = $Line;
653 9         26 $have_icao_code = 1;
654             }
655              
656             #------------------------------------------------------------------------------
657             # Report Time
658             #------------------------------------------------------------------------------
659              
660             elsif ( $Line =~ /([0-9]Z)$/ ) {
661 9         31 my $Timez = substr( $Line, 2, 4 );
662 9         19 $Self->{time} = $Timez;
663 9         29 $Self->{day} = substr( $Line, 0, 2 );
664             }
665              
666             #------------------------------------------------------------------------------
667             # Wind speed and direction
668             #------------------------------------------------------------------------------
669              
670             elsif ( $Line =~ /([0-9]KT)$/ ) {
671 9         17 my $Newline;
672             my $Variable;
673              
674 9 50       23 if ( $Line =~ /VRB/ ) {
675 0         0 $Newline = substr( $Line, 3 );
676 0         0 $Variable = "1";
677             }
678             else {
679 9         15 $Newline = $Line;
680             }
681              
682 9         18 my $Winddir = substr( $Newline, 0, 3 );
683 9         21 $Winddir =~ tr/[A-Z]/ /d;
684 9         19 $Winddir = $Winddir - 0;
685 9         14 my $Winddirtxt;
686              
687 9 50 66     85 if ($Variable) {
    100 33        
    50 66        
    100 33        
    50 33        
    50 33        
    50 0        
    0 0        
    0          
688 0         0 $Winddirtxt = "Variable";
689             }
690             elsif ( ( $Winddir <= 22.5 ) || ( $Winddir >= 337.5 ) ) {
691 2         16 $Winddirtxt = "North";
692             }
693             elsif ( ( $Winddir <= 67.5 ) && ( $Winddir >= 22.5 ) ) {
694 0         0 $Winddirtxt = "Northeast";
695             }
696             elsif ( ( $Winddir <= 112.5 ) && ( $Winddir >= 67.5 ) ) {
697 1         2 $Winddirtxt = "East";
698             }
699             elsif ( ( $Winddir <= 157.5 ) && ( $Winddir >= 112.5 ) ) {
700 0         0 $Winddirtxt = "Southeast";
701             }
702             elsif ( ( $Winddir <= 202.5 ) && ( $Winddir >= 157.5 ) ) {
703 0         0 $Winddirtxt = "South";
704             }
705             elsif ( ( $Winddir <= 247.5 ) && ( $Winddir >= 202.5 ) ) {
706 6         10 $Winddirtxt = "Southwest";
707             }
708             elsif ( ( $Winddir <= 292.5 ) && ( $Winddir >= 247.5 ) ) {
709 0         0 $Winddirtxt = "West";
710             }
711             elsif ( ( $Winddir <= 337.5 ) && ( $Winddir >= 292.5 ) ) {
712 0         0 $Winddirtxt = "Northwest";
713             }
714              
715 9         19 my $Windspeedkts = substr( $Line, 3 );
716 9         13 my $Windgustkts = 0;
717              
718 9 100       24 if ( $Windspeedkts =~ /G/ ) {
719 6         16 my @Splitter = split( /G/, $Windspeedkts );
720              
721 6         12 $Windspeedkts = $Splitter[0];
722 6         11 $Windgustkts = $Splitter[1];
723             }
724              
725 9         14 $Windspeedkts =~ tr/[A-Z]//d;
726 9         13 $Windgustkts =~ tr/[A-Z]//d;
727              
728 9 100       21 if ( $Windspeedkts == 0 ) {
729 1         2 $Winddirtxt = "Calm";
730             }
731              
732 9         30 my $MPH = round( convert_kts_to_mph($Windspeedkts) );
733 9         20 my $GMPH = round( convert_kts_to_mph($Windgustkts) );
734 9         29 my $KMH = round( convert_kts_to_kmh($Windspeedkts) );
735 9         16 my $GKMH = round( convert_kts_to_kmh($Windgustkts) );
736              
737 9         22 $Self->{windspeedkts} = $Windspeedkts;
738 9         47 $Self->{windgustkts} = $Windgustkts;
739 9         22 $Self->{windspeedkts} = $Self->{windspeedkts} - 0;
740 9         19 $Self->{windspeedmph} = $MPH;
741 9         13 $Self->{windspeedkmh} = $KMH;
742 9         15 $Self->{windgustmph} = $GMPH;
743 9         14 $Self->{windgustkmh} = $GKMH;
744 9         17 $Self->{winddirtext} = $Winddirtxt;
745 9         17 $Self->{winddir} = $Winddir;
746 9         31 $Self->{winddir} = $Self->{winddir} - 0;
747             }
748              
749             #------------------------------------------------------------------------------
750             # Current Visibility
751             #------------------------------------------------------------------------------
752              
753             elsif ( $Line =~ /([0-9]SM)$/ ) {
754 7         15 $Line =~ tr/[A-Z]//d;
755              
756             #------------------------------------------------------------------------------
757             # Some stations were reporting fractions for this value
758             #------------------------------------------------------------------------------
759              
760 7 100       23 if ( $Line =~ /\// ) {
761 6         14 my @Splitter = split( /\//, $Line );
762 6         17 $Line = $Splitter[0] / $Splitter[1];
763             }
764              
765 7         24 my $Viskm = convert_miles_to_km($Line);
766 7         15 $Self->{visibility_mi} = round($Line);
767 7         14 $Self->{visibility_km} = round($Viskm);
768             }
769              
770             #------------------------------------------------------------------------------
771             # Current Conditions
772             #------------------------------------------------------------------------------
773              
774             elsif (
775             ( $Line =~ /
776             (BR|TS|MI|PR|BC|DR|BL|SH|FZ|DZ|RA|SN|SG|IC|PE|PL|GR|GS|UP|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS)
777             ([A-Z])*
778             /x
779             )
780             || ( $Line =~ /^VC([A-Z])*/ )
781             || ( $Line =~ /[\+\-]VC([A-Z])*/ )
782             )
783             {
784             my ( $conditionstext, $conditions1, $conditions2, $intensity )
785             = translate_weather( $Line, $Self->{conditionstext},
786 6         18 $Self->{conditions1}, $Self->{conditions2} );
787 6 50       36 $Self->{conditionstext} = $conditionstext
788             if defined $conditionstext;
789 6 50       14 $Self->{conditions1} = $conditions1 if defined $conditions1;
790 6 50       11 $Self->{conditions2} = $conditions2 if defined $conditions2;
791 6 100       20 $Self->{intensity} = $intensity if defined $intensity;
792             }
793              
794             #------------------------------------------------------------------------------
795             # Cloud Cover
796             #------------------------------------------------------------------------------
797              
798             elsif (( $Line =~ /^(VV[0-9])/ )
799             || ( $Line =~ /^(SKC[0-9])/ )
800             || ( $Line =~ /^(CLR)/ )
801             || ( $Line =~ /^(FEW)/ )
802             || ( $Line =~ /^(SCT[0-9])/ )
803             || ( $Line =~ /^(BKN[0-9])/ )
804             || ( $Line =~ /^(OVC[0-9])/ ) )
805             {
806              
807 12         30 push( @Cloudlevels, $Line );
808              
809 12         22 $Self->{cloudcover} = "";
810 12 50       61 if ( $Line =~ /^(CLR)/ ) {
    100          
    100          
    100          
    50          
811 0         0 $Self->{cloudcover} = "Clear";
812             }
813             elsif ( $Line =~ /^(FEW)/ ) {
814 3         7 $Self->{cloudcover} = "Fair";
815             }
816             elsif ( $Line =~ /^(SCT[0-9])/ ) {
817 2         4 $Self->{cloudcover} = "Partly Cloudy";
818             }
819             elsif ( $Line =~ /^(BKN[0-9])/ ) {
820 1         3 $Self->{cloudcover} = "Mostly Cloudy";
821             }
822             elsif ( $Line =~ /^(OVC[0-9])/ ) {
823 6         12 $Self->{cloudcover} = "Cloudy";
824             }
825              
826 12 100       45 if ( !$Self->{conditionstext} ) {
827 3         10 $Self->{conditionstext} = $Self->{cloudcover};
828             }
829             }
830              
831             #------------------------------------------------------------------------------
832             # Get the temperature/dewpoint and calculate windchill/heat index
833             #------------------------------------------------------------------------------
834              
835             elsif (( $Line =~ /^([0-9][0-9]\/[0-9][0-9])/ )
836             || ( $Line =~ /^(M[0-9][0-9]\/)/ )
837             || ( $Line =~ /^(M[0-9][0-9]\/M[0-9][0-9])/ )
838             || ( $Line =~ /^([0-9][0-9]\/M[0-9][0-9])/ ) )
839             {
840 9         25 my @Splitter = split( /\//, $Line );
841 9         16 my $Temperature = $Splitter[0];
842 9         13 my $Dewpoint = $Splitter[1];
843 9 100       16 if ( !defined $Dewpoint ) {
844 1         3 $Dewpoint = "";
845             }
846              
847 9 100       26 if ( $Temperature =~ /M/ ) {
848 1         3 $Temperature =~ tr/[A-Z]//d;
849 1         7 $Temperature = ( $Temperature - ( $Temperature * 2 ) );
850             }
851 9         20 $Temperature += 0;
852              
853 9 50       20 if ( $Dewpoint =~ /M/ ) {
854 0         0 $Dewpoint =~ tr/[A-Z]//d;
855 0         0 $Dewpoint = ( $Dewpoint - ( $Dewpoint * 2 ) );
856             }
857 9 100       22 if ( $Dewpoint ne "" ) {
858 8         12 $Dewpoint += 0;
859             }
860              
861 9         45 my $Tempf = convert_c_to_f($Temperature);
862 9         20 my $Dewf = convert_c_to_f($Dewpoint);
863              
864 9         43 my $rh = "";
865 9 100 66     29 if ( defined $Dewpoint && $Dewpoint ne "" ) {
866 8         32 my $Es = 6.11
867             * 10.0**( 7.5 * $Temperature / ( 237.7 + $Temperature ) );
868 8         40 my $E = 6.11
869             * 10.0**( 7.5 * $Dewpoint / ( 237.7 + $Dewpoint ) );
870 8         20 $rh = round( ( $E / $Es ) * 100 );
871             }
872              
873 9         13 my $F = $Tempf;
874              
875 9         27 my $Heati = heat_index( $F, $rh );
876 9         23 my $Heatic = convert_f_to_c($Heati);
877              
878 9         15 $Tempf = round($Tempf);
879 9         15 $Dewf = round($Dewf);
880 9         19 $Heati = round($Heati);
881 9         16 $Heatic = round($Heatic);
882              
883 9         30 my $Windc = windchill( $F, $Self->{windspeedmph} );
884 9         18 my $Windcc = convert_f_to_c($Windc);
885 9         14 $Windc = round($Windc);
886 9         14 $Windcc = round($Windcc);
887              
888 9         16 $Self->{temperature_c} = $Temperature;
889 9         16 $Self->{temperature_f} = $Tempf;
890 9         16 $Self->{dewpoint_c} = $Dewpoint;
891 9         15 $Self->{dewpoint_f} = $Dewf;
892 9         18 $Self->{relative_humidity} = $rh;
893 9         17 $Self->{heat_index_c} = $Heatic;
894 9         21 $Self->{heat_index_f} = $Heati;
895 9         14 $Self->{windchill_c} = $Windcc;
896 9         37 $Self->{windchill_f} = $Windc;
897             }
898              
899             #------------------------------------------------------------------------------
900             # Calculate the atmospheric pressure in different formats.
901             # Based on report (inches of mercury)
902             #------------------------------------------------------------------------------
903              
904             elsif ( $Line =~ /^(A[0-9]{4})/ ) {
905 8         20 $Line =~ tr/[A-Z]//d;
906 8         19 my $Part1 = substr( $Line, 0, 2 );
907 8         16 my $Part2 = substr( $Line, 2, 4 );
908 8         20 $Self->{pressure_inhg} = "$Part1.$Part2";
909              
910 8         37 my $mb = $Self->{pressure_inhg} * 33.8639;
911 8         12 my $mmHg = $Self->{pressure_inhg} * 25.4;
912 8         13 my $lbin = ( $Self->{pressure_inhg} * 0.491154 );
913 8         12 my $kgcm = ( $Self->{pressure_inhg} * 0.0345316 );
914 8         17 $mb = round($mb);
915 8         36 $mmHg = round($mmHg);
916              
917 8         19 $Self->{pressure_mb} = $mb;
918 8         14 $Self->{pressure_mmhg} = $mmHg;
919 8         14 $Self->{pressure_lbin} = $lbin;
920 8         36 $Self->{pressure_kgcm} = $kgcm;
921             }
922              
923             #------------------------------------------------------------------------------
924             # Calculate the atmospheric pressure in different formats.
925             # Based on report (millibars)
926             #------------------------------------------------------------------------------
927              
928             elsif ( $Line =~ /^(Q[0-9]{4})/ ) {
929 1         3 $Line =~ tr/[A-Z]//d;
930 1         1 $Self->{pressure_mb} = $Line;
931              
932 1         4 my $inhg = ( $Self->{pressure_mb} * 0.02953 );
933 1         6 $Self->{pressure_inhg} = sprintf( "%.2f", $inhg );
934 1         3 my $mmHg = $Self->{pressure_inhg} * 25.4;
935 1         1 my $lbin = ( $Self->{pressure_inhg} * 0.491154 );
936 1         2 my $kgcm = ( $Self->{pressure_inhg} * 0.0345316 );
937 1         1 $mmHg = round($mmHg);
938              
939 1         2 $Self->{pressure_mmhg} = $mmHg;
940 1         16 $Self->{pressure_lbin} = $lbin;
941 1         4 $Self->{pressure_kgcm} = $kgcm;
942             }
943              
944             #------------------------------------------------------------------------------
945             # If the remarks section is starting, we are done
946             #------------------------------------------------------------------------------
947              
948             elsif ( $Line =~ /^(RMK)/ ) {
949 8         27 last;
950             }
951             }
952              
953             #------------------------------------------------------------------------------
954             # Read the remarks into an array for later processing
955             #------------------------------------------------------------------------------
956              
957 9         37 my $Remarks = 0;
958 9         14 my @Remarkarray;
959              
960 9         19 foreach my $Line (@Splitter) {
961 133 100       262 if ( $Line =~ /^(RMK)/ ) {
962 8         13 $Remarks = 1;
963             }
964              
965 133 100       234 if ($Remarks) {
966 31         60 push( @Remarkarray, $Line );
967             }
968             }
969              
970             #------------------------------------------------------------------------------
971             # Delete the temp file
972             #------------------------------------------------------------------------------
973              
974 9         25 $Self->{cloudlevel_arrayref} = \@Cloudlevels;
975 9         19 $Self->{station_type} = "Manual";
976              
977             #------------------------------------------------------------------------------
978             # Now we process remarks. These aren't all going to be in the report,
979             # and usually aren't. This has made it hard to develop. This section
980             # is basically incomplete, but you can get some of the data out
981             #------------------------------------------------------------------------------
982              
983 9         23 foreach my $Remark (@Remarkarray) {
984 31 50       69 if ($Remark) {
985 31         42 my $Line = $Remark;
986              
987 31 100       188 if ( $Remark =~ /^AO[1-2]/ ) {
    100          
    50          
    100          
    50          
    50          
    50          
    100          
988 1         17 $Self->{station_type} = "Automated";
989             }
990             elsif ( $Remark =~ /^SLP/ ) {
991 7         17 $Remark =~ tr/[A-Z]//d;
992              
993 7 50 33     50 if ( !defined $Remark || $Remark eq "" ) {
994 0         0 $Remark = 0;
995             }
996              
997 7 50 33     37 if ( ($Remark) && ( $Remark >= 800 ) ) {
998 0         0 $Remark = $Remark * .1;
999 0         0 $Remark = $Remark + 900;
1000             }
1001             else {
1002 7         16 $Remark = $Remark * .1;
1003 7         13 $Remark = $Remark + 1000;
1004             }
1005              
1006 7         18 $Self->{slp_inhg} = ( $Remark * 0.0295300 );
1007 7         45 $Self->{slp_inhg} = substr( $Self->{slp_inhg}, 0, 5 );
1008 7         19 $Self->{slp_mmhg} = round( $Remark * 0.750062 );
1009 7         16 $Self->{slp_lbin} = ( $Remark * 0.0145038 );
1010 7         15 $Self->{slp_kgcm} = ( $Remark * 0.00101972 );
1011 7         12 $Self->{slp_mb} = round($Remark);
1012             }
1013              
1014             #------------------------------------------------------------------------------
1015             # Thunderstorm info
1016             #------------------------------------------------------------------------------
1017              
1018             elsif ( $Remark =~ /^TS/ ) {
1019 0         0 $Self->{storm} = $Remark;
1020             }
1021              
1022             #------------------------------------------------------------------------------
1023             # Three hour pressure tendency
1024             #------------------------------------------------------------------------------
1025              
1026             elsif ( $Remark =~ /^5[0-9]/ ) {
1027 1         53 $Self->{thpressure} = $Remark;
1028             }
1029              
1030             #------------------------------------------------------------------------------
1031             # Automated station needs maintenance
1032             #------------------------------------------------------------------------------
1033              
1034             elsif ( $Remark =~ /\$/ ) {
1035 0         0 $Self->{maintenance} = $Remark;
1036             }
1037              
1038             #------------------------------------------------------------------------------
1039             # Precipitation since last report (100ths of an inch)
1040             #------------------------------------------------------------------------------
1041              
1042             elsif ( $Remark =~ /^P[0-9]/ ) {
1043 0         0 $Self->{precipslr} = $Remark;
1044             }
1045              
1046             #------------------------------------------------------------------------------
1047             # Event beginning or ending
1048             #------------------------------------------------------------------------------
1049              
1050             elsif ( $Line
1051             =~ /^(BRB|TSB|MIB|PRB|BCB|DRB|BLB|SHB|FZB|DZB|RAB|SNB|SGB|ICB|PEB|GRB|GSB|UPB|FGB|FUB|VAB|DUB|SAB|HZB|PYB|POB|SQB|FCB|SSB)/
1052             )
1053             {
1054 0         0 $Self->{eventbe} = $Remark;
1055             }
1056              
1057             #------------------------------------------------------------------------------
1058             # Precise temperature reading
1059             #------------------------------------------------------------------------------
1060              
1061             elsif ( $Remark =~ /^T[0-9]/ ) {
1062 7         23 $Self->{ptemperature} = $Remark;
1063             }
1064             }
1065             }
1066              
1067 9         18 my $Templatefile = $Self->{tfile};
1068              
1069 9 50 33     22 if ( $Templatefile && $Templatefile ne "" ) {
1070 0         0 local $/; # enable slurp mode
1071 0 0       0 open my $F, '<', $Templatefile
1072             or croak "error opening template file $Templatefile: $!";
1073 0         0 my $tout = <$F>;
1074 0         0 close($F);
1075              
1076 0         0 $tout =~ s{ %% ( .*? ) %% }
1077             { exists( $Self->{$1} )
1078 0 0       0 ? $Self->{$1}
1079             : ""
1080             }gsex;
1081              
1082 0         0 $Self->{templateout} = $tout;
1083             }
1084              
1085 9         15 $Self->{remark_arrayref} = \@Remarkarray;
1086 9         44 return $Self;
1087             }
1088              
1089             1;
1090             __END__