File Coverage

blib/lib/Geo/METAR/Deduced.pm
Criterion Covered Total %
statement 133 133 100.0
branch 38 38 100.0
condition 12 12 100.0
subroutine 37 37 100.0
pod 18 18 100.0
total 238 238 100.0


line stmt bran cond sub pod time code
1             # -*- cperl; cperl-indent-level: 4 -*-
2             # Copyright (C) 2020-2021, Roland van Ipenburg
3             package Geo::METAR::Deduced v1.0.1;
4 7     7   257369 use Moose;
  7         3436035  
  7         53  
5 7     7   59618 use MooseX::NonMoose;
  7         7825  
  7         30  
6 7     7   522803 use Geo::METAR;
  7         42491  
  7         375  
7             extends 'Geo::METAR';
8              
9             #use Log::Log4perl qw(:resurrect :easy get_logger);
10              
11 7     7   5083 use Class::Measure::Scientific::FX_992vb;
  7         556470  
  7         533  
12 7     7   5041 use Geo::ICAO qw( :all );
  7         409147  
  7         86  
13 7     7   14183 use Set::Scalar;
  7         84033  
  7         364  
14             ###l4p use Data::Dumper;
15              
16 7     7   65 use utf8;
  7         16  
  7         55  
17 7     7   324 use 5.016000;
  7         40  
18              
19 7     7   4551 use English qw( -no_match_vars );
  7         13284  
  7         62  
20              
21 7     7   3131 use Readonly;
  7         19  
  7         17058  
