File Coverage

blib/lib/Weather/NHC/TropicalCyclone/ForecastAdvisory.pm
Criterion Covered Total %
statement 243 266 91.3
branch 41 76 53.9
condition 14 27 51.8
subroutine 10 10 100.0
pod 0 4 0.0
total 308 383 80.4


line stmt bran cond sub pod time code
1             package Weather::NHC::TropicalCyclone::ForecastAdvisory;
2              
3 1     1   798 use strict;
  1         2  
  1         24  
4 1     1   5 use warnings;
  1         2  
  1         20  
5 1     1   378 use Date::Calc;
  1         5116  
  1         36  
6 1     1   586 use Getopt::Long;
  1         8498  
  1         3  
7 1     1   519 use Util::H2O::More qw/baptise/;
  1         5317  
  1         2362  
8              
9             sub new {
10 5     5 0 4694 my ( $pkg, %self ) = @_;
11              
12             # input_file and input_text are mutually exclusive via the contructor
13 5 100 100     41 if ( not( exists $self{input_file} or exists $self{input_text} ) or ( exists $self{input_file} and exists $self{input_text} ) or not exists $self{output_file} ) {
      100        
      100        
      100        
14 3         18 die qq{Constructor requires specifying the 'input_file' xor 'input_text', and 'output_file' parameters.\n};
15             }
16 2         7 my @fields = (qw/input_file input_text output_file as_atcf/);
17 2         8 my $self = baptise -recurse, \%self, $pkg, @fields;
18 2         372 return $self;
19             }
20              
21             sub extract_and_save_atcf {
22 2     2 0 606 my $self = shift;
23 2         7 $self->extract_atcf;
24 2         27 return $self->save_atcf;
25             }
26              
27             sub save_atcf {
28 4     4 0 1777 my $self = shift;
29 4 50       17 open( my $fh, q{>}, $self->output_file ) || die qq{Failed to open output ATCF file} . $self->output_file . qq{ : $!.\n};
30 4         253 my $output_ref = $self->as_atcf;
31 4         83 print $fh join( qq{\n}, @$output_ref );
32 4         318 close $fh;
33 4         20 return $self->output_file;
34             }
35              
36             sub extract_atcf {
37 4     4 0 2338 my $self = shift;
38              
39 4         8 my @lines = ();
40              
41             ADVISORY_SOURCE:
42 4 100       8 if ( $self->input_file ) {
    50          
43 2 50       21 open( my $INPUT, q{<}, $self->input_file ) || die q{Failed to open forecast advisory file} . $self->input_file . qq{ for conversion to ATCF format: $!.\n};
44 2         193 @lines = (<$INPUT>);
45 2         25 close $INPUT;
46             }
47             elsif ( $self->input_text ) {
48 2         23 @lines = split /\n/, $self->input_text;
49             }
50              
51 4         42 my @output = (); # accumulate content for $output, do write at the very end
52              
53             # parse rss reports from Nat'l Hurricane Center Atlantic Marine Forecast
54             # Advisory. NHC Mail (Atlantic Marine)
55             # See:
56             # http://www.nhc.noaa.gov/signup.shtml and select the following list: Atlantic
57             # Marine (Forecast/Advisories and updates ONLY)
58              
59             # BASIN,CY,YYYYMMDDHH,TECHNUM,TECH,TAU,LatN/S,LonE/W,VMAX,MSLP,TY,RAD,WINDCODE,RAD1,RAD2,RAD3,RAD4,RADP,RRP,MRD,GUSTS,EYE,SUBREGION,MAXSEAS,INITIALS,DIR,SPEED,STORMNAME,DEPTH,SEAS,SEASCODE,SEAS1,SEAS2,SEAS3,SEAS4
60             #
61             # 1 1 1 1 1 1 1 1 1 1
62             # 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
63             #01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
64             #AL, 01, 2009052912, 03, OFCL, 0, 393N, 649W, 30, 1006, TD, 34, NEQ, 0, 0, 0, 0, 0, 0, 40, 40, 0, , 0, TBK, 65, 17, , , 12, NEQ, 60, 60, 0, 0
65 4         5 my $template = "_BASIN_, 01, 2009010100, , OFCL, 0, 000N, 000W, 30, 0, , 34, NEQ, 0, 0, 0, 0, 0, 0, 0, 40, 0, , 0, TBK, 65, 17, , , 12, NEQ, 60, 60, 0, 0";
66 4         27 my %month_lookup = ( dummy => '00', JAN => '01', FEB => '02', MAR => '03', APR => '04', MAY => '05', JUN => '06', JUL => '07', AUG => '08', SEP => '09', OCT => '10', NOV => '11', DEC => '12' );
67              
68             # These are the data we are extracting
69 4         5 my $pressure;
70 4         5 my $storm_class = "";
71 4         60 my $storm_name;
72             my $storm_basin;
73 4         0 my $storm_number;
74 4         0 my $adv_num;
75 4         0 my $storm_year;
76 4         0 my $nowcast_year;
77 4         0 my $forecast_year;
78 4         0 my $nowcast_month;
79 4         0 my $forecast_month;
80 4         0 my $nowcast_day;
81 4         0 my $forecast_day;
82 4         0 my $nowcast_hour;
83 4         0 my $forecast_hour;
84 4         0 my $date_time;
85 4         0 my $forecast_date_time;
86 4         0 my $nowcast_date_time;
87 4         0 my $nowcast_max_wind;
88 4         0 my $forecast_max_wind;
89 4         6 my $atcf_line = $template;
90 4         11 my $lat;
91             my $lon;
92 4         0 my $vmax;
93 4         0 my $gusts; # in kt
94 4         6 my $center_direction = 65;
95 4         10 my $center_speed = 17;
96              
97 4         5 my $body_ref = \@lines;
98 4         5 my $cnt = @{$body_ref};
  4         7  
99             #
100 4         6 my @match = ();
101              
102             # Get the NHC Number
103             # NWS TPC/NATIONAL HURRICANE CENTER MIAMI FL AL172005
104 4         6 @match = grep /(?:AL|EP|CP|WP|IO|SH|LS)\d{2}\d{4}$/, @{$body_ref};
  4         77  
105 4 50       12 if (@match) {
106 4 50       14 if ( $match[0] =~ /(AL|EP|CP|WP|IO|SH|LS)(\d{2})(\d{4})$/ ) {
107 4         9 $storm_basin = $1;
108 4         8 $storm_number = $2;
109 4         7 $storm_year = $3;
110             }
111 4 50 33     20 die qq{NO NHC NUMBER/YEAR\n} if not $storm_number or not $storm_year or not $storm_basin;
      33        
112             }
113 4         15 $atcf_line =~ s/_BASIN_/$storm_basin/;
114 4         22 my $storm_number_str = sprintf( "%02d", $storm_number );
115 4         8 substr( $atcf_line, 4, 2 ) = $storm_number_str;
116              
117             # Date format
118             # 1500Z THU SEP 02 2004
119             # July 18th TD 2. HAS CHANGED
120             # NOTE NOTE NOW! 1500 UTC TUE JUL 18 2006
121 4 50       9 if ( $storm_year > 2005 ) {
122 4         5 @match = grep /^\d{4} .+ \d{4}$/, @{$body_ref};
  4         71  
123             }
124             else {
125 0         0 @match = grep /^\d{4}Z .+ \d{4}$/, @{$body_ref};
  0         0  
126             }
127             #
128 4 50       9 if (@match) {
129 4         5 $date_time = $match[0];
130 4         18 chomp $date_time;
131 4         15 my @vals = split( ' ', $date_time );
132 4         8 $nowcast_hour = substr( $vals[0], 0, 2 );
133 4 50       13 if ( $storm_year > 2005 ) {
134 4         8 $nowcast_year = $vals[5];
135 4         7 $nowcast_month = $month_lookup{ $vals[3] };
136 4         6 $nowcast_day = $vals[4];
137             }
138             else {
139 0         0 $nowcast_year = $vals[4];
140 0         0 $nowcast_month = $month_lookup{ $vals[2] };
141 0         0 $nowcast_day = $vals[3];
142             }
143 4         9 $nowcast_date_time = $nowcast_year . $nowcast_month . $nowcast_day . $nowcast_hour;
144 4         15 substr( $atcf_line, 8, 10 ) = sprintf( "%10d", $nowcast_date_time );
145             }
146              
147             # advisory number does not appear in the ATCF format
148 4         5 @match = grep /FORECAST.+ADVISORY NUMBER/, @{$body_ref};
  4         47  
149              
150             # HURRICANE FRANCES ADVISORY NUMBER 40
151             # HURRICANE FRANCES FORECAST/ADVISORY NUMBER 37
152             # HURRICANE FRANCES FORECAST/ADVISORY NUMBER 37...CORRECTED
153             # HURRICANE FRANCES SPECIAL FORECAST/ADVISORY NUMBER 37
154 4 50       11 if (@match) {
155 4 50       23 if ( $match[0] =~ /^(.+)\s+FORECAST.+ADVISORY NUMBER\s+(\d{1,3})/ ) {
156 4         7 $storm_name = $1;
157 4         7 $adv_num = $2;
158 4         7 $storm_name =~ s/SPECIAL//;
159             }
160             }
161             else {
162 0         0 @match = grep /ADVISORY NUMBER/, @{$body_ref};
  0         0  
163 0 0       0 if ( $match[0] =~ /^(.+)\s+ADVISORY NUMBER\s+(\d{1,3})/ ) {
164 0         0 $storm_name = $1;
165 0         0 $adv_num = $2;
166             }
167             }
168 4         9 my @tmp = split( ' ', $storm_name );
169 4 50 0     9 if ( $tmp[0] eq 'HURRICANE' ) {
    0 0        
    0 0        
170 4         5 $storm_class = $tmp[0];
171 4         5 $storm_name = $tmp[1];
172             }
173             elsif ( $tmp[0] eq 'POTENTIAL' ) {
174 0         0 $storm_class = "$tmp[0] $tmp[1] $tmp[2]";
175 0         0 $storm_name = $tmp[3];
176             }
177             elsif ( $tmp[0] eq 'TROPICAL' or $tmp[0] eq 'SUBTROPICAL' or $tmp[0] eq 'REMNANTS' or $tmp[0] eq 'POST-TROPICAL' ) {
178              
179             # SUBTROPICAL is rare. see 2007 01
180 0         0 $storm_class = "$tmp[0] $tmp[1]";
181 0         0 $storm_name = $tmp[2];
182             }
183 4         10 substr( $atcf_line, 148, 10 ) = sprintf( "%10s", $storm_name );
184 4         9 my $adv_num_str = sprintf( "%02d", $adv_num );
185 4         8 my $adv_num_url_str = sprintf( "%03d", $adv_num );
186              
187             # HURRICANE CENTER LOCATED NEAR 23.4N 73.9W AT 02/1500Z
188             # or
189             #TROPICAL DEPRESSION DISSIPATING NEAR 29.0N 70.0W AT 24/2100Z
190             # or
191             # TROPICAL DEPRESSION CENTER LOCATED NEAR 11.2N 36.0W AT 25/0300Z
192 4         12 @match = grep /(CENTER LOCATED|DISSIPATING) NEAR/, @{$body_ref};
  4         59  
193 4         7 my $ns_hem = "N";
194 4         11 my $ew_hem = "W";
195 4 50       9 if (@match) {
196 4 50       18 if ( $match[0] =~ /CENTER LOCATED NEAR\s+(\d{1,3}\.\d{1,2})([N|S])\s+(\d{1,3}\.\d{1,2})([E|W])\s+AT/ ) {
197 4         7 $lat = $1;
198 4         6 $ns_hem = $2;
199 4         6 $lon = $3;
200 4         7 $ew_hem = $4;
201             }
202 4 50       9 if ( $match[0] =~ /DISSIPATING NEAR\s+(\d{1,3}\.\d{1,2})([N|S])\s+(\d{1,3}\.\d{1,2})([E|W])\s+AT/ ) {
203 0         0 $lat = $1;
204 0         0 $ns_hem = $2;
205 0         0 $lon = $3;
206 0         0 $ew_hem = $4;
207             }
208             }
209              
210 4         18 my $nowcast_lat = sprintf( "%4d$ns_hem", $lat * 10 );
211 4         11 my $nowcast_lon = sprintf( "%4d$ew_hem", $lon * 10 );
212 4         15 substr( $atcf_line, 34, 5 ) = sprintf( "%5s", $nowcast_lat );
213 4         7 substr( $atcf_line, 41, 5 ) = sprintf( "%5s", $nowcast_lon );
214              
215             #PRESENT MOVEMENT TOWARD THE NORTH-NORTHWEST OR 330 DEGREES AT 9 KT
216 4         6 @match = grep /^PRESENT MOVEMENT TOWARD THE/, @{$body_ref};
  4         36  
217 4 50       9 if (@match) {
218 4 50       17 if ( $match[0] =~ /PRESENT MOVEMENT TOWARD THE.+OR\s+(\d{1,3})\s+DEGREES AT\s+(\d{1,2})\s+KT/ ) {
219 4         8 $center_direction = $1;
220 4         5 $center_speed = $2;
221             }
222 4         9 substr( $atcf_line, 138, 4 ) = sprintf( "%4d", $center_direction );
223 4         9 substr( $atcf_line, 143, 4 ) = sprintf( "%4d", $center_speed );
224             }
225              
226 4         5 @match = grep /^ESTIMATED MINIMUM CENTRAL PRESSURE/, @{$body_ref};
  4         30  
227 4 50       9 if (@match) {
228 4 50       13 if ( $match[0] =~ /^ESTIMATED MINIMUM CENTRAL PRESSURE\s+(.+)\s+MB/ ) {
229 4         8 $pressure = $1;
230             }
231             }
232 4         11 substr( $atcf_line, 53, 4 ) = sprintf( "%4d", $pressure );
233              
234             #MAX SUSTAINED WINDS 25 KT WITH GUSTS TO 35 KT.
235             #MAX SUSTAINED WINDS 125 KT WITH GUSTS TO 155 KT.
236              
237 4         5 @match = grep /^MAX SUSTAINED WINDS/, @{$body_ref};
  4         33  
238              
239 4 50       8 if (@match) {
240 4 50       13 if ( $match[0] =~ /^MAX SUSTAINED WINDS\s+(\d{1,4}) KT WITH GUSTS TO\s+(\d{1,4})/ ) {
241 4         18 $vmax = $1;
242 4         7 $gusts = $2;
243             }
244             }
245              
246 4         10 substr( $atcf_line, 47, 4 ) = sprintf( "%4d", $vmax );
247 4         9 substr( $atcf_line, 113, 4 ) = sprintf( "%4d", $gusts );
248 4         10 my $forecast_atcf_filename = lc($storm_name) . "_advisory_" . $adv_num_str . ".fst";
249             #
250             # collect nowcast wind radii, if any
251 4         5 my $isotachs_found = 0;
252 4         7 my @isotachs;
253 4         7 for my $i ( 0 ... $#{$body_ref} ) {
  4         9  
254 76 100       90 if ( @{$body_ref}[$i] =~ /^MAX SUSTAINED WINDS/ ) {
  76         130  
255              
256             #64 KT....... 45NE 30SE 20SW 30NW.
257             #50 KT.......120NE 75SE 60SW 75NW.
258             #34 KT.......175NE 120SE 120SW 120NW.
259 4         6 $i++;
260 4         6 while (1) {
261 16 100       20 if ( @{$body_ref}[$i] =~ /^(\d{1,2}) KT\.{7}\s{0,}(\d{1,3})[N|S][E|W]\s+(\d{1,3})[N|S][E|W]\s+(\d{1,3})[N|S][E|W]\s+(\d{1,3})[N|S][E|W]/ ) {
  16         56  
262 12         14 $isotachs_found++;
263 12         34 my @wind_radii = ( $1, $2, $3, $4, $5 );
264 12         32 push @isotachs, @wind_radii;
265             }
266             else {
267 4         15 last;
268             }
269 12         16 $i++;
270             }
271 4         11 for ( my $j = $isotachs_found; $j > 0; $j-- ) {
272 12         21 for ( my $k = 0; $k < 4; $k++ ) {
273 48         65 my $starting_pos = 72 + ( $k * 6 );
274 48         59 my $list_pos = 1 + $k + ( 5 * ( $j - 1 ) );
275              
276             # fill in wind radii
277 48         93 substr( $atcf_line, $starting_pos, 5 ) = sprintf( "%5d", $isotachs[$list_pos] );
278              
279             # fill in isotach
280 48         105 substr( $atcf_line, 63, 3 ) = sprintf( "%3d", $isotachs[ 5 * ( $j - 1 ) ] );
281             }
282 12         25 push @output, $atcf_line;
283             }
284 4 50       10 unless ($isotachs_found) {
285 0         0 push @output, $atcf_line;
286             }
287 4         6 last;
288             }
289             }
290              
291             # FORECAST tracks and points
292             # FORECAST VALID 10/0000Z 21.5N 84.5W FORECASTs have a bizzare date format:
293             # dd/hhmm BUT near end of the month say 31/0000 the forecast dates switch to
294             # 01/0000, 01/12000 so we need to check for this and increment the month.
295 4         7 my $forecast_period = 0;
296 4         5 $forecast_year = $nowcast_year;
297 4         21 $forecast_month = $nowcast_month;
298 4         6 $forecast_day = $nowcast_day;
299 4         6 $forecast_hour = $nowcast_hour;
300 4         4 my $i = 0;
301 4         6 while ( $i < $#{$body_ref} ) {
  188         284  
302 184 100       228 if ( @{$body_ref}[$i] =~ /^(FORECAST|OUTLOOK) VALID/ ) {
  184         379  
303 32         48 my $atcf_line = $template;
304 32         87 $atcf_line =~ s/_BASIN_/$storm_basin/;
305              
306             # jgf20160105: fill in the storm number
307 32         50 substr( $atcf_line, 4, 2 ) = $storm_number_str;
308              
309             # fill in the nowcast time
310 32         65 substr( $atcf_line, 8, 10 ) = sprintf( "%10d", $nowcast_date_time );
311              
312             # fill in the storm name
313 32         56 substr( $atcf_line, 148, 10 ) = sprintf( "%10s", $storm_name );
314 32         40 my $line = @{$body_ref}[$i];
  32         54  
315 32         43 chomp $line;
316              
317             # if the storm will dissipate, there is no more data to process
318 32 50       64 if ( $line =~ /DISSIPATED/ ) {
319 0         0 last;
320             }
321 32 50       98 if ( $line =~ /^(FORECAST|OUTLOOK) VALID\s+(\d{2})\/(\d{4})Z/ ) {
322 32         59 $forecast_day = $2;
323 32         66 $forecast_hour = substr( $3, 0, 2 );
324             }
325 32 50       102 if ( $line =~ /Z\s+(\d{1,2}\.\d{1,2})([N|S])\s+(\d{1,2}\.\d{1,2})([E|W])/ ) {
326 32         47 $lat = $1;
327 32         47 $ns_hem = $2;
328 32         38 $lon = $3;
329 32         43 $ew_hem = $4;
330             }
331 32         121 my $forecast_lat = sprintf( "%4d$ns_hem", $lat * 10 );
332 32         73 my $forecast_lon = sprintf( "%4d$ew_hem", $lon * 10 );
333 32         55 substr( $atcf_line, 34, 5 ) = sprintf( "%5s", $forecast_lat );
334 32         53 substr( $atcf_line, 41, 5 ) = sprintf( "%5s", $forecast_lon );
335              
336             # Get the next line
337             # MAX WIND 30 KT...GUSTS 40 KT.
338 32         37 $i++;
339 32         37 $line = @{$body_ref}[$i];
  32         51  
340 32         46 chomp $line;
341 32 50       95 if ( $line =~ /^MAX WIND\s+(\d{1,4}) KT\.\.\.GUSTS\s+(\d{1,4}) KT\./ ) {
342 32         46 $vmax = $1;
343 32         58 $gusts = $2;
344             }
345 32         63 substr( $atcf_line, 47, 4 ) = sprintf( "%4d", $vmax );
346 32         65 substr( $atcf_line, 113, 4 ) = sprintf( "%4d", $gusts );
347 32         68 $forecast_date_time = sprintf( "%04d%02d%02d%02d", $forecast_year, $forecast_month, $forecast_day, $forecast_hour );
348              
349             # check to see if we have crossed into the next month
350 32 50       64 if ( $forecast_date_time < $nowcast_date_time ) {
351 0         0 $forecast_month++;
352 0 0       0 if ( $forecast_month > 12 ) {
353 0         0 $forecast_month = 1;
354             }
355             }
356              
357             # Determine the time in hours (forecast period) between the current
358             # forecast and the nowcast time
359 32         108 ( my $ddays, my $dhrs, my $dsec ) = Date::Calc::Delta_DHMS( $nowcast_year, $nowcast_month, $nowcast_day, $nowcast_hour, 0, 0, $forecast_year, $forecast_month, $forecast_day, $forecast_hour, 0, 0 );
360 32         44 my $forecast_period = $ddays * 24 + $dhrs;
361 32         59 substr( $atcf_line, 29, 4 ) = sprintf( "%4d", $forecast_period );
362              
363             # Get the next line and parse the isotachs
364 32         39 $i++;
365 32         55 ( $i, my $output_ref ) = _parseIotachs( $body_ref, $i, $atcf_line );
366 32         78 push @output, @$output_ref;
367             }
368 184         222 $i++;
369             }
370              
371             # save in instance's internal field
372 4         14 $self->{as_atcf} = \@output;
373              
374             # return array reference for convenience
375 4         12 return $self->as_atcf;
376             }
377              
378             sub _parseIotachs {
379 32     32   45 my ( $body_ref, $i, $atcf_line ) = @_;
380 32         40 my $isotachs_found = 0;
381 32         39 my @isotachs = ();
382 32         41 my @output = ();
383 32         40 while (1) {
384              
385             #64 KT... 45NE 30SE 20SW 30NW.
386 96 100       111 if ( @{$body_ref}[$i] =~ /^(\d{1,2}) KT\.{3}\s{0,}(\d{1,3})[N|S][E|W]\s+(\d{1,3})[N|S][E|W]\s+(\d{1,3})[N|S][E|W]\s+(\d{1,3})[N|S][E|W]/ ) {
  96         287  
387 64         82 $isotachs_found++;
388 64         166 my @wind_radii = ( $1, $2, $3, $4, $5 );
389 64         151 push @isotachs, @wind_radii;
390             }
391             else {
392 32         39 last;
393             }
394 64         77 $i++;
395             }
396 32         73 for ( my $j = $isotachs_found; $j > 0; $j-- ) {
397 64         91 for ( my $k = 0; $k < 4; $k++ ) {
398 256         344 my $starting_pos = 72 + ( $k * 6 );
399 256         329 my $list_pos = 1 + $k + ( 5 * ( $j - 1 ) );
400              
401             # fill in wind radii
402 256         465 substr( $atcf_line, $starting_pos, 5 ) = sprintf( "%5d", $isotachs[$list_pos] );
403              
404             # fill in isotach
405 256         559 substr( $atcf_line, 63, 3 ) = sprintf( "%3d", $isotachs[ 5 * ( $j - 1 ) ] );
406             }
407 64         124 push @output, $atcf_line;
408             }
409 32 100       54 unless ($isotachs_found) {
410 8         15 push @output, $atcf_line;
411             }
412 32         81 return ( $i, \@output );
413             }
414              
415             1;
416              
417             __END__