File Coverage

lib/Astro/Montenbruck/RiseSet/RST.pm
Criterion Covered Total %
statement 105 111 94.5
branch 11 18 61.1
condition 4 11 36.3
subroutine 18 18 100.0
pod 1 1 100.0
total 139 159 87.4


line stmt bran cond sub pod time code
1             package Astro::Montenbruck::RiseSet::RST;
2              
3 3     3   21 use strict;
  3         5  
  3         87  
4 3     3   15 use warnings;
  3         5  
  3         81  
5 3     3   13 no warnings qw/experimental/;
  3         6  
  3         102  
6 3     3   26 use feature qw/switch/;
  3         7  
  3         289  
7              
8 3     3   21 use Exporter qw/import/;
  3         15  
  3         113  
9 3     3   18 use Readonly;
  3         6  
  3         142  
10              
11 3     3   26 use Math::Trig qw/:pi deg2rad rad2deg acos/;
  3         7  
  3         398  
12 3     3   36 use List::Util qw/any/;
  3         7  
  3         179  
13              
14 3     3   1351 use Astro::Montenbruck::Time::Sidereal qw/ramc/;
  3         7  
  3         161  
15 3     3   20 use Astro::Montenbruck::MathUtils qw/diff_angle reduce_deg reduce_rad/;
  3         6  
  3         136  
16 3     3   16 use Astro::Montenbruck::Time qw/cal2jd jd_cent $SEC_PER_DAY/;
  3         7  
  3         246  
17 3     3   1273 use Astro::Montenbruck::Time::DeltaT qw/delta_t/;
  3         10  
  3         211  
18 3     3   23 use Astro::Montenbruck::CoCo qw/equ2hor/;
  3         5  
  3         132  
19 3     3   17 use Astro::Montenbruck::RiseSet::Constants qw/:events :states/;
  3         7  
  3         3634  
