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.3;
4 7     7   248844 use Moose;
  7         3398652  
  7         50  
5 7     7   58665 use MooseX::NonMoose;
  7         7683  
  7         24  
6 7     7   518194 use Geo::METAR;
  7         42374  
  7         366  
7             extends 'Geo::METAR';
8              
9             #use Log::Log4perl qw(:resurrect :easy get_logger);
10              
11 7     7   4850 use Class::Measure::Scientific::FX_992vb;
  7         554792  
  7         487  
12 7     7   4787 use Geo::ICAO qw( :all );
  7         394920  
  7         60  
13 7     7   12602 use Set::Scalar;
  7         80443  
  7         404  
14             ###l4p use Data::Dumper;
15              
16 7     7   59 use utf8;
  7         16  
  7         55  
17 7     7   299 use 5.016000;
  7         31  
18              
19 7     7   4194 use English qw( -no_match_vars );
  7         12728  
  7         55  
20              
21 7     7   2814 use Readonly;
  7         17  
  7         16806  
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   385 my ( $amount, $unit ) = @_;
112 133         736 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 2436 my $self = shift;
176 2         13 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         5 return $self->{'TIME'};
184             }
185              
186             sub mode {
187 1     1 1 2 my $self = shift;
188 1         4 return $self->{'modifier'};
189             }
190              
191             sub wind_dir {
192 2     2 1 1432 my $self = shift;
193             return Class::Measure::Scientific::FX_992vb->angle(
194 2         27 $self->{'WIND_DIR_DEG'} + 0, $DEG );
195             }
196              
197             sub wind_dir_eng {
198 1     1 1 365 my $self = shift;
199 1         5 return $self->{'WIND_DIR_ENG'};
200             }
201              
202             sub wind_dir_abb {
203 1     1 1 2 my $self = shift;
204 1         4 return $self->{'WIND_DIR_ABB'};
205             }
206              
207             sub wind_var {
208 3     3 1 256 my $self = shift;
209 3 100       21 return defined $self->{'WIND_VAR'} ? 1 : 0;
210             }
211              
212             sub wind_low {
213 3     3 1 2010 my $self = shift;
214             return
215             defined $self->{'WIND_VAR_1'}
216 3 100       22 ? 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 2053 my $self = shift;
223             return
224             defined $self->{'WIND_VAR_2'}
225 3 100       24 ? 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 1677 my $self = shift;
232 2         13 return Class::Measure::Scientific::FX_992vb->speed( $self->{'WIND_KTS'} + 0,
233             $KNOTS );
234             }
235              
236             sub wind_gust {
237 2     2 1 235 my $self = shift;
238 2         5 my $gust = $self->{'WIND_GUST_KTS'};
239 2 100       9 if ($gust) {
240 1         2 $gust += 0;
241             }
242             else {
243 1         3 $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 1838 my $self = shift;
252             return Class::Measure::Scientific::FX_992vb->temperature(
253 2         15 $self->{'TEMP_C'} + 0, $CELSIUS );
254             }
255              
256             sub dew {
257 2     2 1 2043 my $self = shift;
258             return Class::Measure::Scientific::FX_992vb->temperature(
259 2         14 $self->{'DEW_C'} + 0, $CELSIUS );
260             }
261              
262             sub alt {
263 1     1 1 216 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 357 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   36 my $self = shift;
277 20         53 my $vv = +$INF;
278 20         55 $self->{'METAR'} =~ m{.*\bVV(?<vv>\d{3})\b.*}msx;
279 20 100       102 if ( defined $LAST_PAREN_MATCH{'vv'} ) {
280 3         17 $vv = $LAST_PAREN_MATCH{'vv'} * $HECTO;
281             }
282 20         57 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 7825 my $self = shift;
289              
290 34         59 my $cloud_ceiling = +$INF;
291             my %TEST = (
292             'ICAO' => sub {
293 8     8   21 my ($base) = @_;
294 8         40 return $base < $ICAO_MAX_CEILING;
295             },
296             'UK' => sub {
297 1     1   4 return 1;
298             },
299             'US' => sub {
300 12     12   41 return 1;
301             },
302 34         290 );
303 34         68 for my $layer ( @{ $self->{'sky'} } ) {
  34         89  
304             ###l4p $log->trace($layer);
305             ## no critic (ProhibitUnusedCapture)
306 48 100       272 if ( $layer =~ m{(?:BKN|OVC)(?<base>\d{3})}igmsx ) {
307             ## use critic
308 22         142 my $cloud_base = $LAST_PAREN_MATCH{'base'};
309 22 100 100     790 if ( $cloud_base < $cloud_ceiling
310             && $TEST{ $self->rules }($cloud_base) )
311             {
312 20         56 $cloud_ceiling = $cloud_base;
313             }
314             }
315             }
316 34 100       1023 if ( q{US} eq $self->rules ) {
317 20         51 my $vv = $self->_vertical_visibility()->ft() / $HECTO;
318 20 100       5523 if ( $vv < $cloud_ceiling ) {
319 3         46 $cloud_ceiling = $vv;
320             }
321             }
322 34 100       327 return ( $INF == $cloud_ceiling )
323             ? _len( $cloud_ceiling, $FT )
324             : _len( $cloud_ceiling * $HECTO, $FT );
325             }
326              
327             sub visibility {
328 37     37 1 4804 my $self = shift;
329 37         71 my $vis = $self->{'visibility'};
330 37         136 my $whole = qr{(?:(?<whole>[[:digit:]]+)[ ])*}smx;
331 37         552 $vis =~ m{$whole(?<amount>[[:digit:]/]+)(?<unit>SM)*$}msx;
332 37         89 my $unit = $METER;
333 37 100       274 if ( $LAST_PAREN_MATCH{'unit'} ) {
334 26         47 $unit = $MI;
335             }
336 37         159 my $amount = $LAST_PAREN_MATCH{'amount'};
337 37         78 my $total = 0;
338 37 100       141 if ( $LAST_PAREN_MATCH{'whole'} ) {
339 4         14 $total = $LAST_PAREN_MATCH{'whole'};
340             }
341 37 100       130 if ( $amount =~ m{(?<num>[[:digit:]]+)[/](?<den>[[:digit:]]+)}msx ) {
342 8         53 $total += $LAST_PAREN_MATCH{'num'} / $LAST_PAREN_MATCH{'den'};
343             }
344             else {
345 29         52 $total = $amount;
346             }
347 37         242 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 6076 my $self = shift;
354 15         28 my $lvl;
355 15 100 100     43 if ( $self->visibility()->mile() < $vis_min{'IFR'}->mile()
    100 100        
    100 100        
356             || $self->ceiling()->ft() < $ceil_min{'IFR'}->ft() )
357             {
358 5         1609 $lvl = $LIFR;
359             }
360             elsif ($self->visibility()->mile() < $vis_min{'MVFR'}->mile()
361             || $self->ceiling()->ft() < $ceil_min{'MVFR'}->ft() )
362             {
363 2         477 $lvl = $IFR;
364             }
365             elsif ($self->visibility()->mile() <= $vis_min{'VFR'}->mile()
366             || $self->ceiling()->ft() <= $ceil_min{'VFR'}->ft() )
367             {
368 5         1222 $lvl = $MVFR;
369             }
370             else {
371 3         718 $lvl = $VFR;
372             }
373              
374 15         584 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   512 my $label = shift;
386 210         1310 $label =~ s{[(].*[)]}{}gmsx;
387 210         871 $label =~ s{(\s|/)+}{_}gmsx;
388 210         1031 return $label;
389             }
390             ## no critic (ProhibitNoStrict)
391 7     7   68 no strict q{refs};
  7         18  
  7         1559  
392             ## use critic
393             *{ _nom( $WX{$k} ) } = sub {
394 7     7   7469 my $self = shift;
395 7         17 my $wx = Set::Scalar->new( @{ $self->weather } );
  7         70  
396 7         958 my $lvl = 0;
397 7         233 my $RE = qr{^(?<modifier>[+-]*)$k}msx;
398 7         41 while ( defined( my $w = $wx->each ) ) {
399 7 100       145 if ( $w =~ $RE ) {
400 6         18 $lvl = $AVERAGE;
401 6 100       81 if ( $LAST_PAREN_MATCH{'modifier'} ) {
402 5 100       42 ( $LAST_PAREN_MATCH{'modifier'} eq $MINUS )
403             ? $lvl--
404             : $lvl++;
405             }
406             }
407             }
408 7         102 return $lvl;
409             };
410             }
411              
412 7     7   60 no Moose;
  7         16  
  7         76  
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.3>.
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              
658             =item * L<Class::Measure::Scientific::FX_992vb>
659              
660             =item * L<English>
661              
662             =item * L<Geo::ICOA>
663              
664             =item * L<Geo::METAR>
665              
666             =item * L<Moose>
667              
668             =item * L<MooseX::NonMoose>
669              
670             =item * L<Readonly> 1.03
671              
672             =item * L<Set::Scalar>
673              
674             =back
675              
676             =head1 INCOMPATIBILITIES
677              
678             This module has the same limitations as L<Geo::METAR>. We suspect there is
679             also an incompatibility with a threaded version of perl 5.22.1.
680              
681             =head1 DIAGNOSTICS
682              
683             This module uses L<Log::Log4perl> for logging.
684              
685             =head1 BUGS AND LIMITATIONS
686              
687             There is still plenty to deduce from the format that METAR has to offer in
688             it's fullest form.
689              
690             Please report any bugs or feature requests at
691             L<Bitbucket
692             |https://bitbucket.org/rolandvanipenburg/geo-metar-deduced/issues>.
693              
694             =head1 AUTHOR
695              
696             Roland van Ipenburg, E<lt>roland@rolandvanipenburg.comE<gt>
697              
698             =head1 LICENSE AND COPYRIGHT
699              
700             Copyright 2020-2021 by Roland van Ipenburg
701             This program is free software; you can redistribute it and/or modify
702             it under the GNU General Public License v3.0.
703              
704             =head1 DISCLAIMER OF WARRANTY
705              
706             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
707             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
708             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
709             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
710             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
711             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
712             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
713             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
714             NECESSARY SERVICING, REPAIR, OR CORRECTION.
715              
716             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
717             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
718             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
719             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
720             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
721             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
722             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
723             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
724             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
725             SUCH DAMAGES.
726              
727             =cut