File Coverage

blib/lib/Astro/Coord/ECI/Moon.pm
Criterion Covered Total %
statement 113 115 98.2
branch 20 26 76.9
condition 10 20 50.0
subroutine 21 22 95.4
pod 6 6 100.0
total 170 189 89.9


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Astro::Coord::ECI::Moon - Compute the position of the Moon.
4              
5             =head1 SYNOPSIS
6              
7             use Astro::Coord::ECI;
8             use Astro::Coord::ECI::Moon;
9             use Astro::Coord::ECI::Utils qw{deg2rad};
10            
11             # 1600 Pennsylvania Ave, Washington DC USA
12             # latitude 38.899 N, longitude 77.038 W,
13             # altitude 16.68 meters above sea level
14             my $lat = deg2rad (38.899); # Radians
15             my $long = deg2rad (-77.038); # Radians
16             my $alt = 16.68 / 1000; # Kilometers
17             my $moon = Astro::Coord::ECI::Moon->new ();
18             my $sta = Astro::Coord::ECI->
19             universal (time ())->
20             geodetic ($lat, $long, $alt);
21             my ($time, $rise) = $sta->next_elevation ($moon);
22             print "Moon @{[$rise ? 'rise' : 'set']} is ",
23             scalar localtime $time, "\n";
24              
25             =head1 DESCRIPTION
26              
27             This module implements the position of the Moon as a function of time,
28             as described in Jean Meeus' "Astronomical Algorithms," second edition.
29             It is a subclass of L, with the id,
30             name, and diameter attributes initialized appropriately, and the
31             time_set() method overridden to compute the position of the Moon at the
32             given time.
33              
34             =head2 Methods
35              
36             The following methods should be considered public:
37              
38             =over
39              
40             =cut
41              
42             package Astro::Coord::ECI::Moon;
43              
44 16     16   1731 use strict;
  16         33  
  16         491  
45 16     16   91 use warnings;
  16         30  
  16         733  
46              
47             our $VERSION = '0.129_01';
48              
49 16     16   94 use base qw{Astro::Coord::ECI};
  16         32  
  16         1547  
50              
51 16     16   108 use Astro::Coord::ECI::Utils qw{ @CARP_NOT :mainstream };
  16         32  
  16         6328  
52 16     16   122 use Carp;
  16         35  
  16         941  
53 16     16   102 use POSIX qw{floor strftime};
  16         42  
  16         104  