22             ## no critic (ProhibitCallsToUnexportedSubs)
23             Readonly::Scalar my $ICAO_MAX_CEILING => 200;
24             Readonly::Scalar my $HECTO => 100;
25             Readonly::Scalar my $INF => q{inf};
26             Readonly::Scalar my $METER => q{m};
27             Readonly::Scalar my $FT => q{ft};
28             Readonly::Scalar my $MI => q{mile};
29             Readonly::Scalar my $PA => q{pa};
30             Readonly::Scalar my $INHG => q{inhg};
31             Readonly::Scalar my $CELSIUS => q{C};
32             Readonly::Scalar my $KNOTS => q{kn};
33             Readonly::Scalar my $DEG => q{deg};
34             Readonly::Scalar my $VFR => 3;
35             Readonly::Scalar my $MVFR => 2;
36             Readonly::Scalar my $IFR => 1;
37             Readonly::Scalar my $LIFR => 0;
38             Readonly::Scalar my $HG => 33.863886;
39             Readonly::Scalar my $AVERAGE => 2;
40             Readonly::Scalar my $MINUS => q{-};
41             Readonly::Scalar my $DEFAULT_RULES => q{ICAO};
42              
43             Readonly::Hash my %VIS_MIN => (
44             'VFR' => 5,
45             'MVFR' => 3,
46             'IFR' => 1,
47             );
48             Readonly::Hash my %CEIL_MIN => (
49             'VFR' => 3000,
50             'MVFR' => 1000,
51             'IFR' => 500,
52             );
53              
54             # Re-use the %_weather_types lookup table from Geo::METAR:
55             Readonly::Hash my %WX => (
56             'MI' => q{shallow},
57             'PI' => q{partial},
58             'BC' => q{patches},
59             'BL' => q{blowing},
60             'SH' => q{shower(s)},
61             'TS' => q{thunderstorm},
62             'FZ' => q{freezing},
63              
64             'DZ' => q{drizzle},
65             'RA' => q{rain},
66             'SN' => q{snow},
67             'SG' => q{snow grains},
68             'IC' => q{ice crystals},
69             'PE' => q{ice pellets},
70             'GR' => q{hail},
71             'GS' => q{small hail/snow pellets},
72             'UP' => q{unknown precip},
73              
74             'BR' => q{mist},
75             'FG' => q{fog},
76             'PRFG' => q{fog banks}, # officially PR is a modifier of FG
77             'FU' => q{smoke},
78             'VA' => q{volcanic ash},
79             'DU' => q{dust},
80             'SA' => q{sand},
81             'HZ' => q{haze},
82             'PY' => q{spray},
83              
84             'PO' => q{dust/sand whirls},
85             'SQ' => q{squalls},
86             'FC' => q{funnel cloud(tornado/waterspout)},
87             'SS' => q{sand storm},
88             'DS' => q{dust storm},
89              
90             );
91             Readonly::Hash my %RULES => (
92             'US' => q{USA},
93             'UK' => q{United Kingdom},
94             );
95             Readonly::Hash my %LOG => (
96             'RESET' => q{Reset properties for population from new METAR string '%s'},
97             'RESET_PROP' => q{Resetting property '%s' to '%s'},
98             'RULES_CHANGED' => q{Rules changed to '%s' based on ICAO '%s'},
99             'INTERSECTION' => q{Overlapping rules for ICAO code '%s'},
100             );
101             ## use critic
102              
103             ## no critic qw(ProhibitCommentedOutCode)
104             ###l4p Log::Log4perl->easy_init($ERROR);
105             ###l4p my $log = get_logger();
106             ## use critic
107              
108             my %rules = ();
109              
110             sub _len {
111 133     133   461 my ( $amount, $unit ) = @_;
112 133         785 return Class::Measure::Scientific::FX_992vb->length( $amount + 0, $unit );
113             }
114              
115             my %vis_min = ();
116             for my $k ( keys %VIS_MIN ) {
117             $vis_min{$k} = _len( $VIS_MIN{$k}, $MI );
118             }
119              
120             my %ceil_min = ();
121             for my $k ( keys %CEIL_MIN ) {
122             $ceil_min{$k} = _len( $CEIL_MIN{$k}, $FT );
123             }
124              
125             my $combined = Set::Scalar->new;
126             for my $k ( keys %RULES ) {
127             ## no critic (ProhibitCallsToUnexportedSubs)
128             $rules{$k} = Set::Scalar->new( Geo::ICAO::country2code( $RULES{$k} ) );
129             ## use critic
130             $combined->insert( $rules{$k}->members );
131             }
132             if ( !$combined->is_universal ) {
133             ###l4p $log->warn( sprintf $LOG{'INTERSECTING'},
134             ###l4p $combined->difference( $combined->universe ) );
135             }
136              
137             has 'rules' => ( 'isa' => 'Str', 'is' => 'rw', 'default' => $DEFAULT_RULES );
138              
139             around 'metar' => sub {
140             my $orig = shift;
141             my $self = shift;
142             my $args = shift;
143             if ( defined $args ) {
144             $args =~ tr{\n}{ };
145              
146             # Reset the object when a new METAR string is loaded because the parent
147             # doesn't do that for us:
148             my $PRISTINE = Geo::METAR->new();
149             ###l4p $log->debug( sprintf $LOG{'RESET'}, $args );
150             for my $k ( keys %{$PRISTINE} ) {
151             ###l4p $log->trace( sprintf $LOG{'RESET_PROP'},
152             ###l4p $k, Data::Dumper::Dumper( ${$PRISTINE}{$k} ) );
153             $self->{$k} = ${$PRISTINE}{$k};
154             }
155             ###l4p $log->debug( join q{,}, @{ $self->{'sky'} } );
156             }
157             return $self->$orig($args);
158             };
159              
160             after 'metar' => sub {
161             my $self = shift;
162             $self->rules($DEFAULT_RULES);
163             for my $k ( keys %rules ) {
164             while ( defined( my $code = $rules{$k}->each ) ) {
165             if ( 0 == rindex $self->{'SITE'}, $code, 0 ) {
166             ###l4p $log->debug( sprintf $LOG{'RULES_CHANGED'},
167             ###l4p $k, $self->{'SITE'} );
168             $self->rules($k);
169             }
170             }
171             }
172             };
173              
174             sub date {
175 2     2 1 2386 my $self = shift;
176 2         15 return $self->{'DATE'} + 0;
177             }
178              
179             ## no critic (ProhibitBuiltinHomonyms)
180             sub time {
181             ## use critic
182 1     1 1 2 my $self = shift;
183 1         6 return $self->{'TIME'};
184             }
185              
186             sub mode {
187 1     1 1 3 my $self = shift;
188 1         5 return $self->{'modifier'};
189             }
190              
191             sub wind_dir {
192 2     2 1 1427 my $self = shift;
193             return Class::Measure::Scientific::FX_992vb->angle(
194 2         25 $self->{'WIND_DIR_DEG'} + 0, $DEG );
195             }
196              
197             sub wind_dir_eng {
198 1     1 1 319 my $self = shift;
199 1         6 return $self->{'WIND_DIR_ENG'};
200             }
201              
202             sub wind_dir_abb {
203 1     1 1 2 my $self = shift;
204 1         6 return $self->{'WIND_DIR_ABB'};
205             }
206              
207             sub wind_var {
208 3     3 1 254 my $self = shift;
209 3 100       19 return defined $self->{'WIND_VAR'} ? 1 : 0;
210             }
211              
212             sub wind_low {
213 3     3 1 1994 my $self = shift;
214             return
215             defined $self->{'WIND_VAR_1'}
216 3 100       25 ? Class::Measure::Scientific::FX_992vb->angle( $self->{'WIND_VAR_1'} + 0,
217             $DEG )
218             : undef;
219             }
220              
221             sub wind_high {
222 3     3 1 1894 my $self = shift;
223             return
224             defined $self->{'WIND_VAR_2'}
225 3 100       104 ? Class::Measure::Scientific::FX_992vb->angle( $self->{'WIND_VAR_2'} + 0,
226             $DEG )
227             : undef;
228             }
229              
230             sub wind_speed {
231 2     2 1 1679 my $self = shift;
232 2         15 return Class::Measure::Scientific::FX_992vb->speed( $self->{'WIND_KTS'} + 0,
233             $KNOTS );
234             }
235              
236             sub wind_gust {
237 2     2 1 230 my $self = shift;
238 2         5 my $gust = $self->{'WIND_GUST_KTS'};
239 2 100       8 if ($gust) {
240 1         3 $gust += 0;
241             }
242             else {
243 1         2 $gust = 0;
244             }
245 2         13 return Class::Measure::Scientific::FX_992vb->speed( $gust, $KNOTS );
246             }
247              
248             ## no critic qw(ProhibitVagueNames)
249             sub temp {
250             ## use critic
251 2     2 1 1737 my $self = shift;
252             return Class::Measure::Scientific::FX_992vb->temperature(
253 2         14 $self->{'TEMP_C'} + 0, $CELSIUS );
254             }
255              
256             sub dew {
257 2     2 1 1929 my $self = shift;
258             return Class::Measure::Scientific::FX_992vb->temperature(
259 2         13 $self->{'DEW_C'} + 0, $CELSIUS );
260             }
261              
262             sub alt {
263 1     1 1 237 my $self = shift;
264             return Class::Measure::Scientific::FX_992vb->pressure(
265 1         5 $self->{'pressure'} * $HECTO, $PA );
266             }
267              
268             sub pressure {
269 1     1 1 336 my $self = shift;
270             return Class::Measure::Scientific::FX_992vb->pressure(
271 1         4 $self->{'pressure'} * $HECTO, $PA );
272             }
273              
274             # This isn't handled in Geo::METAR, it's just tokenized for the parser
275             sub _vertical_visibility {
276 20     20   43 my $self = shift;
277 20         76 my $vv = +$INF;
278 20         64 $self->{'METAR'} =~ m{.*\bVV(?<vv>\d{3})\b.*}msx;
279 20 100       117 if ( defined $LAST_PAREN_MATCH{'vv'} ) {
280 3         15 $vv = $LAST_PAREN_MATCH{'vv'} * $HECTO;
281             }
282 20         59 return _len( $vv, $FT );
283             }
284              
285             # https://en.wikipedia.org/wiki/Ceiling_(cloud)
286             # Rules say 20000ft is 6000m so we use ft to avoid rounding errors.
287             sub ceiling {
288 34     34 1 8487 my $self = shift;
289              
290 34         66 my $cloud_ceiling = +$INF;
291             my %TEST = (
292             'ICAO' => sub {
293 8     8   20 my ($base) = @_;
294 8         39 return $base < $ICAO_MAX_CEILING;
295             },
296             'UK' => sub {
297 1     1   5 return 1;
298             },
299             'US' => sub {
300 12     12   49 return 1;
301             },
302 34         370 );
303 34         74 for my $layer ( @{ $self->{'sky'} } ) {
  34         92  
304             ###l4p $log->trace($layer);
305             ## no critic (ProhibitUnusedCapture)
306 48 100       285 if ( $layer =~ m{(?:BKN|OVC)(?<base>\d{3})}igmsx ) {
307             ## use critic
308 22         154 my $cloud_base = $LAST_PAREN_MATCH{'base'};
309 22 100 100     805 if ( $cloud_base < $cloud_ceiling
310             && $TEST{ $self->rules }($cloud_base) )
311             {
312 20         60 $cloud_ceiling = $cloud_base;
313             }
314             }
315             }
316 34 100       1022 if ( q{US} eq $self->rules ) {
317 20         56 my $vv = $self->_vertical_visibility()->ft() / $HECTO;
318 20 100       5563 if ( $vv < $cloud_ceiling ) {
319 3         49 $cloud_ceiling = $vv;
320             }
321             }
322 34 100       375 return ( $INF == $cloud_ceiling )
323             ? _len( $cloud_ceiling, $FT )
324             : _len( $cloud_ceiling * $HECTO, $FT );
325             }
326              
327             sub visibility {
328 37     37 1 5020 my $self = shift;
329 37         84 my $vis = $self->{'visibility'};
330 37         138 my $whole = qr{(?:(?<whole>[[:digit:]]+)[ ])*}smx;
331 37         589 $vis =~ m{$whole(?<amount>[[:digit:]/]+)(?<unit>SM)*$}msx;
332 37         91 my $unit = $METER;
333 37 100       310 if ( $LAST_PAREN_MATCH{'unit'} ) {
334 26         54 $unit = $MI;
335             }
336 37         176 my $amount = $LAST_PAREN_MATCH{'amount'};
337 37         82 my $total = 0;
338 37 100       151 if ( $LAST_PAREN_MATCH{'whole'} ) {
339 4         17 $total = $LAST_PAREN_MATCH{'whole'};
340             }
341 37 100       135 if ( $amount =~ m{(?<num>[[:digit:]]+)[/](?<den>[[:digit:]]+)}msx ) {
342 8         63 $total += $LAST_PAREN_MATCH{'num'} / $LAST_PAREN_MATCH{'den'};
343             }
344             else {
345 29         56 $total = $amount;
346             }
347 37         99 return _len( $total, $unit );
348             }
349              
350             # https://en.wikipedia.org/wiki/METAR#Flight_categories_in_the_U.S.
351             # https://www.experimentalaircraft.info/wx/colors-metar-taf.php
352             sub flight_rule {
353 15     15 1 5794 my $self = shift;
354 15         34 my $lvl;
355 15 100 100     55 if ( $self->visibility()->mile() < $vis_min{'IFR'}->mile()
    100 100        
    100 100        
356             || $self->ceiling()->ft() < $ceil_min{'IFR'}->ft() )
357             {
358 5         1976 $lvl = $LIFR;
359             }
360             elsif ($self->visibility()->mile() < $vis_min{'MVFR'}->mile()
361             || $self->ceiling()->ft() < $ceil_min{'MVFR'}->ft() )
362             {
363 2         520 $lvl = $IFR;
364             }
365             elsif ($self->visibility()->mile() <= $vis_min{'VFR'}->mile()
366             || $self->ceiling()->ft() <= $ceil_min{'VFR'}->ft() )
367             {
368 5         1328 $lvl = $MVFR;
369             }
370             else {
371 3         746 $lvl = $VFR;
372             }
373              
374 15         586 return $lvl;
375             }
376              
377             # Make it possible to check for weather types and make it return:
378             # 0 when not observed
379             # 1 observed as light
380             # 2 observed as normal
381             # 3 observed as heavy
382             for my $k ( keys %WX ) {
383              
384             sub _nom {
385 210     210   538 my $label = shift;
386 210         1392 $label =~ s{[(].*[)]}{}gmsx;
387 210         819 $label =~ s{(\s|/)+}{_}gmsx;
388 210         1027 return $label;
389             }
390             ## no critic (ProhibitNoStrict)
391 7     7   89 no strict q{refs};
  7         17  
  7         1569  
392             ## use critic
393             *{ _nom( $WX{$k} ) } = sub {
394 7     7   7324 my $self = shift;
395 7         15 my $wx = Set::Scalar->new( @{ $self->weather } );
  7         67  
396 7         907 my $lvl = 0;
397 7         211 my $RE = qr{^(?<modifier>[+-]*)$k}msx;
398 7         36 while ( defined( my $w = $wx->each ) ) {
399 7 100       137 if ( $w =~ $RE ) {
400 6         18 $lvl = $AVERAGE;
401 6 100       65 if ( $LAST_PAREN_MATCH{'modifier'} ) {
402 5 100       40 ( $LAST_PAREN_MATCH{'modifier'} eq $MINUS )
403             ? $lvl--
404             : $lvl++;
405             }
406             }
407             }
408 7         95 return $lvl;
409             };
410             }
411              
412 7     7   62 no Moose;
  7         14  
  7         126  
