File Coverage

blib/lib/Weather/NHC/TropicalCyclone/Storm.pm
Criterion Covered Total %
statement 83 145 57.2
branch 9 32 28.1
condition 5 18 27.7
subroutine 25 34 73.5
pod 19 21 90.4
total 141 250 56.4


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