54              
55             # Load the periodic terms from the table.
56              
57             my %terms;
58             { # Begin local symbol block.
59             my $where;
60             local $_ = undef; # while (<>) ... does not localize $_.
61             while () {
62             chomp;
63             s/ \A \s+ //smx;
64             s/ \s+ \z //smx;
65             next unless $_;
66             next if m/ \A \s* [#] /smx;
67             s/^-// and do {
68             last if $_ eq 'end';
69             $where = $terms{$_} ||= [];
70             next;
71             };
72             s/_//g;
73             push @$where, [split '\s+', $_];
74             }
75             } # End local symbol block.
76              
77             my %static = (
78             id => 'Moon',
79             name => 'Moon',
80             diameter => 3476,
81             );
82              
83             my $weaken = eval {
84             require Scalar::Util;
85             Scalar::Util->can('weaken');
86             };
87              
88             our $Singleton = $weaken;
89              
90             my %object; # By class
91              
92             =item $moon = Astro::Coord::ECI::Moon->new ();
93              
94             This method instantiates an object to represent the coordinates of the
95             Moon. This is a subclass of Astro::Coord::ECI, with the id and name
96             attributes set to 'Moon', and the diameter attribute set to 3476 km
97             per Jean Meeus' "Astronomical Algorithms", 2nd Edition, Appendix I,
98             page 407.
99              
100             Any arguments are passed to the set() method once the object has been
101             instantiated. Yes, you can override the "hard-wired" id and name in
102             this way.
103              
104             If C<$Astro::Coord::ECI::Moon::Singleton> is true, you get a singleton
105             object; that is, only one object is instantiated and subsequent calls to
106             C just return that object. If higher-accuracy subclasses are ever
107             implemented, there will be one singleton for each class.
108              
109             The singleton logic only works if L exports
110             C. If it does not, the setting of
111             C<$Astro::Coord::ECI::Moon::Singleton> is silently ignored. The default
112             is true if L can be loaded and exports
113             C, and false otherwise.
114              
115             =cut
116              
117             sub new {
118 13     13 1 1521 my ($class, @args) = @_;
119 13 50       42 ref $class and $class = ref $class;
120 13 100 66     88 if ( $Singleton && $weaken && __classisa( $class, __PACKAGE__ ) ) {
      66        
121 11         23 my $self;
122 11 100       45 if ( $self = $object{$class} ) {
123 1 50       3 $self->set( @args ) if @args;
124 1         6 return $self;
125             } else {
126 10         61 $self = $object{$class} = $class->SUPER::new (%static, @args);
127 10         39 $weaken->( $object{$class} );
128 10         43 return $self;
129             }
130             } else {
131 2         11 return $class->SUPER::new (%static, @args);
132             }
133             }
134              
135             =item @almanac = $moon->almanac ($station, $start, $end);
136              
137             This method produces almanac data for the Moon for the given observing
138             station, between the given start and end times. The station is assumed
139             to be Earth-Fixed - that is, you can't do this for something in orbit.
140              
141             The C<$station> argument may be omitted if the C attribute has
142             been set. That is, this method can also be called as
143              
144             @almanac = $moon->almanac( $start, $end )
145              
146             The start time defaults to the current time setting of the $moon
147             object, and the end time defaults to a day after the start time.
148              
149             The almanac data consists of a list of list references. Each list
150             reference points to a list containing the following elements:
151              
152             [0] => time
153             [1] => event (string)
154             [2] => detail (integer)
155             [3] => description (string)
156              
157             The @almanac list is returned sorted by time.
158              
159             The following events, details, and descriptions are at least
160             potentially returned:
161              
162             horizon: 0 = Moon set, 1 = Moon rise;
163             transit: 1 = Moon transits meridian;
164             quarter: 0 = new moon, 1 = first quarter,
165             2 = full moon, 3 = last quarter.
166              
167             =cut
168              
169             sub __almanac_event_type_iterator {
170 3     3   10 my ( $self, $station ) = @_;
171              
172 3         6 my $inx = 0;
173              
174 3         13 my $horizon = $station->__get_almanac_horizon();
175              
176 3         32 my @events = (
177             [ $station, next_elevation => [ $self, $horizon, 1 ],
178             horizon => '__horizon_name' ],
179             [ $station, next_meridian => [ $self ],
180             transit => '__transit_name' ],
181             [ $self, next_quarter => [], quarter => '__quarter_name' ],
182             );
183              
184             return sub {
185             $inx < @events
186 12 100   12   42 and return @{ $events[$inx++] };
  9         66  
187 3         17 return;
188 3         20 };
189             }
190              
191 16     16   10555 use Astro::Coord::ECI::Mixin qw{ almanac };
  16         42  
  16         974  
192              
193             =item @almanac = $moon->almanac_hash($station, $start, $end);
194              
195             This convenience method wraps $moon->almanac(), but returns a list of
196             hash references, sort of like Astro::Coord::ECI::TLE->pass()
197             does. The hashes contain the following keys:
198              
199             {almanac} => {
200             {event} => the event type;
201             {detail} => the event detail (typically 0 or 1);
202             {description} => the event description;
203             }
204             {body} => the original object ($moon);
205             {station} => the observing station;
206             {time} => the time the quarter occurred.
207              
208             The {time}, {event}, {detail}, and {description} keys correspond to
209             elements 0 through 3 of the list returned by almanac().
210              
211             =cut
212              
213 16     16   114 use Astro::Coord::ECI::Mixin qw{ almanac_hash };
  16         51  
  16         2252  
