File Coverage

blib/lib/Astro/Coord/ECI/Moon.pm
Criterion Covered Total %
statement 110 112 98.2
branch 20 26 76.9
condition 10 20 50.0
subroutine 20 21 95.2
pod 6 6 100.0
total 166 185 89.7


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