20              
21             our @EXPORT_OK = qw/rst_function/;
22             our $VERSION = 0.01;
23              
24             # Interpolate from three equally spaced tabular angular values.
25             #
26             # [Meeus-1998; equation 3.3]
27             #
28             # This version is suitable for interpolating from a table of
29             # angular values which may cross the origin of the circle,
30             # for example: 359 degrees...0 degrees...1 degree.
31             #
32             # Arguments:
33             # - `n` : the interpolating factor, must be between -1 and 1
34             # - `y` : a sequence of three values
35             #
36             # Results:
37             # - the interpolated value of y
38              
39             sub _interpolate_angle3 {
40 44     44   95 my ( $n, $y ) = @_;
41 44 50 33     184 die "interpolating factor $n out of range" unless ( -1 < $n ) && ( $n < 1 );
42              
43 44         135 my $a = diff_angle( $y->[0], $y->[1], 'radians' );
44 44         129 my $b = diff_angle( $y->[1], $y->[2], 'radians' );
45 44         112 my $c = diff_angle( $a, $b, 'radians' );
46 44         122 $y->[1] + $n / 2 * ( $a + $b + $n * $c );
47             }
48              
49             # Interpolate from three equally spaced tabular values.
50             #
51             # [Meeus-1998; equation 3.3]
52             #
53             # Parameters:
54             # - `n` : the interpolating factor, must be between -1 and 1
55             # - `y` : a sequence of three values
56             #
57             # Results:
58             # - the interpolated value of y
59             sub _interpolate3 {
60 28     28   58 my ( $n, $y ) = @_;
61 28 50 33     109 die "interpolating factor out of range $n" unless ( -1 < $n ) && ( $n < 1 );
62              
63 28         65 my $a = $y->[1] - $y->[0];
64 28         52 my $b = $y->[2] - $y->[1];
65 28         49 my $c = $b - $a;
66 28         84 $y->[1] + $n / 2 * ( $a + $b + $n * $c );
67             }
68              
69             sub rst_function {
70 10     10 1 163 my %arg = @_;
71 10         32 my ( $h, $phi, $lambda ) = map { deg2rad( $arg{$_} ) } qw/h phi lambda/;
  30         245  
72              
73 10         113 my $sin_h = sin($h);
74 10   50     59 my $delta = $arg{delta} || 1 / 1440;
75 10         20 my $jdm = cal2jd( @{ $arg{date} } );
  10         48  
76 10         58 my $gstm = deg2rad( ramc( $jdm, 0 ) );
77 10         92 my @equ = map { [ $arg{get_position}->( $jdm + $_ ) ] } ( -1 .. 1 );
  30         562  
78 10         253 my @alpha = map { $_->[0] } @equ;
  30         67  
79 10         22 my @delta = map { $_->[1] } @equ;
  30         55  
80 10         42 my $cos_h =
81             ( $sin_h - sin($phi) * sin($delta[1]) ) / ( cos($phi) * cos($delta[1]) );
82 10         54 my $dt = delta_t($jdm) / $SEC_PER_DAY;
83              
84             sub {
85 30     30   635 my $evt = shift; # $EVT_RISE, $EVT_SET or $EVT_TRANSIT
86 30 50       241 die "Unknown event: $evt" unless any { $evt eq $_ } @RS_EVENTS;
  60         659  
87 30         176 my %arg = ( max_iter => 50, @_ );
88              
89 30 50       132 if ( $cos_h < -1 ) {
    50          
90 0         0 $arg{on_noevent}->($STATE_CIRCUMPOLAR);
91 0         0 return;
92             }
93             elsif ( $cos_h > 1 ) {
94 0         0 $arg{on_noevent}->($STATE_NEVER_RISES);
95 0         0 return;
96             }
97              
98 30         120 my $h0 = acos($cos_h);
99 30         328 my $m0 = ( reduce_rad( $alpha[1] + $lambda - $gstm ) ) / pi2;
100 30         56 my $m = do {
101 30         52 given ($evt) {
102 30         90 $m0 when $EVT_TRANSIT;
103 20         139 $m0 - $h0 / pi2 when $EVT_RISE;
104 10         61 $m0 + $h0 / pi2 when $EVT_SET;
105             }
106             };
107 30 50       256 if ( $m < 0 ) {
    100          
108 0         0 $m++;
109             }
110             elsif ( $m > 1 ) {
111 3         9 $m--;
112             }
113 30 50 33     140 die "m is out of range: $m" unless ( 0 <= $m ) && ( $m <= 1 );
114              
115 30         94 for ( 0 .. $arg{max_iter} ) {
116 44         80 my $m0 = $m;
117 44         104 my $theta0 =
118             deg2rad( reduce_deg( rad2deg($gstm) + 360.985647 * $m ) );
119 44         318 my $n = $m + $dt;
120 44         123 my $ra = _interpolate_angle3( $n, \@alpha );
121 44         124 my $h1 = diff_angle( 0, $theta0 - $lambda - $ra, 'radians' );
122 44         79 my $dm = do {
123 44         70 given ($evt) {
124 44         116 -( $h1 / pi2 ) when $EVT_TRANSIT;
125 28         155 default {
126 28         71 my $de = _interpolate3( $n, \@delta );
127 56         391 my ( $az, $alt ) = map { deg2rad($_) }
128 28         73 equ2hor( map { rad2deg($_) } ( $h1, $de, $phi ) );
  84         536  
129 28         278 ( $alt - $h ) /
130             ( pi2 * cos($de) * cos($phi) * sin($h1) );
131             }
132             }
133             };
134 44         147 $m += $dm;
135 44 100       132 if ( abs( $m - $m0 ) < $delta ) {
136 30         124 $arg{on_event}->( $jdm + $m );
137 30         19484 return;
138             }
139             }
140 0           die 'bailout!';
141             }
142 10         135 }
143              
144             1;
145             __END__
146              
147             =pod
148              
149             =encoding UTF-8
150              
151             =head1 NAME
152              
153             Astro::Montenbruck::RiseSet::RST — rise, set, transit.
154              
155             =head1 SYNOPSIS
156              
157             use Astro::Montenbruck::MathUtils qw/frac/;
158             use Astro::Montenbruck::RiseSet::Constants qw/:events :altitudes/;
159             use Astro::Montenbruck::RiseSet::RST qw/rst_function/;
160              
161             # create function for calculating Moon events for Munich, Germany, on March 23, 1989.
162             my $func = rst_function(
163             date => [1989, 3, 23],
164             phi => 48.1,
165             lambda => -11.6,
166             get_position => sub {
167             my $jd = shift;
168             # return equatorial coordinates of the celestial body for the Julian Day.
169             }
170             );
171              
172             # calculate rise. Alternatively, use $EVT_SET for set, $EVT_TRANSIT for
173             # transit as the first argument
174             $func->(
175             $EVT_RISE,
176             on_event => sub {
177             my $jd = shift; # Standard Julian date of the event
178             my $ut = frac(jd - 0.5) * 24; # UTC, 18.95 = 18h57m
179             },
180             on_noevent => sub {
181             my $state = shift;
182             say "The body is $state"
183             }
184             });
185              
186              
187             =head1 VERSION
188              
189             Version 0.01
190              
191             =head1 DESCRIPTION
192              
193             Low-level routines for calculating rise, set and transit times of celestial
194             bodies. The calculations are based on I<"Astronomical Algorythms" by Jean Meeus>.
195             The same subject is discussed in I<Montenbruck & Phleger>'s book, but Meeus's
196             method is more general and consistent and generic. Unit tests use examples from
197             the both sources.
198              
199             The general problem here is to find the instant of time at which a celestial
200             body reaches a predetermined I<altitude>.
201              
202              
203             =head1 FUNCTIONS
204              
205             =head2 rst_function( %args )
206              
207             Returns function for calculating time of event. See L</EVENT FUNCTION> below.
208              
209             =head3 Named Arguments
210              
211             =over
212              
213             =item * B<date> — array of B<year> (astronomical, zero-based), B<month> [1..12],
214             and B<day>, [1..31].
215              
216             =item * B<phi> — geographical latitude, degrees, positive northward
217              
218             =item * B<lambda> — geographical longitude, degrees, positive westward
219              
220             =item * B<get_position> — function, which given I<Standard Julian Day>, returns
221             equatorial coordinates of the celestial body, in radians.
222              
223              
224             =item * B<h> — the I<standard altitude>, i.e. the geometric altitude of the
225             center of the body at the time of apparent rising or setting, degrees.
226              
227             =back
228              
229             =head2 EVENT FUNCTION
230              
231             The event function, returned by L</rst_function( %args )>, calculates time of a
232             given event (rise, set or trasnsit).
233              
234             $func->( EVENT_TYPE, on_event => sub{ ... }, on_noevent => sub{ ... } );
235              
236             Its first argument, I<event type>, is one of C<$EVT_RISE>, C<$EVT_SET>, or
237             C<$EVT_TRANSIT>, see L<Astro::Montenbruck::RiseSet::Constants/EVENTS>.
238              
239             Named arguments are callback functions:
240              
241             =over
242              
243             =item * C<on_event> is called when the event time is determined. The argument is
244             I<Standard Julian day> of the event.
245              
246             on_event => sub { my $jd = shift; ... }
247              
248             =item * C<on_noevent> is called when the event does not happen at the given date,
249             either because the body never rises, or is circumpolar. The argument is respectively
250             C<$STATE_NEVER_RISES> or C<$STATE_CIRCUMPOLAR>, see
251             L<Astro::Montenbruck::RiseSet::Constants/STATES>.
252              
253             on_noevent => sub { my $state = shift; ... }
254              
255             =back
256              
257             =head1 AUTHOR
258              
259             Sergey Krushinsky, C<< <krushi at cpan.org> >>
260              
261             =head1 COPYRIGHT AND LICENSE
262              
263             Copyright (C) 2010-2019 by Sergey Krushinsky
264              
265             This library is free software; you can redistribute it and/or modify
266             it under the same terms as Perl itself.
267              
268             =cut