214              
215             =item $coord2 = $coord->clone ();
216              
217             If singleton objects are enabled, this override of the superclass'
218             method simply returns the invocant. Otherwise it does a deep clone of an
219             object, producing a different but identical object.
220              
221             Prior to version 0.099_01 it always returned a clone. Yes,
222             this is a change in long-standing functionality, but a long-standing bug
223             is still a bug.
224              
225             =cut
226              
227             sub clone {
228 2     2 1 471 my ( $self ) = @_;
229 2 100 66     13 $Singleton
230             and $weaken
231             and return $self;
232 1         10 return $self->SUPER::clone();
233             }
234              
235             =item $elevation = $moon->correct_for_refraction( $elevation )
236              
237             This override of the superclass' method simply returns the elevation
238             passed to it. Since the Moon has no atmosphere to speak of, there should
239             be no diffraction to speak of either.
240              
241             See the L C and
242             C documentation for whether this class'
243             C method is actually called by those methods.
244              
245             =cut
246              
247             sub correct_for_refraction {
248 0     0 1 0 my ( undef, $elevation ) = @_; # Invocant unused
249 0         0 return $elevation;
250             }
251              
252             =item ($time, $quarter, $desc) = $moon->next_quarter ($want);
253              
254             This method calculates the time of the next quarter-phase of the Moon
255             after the current time setting of the $moon object. The returns are the
256             time, which quarter-phase it is as a number from 0 (new moon) to
257             3 (last quarter), and a string describing the phase. If called in
258             scalar context, you just get the time.
259              
260             The optional $want argument says which phase you want.
261              
262             As a side effect, the time of the $moon object ends up set to the
263             returned time.
264              
265             The method of calculation is successive approximation, and actually
266             returns the second B the quarter.
267              
268             =cut
269              
270 16     16   125 use constant NEXT_QUARTER_INCREMENT => 86400 * 6; # 6 days.
  16         40  
  16         1365  
271              
272             *__next_quarter_coordinate = __PACKAGE__->can(
273             'phase' );
274              
275 16     16   112 use Astro::Coord::ECI::Mixin qw{ next_quarter };
  16         36  
  16         1374  
276              
277             =item $hash_reference = $moon->next_quarter_hash($want);
278              
279             This convenience method wraps $moon->next_quarter(), but returns the
280             data in a hash reference, sort of like Astro::Coord::ECI::TLE->pass()
281             does. The hash contains the following keys:
282              
283             {body} => the original object ($moon);
284             {almanac} => {
285             {event} => 'quarter',
286             {detail} => the quarter number (0 through 3);
287             {description} => the quarter description;
288             }
289             {time} => the time the quarter occurred.
290              
291             The {time}, {detail}, and {description} keys correspond to elements 0
292             through 2 of the list returned by next_quarter().
293              
294             =cut
295              
296 16     16   135 use Astro::Coord::ECI::Mixin qw{ next_quarter_hash };
  16         29  
  16         15201  
