File Coverage

blib/lib/Weather/NHC/TropicalCyclone/Storm.pm
Criterion Covered Total %
statement 85 147 57.8
branch 11 34 32.3
condition 4 18 22.2
subroutine 25 34 73.5
pod 19 21 90.4
total 144 254 56.6


line stmt bran cond sub pod time code
1             package Weather::NHC::TropicalCyclone::Storm;
2              
3 2     2   13 use strict;
  2         2  
  2         53  
4 2     2   9 use warnings;
  2         4  
  2         49  
5              
6 2     2   9 use HTTP::Tiny ();
  2         4  
  2         38  
7 2     2   8 use HTTP::Status qw/:constants/;
  2         3  
  2         598  
8 2     2   731 use Util::H2O::More qw/baptise/;
  2         1830  
  2         89  
9 2     2   1881 use Validate::Tiny ();
  2         28907  
  2         40  
10 2     2   2005 use HTML::TreeBuilder ();
  2         51589  
  2         4179  
11              
12             our $DEFAULT_GRAPHICS_ROOT = q{https://www.nhc.noaa.gov/storm_graphics};
13             our $DEFAULT_BTK_ROOT = q{https://ftp.nhc.noaa.gov/atcf/btk};
14             our $CLASSIFICATIONS = {
15             TD => q{Tropical Depression},
16             STD => q{Subtropical Depression},
17             TS => q{Tropical Storm},
18             HU => q{Hurricane},
19             STS => q{Subtropical Storm},
20             PTC => q{Post-tropical Cyclone / Remnants},
21             TY => q{Typhoon (we don't use this currently)},
22             PC => q{Potential Tropical Cyclone},
23             };
24              
25             # constructor
26             sub new {
27 6     6 0 12 my ( $pkg, $self ) = @_;
28              
29 6         22 my $v = Validate::Tiny->new;
30 6         48 my $validation = $v->check( $self, $pkg->_get_validation_rules );
31 6 50       142 if ( not $v->success ) {
32 0         0 die qq{Field validation errors found creating package instance for: } . join( q{, }, keys %{ $validation->error } ) . qq{\n};
  0         0  
33             }
34              
35 6         113 my @fields =
36             (qw/id binNumber name classification intensity pressure latitudelongitude latitude_numberic movementDir movementSpeed lastUpdate publicAdvisory forecastAdvisory windSpeedProbabilities forecastDiscussion forecastGraphics forecastTrack windWatchesWarnings trackCone initialWindExtent forecastWindRadiiGIS bestTrackGIS earliestArrivalTimeTSWindsGIS mostLikelyTimeTSWindsGIS windSpeedProbabilitiesGIS kmzFile34kt kmzFile50kt kmzFile64kt stormSurgeWatchWarningGIS potentialStormSurgeFloodingGIS/);
37              
38 6         21 return baptise -recurse, $self, $pkg, @fields;
39             }
40              
41             sub _get_validation_rules {
42 6     6   10 my $self = shift;
43             return {
44             fields => [qw/id binNumber name classification/],
45             checks => [
46             [qw/id binNumber name classification/] => Validate::Tiny::is_required(),
47             classification => sub {
48 6     6   1458 my ( $value, $params ) = @_;
49              
50             # branch, if true indicates failed validation
51 6 50       25 if ( not grep { /$value/ } ( keys %$CLASSIFICATIONS ) ) {
  48         158  
52 0         0 return q{Invalid classification, not defined in NHC specification.};
53             }
54              
55             # indicates successful validation
56 6         16 return undef;
57             },
58 6         24 ],
59             };
60             }
61              
62             sub _fetch_text_types {
63 0     0   0 my $self = shift;
64              
65             # white list of resources and URL attributes they provide
66 0         0 my $types = {
67             text => [qw/publicAdvisory forecastAdvisory forecastDiscussion windSpeedProbabilities/],
68             };
69              
70 0         0 return $types;
71             }
72              
73             sub _fetch_data_types {
74 41     41   59 my $self = shift;
75              
76             # white list of resources and URL attributes they provide
77 41         262 my $types = {
78             zipFile => [qw/forecastTrack windWatchesWarnings trackCone initialWindExtent forecastWindRadiiGIS bestTrackGIS potentialStormSurgeFloodingGIS/],
79             kmzFile => [qw/forecastTrack windWatchesWarnings trackCone initialWindExtent forecastWindRadiiGIS bestTrackGIS earliestArrivalTimeTSWindsGIS mostLikelyTimeTSWindsGIS/],
80             zipFile5km => [qw/windSpeedProbabilitiesGIS/],
81             zipFile0p5deg => [qw/windSpeedProbabilitiesGIS/],
82             kmzFile34kt => [qw/windSpeedProbabilitiesGIS/],
83             kmzFile50kt => [qw/windSpeedProbabilitiesGIS/],
84             kmzFile64kt => [qw/windSpeedProbabilitiesGIS/],
85             kmlFile => [qw/stormSurgeWatchWarningGIS/],
86             zipFileTidalMask => [qw/potentialStormSurgeFloodingGIS/],
87             };
88              
89 41         79 return $types;
90             }
91              
92             # get storm classification "real classification"
93             sub kind {
94 2     2 1 6347 my $self = shift;
95 2 50       6 die qq{'classification' field not set\n} if not $self->classification;
96 2 50       16 die qq{Unknown storm classification\n} if not $CLASSIFICATIONS->{ $self->classification };
97 2         12 return $CLASSIFICATIONS->{ $self->classification };
98             }
99              
100             # determine basin based on binNumber
101             sub basin {
102 0     0 0 0 my $self = shift;
103 0 0       0 die qq{'binNumber' field not set\n} if not $self->binNumber;
104              
105             # allow for easy querying of "basin"
106 0         0 my $BASINS = {
107             atlantic => qr/^AT[1-5]$/i,
108             pacific => qr/^EP[1-5]$/i,
109             };
110 0         0 for my $basin ( keys %$BASINS ) {
111 0 0       0 return $basin if $self->binNumber =~ $BASINS->{$basin};
112             }
113 0         0 return undef;
114             }
115              
116             # attempts to get base graphics directory, then scrapes
117             # the index page for the files and returns an array reference
118             # of all image addresses for this storm
119             sub fetch_forecastGraphics_urls {
120 0     0 1 0 my $self = shift;
121              
122 0         0 my $url = $self->forecastGraphics->url;
123              
124 0         0 my $http = HTTP::Tiny->new;
125              
126 0         0 my $response = $http->get($url);
127              
128 0         0 my $html = $response->{content};
129              
130 0         0 $html =~ m/storm_graphics\/(.+)\/refresh/;
131 0         0 my $prefix = $1;
132 0 0       0 return [] if not $prefix;
133              
134 0         0 my $base = sprintf( qq{%s/%s}, $DEFAULT_GRAPHICS_ROOT, $prefix );
135 0         0 $response = $http->get($base);
136              
137 0         0 $html = $response->{content};
138 0         0 my $id = uc $self->id;
139 0         0 my @imgs = ( $html =~ m/href="($id.+\.png)"/g );
140 0         0 @imgs = map { qq{$base/$_} } @imgs;
  0         0  
141              
142 0         0 return \@imgs;
143             }
144              
145             # rolls up requesting url and extracting text inside of the
 
146             # tag into one subroutine
147             sub _get_text {
148 0     0   0 my ( $self, $resource, $local_file ) = @_;
149              
150             # note, accessors like "->advNum" are generated in Weather::NHC::TropicalCyclone using Util::H2O::h2o
151 0 0 0     0 if ( not( $self->$resource->advNum or $self->$resource->issuance or $self->$resource->url ) ) {
      0        
152 0         0 die qq{Resource must be one of: 'publicAdvisory', 'forecastAdvisory', or 'forecastDiscssion'\n};
153             }
154              
155 0         0 my $url = $self->$resource->url;
156              
157 0         0 my $http = HTTP::Tiny->new;
158              
159 0         0 my $response = $http->get($url);
160              
161             # extract actual advisory text from
 and return just that text 
162 0         0 my $htb = HTML::TreeBuilder->new;
163 0         0 $htb->parse( $response->{content} );
164              
165 0         0 my $pre = $htb->look_down( '_tag', 'pre' );
166              
167 0 0       0 if ($local_file) {
168 0 0       0 open my $fh, q{>}, $local_file or die qq{Failed to open '$local_file' for writing: $!\n};
169 0         0 print $fh $pre->as_text;
170 0         0 close $fh;
171             }
172              
173 0         0 return ( $pre->as_text, $self->$resource->advNum, $local_file );
174             }
175              
176             # optionally provide a local file name to save fetched file to
177             sub fetch_publicAdvisory {
178 0     0 1 0 my ( $self, $local_file ) = @_;
179 0         0 return $self->_get_text( q{publicAdvisory}, $local_file );
180             }
181              
182             # optionally provide a local file name to save fetched file to
183             sub fetch_forecastAdvisory {
184 0     0 1 0 my ( $self, $local_file ) = @_;
185 0         0 return $self->_get_text( q{forecastAdvisory}, $local_file );
186             }
187              
188             # in this case, the $local_file is the file to which the ATCF data is saved,
189             # the forecast advisory is only handled as text; ATCF is returned as an arrary
190             # ref
191             sub fetch_forecastAdvisory_as_atcf {
192 0     0 1 0 my ( $self, $local_file ) = @_;
193 0         0 my ( $text, $advNum, $_ignore ) = $self->_get_text(q{forecastAdvisory});
194 0         0 require Weather::NHC::TropicalCyclone::ForecastAdvisory;
195              
196 0         0 my $fst_ref = Weather::NHC::TropicalCyclone::ForecastAdvisory->new( input_text => $text, output_file => $local_file );
197 0         0 my $atcf_text = $fst_ref->extract_atcf;
198              
199             # save file if $local_file is passed
200 0 0       0 if ($local_file) {
201 0         0 $fst_ref->save_atcf;
202             }
203              
204 0         0 return ( $fst_ref->as_atcf, $advNum, $local_file );
205             }
206              
207             # optionally provide a local file name to save fetched file to
208             sub fetch_forecastDiscussion {
209 0     0 1 0 my ( $self, $local_file ) = @_;
210 0         0 return $self->_get_text( q{forecastDiscussion}, $local_file );
211             }
212              
213             # optionally provide a local file name to save fetched file to
214             sub fetch_windspeedProbabilities {
215 0     0 1 0 my ( $self, $local_file ) = @_;
216 0         0 return $self->_get_text( q{windSpeedProbabilities}, $local_file );
217             }
218              
219             # optionally provide a local file name to save fetched file to
220             sub fetch_forecastTrack {
221 4     4 1 2272 my ( $self, $type, $local_file ) = @_;
222 4         12 return $self->_get_file( q{forecastTrack}, $type, $local_file );
223             }
224              
225             # optionally provide a local file name to save fetched file to
226             sub fetch_windWatchesWarnings {
227 4     4 1 1062 my ( $self, $type, $local_file ) = @_;
228 4         10 return $self->_get_file( q{windWatchesWarnings}, $type, $local_file );
229             }
230              
231             # optionally provide a local file name to save fetched file to
232             sub fetch_trackCone {
233 4     4 1 1058 my ( $self, $type, $local_file ) = @_;
234 4         9 return $self->_get_file( q{trackCone}, $type, $local_file );
235             }
236              
237             # optionally provide a local file name to save fetched file to
238             sub fetch_initialWindExtent {
239 4     4 1 1122 my ( $self, $type, $local_file ) = @_;
240 4         11 return $self->_get_file( q{initialWindExtent}, $type, $local_file );
241             }
242              
243             # optionally provide a local file name to save fetched file to
244             sub fetch_forecastWindRadiiGIS {
245 4     4 1 1087 my ( $self, $type, $local_file ) = @_;
246 4         9 return $self->_get_file( q{forecastWindRadiiGIS}, $type, $local_file );
247             }
248              
249             # optionally provide a local file name to save fetched file to
250             sub fetch_bestTrackGIS {
251 4     4 1 1057 my ( $self, $type, $local_file ) = @_;
252 4         10 return $self->_get_file( q{bestTrackGIS}, $type, $local_file );
253             }
254              
255             # optionally provide a local file name to save fetched file to
256             sub fetch_earliestArrivalTimeTSWindsGIS {
257 2     2 1 530 my ( $self, $type, $local_file ) = @_;
258 2         7 return $self->_get_file( q{earliestArrivalTimeTSWindsGIS}, $type, $local_file );
259             }
260              
261             # optionally provide a local file name to save fetched file to
262             sub fetch_mostLikelyTimeTSWindsGIS {
263 2     2 1 531 my ( $self, $type, $local_file ) = @_;
264 2         6 return $self->_get_file( q{mostLikelyTimeTSWindsGIS}, $type, $local_file );
265             }
266              
267             # optionally provide a local file name to save fetched file to
268             sub fetch_windSpeedProbabilitiesGIS {
269 5     5 1 1425 my ( $self, $type, $local_file ) = @_;
270 5         11 return $self->_get_file( q{windSpeedProbabilitiesGIS}, $type, $local_file );
271             }
272              
273             # optionally provide a local file name to save fetched file to
274             sub fetch_stormSurgeWatchWarningGIS {
275 2     2 1 1245 my ( $self, $type, $local_file ) = @_;
276 2         5 return $self->_get_file( q{stormSurgeWatchWarningGIS}, $type, $local_file );
277             }
278              
279             # optionally provide a local file name to save fetched file to
280             sub fetch_potentialStormSurgeFloodingGIS {
281 4     4 1 1341 my ( $self, $type, $local_file ) = @_;
282 4         13 return $self->_get_file( q{potentialStormSurgeFloodingGIS}, $type, $local_file );
283             }
284              
285             # rolls up requesting file, based on url associated with file key ("url" key is not specified)
286             sub _get_file {
287 39     39   71 my ( $self, $resource, $urlKey, $local_file ) = @_;
288              
289 39         72 my $types = $self->_fetch_data_types;
290              
291             # make sure $urlKey is provided by the resource (defined in $types hash ref above)
292 39 50       55 die qq{'$urlKey' is not a valid type provided by '$resource'.\n} if not grep { /$resource/ } ( @{ $types->{$urlKey} } );
  235         860  
  39         74  
293              
294             # check to make sure $resource is not 'null'
295 39 50 33     112 return undef if ref $self->$resource ne q{HASH} and not $self->$resource->{$urlKey};
296              
297 39         463 my $url = $self->$resource->{$urlKey};
298              
299             # extract file name from URL if no $local_file is specified
300 39 50       187 if ( not $local_file ) {
301              
302             # extract file name from the end of the URL
303 39         207 $url =~ m/\/([a-zA-Z0-9_]+)\.([a-zA-Z]+)$/;
304 39         118 $local_file = qq{$1.$2};
305             }
306              
307 39         112 my $http = HTTP::Tiny->new;
308              
309 39         2367 my $response = $http->mirror( $url, $local_file );
310              
311 39 50       211 if ( not $response->{success} ) {
312 0   0     0 my $status = $response->{status} // q{Unknown};
313 0         0 die qq{Download of $url failed. HTTP status: $status\n};
314             }
315              
316             # bestTrackGIS resource doesn't provide "advNum" per specification
317             # so it doesn't try to deref an method that may not exist
318              
319             # 'can($method)' is a limitation of what Util:H2O::h2o gives us, unfortunately
320 39 100 50     87 my $advNum = ( $self->$resource->can('advNum') ) ? $self->$resource->advNum // q{N/A} : q{N/A};
321 39         719 return ( $local_file, $advNum );
322             }
323              
324             # auxillary methods to fetch the best track ".dat" file via NHC's FTP over HTTPS
325              
326             sub fetch_best_track {
327 4     4 1 2433 my ( $self, $local_file ) = @_;
328              
329 4         12 my $btk_file = sprintf( "b%s.dat", $self->id );
330 4         34 my $url = sprintf( "%s/%s", $DEFAULT_BTK_ROOT, $btk_file );
331              
332 4   66     15 $local_file //= $btk_file;
333              
334 4         11 my $http = HTTP::Tiny->new;
335              
336 4         250 my $response = $http->mirror( $url, $local_file );
337              
338 4 50       24 if ( not $response->{success} ) {
339 0   0     0 my $status = $response->{status} // q{Unknown};
340 0         0 die qq{Download of $url failed. HTTP status: $status\n};
341             }
342              
343             # bestTrackGIS resource doesn't provide "advNum" per specification
344 4         18 return $local_file;
345             }
346              
347             1;
348              
349             __END__