File Coverage

lib/Astro/Montenbruck/RiseSet/Sunset.pm
Criterion Covered Total %
statement 79 85 92.9
branch 13 16 81.2
condition 9 9 100.0
subroutine 15 15 100.0
pod 1 1 100.0
total 117 126 92.8


line stmt bran cond sub pod time code
1             package Astro::Montenbruck::RiseSet::Sunset;
2              
3 3     3   22 use strict;
  3         6  
  3         90  
4 3     3   15 use warnings;
  3         7  
  3         79  
5 3     3   16 no warnings qw/experimental/;
  3         8  
  3         90  
6 3     3   16 use feature qw/switch/;
  3         5  
  3         225  
7              
8 3     3   20 use Exporter qw/import/;
  3         6  
  3         102  
9 3     3   24 use Readonly;
  3         6  
  3         132  
10              
11 3     3   25 use Math::Trig qw/:pi deg2rad/;
  3         7  
  3         409  
12              
13 3     3   23 use Astro::Montenbruck::Time qw/cal2jd jd_cent/;
  3         6  
  3         230  
14 3     3   22 use Astro::Montenbruck::Time::Sidereal qw/ramc/;
  3         6  
  3         137  
15 3     3   19 use Astro::Montenbruck::RiseSet::Constants qw/:events :states/;
  3         14  
  3         2707  