297              
298             =item $period = $moon->period ()
299              
300             This method returns the sidereal period of the Moon, per Appendix I
301             (pg 408) of Jean Meeus' "Astronomical Algorithms," 2nd edition.
302              
303             =cut
304              
305 18     18 1 76 sub period {return 2360591.5968} # 27.321662 * 86400
306              
307             sub __horizon_name_tplt {
308 6     6   12 my ( $self ) = @_;
309 6 50       32 return $self->__object_is_self_named() ?
310             [ '%s set', '%s rise' ] :
311             $self->SUPER::__horizon_name_tplt();
312             }
313              
314             sub __quarter_name {
315 4     4   16 my ( $self, $event, $tplt ) = @_;
316 4   50     32 $tplt ||= [ 'New %s', 'First quarter %s', 'Full %s', 'Last quarter %s' ];
317 4         25 return $self->__event_name( $event, $tplt );
318             }
319              
320             =item ($phase, $illum) = $moon->phase ($time);
321              
322             This method calculates the current phase of the moon and its illuminated
323             fraction. If the time is omitted, the current time of the $moon object
324             is used.
325              
326             The phase is returned as a number from C<0> to C<2 * PI> radians, with
327             C<0> being New Moon, C being First Quarter, and so on. The
328             illuminated fraction is a number from C<0> (New Moon) to C<1> (Full
329             Moon).
330              
331             If called in scalar context, you get the phase.
332              
333             This can be called as a class method, but if you do this the time
334             must be specified.
335              
336             Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 49 page
337             349, defines the phases of the moon in terms of the difference between
338             the geocentric longitudes of the Moon and Sun - specifically, that
339             new, first quarter, full, and last quarter are the moments when this
340             difference is 0, 90, 180, and 270 degrees respectively.
341              
342             Not quite above reproach, this module simply defines the phase of the
343             Moon as the difference between these two quantities, even if it is not
344             a multiple of 90 degrees. This is different than the "phase angle" of
345             the Moon, which Meeus defines as the elongation of the Earth from the
346             Sun, as seen from the Moon. Because we take the "phase angle" as just
347             pi - the phase (in radians), we introduce an error of about 0.3% in
348             the illumination calculation.
349              
350             =cut
351              
352             sub phase {
353 122     122 1 256 my ($self, $time) = @_;
354              
355 122 50 33     310 (ref $self || $time) or croak <
356             Error - You must specify a time if you call phase() as a class method.
357             eod
358              
359 122 50       225 $self = $self->new () unless ref $self;
360              
361 122 50       254 $self->universal ($time) if $time;
362              
363 122         278 my $sun = $self->get( 'sun' )->universal( $self->universal() );
364              
365 122         282 my (undef, $longs) = $sun->ecliptic ();
366 122         359 my (undef, $longm) = $self->ecliptic ();
367              
368 122         312 my $phase = mod2pi ($longm - $longs);
369 122 100       516 return wantarray ? ($phase, (1 + cos ($self->PI - $phase)) / 2) : $phase;
370             }
371              
372             =item $moon->time_set ()
373              
374             This method sets coordinates of the object to the coordinates of the
375             Moon at the object's currently-set universal time. The velocity
376             components are arbitrarily set to 0, since Meeus' algorithm does not
377             provide this information. The 'equinox_dynamical' attribute is set to
378             the currently-set dynamical time.
379              
380             Although there's no reason this method can't be called directly, it
381             exists to take advantage of the hook in the B
382             object, to allow the position of the Moon to be computed when the
383             object's time is set.
384              
385             The computation comes from Jean Meeus' "Astronomical Algorithms", 2nd
386             Edition, Chapter 47, pages 337ff. Meeus gives the accuracy as 10
387             seconds of arc in latitude, and 4 seconds of arc in longitude. He
388             credits the algorithm to M. Chalpront-Touze and J. Chalpront, "The
389             Lunar Ephemeris ELP 2000" from I volume
390             124, pp 50-62 (1983), but the formulae for the mean arguments to
391             J. Chalpront, M. Chalpront-Touze, and G. Francou, I
392             ELP 2000-82B de nouvelles valeurs des parametres orbitaux de la Lune
393             et du barycentre Terre-Lune>, Paris, January 1998.
394              
395             =cut
396              
397             sub time_set {
398 675     675 1 1063 my $self = shift;
399              
400 675         1705 my $time = $self->dynamical;
401              
402 675         1616 my $T = jcent2000 ($time); # Meeus (22.1)
403              
404             # Moon's mean longitude.
405              
406 675         2032 my $Lprime = mod2pi (deg2rad ( # Meeus (47.1)
407             (((- ($T / 65_194_000) +
408             1 / 538_841) * $T - 0.0015786) * $T +
409             481267.88123421) * $T + 218.3164477));
410              
411             # Moon's mean elongation.
412              
413 675         1823 my $D = mod2pi (deg2rad (((($T / 113_065_000 + # Meeus (47.2)
414             1 / 545_868) * $T - 0.0018819) * $T +
415             445267.1114034) * $T + 297.8501921));
416              
417             # Sun's mean anomaly.
418              
419 675         1561 my $M = mod2pi (deg2rad ((($T / 24_490_000 - # Meeus (47.3)
420             0.000_1536) * $T + 35999.050_2909) * $T +
421             357.5291092));
422              
423             # Moon's mean anomaly.
424              
425 675         1743 my $Mprime = mod2pi (deg2rad ((((- $T / 14_712_000 + # Meeus (47.4)
426             1 / 69_699) * $T + 0.008_7414) * $T +
427             477198.867_5055) * $T + 134.963_3964));
428              
429             # Moon's argument of latitude (mean distance
430             # from ascending node).
431              
432 675         1608 my $F = mod2pi (deg2rad (((($T / 863_310_000 - # Meeus (47.5)
433             1 / 3_526_000) * $T - 0.003_6539) * $T +
434             483202.017_5233) * $T + 93.272_0950));
435              
436             # Eccentricity correction factor.
437              
438 675         1180 my $E = (- 0.000_0074 * $T - 0.002_516) * $T + 1; # Meeus (47.6)
439 675         1409 my @efac = (1, $E, $E * $E);
440              
441             # Compute "further arguments".
442              
443 675         1420 my $A1 = mod2pi (deg2rad (131.849 * $T + 119.75)); # Venus
444 675         1421 my $A2 = mod2pi (deg2rad (479264.290 * $T + 53.09)); # Jupiter
445 675         1479 my $A3 = mod2pi (deg2rad (481266.484 * $T + 313.45)); # undocumented
446              
447             # Compute periodic terms for longitude (sigma l) and
448             # distance (sigma r).
449              
450 675         1288 my ($sigmal, $sigmar) = (0, 0);
451 675         1057 foreach (@{$terms{lr}}) {
  675         1634  
452 40500         76272 my ($mulD, $mulM, $mulMprime, $mulF, $sincof, $coscof) = @$_;
453 40500 100       68360 if ($mulM) {
454 18900   33     34497 my $corr = $efac[abs $mulM] || confess <
455             Programming error - M multiple greater than 2.
456             eod
457 18900         29764 $sincof *= $corr;
458 18900         28160 $coscof *= $corr;
459             }
460 40500         72681 my $arg = $D * $mulD + $M * $mulM + $Mprime * $mulMprime +
461             $F * $mulF;
462 40500         67933 $sigmal += $sincof * sin ($arg);
463 40500         73410 $sigmar += $coscof * cos ($arg);
464             }
465              
466             # Compute periodic terms for latitude (sigma b).
467              
468 675         1286 my $sigmab = 0;
469 675         996 foreach (@{$terms{b}}) {
  675         1567  
470 40500         73234 my ($mulD, $mulM, $mulMprime, $mulF, $sincof) = @$_;
471 40500 100       67953 if ($mulM) {
472 17550   33     32299 my $corr = $efac[abs $mulM] || confess <
473             Programming error - M multiple greater than 2.
474             eod
475 17550         29155 $sincof *= $corr;
476             }
477 40500         73187 my $arg = $D * $mulD + $M * $mulM + $Mprime * $mulMprime +
478             $F * $mulF;
479 40500         75666 $sigmab += $sincof * sin ($arg);
480             }
481              
482             # Add other terms
483              
484 675         1753 $sigmal += 3958 * sin ($A1) + 1962 * sin ($Lprime - $F) +
485             318 * sin ($A2);
486 675         2091 $sigmab += - 2235 * sin ($Lprime) + 382 * sin ($A3) +
487             175 * sin ($A1 - $F) + 175 * sin ($A1 + $F) +
488             127 * sin ($Lprime - $Mprime) - 115 * sin ($Lprime + $Mprime);
489              
490             # Coordinates of Moon (finally!)
491              
492 675         1919 my $lambda = deg2rad ($sigmal / 1_000_000) + $Lprime;
493 675         1506 my $beta = deg2rad ($sigmab / 1_000_000);
494 675         1151 my $delta = $sigmar / 1000 + 385_000.56;
495              
496             # Correct longitude for nutation (from Chapter 22, pg 144).
497              
498 675         2005 $lambda += ( $self->nutation( $time ) )[0];
499              
500 675         1449 $self->ecliptic ($beta, mod2pi( $lambda ), $delta);
501             ## $self->set (equinox_dynamical => $time);
502 675         2035 $self->equinox_dynamical ($time);
503 675         1603 return $self;
504             }
505              
506             # The moon is normally positioned in inertial coordinates.
507              
508 12     12   45 sub __initial_inertial { return 1 }
509              
510             1;
511              
512             =back
513              
514             =head2 Historical Calculations
515              
516             This class was written for the purpose of calculating whether the Moon
517             was visible from given point on the Earth (or in space) at a given time
518             in or reasonably close to the present. I can not say how accurate it is
519             at times far from the present.
520              
521             See
522             L
523             in the L documentation
524             for a discussion of input and output time conversion.
525              
526             =head1 ACKNOWLEDGMENTS
527              
528             The author wishes to acknowledge Jean Meeus, whose book "Astronomical
529             Algorithms" (second edition) formed the basis for this module.
530              
531             =head1 SEE ALSO
532              
533             The L
534             documentation for a discussion of how the pieces/parts of this
535             distribution go together and how to use them.
536              
537             L by Brett Hamilton, which contains a
538             function-based module to compute the current phase, distance and angular
539             diameter of the Moon, as well as the angular diameter and distance of
540             the Sun.
541              
542             =head1 SUPPORT
543              
544             Support is by the author. Please file bug reports at
545             L,
546             L, or in
547             electronic mail to the author.
548              
549             =head1 AUTHOR
550              
551             Thomas R. Wyant, III (F)
552              
553             =head1 COPYRIGHT AND LICENSE
554              
555             Copyright (C) 2005-2023 by Thomas R. Wyant, III
556              
557             This program is free software; you can redistribute it and/or modify it
558             under the same terms as Perl 5.10.0. For more details, see the full text
559             of the licenses in the directory LICENSES.
560              
561             This program is distributed in the hope that it will be useful, but
562             without any warranty; without even the implied warranty of
563             merchantability or fitness for a particular purpose.
564              
565             =cut
566              
567             __DATA__