413             __PACKAGE__->meta->make_immutable;
414              
415             1;
416              
417             __END__
418              
419             =for stopwords Ipenburg merchantability METAR
420              
421             =head1 NAME
422              
423             Geo::METAR::Deduced - deduce aviation information from parsed METAR data
424              
425             =head1 VERSION
426              
427             This document describes Geo::METAR::Deduced C<v1.0.1>.
428              
429             =head1 SYNOPSIS
430              
431             use Geo::METAR::Deduced;
432             $m = new Geo::METAR::Deduced;
433             $m->metar("KFDY 251450Z 21012G21KT 8SM OVC065 04/M01 A3010 RMK 57014");
434             $m->alt();
435             $m->pressure();
436             $m->date();
437             $m->dew();
438             $m->ice_crystals();
439             $m->mode();
440             $m->temp();
441             $m->time();
442             $m->ceiling();
443             $m->flight_rule();
444             $m->wind_dir();
445             $m->wind_speed();
446             $m->wind_gust();
447             $m->wind_var();
448             $m->wind_high();
449             $m->wind_low();
450             $m->snow();
451             $m->dust();
452             $m->rain();
453             $m->ice_pellets();
454             $m->drizzle();
455             $m->funnel_cloud();
456             $m->hail();
457             $m->squalls();
458             $m->partial();
459             $m->patches();
460             $m->dust_storm();
461             $m->small_hail_snow_pellets();
462             $m->volcanic_ash();
463             $m->freezing();
464             $m->fog();
465             $m->spray();
466             $m->mist();
467             $m->fog_banks();
468             $m->shallow();
469             $m->sand();
470             $m->sand_storm();
471             $m->smoke();
472             $m->haze();
473             $m->shower();
474             $m->dust_sand_whirls();
475             $m->thunderstorm();
476             $m->snow_grains();
477             $m->blowing();
478             $m->unknown_precip();
479             $m->visibility();
480              
481             =head1 DESCRIPTION
482              
483             Get information from METAR that isn't explicitly in the METAR.
484              
485             =head1 SUBROUTINES/METHODS
486              
487             Methods that return a measurement return that as a
488             L<Class::Measure::Scientific::FX_992vb> object so the value can be converted
489             to other units, like from feet to meters or from miles to kilometers.
490              
491             =over 4
492              
493             =item C<Geo::METAR::Deduced-E<gt>new()>
494              
495             Constructs a new Geo::METAR::Deduced object.
496              
497             =item C<$m-E<gt>metar()>
498              
499             Gets or sets the METAR string.
500              
501             =item C<$m-E<gt>mode()>
502              
503             Returns the METAR mode.
504              
505             =item C<$m-E<gt>date()>
506              
507             Returns the day of the month of the METAR. It doesn't return a date object
508             because we don't want to make the implied month and year explicit.
509              
510             =item C<$m-E<gt>time()>
511              
512             Returns the time of the METAR as string. It doesn't return a date object
513             because we don't want to make the implied month and year explicit.
514              
515             =item C<$m-E<gt>ceiling()>
516              
517             Returns the ceiling based on cloud level or vertical visibility data as
518             measurement.
519              
520             =item C<$m-E<gt>visibility()>
521              
522             Returns the visibility as measurement.
523              
524             =item C<$m-E<gt>flight_rule()>
525              
526             Returns the flight rule based on ceiling and visibility as 0 for low C<IFR>, 1
527             for C<IFR>, 2 for C<marginal VFR> and 3 for C<VFR>.
528              
529             =item C<$m-E<gt>alt()>
530              
531             Returns the altimeter setting as pressure measurement.
532              
533             =item C<$m-E<gt>pressure()>
534              
535             Returns the pressure as measurement.
536              
537             =item C<$m-E<gt>dew()>
538              
539             Returns the dew temperature as measurement.
540              
541             =item C<$m-E<gt>temp()>
542              
543             Returns the temperature as measurement.
544              
545             =item C<$m-E<gt>wind_dir()>
546              
547             Returns the wind direction as angle measurement.
548              
549             =item C<$m-E<gt>wind_dir_eng()>
550              
551             Returns the wind direction in English, like C<Northwest>.
552              
553             =item C<$m-E<gt>wind_dir_abb()>
554              
555             Returns the wind direction abbreviation in English, like C<NW>.
556              
557             =item C<$m-E<gt>wind_speed()>
558              
559             Returns the wind speed as speed measurement.
560              
561             =item C<$m-E<gt>wind_gust()>
562              
563             Returns the wind gust speed as speed measurement.
564              
565             =item C<$m-E<gt>wind_var()>
566              
567             Returns if the wind is varying.
568              
569             =item C<$m-E<gt>wind_high()>
570              
571             Returns the highest direction of the varying wind as angle measurement.
572              
573             =item C<$m-E<gt>wind_low()>
574              
575             Returns the lowest direction of the varying wind as angle measurement.
576              
577             =item Weather types
578              
579             The following weather types return 0 when they are not observed, 1 when in a
580             light condition, 2 for a normal condition and 3 for heavy:
581              
582             =over 8
583              
584             =item C<$m-E<gt>snow()>
585              
586             =item C<$m-E<gt>dust()>
587              
588             =item C<$m-E<gt>rain()>
589              
590             =item C<$m-E<gt>ice_crystals()>
591              
592             =item C<$m-E<gt>ice_pellets()>
593              
594             =item C<$m-E<gt>drizzle()>
595              
596             =item C<$m-E<gt>funnel_cloud()>
597              
598             =item C<$m-E<gt>hail()>
599              
600             =item C<$m-E<gt>squalls()>
601              
602             =item C<$m-E<gt>partial()>
603              
604             =item C<$m-E<gt>patches()>
605              
606             =item C<$m-E<gt>dust_storm()>
607              
608             =item C<$m-E<gt>small_hail_snow_pellets()>
609              
610             =item C<$m-E<gt>volcanic_ash()>
611              
612             =item C<$m-E<gt>freezing()>
613              
614             =item C<$m-E<gt>fog()>
615              
616             =item C<$m-E<gt>spray()>
617              
618             =item C<$m-E<gt>mist()>
619              
620             =item C<$m-E<gt>fog_banks()>
621              
622             =item C<$m-E<gt>shallow()>
623              
624             =item C<$m-E<gt>sand()>
625              
626             =item C<$m-E<gt>sand_storm()>
627              
628             =item C<$m-E<gt>smoke()>
629              
630             =item C<$m-E<gt>haze()>
631              
632             =item C<$m-E<gt>shower()>
633              
634             =item C<$m-E<gt>dust_sand_whirls()>
635              
636             =item C<$m-E<gt>thunderstorm()>
637              
638             =item C<$m-E<gt>snow_grains()>
639              
640             =item C<$m-E<gt>blowing()>
641              
642             =item C<$m-E<gt>unknown_precip()>
643              
644             =back
645              
646             =back
647              
648             =head1 CONFIGURATION AND ENVIRONMENT
649              
650             None.
651              
652             =head1 DEPENDENCIES
653              
654             =over 4
655              
656             =item * Perl 5.16
657             =item * L<Class::Measure::Scientific::FX_992vb>
658             =item * L<English>
659             =item * L<Geo::ICOA>
660             =item * L<Geo::METAR>
661             =item * L<Moose>
662             =item * L<MooseX::NonMoose>
663             =item * L<Readonly> 1.03
664             =item * L<Set::Scalar>
665              
666             =back
667              
668             =head1 INCOMPATIBILITIES
669              
670             This module has the same limitations as L<Geo::METAR>. We suspect there is
671             also an incompatibility with a threaded version of perl 5.22.1.
672              
673             =head1 DIAGNOSTICS
674              
675             This module uses L<Log::Log4perl> for logging.
676              
677             =head1 BUGS AND LIMITATIONS
678              
679             There is still plenty to deduce from the format that METAR has to offer in
680             it's fullest form.
681              
682             Please report any bugs or feature requests at
683             L<Bitbucket
684             |https://bitbucket.org/rolandvanipenburg/geo-metar-deduced/issues>.
685              
686             =head1 AUTHOR
687              
688             Roland van Ipenburg, E<lt>roland@rolandvanipenburg.comE<gt>
689              
690             =head1 LICENSE AND COPYRIGHT
691              
692             Copyright 2020-2021 by Roland van Ipenburg
693             This program is free software; you can redistribute it and/or modify
694             it under the GNU General Public License v3.0.
695              
696             =head1 DISCLAIMER OF WARRANTY
697              
698             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
699             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
700             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
701             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
702             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
703             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
704             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
705             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
706             NECESSARY SERVICING, REPAIR, OR CORRECTION.
707              
708             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
709             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
710             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
711             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
712             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
713             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
714             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
715             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
716             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
717             SUCH DAMAGES.
718              
719             =cut