16              
17             our @EXPORT_OK = qw/riseset/;
18             our $VERSION = 0.01;
19              
20             sub _cs_phi {
21 31     31   78 my $phi = shift;
22 31         89 my $rphi = deg2rad($phi);
23 31         348 cos($rphi), sin($rphi);
24             }
25              
26             # Finds a parabola through 3 points: (-1, $y_minus), (0, $y_0) and (1, $y_plus),
27             # that do not lie on straight line.
28             # Arguments:
29             # $y_minus, $y_0, $y_plus - three Y-values
30             # Returns:
31             # $nz - number of roots within the interval [-1, +1]
32             # $xe, $ye - X and Y of the extreme value of the parabola
33             # $zero1 - first root within [-1, +1] (for $nz = 1, 2)
34             # $zero2 - second root within [-1, +1] (only for $nz = 2)
35             sub _quad {
36 291     291   692 my ( $y_minus, $y_0, $y_plus ) = @_;
37 291         482 my $nz = 0;
38 291         587 my $a = 0.5 * ( $y_minus + $y_plus ) - $y_0;
39 291         514 my $b = 0.5 * ( $y_plus - $y_minus );
40 291         491 my $c = $y_0;
41              
42 291         611 my $xe = -$b / ( 2 * $a );
43 291         612 my $ye = ( $a * $xe + $b ) * $xe + $c;
44 291         592 my $dis = $b * $b - 4 * $a * $c; # discriminant of y = axx+bx+c
45 291         499 my @zeroes;
46 291 100       793 if ( $dis >= 0 ) {
47              
48             # parabola intersects x-axis
49 286         606 my $dx = 0.5 * sqrt($dis) / abs($a);
50 286         746 @zeroes[ 0, 1 ] = ( $xe - $dx, $xe + $dx );
51 286 100       751 $nz++ if abs( $zeroes[0] ) <= 1;
52 286 100       644 $nz++ if abs( $zeroes[1] ) <= 1;
53 286 100       749 $zeroes[0] = $zeroes[1] if $zeroes[0] < -1;
54             }
55 291         1095 $nz, $xe, $ye, @zeroes;
56             }
57              
58             # Calculates sine of the altitude at hourly intervals.
59             sub _sin_alt {
60 613     613   1733 my ( $jd, $lambda, $cphi, $sphi, $get_position ) = @_;
61 613         1696 my ( $ra, $de ) = $get_position->($jd);
62 613         12632 my $tau = deg2rad( ramc( $jd, $lambda ) ) - $ra;
63 613         5913 $sphi * sin($de) + $cphi * cos($de) * cos($tau);
64             }
65              
66             sub riseset {
67 31     31 1 2871 my %arg = @_;
68 31         87 my $jd0 = cal2jd( @{$arg{date}} );
  31         176  
69 31         127 my ( $cphi, $sphi ) = _cs_phi($arg{phi});
70             my $sin_alt = sub {
71 613     613   1052 my $hour = shift;
72 613         1771 _sin_alt( $jd0 + $hour / 24, $arg{lambda}, $cphi, $sphi, $arg{get_position} );
73 31         151 };
74 31         66 my $hour = 1;
75 31         95 my $y_minus = $sin_alt->( $hour - 1 ) - $arg{sin_h0};
76 31         84 my $above = $y_minus > 0;
77 31         77 my ( $rise_found, $set_found ) = ( 0, 0 );
78              
79             # loop over search intervals from [0h-2h] to [22h-24h]
80 31   100     58 do {
      100        
81 291         680 my $y_0 = $sin_alt->($hour) - $arg{sin_h0};
82 291         756 my $y_plus = $sin_alt->( $hour + 1 ) - $arg{sin_h0};
83              
84             # find parabola through three values $y_minus, $y_0, $y_plus
85 291         877 my ( $nz, $xe, $ye, @zeroes ) = _quad( $y_minus, $y_0, $y_plus );
86 291         603 given ($nz) {
87 291         752 when (1) {
88 59 100       181 if ( $y_minus < 0 ) {
89 29         188 $arg{on_event}->( $EVT_RISE, $hour + $zeroes[0] );
90 29         18723 $rise_found = 1;
91             }
92             else {
93 30         172 $arg{on_event}->( $EVT_SET, $hour + $zeroes[0] );
94 30         22479 $set_found = 1;
95             }
96             }
97 232         470 when (2) {
98 0 0       0 if ( $ye < 0 ) {
99 0         0 $arg{on_event}->( $EVT_RISE, $hour + $zeroes[1] );
100 0         0 $arg{on_event}->( $EVT_SET, $hour + $zeroes[0] );
101             }
102             else {
103 0         0 $arg{on_event}->( $EVT_RISE, $hour + $zeroes[0] );
104 0         0 $arg{on_event}->( $EVT_SET, $hour + $zeroes[1] );
105             }
106 0         0 ( $rise_found, $set_found ) = ( 1, 1 );
107             }
108             }
109              
110             # prepare for next interval
111 291         549 $y_minus = $y_plus;
112 291         1761 $hour += 2;
113             } until ( ( $hour == 25 ) || ( $rise_found && $set_found ) );
114              
115 31 50 100     598 $arg{on_noevent}->( $above ? $STATE_CIRCUMPOLAR : $STATE_NEVER_RISES)
    100          
116             unless ( $rise_found || $set_found );
117             }
118              
119             1;
120             __END__
121              
122             =pod
123              
124             =encoding UTF-8
125              
126             =head1 NAME
127              
128             Astro::Montenbruck::RiseSet::Sunset — rise and set.
129              
130             =head1 SYNOPSIS
131              
132             use Astro::Montenbruck::MathUtils qw/frac/;
133             use Astro::Montenbruck::RiseSet::Constants qw/:events :altitudes/;
134             use Astro::Montenbruck::RiseSet::Sunset qw/:riseset/;
135              
136             riseset(
137             date => [1989, 3, 23],
138             phi => 48.1,
139             lambda => -11.6,
140             get_position => sub {
141             my $jd = shift;
142             # return equatorial coordinates of the celestial body for the Julian Day.
143             },
144             sin_h0 => sin( deg2rad($H0_PLANET) ),
145             on_event => sub {
146             my ($evt, $ut) = @_;
147             say "$evt: $ut";
148             },
149             on_noevent => sub {
150             my $state = shift;
151             say $state;
152             }
153             );
154              
155             =head1 VERSION
156              
157             Version 0.01
158              
159             =head1 DESCRIPTION
160              
161             Low level routines for calculating rise and set times of celestial bodies. Unlike
162             L<Astro::Montenbruck::RiseSet::RST> module, they are based on algorithms from the
163             I<Montenbruck & Phleger> book. They are especially usefull for calculating
164             different types of twilight. Meeus's method is unsuitable for calculating
165             I<astronomical twilight>.
166              
167             =head1 FUNCTIONS
168              
169             =head2 riseset ( %args )
170              
171             time of rise and set events.
172              
173             =head3 Named Arguments
174              
175             =over
176              
177             =item * B<get_position> — function, which given I<Standard Julian Day>,
178             returns equatorial coordinates of the celestial body, in radians.
179              
180             =item * B<date> — array of B<year> (astronomical, zero-based), B<month> [1..12]
181             and B<day>, [1..31].
182              
183              
184             =item * B<phi> — geographic latitude, degrees, positive northward
185              
186             =item * B<lambda> —geographic longitude, degrees, positive westward
187              
188             =item * B<get_position> — function, which given I<Standard Julian Day>,
189             returns equatorial coordinates of the celestial body, in radians.
190              
191             =item * B<sin_h0> — sine of the I<standard altitude>, i.e. the geometric altitude
192             of the center of the body at the time of apparent rising or setting.
193              
194              
195             =item * C<on_event> callback is called when the event time is determined.
196             The arguments are:
197              
198             =over
199              
200             =item * event type, one of C<$EVT_RISE> or C<$EVT_SET>
201              
202             =item * Univerrsal time of the event
203              
204             =back
205              
206             on_event => sub { my ($evt, $ut) = @_; ... }
207              
208             =item * C<on_noevent> is called when the event does not happen at the given date,
209             either because the body never rises, or is circumpolar. The argument is respectively
210             C<$STATE_NEVER_RISES> or C<$STATE_CIRCUMPOLAR>.
211              
212             on_noevent => sub { my $state = shift; ... }
213              
214             =back
215              
216             =head1 AUTHOR
217              
218             Sergey Krushinsky, C<< <krushi at cpan.org> >>
219              
220             =head1 COPYRIGHT AND LICENSE
221              
222             Copyright (C) 2010-2019 by Sergey Krushinsky
223              
224             This library is free software; you can redistribute it and/or modify
225             it under the same terms as Perl itself.
226              
227             =cut