File Coverage

blib/lib/Astro/App/Satpass2.pm
Criterion Covered Total %
statement 1667 2468 67.5
branch 625 1338 46.7
condition 168 423 39.7
subroutine 276 350 78.8
pod 59 63 93.6
total 2795 4642 60.2


line stmt bran cond sub pod time code
1             package Astro::App::Satpass2;
2              
3 20     20   215872 use 5.008;
  20         79  
4              
5 20     20   113 use strict;
  20         53  
  20         558  
6 20     20   112 use warnings;
  20         37  
  20         1364  
7              
8 20     20   7061 use Astro::App::Satpass2::Locale qw{ __localize };
  20         77  
  20         17941  
9 20     20   11551 use Astro::App::Satpass2::Macro::Command;
  20         66  
  20         798  
10 20     20   10693 use Astro::App::Satpass2::Macro::Code;
  20         65  
  20         1060  
11 20     20   7813 use Astro::App::Satpass2::ParseTime;
  20         76  
  20         1038  
12 20         4729 use Astro::App::Satpass2::Utils qw{
13             :os
14             :ref
15             __arguments __legal_options
16             expand_tilde find_package_pod
17             has_method instance load_package
18             my_dist_config quoter
19             __parse_class_and_args
20 20     20   154 };
  20         41  
21              
22 20     20   18762 use Astro::Coord::ECI 0.077; # This needs at least 0.049.
  20         348064  
  20         1086  
23 20     20   12993 use Astro::Coord::ECI::Moon 0.077;
  20         143780  
  20         986  
24 20     20   11467 use Astro::Coord::ECI::Star 0.077;
  20         40409  
  20         887  
25 20     20   11824 use Astro::Coord::ECI::Sun 0.077;
  20         100724  
  20         1049  
26 20     20   29906 use Astro::Coord::ECI::TLE 0.077 qw{:constants}; # This needs at least 0.059.
  20         1619772  
  20         6695  
27 20     20   14676 use Astro::Coord::ECI::TLE::Set 0.077;
  20         70304  
  20         1018  
28             # The following includes @CARP_NOT.
29 20     20   203 use Astro::Coord::ECI::Utils 0.112 qw{ :all }; # This needs at least 0.112.
  20         340  
  20         11622  
30              
31             {
32             local $@ = undef;
33 20   50     55 use constant HAVE_TLE_IRIDIUM => eval {
34             require Astro::Coord::ECI::TLE::Iridium;
35             Astro::Coord::ECI::TLE::Iridium->VERSION( 0.077 );
36             1;
37 20     20   210 } || 0;
  20         46  
38             # Unfortunately the alias code creates the alias even if the version
39             # is unacceptable; so we may have to just delete the Iridium aliases
40             unless( HAVE_TLE_IRIDIUM ) {
41             my %type_map = Astro::Coord::ECI::TLE->alias();
42             foreach my $name ( keys %type_map ) {
43             $type_map{$name} eq 'Astro::Coord::ECI::TLE::Iridium'
44             and Astro::Coord::ECI::TLE->alias( $name, undef );
45             }
46             }
47             }
48              
49 20     20   13293 use Attribute::Handlers;
  20         149637  
  20         179  
50 20     20   996 use Clone ();
  20         47  
  20         377  
51 20     20   105 use Cwd ();
  20         43  
  20         649  
52 20     20   135 use File::Glob qw{ :glob };
  20         41  
  20         5309  
53 20     20   165 use File::HomeDir;
  20         45  
  20         1445  
54 20     20   136 use File::Spec;
  20         47  
  20         671  
55 20     20   20406 use File::Temp;
  20         301358  
  20         2262  
56 20     20   183 use Getopt::Long 2.33;
  20         214  
  20         714  
57 20     20   3323 use IO::File 1.14;
  20         480  
  20         4083  
58 20     20   146 use IO::Handle;
  20         42  
  20         939  
59 20     20   117 use POSIX qw{ floor };
  20         41  
  20         185  
60 20     20   1962 use Scalar::Util 1.26 qw{ blessed isdual openhandle };
  20         475  
  20         1196  
61 20     20   15834 use Text::Abbrev;
  20         1120  
  20         1363  
62 20     20   178 use Text::ParseWords (); # Used only for {level1} stuff.
  20         72  
  20         585  
63              
64 20     20   106 use constant ASTRO_SPACETRACK_VERSION => 0.105;
  20         47  
  20         1664  
65 20     20   118 use constant DEFAULT_STDOUT_LAYERS => ':encoding(utf-8)';
  20         40  
  20         2207  
66              
67             BEGIN {
68             eval {
69 20 50       143 load_package( 'Time::y2038' )
70             and Time::y2038->import();
71 20         1721 1;
72             }
73 20 50   20   79 or do {
74 0         0 require Time::Local;
75 0         0 Time::Local->import();
76             };
77             }
78              
79             # The following is returned by method _attribute_value() when a
80             # non-existent attribute is specified. We can't use undef for this,
81             # because the attribute might really be undef.
82             # NOTE that this used to be just bless \( $x = undef ) ..., but blead
83             # Perl 6a011f13d7690dbe2e03ad7500756c983bcb1834 did not like this
84             # (modificatoin of read-only variable).
85 20         54 use constant NULL => do {
86 20         47 my $x = undef;
87 20         1813 bless \$x, 'Null';
88 20     20   135 };
  20         56  
89             # The canonical way to see if $rslt actually contains the above is
90             # NULL_REF eq ref $rslt
91 20     20   147 use constant NULL_REF => ref NULL;
  20         49  
  20         1307  
92              
93 20     20   121 use constant SUN_CLASS_DEFAULT => 'Astro::Coord::ECI::Sun';
  20         53  
  20         9720  
94              
95             our $VERSION = '0.057';
96              
97             # The following 'cute' code is so that we do not determine whether we
98             # actually have optional modules until we really need them, and yet do
99             # not repeat the process once it is done.
100              
101             my $have_time_hires;
102             $have_time_hires = sub {
103             my $value = load_package( 'Time::HiRes' );
104             $have_time_hires = sub { return $value };
105             return $value;
106             };
107              
108             my $have_astro_spacetrack;
109             $have_astro_spacetrack = sub {
110             my $value = load_package( { lib => undef }, 'Astro::SpaceTrack' ) && eval {
111             Astro::SpaceTrack->VERSION( ASTRO_SPACETRACK_VERSION );
112             1;
113             };
114             $have_astro_spacetrack = sub { $value };
115             return $value;
116             };
117              
118             my $default_geocoder;
119             $default_geocoder = sub {
120             my $value =
121             _can_use_geocoder( 'Astro::App::Satpass2::Geocode::OSM'
122             );
123             $default_geocoder = sub { return $value };
124             return $value;
125             };
126              
127             sub _can_use_geocoder {
128 0     0   0 my ( $geocoder ) = @_;
129 0 0       0 my $pkg = load_package( $geocoder )
130             or return;
131 0 0       0 load_package( $pkg->GEOCODER_CLASS() )
132             or return;
133 0         0 return $pkg;
134             }
135              
136             my $interrupted = 'Interrupted by user.';
137              
138             my %twilight_def = (
139             civil => deg2rad (-6),
140             nautical => deg2rad (-12),
141             astronomical => deg2rad (-18),
142             );
143             my %twilight_abbr = abbrev (keys %twilight_def);
144              
145             # Individual commands are defined by subroutines of the same name,
146             # and having the Verb attribute. You can specify additional
147             # attributes if you need to. Following are descriptions of the
148             # attributes used by this script.
149             #
150             # Configure(configurations)
151             #
152             # The 'Configure' attribute specifies options to be passed to
153             # Getopt::Long::Configure before the options are parsed. For
154             # example, if a command wants to keep unrecognized options on the
155             # command you would specify:
156             # sub foo : Configure(pass_through) Verb
157             #
158             # Tokenize(options)
159             #
160             # The 'Tokenize' attribute specifies tokenizatino options. These
161             # can not take effect until fairly late in the parse when the
162             # tokens are known. These options are parsed by Getopt::Long, and
163             # the value of the attribute is a reference to the options hash
164             # thus generated. Possible options are:
165             # -expand_tilde - Expand tildes in the tokens. For historical
166             # reasons this is the default, but it can be negated by
167             # specifying -noexpand_tilde. Tildes in redirect
168             # specifications are always expanded.
169             #
170             # Tweak(options)
171             #
172             # The 'Tweak' attribute specifies miscellaneous tweaks to
173             # subroutine usage. Possible options are:
174             # -unsatisfied - Execute even inside an unsatisfied if().
175             # Subroutines with this attribute may have to be aware
176             # that they are being called within the scope of an
177             # unsatisfied if(). All interactive methods that must be
178             # called even inside an unsatisfied if() MUST have this
179             # attribute. These are begin() and end(), and anything
180             # that might dispatch either of these. At the moment this
181             # means if() and time().
182             # -completion - Requires as argument the name of the command
183             # completion method. This can not be checked at compile
184             # time. It will be called with the following arguments:
185             # $code - the relevant code reference
186             # $text - the text being completed
187             # $line - the line being completed
188             # $start - the current position in the line.
189             # It should return either a reference to an array
190             # containing possible completions, or nothing to fall
191             # through to standard completion
192             #
193             # Verb(options)
194             #
195             # The 'Verb' attribute identifies the subroutine as representing a
196             # cvsx command. If it has options, they should be specified inside
197             # parentheses as a whitespace-separated list of option
198             # specifications appropriate for Getopt::Long. For example:
199             # sub foo : Verb(bar baz=s)
200             # specifies that 'foo' is a command, taking options -bar, and
201             # -baz; the latter takes a string value.
202              
203             {
204             my %attr;
205              
206             sub Configure : ATTR(CODE,RAWDATA) {
207 0     0 0 0 my ( undef, undef, $code, $name, $data ) = @_;
208 0         0 $attr{$code}{$name} = _attr_list( $data );
209 0         0 return;
210 20     20   153 }
  20         44  
  20         210  
211              
212             sub Tokenize : ATTR(CODE,RAWDATA) {
213 20     20 0 34019 my ( undef, undef, $code, $name, $data ) = @_;
214 20         93 my $opt = _attr_hash( $name, $data, qw{ expand_tilde|expand-tilde! } );
215             exists $opt->{expand_tilde}
216 20 50       121 or $opt->{expand_tilde} = 1;
217 20         98 $attr{$code}{$name} = $opt;
218 20         78 return;
219 20     20   25728 }
  20         59  
  20         145  
220              
221             sub Tweak : ATTR(CODE,RAWDATA) {
222 300     300 0 10009 my ( undef, undef, $code, $name, $data ) = @_;
223 300         1614 $attr{$code}{$name} = _attr_hash( $name, $data,
224             qw{ completion=s unsatisfied! } );
225 300         1039 return;
226 20     20   22499 }
  20         44  
  20         105  
227              
228             sub Verb : ATTR(CODE,RAWDATA) {
229 1304     1304 0 2851521 my ( undef, undef, $code, $name, $data ) = @_;
230 1304         3507 $attr{$code}{$name} = _attr_list( $data );
231 1304         4044 return;
232 20     20   24371 }
  20         120  
  20         111  
233              
234             sub _attr_hash {
235 320     320   1101 my ( $name, $arg, @legal ) = @_;
236 320         1808 my $gol = Getopt::Long::Parser->new();
237 320         32021 my %opt;
238             $gol->getoptionsfromarray(
239             _attr_list( $arg ),
240             \%opt,
241             @legal,
242 320 50       952 ) or do {
243 0         0 require Carp;
244 0         0 Carp::croak( "Bad $name option" );
245             };
246 320         144200 return \%opt;
247             }
248              
249             sub _attr_list {
250 1624 50   1624   5506 defined( local $_ = $_[0] )
251             or return [];
252 1624         6800 s/ \A \s+ //smx;
253 1624         19215 return [ split qr< \s+ >smx ];
254             }
255              
256             sub __get_attr {
257 1235     1235   3452 my ( undef, $code, $name, $dflt ) = @_; # $pkg unused
258 1235 50       2905 defined $code
259             or return \%attr;
260             defined $name
261 1235 50       2740 or return $attr{$code};
262             exists $attr{$code}{$name}
263 1235 100       6770 and return $attr{$code}{$name};
264 624         2747 return $dflt;
265             }
266             }
267              
268             my %mutator = (
269             almanac_horizon => \&_set_almanac_horizon,
270             appulse => \&_set_angle,
271             autoheight => \&_set_unmodified,
272             backdate => \&_set_unmodified,
273             background => \&_set_unmodified,
274             continuation_prompt => \&_set_unmodified,
275             country => \&_set_unmodified,
276             date_format => \&_set_formatter_attribute,
277             desired_equinox_dynamical => \&_set_formatter_attribute,
278             debug => \&_set_unmodified,
279             echo => \&_set_unmodified,
280             edge_of_earths_shadow => \&_set_unmodified,
281             ellipsoid => \&_set_ellipsoid,
282             error_out => \&_set_unmodified,
283             events => \&_set_unmodified,
284             exact_event => \&_set_unmodified,
285             execute_filter => \&_set_code_ref, # Undocumented and unsupported
286             explicit_macro_delete => \&_set_unmodified,
287             extinction => \&_set_unmodified,
288             filter => \&_set_unmodified,
289             flare_mag_day => \&_set_unmodified,
290             flare_mag_night => \&_set_unmodified,
291             formatter => \&_set_formatter,
292             geocoder => \&_set_geocoder,
293             geometric => \&_set_unmodified,
294             gmt => \&_set_formatter_attribute,
295             height => \&_set_distance_meters,
296             horizon => \&_set_angle,
297             illum => \&_set_illum_class,
298             latitude => \&_set_angle_or_undef,
299             local_coord => \&_set_formatter_attribute,
300             location => \&_set_unmodified,
301             longitude => \&_set_angle_or_undef,
302             model => \&_set_model,
303             max_mirror_angle => \&_set_angle,
304             output_layers => \&_set_output_layers,
305             pass_threshold => \&_set_angle_or_undef,
306             pass_variant => \&_set_pass_variant,
307             perltime => \&_set_time_parser_attribute,
308             prompt => \&_set_unmodified,
309             refraction => \&_set_unmodified,
310             simbad_url => \&_set_unmodified,
311             singleton => \&_set_unmodified,
312             spacetrack => \&_set_spacetrack,
313             stdout => \&_set_stdout,
314             sun => \&_set_sun_class, # Only in {level1}
315             time_format => \&_set_formatter_attribute,
316             time_formatter => \&_set_formatter_attribute,
317             time_parser => \&_set_time_parser,
318             ## timing => \&_set_unmodified,
319             twilight => \&_set_twilight, # 'civil', 'nautical', 'astronomical'
320             # (or a unique abbreviation thereof),
321             # or degrees above (positive) or below
322             # (negative) the geometric horizon.
323             tz => \&_set_tz,
324             verbose => \&_set_unmodified, # 0 = events only
325             # 1 = whenever above horizon
326             # 2 = anytime
327             visible => \&_set_unmodified, # 1 = only if sun down & sat illuminated
328             warning => \&_set_warner_attribute, # True to warn/die; false to carp/croak.
329             warn_on_empty => \&_set_unmodified,
330             # True to have list commands warn on
331             # an empty list.
332             webcmd => \&_set_webcmd, # Command to spawn for web pages
333             );
334              
335             my %accessor = (
336             date_format => \&_get_formatter_attribute,
337             desired_equinox_dynamical => \&_get_formatter_attribute,
338             geocoder => \&_get_geocoder,
339             gmt => \&_get_formatter_attribute,
340             local_coord => \&_get_formatter_attribute,
341             perltime => \&_get_time_parser_attribute,
342             spacetrack => \&_get_spacetrack,
343             time_format => \&_get_formatter_attribute,
344             time_formatter => \&_get_formatter_attribute,
345             tz => \&_get_time_parser_attribute,
346             warning => \&_get_warner_attribute,
347             );
348              
349             foreach ( keys %mutator, qw{ initfile } ) {
350             $accessor{$_} ||= sub { return $_[0]->{$_[1]} };
351             }
352              
353             my %shower = (
354             date_format => \&_show_formatter_attribute,
355             desired_equinox_dynamical => \&_show_formatter_attribute,
356             formatter => \&_show_copyable,
357             geocoder => \&_show_copyable,
358             gmt => \&_show_formatter_attribute,
359             local_coord => \&_show_formatter_attribute,
360             pass_variant => \&_show_pass_variant,
361             sun => \&_show_sun_class, # only in {level1}
362             time_parser => \&_show_time_parser,
363             time_format => \&_show_formatter_attribute,
364             time_formatter => \&_show_formatter_attribute,
365             );
366             foreach ( keys %accessor ) { $shower{$_} ||= \&_show_unmodified }
367              
368             # Attributes which must be set programmatically (i.e. not
369             # interactively or in the initialization file).
370              
371             my %nointeractive = map {$_ => 1} qw{
372             execute_filter
373             spacetrack
374             stdout
375             };
376              
377             # Initial object contents
378              
379             my %static = (
380             almanac_horizon => 0,
381             appulse => 0,
382             autoheight => 1,
383             background => 1,
384             backdate => 0,
385             continuation_prompt => '> ',
386             date_format => '%a %d-%b-%Y',
387             debug => 0,
388             echo => 0,
389             edge_of_earths_shadow => 1,
390             ellipsoid => Astro::Coord::ECI->get ('ellipsoid'),
391             error_out => 0,
392             events => 0,
393             exact_event => 1,
394             execute_filter => sub { return 1 }, # Undocumented and unsupported
395             ## explicit_macro_delete => 1, # Deprecated
396             extinction => 1,
397             filter => 0,
398             flare_mag_day => -6,
399             flare_mag_night => 0,
400             formatter => 'Astro::App::Satpass2::Format::Template', # Formatter class.
401             ## geocoder => $default_geocoder->(), # Geocoder class set when accessed
402             geometric => 1,
403             height => undef, # meters
404             # initfile => undef, # Set by init()
405             horizon => 20, # degrees
406             illum => SUN_CLASS_DEFAULT,
407             latitude => undef, # degrees
408             longitude => undef, # degrees
409             max_mirror_angle => HAVE_TLE_IRIDIUM ? rad2deg(
410             Astro::Coord::ECI::TLE::Iridium->DEFAULT_MAX_MIRROR_ANGLE ) :
411             undef,
412             model => 'model',
413             # pending => undef, # Continued input line if it exists.
414             pass_variant => PASS_VARIANT_NONE,
415             perltime => 0,
416             prompt => 'satpass2> ',
417             refraction => 1,
418             simbad_url => 'simbad.u-strasbg.fr',
419             singleton => 0,
420             # spacetrack => undef, # Astro::SpaceTrack object set when accessed
421             # stdout => undef, # Set to stdout in new().
422             output_layers => DEFAULT_STDOUT_LAYERS,
423             time_parser => 'Astro::App::Satpass2::ParseTime', # Time parser class.
424             twilight => 'civil',
425             tz => $ENV{TZ},
426             verbose => 0,
427             visible => 1,
428             warning => 0,
429             warn_on_empty => 1,
430             webcmd => ''
431             );
432              
433             my %sky_class = (
434             fold_case( 'Sun' ) => [ SUN_CLASS_DEFAULT, name => 'Sun' ],
435             fold_case( 'Moon' ) => [ 'Astro::Coord::ECI::Moon', name => 'Moon' ],
436             # # The shape of things to come -- maybe
437             # # but commented out because Astro-App-Satpass2 does not depend on
438             # # these
439             # ( map { fold_case( $_ ) =>
440             # "Astro::Coord::ECI::VSOP87D::$_" } qw{ Mercury Venus
441             # Mars Jupiter Saturn Uranus Neptune } ),
442             );
443              
444             sub new {
445 7     7   1408621 my ( $class, %args ) = @_;
446 7 50       39 ref $class and $class = ref $class;
447 7         22 my $self = {};
448 7         27 $self->{bodies} = [];
449 7         22 $self->{macro} = {};
450             $self->{sky} = [
451 7         97 SUN_CLASS_DEFAULT->new (),
452             Astro::Coord::ECI::Moon->new (),
453             ];
454 7         3267 $self->{sky_class} = { %sky_class };
455             $self->{_help_module} = {
456 7         80 '' => __PACKAGE__,
457             eci => 'Astro::Coord::ECI',
458             moon => 'Astro::Coord::ECI::Moon',
459             set => 'Astro::Coord::ECI::TLE::Set',
460             sun => SUN_CLASS_DEFAULT,
461             spacetrack => 'Astro::SpaceTrack',
462             star => 'Astro::Coord::ECI::Star',
463             tle => 'Astro::Coord::ECI::TLE',
464             utils => 'Astro::Coord::ECI::Utils',
465             };
466             HAVE_TLE_IRIDIUM
467 7         18 and $self->{_help_module}{iridium} = 'Astro::Coord::ECI::TLE::Iridium';
468 7         22 bless $self, $class;
469 7         45 $self->_frame_push(initial => []);
470 7         53 $self->set(stdout => select());
471              
472 7         138 foreach my $name ( keys %static ) {
473 301 50       899 exists $args{$name} or $args{$name} = $static{$name};
474             }
475              
476             $self->{_warner} = Astro::App::Satpass2::Warner->new(
477             warning => delete $args{warning}
478 7         106 );
479              
480 7         22 foreach my $name ( qw{ formatter time_parser } ) {
481 14         116 $self->set( $name => delete $args{$name} );
482             }
483              
484 7         186 $self->set( %args );
485              
486 7         125 return $self;
487             }
488              
489             sub add {
490 1     1 1 15 my ( $self, @bodies ) = @_;
491 1         14 foreach my $body ( @bodies ) {
492 1 50       25 embodies( $body, 'Astro::Coord::ECI::TLE' )
493             or $self->wail(
494             'Arguments must represent Astro::Coord::ECI::TLE objects' );
495             }
496 1         77 push @{ $self->{bodies} }, @bodies;
  1         27  
497 1         17 return $self;
498             }
499              
500             sub alias : Verb() {
501 5     5 1 19 my ( undef, undef, @args ) = __arguments( @_ ); # Invocant, $opt unused
502              
503 5 100       12 if ( @args ) {
504 2         11 Astro::Coord::ECI::TLE->alias( @args );
505 2         48 return;
506             } else {
507 3         5 my $output;
508 3         18 my %alias = Astro::Coord::ECI::TLE->alias();
509 3         48 foreach my $key ( sort keys %alias ) {
510 10         25 $output .= join( ' ', 'alias', $key, $alias{$key} ) . "\n";
511             }
512 3         11 return $output;
513             }
514 20     20   60254 }
  20         51  
  20         159  
515              
516             # Attributes must all be on one line to process correctly under Perl
517             # 5.8.8.
518             sub almanac : Verb( choose=s@ dump! horizon|rise|set! transit! twilight! quarter! ) {
519 3     3 1 17 my ( $self, $opt, @args ) = __arguments( @_ );
520 3         42 $self->_apply_boolean_default(
521             $opt, 0, qw{ horizon transit twilight quarter } );
522              
523 3         16 my $almanac_start = $self->__parse_time(
524             shift @args, $self->_get_day_midnight());
525 3   50     33 my $almanac_end = $self->__parse_time (shift @args || '+1');
526              
527 3 50       9 $almanac_start >= $almanac_end
528             and $self->wail( 'End time must be after start time' );
529              
530             # Build an object representing our ground location.
531              
532 3         21 my $sta = $self->station();
533              
534 3         1221 my @almanac;
535              
536             # Iterate through the background bodies, accumulating data or
537             # complaining about the lack of an almanac() method as
538             # appropriate.
539              
540             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
541 3 50       25 or return $self->__wail( 'No bodies selected' );
542              
543 3         11 foreach my $body ( @sky ) {
544 6 50       521559 $body->can ('almanac') or do {
545 0         0 $self->whinge(
546             ref $body, ' does not support the almanac method');
547 0         0 next;
548             };
549             $body->set (
550             station => $sta,
551             twilight => $self->{_twilight},
552 6         55 );
553 6         829 push @almanac, $body->almanac_hash(
554             $almanac_start, $almanac_end);
555             }
556              
557             # Record number of events found
558              
559 3         980310 @almanac = grep { $opt->{$_->{almanac}{event}} } @almanac;
  27         82  
560 3         15 $self->{events} += @almanac;
561              
562             # Localize the event descriptions if appropriate.
563              
564 3         19 _almanac_localize( @almanac );
565              
566             # Sort the almanac data by date, and display the results.
567              
568             return $self->__format_data(
569             almanac => [
570 3         19 sort { $a->{time} <=> $b->{time} }
  41         71  
571             @almanac
572             ], $opt );
573              
574 20     20   12789 }
  20         66  
  20         121  
575             sub _almanac_localize {
576 9     9   58 my @almanac = @_;
577 9         34 foreach my $event ( @almanac ) {
578             $event->{almanac}{description} = __localize(
579             text => [ almanac => $event->{body}->get( 'name' ),
580             $event->{almanac}{event}, $event->{almanac}{detail} ],
581             default => $event->{almanac}{description},
582             argument => $event->{body},
583 46         224 );
584             }
585 9         29 return;
586             }
587              
588             sub begin : Verb() Tweak( -unsatisfied ) {
589 5     5 1 45 my ( $self, $opt, @args ) = __arguments( @_ );
590             $self->_frame_push(
591 5 50       58 begin => @args ? \@args : $self->{frame}[-1]{args});
592 5         21 $self->{frame}[-1]{level1} = $opt->{level1};
593 5         18 return;
594 20     20   10022 }
  20         47  
  20         114  
595              
596             # -level1 is UNSUPPORTED and may be removed without warning. It is only
597             # there for me to screw around with.
598             BEGIN {
599             $ENV{SATPASS2_LEVEL1}
600 20 50   20   9285 and __PACKAGE__->MODIFY_CODE_ATTRIBUTES(
601             \&begin,
602             'Verb( level1! )',
603             );
604             }
605              
606             sub cd : Verb() {
607 2     2 1 55 my ( $self, undef, $dir ) = __arguments( @_ ); # $opt unused
608 2 100       29 if (defined($dir)) {
609 1 50       38 chdir $dir or $self->wail("Can not cd to $dir: $!");
610             } else {
611 1 50       20 chdir File::HomeDir->my_home()
612             or $self->wail("Can not cd to home: $!");
613             }
614 2         90 return;
615 20     20   151 }
  20         79  
  20         168  
616              
617             sub choose : Verb( epoch=s ) {
618 2     2 1 9 my ( $self, $opt, @args ) = __arguments( @_ );
619              
620 2 50       11 if ($opt->{epoch}) {
621 0         0 my $epoch = $self->__parse_time($opt->{epoch});
622             $self->{bodies} = [
623             map {
624 0         0 $_->select($epoch);
625             }
626             $self->_aggregate( $self->{bodies} )
627 0         0 ];
628             }
629 2 50       6 if ( @args ) {
630 2 50       5 my @bodies = @{ $self->__choose( \@args, $self->{bodies} ) }
  2         28  
631             or return $self->__wail( 'No bodies chosen' );
632 2         5 @{ $self->{bodies} } = @bodies;
  2         39  
633             }
634 2         8 return;
635 20     20   12273 }
  20         63  
  20         100  
636              
637             sub clear : Verb() {
638 5     5 1 70 my ( $self ) = __arguments( @_ ); # $opt, @args unused
639 5         27 @{$self->{bodies}} = ();
  5         149  
640 5         22 return;
641 20     20   5588 }
  20         60  
  20         123  
642              
643             sub dispatch {
644 289     289 1 1141 my ($self, $verb, @args) = @_;
645              
646 289 50       1070 defined $verb or return;
647              
648 289         1051 my $unsatisfied = $self->_in_unsatisfied_if();
649              
650 289 100       1085 if ( $self->{macro}{$verb} ) {
651 19 50       57 $unsatisfied
652             and return;
653 19         83 return $self->_macro( $verb, @args );
654             }
655              
656 270         463 my $code;
657 270         680 $verb =~ s/ \A core [.] //smx;
658 270 100 66     1799 $code = $self->can($verb)
659             and $self->__get_attr($code, 'Verb')
660             or $self->wail("Unknown interactive method '$verb'");
661              
662 269         493 my $rslt;
663             $unsatisfied
664             and not $self->__get_attr( $code, Tweak => {} )->{unsatisfied}
665 269 100 100     1646 or $rslt = $code->( $self, @args );
666              
667 261 100       2931690 defined $rslt
668             and $rslt =~ s/ (?
669              
670 261         559 foreach my $code (
671 261 100       1979 reverse @{ delete( $self->{frame}[-1]{post_dispatch} ) || [] }
672             ) {
673 23         48 my $append;
674 23 100       69 defined( $append = $code->( $self ) )
675             and $rslt .= $append;
676             }
677 261         1564 return $rslt;
678             }
679              
680             {
681             my %special = (
682             begin => sub {
683             my ( $self, $verb ) = @_;
684             $self->_is_interactive()
685             or $self->wail(
686             "'begin' forbidden in non-interactive $verb()" );
687             return;
688             },
689             end => sub {
690             my ( $self, $verb ) = @_;
691             $self->wail( "'end' forbidden in $verb()" );
692             },
693             );
694              
695             sub _dispatch_check {
696 23     23   70 my ( $self, $verb, $disp ) = @_;
697 23 100       82 my $code = $special{$disp}
698             or return;
699 4         23 return $code->( $self, $verb, $disp );
700             }
701             }
702              
703             sub drop : Verb() {
704 1     1 1 8 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
705              
706             @args
707 1 50       7 or return;
708              
709             my @bodies = @{
710 1 50       3 $self->__choose( { invert => 1 }, \@args, $self->{bodies} ) }
  1         20  
711             or return $self->__wail( 'No bodies left' );
712              
713 1         4 @{ $self->{bodies} } = @bodies;
  1         5  
714              
715 1         4 return;
716 20     20   18039 }
  20         50  
  20         263  
717              
718             sub dump : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
719 0     0 1 0 my ( $self, undef, @arg ) = __arguments( @_ ); # $opt unused
720              
721 0         0 local $self->{time_parser} = ref $self->{time_parser};
722              
723 0         0 my $dumper = $self->_get_dumper();
724              
725             @arg
726 0 0       0 or return $dumper->( $self );
727              
728 0         0 local $_ = shift @arg;
729              
730             ref
731 0 0       0 and return $dumper->( $_ );
732              
733             m/ \A frames? \z /smxi
734 0 0       0 and return $dumper->( $self->{frame} );
735              
736 0 0       0 m/ \A tokens? \z /smxi
737             and return $dumper->( $self->__tokenize( @arg ) );
738              
739             m/ \A twilight \z /smxi
740             and return $dumper->(
741 0 0       0 { map { $_ => $self->{$_} } qw{ twilight _twilight } } );
  0         0  
742              
743 0         0 my @stuff = $self->__choose( [ $_ ], $self->{bodies} );
744 0 0       0 if ( defined( my $inx = $self->_find_in_sky( $_ ) ) ) {
745 0         0 push @stuff, $self->{sky}[$inx];
746             }
747             @stuff
748 0 0       0 and return $dumper->( @stuff );
749              
750 0         0 $self->whinge( "Dump argument '$_' not recognized" );
751            
752 0         0 return;
753 20     20   13484 }
  20         48  
  20         110  
754              
755             sub echo : Verb( n! ) {
756 44     44 1 223 my ( undef, $opt, @args ) = __arguments( @_ ); # Invocant unused
757 44         192 my $output = join( ' ', @args );
758 44 50       166 $opt->{n} or $output .= "\n";
759 44         175 return $output;
760 20     20   7669 }
  20         44  
  20         117  
761              
762             sub else : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
763 2     2 1 12 my ( $self ) = __arguments( @_ ); # $opt, @args unused
764              
765 2         39 @{ $self->{frame} } > 1
766             and 'begin' eq $self->{frame}[-1]{type}
767             and 'if' eq $self->{frame}[-2]{type}
768 2 50 33     7 or $self->wail( 'Else without if ... then begin' );
      33        
769              
770 2 50       13 $self->{frame}[-1]{in_else}++
771             and $self->wail( 'Only one else may follow an if' );
772              
773 2         10 return $self->_twiddle_condition( ! $self->{frame}[-2]{condition} );
774 20     20   7672 }
  20         80  
  20         122  
775              
776             sub _twiddle_condition {
777 4     4   13 my ( $self, $cond ) = @_;
778              
779             # Here is where I pay for the convenience of the if()
780             # implementation. The if() itself is a frame because I do not yet
781             # know if it will entail a begin(). But I can't do an else() unless
782             # there is in fact a begin(), which creates another frame. So I end
783             # up twiddling values in both frames.
784              
785             $self->{frame}[-1]{unsatisfied_if} =
786             $self->{frame}[-2]{unsatisfied_if} =
787             ! $cond || (
788             @{ $self->{frame} } > 2 ?
789             $self->{frame}[-3]{unsatisfied_if} :
790 4   66     50 0
791             );
792              
793             $self->{frame}[-1]{condition} =
794 4         13 $self->{frame}[-2]{condition} = $cond;
795              
796 4         19 return;
797             }
798              
799             sub end : Verb() Tweak( -unsatisfied ) {
800 5     5 1 51 my ( $self ) = __arguments( @_ ); # $opt, @args unused
801              
802 5 50       90 $self->{frame}[-1]{type} eq 'begin'
803             or $self->wail( 'End without begin' );
804 5         53 $self->_frame_pop();
805 5         20 return;
806 20     20   10038 }
  20         44  
  20         115  
807              
808             sub error : Verb() {
809 1     1 1 6 my ( $self, undef, @arg ) = __arguments( @_ );
810             @arg
811 1 50       7 or push @arg, 'An error has occurred';
812 1         8 $self->wail( @arg );
813 0         0 return;
814 20     20   7616 }
  20         58  
  20         188  
815              
816             # Tokenize and execute one or more commands. Optionally (and
817             # unsupportedly) you can pass a code reference as the first argument.
818             # This code reference will be used to fetch commands when the arguments
819             # are exhausted. IF you pass your own code reference, we return after
820             # the first command, since the code reference is presumed to manage the
821             # input stream itself.
822             sub execute {
823 264     264 1 1013 my ($self, @args) = @_;
824 264         728 my $accum;
825             my $in;
826 264         0 my $extern;
827 264 100       926 if ( CODE_REF eq ref $args[0] ) {
828 13         27 $extern = shift @args;
829             $in = sub {
830 21     21   38 my ( $prompt ) = @_;
831 21 100       64 @args and return shift @args;
832 8         18 return $extern->( $prompt );
833 13         81 };
834             } else {
835 251     502   1419 $in = sub { return shift @args };
  502         1915  
836             }
837 264         767 @args = map { split qr{ (?<= \n ) }smx, $_ } @args;
  265         4691  
838 264         1262 while ( defined ( local $_ = $in->( $self->get( 'prompt' ) ) ) ) {
839 280 50       997 $self->{echo} and $self->whinge($self->get( 'prompt' ), $_);
840 280 100       1050 m/ \A \s* [#] /smx and next;
841 277         975 my $stdout = $self->{frame}[-1]{stdout};
842             my ($args, $redirect) = $self->__tokenize(
843 277         1899 { in => $in }, $_, $self->{frame}[-1]{args});
844             # NOTICE
845             #
846             # The execute_filter attribute is undocumented and unsupported.
847             # It exists only so I can scavenge the user's initialization
848             # file for the (possible) Space Track username and password, to
849             # be used in testing, without being subject to any other
850             # undesired side effects, such as running a prediction and
851             # exiting. If I change my mind on how or whether to do this,
852             # execute_filter will be altered or retracted without warning,
853             # much less a deprecation cycle. If you have a legitimate need
854             # for this functionality, contact me.
855             #
856             # YOU HAVE BEEN WARNED.
857 273 100       1562 $self->{execute_filter}->( $self, $args ) or next;
858 267 100       1414 @{ $args } or next;
  267         792  
859 266 100       770 if ($redirect->{'>'}) {
860 1         4 my ( $mode, $name ) = map { $redirect->{'>'}{$_} } qw{ mode name };
  2         8  
861 1         3 my $fh;
862             $stdout = sub {
863 1     1   4 my ( $output ) = @_;
864 1   33     13 $fh ||= $self->_file_opener( $name, $mode );
865 1         10 $fh->print( $output );
866 1         10 return;
867 1         6 };
868             }
869              
870             # {localout} is the output to be used for this command. It goes
871             # in the frame stack because our command may start a new frame,
872             # and _frame_push() needs to have a place to get the correct
873             # output handle.
874              
875 266         475 my $frame_depth = $#{$self->{frame}};
  266         694  
876 266         973 $self->{frame}[-1]{localout} = $stdout;
877              
878 266         1092 my $output = $self->dispatch( @$args );
879              
880 256         1328 $#{$self->{frame}} >= $frame_depth
881 256 100       515 and delete $self->{frame}[ $frame_depth ]{localout};
882              
883 256 100       1543 $self->_execute_output( $output,
884             defined $stdout ? $stdout : \$accum );
885              
886 256 100       2381 $extern and last;
887             }
888 250         2255 return $accum;
889             }
890              
891             # $satpass2->_execute(...);
892             #
893             # This subroutine calls $satpass2->execute() once for each
894             # argument. The call is wrapped in an eval{}; if an exception
895             # occurs the user is notified via warn.
896              
897             sub _execute {
898 0     0   0 my ($self, @args) = @_;
899             my $in = CODE_REF eq ref $args[0] ? shift @args : sub { return shift
900 0 0   0   0 @args };
  0         0  
901 0         0 while ( @args ) {
902 0     0   0 local $SIG{INT} = sub {die "\n$interrupted\n"};
  0         0  
903 0 0       0 eval {
904 0         0 $self->execute( $in, shift @args );
905 0         0 1;
906             } or warn $@; # Not whinge, since presumably we already did.
907             }
908 0         0 return;
909             }
910              
911             # $satpass2->_execute_output( $output, $stdout );
912             #
913             # If $output is defined, sends it to $stdout.
914              
915             sub _execute_output {
916 256     256   867 my ( undef, $output, $stdout ) = @_; # Invocant unused
917 256 100       759 defined $output or return;
918 152         480 my $ref = ref $stdout;
919 152 50       813 if ( !defined $stdout ) {
    100          
    100          
    50          
920 0         0 return $output;
921             } elsif ( SCALAR_REF eq $ref ) {
922 149         455 $$stdout .= $output;
923             } elsif ( CODE_REF eq $ref ) {
924 2         9 $stdout->( $output );
925             } elsif ( ARRAY_REF eq $ref ) {
926 1         9 push @$stdout, split qr{ (?<=\n) }smx, $output;
927             } else {
928 0         0 $stdout->print( $output );
929             }
930 152         345 return;
931             }
932              
933             sub exit : method Verb() { ## no critic (ProhibitBuiltInHomonyms)
934 1     1 1 5 my ( $self ) = __arguments( @_ ); # $opt, @args unused
935              
936 1         11 $self->_frame_pop(1); # Leave only the inital frame.
937              
938 1         2 eval { ## no critic (RequireCheckingReturnValueOfEval)
939 20     20   27440 no warnings qw{exiting};
  20         58  
  20         2724  
940 1         20 last SATPASS2_EXECUTE;
941             };
942 0         0 $self->whinge("$@Exiting Perl");
943 0         0 exit;
944              
945 20     20   171 }
  20         43  
  20         105  
946              
947             sub export : Verb() {
948 4     4 1 19 my ( $self, undef, $name, @args ) = __arguments( @_ ); # $opt unused
949 4 100       32 if ($mutator{$name}) {
950 1 50       38 @args and $self->set ($name, shift @args);
951 1         5 $self->{exported}{$name} = 1;
952             } else {
953 3 100       37 @args or return $self->wail( 'You must specify a value' );
954 2         28 $self->{exported}{$name} = shift @args;
955             }
956 3         12 return;
957 20     20   7819 }
  20         69  
  20         147  
958              
959             # Attributes must all be on one line to process correctly under Perl
960             # 5.8.8.
961             sub flare : Verb( algorithm=s am! choose=s@ day! dump! pm! questionable|spare! quiet! tz|zone=s )
962             {
963 0     0 1 0 my ( $self, $opt, @args ) = __arguments( @_ );
964 0         0 HAVE_TLE_IRIDIUM
965             or $self->wail( 'Astro::Coord::ECI::TLE::Iridium not available' );
966 0         0 my $pass_start = $self->__parse_time (
967             shift @args, $self->_get_day_noon());
968 0   0     0 my $pass_end = $self->__parse_time (shift @args || '+7');
969 0 0       0 $pass_start >= $pass_end
970             and $self->wail( 'End time must be after start time' );
971 0         0 my $sta = $self->station();
972              
973 0         0 my $max_mirror_angle = deg2rad( $self->{max_mirror_angle} );
974 0         0 my $horizon = deg2rad ($self->{horizon});
975 0         0 my $twilight = $self->{_twilight};
976 0         0 my @flare_mag = ($self->{flare_mag_night}, $self->{flare_mag_day});
977             my $zone = exists $opt->{tz} ? $opt->{tz} :
978             $self->{formatter}->gmt() ? 0 :
979 0 0 0     0 $self->{formatter}->tz() || undef;
    0          
980              
981 0         0 $self->_apply_boolean_default(
982             $opt, 0, qw{ am day pm } );
983              
984             # Decide which model to use.
985              
986 0         0 my $model = $self->{model};
987              
988             # Select only the bodies capable of flaring.
989              
990 0         0 my @active;
991 0         0 foreach my $tle ( $self->_aggregate(
992             scalar $self->__choose( $opt->{choose}, $self->{bodies} )
993             ) )
994             {
995 0 0       0 $tle->can_flare( $opt->{questionable} ) or next;
996             $tle->set (
997             algorithm => $opt->{algorithm} || 'fixed',
998             backdate => $self->{backdate},
999             edge_of_earths_shadow => $self->{edge_of_earths_shadow},
1000             horizon => $horizon,
1001             twilight => $twilight,
1002             model => $model,
1003             am => $opt->{am},
1004             max_mirror_angle => $max_mirror_angle,
1005             day => $opt->{day},
1006             pm => $opt->{pm},
1007             extinction => $self->{extinction},
1008 0   0     0 station => $sta,
1009             zone => $zone,
1010             );
1011 0         0 push @active, $tle;
1012             }
1013 0 0       0 @active or return $self->__wail( 'No bodies capable of flaring' );
1014              
1015 0         0 my @flares;
1016 0         0 foreach my $tle (@active) {
1017             eval {
1018 0         0 push @flares, $tle->flare( $pass_start, $pass_end );
1019 0         0 1;
1020 0 0       0 } or do {
1021 0 0       0 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
1022 0 0       0 $opt->{quiet} or $self->whinge($@);
1023             };
1024             }
1025              
1026             # Record number of events found
1027              
1028 0         0 @flares = sort { $a->{time} <=> $b->{time} }
1029 0         0 grep { $_->{magnitude} <= $flare_mag[
1030 0 0       0 ( $_->{type} eq 'day' ? 1 : 0 ) ] }
1031             @flares;
1032 0         0 $self->{events} += @flares;
1033              
1034 0         0 return $self->__format_data( flare => \@flares, $opt );
1035 20     20   17031 }
  20         176  
  20         131  
1036              
1037             sub formatter : Verb() Tweak( -completion _readline_complete_subcommand ) {
1038 9 50   9 1 73 splice @_, ( HASH_REF eq ref $_[1] ? 2 : 1 ), 0, 'formatter';
1039 9         70 goto &_helper_handler;
1040 20     20   6574 }
  20         61  
  20         121  
1041              
1042             # Calls to the following _formatter_sub method are generated dynamically
1043             # above, so there is no way Perl::Critic can find them.
1044              
1045             sub _formatter_sub { ## no critic (ProhibitUnusedPrivateSubroutines)
1046 0     0   0 my ( $app, $text, $line, $start, @arg ) = @_;
1047 0         0 my $fmtr = $app->get( 'formatter' );
1048 0 0       0 if ( @arg == 2 ) {
1049 0         0 my @list = qw{
1050             date_format
1051             desired_equinox_dynamical
1052             gmt
1053             local_coord
1054             time_format
1055             tz
1056             };
1057 0 0       0 $fmtr->can( '__list_templates' )
1058             and push @list, 'template';
1059 0         0 my $re = qr/ \A \Q$arg[1]\E /smx;
1060 0         0 return [ grep { $_ =~ $re } sort @list ];
  0         0  
1061             }
1062 0 0       0 my $code = $app->can( "_formatter_complete_$arg[1]" )
1063             or return;
1064              
1065 0         0 my $r;
1066 0 0       0 $r = $app->_readline_complete_options( $code, $text, $line,
1067             $start )
1068             and return $r;
1069              
1070 0         0 return $code->( $app, @arg );
1071             }
1072              
1073             # Calls to the following _formatter_complete_... methods are generated
1074             # dynamically above, so there is no way Perl::Critic can find them.
1075             # The Verb attribute must aggree with _helper_handler().
1076              
1077             sub _formatter_complete_template : Verb( changes! raw! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1078 0     0   0 my ( $app, undef, @arg ) = __arguments( @_ );
1079 0         0 my $fmtr = $app->get( 'formatter' );
1080 0         0 my $re = qr/ \A \Q$arg[2]\E /smx;
1081             return [
1082 0         0 grep { $_ =~ $re }
  0         0  
1083             sort( $fmtr->__list_templates() )
1084             ];
1085 20     20   13760 }
  20         60  
  20         171  
1086              
1087             sub geocode : Verb( debug! ) {
1088 0     0 1 0 my ( $self, $opt, $loc ) = __arguments( @_ );
1089              
1090 0         0 my $set_loc;
1091 0 0       0 if ( defined $loc ) {
1092 0         0 $set_loc = 1;
1093             } else {
1094 0         0 $loc = $self->get( 'location' );
1095             }
1096              
1097 0         0 my $geocoder = $self->_helper_get_object( 'geocoder' );
1098              
1099 0         0 my @rslt = $geocoder->geocode( $loc );
1100              
1101 0         0 my $output;
1102 0 0       0 if ( @rslt == 1 ) {
1103             $set_loc
1104 0 0       0 and $self->set( location => $rslt[0]{description} );
1105 0         0 $self->set( map { $_ => $rslt[0]{$_} } qw{ latitude
  0         0  
1106             longitude } );
1107 0 0       0 $output .= $self->show(
1108             ( $set_loc ? 'location' : () ), qw{latitude longitude} );
1109 0 0       0 if ( $self->get( 'autoheight' ) ) {
1110 0         0 $opt->{geocoding} = 1;
1111 0         0 $output .= $self->_height_us($opt);
1112             }
1113             } else {
1114 0         0 foreach my $poi ( @rslt ) {
1115 0         0 $output .= join ' ', map { $poi->{$_} } qw{ latitude
  0         0  
1116             longitude description };
1117 0         0 $output =~ s/ (?: \A | (?
1118             }
1119             }
1120 0         0 return $output;
1121 20     20   12066 }
  20         47  
  20         522  
1122              
1123             sub geodetic : Verb() {
1124 0     0 1 0 my ( $self, undef, $name, $lat, $lon, $alt ) = __arguments( @_ ); # $opt unused
1125 0 0       0 @_ == 5 or $self->wail( 'Want exactly four arguments' );
1126 0         0 my $body = Astro::Coord::ECI::TLE->new(
1127             name => $name,
1128             id => '',
1129             model => 'null',
1130             )->geodetic(
1131             deg2rad( $self->__parse_angle( $lat ) ),
1132             deg2rad( $self->__parse_angle( $lon ) ),
1133             $self->__parse_distance( $alt ),
1134             );
1135 0         0 push @{ $self->{bodies} }, $body;
  0         0  
1136 0         0 return;
1137 20     20   8809 }
  20         51  
  20         164  
1138              
1139             sub get {
1140 927     927   3281 my ($self, $name) = @_;
1141 927         4458 $self->_attribute_exists( $name );
1142 927         3775 $self->_deprecation_notice( attribute => $name );
1143 927         3878 return $accessor{$name}->($self, $name);
1144             }
1145              
1146             sub height : Verb( debug! ) {
1147 0     0 1 0 return _height_us( __arguments( @_ ) );
1148 20     20   7772 }
  20         76  
  20         184  
1149              
1150             sub _height_us {
1151 0     0   0 my ($self, $opt, @args) = @_;
1152 0         0 $self->_load_module ('Geo::WebService::Elevation::USGS');
1153 0         0 my $eq = Geo::WebService::Elevation::USGS->new(
1154             places => 2, # Service returns unreasonable precision
1155             units => 'METERS', # default for service is 'FEET'
1156             croak => 0, # Handle our own errors
1157             );
1158 0 0       0 @args or push @args, $self->get('latitude'), $self->get('longitude');
1159 0         0 my $output;
1160 0         0 my ( $rslt ) = $eq->elevation(@args);
1161 0 0       0 if ( $eq->is_valid( $rslt ) ) {
1162 0         0 $self->set( height => $rslt->{Elevation} );
1163             } else {
1164             $opt->{geocoding}
1165 0 0 0     0 or $self->wail( $eq->get( 'error' ) || 'No valid result found' );
1166 0         0 $self->set( height => 0 );
1167 0         0 $output .= "# Unable to obtain height. Setting to 0\n";
1168             }
1169 0         0 $output .= $self->show( 'height' );
1170 0         0 return $output;
1171             }
1172              
1173             sub help : Verb() {
1174 0     0 1 0 my ( $self, undef, $arg ) = __arguments( @_ ); # $opt unused
1175 0 0       0 defined $arg
1176             or $arg = '';
1177             defined $self->{_help_module}{$arg}
1178 0 0       0 and $arg = $self->{_help_module}{$arg};
1179 0 0       0 if ( my $cmd = $self->_get_browser_command() ) {
1180 0 0       0 my $kind = $arg =~ m/ - /smx ? 'release' : 'pod';
1181 0         0 $self->system( $cmd,
1182             "https://metacpan.org/$kind/$arg" );
1183             } else {
1184              
1185 0         0 my $os_specific = "_help_$^O";
1186 0 0       0 if (__PACKAGE__->can ($os_specific)) {
    0          
1187 0         0 return __PACKAGE__->$os_specific ();
1188             } elsif ( load_package( 'Pod::Usage' ) ) {
1189 0         0 my @ha;
1190 0 0       0 if ( defined( my $path = find_package_pod( $arg ) ) ) {
1191 0         0 push @ha, '-input' => $path;
1192             }
1193 0         0 my $stdout = $self->{frame}[-1]{localout};
1194 0 0 0     0 if (openhandle $stdout && !-t $stdout) {
1195 0         0 push @ha, -output => $stdout;
1196             }
1197             Pod::Usage::pod2usage (
1198 0         0 -verbose => 2, -exitval => 'NOEXIT', @ha);
1199             } else {
1200             # This should never happen, since Pod::Usage is core
1201             # since 5.6. On the other hand we have not declared it
1202             # as a dependency, and some downstream packagers seem to
1203             # think they know more than the author what should be in
1204             # a package.
1205             return <<'EOD'
1206             No help available; Pod::Usage can not be loaded.
1207             EOD
1208 0         0 }
1209             }
1210 0         0 return;
1211 20     20   17862 }
  20         52  
  20         108  
1212              
1213             # The call to this is generated dynamically above, and there is no way
1214             # Perl::Critic can find it.
1215             sub _help_MacOS { ## no critic (ProhibitUnusedPrivateSubroutines)
1216 0     0   0 return <<'EOD';
1217              
1218             Normally, we would display the documentation for the satpass2
1219             script here. But unfortunately this depends on the ability to
1220             spawn the perldoc command, and we do not have this ability under
1221             Mac OS 9 and earlier. You can find the same thing online at
1222             https://metacpan.org/release/Astro-App-Satpass2
1223              
1224             EOD
1225             }
1226              
1227             {
1228             # This hash specifies the specific grammar passed to
1229             # __infix_engine(). The keys are:
1230             # {done} optional; called when parse is complete.
1231             # {oper} defines operators. Values are hash refs with:
1232             # {handler} code that handles operator;
1233             # {validation} name of validation style (see {vld} below).
1234             # {vld} defines operator validation. There must be a key for each
1235             # distinct value of {oper}{$name}{validation}.
1236             # NOTE WELL
1237             # Because if() has the Tweak( -unsatisfied ) attribute, any
1238             # operators that have side effects will need to be aware of whether
1239             # they are running inside an unsatisfied if().
1240             my %define = (
1241             done => sub {
1242             # my ( $self, $def, $ctx, $tokens ) = @_;
1243             my ( $self, undef, $ctx ) = @_;
1244             @{ $ctx }
1245             and $self->wail( q );;
1246             return;
1247             },
1248             oper => {
1249             '(' => {
1250             handler => sub {
1251             my ( $self, $def, $ctx, $tokens ) = @_;
1252             my $want = delete $ctx->[-1]{want};
1253             defined $want
1254             or $want = 1;
1255             push @{ $ctx }, {
1256             want => $want,
1257             value => [],
1258             };
1259             $ctx->[-2]{shortcut}
1260             and $ctx->[1]{shortcut} = $ctx->[-2]{shortcut};
1261             my $depth = @{ $ctx };
1262             while ( $depth <= @{ $ctx } ) {
1263             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1264             }
1265             return;
1266             },
1267             },
1268             ')' => {
1269             handler => sub {
1270             # my ( $self, $def, $ctx, $tokens ) = @_;
1271             my ( $self, undef, $ctx ) = @_;
1272             @{ $ctx }
1273             or $self->wail( 'Unpaired right parentheses' );
1274             $ctx->[-1]{want} == @{ $ctx->[-1]{value} }
1275             or $self->wail(
1276             "Expected $ctx->[-1]{want} value(s), got " .
1277             scalar @{ $ctx->[-1]{value} } );
1278             push @{ $ctx->[-2]{value} }, @{ $ctx->[-1]{value} };
1279             pop @{ $ctx };
1280             return;
1281             },
1282             },
1283             '-n' => {
1284             handler => sub {
1285             # my ( $self, $def, $ctx, $tokens ) = @_;
1286             my ( undef, undef, $ctx, $tokens ) = @_;
1287             my $v = shift @{ $tokens };
1288             defined $v
1289             or $v = '';
1290             push @{ $ctx->[-1]{value} }, '' ne $v;
1291             },
1292             validation => 'prefix',
1293             },
1294             '-z' => {
1295             handler => sub {
1296             # my ( $self, $def, $ctx, $tokens ) = @_;
1297             my ( undef, undef, $ctx, $tokens ) = @_;
1298             my $v = shift @{ $tokens };
1299             defined $v
1300             or $v = '';
1301             push @{ $ctx->[-1]{value} }, '' eq $v;
1302             },
1303             validation => 'prefix',
1304             },
1305             and => {
1306             handler => sub {
1307             my ( $self, $def, $ctx, $tokens ) = @_;
1308             $ctx->[-1]{value}[-1]
1309             or $ctx->[-1]{shortcut} = 1;
1310             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1311             # For some reason the following has to be done in
1312             # two statements, or both operands remain on the
1313             # stack.
1314             my $ro = pop @{ $ctx->[-1]{value} };
1315             $ctx->[-1]{value}[-1] &&= $ro
1316             unless delete $ctx->[-1]{shortcut};
1317             return;
1318             },
1319             validation => 'infix',
1320             },
1321             attr => {
1322             handler => sub {
1323             # my ( $self, $def, $ctx, $tokens ) = @_;
1324             my ( $self, undef, $ctx, $tokens ) = @_;
1325             my $attr = shift @{ $tokens };
1326             my $val;
1327             $ctx->[-1]{shortcut}
1328             or $val = $self->_attribute_value( $attr );
1329             NULL_REF eq ref $val
1330             and $self->wail( "No such attribute as '$attr'" );
1331             push @{ $ctx->[-1]{value} }, $val;
1332             return;
1333             },
1334             validation => 'prefix',
1335             },
1336             env => {
1337             handler => sub {
1338             # my ( $self, $def, $ctx, $tokens ) = @_;
1339             my ( undef, undef, $ctx, $tokens ) = @_;
1340             my $name = shift @{ $tokens };
1341             my $val;
1342             $ctx->[-1]{shortcut}
1343             or $val = $ENV{$name};
1344             push @{ $ctx->[-1]{value} }, $val;
1345             return;
1346             },
1347             validation => 'prefix',
1348             },
1349             loaded => {
1350             handler => sub {
1351             # my ( $self, $def, $ctx, $tokens ) = @_;
1352             my ( $self, undef, $ctx, $tokens ) = @_;
1353             my $name = shift @{ $tokens };
1354             my @loaded;
1355             $ctx->[-1]{shortcut}
1356             or @loaded = $self->__choose(
1357             { bodies => 1 },
1358             [ $name ],
1359             );
1360             push @{ $ctx->[-1]{value} }, scalar @loaded;
1361             return;
1362             },
1363             validation => 'prefix',
1364             },
1365             not => {
1366             handler => sub {
1367             my ( $self, $def, $ctx, $tokens ) = @_;
1368             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1369             $ctx->[-1]{value}[-1] = ! $ctx->[-1]{value}[-1];
1370             return;
1371             },
1372             validation => 'prefix',
1373             },
1374             or => {
1375             handler => sub {
1376             my ( $self, $def, $ctx, $tokens ) = @_;
1377             $ctx->[-1]{value}[-1]
1378             and $ctx->[-1]{shortcut} = 1;
1379             $self->_infix_engine_dispatch( $def, $ctx, $tokens );
1380             # For some reason the following has to be done in
1381             # two statements, or both operands remain on the
1382             # stack.
1383             my $ro = pop @{ $ctx->[-1]{value} };
1384             $ctx->[-1]{value}[-1] ||= $ro
1385             unless delete $ctx->[-1]{shortcut};
1386             return;
1387             },
1388             validation => 'infix',
1389             },
1390             os => {
1391             handler => sub {
1392             # my ( $self, $def, $ctx, $tokens ) = @_;
1393             my ( undef, undef, $ctx, $tokens ) = @_;
1394             my $re = qr< \A \Q$^O\E \z >smxi;
1395             my $rslt = 0;
1396             my $name = shift @{ $tokens };
1397             unless ( $ctx->[-1]{shortcut} ) {
1398             foreach my $os ( split qr< [|] >smx, $name ) {
1399             $os =~ $re
1400             or next;
1401             $rslt = 1;
1402             last;
1403             }
1404             }
1405             push @{ $ctx->[-1]{value} }, $rslt;
1406             return;
1407             },
1408             validation => 'prefix',
1409             },
1410             then => {
1411             handler => sub {
1412             # my ( $self, $def, $ctx, $tokens ) = @_;
1413             my ( $self, undef, $ctx, $tokens ) = @_;
1414             1 == @{ $ctx }
1415             or $self->wail( 'Unclosed left parentheses' );
1416             my $last = pop @{ $ctx };
1417             my @arg = splice @{ $tokens };
1418             if ( $last->{dispatch} ) {
1419             $self->_dispatch_check( if => $arg[0] );
1420             $self->_frame_push( if => [], {
1421             condition => $last->{value}[-1],
1422             },
1423             );
1424             $self->_add_post_dispatch( sub {
1425             $self->_frame_pop( if => undef );
1426             },
1427             );
1428             return $self->dispatch( @arg );
1429             } else {
1430             $self->_twiddle_condition( $last->{value}[-1] );
1431             }
1432             },
1433             validation => 'terminal',
1434             },
1435             },
1436             val => sub {
1437             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1438             my ( undef, undef, $ctx, $tkn ) = @_;
1439             push @{ $ctx->[-1]{value} }, $tkn;
1440             return;
1441             },
1442             vld => {
1443             infix => sub {
1444             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1445             my ( $self, undef, $ctx, $tkn, $tokens ) = @_;
1446             @{ $ctx->[-1]{value} }
1447             or $self->wail( "'$tkn' requires a left argument" );
1448             @{ $tokens }
1449             or $self->wail( "'$tkn' requires a right argument" );
1450             return;
1451             },
1452             prefix => sub {
1453             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1454             my ( $self, undef, undef, $tkn, $tokens ) = @_;
1455             @{ $tokens }
1456             or $self->wail( "'$tkn' requires an argument" );
1457             return;
1458             },
1459             terminal => sub {
1460             # my ( $self, $def, $ctx, $tkn, $tokens ) = @_;
1461             my ( $self, undef, $ctx, $tkn, $tokens ) = @_;
1462             @{ $ctx->[-1]{value} }
1463             or $self->wail( "'$tkn' requires a left argument" );
1464             if ( $ctx->[-1]{dispatch} ) {
1465             @{ $tokens }
1466             or $self->wail( "Command required after '$tkn'" );
1467             } else {
1468             @{ $tokens }
1469             and $self->wail( "Command not allowed after '$tkn'" );
1470             }
1471             return;
1472             }
1473             },
1474             );
1475              
1476             sub elsif : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
1477 2     2 1 10 my ( $self, @args ) = @_;
1478             @args
1479 2 50       7 or $self->wail( 'Arguments required' );
1480              
1481 2         56 @{ $self->{frame} } > 1
1482             and 'begin' eq $self->{frame}[-1]{type}
1483             and 'if' eq $self->{frame}[-2]{type}
1484 2 50 33     5 or $self->wail( 'Elsif without if ... then begin' );
      33        
1485              
1486 2         12 my @ctx = ( {
1487             dispatch => 0,
1488             value => [],
1489             } );
1490              
1491             # If any previous if() or elsif() evaluates true, we do not
1492             # evaluate subsequent elsif() calls.
1493             $self->{frame}[-2]{condition}
1494 2 50       8 and return;
1495 2         8 return $self->__infix_engine( \%define, \@ctx, @args );
1496 20     20   53362 }
  20         52  
  20         114  
1497              
1498             sub if : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
1499 22     22 1 109 my ( $self, @args ) = @_;
1500             @args
1501 22 50       64 or $self->wail( 'Arguments required' );
1502 22         143 my @ctx = ( {
1503             dispatch => 1,
1504             value => [],
1505             } );
1506 22         106 return $self->__infix_engine( \%define, \@ctx, @args );
1507 20     20   7746 }
  20         47  
  20         119  
1508             }
1509              
1510             sub init {
1511 0     0 1 0 my ( $self, @args ) = @_;
1512              
1513 0 0       0 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
1514 0         0 my $init_file = shift @args;
1515              
1516 0         0 $self->{initfile} = undef;
1517              
1518 0 0       0 foreach (
1519             defined $init_file ? (
1520             sub {
1521             # A missing init file is only an error if it was
1522             # specified explicitly.
1523 0 0 0 0   0 -e $init_file
1524             and not -d _
1525             or $self->wail(
1526             "Initialization file $init_file not found, or is a directory"
1527             );
1528             return ( $init_file, $opt->{level1} )
1529 0         0 },
1530             ) : (
1531 0     0   0 sub { return $ENV{SATPASS2INI} },
1532 0     0   0 sub { $self->initfile( { quiet => 1 } ) },
1533 0     0   0 sub { return ( $ENV{SATPASSINI}, 1 ) },
1534             \&_init_file_01,
1535             )
1536             ) {
1537              
1538 0         0 my ( $fn, $level1 ) = $_->($self);
1539 0 0       0 my $reader = $self->_file_reader( $fn, { optional => 1 } )
1540             or next;
1541 0         0 $self->{initfile} = $fn;
1542 0         0 return $self->source( { level1 => $level1 }, $reader );
1543              
1544             }
1545              
1546 0         0 return;
1547             }
1548              
1549             sub initfile : Verb( create-directory! quiet! ) {
1550 0     0 1 0 my ( $self, $opt ) = __arguments( @_ ); # @args unused
1551              
1552             my $init_dir = my_dist_config(
1553 0         0 { create => $opt->{'create-directory'} } );
1554              
1555             defined $init_dir
1556 0 0       0 or do {
1557 0 0       0 $opt->{quiet} and return;
1558 0         0 $self->wail(
1559             'Init file directory not found' );
1560             };
1561              
1562 0         0 return File::Spec->catfile( $init_dir, 'satpass2rc' );
1563 20     20   15479 }
  20         51  
  20         131  
1564              
1565             sub _in_unsatisfied_if {
1566 290     290   675 my ( $self ) = @_;
1567 290 50       454 return @{ $self->{frame} } ? $self->{frame}[-1]{unsatisfied_if} : 0;
  290         1576  
1568             }
1569              
1570             # This is a generalized infix expression engine. It does not implement
1571             # operator precedence and is therefore very small. The arguments are:
1572             # - $self is the invocant, which must be an
1573             # Astro::App::Satpass2::Copier.
1574             # - $def is the hash that defines the grammar. This provides the
1575             # following keys:
1576             # {done} is an optional code reference. If present, the code
1577             # reference is called once the parse is complete, and passed
1578             # ( $self, $def, $ctx, \@tokens ). It returns nothing. The intent
1579             # is to throw an exception if the parse is incomplete.
1580             # {oper} defines the operators. This is a hash keyed by the literal
1581             # operator (i.e. '+' to implement a '+' operator), and having the
1582             # following values:
1583             # {handler} is a required code reference, which implements the
1584             # operator. It is passed ( $self, $def, $ctx, \@tokens ). The
1585             # @tokens do not include the operator itself.
1586             # {validation} is an optional validation specification. If
1587             # present it is a key in the {vld} (see below).
1588             # {val} is an optional code reference. If present, it is called if a
1589             # token is not recognized as an operator, and passed ( $self,
1590             # $def, $ctx, \@tokens ). The @tokens include the unrecognized
1591             # token, which is presumed to be a value, and must be removed
1592             # from @tokens.
1593             # {vld} is a hash of validators. The keys are values in the
1594             # {validation} key documented under {oper} (above), and the
1595             # values are code references which are called with ( $self, $ctx,
1596             # $tkn, \@tokens ) where $tkn is the token being validated, and
1597             # @tokens is the rest of the tokens. This hash must exist if the
1598             # {validation} key is used in {oper}; otherwise it is optional.
1599             # - $ctx is context for the operations. It is not used by the engine
1600             # itself, but the individual operator code will need to use it as
1601             # context for the parse. See if() for an example.
1602             # - @tokens are the tokens to be evaluated by the engine.
1603             sub __infix_engine {
1604 24     24   181 my ( $self, $def, $ctx, @tokens ) = @_;
1605             @tokens
1606 24 50       69 or $self->wail( 'Nothing to compute' );
1607 24         43 my $rslt;
1608 24         60 while ( @tokens ) {
1609 50         173 $rslt = $self->_infix_engine_dispatch( $def, $ctx, \@tokens );
1610             }
1611             $def->{done}
1612 24 50       154 and $def->{done}->( $self, $def, $ctx, \@tokens );
1613 24         112 return $rslt;
1614             }
1615              
1616             sub _infix_engine_dispatch {
1617 61     61   145 my ( $self, $def, $ctx, $tokens ) = @_;
1618 61 50       103 @{ $tokens }
  61         146  
1619             or return;
1620 61         113 my $tkn = shift @{ $tokens };
  61         137  
1621 61 100       240 if ( my $info = $def->{oper}{$tkn} ) {
    50          
1622             $info->{validation}
1623 57 100       325 and $def->{vld}{ $info->{validation} }->(
1624             $self, $def, $ctx, $tkn, $tokens );
1625 57         176 return $info->{handler}->( $self, $def, $ctx, $tokens );
1626             } elsif ( $def->{val} ) {
1627 4         15 return $def->{val}->( $self, $def, $ctx, $tkn, $tokens );
1628             } else {
1629 0         0 $self->wail( "Unrecognized token '$tkn'" );
1630             }
1631 0         0 return; # We can't get here, but Perl::Critic does not know this.
1632             }
1633              
1634             # $file_name = _init_file_01()
1635             #
1636             # This subroutine returns the first alternate init file name,
1637             # which is the standard name for the Astro-satpass 'satpass'
1638             # script. If called in list context it returns not only the name,
1639             # but a 1 to tell the caller this is a 'level1' file.
1640              
1641             sub _init_file_01 {
1642 0 0 0 0   0 my $inifn = $^O eq 'MSWin32' || $^O eq 'VMS' || $^O eq 'MacOS' ?
1643             'satpass.ini' : '.satpass';
1644             my $inifile = $^O eq 'VMS' ? "SYS\$LOGIN:$inifn" :
1645             $^O eq 'MacOS' ? $inifn :
1646             $ENV{HOME} ? "$ENV{HOME}/$inifn" :
1647             $ENV{LOGDIR} ? "$ENV{LOGDIR}/$inifn" :
1648 0 0       0 $ENV{USERPROFILE} ? "$ENV{USERPROFILE}" : undef;
    0          
    0          
    0          
    0          
1649 0 0       0 return wantarray ? ( $inifile, 1 ) : $inifile;
1650             }
1651              
1652             sub list : Verb( choose=s@ ) {
1653 7     7 1 48 my ( $self, $opt, @args ) = __arguments( @_ );
1654              
1655             @args
1656             and not $opt->{choose}
1657 7 50 33     89 and $opt->{choose} = \@args;
1658 7         77 my @bodies = $self->__choose( $opt->{choose}, $self->{bodies} );
1659              
1660             @bodies
1661 7 100       90 and return $self->__format_data(
1662             list => \@bodies, $opt );
1663              
1664             $self->{warn_on_empty}
1665 2 50       9 and $self->whinge( 'The observing list is empty' );
1666              
1667 2         7 return;
1668 20     20   19317 }
  20         65  
  20         147  
1669              
1670             sub load : Verb( verbose! ) {
1671 6     6 1 34 my ( $self, $opt, @names ) = __arguments( @_ );
1672 6 50       39 @names or $self->wail( 'No file names specified' );
1673              
1674 6         29 my $attrs = {
1675             illum => $self->get( 'illum' ),
1676             model => $self->get( 'model' ),
1677             sun => $self->_sky_object( 'sun' ),
1678             };
1679              
1680 6         678 foreach my $fn ( @names ) {
1681 6 50       29 $opt->{verbose} and warn "Loading $fn\n";
1682 6         79 my $data = $self->_file_reader( $fn, { glob => 1 } );
1683 5         378 $self->__add_to_observing_list(
1684             Astro::Coord::ECI::TLE->parse( $attrs, $data ) );
1685             }
1686 5         31 return;
1687 20     20   9023 }
  20         48  
  20         114  
1688              
1689             sub localize : Verb( all|except! ) {
1690 1     1 1 6 my ( $self, $opt, @args ) = __arguments( @_ );
1691              
1692 1         5 foreach my $name ( @args ) {
1693 2         7 $self->_attribute_exists( $name );
1694             }
1695              
1696 1 50       5 if ( $opt->{all} ) {
1697 0         0 my %except = map { $_ => 1 } @args;
  0         0  
1698 0         0 @args = grep { ! $except{$_} } sort keys %mutator;
  0         0  
1699             }
1700              
1701 1         3 foreach my $name ( @args ) {
1702 2         6 $self->_localize( $name );
1703             }
1704              
1705 1         4 return;
1706 20     20   8801 }
  20         55  
  20         137  
1707              
1708             sub _localize {
1709 2     2   6 my ( $self, $key ) = @_;
1710              
1711             my $val = exists $self->{$key} ?
1712 2 50       10 $self->{$key} :
1713             $self->get( $key );
1714 2 50 33     10 my $clone = ( blessed( $val ) && $val->can( 'clone' ) ) ?
    50          
1715             $val->clone() :
1716             ref $val ? Clone::clone( $val ) : $val;
1717              
1718 2         8 $self->{frame}[-1]{local}{$key} = $val;
1719 2 50       5 if ( exists $self->{$key} ) {
1720 2         4 $self->{$key} = $clone;
1721             } else {
1722 0         0 $self->set( $key => $clone );
1723             }
1724              
1725 2         6 return;
1726             }
1727              
1728             sub location : Verb( dump! ) {
1729 3     3 1 15 my ( $self, $opt ) = __arguments( @_ );
1730 3         34 return $self->__format_data(
1731             location => $self->station(), $opt );
1732 20     20   10721 }
  20         58  
  20         138  
1733              
1734             {
1735              
1736             # TODO the %mac_cmd hash is only needed for level1 compatibility.
1737             # Once that goes away, it can too PROVIDED we also drop the
1738             # subcommand defaulting functionality.
1739             # Subcommand defaulting dropped 2021-09-20 unless explicitly level1,
1740             # after I discovered that my init file defined an unwanted macro
1741             # when I mistyped 'define' as 'defined'.
1742             my %mac_cmd;
1743             {
1744             my $stb = __PACKAGE__ . '::';
1745             my @cmdnam;
1746             {
1747 20     20   5026 no strict qw{ refs };
  20         45  
  20         10549  
1748             foreach my $entry ( keys %{ $stb } ) {
1749             $entry =~ m/ \A _macro_sub_ ( \w+ ) /smx
1750             or next;
1751             # Strictly speaking I should make sure the {CODE} slot
1752             # is occupied here.
1753             push @cmdnam, $1;
1754             }
1755             }
1756             my %abbr = abbrev(@cmdnam);
1757             foreach (keys %abbr) {
1758             $mac_cmd{'-' . $_} = $abbr{$_};
1759             }
1760             foreach (@cmdnam) {
1761             $mac_cmd{$_} = $_;
1762             }
1763             }
1764              
1765             # NOTE that we must not define command options here, but on the
1766             # individual _macro_sub_* methods. Or at least we must not define
1767             # any command options here that get passed to the _macro_sub_*
1768             # methods.
1769             sub macro : Verb() Tweak( -completion _readline_complete_subcommand ) {
1770 29     29 1 165 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
1771 29         717 my $cmd;
1772 29 50       191 if (!@args) {
    100          
1773 0         0 $cmd = 'brief';
1774             } elsif ( $self->{frame}[-1]{level1} ) {
1775 8 50       33 if ($mac_cmd{$args[0]}) {
    50          
1776 0         0 $cmd = $mac_cmd{shift @args};
1777             } elsif (@args > 1) {
1778 8         17 $cmd = 'define';
1779             } else {
1780 0         0 $cmd = 'list';
1781             }
1782             } else {
1783 21 50       101 defined( $cmd = $mac_cmd{ $args[0] } )
1784             or $cmd = $args[0];
1785 21         62 shift @args;
1786             }
1787              
1788 29 50       235 my $code = $self->can( "_macro_sub_$cmd" )
1789             or $self->wail( "Subcommand '$cmd' unknown" );
1790 29         126 return $code->( $self, @args );
1791 20     20   176 }
  20         42  
  20         116  
1792             }
1793              
1794             # Calls to the following _macro_sub_... methods are generated dynamically
1795             # above, so there is no way Perl::Critic can find them.
1796             sub _macro_sub_brief : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1797 3     3   15 my ( $self, undef, @args ) = __arguments( @_ );
1798 3         11 my $output;
1799 3 50       12 foreach my $name (sort @args ? @args : keys %{$self->{macro}}) {
  3         20  
1800 1 50       26 $self->{macro}{$name} and $output .= $name . "\n";
1801             }
1802 3         13 return $output;
1803 20     20   7840 }
  20         55  
  20         140  
1804              
1805             sub _macro_sub_define : Verb( completion=s@ ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1806 16     16   62 my ( $self, $opt, $name, @args ) = __arguments( @_ );
1807 16         45 my $output;
1808 16 50       216 defined $name
1809             or return $self->__wail( 'You must provide a name for the macro' );
1810             @args
1811 16 50       65 or return $self->__wail( 'You must provide a definition for the macro' );
1812              
1813 16 50 33     161 $name !~ m/ \W /smx
1814             and $name !~ m/ \A _ /smx
1815             or return $self->__wail("Invalid macro name '$name'");
1816              
1817             # NOTE the value of {def} used to be unescaped, but I do not now
1818             # know why, and the implementation of \U and friends is more natural
1819             # with this stripped out.
1820             $self->{macro}{$name} =
1821             Astro::App::Satpass2::Macro::Command->new(
1822             name => $name,
1823             parent => $self,
1824             completion => $opt->{completion},
1825             def => \@args,
1826             generate => \&_macro_define_generator,
1827             level1 => $self->{frame}[-1]{level1},
1828             warner => $self->{_warner},
1829 16         268 );
1830 16         96 return $output;
1831 20     20   12067 }
  20         412  
  20         218  
1832              
1833             sub _macro_define_generator {
1834 9     9   25 my ( $self, @args ) = @_; # $self if Macro object
1835 9         18 my $output;
1836 9         23 foreach my $macro ( @args ) {
1837 9 50       39 if ( my $comp = $self->completion() ) {
1838 0         0 $output .= "macro define \\\n " .
1839             "--completion '@$comp' \\\n " .
1840             "$macro \\\n ";
1841             } else {
1842 9         58 $output .= "macro define $macro \\\n ";
1843             }
1844 9         38 $output .= join( " \\\n ", map { quoter( $_ ) } $self->def() ) .
  17         72  
1845             "\n";
1846             }
1847 9         45 return $output;
1848             }
1849              
1850             sub _macro_sub_delete : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1851 1     1   24 my ( $self, undef, @args ) = __arguments( @_ );
1852 1         4 my $output;
1853 1 50       8 foreach my $name (@args ? @args : keys %{$self->{macro}}) {
  0         0  
1854 1         23 delete $self->{macro}{$name};
1855             }
1856 1         5 return $output;
1857 20     20   10999 }
  20         270  
  20         522  
1858              
1859             sub _macro_sub_list : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1860 9     9   33 my ( $self, undef, @args ) = __arguments( @_ );
1861 9         23 my $output;
1862 9 100       56 foreach my $name (sort @args ? @args : keys %{$self->{macro}}) {
  1         17  
1863 9 50       40 $self->{macro}{$name}
1864             or next;
1865 9         57 $output .= $self->{macro}{$name}->generator( $name );
1866             }
1867 9         31 return $output;
1868 20     20   9045 }
  20         214  
  20         204  
1869              
1870             sub _macro_sub_load : Verb( lib=s verbose! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1871 0     0   0 my ( $self, $opt, $name, @args ) = __arguments( @_ );
1872 0         0 my $output;
1873 0 0       0 defined $name
1874             or $self->wail( 'Must provide name of macro to load' );
1875             my %marg = (
1876             name => $name,
1877             parent => $self,
1878             generate => \&_macro_load_generator,
1879             warner => $self->{_warner},
1880 0         0 );
1881             exists $opt->{lib}
1882 0 0       0 and $marg{lib} = $opt->{lib};
1883 0   0     0 my $obj = $self->{_macro_load}{$name} ||=
1884             Astro::App::Satpass2::Macro::Code->new( %marg );
1885 0 0       0 foreach my $mn ( @args ? @args : $obj->implements() ) {
1886             $obj->implements( $mn, required => 1 )
1887 0 0       0 and $self->{macro}{$mn} = $obj;
1888             }
1889 0 0       0 if ( $opt->{verbose} ) {
1890 0         0 ( my $fn = "$name.pm" ) =~ s| :: |/|smxg;
1891 0         0 $output .= "Macro $name\n loaded from $INC{$fn}\n";
1892 0         0 $output .= " implements:\n";
1893 0         0 $output .= " $_\n" for sort $obj->implements();
1894             }
1895 0 0       0 $obj->implements( 'after_load', required => 0 )
1896             and $output .= $self->dispatch( after_load => $opt, $name, @args );
1897 0         0 return $output;
1898 20     20   13649 }
  20         44  
  20         182  
1899              
1900             sub _macro_load_generator {
1901 0     0   0 my ( $self, @args ) = @_;
1902 0         0 my @preamble = qw{ macro load };
1903 0 0       0 if ( $self->has_lib() ) {
1904 0         0 push @preamble, '-lib', $self->lib();
1905 0 0       0 $self->relative()
1906             and push @preamble, '-relative';
1907             }
1908 0         0 push @preamble, $self->name();
1909 0         0 my $output;
1910 0         0 foreach my $macro ( @args ) {
1911 0         0 $output .= quoter( @preamble, $macro ) . "\n";
1912             }
1913 0         0 return $output;
1914             }
1915              
1916             sub magnitude_table : Verb( name! reload! ) {
1917 0     0 1 0 my ( undef, undef, @args ) = __arguments( @_ ); # Invocant, $opt unused
1918              
1919 0 0       0 @args or @args = qw{show};
1920              
1921 0   0     0 my $verb = lc (shift (@args) || 'show');
1922              
1923 0         0 my $output;
1924              
1925 0 0 0     0 if ( $verb eq 'show' || $verb eq 'list' ) {
1926              
1927 0         0 my %data = Astro::Coord::ECI::TLE->magnitude_table( 'show', @args );
1928              
1929 0         0 foreach my $oid ( sort keys %data ) {
1930 0         0 $output .= quoter( 'status', 'add', $oid, $data{$oid} )
1931             . "\n";
1932             }
1933              
1934             } else {
1935 0         0 Astro::Coord::ECI::TLE->magnitude_table( $verb, @args );
1936             }
1937              
1938 0         0 return $output;
1939              
1940 20     20   12841 }
  20         68  
  20         134  
1941              
1942             # Attributes must all be on one line to process correctly under Perl
1943             # 5.8.8.
1944             sub pass : Verb( :compute __pass_options ) {
1945 20     20 1 145 my ( $self, $opt, @args ) = __arguments( @_ );
1946              
1947             $opt->{ephemeris}
1948 20 100       126 and $opt->{almanac} = 1;
1949             $opt->{almanac}
1950             and not defined $opt->{ephemeris}
1951             and $opt->{ephemeris} = {
1952             pass_ics => 1,
1953 20 100 100     148 }->{$opt->{_template}};
1954              
1955 20         154 $self->_apply_boolean_default(
1956             $opt, 0, qw{ horizon illumination transit appulse } );
1957 20         80 $self->_apply_boolean_default( $opt, 0, qw{ am pm } );
1958 20 50 66     80 $opt->{am} or $opt->{pm} or $opt->{am} = $opt->{pm} = 1;
1959 20         176 my $pass_start = $self->__parse_time (
1960             shift @args, $self->_get_day_noon());
1961 20   100     150 my $pass_end = $self->__parse_time (shift @args || '+7');
1962 20 50       85 $pass_start >= $pass_end
1963             and $self->wail( 'End time must be after start time' );
1964              
1965 20         235 my $sta = $self->station();
1966             my @bodies = $self->__choose( $opt->{choose}, $self->{bodies} )
1967 20 50       9536 or $self->wail( 'No bodies selected' );
1968 20   50     160 my $pass_step = shift @args || 60;
1969              
1970             # Decide which model to use.
1971              
1972 20         76 my $model = $self->{model};
1973              
1974             # Set the station for the objects in the sky.
1975              
1976 20         40 foreach my $body ( @{ $self->{sky} } ) {
  20         113  
1977 41         2760 $body->set( station => $sta );
1978             }
1979              
1980             # Pick up horizon and appulse distance.
1981              
1982 20         1444 my $horizon = deg2rad ($self->{horizon});
1983 20         128 my $appulse = deg2rad ($self->{appulse});
1984 20         115 my $pass_threshold = deg2rad( $self->{pass_threshold} );
1985              
1986             # In order that the interface not be completely rude, the interface
1987             # allows -brightest to specify that you want the 'brightest' event.
1988             # But this is controlled by the pass_variant attribute. So if
1989             # -brightest appears, the pass_variant from it; otherwise we default
1990             # -brightest from the pass_variant attribute. We localize the
1991             # pass_variant attribute before modifying it, since the -brightest
1992             # option is to hold for this call only. We modify it (rather than
1993             # just passing a local copy to the bodies) because
1994             # Formatter::Template needs to know what it is, and modifying this
1995             # object is the obvious way to pass the information.
1996 20         127 local $self->{pass_variant} = $self->{pass_variant};
1997 20 50       158 if ( $opt->{brightest} ) {
    50          
1998 0         0 $self->{pass_variant} |= PASS_VARIANT_BRIGHTEST;
1999             } elsif ( exists $opt->{brightest} ) {
2000 0         0 $self->{pass_variant} &= ~ PASS_VARIANT_BRIGHTEST;
2001             } else {
2002 20         124 $opt->{brightest} = $self->{pass_variant} & PASS_VARIANT_BRIGHTEST;
2003             }
2004 20         53 my $pass_variant = $self->{pass_variant};
2005              
2006             # Foreach body to be modelled
2007              
2008 20         61 my @accumulate; # For chronological output.
2009 20         136 foreach my $tle ( $self->_aggregate( \@bodies ) ) {
2010              
2011             {
2012 39 50       3000 my $mdl = $tle->get('inertial') ? $model :
  39         196  
2013             $tle->get('model');
2014             $tle->set (
2015             appulse => $appulse,
2016             backdate => $self->{backdate},
2017             debug => $self->{debug},
2018             edge_of_earths_shadow => $self->{edge_of_earths_shadow},
2019             geometric => $self->{geometric},
2020             horizon => $horizon,
2021             interval => ( $self->{verbose} ? $pass_step : 0 ),
2022             model => $mdl,
2023             pass_threshold => $pass_threshold,
2024             pass_variant => $pass_variant,
2025             station => $sta,
2026             twilight => $self->{_twilight},
2027             visible => $self->{visible},
2028 39 50       1672 );
2029             }
2030              
2031             eval {
2032             push @accumulate, $self->_pass_select_event( $opt, $tle->pass (
2033 39         356 $pass_start, $pass_end, $self->{sky} ) );
2034 39         257 1;
2035 39 50       15488 } or do {
2036 0 0       0 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
2037 0 0       0 $opt->{quiet} or $self->whinge($@);
2038             };
2039             }
2040              
2041 20         175 @accumulate = $self->__pass_filter_am_pm( $opt, @accumulate );
2042              
2043             $opt->{chronological}
2044 20 100       124 and @accumulate = sort { $a->{time} <=> $b->{time} }
  0         0  
2045             @accumulate;
2046              
2047             # Record number of events found.
2048             # NOTE that in this case an event is an entire pass.
2049              
2050 20         149 $self->{events} += @accumulate;
2051              
2052 20 100       113 if ( $opt->{almanac} ) {
2053 4         12 my %almanac;
2054 4         16 foreach my $pass ( @accumulate ) {
2055 6         66 my $illum = $pass->{body}->get( 'illum' );
2056 6         143 my $noon = $self->_get_day_noon( $pass->{time} );
2057 6   33     449 $almanac{$noon}{$illum} ||= do {
2058 6         33 my @day;
2059              
2060             my @events = grep { {
2061             horizon => 1,
2062             twilight => 1,
2063             }->{$_->{almanac}{event}}
2064 36         1143936 } $illum->almanac_hash(
2065 6         65 $self->_get_day_midnight( $pass->{time} ) );
2066              
2067 6         101 _almanac_localize( @events );
2068              
2069 6         24 foreach my $evt ( @events ) {
2070 24         63 $evt->{event} = 'almanac';
2071 24 100       77 my $pm = $evt->{time} >= $noon ? 1 : 0;
2072 24         35 push @{ $day[$pm] }, $evt;
  24         68  
2073             }
2074              
2075 6         46 \@day;
2076             };
2077              
2078 6 50       97 $pass->{_pm} = my $pm = $pass->{time} >= $noon ? 1 : 0;
2079             # TODO this way ALL passes get the almanac events. Is this
2080             # what I want? It varies. For --ics it is. For --events it
2081             # is not. For neither it's probably not.
2082 6 100       141 if ( $opt->{ephemeris} ) {
2083 3         29 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time}
2084 3         17 } @{ $pass->{events} }, @{ $almanac{$noon}{$illum}[$pm] };
  26         54  
  3         14  
  3         30  
2085             }
2086             }
2087              
2088 4 100       49 unless( $opt->{ephemeris} ) {
2089 2         7 foreach my $pass ( @accumulate ) {
2090             $pass->{_pm}
2091 3 50       35 or next;
2092 0         0 my $illum = $pass->{body}->get( 'illum' );
2093 0         0 my $noon = $self->_get_day_noon( $pass->{time} );
2094 0 0       0 $almanac{$noon}{$illum}[1]
2095             or next;
2096 0         0 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time} }
  0         0  
2097 0         0 @{ $pass->{events} },
2098 0         0 @{ $almanac{$noon}{$illum}[1] };
  0         0  
2099 0         0 $almanac{$noon}{$illum}[1] = undef;
2100             }
2101 2         7 foreach my $pass ( reverse @accumulate ) {
2102             $pass->{_pm}
2103 3 50       13 and next;
2104 3         25 my $illum = $pass->{body}->get( 'illum' );
2105 3         72 my $noon = $self->_get_day_noon( $pass->{time} );
2106 3 50       199 $almanac{$noon}{$illum}[0]
2107             or next;
2108 3         19 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time} }
  26         61  
2109 3         28 @{ $pass->{events} },
2110 3         9 @{ $almanac{$noon}{$illum}[0] };
  3         56  
2111 3         46 $almanac{$noon}{$illum}[0] = undef;
2112             }
2113             }
2114             }
2115              
2116             return $self->__format_data(
2117 20         236 $opt->{_template} => \@accumulate, $opt );
2118              
2119 20     20   32858 }
  20         48  
  20         117  
2120              
2121             sub __pass_filter_am_pm {
2122 20     20   108 my ( $self, $opt, @accumulate ) = @_;
2123 20   50     61 $opt ||= {};
2124             $opt->{am} xor $opt->{pm}
2125 20 100 75     195 or return @accumulate;
2126             return (
2127 6         14 map { $_->[0] }
2128 12   50     179 grep { $opt->{am} xor $_->[1] }
2129 2         19 map { [
2130             $_,
2131             $_->{time} >= $self->_get_day_noon( $_->{time} )
2132 12         484 ] } @accumulate
2133             );
2134             }
2135              
2136             sub __pass_options {
2137 20     20   67 my ( $self, $opt ) = @_;
2138             return [
2139 20         172 qw{
2140             almanac! am! appulse! brightest|magnitude! choose=s@
2141             chronological! ephemeris! dump! horizon|rise|set!
2142             illumination! pm!
2143             quiet! transit|maximum|culmination!
2144             },
2145             $self->_templates_to_options( pass => $opt ),
2146             ];
2147             }
2148              
2149             {
2150             my @selector;
2151             $selector[ PASS_EVENT_SHADOWED ] = 'illumination';
2152             $selector[ PASS_EVENT_LIT ] = 'illumination';
2153             $selector[ PASS_EVENT_DAY ] = 'illumination';
2154             $selector[ PASS_EVENT_RISE ] = 'horizon';
2155             $selector[ PASS_EVENT_MAX ] = 'transit';
2156             $selector[ PASS_EVENT_SET ] = 'horizon';
2157             $selector[ PASS_EVENT_APPULSE ] = 'appulse';
2158             $selector[ PASS_EVENT_START ] = 'horizon';
2159             $selector[ PASS_EVENT_END ] = 'horizon';
2160             $selector[ PASS_EVENT_BRIGHTEST ] = 'brightest';
2161              
2162             # Remove from the pass data any events that are not wanted. The
2163             # arguments are $self, the $opt hash reference that (among other
2164             # things) specifies the desired events, and the passes, each pass
2165             # being an argument. The modified passes are returned.
2166             sub _pass_select_event {
2167 39     39   69667647 my ( undef, $opt, @passes ) = @_; # Invocant unused
2168 39         154 my @rslt;
2169 39         202 foreach my $pass ( @passes ) {
2170 38         220 @{ $pass->{events} } = grep {
2171             _pass_select_event_code( $opt, $_->{event} )
2172 38 50       88 } @{ $pass->{events} }
  136         398  
  38         175  
2173             and push @rslt, $pass;
2174             }
2175             return @rslt
2176 39         164 }
2177              
2178             # Determine whether an event is to be reported for the pass. The
2179             # arguments are the $opt hash reference and the event code or name.
2180             # Anything that is not a dualvar and not an integer is accepted, on
2181             # the presumption that it is an ad-hoc event provided by some
2182             # subclass. The null event is always accepted on the presumption
2183             # that if the user did not want it he or she would not have asked
2184             # for it. Anything that is left is accepted or rejected based on the
2185             # option hash and the @selector array (defined above).
2186             sub _pass_select_event_code {
2187 136     136   310 my ( $opt, $event ) = @_;
2188 136 50 33     454 isdual( $event )
2189             or $event !~ m/ \D /smx
2190             or return 1;
2191 136 50       324 $event == PASS_EVENT_NONE
2192             and return 1;
2193 136   66     1875 return defined $selector[ $event ] && $opt->{ $selector[ $event ] };
2194             }
2195             }
2196              
2197             sub perl : Tokenize( -noexpand_tilde ) : Verb( eval! setup! ) {
2198 2     2 1 21 my ( $self, $opt, $file, @args ) = __arguments( @_ );
2199 2 50       38 defined $file
2200             or $self->wail( 'At least one argument is required' );
2201             $opt->{setup}
2202 2 50 0     16 and push @{ $self->{_perl} ||= [] }, [ $opt, $file, @args ];
  0         0  
2203 2         19 local @ARGV = ( $self, map { $self->expand_tilde( $_ ) } @args );
  0         0  
2204             $opt->{eval}
2205 2 100       34 or local $0 = $self->expand_tilde( $file );
2206              
2207             my $data = $opt->{eval} ?
2208 2 100       38 $file :
2209             $self->_file_reader( $file, { glob => 1 } );
2210 2         61 my $rslt;
2211             {
2212             # "random" package to prevent whoopsies in our own name space
2213 2         4 package qq_eval_namespace; ## no critic (Modules::ProhibitMultiplePackages)
2214 2         491 $rslt = eval $data; ## no critic (BuiltinFunctions::ProhibitStringyEval)
2215 2 100       37 $@
2216             and $self->wail( "Failed to eval '$file': $@" );
2217             }
2218 1 50       13 instance( $rslt, 'Astro::App::Satpass2' )
2219             or return $rslt;
2220 0         0 return;
2221 20     20   22512 }
  20         51  
  20         123  
2222              
2223             sub phase : Verb( choose=s@ ) {
2224 1     1 1 7 my ( $self, $opt, @args ) = __arguments( @_ );
2225              
2226 1         23 my $time = $self->__parse_time (shift @args, time );
2227              
2228             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
2229 1 50       25 or $self->wail( 'No bodies selected' );
2230             return $self->__format_data(
2231             phase => [
2232 1         11 map { { body => $_->universal( $time ), time => $time } }
2233 1         5 grep { $_->can( 'phase' ) }
  2         128  
2234             @sky
2235             ], $opt );
2236 20     20   9207 }
  20         64  
  20         228  
2237              
2238             sub position : Verb( choose=s@ questionable|spare! quiet! ) {
2239 4     4 1 34732 my ( $self, $opt, $time ) = __arguments( @_ );
2240              
2241 4 50       22 if ( defined $time ) {
2242 4         22 $time = $self->__parse_time($time);
2243             } else {
2244 0         0 $time = time;
2245             }
2246              
2247             # Define the observing station.
2248              
2249 4         27 my $sta = $self->station();
2250 4         2084 $sta->universal( $time );
2251              
2252             my @list = $self->__choose( { bodies => 1, sky => 1 },
2253 4         299 $opt->{choose} );
2254              
2255 4         20 my @good;
2256 4         46 my $horizon = deg2rad ($self->{horizon});
2257 4         61 foreach my $body (@list) {
2258 13 100       71 if ( $body->represents( 'Astro::Coord::ECI::TLE' ) ) {
2259             $body->set (
2260             backdate => $self->{backdate},
2261             debug => $self->{debug},
2262             edge_of_earths_shadow => $self->{edge_of_earths_shadow},
2263             geometric => $self->{geometric},
2264             horizon => $horizon,
2265             station => $sta,
2266             twilight => $self->{_twilight},
2267 4         154 );
2268             $body->get('inertial')
2269 4 50       1216 and $body->set( model => $self->{model} );
2270             }
2271             eval {
2272 13         53 $body->universal ($time);
2273 10         4728 push @good, $body;
2274 10         62 1;
2275 13 100       538 } or do {
2276 3 50       3790 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
2277 3 50       26 $opt->{quiet} or $self->whinge($@);
2278             };
2279             }
2280              
2281             return $self->__format_data(
2282             position => {
2283             bodies => \@good,
2284             questionable => $opt->{questionable},
2285             station => $self->station()->universal(
2286             $time ),
2287             time => $time,
2288             twilight => $self->{_twilight},
2289 4         28 }, $opt );
2290 20     20   12748 }
  20         51  
  20         109  
2291              
2292             sub pwd : Verb() {
2293 1     1 1 9637 return Cwd::cwd() . "\n";
2294 20     20   5833 }
  20         49  
  20         143  
2295              
2296             {
2297             my @quarter_name = map { "q$_" } 0 .. 3;
2298              
2299             sub quarters : Verb( choose=s@ dump! q0|new|spring! q1|first|summer! q2|full|fall q3|last|winter ) {
2300 1     1 1 30 my ( $self, $opt, @args ) = __arguments( @_ );
2301              
2302 1         30 my $start = $self->__parse_time (
2303             $args[0], $self->_get_day_midnight() );
2304 1   50     43 my $end = $self->__parse_time ($args[1] || '+30');
2305              
2306 1         14 $self->_apply_boolean_default( $opt, 0, map { "q$_" } 0 .. 3 );
  4         36  
2307              
2308             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
2309 1 50       37 or $self->wail( 'No bodies selected' );
2310              
2311 1         4 my @almanac;
2312              
2313             # Iterate over any background objects, accumulating all
2314             # quarter-phases of each until we get one after the end time. We
2315             # silently ignore bodies that do not support the next_quarter()
2316             # method.
2317              
2318 1         14 foreach my $body ( @sky ) {
2319 2 50       79 next unless $body->can ('next_quarter_hash');
2320 2         50 $body->universal ($start);
2321              
2322 2         3181 while (1) {
2323 7         100 my $hash = $body->next_quarter_hash();
2324 7 100       273463 $hash->{time} > $end and last;
2325 5 50       51 $opt->{$quarter_name[$hash->{almanac}{detail}]}
2326             or next;
2327 5         19 push @almanac, $hash;
2328             }
2329             }
2330              
2331             # Localize the event descriptions if appropriate.
2332              
2333 1         6 foreach my $event ( @almanac ) {
2334             $event->{almanac}{description} = __localize(
2335             text => [ almanac => $event->{body}->get( 'name' ),
2336             $event->{almanac}{event}, $event->{almanac}{detail}
2337             ],
2338             default => $event->{almanac}{description},
2339             argument => $event->{body},
2340 5         33 );
2341             }
2342              
2343             # Record number of events found
2344              
2345 1         13 $self->{events} += @almanac;
2346              
2347             # Sort and display the quarter-phase information.
2348              
2349             return $self->__format_data(
2350             almanac => [
2351 1         40 sort { $a->{time} <=> $b->{time} }
  9         59  
2352             @almanac
2353             ], $opt );
2354              
2355 20     20   14862 }
  20         48  
  20         116  
2356             }
2357              
2358             {
2359             my $go;
2360              
2361             sub run {
2362 0     0 1 0 my ( $self, @args ) = @_;
2363              
2364             # We can be called statically. If we are, instantiate.
2365 0 0       0 ref $self or $self = $self->new(warning => 1);
2366              
2367             # Put all the I/O into UTF-8 mode.
2368 0         0 binmode STDIN, ':encoding(UTF-8)';
2369 0         0 binmode STDOUT, DEFAULT_STDOUT_LAYERS;
2370 0         0 binmode STDERR, ':encoding(UTF-8)';
2371              
2372             # If the undocumented first option is a code reference, use it to
2373             # get input.
2374 0         0 my $in;
2375 0 0       0 CODE_REF eq ref $args[0]
2376             and $in = shift @args;
2377              
2378             # Parse the command options. -level1 is undocumented.
2379 0         0 my %opt;
2380 0   0     0 $go ||= Getopt::Long::Parser->new();
2381 0 0       0 $go->getoptionsfromarray(
2382             \@args,
2383             \%opt,
2384             qw{
2385             echo! filter! gmt! help initialization_file|initfile=s
2386             level1! version
2387             },
2388             )
2389             or $self->wail( 'See the help method for valid options' );
2390              
2391             # If -version, do it and return.
2392 0 0       0 if ( $opt{version} ) {
2393 0         0 print $self->version();
2394 0         0 return;
2395             }
2396              
2397             # If -help, do it and return.
2398 0 0       0 if ( $opt{help} ) {
2399 0         0 $self->help();
2400 0         0 return;
2401             }
2402              
2403             # Get an input routine if we do not already have one.
2404 0   0     0 $in ||= $self->_get_readline();
2405              
2406             # Some options get processed before we initialize.
2407 0         0 foreach my $name ( qw{ echo filter } ) {
2408             exists $opt{$name}
2409 0 0       0 and $self->set( $name => delete( $opt{$name} ) );
2410             }
2411              
2412             # Display the front matter if desired.
2413 0 0 0     0 (!$self->get('filter') && $self->_get_interactive())
2414             and print $self->version();
2415              
2416             # Execute the initialization file.
2417 0 0       0 eval {
2418             $self->_execute_output( $self->init(
2419             { level1 => delete $opt{level1} },
2420             delete $opt{initialization_file},
2421 0         0 ), $self->get( 'stdout' ) );
2422 0         0 1;
2423             } or warn $@; # Not whinge, since presumably we already did.
2424              
2425             # The remaining options set the corresponding attributes.
2426 0 0       0 %opt and $self->set(%opt);
2427              
2428             # Execution loop. What exit() really does is a last on this.
2429             SATPASS2_EXECUTE:
2430             {
2431 0         0 $self->_execute( @args );
  0         0  
2432 0         0 while ( defined ( my $buffer = $in->( $self->get( 'prompt' ) ) ) ) {
2433 0         0 $self->_execute( $in, $buffer );
2434             }
2435             }
2436 0         0 $self->_execute( q{echo ''} ); # The lazy way to be sure we
2437             # have a newline before exit.
2438 0         0 return;
2439             }
2440             }
2441              
2442             sub save : Verb( changes! overwrite! ) {
2443 0     0 1 0 my ( $self, $opt, $fn ) = __arguments( @_ );
2444              
2445 0 0       0 defined $fn or $fn = $self->initfile( { 'create-directory' => 1 } );
2446 0         0 chomp $fn; # because initfile() adds a newline for printing
2447 0 0 0     0 if ($fn ne '-' && -e $fn) {
2448 0 0       0 -f $fn or $self->wail(
2449             "Can not overwrite $fn: not an ordinary file");
2450 0 0       0 $opt->{overwrite} or do {
2451 0         0 my $rslt = $self->_get_readline()->(
2452             "File $fn exists. Overwrite [y/N]? ");
2453 0 0       0 'y' eq lc substr($rslt, 0, 1)
2454             or return;
2455             };
2456             }
2457 0         0 my @show_opt;
2458 0         0 my $title = 'settings';
2459 0 0       0 if ($opt->{changes}) {
2460 0         0 push @show_opt, '-changes';
2461 0         0 $title = 'setting changes';
2462             }
2463              
2464 0         0 my $output = <<"EOD" .
2465              
2466             # Astro::App::Satpass2 $title
2467              
2468             EOD
2469             $self->show( @show_opt, qw{ -nodeprecated -noreadonly } ) .
2470             <<"EOD" . $self->macro('list');
2471              
2472             # Astro::App::Satpass2 macros
2473              
2474             EOD
2475              
2476 0 0       0 if ( $self->{_perl} ) {
2477 0         0 $output .= <<'EOD';
2478              
2479             # Astro::App::Satpass2 setup
2480              
2481             EOD
2482 0         0 foreach my $item ( @{ $self->{_perl} } ) {
  0         0  
2483 0         0 my ( $opt, @arg ) = @{ $item };
  0         0  
2484 0         0 my @cmd = ( 'perl' );
2485 0         0 push @cmd, map { "-$_" } grep { $opt->{$_} } sort keys %{ $opt };
  0         0  
  0         0  
  0         0  
2486 0         0 $output .= join ' ', quoter( @cmd, @arg );
2487 0         0 $output .= "\n";
2488             }
2489             }
2490              
2491 0         0 foreach my $attribute ( qw{ formatter spacetrack time_parser } ) {
2492 0 0       0 my $obj = $self->get( $attribute ) or next;
2493 0 0 0     0 my $class = $obj->can( 'class_name_of_record' ) ?
2494             $obj->class_name_of_record() :
2495             ref $obj || $obj;
2496 0   0     0 $output .= <<"EOD" .
2497              
2498             # $class $title
2499              
2500             EOD
2501             ( $self->$attribute( $opt, 'config' ) || "# none\n" );
2502             }
2503              
2504 0         0 $output .= $self->_save_sky( $opt );
2505              
2506 0 0       0 if ($fn ne '-') {
2507 0 0       0 my $fh = IO::File->new( $fn, '>:encoding(utf-8)')
2508             or $self->wail("Unable to open $fn: $!");
2509 0         0 print { $fh } $output;
  0         0  
2510 0         0 $output = "$fn\n";
2511             }
2512 0         0 return $output;
2513 20     20   28267 }
  20         56  
  20         108  
2514              
2515             # Formats the commands to reconstitute the sky. This is only called from
2516             # save(), but it is a subroutine for organizational reasons.
2517             sub _save_sky {
2518 0     0   0 my ( $self, $opt ) = @_;
2519              
2520 0         0 my $output = <<'EOD';
2521              
2522             # Astro::App::Satpass2 sky
2523              
2524             EOD
2525              
2526 0         0 foreach my $body ( sort keys %{ $self->{sky_class} } ) {
  0         0  
2527             $opt->{changes}
2528             and $sky_class{$body}
2529 0 0 0     0 and $sky_class{$body} eq $self->{sky_class}{$body}
      0        
2530             and next;
2531 0         0 $output .= $self->_sky_class_components( $body ) . "\n";
2532             }
2533 0         0 foreach my $body ( sort keys ( %sky_class ) ) {
2534 0 0       0 $self->{sky_class}{$body}
2535             or $output .= $self->_sky_class_components( $body ) . "\n";
2536             }
2537              
2538 0         0 my %exclude;
2539 0 0       0 if ( $opt->{changes} ) {
2540 0         0 %exclude = map { $_ => 1 }
  0         0  
2541             SUN_CLASS_DEFAULT, 'Astro::Coord::ECI::Moon';
2542 0         0 foreach my $name ( qw{ sun moon } ) {
2543 0 0       0 defined $self->_find_in_sky( $name )
2544             or $output .= "sky drop $name\n";
2545             }
2546             } else {
2547 0         0 $output .= "sky clear\n";
2548             }
2549 0         0 foreach my $body ( @{ $self->{sky} } ) {
  0         0  
2550 0 0       0 $exclude{ ref $body }
2551             and next;
2552 0         0 $output .= _sky_list_body( $body );
2553             }
2554              
2555 0         0 return $output;
2556             }
2557              
2558             sub set : Verb() {
2559 72     72   461 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
2560              
2561 72         296 while (@args) {
2562 351         1105 my ( $name, $value ) = splice @args, 0, 2;
2563 351         1189 $self->_attribute_exists( $name );
2564 351 100       842 if ( _is_interactive() ) {
2565 28 100       144 $nointeractive{$name}
2566             and $self->wail(
2567             "Attribute '$name' may not be set interactively");
2568 27 50 66     212 defined $value and $value eq 'undef'
2569             and $value = undef;
2570             }
2571 350 50       1062 if ( $mutator{$name} ) {
2572 350         1179 $self->_deprecation_notice( attribute => $name );
2573 350         1186 $mutator{$name}->($self, $name, $value);
2574             } else {
2575 0         0 $self->wail("Read-only attribute '$name'");
2576             }
2577             }
2578 71         257 return;
2579 20     20   17798 }
  20         53  
  20         130  
2580              
2581             sub _set_almanac_horizon {
2582 7     7   57 my ( $self, $name, $value ) = @_;
2583 7         45 my $parsed = $self->__parse_angle( { accept => 1 }, $value );
2584 7 50       84 my $internal = looks_like_number( $parsed ) ? deg2rad( $parsed ) :
2585             $parsed;
2586 7         113 my $eci = Astro::Coord::ECI->new();
2587 7         776 $eci->set( $name => $internal ); # To validate.
2588 7         276 $self->{"_$name"} = $internal;
2589 7         136 return( $self->{$name} = $parsed );
2590             }
2591              
2592             {
2593             my $plus_or_minus_90 = sub { $_[0] >= -90 && $_[0] <= 90 };
2594             my %validate = (
2595             horizon => $plus_or_minus_90,
2596             latitude => $plus_or_minus_90,
2597             longitude => sub {
2598             $_[0] > 360
2599             and return 0;
2600             $_[0] > 180
2601             and $_[0] -= 360;
2602             $_[0] >= -180 && $_[0] <= 180;
2603             },
2604             );
2605             sub _set_angle {
2606 31     31   98 my ( $self, $name, $value ) = @_;
2607 31         139 my $angle = $self->__parse_angle( $value );
2608 31 100       179 if ( my $code = $validate{$name} ) {
2609 15 0       59 defined $angle or $self->weep(
    50          
2610             "$name angle is undef for value ", defined $value ? $value : 'undef' );
2611 15 50       87 $code->( $angle )
2612             or $self->wail( "Value $value is invalid for $name" );
2613             }
2614 31         230 $self->{"_$name"} = deg2rad( $angle );
2615 31         367 return ( $self->{$name} = $angle );
2616             }
2617             }
2618              
2619             sub _set_angle_or_undef {
2620 21     21   65 my ( $self, $name, $value ) = @_;
2621 21 100 66     155 defined $value and 'undef' ne $value and goto &_set_angle;
2622 15         63 return ( $self->{$name} = undef );
2623             }
2624              
2625             sub _set_code_ref {
2626 11 50   11   59 CODE_REF eq ref $_[2]
2627             or $_[0]->wail( "Attribute $_[1] must be a code reference" );
2628 11         61 return( $_[0]{$_[1]} = $_[2] );
2629             }
2630              
2631             # Set an attribute whose value is an Astro::App::Satpass2::Copier object
2632             # %arg is a hash of argument name/value pairs:
2633             # {name} is the required name of the attribute to set;
2634             # {value} is the required value of the attribute to set;
2635             # {class} is the optional class that the object must be;
2636             # {default} is the optional default value if the required value is
2637             # undef or '';
2638             # {undefined} is an optional value which, if true, permits the
2639             # attribute to be set to undef;
2640             # {nocopy} is an optional value which, if true, causes the old
2641             # object's attributes not to be copied to the new object;
2642             # {message} is an optional message to emit if the object can not be
2643             # instantiated;
2644             # {prefix} is an optional reference to an array of name prefixes to
2645             # try if the named module does not load.
2646              
2647             sub _set_copyable {
2648 14     14   118 my ( $self, %arg ) = @_;
2649 14         49 my $old = $self->{$arg{name}};
2650 14         30 my $obj;
2651 14 50       72 if ( ref $arg{value} ) {
2652             blessed( $arg{value} )
2653 0 0       0 or $self->wail( "$arg{name} may not be unblessed reference" );
2654 0         0 $obj = $arg{value};
2655             $obj->can( 'warner' )
2656 0 0       0 and $obj->warner( $self->{_warner} );
2657             } else {
2658 14 50       51 if ( defined $arg{default} ) {
2659             defined $arg{value}
2660             and '' ne $arg{value}
2661 14 50 33     121 or $arg{value} = $arg{default};
2662             }
2663 14 50 33     100 if ( ! defined $arg{value} || $arg{value} eq '' ) {
2664             $arg{undefined}
2665 0 0       0 or $self->wail(
2666             "$arg{name} must be defined and not empty",
2667             );
2668 0         0 return ( $self->{$arg{name}} = $arg{value} = undef );
2669             }
2670 14         92 my ( $pkg, @args ) = $self->__parse_class_and_args( $arg{value} );
2671             my $cls = $self->load_package(
2672 14 50       68 { fatal => 'wail' }, $pkg, @{ $arg{prefix} || [] } );
  14         115  
2673 14 50 33     258 not $cls->can( 'init' )
2674             and _is_case_tolerant()
2675             and $self->wail(
2676             "$cls is missing methods. This can happen on a ",
2677             'case-tolerant system if you specify the class ',
2678             'name in the wrong case.' );
2679 14 100       131 $cls->can( 'parent' )
2680             and push @args, parent => $self;
2681             $obj = $cls->new(
2682             warner => $self->{_warner},
2683             @args,
2684             )
2685             or $self->wail( $arg{message} ||
2686 14 50 0     99 "Can not instantiate object from '$arg{value}'" );
2687             }
2688             defined $arg{class}
2689             and not $obj->isa( $arg{class} )
2690 14 50 66     163 and $self->wail( "$arg{name} must be of class $arg{class}" );
2691             blessed( $old )
2692             and not $arg{nocopy}
2693 14 0 33     61 and $old->can( 'copy' )
      33        
2694             and $old->copy( $obj );
2695 14         65 $self->{$arg{name}} = $obj;
2696 14         181 return $arg{value};
2697             }
2698              
2699             sub _set_distance_meters {
2700 9 100   9   71 return ( $_[0]{$_[1]} = defined $_[2] ?
2701             ( $_[0]->__parse_distance( $_[2], '0m' ) * 1000 ) : $_[2] );
2702             }
2703              
2704             sub _set_ellipsoid {
2705 7     7   27 my ($self, $name, $val) = @_;
2706 7         101 Astro::Coord::ECI->set (ellipsoid => $val);
2707 7         352 return ($self->{$name} = $val);
2708             }
2709              
2710             sub _set_formatter {
2711 7     7   27 my ( $self, $name, $val ) = @_;
2712 7         50 return $self->_set_copyable(
2713             name => $name,
2714             value => $val,
2715             message => 'Unknown formatter',
2716             default => 'Astro::App::Satpass2::Format::Template',
2717             prefix => [ 'Astro::App::Satpass2::Format' ]
2718             );
2719             }
2720              
2721             sub _set_formatter_attribute {
2722 24     24   79 my ( $self, $name, $val ) = @_;
2723 24         112 $self->get( 'formatter' )->$name( $val );
2724 24         89 return $val;
2725             }
2726              
2727             sub _set_geocoder {
2728 0     0   0 my ( $self, $name, $val ) = @_;
2729 0         0 return $self->_set_copyable(
2730             name => $name,
2731             value => $val,
2732             class => 'Astro::App::Satpass2::Geocode',
2733             message => 'Unknown formatter',
2734             default => $default_geocoder->(),
2735             undefined => 1,
2736             nocopy => 1,
2737             prefix => [ 'Astro::App::Satpass2::Geocode' ]
2738             );
2739             }
2740              
2741             sub _set_illum_class {
2742 7     7   28 my ( $self, $name, $class ) = @_;
2743 7         22 my $want_class = 'Astro::Coord::ECI';
2744 7 50       35 ref $class and $self->wail( "$name must not be a reference" );
2745 7 50       26 if ( defined $class ) {
2746 7         70 $self->load_package( { fatal => 'wail' }, $class );
2747 7 50       120 $class->isa( $want_class )
2748             or $self->wail( "$name must be an $want_class" );
2749             } else {
2750 0         0 $class = $want_class;
2751             }
2752 7         49 $self->{$name} = $class;
2753 7         25 $self->{_help_module}{$name} = $class;
2754 7         36 foreach my $body ( @{ $self->{bodies} } ) {
  7         29  
2755 0         0 $body->set( $name => $class );
2756             }
2757 7         71 return;
2758             }
2759              
2760             sub _set_model {
2761 7     7   25 my ( $self, $name, $val ) = @_;
2762 7 50       98 Astro::Coord::ECI::TLE->is_valid_model( $val )
2763             or $self->wail(
2764             "'$val' is not a valid Astro::Coord::ECI::TLE model" );
2765 7         59 foreach my $body ( @{ $self->{bodies} } ) {
  7         34  
2766 0         0 $body->set( model => $val );
2767             }
2768 7         61 return ( $self->{$name} = $val );
2769             }
2770              
2771             sub _set_output_layers {
2772 7     7   64 my ( $self, $name, $val ) = @_;
2773              
2774 7 50 33     57 if ( defined $val && '' ne $val ) {
2775 7 50   7   6129 open my $fh, ">$val", File::Spec->devnull()
  7         140  
  7         44  
  7         514  
2776             or $self->wail( "Invalid $name value '$val'" );
2777 7         10613 close $fh;
2778             }
2779 7         119 return ( $self->{$name} = $val );
2780             }
2781              
2782             {
2783             my %variant_def = (
2784             visible_events => PASS_VARIANT_VISIBLE_EVENTS,
2785             fake_max => PASS_VARIANT_FAKE_MAX,
2786             start_end => PASS_VARIANT_START_END,
2787             no_illumination => PASS_VARIANT_NO_ILLUMINATION,
2788             brightest => PASS_VARIANT_BRIGHTEST,
2789             );
2790              
2791             my @option_names;
2792             foreach my $key ( keys %variant_def ) {
2793             if ( $key =~ m/ _ /smx ) {
2794             ( my $dashed = $key ) =~ s/ _ /-/smxg;
2795             $key = "$key|$dashed";
2796             }
2797             push @option_names, "$key!";
2798             }
2799              
2800             my $go;
2801              
2802             sub _set_pass_variant {
2803 8     8   41 my ( $self, $name, $val ) = @_;
2804 8 100       89 if ( $val =~ m/ \A (?: 0 x? ) [0-9]* \z /smx ) {
    50          
2805 7         26 $val = oct $val;
2806             } elsif ( $val !~ m/ \A [0-9]+ \z /smx ) {
2807 1         27 my @args = split qr{ [^\w-] }smx, $val;
2808 1         6 foreach ( @args ) {
2809 1         11 s/ \A (?! - ) /-/smx;
2810             }
2811 1   33     29 $go ||= Getopt::Long::Parser->new();
2812 1         59 $val = $self->get( $name );
2813             $go->getoptionsfromarray( \@args,
2814 0     0   0 none => sub { $val = PASS_VARIANT_NONE },
2815 1 50       34 map { $_ => sub {
2816 1     1   1553 my ( $name, $value ) = @_;
2817 1         6 my $mask = $variant_def{$name};
2818 1 50       12 if ( $value ) {
2819 0         0 $val |= $mask;
2820             } else {
2821 1         3 $val &= ~ $mask;
2822             }
2823 1         7 return;
2824             }
2825 5         43 } @option_names )
2826             or $self->wail( "Invalid $name value '$val'" );
2827             }
2828 8         145 return ( $self->{$name} = $val );
2829             }
2830              
2831             sub _show_pass_variant {
2832 1     1   4 my ( $self, $name ) = @_;
2833 1         24 my $val = $self->get( $name );
2834 1         3 my @options;
2835 1         16 foreach my $key ( keys %variant_def ) {
2836 5 50       16 $val & $variant_def{$key}
2837             and push @options, "$key";
2838             }
2839             @options
2840 1 50       6 or push @options, 'none';
2841 1         12 return ( set => $name, join ',', @options );
2842             }
2843              
2844             sub want_pass_variant {
2845 138     138 1 17419 my ( $self, $variant ) = @_;
2846 138 50       790 $variant_def{$variant}
2847             or $self->wail( "Invalid pass_variant name '$variant'" );
2848 138         811 my $val = $self->get( 'pass_variant' ) & $variant_def{$variant};
2849 138         647 return $val;
2850             }
2851              
2852             }
2853              
2854             sub _set_spacetrack {
2855 0     0   0 my ($self, $name, $val) = @_;
2856 0 0       0 if (defined $val) {
2857 0 0       0 instance($val, 'Astro::SpaceTrack')
2858             or $self->wail("$name must be an Astro::SpaceTrack instance");
2859 0         0 my $version = $val->VERSION();
2860 0         0 $version =~ s/ _ //smxg;
2861 0 0       0 $version >= ASTRO_SPACETRACK_VERSION
2862             or $self->wail("$name must be Astro::SpaceTrack version ",
2863             ASTRO_SPACETRACK_VERSION, ' or greater' );
2864             }
2865 0         0 return ($self->{$name} = $val);
2866             }
2867              
2868             sub _set_stdout {
2869 15     15   45 my ($self, $name, $val) = @_;
2870             $self->{frame}
2871 15 50       74 and $self->{frame}[-1]{$name} = $val;
2872 15         70 return ($self->{$name} = $val);
2873             }
2874              
2875             sub _set_sun_class {
2876 0     0   0 my ( $self, $name, $val ) = @_;
2877 0         0 $self->_attribute_exists( $name );
2878 0         0 return $self->sky( class => $name, $val );
2879             }
2880              
2881             sub _set_time_parser {
2882 7     7   31 my ( $self, $name, $val ) = @_;
2883              
2884 7 50 33     84 if ( CODE_REF eq ref $val ) {
    50          
2885 0         0 $val = _set_time_parser_code( $val );
2886             } elsif ( defined $val and my $macro = $self->{macro}{$val} ) {
2887 0         0 $val = _set_time_parser_code(
2888             $macro->implements( $val, required => 1 ),
2889             $val,
2890             );
2891             }
2892              
2893 7         81 return $self->_set_copyable(
2894             name => $name,
2895             value => $val,
2896             class => 'Astro::App::Satpass2::ParseTime',
2897             message => 'Unknown time parser',
2898             default => 'Astro::App::Satpass2::ParseTime',
2899             nocopy => 1,
2900             prefix => [ 'Astro::App::Satpass2::ParseTime' ],
2901             );
2902             }
2903              
2904             sub _set_time_parser_attribute {
2905 14     14   62 my ( $self, $name, $val ) = @_;
2906 14 50 66     142 defined $val and $val eq 'undef' and $val = undef;
2907 14         125 $self->{time_parser}->$name( $val );
2908 14         37 return $val;
2909             }
2910              
2911             sub _set_time_parser_code {
2912 0     0   0 my ( $code, $name ) = @_;
2913 0         0 require Astro::App::Satpass2::ParseTime::Code;
2914 0         0 my $obj = Astro::App::Satpass2::ParseTime::Code->new();
2915 0         0 return $obj->code( $code, $name );
2916             }
2917              
2918             _frame_pop_force_set ( 'twilight' ); # Force use of the set() method
2919             # in _frame_pop(), because we
2920             # need to set {_twilight} as
2921             # well.
2922             sub _set_twilight {
2923 9     9   45 my ($self, $name, $val) = @_;
2924 9 50       65 if (my $key = $twilight_abbr{lc $val}) {
2925 9         53 $self->{$name} = $key;
2926 9         46 $self->{_twilight} = $twilight_def{$key};
2927             } else {
2928 0         0 my $angle = $self->__parse_angle( { accept => 1 }, $val );
2929 0 0       0 looks_like_number( $angle )
2930             or $self->wail( 'Twilight must be number or known keyword' );
2931 0         0 $self->{$name} = $val;
2932 0         0 $self->{_twilight} = deg2rad ($angle);
2933             }
2934 9         37 return $val;
2935             }
2936              
2937             sub _set_tz {
2938 7     7   24 my ( $self, $name, $val ) = @_;
2939 7         40 $self->_set_formatter_attribute( $name, $val );
2940 7         61 $self->_set_time_parser_attribute( $name, $val );
2941 7         45 return $val;
2942             }
2943              
2944             sub _set_unmodified {
2945 165     165   708 return ($_[0]{$_[1]} = $_[2]);
2946             }
2947              
2948             sub _set_warner_attribute {
2949 0     0   0 my ( $self, $name, $val ) = @_;
2950 0 0 0     0 defined $val and $val eq 'undef' and $val = undef;
2951 0         0 $self->{_warner}->$name( $val );
2952 0         0 return $val;
2953             }
2954              
2955             sub _set_webcmd {
2956 7     7   29 my ($self, $name, $val) = @_;
2957             # TODO warn if $val is true but not '1'.
2958 7 50       40 if ( my $st = $self->get( 'spacetrack' ) ) {
2959             # TODO once spacetrack supports '1', just pass $val.
2960 0         0 $st->set( webcmd => $self->_get_browser_command( $val ) );
2961             }
2962 7         38 return ($self->{$name} = $val);
2963             }
2964              
2965             sub show : Verb( changes! deprecated! readonly! ) Tweak( -completion _readline_complete_subcommand ) {
2966 23     23 1 137 my ( $self, $opt, @args ) = __arguments( @_ );
2967              
2968 23         113 foreach my $name ( qw{ deprecated readonly } ) {
2969 46 50       204 exists $opt->{$name} or $opt->{$name} = 1;
2970             }
2971 23         47 my $output;
2972              
2973 23 50       101 unless ( @args ) {
2974 0         0 foreach my $name ( sort keys %accessor ) {
2975 0 0       0 $self->_attribute_exists( $name, query => 1 )
2976             or next;
2977 0 0       0 $nointeractive{$name}
2978             and next;
2979             exists $mutator{$name}
2980             or $opt->{readonly}
2981 0 0 0     0 or next;
2982 0         0 my $depr;
2983             ( $depr = $self->_deprecation_in_progress( attribute =>
2984             $name ) )
2985 0 0 0     0 and ( not $opt->{deprecated} or $depr >= 3 )
      0        
2986             and next;
2987 0         0 push @args, $name;
2988             }
2989             }
2990              
2991 23         69 foreach my $name (@args) {
2992 23 50       96 exists $shower{$name}
2993             or $self->wail("No such attribute as '$name'");
2994              
2995 23         174 my @val = $shower{$name}->( $self, $name );
2996 23 50       84 if ( $opt->{changes} ) {
2997 20     20   90126 no warnings qw{ uninitialized };
  20         50  
  20         4288  
2998 0 0       0 $static{$name} eq $val[-1] and next;
2999             }
3000              
3001 23 50       73 exists $mutator{$name} or unshift @val, '#';
3002 23         184 $output .= quoter( @val ) . "\n";
3003             }
3004 23         93 return $output;
3005 20     20   155 }
  20         48  
  20         159  
3006              
3007             sub _show_copyable {
3008 0     0   0 my ( $self, $name ) = @_;
3009 0         0 my $obj = $self->get( $name );
3010 0         0 my $val = $obj->class_name_of_record();
3011 0         0 return ( 'set', $name, $val );
3012             }
3013              
3014             sub _show_formatter_attribute {
3015 2     2   5 my ( $self, $name ) = @_;
3016 2         10 my $val = $self->{formatter}->decode( $name );
3017 2         7 return ( qw{ formatter }, $name, $val );
3018             }
3019              
3020             # Calls to the following _show_sub method are generated dynamically
3021             # above, so there is no way Perl::Critic can find them.
3022              
3023             sub _show_sub { ## no critic (ProhibitUnusedPrivateSubroutines)
3024             # my ( $app, $text, $line, $start, @arg ) = @_;
3025 0     0   0 my ( undef, undef, undef, undef, @arg ) = @_;
3026 0 0       0 @arg > 1
3027             or return;
3028 0         0 my $re = qr/ \A \Q$arg[-1]\E /smx;
3029 0         0 return [ grep { $_ =~ $re } sort keys %accessor ];
  0         0  
3030             }
3031              
3032             sub _show_sun_class {
3033 0     0   0 my ( $self, $name ) = @_;
3034 0         0 $self->_attribute_exists( $name );
3035 0         0 return $self->_sky_class_components( $name );
3036             }
3037              
3038             sub _show_time_parser {
3039 0     0   0 my ( $self, $name ) = @_;
3040 0         0 my $obj = $self->get( $name );
3041 0         0 my $val = $obj->class_name_of_record();
3042 0 0       0 if ( my $back_end = $obj->back_end() ) {
3043 0         0 $val = "$val,back_end=$back_end";
3044             }
3045 0         0 return ( set => $name, $val );
3046             }
3047              
3048             sub _show_unmodified {
3049 20     20   57 my ($self, $name) = @_;
3050 20         91 my $val = $self->get( $name );
3051 20         120 return ( 'set', $name, $val );
3052             }
3053              
3054             # For proper motion, we need to convert arc seconds per year to degrees
3055             # per second. Perl::Critic does not like 'use constant' because they do
3056             # not interpolate, but they really do: "@{[SPY2DPS]}".
3057              
3058 20     20   16412 use constant SPY2DPS => 3600 * 365.24219 * SECSPERDAY;
  20         89  
  20         10825  
3059              
3060             # Given a body in the sky, encodes it in 'sky add' format
3061             sub _sky_list_body {
3062 8     8   23 my ( $body ) = @_;
3063 8 50       35 if ( embodies( $body, 'Astro::Coord::ECI::TLE' ) ) {
    100          
3064 0         0 return sprintf "sky tle %s\n", quoter(
3065             $body->get( 'tle' ) );
3066             } elsif ( $body->isa( 'Astro::Coord::ECI::Star' ) ) {
3067 1         109 my ( $ra, $dec, $rng, $pmra, $pmdec, $vr ) = $body->position();
3068 1         15 $rng /= PARSEC;
3069 1         23 $pmra = rad2deg( $pmra / 24 * 360 * cos( $ra ) ) * SPY2DPS;
3070 1         9 $pmdec = rad2deg( $pmdec ) * SPY2DPS;
3071 1         9 return sprintf
3072             "sky add %s %s %7.3f %.2f %.4f %.5f %s\n",
3073             quoter( $body->get( 'name' ) ), _rad2hms( $ra ),
3074             rad2deg( $dec ), $rng, $pmra, $pmdec, $vr;
3075             } else {
3076 7         290 return sprintf "sky add %s\n", quoter( $body->get( 'name' ) );
3077             }
3078             }
3079              
3080             sub sky : Verb() Tweak( -completion _readline_complete_subcommand ) {
3081 12     12 1 53 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3082              
3083 12   50     93 my $verb = lc ( shift @args || 'list' );
3084              
3085 12 50       1356 if ( my $code = $self->can( "_sky_sub_$verb") ) {
3086 12         117 return $code->( $self, @args );
3087             } else {
3088 0         0 $self->wail("'sky' subcommand '$verb' not known");
3089             }
3090 0         0 return; # We can't get here, but Perl::Critic does not know this.
3091 20     20   172 }
  20         176  
  20         143  
3092              
3093             # Given the name of a potential background object, return its
3094             # definition. This is an array in list context, or a quoted string in
3095             # scalar context.
3096             sub _sky_class_components {
3097 0     0   0 my ( $self, $name ) = @_;
3098 0 0       0 my $info = $self->{sky_class}{ fold_case( $name ) }
3099             or $self->weep( "No class defined for $name" );
3100 0         0 my ( $class, @attr ) = @{ $info };
  0         0  
3101             # We rely on sky( class => $name, $class, ... ) keeping the name
3102             # last.
3103 0         0 $name = pop @attr;
3104 0         0 pop @attr; # 'name';
3105 0         0 my @parts = ( qw{ sky class }, $name, $class, @attr );
3106             wantarray
3107 0 0       0 and return @parts;
3108 0         0 return join ' ', map { quoter( $_ ) } @parts;
  0         0  
3109             }
3110              
3111             # Given the name of a potential sky object, instantiate it. Named
3112             # arguments are optional; the following are supported:
3113             # fatal = Whether failure to find the name is fatal. Default is true.
3114             sub _sky_object {
3115 12     12   57 my ( $self, $name, %opt ) = @_;
3116             defined $opt{fatal}
3117 12 100       63 or $opt{fatal} = 1;
3118 12 100       92 if ( my $info = $self->{sky_class}{ fold_case( $name ) } ) {
    50          
3119 10         24 my ( $class, @attr ) = @{ $info };
  10         120  
3120 10         162 return $class->new( @attr );
3121             } elsif ( $opt{fatal} ) {
3122 0         0 $self->weep( "No class defined for $name" );
3123             }
3124 2         8 return;
3125             }
3126              
3127             # Calls to the following _sky_sub_... methods are generated dynamically
3128             # above, so there is no way Perl::Critic can find them.
3129             #
3130             sub _sky_sub_add : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3131 5     5   27 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3132 5 50       31 my $name = shift @args
3133             or $self->wail( 'You did not specify what to add' );
3134 5 50       40 defined $self->_find_in_sky( $name )
3135             and return;
3136 5 100       25 if ( my $obj = $self->_sky_object( $name, fatal => 0 ) ) {
3137 3         323 push @{ $self->{sky} }, $obj;
  3         9  
3138             } else {
3139 2 100       30 @args >= 2
3140             or $self->wail(
3141             'You must give at least right ascension and declination' );
3142 1         17 my $ra = deg2rad( $self->__parse_angle( shift @args ) );
3143 1         16 my $dec = deg2rad( $self->__parse_angle( shift @args ) );
3144 1 50       29 my $rng = @args ?
3145             $self->__parse_distance( shift @args, '1pc' ) :
3146             10000 * PARSEC;
3147 1 50       13 my $pmra = @args ? do {
3148 1         8 my $angle = shift @args;
3149 1 50       18 $angle =~ s/ s \z //smxi
3150             or $angle *= 24 / 360 / cos( $ra );
3151 1         10 deg2rad( $angle / SPY2DPS );
3152             } : 0;
3153 1 50       12 my $pmdec = @args ? deg2rad( shift( @args ) / SPY2DPS ) : 0;
3154 1 50       10 my $pmrec = @args ? shift @args : 0;
3155 1         8 push @{ $self->{sky} }, Astro::Coord::ECI::Star->new(
3156             debug => $self->{debug},
3157 1         6 name => $name,
3158             sun => $self->_sky_object( 'sun' ),
3159             )->position( $ra, $dec, $rng, $pmra, $pmdec, $pmrec );
3160             }
3161 4         2475 return;
3162 20     20   21873 }
  20         145  
  20         221  
3163              
3164             sub _sky_sub_class : Verb( add! delete! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3165 0     0   0 my ( $self, $opt, @arg ) = __arguments( @_ );
3166              
3167             $opt->{add}
3168             and $opt->{delete}
3169 0 0 0     0 and $self->wail( 'May not specify both add and delete' );
3170              
3171 0 0       0 if ( $opt->{delete} ) {
    0          
3172 0         0 foreach my $name ( @arg ) {
3173 0 0       0 $name =~ m/ \A sun \z /smxi
3174             and $self->wail( 'Can not remove Sun class' );
3175 0 0       0 defined $self->_find_in_sky( $name )
3176             and $self->wail( 'Can not remove in-use class' );
3177 0         0 delete $self->{sky_class}{ fold_case( $name ) };
3178             }
3179             } elsif ( @arg < 2 ) {
3180             @arg
3181 0 0       0 or @arg = sort keys %{ $self->{sky_class} };
  0         0  
3182             return join '', map {
3183 0         0 $self->_sky_class_components( $_ ) . "\n" }
  0         0  
3184             @arg;
3185             } else {
3186 0         0 my ( $name, $class, @attr ) = @arg;
3187 0         0 $self->load_package( { fatal => 'wail' }, $class );
3188 0 0       0 my $want_class = $name =~ m/ \A sun \z /smxi ?
3189             SUN_CLASS_DEFAULT :
3190             'Astro::Coord::ECI';
3191 0 0       0 embodies( $class, $want_class )
3192             or $self->wail(
3193             "Must be a subclass of $want_class" );
3194             +{ @attr }->{name}
3195 0 0       0 and $self->wail( 'May not specify name explicitly' );
3196             # name must be last, because _sky_class_components()
3197             # needs to recover it.
3198 0         0 push @attr, name => $name;
3199 0         0 my $obj = $class->new( @attr );
3200 0         0 my $folded_name = fold_case( $name );
3201 0         0 $self->{sky_class}{$folded_name} = [ $class, @attr ];
3202             $self->_replace_in_sky( $folded_name, $obj )
3203             or $opt->{add}
3204 0 0 0     0 and push @{ $self->{sky} }, $obj;
  0         0  
3205 0         0 $self->{_help_module}{$folded_name} = $class;
3206 0 0       0 if ( $obj->isa( 'Astro::Coord::ECI::Sun' ) ) {
3207 0         0 foreach my $body (
3208 0         0 @{ $self->{bodies} }, @{ $self->{sky} }
  0         0  
3209             ) {
3210 0         0 $body->set(
3211             sun => $self->_sky_object( 'sun' ),
3212             );
3213             }
3214             }
3215             }
3216              
3217 0         0 return;
3218 20     20   16740 }
  20         57  
  20         132  
3219              
3220             sub _sky_sub_clear : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3221 1     1   7 my ( $self ) = __arguments( @_ ); # $opt and args unused
3222 1         5 @{ $self->{sky} } = ();
  1         14  
3223 1         5 return;
3224 20     20   6254 }
  20         48  
  20         115  
3225              
3226             sub _sky_sub_drop : Verb() Tweak( -completion _sky_body_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3227 1     1   27 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3228 1 50       6 @args or $self->wail(
3229             'You must specify at least one name to drop' );
3230 1         13 foreach my $name ( @args ) {
3231 1         9 $self->_drop_from_sky( $name );
3232             }
3233 1         6 return;
3234 20     20   7036 }
  20         83  
  20         127  
3235              
3236             sub _sky_sub_list : Verb( verbose! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3237 5     5   22 my ( $self, $opt ) = __arguments( @_ ); # args unused
3238 5         18 my $output;
3239 5         42 foreach my $body (
3240 8         68 map { $_->[1] }
3241 4         90 sort { $a->[0] cmp $b->[0] }
3242 8   33     225 map { [ lc( $_->get( 'name' ) || $_->get( 'id' ) ), $_ ] }
3243 5         31 @{$self->{sky}}
3244             ) {
3245 8         60 $output .= _sky_list_body( $body );
3246 8 50       73 if ( $opt->{verbose} ) {
3247 0         0 $output .= "# Class: @{[ ref $body ]}\n";
  0         0  
3248             }
3249             }
3250 5 100       22 unless (@{$self->{sky}}) {
  5         25  
3251             $self->{warn_on_empty}
3252 1 50       7 and $self->whinge( 'The sky is empty' );
3253             }
3254 5         24 return $output;
3255 20     20   11607 }
  20         58  
  20         171  
3256              
3257             # Undocumented. That means I can revoke at any time, without notice. If
3258             # you need this functionality, please contact me.
3259             sub _sky_sub_load : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3260 0     0   0 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3261 0         0 my $tle;
3262 0         0 foreach my $fn ( @args ) {
3263 0         0 local $/ = undef;
3264 0 0       0 open my $fh, '<', $fn
3265             or $self->wail( "Failed to open $fn: $!" );
3266 0         0 $tle .= <$fh>;
3267 0         0 close $fh;
3268             }
3269 0         0 return $self->_sky_sub_tle( $tle );
3270 20     20   8316 }
  20         58  
  20         142  
3271              
3272             sub _sky_sub_lookup : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3273 0     0   0 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3274 0         0 my $output;
3275 0         0 my $name = shift @args;
3276 0 0       0 defined $self->_find_in_sky( $name )
3277             and $self->wail( "Duplicate sky entry '$name'" );
3278 0         0 my ($ra, $dec, $rng, $pmra, $pmdec, $pmrec) =
3279             $self->_simbad4 ($name);
3280 0         0 $rng = sprintf '%.2f', $rng;
3281 0         0 $output .= 'sky add ' . quoter ($name) .
3282             " $ra $dec $rng $pmra $pmdec $pmrec\n";
3283 0         0 $ra = deg2rad ($self->__parse_angle ($ra));
3284 0         0 my $body = Astro::Coord::ECI::Star->new(
3285             name => $name,
3286             sun => $self->_sky_object( 'sun' ),
3287             );
3288 0         0 $body->position ($ra, deg2rad ($self->__parse_angle ($dec)),
3289             $rng * PARSEC, deg2rad ($pmra * 24 / 360 / cos ($ra) / SPY2DPS),
3290             deg2rad ($pmdec / SPY2DPS), $pmrec);
3291 0         0 push @{$self->{sky}}, $body;
  0         0  
3292 0         0 return $output;
3293 20     20   11745 }
  20         166  
  20         607  
3294              
3295             # Undocumented. That means I can revoke at any time, without notice. If
3296             # you need this functionality, please contact me.
3297             sub _sky_sub_tle : Verb() {
3298 0     0   0 my ( $self, undef, $tle ) = __arguments( @_ ); # $opt unused
3299 0         0 my @bodies = Astro::Coord::ECI::TLE::Set->aggregate(
3300             Astro::Coord::ECI::TLE->parse( $tle ) );
3301 0         0 my %extant = map { $_->get( 'id' ) => 1 }
3302 0         0 grep { embodies( $_, 'Astro::Coord::ECI::TLE' ) }
3303 0         0 @{ $self->{sky} };
  0         0  
3304 0         0 foreach my $body ( @bodies ) {
3305 0         0 my $id = $body->get( 'id' );
3306 0 0       0 $extant{$id}
3307             and $self->wail( "Duplicate sky entry $id" );
3308             }
3309 0         0 push @{ $self->{sky} }, @bodies;
  0         0  
3310 0         0 return sprintf "sky tle %s\n", quoter( $tle );
3311 20     20   10381 }
  20         59  
  20         135  
3312              
3313             sub source : Verb( optional! ) {
3314 8     8 1 36 my ( $self, $opt, $src, @args ) = __arguments( @_ );
3315              
3316 8         20 my $output;
3317 8 100       38 my $reader = $self->_file_reader( $src, $opt )
3318             or return;
3319              
3320 6         11 my @level1_cache;
3321 6         12 my $level1_context = {};
3322             my $fetcher = $opt->{level1} ? sub {
3323             @level1_cache
3324 21 100   21   47 and return shift @level1_cache;
3325 19         35 my $buffer = $reader->();
3326 19         72 @level1_cache = $self->_rewrite_level1_command(
3327             $buffer, $level1_context );
3328 19         57 return shift @level1_cache;
3329 6 100       45 } : $reader;
3330              
3331 6         72 my $frames = $self->_frame_push( source => \@args );
3332             # Note that level1 is unsupported, and works only when the
3333             # options are passed as a hash. It will go away when support for
3334             # the original satpass script is dropped.
3335 6         36 $self->{frame}[-1]{level1} = $opt->{level1};
3336 6         21 my $err;
3337 6 50       15 my $ok = eval { while ( defined( my $input = $fetcher->() ) ) {
  6         24  
3338 13 100       86 if ( defined ( my $buffer = $self->execute( $fetcher,
3339             $input ) ) ) {
3340 2         11 $output .= $buffer;
3341             }
3342             }
3343 6         24 1;
3344             } or $err = $@;
3345              
3346 6         28 $self->_frame_pop( $frames );
3347 6 50       16 $ok or $self->whinge( $err );
3348              
3349 6 100       36 $opt->{level1} and $self->_rewrite_level1_macros();
3350 6         143 return $output;
3351 20     20   12303 }
  20         45  
  20         107  
3352              
3353             {
3354              
3355             my %handler = (
3356             config => sub {
3357             my ( $self, $obj, undef, $opt, @args ) = @_; # $method unused
3358             @args or @args = $obj->attribute_names();
3359             my ( $rslt, @values, $virgin );
3360             $opt->{changes}
3361             and $virgin = $self->_get_spacetrack_default();
3362             foreach my $name ( @args ) {
3363             $rslt = $obj->get( $name );
3364             $rslt->is_success()
3365             or return $rslt;
3366             my $value = $rslt->content();
3367 20     20   7125 no warnings qw{ uninitialized };
  20         46  
  20         20751  
3368             $opt->{changes}
3369             and $value eq $virgin->getv( $name )
3370             and next;
3371             push @values, [ $name, $value ];
3372             }
3373             if ( $opt->{raw} ) {
3374             $rslt->content( \@values );
3375             } else {
3376             $opt->{raw} and return \@values;
3377             my $output = '';
3378             foreach ( @values ) {
3379             $output .= quoter( qw{ spacetrack set }, @{ $_ } ) . "\n";
3380             }
3381             $rslt->content( $output );
3382             }
3383             return $rslt;
3384             },
3385             get => sub {
3386             my ( undef, $obj, undef, $opt, @args ) = @_; # Invocant, $method unused
3387             my $rslt = $obj->get( @args );
3388             $rslt->is_success
3389             and not $opt->{raw}
3390             and $rslt->content( scalar quoter(
3391             qw{ spacetrack set }, $args[0], $rslt->content() ) );
3392             return $rslt;
3393             },
3394             set => sub {
3395             my ( undef, $obj, $method, undef, @args ) = @_; # Invocant, $opt unused
3396             return $obj->$method( @args );
3397             },
3398             );
3399             $handler{getv} = $handler{get};
3400             $handler{show} = $handler{config};
3401             $handler{spacetrack_query_v2} = $handler{set};
3402              
3403             my %suppress_output = map { $_ => 1 } '', 'set';
3404              
3405             # Attributes must all be on one line to process correctly under
3406             # 5.8.8.
3407             sub spacetrack : Verb( all! changes! descending! effective! end_epoch=s exclude=s last5! raw! rcs! status=s sort=s start_epoch=s tle! verbose! ) {
3408 0     0 1 0 my ( $self, $opt, $method, @args ) = __arguments( @_ );
3409              
3410             exists $opt->{raw}
3411 0 0       0 or $opt->{raw} = ( ! _is_interactive() );
3412              
3413 0         0 my $verbose = delete $opt->{verbose};
3414              
3415 0         0 my $object = $self->_helper_get_object( 'spacetrack' );
3416             $method !~ m/ \A _ /smx and $object->can( $method )
3417 0 0 0     0 or $handler{$method}
      0        
3418             or $self->wail("No such spacetrack method as '$method'");
3419              
3420             $opt->{start_epoch}
3421             and $opt->{start_epoch} = $self->__parse_time(
3422 0 0       0 $opt->{start_epoch} );
3423             $opt->{end_epoch}
3424             and $opt->{end_epoch} = $self->__parse_time(
3425 0 0       0 $opt->{end_epoch} );
3426              
3427 0         0 my ( $rslt, @rest );
3428 0 0       0 if ( $handler{$method} ) {
3429 0         0 ( $rslt, @rest ) = $handler{$method}->(
3430             $self, $object, $method, $opt, @args );
3431             } else {
3432 0         0 delete $opt->{raw};
3433 0         0 ( $rslt, @rest ) = $object->$method( $opt, @args );
3434             }
3435              
3436 0 0       0 $rslt->is_success()
3437             or $self->wail( $rslt->status_line() );
3438              
3439 0         0 my $output;
3440 0   0     0 my $content_type = $object->content_type || '';
3441              
3442 0 0 0     0 if ($content_type eq 'orbit') {
    0          
    0          
3443              
3444 0         0 push @{$self->{bodies}},
  0         0  
3445             Astro::Coord::ECI::TLE->parse ($rslt->content);
3446 0 0       0 $verbose
3447             and $output .= $rslt->content;
3448              
3449             } elsif ($content_type eq 'iridium-status') {
3450              
3451 0         0 $self->_iridium_status( @rest );
3452 0 0       0 $verbose
3453             and $output .= $rslt->content;
3454              
3455             } elsif ( ! $suppress_output{$content_type} || $verbose ) {
3456              
3457 0         0 $output .= $rslt->content;
3458              
3459             }
3460              
3461 0 0       0 defined $output
3462             and $output =~ s/ (?
3463 0         0 return $output;
3464 20     20   175 }
  20         46  
  20         135  
3465              
3466             }
3467              
3468             sub st : Verb() {
3469 0     0 1 0 my ( $self, undef, $func, @args ) = __arguments( @_ ); # $opt unused
3470              
3471 0         0 $self->_deprecation_notice( method => 'st' );
3472 0 0       0 if ( 'localize' eq $func ) {
3473 0         0 my $st = $self->_helper_get_object( 'spacetrack' );
3474 0         0 foreach my $key (@args) {
3475             exists $self->{frame}[-1]{spacetrack}{$key}
3476 0 0       0 or $self->{frame}[-1]{spacetrack}{$key} =
3477             $st->get ($key)->content
3478             }
3479             } else {
3480 0         0 goto &spacetrack;
3481             }
3482 0         0 return;
3483 20     20   9413 }
  20         58  
  20         111  
3484              
3485             sub station {
3486 34     34 1 139 my ( $self ) = @_;
3487              
3488             defined $self->{height}
3489             and defined $self->{latitude}
3490             and defined $self->{longitude}
3491 34 50 33     515 or $self->wail( 'You must set height, latitude, and longitude' );
      33        
3492              
3493             return Astro::Coord::ECI->new (
3494             almanac_horizon => $self->{_almanac_horizon},
3495             horizon => deg2rad( $self->get( 'horizon' ) ),
3496             id => 'station',
3497             name => $self->{location} || '',
3498             refraction => $self->{refraction} || 0,
3499             )->geodetic (
3500             deg2rad( $self->{latitude} ),
3501             deg2rad( $self->{longitude} ),
3502 34   100     207 $self->{height} / 1000
      50        
3503             );
3504             }
3505              
3506             # TODO I must have thought -reload would be good for something, but it
3507             # appears I never implemented it.
3508              
3509             sub status : Verb( name! reload! ) {
3510 3     3 1 26 my ( $self, $opt, @args ) = __arguments( @_ );
3511              
3512 3 100       26 @args or @args = qw{show};
3513              
3514 3   50     14 my $verb = lc (shift (@args) || 'show');
3515              
3516 3 50       29 if ( $verb eq 'iridium' ) {
3517 0         0 $self->_deprecation_notice( status => 'iridium', 'show' );
3518 0         0 $verb = 'show';
3519             }
3520              
3521 3         19 my $output;
3522              
3523 3 100 66     51 if ($verb eq 'add' || $verb eq 'drop') {
    100 33        
    50          
3524              
3525 1         54 Astro::Coord::ECI::TLE->status ($verb, @args);
3526 1         85 foreach my $tle (@{$self->{bodies}}) {
  1         12  
3527 1 50       13 $tle->get ('id') == $args[0] and $tle->rebless ();
3528             }
3529              
3530             } elsif ($verb eq 'clear') {
3531              
3532 1         36 Astro::Coord::ECI::TLE->status ($verb, @args);
3533 1         37 foreach my $tle (@{$self->{bodies}}) {
  1         11  
3534 2         184 $tle->rebless ();
3535             }
3536              
3537             } elsif ($verb eq 'show' || $verb eq 'list') {
3538              
3539 1         8 my @data = Astro::Coord::ECI::TLE->status( 'show', @args );
3540 1 50       20 @data = sort {$a->[3] cmp $b->[3]} @data if $opt->{name};
  0         0  
3541 1         4 $output .= ''; # Don't want it to be undef.
3542              
3543             my $encoder = ( HAVE_TLE_IRIDIUM &&
3544             Astro::Coord::ECI::TLE::Iridium->can(
3545 1     0   12 '__encode_operational_status' ) ) || sub { return $_[2] };
  0         0  
3546              
3547 1         6 foreach my $tle (@data) {
3548 0         0 my $status = $encoder->( undef, status => $tle->[2] );
3549 0         0 $output .= quoter( 'status', 'add',
3550             $tle->[0], $tle->[1], $status,
3551             $tle->[3], $tle->[4] ) . "\n";
3552             }
3553              
3554             } else {
3555 0         0 $output .= ''; # Don't want it to be undef.
3556 0         0 $output .= Astro::Coord::ECI::TLE->status ($verb, @args);
3557             }
3558              
3559 3         399 return $output;
3560              
3561 20     20   19108 }
  20         54  
  20         123  
3562              
3563             sub system : method Verb() { ## no critic (ProhibitBuiltInHomonyms)
3564 4     4 1 20 my ( $self, undef, $verb, @args ) = __arguments( @_ ); # $opt unused
3565              
3566             @args = map {
3567 4         16 bsd_glob( $_, GLOB_NOCHECK | GLOB_BRACE | GLOB_QUOTE )
  8         475  
3568             } @args;
3569 4         22 my $stdout = $self->{frame}[-1]{localout};
3570 4         9 my @exported = keys %{ $self->{exported} };
  4         29  
3571 4         10 local @ENV{@exported} = map { $mutator{$_} ? $self->get( $_ ) :
3572 5 100       84 $self->{exported}{$_} } @exported;
3573 4 50 33     22 if ( defined $stdout && -t $stdout ) {
3574 0         0 CORE::system {$verb} $verb, @args;
  0         0  
3575 0         0 return;
3576             } else {
3577 4         77 $self->load_package( { fatal => 'wail' }, 'IPC::System::Simple' );
3578 4         32 return IPC::System::Simple::capturex( $verb, @args );
3579             }
3580 20     20   11036 }
  20         51  
  20         112  
3581              
3582             sub time : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms,RequireArgUnpacking)
3583 1 50   1 1 15 my ($self, @args) = map { ARRAY_REF eq ref $_ ? @{ $_ } : $_ } @_;
  2         54  
  0         0  
3584 1 50       27 $have_time_hires->() or $self->wail( 'Time::HiRes not available' );
3585 1         10 $self->_dispatch_check( time => $args[0] );
3586 1         6 my $start = Time::HiRes::time();
3587             # If we're inside an unsatisfied if() we do not do the timing,
3588             # because dispatch() is probably a no-op.
3589             $self->_in_unsatisfied_if()
3590             or $self->_add_post_dispatch(
3591             sub {
3592 1     1   7 return sprintf "%.3f seconds\n", Time::HiRes::time() - $start;
3593             },
3594 1 50       7 );
3595 1         6 return $self->dispatch( @args );
3596 20     20   9228 }
  20         52  
  20         112  
3597              
3598             sub time_parser : Verb() {
3599 0 0   0 1 0 splice @_, ( HASH_REF eq ref $_[1] ? 2 : 1 ), 0, 'time_parser';
3600 0         0 goto &_helper_handler;
3601 20     20   7036 }
  20         60  
  20         135  
3602              
3603             sub tle : Verb( :compute __tle_options ) {
3604 4     4 1 34 my ( $self, $opt, @args ) = __arguments( @_ );
3605             @args
3606             and not $opt->{choose}
3607 4 50 33     19 and $opt->{choose} = \@args;
3608              
3609 4         29 my $bodies = $self->__choose( $opt->{choose}, $self->{bodies} );
3610 4         10 @{ $bodies } = map { $_->[0] }
  5         139  
3611 1 50       47 sort { $a->[1] <=> $b->[1] || $a->[2] <=> $b->[2] }
3612 5         60 map { [ $_, $_->get( 'id' ), $_->get( 'epoch' ) ] }
3613 4         13 @{ $bodies };
  4         10  
3614 4         13 my $tplt_name = delete $opt->{_template};
3615 4         23 return $self->__format_data( $tplt_name => $bodies, $opt );
3616 20     20   10490 }
  20         58  
  20         123  
3617              
3618             sub __tle_options {
3619 4     4   12 my ( $self, $opt ) = @_;
3620             return [
3621 4         23 qw{ choose=s@ },
3622             $self->_templates_to_options( tle => $opt ),
3623             ];
3624             }
3625              
3626             sub unexport : Verb() {
3627 1     1 1 7 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3628              
3629 1         5 foreach my $name ( @args ) {
3630 1         5 delete $self->{exported}{$name};
3631             }
3632 1         5 return;
3633 20     20   8508 }
  20         61  
  20         124  
3634              
3635             sub validate : Verb( quiet! ) {
3636 1     1 1 13 my ( $self, $opt, @args ) = __arguments( @_ );
3637              
3638 1         9 my $pass_start = $self->__parse_time (
3639             shift @args, $self->_get_day_noon());
3640 1   50     24 my $pass_end = $self->__parse_time (shift @args || '+7');
3641 1 50       7 $pass_start >= $pass_end
3642             and $self->wail( 'End time must be after start time' );
3643              
3644 1 50       13 @{ $self->{bodies} }
  1         8  
3645             or $self->wail( 'No bodies selected' );
3646              
3647             # Validate each body.
3648              
3649 1         3 my @valid;
3650 1         6 foreach my $tle ( $self->_aggregate( $self->{bodies} ) ) {
3651 2 100       2092 $tle->validate( $opt, $pass_start, $pass_end )
3652             and push @valid, $tle->members();
3653             }
3654              
3655 1         980 $self->{bodies} = \@valid;
3656              
3657 1         6 return;
3658 20     20   8900 }
  20         66  
  20         138  
3659              
3660             sub version : Verb() {
3661 0     0 1 0 return <<"EOD";
3662              
3663 0         0 @{[__PACKAGE__]} $VERSION - Satellite pass predictor
3664 0         0 based on Astro::Coord::ECI @{[Astro::Coord::ECI->VERSION]}
3665             Copyright (C) 2009-2025 by Thomas R. Wyant, III
3666              
3667             EOD
3668 20     20   6729 }
  20         46  
  20         141  
3669              
3670             ########################################################################
3671              
3672             # $self->_add_post_dispatch( $code_ref );
3673              
3674             # Add a reference to code to be executed after the current interactive
3675             # method is dispatched. All such code is executed, in the reverse of
3676             # the order it was added. The only argument will be the invocant.
3677             # Because it is added to the current execution frame, if the
3678             # interactive method being dispatched is begin(), the code will be
3679             # executed after the corresponding end(). Code to make the execution
3680             # happen is, of course, in dispatch().
3681             sub _add_post_dispatch {
3682 23     23   65 my ( $self, $code ) = @_;
3683 23   50     37 push @{ $self->{frame}[-1]{post_dispatch} ||= [] }, $code;
  23         166  
3684 23         52 return;
3685             }
3686              
3687             # $self->_aggregate( $list_ref );
3688              
3689             sub __add_to_observing_list {
3690 5     5   15949 my ( $self, @args ) = @_;
3691 5         30 foreach my $body ( @args ) {
3692 10 50       241 embodies( $body, 'Astro::Coord::ECI::TLE' )
3693             and next;
3694 0         0 my $id = $body->get( 'id' );
3695 0 0       0 defined $id
3696             or $id = $body->get( 'name' );
3697 0         0 $self->wail( "Body $id is not a TLE" );
3698             }
3699 5         129 push @{ $self->{bodies} }, @args;
  5         27  
3700 5         21 return $self;
3701             }
3702              
3703             # This is just a wrapper for
3704             # Astro::Coord::ECI::TLE::Set->aggregate.
3705              
3706             sub _aggregate {
3707 27     27   102 my ( $self, $bodies ) = @_;
3708 27         143 local $Astro::Coord::ECI::TLE::Set::Singleton = $self->{singleton};
3709 27         67 return Astro::Coord::ECI::TLE::Set->aggregate ( @{ $bodies } );
  27         384  
3710             }
3711              
3712             # _apply_boolean_default( \%opt, $invert, @keys );
3713             #
3714             # This subroutine defaults a set of boolean options. The keys in
3715             # the set are specified in @keys, and the defined values are
3716             # inverted before the defaults are applied if $invert is true.
3717             # Nothing is returned.
3718              
3719             sub _apply_boolean_default {
3720 44     44   201 my ( $self, $opt, $invert, @keys ) = @_;
3721 44         126 my $state = my $found = 0;
3722 44         122 foreach my $key ( @keys ) {
3723 136 100       347 if ( exists $opt->{$key} ) {
3724 8         18 $found++;
3725             $invert
3726 8 50       30 and $opt->{$key} = ( ! $opt->{$key} );
3727 8 100       35 $state |= ( $opt->{$key} ? 2 : 1 );
3728             }
3729             }
3730             1 == $state # Only negated options found
3731             and @keys == $found # All options in group were specified
3732             and $self->wail( 'May not negate all of ' . join ', ', map {
3733 44 50 66     185 "-$_" } @keys );
  0         0  
3734 44         88 my $default = $state < 2;
3735 44         90 foreach my $key ( @keys ) {
3736             exists $opt->{$key}
3737 136 100       492 or $opt->{$key} = $default;
3738             }
3739 44         124 return;
3740             }
3741              
3742             # $self->_attribute_exists( $name, %arg );
3743             #
3744             # This method returns true if an accessor for the given attribute
3745             # exists, and croaks otherwise.
3746             # Attributes in the %level1_attr hash fail unless in level1 mode
3747             # Named arguments:
3748             # query: if true, returns false if attribute does not exist
3749              
3750             {
3751             my %level1_attr = map { $_ => 1 } qw{ sun };
3752              
3753             sub _attribute_exists {
3754 1280     1280   3792 my ( $self, $name, %arg ) = @_;
3755             exists $accessor{$name}
3756             and ( ! $level1_attr{$name} || $self->{frame}[-1]{level1} )
3757 1280 50 33     9413 and return $accessor{$name};
      33        
3758             $arg{query}
3759 0 0       0 or $self->wail("No such attribute as '$name'");
3760 0         0 return;
3761             }
3762             }
3763              
3764             {
3765              
3766             my %spacetrack_attributes;
3767             $have_astro_spacetrack->()
3768             and %spacetrack_attributes = map { $_ => 1 }
3769             Astro::SpaceTrack->attribute_names();
3770              
3771             my %special = (
3772             formatter => sub {
3773             my ( $obj, $attr ) = @_;
3774             $obj->can( $attr )
3775             or return NULL;
3776             return $obj->$attr();
3777             },
3778             spacetrack => sub {
3779             my ( $obj, $attr ) = @_;
3780             $spacetrack_attributes{$attr}
3781             or return NULL;
3782             return $obj->getv( $attr );
3783             },
3784             time_parser => sub {
3785             my ( $obj, $attr ) = @_;
3786             $obj->can( $attr )
3787             or return NULL;
3788             return $obj->$attr();
3789             },
3790             );
3791              
3792             # my $value = $self->_attribute_value( $name );
3793             #
3794             # Return an attribute value. If the attribute is 'formatter',
3795             # 'spacetrack' or 'time_parser' you can specify a dot and the name
3796             # of an attribute of the relevant object, e.g. spacetrack.username.
3797             # If the attribute does not exist you get back manifest constant
3798             # NULL, which is a reference to undef blessed into class 'Null'.
3799             sub _attribute_value {
3800 43     43   110 my ( $self, $name ) = @_;
3801 43         423 my ( $attr, $sub ) = split qr{ [.] }smx, $name, 2;
3802 43 100       240 $accessor{$attr}
3803             or return NULL;
3804 9         33 my $rslt = $self->get( $attr );
3805 9 100       31 if ( defined $sub ) {
3806             $rslt
3807 2 50 33     17 and my $code = $special{$attr}
3808             or return NULL;
3809 2         8 $rslt = $code->( $rslt, $sub );
3810             }
3811 9         30 return $rslt;
3812             }
3813             }
3814              
3815             # Documented in POD
3816              
3817             {
3818             my %chooser = (
3819             '' => sub {
3820             my ( $sel ) = @_;
3821             my @rslt;
3822             foreach my $s ( split qr{ \s* , \s* }smx, $sel ) {
3823             if ( $s =~ m/ \D /smx || $s < 1000 ) {
3824             my $re = qr{\Q$s\E}i;
3825             push @rslt, sub {
3826             my ( $tle, $context ) = @_;
3827             $context->{name} ||= $tle->get( 'name' );
3828             defined $context->{name}
3829             or return;
3830             return $context->{name} =~ $re;
3831             };
3832             } else {
3833             push @rslt, sub {
3834             my ( $tle, $context ) = @_;
3835             $context->{id} ||= $tle->get( 'id' );
3836             return $context->{id} == $s;
3837             };
3838             }
3839             }
3840             return @rslt;
3841             },
3842             CODE_REF() => sub {
3843             my ( $sel ) = @_;
3844             return $sel;
3845             },
3846             REGEXP_REF() => sub {
3847             my ( $sel ) = @_;
3848             return sub {
3849             my ( $tle, $context ) = @_;
3850             $context->{name} ||= $tle->get( 'name' );
3851             return $context->{name} =~ $sel;
3852             };
3853             },
3854             );
3855              
3856             sub __choose {
3857 45     45   348 my ( $self, @args ) = @_;
3858 45 100       246 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
3859 45         148 my $choice = shift @args;
3860 45 100       161 defined $choice
3861             or $choice = [];
3862 45 50       218 ARRAY_REF eq ref $choice
3863             or $self->weep( 'Choice must be an ARRAY ref' );
3864 45         122 my @rslt;
3865             my @selector;
3866 45         88 foreach my $sel ( @{ $choice } ) {
  45         180  
3867 5         12 my $ref = ref $sel;
3868 5 50       23 my $code = $chooser{$ref}
3869             or $self->weep( "$ref not supported as chooser" );
3870 5         18 push @selector, $code->( $sel );
3871             }
3872              
3873             $opt->{bodies}
3874             and push @args,
3875 45 100       187 $self->_aggregate( $self->{bodies} );
3876             $opt->{sky}
3877 45 100       912 and push @args, $self->{sky};
3878              
3879 45 100       117 @args = map { ARRAY_REF eq ref $_ ? @{ $_ } : $_ } @args;
  51         211  
  43         187  
3880              
3881             not @selector
3882 45 100       335 and return wantarray ? @args : \@args;
    100          
3883              
3884 5         14 foreach my $tle ( @args ) {
3885 10 50       33 ARRAY_REF eq ref $tle
3886             and $self->weep( 'Schwartzian-transform objects not supported' );
3887              
3888 10         21 my $match = $opt->{invert};
3889 10         15 my $context = {};
3890 10         20 foreach my $sel ( @selector ) {
3891 10 100       23 $sel->( $tle, $context )
3892             or next;
3893 4         12 $match = !$match;
3894 4         12 last;
3895             }
3896              
3897 10 100       39 $match and push @rslt, $tle;
3898             }
3899              
3900 5 100       52 return wantarray ? @rslt : \@rslt;
3901             }
3902              
3903             }
3904              
3905             # $self->_deprecation_notice( $type, $name );
3906             #
3907             # This method centralizes deprecation. Type is 'attribute' or
3908             # 'method'. Deprecation is driven of the %deprecate hash. Values
3909             # are:
3910             # false - no warning
3911             # 1 - warn on first use
3912             # 2 - warn on each use
3913             # 3 - die on each use.
3914             #
3915             # $self->_deprecation_in_progress( $type, $name )
3916             #
3917             # This method returns true if the deprecation is in progress. In
3918             # fact it returns the deprecation level.
3919              
3920             {
3921              
3922             my %deprecate = (
3923             attribute => {
3924             country => 0,
3925             date_format => 0,
3926             desired_equinox_dynamical => 0,
3927             explicit_macro_delete => 0,
3928             gmt => 0,
3929             local_coord => 0,
3930             perltime => 0,
3931             time_format => 0,
3932             tz => 0,
3933             },
3934             method => {
3935             st => 0,
3936             },
3937             status => {
3938             iridium => 3,
3939             },
3940             );
3941              
3942             sub _deprecation_notice {
3943 1277     1277   3739 my ( $self, $type, $name, $repl ) = @_;
3944 1277 50       4385 $deprecate{$type} or return;
3945 1277 50       4053 $deprecate{$type}{$name} or return;
3946             my $msg = sprintf 'The %s %s is %s', $name, $type,
3947 0 0       0 $deprecate{$type}{$name} > 2 ? 'removed' : 'deprecated';
3948 0 0       0 defined $repl
3949             and $msg .= "; use $repl instead";
3950 0 0       0 $deprecate{$type}{$name} >= 3
3951             and $self->wail( $msg );
3952 0 0       0 warnings::enabled( 'deprecated' )
3953             and $self->whinge( $msg );
3954             $deprecate{$type}{$name} == 1
3955 0 0       0 and $deprecate{$type}{$name} = 0;
3956 0         0 return;
3957             }
3958              
3959             sub _deprecation_in_progress {
3960 0     0   0 my ( undef, $type, $name ) = @_; # Invocant unused
3961 0 0       0 $deprecate{$type} or return;
3962 0         0 return $deprecate{$type}{$name};
3963             }
3964              
3965             }
3966              
3967             # my ( $obj ) = $self->_drop_from_sky( $name );
3968             # The return is an array containing the dropped body, or nothing if the
3969             # body was not found.
3970             sub _drop_from_sky {
3971 1     1   4 my ( $self, $name ) = @_;
3972 1 50       6 defined( my $inx = $self->_find_in_sky( $name ) )
3973             or return;
3974 1         34 return splice @{ $self->{sky} }, $inx, 1;
  1         29  
3975             }
3976              
3977             # $fh = $self->_file_opener( $name, $mode );
3978             #
3979             # This method opens the given file, returning the handle. If the
3980             # mode is output, the current value of output_layers is appended.
3981             # An exception is thrown if the file can not be opened.
3982              
3983             sub _file_opener {
3984 1     1   4 my ( $self, $name, $mode ) = @_;
3985              
3986             # NOTE special case for &1 (stdout) and &2 (stderr).
3987             my $fh = ( $name =~ m/ \A & ( [12] ) \z /smx ) ?
3988             [
3989             undef,
3990 1 50 0     23 $self->{frame}[-1]{localout} || \*STDOUT,
    50          
3991             \*STDERR,
3992             ]->[ $1 ] :
3993             IO::File->new( $name, $mode )
3994             or $self->wail( "Unable to open $name: $!" );
3995              
3996 1 50       304 if ( $mode =~ m/ \A (?: [+>] | [|] - ) /smx ) {
3997              
3998 1         8 my $layers = $self->get( 'output_layers' );
3999 1 50 33     8 if ( defined $layers && '' ne $layers ) {
4000 1 50       23 binmode $fh, $layers
4001             or $self->wail(
4002             "Unable to set '$layers' on $name: $!" );
4003             }
4004             }
4005              
4006 1         89 return $fh;
4007             }
4008              
4009             # $code = $self->_file_reader( $file, \%opt );
4010             #
4011             # This method returns a code snippet that returns the contents of
4012             # the file one line at a time. The $file can be any of:
4013             #
4014             # * An open handle
4015             # * A URL (if LWP::UserAgent can be loaded)
4016             # * A file name
4017             # * A scalar reference
4018             # * An array reference
4019             # * A code reference, which is returned unmodified
4020             #
4021             # The code snippet will return undef at end-of-file.
4022             #
4023             # The following keys in %opt are recognized:
4024             # {encoding} specifies the encoding of the file. How this is used
4025             # on the $file argument as follows:
4026             # * An open handle -- unused
4027             # * A URL ----------- unused (encoding taken from HTTP::Response)
4028             # * A file name ----- used (default is utf-8)
4029             # * A scalar ref ---- used (default is un-encoded)
4030             # * An array ref ---- unused
4031             # * A code ref ------ unused
4032             # {glob} causes the contents of the file to be returned, rather
4033             # than a reader.
4034             # {optional} causes the code to simply return on an error, rather
4035             # than failing.
4036              
4037             sub _file_reader {
4038 30     30   14705 my ( $self, $file, $opt ) = @_;
4039              
4040 30 100       179 if ( openhandle( $file ) ) {
4041             $opt->{glob}
4042 2 100   1   46 or return sub { return scalar <$file> };
  1         24  
4043 1         7 local $/ = undef;
4044 1         32 return scalar <$file>;
4045             }
4046              
4047 28         99 my $ref = ref $file;
4048 28 50       215 my $code = $self->can( "_file_reader_$ref" )
4049             or $self->wail( sprintf "Opening a $ref ref is unsupported" );
4050              
4051 28         162 goto &$code;
4052             }
4053              
4054             # Most of the following are called using '$self->can(
4055             # "_file_reader_$ref" )', and there is no way a static analysis tool can
4056             # find such calls. So we just have to exempt them from Perl::Critic
4057              
4058             sub _file_reader_ { ## no critic (ProhibitUnusedPrivateSubroutines)
4059 17     17   52 my ( $self, $file, $opt ) = @_;
4060 17   100     75 $opt ||= {};
4061              
4062 17 50       68 defined $file
4063             and chomp $file;
4064              
4065 17 50 33     182 if ( ! defined $file || ! ref $file && '' eq $file ) {
      33        
4066 0 0       0 $opt->{optional} and return;
4067 0         0 $self->wail( 'Defined file required' );
4068             }
4069              
4070 17 100       87 if ( $self->_file_reader__validate_url( $file ) ) {
4071 2         24 my $ua = LWP::UserAgent->new();
4072 2         5097 my $resp = $ua->get( $file );
4073             $resp->is_success()
4074 2 50       14142 or do {
4075 0 0       0 $opt->{optional} and return;
4076 0         0 $self->wail( "Failed to retrieve $file: ",
4077             $resp->status_line() );
4078             };
4079 2         74 $opt = { %{ $opt }, encoding => $resp->content_charset() };
  2         21  
4080 2         3656 return $self->_file_reader(
4081             \( scalar $resp->content() ),
4082             $opt,
4083             );
4084             } else {
4085 15         961 my $encoding = $self->_file_reader__encoding( $opt );
4086             open my $fh, "<$encoding", $self->expand_tilde( $file ) ## no critic (RequireBriefOpen)
4087 15 100       213 or do {
4088 4 100       54 $opt->{optional} and return;
4089 3         59 $self->wail( "Failed to open $file: $!" );
4090             };
4091             $opt->{glob}
4092 11 100   16   1846 or return sub { return scalar <$fh> };
  16         303  
4093 7         46 local $/ = undef;
4094 7         469 return scalar <$fh>;
4095             }
4096             }
4097              
4098             sub _file_reader__encoding {
4099 19     19   64 my ( undef, $opt ) = @_;
4100 19   100     68 $opt ||= {};
4101 19   100     162 my $encoding = $opt->{encoding} || 'utf-8';
4102 19         68 $encoding = ":encoding($encoding)";
4103 19         59 OS_IS_WINDOWS
4104             and substr $encoding, 0, 0, ':crlf';
4105 19         92 return $encoding;
4106             }
4107              
4108              
4109             sub _file_reader__validate_url {
4110 17     17   47 my ( undef, $url ) = @_; # Invocant unused
4111              
4112 17 50       155 load_package( 'LWP::UserAgent' )
4113             or return;
4114              
4115 17 50       81 load_package( 'URI' )
4116             or return;
4117              
4118 17 50       95 load_package( 'LWP::Protocol' )
4119             or return;
4120              
4121 17 50       368 my $obj = URI->new( $url )
4122             or return;
4123 17 50       30636 $obj->can( 'authority' )
4124             or return 1;
4125              
4126 17 100       119 defined( my $scheme = $obj->scheme() )
4127             or return;
4128 3 100       126 LWP::Protocol::implementor( $scheme )
4129             or return;
4130              
4131 2         24694 return 1;
4132             }
4133              
4134             sub _file_reader_ARRAY { ## no critic (ProhibitUnusedPrivateSubroutines)
4135 5     5   13 my ( undef, $file, $opt ) = @_; # Invocant unused
4136              
4137 5         8 my $inx = 0;
4138             $opt->{glob}
4139 5 100   11   34 or return sub { return $file->[$inx++] };
  11         50  
4140 1         4 my $buffer;
4141 1         3 foreach ( @{ $file } ) {
  1         3  
4142 5         16 $buffer .= $_;
4143 5 50       28 $buffer =~ m/ \n \z /smx
4144             or $buffer .= "\n";
4145             }
4146 1         8 return $buffer;
4147             }
4148              
4149             sub _file_reader_CODE { ## no critic (ProhibitUnusedPrivateSubroutines)
4150 2     2   7 my ( undef, $file, $opt ) = @_; # Invocant unused
4151             $opt->{glob}
4152 2 100       13 or return $file;
4153 1         4 my $buffer;
4154 1         4 local $_;
4155 1         5 while ( defined( $_ = $file->() ) ) {
4156 5         33 $buffer .= $_;
4157 5 50       32 $buffer =~ m/ \n \z /smx
4158             or $buffer .= "\n";
4159             }
4160 1         10 return $buffer;
4161             }
4162              
4163             sub _file_reader_SCALAR { ## no critic (ProhibitUnusedPrivateSubroutines)
4164 4     4   14 my ( $self, $file, $opt ) = @_;
4165              
4166 4         17 my $encoding = $self->_file_reader__encoding( $opt );
4167             open my $fh, "<$encoding", $file ## no critic (RequireBriefOpen)
4168 4 50       94 or do {
4169 0 0       0 $opt->{optional} and return;
4170 0         0 $self->wail( "Failed to open SCALAR reference: $!" );
4171             };
4172             $opt->{glob}
4173 4 100   2   1043 or return sub { return scalar <$fh> };
  2         108  
4174 2         14 local $/ = undef;
4175 2         62 return scalar <$fh>;
4176             }
4177              
4178             # $inx = $self->_find_in_sky( $name )
4179             # The return is the index of the named body in @{ $self->{sky} }, or
4180             # undef if it is not present. 'Sun' and 'Moon' are special cases;
4181             # everything else is presumed to be found by name.
4182             sub _find_in_sky {
4183 6     6   16 my ( $self, $name ) = @_;
4184              
4185 6         113 my $re = qr/ \A \Q$name\E \z /smxi;
4186 6         17 foreach my $inx ( 0 .. $#{ $self->{sky} } ) {
  6         30  
4187 8 100       180 $self->{sky}[$inx]->get( 'name' ) =~ $re
4188             and return $inx;
4189             }
4190 5         158 return;
4191             }
4192              
4193             # Documented in POD
4194              
4195             sub __format_data {
4196 41     41   5544 my ( $self, $action, $data, $opt ) = @_;
4197 41         267 return $self->_get_formatter_object( $opt )->format(
4198             sp => $self,
4199             template => $action,
4200             data => $data,
4201             opt => $opt,
4202             );
4203             }
4204              
4205             # $frames = $satpass2->_frame_push($type, \@args);
4206             #
4207             # This method pushes a context frame on the stack. The $type
4208             # describes the frame, and goes in the frame's {type} entry, but
4209             # is currently unused. The \@args entry goes in the {args} key,
4210             # and is the basis of argument expansion. The return is the number
4211             # of frames that were on the stack _BEFORE_ the now-current frame
4212             # was added to the stack. This gets passed to _frame_pop() to
4213             # restore the context stack to its status before the current frame
4214             # was added.
4215              
4216             sub _frame_push {
4217 59     59   202 my ( $self, $type, $args, $opt ) = @_;
4218 59   50     202 $args ||= [];
4219 59   100     327 $opt ||= {};
4220 59   100     126 my $frames = scalar @{$self->{frame} ||= []};
  59         275  
4221 59 100       241 my $prior = $frames ? $self->{frame}[-1] : {
4222             condition => 1,
4223             stdout => select(),
4224             };
4225             my $condition = exists $opt->{condition} ?
4226             $opt->{condition} :
4227 59 100       192 $prior->{condition};
4228             #### defined $stdout or $stdout = select();
4229 59         268 my ( undef, $filename, $line ) = caller;
4230 59         1467 push @{$self->{frame}}, {
4231             type => $type,
4232             args => $args,
4233             condition => $condition,
4234             define => {}, # Macro defaults done with :=
4235             local => {},
4236             localout => undef, # Output for statement.
4237             macro => {},
4238             pushed_by => "$filename line $line",
4239             spacetrack => {},
4240             stdout => $prior->{localout} || $prior->{stdout},
4241 59   66     123 unsatisfied_if => $prior->{unsatisfied_if} || ! $condition,
      100        
4242             };
4243 59         248 return $frames;
4244             }
4245              
4246             # $satpass2->_frame_pop($frames);
4247             # $satpass2->_frame_pop($type => $frames);
4248             # $satpass2->_frame_pop();
4249             #
4250             # This method pops context frames off the stack until there are
4251             # $frames frames left. The optional $type argument is currently
4252             # unused, but was intended for type checking should that become
4253             # necessary. The zero-argument call pops one frame off the stack.
4254             # An exception is thrown if there are no frames left to pop. After
4255             # all required frames are popped, an exception is thrown if the
4256             # pop was done with a continued input line pending.
4257              
4258             {
4259              
4260             my %force_set; # If true, the named attribute is set with the
4261             # set() method even if a hash key of the same
4262             # name exists. This is set with
4263             # _frame_pop_force_set(), typically where the
4264             # mutator is defined.
4265              
4266             sub _frame_pop {
4267 53     53   160 my ($self, @args) = @_;
4268             ## my $type = @args > 1 ? shift @args : undef;
4269 53 100       173 @args > 1 and shift @args; # Currently unused
4270             my $frames = ( @args && defined $args[0] ) ?
4271             shift @args :
4272 53 100 100     303 @{$self->{frame}} - 1;
  27         107  
4273 53         117 while (@{$self->{frame}} > $frames) {
  105         373  
4274 52 50       130 my $frame = pop @{$self->{frame}}
  52         231  
4275             or $self->weep( 'No frame to pop' );
4276 52   50     200 my $local = $frame->{local} || {};
4277 52         131 foreach my $name ( keys %{ $local } ) {
  52         200  
4278 2         9 my $value = $local->{$name};
4279 2 100 66     26 if ( exists $self->{$name} && !$force_set{$name} ) {
4280 1         16 $self->{$name} = $value;
4281             } else {
4282 1         300 $self->set( $name, $value );
4283             }
4284             }
4285 52         124 foreach my $key (qw{macro}) {
4286 52   50     189 my $info = $frame->{$key} || {};
4287 52         108 foreach my $name ( keys %{ $info } ) {
  52         150  
4288 19         82 $self->{$key}{$name} = $info->{ $name };
4289             }
4290             }
4291 52         466 ($frame->{spacetrack} && %{$frame->{spacetrack}})
4292 52 50 33     397 and $self->_get_spacetrack()->set(%{$frame->{spacetrack}});
  0         0  
4293             }
4294 53 50       186 if (delete $self->{pending}) {
4295 0         0 $self->wail('Input ended on continued line');
4296             }
4297 53         296 return;
4298             }
4299              
4300             # Force use of the set() method even if there is an attribute of the
4301             # same name.
4302             sub _frame_pop_force_set {
4303 20     20   83 foreach my $name ( @_ ) {
4304 20         83 $force_set{$name} = 1;
4305             }
4306 20         70 return;
4307             }
4308             }
4309              
4310             sub _get_browser_command {
4311 0     0   0 my ( $self, $val ) = @_;
4312             defined $val
4313 0 0       0 or $val = $self->{webcmd};
4314 0 0 0     0 defined $val
4315             and '' ne $val
4316             or return $val;
4317 0 0       0 '1' eq $val
4318             or return $val;
4319 0         0 require Browser::Open;
4320 0         0 return Browser::Open::open_browser_cmd();
4321             }
4322              
4323             # $dumper = $self->_get_dumper();
4324             #
4325             # This method returns a reference to code that can be used to dump
4326             # data. The first time it is called it goes through a list of
4327             # possible classes, and uses the first one it can load, dying if
4328             # it can not load any of them. After the first successful call, it
4329             # simply returns the cached dumper.
4330              
4331             {
4332             my $dumper;
4333             my %kode = (
4334             'Data::Dumper' => sub {
4335             local $Data::Dumper::Terse = 1;
4336             Data::Dumper::Dumper(@_);
4337             },
4338             );
4339             sub _get_dumper {
4340 0     0   0 my ($self) = @_;
4341 0         0 my %dmpr;
4342             my @mod;
4343 0   0     0 return $dumper ||= do {
4344 0         0 foreach (qw{YAML::Dump Data::Dumper::Dumper}) {
4345 0         0 my ($module, $routine) = m/ (.*) :: (.*) /smx;
4346 0         0 push @mod, $module;
4347 0         0 $dmpr{$module} = $routine;
4348             }
4349 0         0 my $mod = $self->_load_module(@mod);
4350 0 0       0 $kode{$mod} || $mod->can($dmpr{$mod});
4351             };
4352             }
4353             }
4354              
4355             # $fmt = $satpass2->_get_dumper_object();
4356             #
4357             # Gets a dumper object. This object must conform to the
4358             # Astro::App::Satpass2::Format interface.
4359              
4360             {
4361              
4362             my $dumper;
4363              
4364             sub _get_dumper_object {
4365 0   0 0   0 return ( $dumper ||= do {
4366 0         0 require Astro::App::Satpass2::Format::Dump;
4367 0         0 Astro::App::Satpass2::Format::Dump->new();
4368             }
4369             );
4370             }
4371              
4372             }
4373              
4374             # $fmt = $satpass2->_get_formatter_object( $opt );
4375             #
4376             # Gets the Astro::App::Satpass2::Format object. If $opt->{dump} is true,
4377             # returns a dumper object; otherwise returns the currently-set
4378             # formatter object.
4379              
4380             sub _get_formatter_object {
4381 41     41   166 my ( $self, $opt ) = @_;
4382 41   50     184 $opt ||= {};
4383 41 50 33     402 return ( $opt && $opt->{dump} ) ? $self->_get_dumper_object() :
4384             $self->get( 'formatter' );
4385             }
4386              
4387             sub _get_formatter_attribute {
4388 0     0   0 my ( $self, $name ) = @_;
4389 0         0 return $self->get( 'formatter' )->$name();
4390             }
4391              
4392             # $st = $satpass2->_get_geocoder()
4393              
4394             # Gets the geocoder object, instantiating it if
4395             # necesary.
4396              
4397             sub _get_geocoder {
4398 0     0   0 my ( $self ) = @_;
4399 0 0       0 if ( ! exists $self->{geocoder} ) {
4400 0         0 my ( $class, $obj );
4401 0 0       0 $class = $default_geocoder->()
4402             and $obj = $class->new();
4403 0         0 $self->{geocoder} = $obj;
4404             }
4405 0         0 return $self->{geocoder};
4406             }
4407              
4408             # $boolean = $satpass2->_get_interactive();
4409             #
4410             # This method returns true if the script is running interactively,
4411             # and false otherwise. Currently, it returns the results of -t
4412             # STDIN.
4413              
4414             sub _get_interactive {
4415 1     1   12 return -t STDIN;
4416             }
4417              
4418             # $code = $satpass2->_get_readline();
4419             #
4420             # Returns code to read input. The code takes an argument which
4421             # will be used as a prompt if one is needed. What is actually
4422             # returned is:
4423             #
4424             # If $satpass2->_get_interactive() is false, the returned code
4425             # just reads standard in. Otherwise,
4426             #
4427             # if Term::ReadLine can be loaded, a Term::ReadLine object is
4428             # instantiated if need be, and the returned code calls
4429             # Term::ReadLine->readline($_[0]) and returns whatever that gives
4430             # you. Otherwise,
4431             #
4432             # Otherwise the returned code writes its argument to STDERR and
4433             # reads STDIN.
4434             #
4435             # Note that the return from this subroutine may or may not be
4436             # chomped.
4437              
4438             my $readline_word_break_re;
4439              
4440             {
4441             my $rl;
4442              
4443             sub _get_readline {
4444 1     1   8 my ($self) = @_;
4445             # The Perl::Critic recommendation is IO::Interactive, but that
4446             # fiddles with STDOUT. We want STDIN, because we want to behave
4447             # differently if STDIN is a pipe, but not if STDOUT is a pipe.
4448             # We're still missing the *ARGV logic, but that's OK too, since
4449             # we use the contents of @ARGV as commands, not as file names.
4450 1         3 return do {
4451 1         3 my $buffer = '';
4452 1 50       4 if ($self->_get_interactive()) {
4453             eval {
4454 0 0       0 load_package( 'Term::ReadLine' )
4455             or return;
4456 0 0       0 unless ( $rl ) {
4457 0         0 $rl = Term::ReadLine->new( 'satpass2' );
4458 0 0       0 if ( 'Term::ReadLine::Perl' eq $rl->ReadLine() ) {
4459              
4460 0   0     0 $readline_word_break_re ||= qr<
4461             [\Q$readline::rl_completer_word_break_characters\E]+
4462             >smx;
4463              
4464 20     20   121907 no warnings qw{ once };
  20         54  
  20         17874  
4465             $readline::rl_completion_function = sub {
4466 0     0   0 my ( $text, $line, $start ) = @_;
4467 0         0 return $self->__readline_completer(
4468             $text, $line, $start );
4469 0         0 };
4470             }
4471             }
4472             sub {
4473 0 0   0   0 defined $buffer or return $buffer;
4474 0         0 return ( $buffer = $rl->readline($_[0]) );
4475             }
4476 0         0 } || sub {
4477 0 0   0   0 defined $buffer or return $buffer;
4478 0         0 print STDERR $_[0];
4479             return (
4480 0         0 $buffer = ## no critic (ProhibitExplicitStdin)
4481             );
4482 0 0       0 };
4483             } else {
4484             sub {
4485 0 0   0   0 defined $buffer or return $buffer;
4486             return (
4487 0         0 $buffer = ## no critic (ProhibitExplicitStdin)
4488             );
4489 1         7 };
4490             }
4491             };
4492             }
4493             }
4494              
4495             sub __readline_completer {
4496 0     0   0 my ( $app, $text, $line, $start ) = @_;
4497              
4498 0 0       0 $start
4499             or return $app->_readline_complete_command( $text );
4500              
4501 0         0 my ( $cmd ) = split $readline_word_break_re, $line, 2;
4502 0         0 my $code;
4503             not $cmd =~ s/ \A core [.] //smx
4504             and ref $app
4505             and $app->{macro}{$cmd}
4506 0 0 0     0 and $code = $app->{macro}{$cmd}->implements( $cmd );
      0        
4507 0   0     0 $code ||= $app->can( $cmd );
4508              
4509 0 0       0 if ( CODE_REF eq ref $code ) {
    0          
4510             # builtins and code macros go here
4511              
4512 0         0 my $rslt;
4513              
4514 0 0       0 if ( my $method = $app->__get_attr( $code, Tweak => {}
4515             )->{completion} ) {
4516             $rslt = $app->$method( $code, $text, $line, $start )
4517 0 0       0 and return @{ $rslt };
  0         0  
4518             }
4519              
4520             $rslt = $app->_readline_complete_options( $code, $text,
4521             $line, $start )
4522 0         0 and @{ $rslt }
4523 0 0 0     0 and return @{ $rslt };
  0         0  
4524              
4525             } elsif ( my $macro = $app->{macro}{$cmd} ) {
4526             # command macros go here
4527              
4528 0         0 my $rslt;
4529             $rslt = $macro->completion( $text )
4530 0 0       0 and return @{ $rslt };
  0         0  
4531             }
4532              
4533 0         0 my @files = bsd_glob( "$text*" );
4534 0 0       0 if ( 1 == @files ) {
    0          
4535 0 0       0 $files[0] .= -d $files[0] ? '/' : ' ';
4536             } elsif ( $readline::var_CompleteAddsuffix ) {
4537 0         0 foreach ( @files ) {
4538 0 0 0     0 if ( -l $_ ) {
    0          
    0          
    0          
4539 0         0 $_ .= '@';
4540             } elsif ( -d $_ ) {
4541 0         0 $_ .= '/';
4542             } elsif ( -x _) {
4543 0         0 $_ .= '*';
4544             } elsif ( -S _ || -p _ ) {
4545 0         0 $_ .= '=';
4546             }
4547             }
4548             }
4549 0         0 $readline::rl_completer_terminator_character = '';
4550 0         0 return @files;
4551             }
4552              
4553             {
4554             my @builtins;
4555             sub _readline_complete_command {
4556 0     0   0 my ( $app, $text ) = @_;
4557 0 0       0 unless ( @builtins ) {
4558 0   0     0 my $stash = ( ref $app || $app ) . '::';
4559 20     20   187 no strict qw{ refs };
  20         55  
  20         19208  
4560 0         0 foreach my $sym ( keys %$stash ) {
4561 0 0       0 $sym =~ m/ \A _ /smx
4562             and next;
4563 0 0       0 my $code = $app->can( $sym )
4564             or next;
4565 0 0       0 $app->__get_attr( $code, 'Verb' )
4566             or next;
4567 0         0 push @builtins, $sym;
4568             }
4569 0         0 @builtins = sort @builtins;
4570             }
4571 0         0 my @rslt;
4572 0 0       0 if ( $text =~ s/ \A core [.] //smx ) {
4573 0         0 my $match = qr< \A \Q$text\E >smx;
4574 0         0 @rslt = map { "core.$_" } grep { $_ =~ $match } @builtins;
  0         0  
  0         0  
4575             } else {
4576 0         0 my $match = qr< \A \Q$text\E >smx;
4577 0         0 @rslt = grep { $_ =~ $match } @builtins, 'core.',
4578 0 0       0 ref $app ? keys %{ $app->{macro} } : ();
  0         0  
4579             }
4580 0 0 0     0 1 == @rslt
4581             and $rslt[0] =~ m/ \W \z /smx
4582             and $readline::rl_completer_terminator_character = '';
4583 0         0 return ( sort @rslt );
4584             }
4585             }
4586              
4587             sub _readline_complete_options {
4588             # my ( $app, $code, $text, $line, $start ) = @_;
4589 0     0   0 my ( $app, $code, $text ) = @_;
4590 0 0       0 $text =~ m/ \A ( --? ) ( .* ) /smx
4591             or return;
4592 0         0 my ( $prefix, $match ) = ( $1, $2 );
4593 0         0 my $lgl = $app->__legal_options( $code );
4594 0         0 my $re = qr< \A \Q$match\E >smx;
4595 0         0 my @rslt;
4596 0         0 foreach ( @{ $lgl } ) {
  0         0  
4597 0 0       0 next if ref;
4598             # De-alias before modifying
4599 0         0 ( my $o = $_ ) =~ s/ [!=?] .* //smx;
4600 0         0 push @rslt, grep { m/$re/ } split qr< \| >smx, $o;
  0         0  
4601             }
4602             @rslt
4603 0 0       0 and return [ map { "$prefix$_" } sort @rslt ];
  0         0  
4604 0         0 return;
4605             }
4606              
4607             # The following subroutine is called dynamically
4608             sub _readline_complete_subcommand { ## no critic (ProhibitUnusedPrivateSubroutines)
4609             # my ( $app, $code, $text, $line, $start ) = @_;
4610 0     0   0 my ( $app, undef, $text, $line, $start ) = @_;
4611 0         0 my @part = _readline_line_to_parts( $line );
4612 0 0       0 if ( my $code = $app->can( "_$part[0]_sub" ) ) {
4613 0         0 return $code->( $app, $text, $line, $start, @part );
4614             }
4615 0         0 my @rslt;
4616 0 0       0 if ( 2 == @part ) {
4617 0         0 my $re = qr< \A _$part[0]_sub_ ( \Q$part[1]\E \w* ) >smx;
4618 0   0     0 my $stash = ( ref $app || $app ) . '::';
4619 20     20   189 no strict qw{ refs };
  20         45  
  20         38582  
4620 0         0 foreach my $key ( keys %$stash ) {
4621 0 0       0 $key =~ m/$re/smx
4622             and push @rslt, "$1";
4623             }
4624 0         0 return [ sort @rslt ];
4625             }
4626              
4627 0 0       0 my $code = $app->can( "_$part[0]_sub_$part[1]" )
4628             or return;
4629              
4630 0         0 my $r;
4631 0 0       0 $r = $app->_readline_complete_options( $code, $text, $line,
4632             $start )
4633             and return $r;
4634              
4635             my $complete = $app->__get_attr( $code, Tweak => {} )->{completion}
4636 0 0       0 or return;
4637              
4638 0 0       0 $r = $app->$complete( $code, $text, $line, $start )
4639             and return $r;
4640              
4641 0         0 return;
4642             }
4643              
4644             sub _macro_list_complete { ## no critic (ProhibitUnusedPrivateSubroutines)
4645             # my ( $app, $code, $text, $line, $start ) = @_;
4646 0     0   0 my ( $app, undef, undef, $line, undef ) = @_;
4647 0 0       0 ref $app
4648             or return;
4649 0         0 my @part = _readline_line_to_parts( $line );
4650 0 0       0 3 == @part
4651             or return;
4652 0         0 my $re = qr< \A \Q$part[2]\E >smx;
4653 0         0 my @rslt;
4654 0         0 foreach ( sort keys %{ $app->{macro} } ) {
  0         0  
4655 0 0       0 m/$re/smx
4656             and push @rslt, $_;
4657             }
4658 0         0 return \@rslt;
4659             }
4660              
4661             sub _sky_body_complete { ## no critic (ProhibitUnusedPrivateSubroutines)
4662             # my ( $app, $code, $text, $line, $start ) = @_;
4663 0     0   0 my ( $app, undef, undef, $line, undef ) = @_;
4664 0 0       0 ref $app
4665             or return;
4666 0         0 my @part = _readline_line_to_parts( $line );
4667 0 0       0 3 == @part
4668             or return;
4669 0         0 my $re = qr< \A \Q$part[2]\E >smxi;
4670 0         0 my @rslt;
4671 0         0 foreach my $body ( @{ $app->{sky} } ) {
  0         0  
4672 0 0       0 if ( ( my $name = $body->get( 'name' ) ) =~ $re ) {
    0          
4673 0         0 push @rslt, $name;
4674             } elsif ( ( my $id = $body->get( 'id' ) ) =~ $re ) {
4675 0         0 push @rslt, $id;
4676             }
4677             }
4678 0         0 return [ sort @rslt ];
4679             }
4680              
4681             sub _readline_line_to_parts {
4682 0     0   0 my ( $line ) = @_;
4683             # NOTE that the field count of -1 causes a trailing separator to
4684             # result in a trailing empty field.
4685 0         0 my @parts = split $readline_word_break_re, $line, -1;
4686             # NOTE that we strip the leading 'core.' if any, so the return from
4687             # this method does not distinguish between a core command and the
4688             # same-named macro if any.
4689             @parts
4690 0 0       0 and $parts[0] =~ s/ \A core [.] //smx;
4691 0         0 return @parts;
4692             }
4693              
4694             sub _get_time_parser_attribute {
4695 0     0   0 my ( $self, $name ) = @_;
4696 0         0 return $self->{time_parser}->$name();
4697             }
4698              
4699             # $st = $satpass2->_get_spacetrack()
4700              
4701             # Gets the Astro::SpaceTrack object, instantiating it if
4702             # necesary.
4703              
4704             sub _get_spacetrack {
4705 7     7   24 my ( $self ) = @_;
4706             exists $self->{spacetrack}
4707 7 50       50 or $self->{spacetrack} = $self->_get_spacetrack_default();
4708 7         50 return $self->{spacetrack};
4709             }
4710              
4711             # $st = $satpass2->_get_spacetrack_default();
4712             #
4713             # Returns a new Astro::SpaceTrack object, initialized with this
4714             # object's webcmd, and with its filter attribute set to 1.
4715              
4716             sub _get_spacetrack_default {
4717 7     7   21 my ( $self ) = @_;
4718 7 50       49 $have_astro_spacetrack->()
4719             or return;
4720             return Astro::SpaceTrack->new (
4721             webcmd => $self->{webcmd},
4722 0         0 filter => 1,
4723             );
4724             }
4725              
4726             sub _get_day_midnight {
4727 10     10   40 my ( $self, $day ) = @_;
4728 10 100       41 defined $day
4729             or $day = time;
4730 10         39 my $gmt = $self->get( 'formatter' )->gmt();
4731 10 50       111 my @time = $gmt ? gmtime( $day ) : localtime( $day );
4732 10         28 $time[0] = $time[1] = $time[2] = 0;
4733 10         29 $time[5] += 1900;
4734 10 50       62 return $gmt ? greg_time_gm(@time) : greg_time_local(@time);
4735             }
4736              
4737             sub _get_day_noon {
4738 42     42   172 my ( $self, $day ) = @_;
4739 42 100       151 defined $day
4740             or $day = time;
4741 42         232 my $gmt = $self->get( 'formatter' )->gmt();
4742 42 50       353 my @time = $gmt ? gmtime( $day ) : localtime( $day );
4743 42         126 $time[0] = $time[1] = 0;
4744 42         80 $time[2] = 12;
4745 42         117 $time[5] += 1900;
4746 42 50       313 return $gmt ? greg_time_gm(@time) : greg_time_local(@time);
4747             }
4748              
4749             sub _get_warner_attribute {
4750 0     0   0 my ( $self, $name ) = @_;
4751 0         0 return $self->{_warner}->$name();
4752             }
4753              
4754             sub _helper_get_object {
4755 9     9   28 my ( $self, $attribute ) = @_;
4756 9 50       32 my $object = $self->get( $attribute )
4757             or $self->wail( "No $attribute object available" );
4758 9         17 return $object;
4759             }
4760              
4761             {
4762              
4763             my %parse_input = (
4764             formatter => {
4765             desired_equinox_dynamical => sub {
4766             my ( $self, undef, @args ) = @_; # $opt unused
4767             if ( $args[0] ) {
4768             $args[0] = $self->__parse_time( $args[0], 0 );
4769             }
4770             return @args;
4771             },
4772             format => sub {
4773             my ( $self, $opt, $template, @args ) = @_;
4774             $opt->{raw} = 1;
4775             return (
4776             arg => \@args,
4777             sp => $self,
4778             template => $template,
4779             );
4780             },
4781             },
4782             time_parser => {
4783             base => sub {
4784             my ( $self, undef, @args ) = @_; # $opt unused
4785             if ( @args && defined $args[0] ) {
4786             $args[0] = $self->__parse_time( $args[0], time );
4787             }
4788             return @args;
4789             }
4790             },
4791             );
4792              
4793             sub _helper_handler : Verb( changes! raw! ) {
4794 9     9   47 my ( $self, $opt, $name, $method, @args ) = __arguments( @_ );
4795              
4796             exists $opt->{raw}
4797 9 50       76 or $opt->{raw} = ( ! _is_interactive() );
4798              
4799 9 50       33 defined $method
4800             or $self->wail( 'No method name specified' );
4801              
4802 9 50       45 'config' eq $method
4803             and return $self->_helper_config_handler( $name => $opt );
4804              
4805 9         53 my $object = $self->_helper_get_object( $name );
4806 9 50 33     114 $method !~ m/ \A _ /smx and $object->can( $method )
4807             or $self->wail("No such $name method as '$method'");
4808              
4809             @args
4810             and $parse_input{$name}
4811             and $parse_input{$name}{$method}
4812 9 100 66     97 and @args = $parse_input{$name}{$method}->( $self, $opt, @args );
      66        
4813             delete $opt->{raw}
4814 9 100       70 and return $object->$method( @args );
4815 5         29 my @rslt = $object->decode( $method, @args );
4816              
4817 5 100       28 instance( $rslt[0], ref $object ) and return;
4818 2 50       6 ref $rslt[0] and return $rslt[0];
4819 2         7 return quoter( $name, $method, @rslt ) . "\n";
4820 20     20   177 }
  20         45  
  20         137  
4821             }
4822              
4823             sub _helper_config_handler {
4824 0     0   0 my ( $self, $name, $opt ) = @_;
4825 0         0 my $object = $self->_helper_get_object( $name );
4826             my $rslt = $object->config(
4827             changes => $opt->{changes},
4828             decode => ! $opt->{raw},
4829 0         0 );
4830 0 0       0 $opt->{raw} and return $rslt;
4831 0         0 my $output = '';
4832 0         0 foreach my $item ( @{ $rslt } ) {
  0         0  
4833 0         0 $output .= quoter( $name, @{ $item } ) . "\n";
  0         0  
4834             }
4835 0         0 return $output;
4836             }
4837              
4838             # $satpass2->_iridium_status(\@status)
4839              
4840             # Updates the status of all Iridium satellites from the given
4841             # array, which is compatible with the second item returned by
4842             # Astro::SpaceTrack->iridium_status(). If no argument is passed,
4843             # the status is retrieved using Astro::SpaceTrack->iridium_status()
4844              
4845             sub _iridium_status {
4846 0     0   0 my ($self, $status) = @_;
4847 0 0       0 unless ($status) {
4848 0         0 my $st = $self->_get_spacetrack();
4849 0         0 (my $rslt, $status) = $st->iridium_status;
4850 0 0       0 $rslt->is_success or $self->wail($rslt->status_line);
4851             }
4852              
4853 0 0       0 if ( ARRAY_REF eq ref $status ) {
4854 0         0 Astro::Coord::ECI::TLE->status (clear => 'iridium');
4855 0         0 foreach (@$status) {
4856 0         0 Astro::Coord::ECI::TLE->status (add => $_->[0], iridium =>
4857             $_->[4], $_->[1], $_->[3]);
4858             }
4859             } else {
4860 0         0 $self->weep(
4861             'Portable status not passed, and unavailable from Astro::SpaceTrack'
4862             );
4863             }
4864              
4865 0         0 foreach my $tle (@{$self->{bodies}}) {
  0         0  
4866 0         0 $tle->rebless ();
4867             }
4868              
4869 0         0 return;
4870              
4871             }
4872              
4873             # _is_case_tolerant()
4874             # Returns true if the OS supports case-tolerant file names. Yes, I know
4875             # it's the file system that is important, but I don't have access to
4876             # that level of detail.
4877             {
4878             my %os = map { $_ => 1 } qw{ darwin };
4879              
4880             sub _is_case_tolerant {
4881             exists $os{$^O}
4882 0 0   0   0 and return $os{$^O};
4883 0         0 return File::Spec->case_tolerant();
4884             }
4885             }
4886              
4887             # _is_interactive()
4888             #
4889             # Returns true if the dispatch() method is above us on the call
4890             # stack, otherwise returns false.
4891              
4892 20     20   15932 use constant INTERACTIVE_CALLER => __PACKAGE__ . '::dispatch';
  20         50  
  20         3966  
4893             sub _is_interactive {
4894 364     364   599 my $level = 0;
4895 364         2292 while ( my @info = caller( $level ) ) {
4896 1734 100       3698 INTERACTIVE_CALLER eq $info[3]
4897             and return $level;
4898 1696         7327 $level++;
4899             }
4900 326         842 return;
4901             }
4902              
4903             # $self->_load_module ($module_name)
4904              
4905             # Loads the module if it has not yet been loaded. Dies if it
4906             # can not be loaded.
4907              
4908             { # Begin local symbol block
4909              
4910             my %version;
4911             BEGIN {
4912 20     20   157334 %version = (
4913             'Astro::SpaceTrack' => ASTRO_SPACETRACK_VERSION,
4914             );
4915             }
4916              
4917             sub _load_module {
4918 0     0   0 my ($self, @module) = @_;
4919             ARRAY_REF eq ref $module[0]
4920 0 0       0 and @module = @{$module[0]};
  0         0  
4921 0 0       0 @module or $self->weep( 'No module specified' );
4922 0         0 my @probs;
4923 0         0 foreach my $module (@module) {
4924 0 0       0 load_package ($module) or do {
4925 0         0 push @probs, "$module needed";
4926 0         0 next;
4927             };
4928 0         0 my $modver;
4929 0 0 0     0 ($version{$module} && ($modver = $module->VERSION)) and do {
4930 0         0 $modver =~ s/_//g;
4931 0 0       0 $modver < $version{$module} and do {
4932 0         0 push @probs,
4933             "$module version $version{$module} needed";
4934 0         0 next;
4935             };
4936             };
4937 0         0 return $module;
4938             }
4939             {
4940 0         0 my $inx = 1;
  0         0  
4941 0         0 while (my @clr = caller($inx++)) {
4942 0 0       0 $clr[3] eq '(eval)' and next;
4943 0         0 my @raw = split '::', $clr[3];
4944 0 0       0 substr ($raw[-1], 0, 1) eq '_' and next;
4945 0         0 push @probs, "for method $raw[-1]";
4946 0         0 last;
4947             }
4948             }
4949 0         0 my $pfx = 'Error -';
4950 0         0 $self->wail(map {my $x = "$pfx $_\n"; $pfx = ' ' x 7; $x} @probs);
  0         0  
  0         0  
  0         0  
4951 0         0 return; # Can't get here, but Perl::Critic does not know this.
4952             }
4953              
4954             } # end local symbol block.
4955              
4956             # $output = $self->_macro($name,@args)
4957             #
4958             # Execute the named macro. The @args are of course optional.
4959              
4960             sub _macro {
4961 19     19   53 my ($self, $name, @args) = @_;
4962 19 50       68 $self->{macro}{$name} or $self->wail("No such macro as '$name'");
4963 19         90 my $frames = $self->_frame_push(macro => [@args]);
4964             my $macro = $self->{frame}[-1]{macro}{$name} =
4965 19         84 delete $self->{macro}{$name};
4966 19         38 my $output;
4967             my $err;
4968 19 100       46 my $ok = eval {
4969 19         106 $output = $macro->execute( $name, @args );
4970 18         79 1;
4971             } or $err = $@;
4972 19         88 $self->_frame_pop($frames);
4973 19 100       92 $ok or $self->wail($err);
4974 18         107 return $output;
4975             }
4976              
4977             # $angle = _parse_angle_parts ( @parts );
4978             #
4979             # Joins parts of angles into an angle.
4980             # The @parts array is array references describing the parts in
4981             # decreasing significance, with [0] being the value, and [1] being
4982             # the number in the next larger part. For the first piece, [1]
4983             # should be the number in an entire circle.
4984              
4985             sub _parse_angle_parts {
4986 3     3   9 my @parts = @_;
4987 3         5 my $angle = 0;
4988 3         6 my $circle = 1;
4989 3         24 my $places;
4990 3         10 foreach ( @parts ) {
4991 9         12 my ( $part, $size ) = @{ $_ };
  9         20  
4992 9 50       20 defined $part or last;
4993 9         12 $circle *= $size;
4994 9         15 $angle = $angle * $size + $part;
4995 9 50       25 $places = $part =~ m/ [.] ( [0-9]+ ) /smx ? length $1 : 0;
4996             }
4997 3         9 $angle *= 360 / $circle;
4998 3 50       25 if ( my $mag = sprintf '%d', $circle / 360 ) {
4999 3         6 $places += length $mag;
5000             }
5001 3         79 return sprintf( '%.*f', $places, $angle ) + 0;
5002             }
5003              
5004             # Documented in POD
5005              
5006             sub __parse_angle {
5007 40     40   131 my ( $self, @args ) = @_;
5008 40 100       149 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
5009 40         94 my ( $angle ) = @args;
5010 40 100       111 defined $angle or return;
5011              
5012 33 100       349 if ( $angle =~ m/ : /smx ) {
    100          
5013              
5014 2         10 my ($h, $m, $s) = split ':', $angle;
5015 2         38 return _parse_angle_parts(
5016             [ $h => 24 ],
5017             [ $m => 60 ],
5018             [ $s => 60 ],
5019             );
5020              
5021             } elsif ( $angle =~
5022             m{ \A ( [-+] )? ( [0-9]* ) d
5023             ( [0-9]* (?: [.] [0-9]* )? ) (?: m
5024             ( [0-9]* (?: [.] [0-9]* )? ) s? )? \z
5025             }smxi ) {
5026 1         8 my ( $sgn, $deg, $min, $sec ) = ( $1, $2, $3, $4 );
5027 1         7 $angle = _parse_angle_parts(
5028             [ $deg => 360 ],
5029             [ $min => 60 ],
5030             [ $sec => 60 ],
5031             );
5032 1 50 33     5 $sgn and '-' eq $sgn and return -$angle;
5033 1         3 return $angle;
5034             }
5035              
5036             $opt->{accept}
5037 30 50 66     341 or looks_like_number( $angle )
5038             or $self->wail( "Invalid angle '$angle'" );
5039              
5040 30         122 return $angle;
5041             }
5042              
5043             # Documented in POD
5044             {
5045             my %units = (
5046             au => AU,
5047             ft => 0.0003048,
5048             km => 1,
5049             ly => LIGHTYEAR,
5050             m => .001,
5051             mi => 1.609344,
5052             pc => PARSEC,
5053             );
5054              
5055             sub __parse_distance {
5056 3     3   16 my ($self, $string, $dfdist) = @_;
5057 3 50       12 defined $dfdist or $dfdist = 'km';
5058 3 50       50 my $dfunits = $dfdist =~ s/ ( [[:alpha:]]+ ) \z //smx ? $1 : 'km';
5059 3 50       65 my $units = lc (
5060             $string =~ s/ \s* ( [[:alpha:]]+ ) \z //smx ? $1 : $dfunits );
5061 3 50       16 $units{$units}
5062             or $self->wail( "Units of '$units' are unknown" );
5063 3 50       24 $string ne '' or $string = $dfdist;
5064 3 50       19 looks_like_number ($string)
5065             or $self->wail( "'$string' is not a number" );
5066 3         27 return $string * $units{$units};
5067             }
5068             }
5069              
5070             # Documented in POD
5071              
5072             sub __parse_time {
5073 55     55   1927 my ($self, $time, $default) = @_;
5074             my $pt = $self->{time_parser}
5075 55 50       312 or $self->wail( 'No time parser available' );
5076 55 50       394 $self->{time_parser}->can( 'station' )
5077             and $self->_set_time_parser_attribute(
5078             station => $self->station() );
5079 55 50       334 if ( defined( my $time = $pt->parse( $time, $default ) ) ) {
5080 55         181 return $time;
5081             }
5082 0         0 $self->wail( "Invalid time '$time'" );
5083 0         0 return;
5084             }
5085              
5086             # Reset the last time set. This is called from __arguments() in
5087             # ::Utils if the invocant is an Astro::App::Satpass2.
5088              
5089             sub __parse_time_reset {
5090 332     332   770 my ( $self ) = @_;
5091             defined ( my $pt = $self->{time_parser} )
5092 332 100       1452 or return;
5093 311         1983 $pt->reset();
5094 311         693 return;
5095             }
5096              
5097             # $string = _rad2hms ($angle)
5098              
5099             # Converts the given angle in radians to hours, minutes, and
5100             # seconds (of right ascension, presumably)
5101              
5102             sub _rad2hms {
5103 1     1   3 my $sec = shift;
5104 1         3 $sec *= 12 / PI;
5105 1         6 my $hr = floor( $sec );
5106 1         4 $sec = ( $sec - $hr ) * 60;
5107 1         13 my $min = floor( $sec );
5108 1         3 $sec = ( $sec - $min ) * 60;
5109 1         9 my $rslt = sprintf '%2d:%02d:%02d', $hr, $min, floor( $sec + .5 );
5110 1         16 return $rslt;
5111             }
5112              
5113             # $line = $self->_read_continuation( $in, $error_message );
5114             #
5115             # Acquire a line from $in, which must be a code reference taking
5116             # the prompt as an argument. If $in is not a code reference, or if
5117             # it returns undef, we wail() with the error message. Otherwise
5118             # we return the line read. I expect this to be used only by
5119             # __tokenize().
5120              
5121             sub _read_continuation {
5122 15     15   47 my ( $self, $in, $error ) = @_;
5123             $in and defined( my $more = $in->(
5124             my $prompt = $self->get( 'continuation_prompt' ) ) )
5125 15 100 66     84 or do {
5126 1 50       4 $error or return;
5127 1 50       4 ref $error eq CODE_REF
5128             and return $error->();
5129 1         5 $self->wail( $error );
5130             };
5131 14 50       98 $self->{echo} and $self->whinge( $prompt, $more );
5132 14 100       92 $more =~ m/ \n \z /smx or $more .= "\n";
5133 14         38 return $more;
5134             }
5135              
5136             # my ( $old_obj ) = $self->_replace_in_sky( $name, $new_obj );
5137             # This is restricted to objects constructed via {sky_class}.
5138             # The return is an array containing the replaced body, or nothing if
5139             # the body was not found. The $new_obj is optional; if not provided a
5140             # new object is created.
5141             sub _replace_in_sky {
5142 0     0   0 my ( $self, $name, $new_obj ) = @_;
5143             $new_obj
5144 0 0 0     0 or $self->{sky_class}{ fold_case( $name ) }
5145             or $self->weep( "Can not replace $name; no class defined" );
5146 0 0       0 defined( my $inx = $self->_find_in_sky( $name ) )
5147             or return;
5148 0   0     0 return splice @{ $self->{sky} }, $inx, $inx + 1,
  0         0  
5149             $new_obj || $self->_sky_object( $name );
5150             }
5151              
5152             # $self->_rewrite_level1_command( $buffer, $context );
5153             #
5154             # This method rewrites a level1 command to its current form. The
5155             # arguments are the buffer containing the command, and an
5156             # initially-empty hash reference, which the method will use to
5157             # preserve context across lines of command. NOTE that more than
5158             # one rewritten command may be returned (e.g. 'almanac' into
5159             # ( 'location', 'almanac' ).
5160              
5161             {
5162              
5163             my %level1_map = (
5164             almanac => sub {
5165             return ( 'location', $_[0] );
5166             },
5167             flare => sub {
5168             local $_ = $_[0];
5169             s/ (?<= \s ) - ( am|pm|day ) \b /-no$1/sxmg;
5170             return $_;
5171             },
5172             pass => sub {
5173             return ( 'location', $_[0] );
5174             },
5175             );
5176              
5177             my %level1_requote = (
5178             # In a macro definition:
5179             macro => {
5180             # In single-quoted strings,
5181             q{'} => sub {
5182             # escaped interpolations and double quotes may be
5183             # unescaped,
5184             s{ (?: \A | (?
5185             }{$1$2}sxmg;
5186             # and the string remains single-quoted.
5187             $_ = qq{'$_'};
5188             return;
5189             },
5190             # In double-quoted strings,
5191             q{"} => sub {
5192             # escaped interpolations and double quotes may be
5193             # unescaped,
5194             s{ (?: \A | (?
5195             }{$1$2}sxmg;
5196             # unescaped single quotes become double quotes,
5197             s/ (?: \A | (?
5198             # and the string becomes single-quoted.
5199             $_ = qq{'$_'};
5200             return;
5201             },
5202             },
5203             # Anywhere else
5204             '' => {
5205             # In single-quoted strings,
5206             q{'} => sub {
5207             # unescaped double quotes must be escaped,
5208             s/ (?: \A | (?
5209             # escaped single quotes may be unescaped,
5210             s/ (?: \A | (?
5211             # and the string becomes double-quoted.
5212             $_ = qq{"$_"};
5213             return;
5214             },
5215             # In double-quoted strings,
5216             q{"} => sub {
5217             # no changes need to be made.
5218             $_ = qq{"$_"};
5219             return;
5220             },
5221             },
5222             );
5223              
5224             sub _rewrite_level1_command {
5225 19     19   36 my ( undef, $buffer, $context ) = @_; # Invocant unused
5226              
5227 19         35 my $command = delete $context->{command};
5228              
5229 19 100       43 defined $buffer
5230             or return $buffer;
5231 12 50       81 $buffer =~ m/ \A \s* \z /sxm
5232             and return $buffer;
5233 12 50       42 $buffer =~ s/ \A \s* [#] 2 [#] \s* //sxm
5234             and return $buffer;
5235 12 50       30 $buffer =~ m/ \A \s* [#] /sxm
5236             and return $buffer;
5237              
5238 12 50       26 if ( ! defined $command ) {
5239 12 100       55 $buffer =~ m/ \A \s* ( \w+ ) /sxm
5240             or return $buffer;
5241 11         34 $command = $1;
5242             }
5243 11         20 my $append = '';
5244 11 100       133 $buffer =~ s/ ( \s* \\? \n ) //sxm
5245             and $append = $1;
5246             $append =~ m/ \\ /sxm
5247 11 50       31 and $context->{command} = $command;
5248              
5249 11   66     78 my $handler = $level1_requote{$command} || $level1_requote{''};
5250 11         22 my ( $this_quote, $start_pos );
5251 11         137 while ( $buffer =~ m/ (?: \A | (?
5252             ) {
5253 22 100       97 if ( ! defined $start_pos ) {
    100          
5254 9         32 $start_pos = $+[0] - 1;
5255 9         106 $this_quote = $1;
5256             } elsif ( $1 eq $this_quote ) {
5257 9         25 my $length = $+[0] - $start_pos;
5258 9         38 local $_ = substr $buffer, $start_pos + 1, $length - 2;
5259 9         31 $handler->{$this_quote}->();
5260 9         26 substr $buffer, $start_pos, $length, $_;
5261 9         31 pos( $buffer ) = $start_pos + length $_;
5262 9         43 $start_pos = undef;
5263             }
5264             }
5265              
5266 11 100       73 my $code = $level1_map{$command}
5267             or return $buffer . $append;
5268              
5269 3         7 my @rslt = $code->( $buffer );
5270 3         6 $rslt[-1] .= $append;
5271 3         9 return @rslt;
5272              
5273             }
5274             }
5275              
5276             # $self->_rewrite_level1_macros();
5277             #
5278             # This method rewrites all macros defined by a satpass
5279             # initialization file (as opposed to a satpass2 initialization
5280             # file) to be satpass2-compatible. It also clears the level1 flag
5281             # so that the satpass-compatible functionality is not invoked.
5282             #
5283             # Specifically it:
5284             # * Inserts a 'location' command before 'almanac' and 'pass';
5285             # * Changes the senses of the -am, -day, and -pm options in
5286             # 'flare';
5287             # * Removes delegated attributes from 'localize', replacing them
5288             # with a localization of the helper object.
5289             #
5290             # This method goes away when the satpass functionality does.
5291              
5292             {
5293             my %helper_map = (
5294             date_format => {
5295             helper => 'formatter', # Helper obj attr. Req'd.
5296             },
5297             desired_equinox_dynamical => {
5298             helper => 'formatter',
5299             },
5300             gmt => {
5301             helper => 'formatter',
5302             },
5303             local_coord => {
5304             helper => 'formatter',
5305             },
5306             time_format => {
5307             helper => 'formatter',
5308             },
5309             );
5310              
5311             my %filter = (
5312             almanac => sub {
5313             my ( undef, $line ) = @_; # $verb unused
5314             return ( 'location', $line );
5315             },
5316             flare => sub {
5317             my ( undef, $line ) = @_; # $verb unused
5318             $line =~ s/ (?<= \s ) - (am|day|pm) \b /-no$1/smx;
5319             return $line;
5320             },
5321             localize => sub {
5322             my ( undef, $line ) = @_; # $verb unused
5323             my @things = split qr{ \s+ }smx, $line;
5324             my @output;
5325             my %duplicate;
5326             foreach my $token ( @things ) {
5327             $helper_map{$token}
5328             and $token = $helper_map{$token}{helper};
5329             $duplicate{$token}++ or push @output, $token;
5330             }
5331             return join ' ', @output;
5332             },
5333             pass => sub {
5334             my ( undef, $line ) = @_; # $verb unused
5335             return ( 'location', $line );
5336             },
5337             set => sub {
5338             my ( undef, $line ) = @_; # $verb unused
5339             my @output = [ 'fubar' ]; # Prime the pump.
5340             my @input = Text::ParseWords::quotewords( qr{ \s+ }smx, 1,
5341             $line );
5342             shift @input;
5343             while ( @input ) {
5344             my ( $attr, $val ) = splice @input, 0, 2;
5345             if ( my $helper = $helper_map{$attr} ) {
5346             push @output, [ $helper->{helper},
5347             # not quoter( $val ) here, because presumably it
5348             # is already quoted if it needs to be.
5349             $helper->{attribute} || $attr, $val ];
5350             } else {
5351             'set' eq $output[-1][0]
5352             or push @output, [ 'set' ];
5353             # not quoter( $val ) here, because presumably it is
5354             # already quoted if it needs to be.
5355             push @{ $output[-1] }, $attr, $val;
5356             }
5357             }
5358             shift @output; # Get rid of the pump priming.
5359             return ( map { join ' ', @{ $_ } } @output );
5360             },
5361             st => sub {
5362             my ( undef, $line ) = @_; # $verb unused
5363             m/ \A \s* st \s+ localize \b /smx
5364             and return $line;
5365             $line =~ s/ \b st \b /spacetrack/smx;
5366             return $line;
5367             },
5368             show => sub {
5369             my ( undef, $line ) = @_; # $verb unused
5370             my @output = [ 'fubar' ];
5371             my @input = split qr{ \s+ }smx, $line;
5372             shift @input;
5373             foreach my $attr ( @input ) {
5374             if ( my $helper = $helper_map{$attr} ) {
5375             push @output, [ $helper->{helper},
5376             $helper->{attribute} || $attr ];
5377             } else {
5378             'show' eq $output[-1][0]
5379             or push @output, [ 'show' ];
5380             push @{ $output[-1] }, $attr;
5381             }
5382             }
5383             shift @output;
5384             return ( map { join ' ', @{ $_ } } @output );
5385             },
5386             );
5387              
5388             # Called by macro object's __level1_rewrite().
5389             sub __rewrite_level1_macro_def {
5390 8     8   18 my ( $self, $name, $args ) = @_;
5391              
5392 8         12 my ( $rewrote, @rslt );
5393 8         16 foreach ( @{ $args } ) {
  8         17  
5394 8 100 100     105 if ( m/ ( \S+ ) /smx
      66        
      66        
5395             and ( not $self->{macro}{$1}
5396             or $1 eq $name )
5397             and my $code = $filter{$1} ) {
5398 7         21 push @rslt, $code->( $1, $_ );
5399 7         17 $rewrote++;
5400             } else {
5401 1         4 push @rslt, $_;
5402             }
5403             }
5404              
5405 8 100       34 return $rewrote ? \@rslt : $args;
5406             }
5407              
5408             sub _rewrite_level1_macros {
5409 4     4   8 my ( $self ) = @_;
5410              
5411 4         5 foreach my $macro ( values %{ $self->{macro} } ) {
  4         13  
5412 8         26 $macro->__level1_rewrite();
5413             }
5414              
5415 4         7 return;
5416             }
5417             }
5418              
5419             # @coordinates = $self->_simbad4 ($query)
5420              
5421             # Look up the given star in the SIMBAD catalog. This assumes
5422             # SIMBAD 4.
5423              
5424             # We die on any error.
5425              
5426             sub _simbad4 {
5427 0     0   0 my $self = shift;
5428 0         0 $self->_load_module ('Astro::SIMBAD::Client');
5429 0         0 my $query = shift;
5430             my $simbad = Astro::SIMBAD::Client->new (
5431             format => {txt => 'FORMAT_TXT_SIMPLE_BASIC'},
5432             parser => {
5433             script => 'Parse_TXT_Simple',
5434             txt => 'Parse_TXT_Simple',
5435             },
5436             server => $self->{simbad_url},
5437 0         0 type => 'txt',
5438             );
5439             # I prefer script() to query() these days because the former does
5440             # not require SOAP::Lite, which seems to be getting flakier as time
5441             # goes on.
5442             # TODO get rid of $fmt =~ s/// once I massage
5443             # FORMAT_TXT_SIMPLE_BASIC in Astro::SIMBAD::Client
5444             # my @rslt = $simbad->query (id => $query)
5445 0         0 my $fmt = Astro::SIMBAD::Client->FORMAT_TXT_SIMPLE_BASIC();
5446 0         0 $fmt =~ s/ \n //smxg;
5447 0 0       0 my @rslt = $simbad->script( <<"EOD" )
5448             format obj "$fmt"
5449             query id $query
5450             EOD
5451             or $self->wail("No entry found for $query");
5452 0 0       0 @rslt > 1
5453             and $self->wail("More than one entry found for $query");
5454 0 0 0     0 @rslt = map {$rslt[0]{$_} eq '~' ? 0 : $rslt[0]{$_} || 0} qw{
  0         0  
5455             ra dec plx pmra pmdec radial};
5456 0 0 0     0 ($rslt[0] && $rslt[1])
5457             or $self->wail("No position returned by $query");
5458 0 0       0 $rslt[2] = $rslt[2] ? 1000 / $rslt[2] : 10000;
5459 0 0       0 $rslt[3] and $rslt[3] /= 1000;
5460 0 0       0 $rslt[4] and $rslt[4] /= 1000;
5461 0 0       0 return wantarray ? @rslt : join ' ', @rslt;
5462             }
5463              
5464             sub _templates_to_options {
5465 24     24   93 my ( $self, $name, $opt ) = @_;
5466 24         103 $opt->{_template} = $name;
5467             my $code = sub {
5468 5     5   4078 my ( $opt_name, $opt_value ) = @_;
5469 5 50       107 $opt->{_template} = $opt_value ? "${name}_$opt_name" : $name;
5470 5         84 return;
5471 24         239 };
5472 24         462 my $re = qr< \A \Q$name\E _ ( \w+ ) \z >smx;
5473 24         69 my @rslt;
5474 24         130 my $fmtr = $self->get( 'formatter' );
5475 24 50       178 if ( $fmtr->can( '__list_templates' ) ) {
5476 24         150 foreach ( $fmtr->__list_templates() ) {
5477 672 100       2345 $_ =~ $re
5478             or next;
5479 44         1210 push @rslt, "$1!", $code;
5480             }
5481             }
5482 24         550 return @rslt;
5483             }
5484              
5485             # ($tokens, $redirect) = $self->__tokenize(
5486             # {option => $value}, $buffer, [$arg0 ...]);
5487             #
5488             # This method tokenizes the buffer. The options hash may be
5489             # omitted, in which case the $buffer to be tokenized is the first
5490             # argument. After the buffer is an optional reference to an array
5491             # of arguments to be substituted in.
5492             #
5493             # This method attempts to parse and tokenize the buffer in a way
5494             # similar to the bash shell. That is, parameters are interpolated
5495             # inside double quotes but not single quotes, tilde expansion
5496             # takes place unless quoted, and spaces delimit tokens only when
5497             # occurring outside quotes.
5498             #
5499             # The back slash character ('\') is an escape character. Inside
5500             # single quotes only the back slash itself and a single quote may
5501             # be escaped. Otherwise, anything can be escaped.
5502             #
5503             # The returns are a reference to an array of tokens found, and a
5504             # reference to a hash of redirections found. This hash will have
5505             # zero or more of the keys '>' (standard output redirection) and
5506             # '<' (standard input redirection. The value of each key will be a
5507             # reference to a hash containing keys 'mode' ('>' or '>>' for
5508             # output, '<' or '<<' for input) and 'name' (normally the file
5509             # name).
5510             #
5511             # The recognized options are:
5512             #
5513             # single => 1
5514             # causes the buffer to be interpreted as a single token.
5515             #
5516             # noredirect => 1
5517             # causes redirects to be illegal.
5518             #
5519             # If noredirect is specified, only the $tokens reference is
5520             # returned. If noredirect and single are both specified, the
5521             # parsed and interpolated token is returned.
5522             #
5523             # If interpolation is being done, an unescaped dollar sign
5524             # introduces the interpolation. This works pretty much the same
5525             # way as under bash: if the first character after the dollar sign
5526             # is a left curly bracket, everything to the corresponding right
5527             # curly bracked specifies the interpolation; if not, the rule is
5528             # that word characters specify the interpolation.
5529             #
5530             # A number (i.e. $1) specifies interpolation of an argument.
5531             # Arguments are numbered starting at 1.
5532             #
5533             # Otherwise, if the interpolation names an attribute, the value of
5534             # that attribute is interpolated in, otherwise the named
5535             # environment variable is interpolated in.
5536             #
5537             # Most of the fancier forms of interpolation are suported. In the
5538             # following, word is expanded by recursively calling __tokenize
5539             # with options {single => 1, noredirect => 1}. But unlike bash, we
5540             # make no distinction between unset or null. The ':' can be
5541             # omitted before the '-', '=', '?' or '+', but it does not change
5542             # the functionality.
5543             #
5544             # ${parameter:-word} causes the given word to be substituted if
5545             # the parameter is undefined.
5546             #
5547             # ${parameter:=word} is the same as above, but also causes the
5548             # word to be assigned to the parameter if it is unassigned. Unlike
5549             # bash, this assignment takes place on positional parameters. If
5550             # done on an attribute or environment variable, it causes that
5551             # attribute or environment variable to be set to the given value.
5552             #
5553             # ${parameter:?word} causes the parse to fail with the error
5554             # 'word' if the parameter is undefined.
5555             #
5556             # ${parameter:+word} causes the value of the given word to be used
5557             # if the parameter is defined, otherwise '' is used.
5558             #
5559             # ${parameter:offset} and ${parameter:offset:length} take
5560             # substrings of the parameter value. The offset and length must be
5561             # numeric.
5562              
5563             {
5564              
5565             # Special variables.
5566             # Calling sequence: $special{$name}->(\@args, $relquote)
5567             my %special = (
5568             '0' => sub { return $0 },
5569             '#' => sub { return scalar @{ $_[0] } },
5570             ## '*' => sub { return join ' ', @{ $_[0] } },
5571             ## '@' => sub { return $_[1] ? join( ' ', @{ $_[0] } ) : $_[0] },
5572             '*' => sub { return $_[1] ? join( ' ', @{ $_[0] } ) : $_[0] },
5573             '@' => sub { return $_[0] },
5574             '$' => sub { return $$ },
5575             '_' => sub { return $^X },
5576             );
5577              
5578             my %case_ctl = (
5579             E => sub { delete $_[0]->{_case_mod} },
5580             F => sub { $_[0]->{_case_mod}{case} = sub { fold_case( $_[1] ) } },
5581             L => sub { $_[0]->{_case_mod}{case} = sub { lc $_[1] } },
5582             U => sub { $_[0]->{_case_mod}{case} = sub { uc $_[1] } },
5583             l => sub { $_[0]->{_case_mod}{single} = sub { lcfirst $_[1] } },
5584             u => sub { $_[0]->{_case_mod}{single} = sub { ucfirst $_[1] } },
5585             );
5586              
5587             # Leading punctuation that is equivalent to a method.
5588             my %command_equivalent = (
5589             '.' => 'source',
5590             '!' => 'system',
5591             );
5592             my $command_equiv_re = do {
5593             my $keys = join '', sort keys %command_equivalent;
5594             qr{ [$keys] }smx;
5595             };
5596              
5597             my %escape = (
5598             t => "\t",
5599             n => "\n",
5600             r => "\r",
5601             f => "\f",
5602             b => "\b",
5603             a => "\a",
5604             e => "\e",
5605             );
5606              
5607             sub __tokenize {
5608 381     381   123060 my ($self, @parms) = @_;
5609 381         1365 local $self->{_case_mod} = undef;
5610 381 100       1482 my $opt = HASH_REF eq ref $parms[0] ? shift @parms : {};
5611 381         993 my $in = $opt->{in};
5612 381         908 my $buffer = shift @parms;
5613 381 100       1817 $buffer =~ m/ \n \z /smx or $buffer .= "\n";
5614 381   100     1202 my $args = shift @parms || [];
5615 381         919 my @rslt = ( {} );
5616 381         829 my $absquote; # True if inside ''
5617             my $relquote; # True if inside "" (and not in '')
5618 381         773 my $len = length $buffer;
5619 381         668 my $inx = 0;
5620              
5621             # Because I'm not smart enough to do all this with a regular
5622             # expression, I take the brute force approach and iterate
5623             # through the buffer to be tokenized. It's a 'while' rather than
5624             # a 'for' or 'foreach' because that way I get to muck around
5625             # with the current position inside the loop.
5626              
5627 381         1037 while ($inx < $len) {
5628 6313         13066 my $char = substr $buffer, $inx++, 1;
5629              
5630             # If we're inside single quotes, the only escapable
5631             # characters are single quote and back slash, and all
5632             # characters until the next unescaped single quote go into
5633             # the current token
5634              
5635 6313 100 66     49584 if ( $absquote ) {
    100 100        
    100 100        
    100 66        
    100 66        
    100          
    100          
    100          
    100          
    100          
5636 621 50       1389 if ( $char eq '\\' ) {
    100          
5637 0 0       0 if ( (my $next = substr $buffer, $inx, 1) =~
5638             m/ ['\\] /smx ) {
5639 0         0 $inx++;
5640 0         0 $rslt[-1]{token} .= $next;
5641             } else {
5642 0         0 $rslt[-1]{token} .= $char;
5643             }
5644             } elsif ( $char eq q{'} ) {
5645 34         93 $absquote = undef;
5646             } else {
5647 587         969 $rslt[-1]{token} .= $char;
5648 587 100       1249 if ( $inx >= $len ) {
5649 2         14 $buffer .= $self->_read_continuation( $in,
5650             'Unclosed single quote' );
5651 1         3 $len = length $buffer;
5652             }
5653             }
5654              
5655             # If we have a backslash, it escapes the next character,
5656             # which goes on the current token no matter what it is.
5657              
5658             } elsif ( $char eq '\\' ) {
5659 10         36 my $next = substr $buffer, $inx++, 1;
5660 10 100       36 if ( $inx >= $len ) { # At end of line
    100          
5661 2 50       32 if ( $relquote ) { # Inside ""
5662 0         0 $buffer .= $self->_read_continuation( $in,
5663             'Unclosed double quote' );
5664             } else { # Between tokens
5665 2         32 $buffer .= $self->_read_continuation( $in,
5666             'Dangling continuation' );
5667 2 50       15 $opt->{single} or push @rslt, {}; # New token
5668             }
5669 2         13 $len = length $buffer;
5670             } elsif ( $relquote ) {
5671 7 100       23 if ( my $code = $case_ctl{$next} ) {
5672 6         24 $code->( $self );
5673             } else {
5674 1   33     5 $rslt[-1]{token} .= $escape{$next} || $next;
5675             }
5676             } else {
5677 1         4 $rslt[-1]{token} .= $next;
5678             }
5679              
5680             # If we have a single quote and we're not inside double
5681             # quotes, we go into absolute quote mode. We also append an
5682             # empty string to the current token to force its value to be
5683             # defined; otherwise empty quotes do not generate tokens.
5684              
5685             } elsif ($char eq q{'} && !$relquote) {
5686 35         169 $rslt[-1]{token} .= ''; # Empty string, to force defined.
5687 35         89 $absquote++;
5688              
5689             # If we have a double quote, we toggle relative quote mode.
5690             # We also append an empty string to the current tokens for
5691             # the reasons discussed above.
5692              
5693             } elsif ($char eq '"') {
5694 44         152 $rslt[-1]{token} .= ''; # Empty string, to force defined.
5695             ( $relquote = !$relquote )
5696 44 100       147 or delete $self->{_case_mod};
5697              
5698             # If we have a whitespace character and we're not inside
5699             # quotes and not in single-token mode, we start a new token.
5700             # It is possible that we generate redundant tokens this way,
5701             # but the unused ones are eliminated later.
5702              
5703             } elsif ($char =~ m/ \s /smx && !$relquote && !$opt->{single}) {
5704 937         2010 push @rslt, {};
5705              
5706             # If we have a dollar sign, it introduces parameter
5707             # substitution, a non trivial endeavor.
5708              
5709             } elsif ( $char eq '$' && $inx < $len ) {
5710 72         203 my $name = substr $buffer, $inx++, 1;
5711 72         132 my $brkt;
5712              
5713             # Names beginning with brackets are special. We note the
5714             # fact and scan for the matching close bracket, throwing
5715             # an exception if we do not have one.
5716              
5717 72 100 66     459 if ($name eq '{' && $inx < $len) {
    100          
5718 34         68 $brkt = 1;
5719 34         61 $name = '';
5720 34         59 my $nest = 1;
5721 34         102 while ($inx < $len) {
5722 369         741 $char = substr $buffer, $inx++, 1;
5723 369 50       972 if ($char eq '{') {
    100          
5724 0         0 $nest++;
5725             } elsif ($char eq '}') {
5726 33 50       251 --$nest or last;
5727             }
5728 336         2262 $name .= $char;
5729             }
5730 34 100       100 $char eq '}'
5731             or $self->wail('Missing right curly bracket');
5732              
5733             # If the name begins with an alpha or an underscore, we
5734             # simply append any word ('\w') characters to it. If it
5735             # the word characters are immediately followed by a dot
5736             # and more word characters we grab them too, and advance
5737             # the current location past whatever we grabbed. The dot
5738             # syntax is in aid of accessing attributes of
5739             # attributes (e.g. $formatter.time_format)
5740              
5741             } elsif ( $name =~ m/ \A [[:alpha:]_] \z /smx ) {
5742 21         99 pos( $buffer ) = $inx;
5743 21 50       166 if ( $buffer =~ m/ \G ( \w* (?: [.] \w+ )? ) /smxgc ) {
5744 21         97 $name .= $1;
5745 21         64 $inx += length $1;
5746             }
5747             }
5748              
5749             # Only bracketed names can be indirected, and then only
5750             # if the first character is a bang.
5751              
5752 71         175 my ($indirect, $value);
5753 71 100       259 $brkt and $indirect = $name =~ s/ \A ! //smx;
5754              
5755             # If we find a colon and/or one of the other cabbalistic
5756             # characters, we need to do some default processing.
5757              
5758 71 100       574 if ($name =~ m/ (.*?) ( [:]? [\-\+\=\?] | [:] ) (.*) /smx) {
5759 28         210 my ($name, $flag, $rest) = ($1, $2, $3);
5760              
5761             # First we do indirection if that was required.
5762              
5763 28 50       86 $indirect
5764             and $name = $self->_tokenize_var(
5765             $name, $args, $relquote, $indirect);
5766              
5767             # Next we find out whether we have an honest-to-God
5768             # colon, since that might specify substring
5769             # processing.
5770              
5771             ## my $colon = $flag =~ s/ \A : //smx ? ':' : '';
5772 28         135 $flag =~ s/ \A : //smx;
5773              
5774             # We run the stuff after the first cabbalistic
5775             # character through the tokenizer, since further
5776             # expansion is possible here.
5777              
5778 28         209 my $mod = __tokenize(
5779             $self,
5780             { single => 1, noredirect => 1, in => $in },
5781             $rest, $args);
5782 28         107 chomp $mod; # Don't want trailing \n here.
5783              
5784             # At long last we get the actual value of the
5785             # variable. This will be either undef, a scalar, or
5786             # a list reference.
5787              
5788 28         93 $value = $self->_tokenize_var(
5789             $name, $args, $relquote);
5790              
5791             # The value is logically defined if it is a scalar
5792             # and not undef, or if it is an array reference and
5793             # the array is not empty.
5794              
5795 28 100       93 my $defined = ref $value ? @$value : defined $value;
5796              
5797             # The '+' cabbalistic sign replaces the value of the
5798             # variable if it is logically defined.
5799              
5800 28 100       159 if ($flag eq '+') {
    100          
    100          
    100          
    100          
    50          
5801 4 100       21 $value = $defined ? $mod : '';
5802              
5803             # If the variable is defined, only substring
5804             # processing is possible. This actually is
5805             # implemented as slice processing if the value is an
5806             # array reference.
5807              
5808             } elsif ($defined) {
5809 16 100       64 if ($flag eq '') {
5810 10         56 my @pos = split ':', $mod, 2;
5811 10         23 foreach ( @pos ) {
5812 18         85 s/ \A \s+ //smx;
5813             }
5814 10 50       33 @pos > 2
5815             and $self->wail(
5816             'Substring expansion has extra arguments' );
5817 10         23 foreach ( @pos ) {
5818 18 50       88 m/ \A [-+]? [0-9]+ \z /smx
5819             or $self->wail(
5820             'Substring expansion argument non-numeric'
5821             );
5822             }
5823 10 100       29 if (ref $value) {
5824 4 50       13 if (@pos > 1) {
5825 4         15 $pos[1] += $pos[0] - 1;
5826             } else {
5827 0         0 $pos[1] = $#$args;
5828             }
5829 4 100       14 $pos[1] > $#$value and $pos[1] = $#$value;
5830 4         22 $value = [@$value[$pos[0] .. $pos[1]]];
5831             } else {
5832             # We want to disable warnings if we slop
5833             # outside the string.
5834 20     20   190 no warnings qw{substr};
  20         73  
  20         61261  
5835 6 100       37 $value = @pos == 1 ? substr $value, $pos[0] :
5836             substr $value, $pos[0], $pos[1];
5837             }
5838             }
5839              
5840             # If the cabbalistic sign is '-', we supply the
5841             # remainder of the specification as the default.
5842              
5843             } elsif ($flag eq '-') {
5844 2         9 $value = $mod;
5845              
5846             # If the cabbalistic sign is '=', we supply the
5847             # remainder of the specification as the default. We
5848             # also set the variable to the value, for future
5849             # use. Note that special variables may not be set,
5850             # and result in an exception.
5851              
5852             } elsif ($flag eq '=') {
5853 3         20 $value = $mod;
5854 3 50 33     57 if ( $special{$name} || $name !~ m/ \D /smx ) {
    50          
5855 0         0 $self->wail("Cannot assign to \$$name");
5856             ## } elsif ($name !~ m/\D/) {
5857             ## $args->[$name - 1] = $value;
5858             } elsif (exists $mutator{$name}) {
5859 0         0 $self->set($name => $value);
5860             } else {
5861 3         28 $self->{frame}[-1]{define}{$name} = $value;
5862             }
5863              
5864             # If the cabbalistic sign is '?', we throw an
5865             # exception with the remainder of the specification
5866             # as the text.
5867              
5868             } elsif ($flag eq '?') {
5869 2         11 $self->wail($mod);
5870              
5871             # If there is no cabbalistic sign at all, we fell
5872             # through here trying to do substring expansion on
5873             # an undefined variable. Since Bash allows this, we
5874             # will to, though with misgivings.
5875              
5876             } elsif ( $flag eq '' ) {
5877 1         5 $value = '';
5878              
5879             # Given the way the parser works, the above should
5880             # have exhausted all possibilities. But being a
5881             # cautious programmer ...
5882              
5883             } else {
5884 0         0 $self->weep(
5885             "\$flag = '$flag'. This should not happen"
5886             );
5887             }
5888              
5889             # Without any cabbalistic signs, variable expansion is
5890             # easy. We perform the indirection if needed, and then
5891             # grab the value of the variable, which still can be
5892             # undef, a scalar, or an array reference.
5893              
5894             } else {
5895 43 100       160 $indirect
5896             and $name = $self->_tokenize_var(
5897             $name, $args, $relquote, $indirect);
5898 43         181 $value = $self->_tokenize_var(
5899             $name, $args, $relquote);
5900             }
5901              
5902             # For simplicity in what follows, make the value into an
5903             # array reference.
5904 69 100       318 ref $value
    100          
5905             or $value = defined $value ? [ $value ] : [];
5906              
5907             # If we are inside quotes
5908 69 100       221 if ( $relquote ) {
5909             # do case modification
5910             # NOTE that the argument list is modified in-place.
5911 12         27 $self->_case_mod( @{ $value } );
  12         66  
5912             } else {
5913             # otherwise do word splitting
5914 57         102 $value = [ map { split qr{ \s+ }smx } @{ $value } ];
  71         652  
  57         185  
5915             }
5916              
5917             # If we have a value, append each element to the current
5918             # token, and then create a new token for the next
5919             # element. The last element's empty token gets
5920             # discarded, since we may need to append more data to
5921             # the last element (e.g. "$@ foo").
5922 69 100       180 if ( @{ $value } ) {
  69         257  
5923 58         152 foreach ( @$value ) {
5924 86         278 $rslt[-1]{token} .= $_;
5925 86         206 push @rslt, {};
5926             }
5927 58         165 pop @rslt;
5928             }
5929              
5930             # Here ends the variable expansion code.
5931              
5932             # If the character is an angle bracket or a pipe, we have a
5933             # redirect specification. This always starts a new token. We
5934             # flag the token as a redirect, stuff all matching
5935             # characters into the mode (throwing an exception if there
5936             # are too many), consume any trailing spaces, and set the
5937             # token value to the empty string to prevent executing this
5938             # code again when we hit the first character of the file
5939             # name. Note that redirect tokens always get tilde
5940             # expansion.
5941              
5942             } elsif ( $char =~ m/ [<>|] /smx ) {
5943 6 100       73 push @rslt, {
    50          
5944             redirect => 1,
5945             type => ($char eq '<' ? '<' : '>'),
5946             mode => ($char eq '|' ? '|-' : $char),
5947             expand => ($char ne '|')
5948             };
5949 6         23 while ($inx < $len) {
5950 11         23 my $next = substr $buffer, $inx++, 1;
5951 11 50       33 $next =~ m/ \s /smx and next;
5952 11 100       23 if ($next eq $char) {
5953 6         17 $rslt[-1]{mode} .= $next;
5954 6 100       23 length $rslt[-1]{mode} > 2
5955             and $self->wail(
5956             "Syntax error near $rslt[-1]{mode}");
5957             } else {
5958 5         10 --$inx;
5959 5         14 $rslt[-1]{token} = '';
5960 5         31 last;
5961             }
5962             }
5963 5 100       21 if ( '<<' eq $rslt[-1]{mode} ) { # Heredoc
5964 4         10 delete $rslt[-1]{redirect};
5965 4         10 delete $rslt[-1]{type};
5966 4         7 delete $rslt[-1]{mode};
5967 4         9 my $quote = '';
5968 4         12 while ( $inx < $len ) {
5969 62         99 my $next = substr $buffer, $inx++, 1;
5970 62 100       126 if ( $next =~ m/ \s /smx ) {
5971 2 50       9 $quote or last;
5972 0         0 $rslt[-1]{token} .= $next;
5973             } else {
5974             '' eq $rslt[-1]{token}
5975             and $next =~ m/ ['"] /smx
5976             and $quote = $next
5977 60 100 100     206 or $rslt[-1]{token} .= $next;
      66        
5978             $quote
5979             and $next eq $quote
5980 60 100 100     224 and $rslt[-1]{token} ne ''
      100        
5981             and last;
5982             }
5983             }
5984 4 100       22 $quote and $rslt[-1]{token} =~ s/ . \z //sxm;
5985 4         17 my $terminator = $rslt[-1]{token};
5986 4         12 my $look_for = $terminator . "\n";
5987 4         10 $rslt[-1]{token} = '';
5988 4         11 $rslt[-1]{expand} = $quote ne q<'>;
5989 4         8 while ( 1 ) {
5990 9         48 my $buffer = $self->_read_continuation( $in,
5991             "Here doc terminator $terminator not found" );
5992 9 100       29 $buffer eq $look_for and last;
5993 5         14 $rslt[-1]{token} .= $buffer;
5994             }
5995 4 100       12 if ( $quote ne q<'> ) {
5996             $rslt[-1]{token} = __tokenize(
5997             $self,
5998             { single => 1, noredirect => 1, in => $in },
5999 3         99 $rslt[-1]{token}, $args
6000             );
6001             }
6002 4         14 push @rslt, {}; # New token
6003             }
6004              
6005             # If the token already exists at this point, the current
6006             # character, whatever it is, is simply appended to it.
6007              
6008             } elsif (exists $rslt[-1]{token} || $relquote) {
6009             # do case modification
6010             # NOTE that the argument list is modified in-place.
6011 3745         9889 $self->_case_mod( $char );
6012 3745         6632 $rslt[-1]{token} .= $char;
6013              
6014             # If the character is a tilde, we flag the token for tilde
6015             # expansion.
6016              
6017             } elsif ($char eq '~') {
6018 12         81 $rslt[-1]{tilde}++;
6019 12         58 $rslt[-1]{token} .= $char;
6020              
6021             # If the character is a hash mark, it means a comment. Bail
6022             # out of the loop.
6023             } elsif ( $char eq '#' ) {
6024 2         5 last;
6025              
6026             # Else we just put it in the token.
6027             } else {
6028 829         2843 $rslt[-1]{token} .= $char;
6029             }
6030              
6031             # If we're at the end of the buffer but we're inside quotes,
6032             # we need to read another line.
6033 6306 100 66     18966 if ( $inx >= $len && ( $absquote || $relquote ) ) {
      100        
6034 2 50       9 $buffer .= $self->_read_continuation( $in,
6035             $absquote ? 'Unclosed single quote' :
6036             'Unclosed double quote'
6037             );
6038 2         5 $len = length $buffer;
6039             }
6040              
6041             }
6042              
6043             # We have run through the entire string to be tokenized. If
6044             # there are unclosed quotes of either sort, we declare an error
6045             # here. This should actually not happen, since we allow
6046             # multi-line quotes, and if we have run out of input we catch it
6047             # above.
6048              
6049 376 50       866 $absquote and $self->wail( 'Unclosed terminal single quote' );
6050 376 50       851 $relquote and $self->wail( 'Unclosed terminal double quote' );
6051              
6052             # Replace leading punctuation with the corresponding method.
6053              
6054             shift @rslt
6055 376   100     1911 while @rslt && ! defined $rslt[0]{token};
6056 376 50 66     4457 if ( defined $rslt[0]{token} and
6057             $rslt[0]{token} =~ s/ \A ( $command_equiv_re ) //smx ) {
6058 0 0       0 if ( $rslt[0]{token} eq '' ) {
    0          
6059 0         0 $rslt[0]{token} = $command_equivalent{$1};
6060             } elsif ( $opt->{single} ) {
6061             $rslt[0]{token} = join ' ', $command_equivalent{$1},
6062 0         0 $rslt[0]{token};
6063             } else {
6064             unshift @rslt, {
6065 0         0 token => $command_equivalent{$1},
6066             };
6067             }
6068             }
6069              
6070             # Go through our prospective tokens, keeping only those that
6071             # were actually defined, and shuffling the redirects off into
6072             # the redirect hash.
6073              
6074 376         940 my (@tokens, %redir);
6075 376         639 my $expand_tildes = 1;
6076 376 100 100     4183 if ( defined $rslt[0]{token}
6077             and my $kode = $self->can( $rslt[0]{token} ) ) {
6078 252 100       1123 if ( my $hash = $self->__get_attr( $kode, 'Tokenize' ) ) {
6079 2         16 $expand_tildes = $hash->{expand_tilde};
6080             }
6081             }
6082 376         1000 foreach (@rslt) {
6083 1318 100       3432 exists $_->{token} or next;
6084 966 100 66     3505 if ($_->{redirect}) {
    100          
6085 1 50       5 if ( $_->{mode} eq '<' ) {
6086             push @tokens, $self->_file_reader(
6087 0         0 $_->{token}, { glob => 1 } );
6088             } else {
6089 1         3 my $type = $_->{type};
6090             $redir{$type} = {
6091             mode => $_->{mode},
6092             name => ($_->{expand} ?
6093             $self->expand_tilde($_->{token}) :
6094 1 50       16 $_->{token}),
6095             };
6096             }
6097             } elsif ( $expand_tildes && $_->{tilde} ) {
6098 12         130 push @tokens, $self->expand_tilde( $_->{token} );
6099             } else {
6100 953         2559 push @tokens, $_->{token};
6101             }
6102             }
6103              
6104             # With the {single} and {noredirect} options both asserted,
6105             # there is only one token, so we return it directly.
6106              
6107 372 50 66     1346 ($opt->{single} && $opt->{noredirect}) and return $tokens[0];
6108              
6109             # With the {noredirect} option asserted, we just return a
6110             # reference to the tokens found.
6111              
6112 341 50       1041 $opt->{noredirect} and return \@tokens;
6113              
6114             # Otherwise we return a list, with a reference to the token list
6115             # as the first element, and a reference to the redirect hash as
6116             # the second element.
6117              
6118 341         3155 return (\@tokens, \%redir);
6119             }
6120              
6121             # Retrieve the value of a variable.
6122             sub _tokenize_var {
6123 74     74   263 my ($self, $name, $args, $relquote, $indirect) = @_;
6124              
6125 74 0 33     356 defined $name and $name ne ''
    50          
6126             or return $indirect ? '' : undef;
6127              
6128 74 100       275 $special{$name} and do {
6129 19         97 my $val = $special{$name}->($args, $relquote);
6130 19 50 33     116 return ($indirect && ref $val) ? '' : $val;
6131             };
6132              
6133 55 100       280 $name !~ m/ \D /smx
6134             and return $args->[$name - 1];
6135              
6136 40         153 my $value = $self->_attribute_value( $name );
6137 40 100       151 NULL_REF eq ref $value
6138             or return $value;
6139              
6140             exists $self->{exported}{$name}
6141 34 100       210 and return $self->{exported}{$name};
6142              
6143             defined $ENV{$name}
6144 32 100       146 and return $ENV{$name};
6145              
6146 14         30 foreach my $frame ( reverse @{ $self->{frame} } ) {
  14         95  
6147             defined $frame->{define}{$name}
6148 17 100       96 and return $frame->{define}{$name};
6149             }
6150              
6151 11         37 return;
6152             }
6153              
6154             }
6155              
6156             # Apply case modification to the arguments
6157             # NOTE that the argument list is modified in-place. I'm a little
6158             # surprised that this didn't tickle Perl::Critic.
6159             sub _case_mod {
6160 3757     3757   6048 my $self = shift;
6161 3757         6669 foreach ( @_ ) {
6162             $self->{_case_mod}{case}
6163 3760 100       8351 and $_ = $self->{_case_mod}{case}->( $self, $_ );
6164 3760         5533 my $code;
6165             $code = delete $self->{_case_mod}{single}
6166 3760 100       9262 and $_ = $code->( $self, $_ );
6167             }
6168 3757         6112 return;
6169             }
6170              
6171             # $self->wail(...)
6172             #
6173             # Either die or croak with the arguments, depending on the value
6174             # of the 'warning' attribute. If we die, a trailing period and
6175             # newline are provided if necessary. If we croak, any trailing
6176             # punctuation and newline are stripped.
6177              
6178             sub wail {
6179 19     19 1 73 my ($self, @args) = @_;
6180 19         224 $self->{_warner}->wail( @args );
6181 0         0 return; # We can't hit this, but Perl::Critic does not know that.
6182             }
6183              
6184             # $self->__wail(...)
6185             #
6186             # either wail() or whinge() depending on error_out.
6187             sub __wail {
6188 1     1   4 my ($self, @args) = @_;
6189 1 50       5 if ( $self->get( 'error_out' ) ) {
6190 1         5 $self->{_warner}->wail( @args );
6191             } else {
6192 0         0 $self->{_warner}->whinge( @args );
6193             }
6194 0         0 return;
6195             }
6196              
6197             # $self->weep(...)
6198             #
6199             # Die with a stack dump (Carp::confess).
6200              
6201             sub weep {
6202 0     0 1 0 my ($self, @args) = @_;
6203 0         0 $self->{_warner}->weep( @args );
6204 0         0 return; # We can't hit this, but Perl::Critic does not know that.
6205             }
6206              
6207             # $self->whinge(...)
6208             #
6209             # Either warn or carp with the arguments, depending on the value
6210             # of the 'warn' attribute. If we warn, a trailing period and
6211             # newline are provided if necessary. If we carp, any trailing
6212             # punctuation and newline are stripped.
6213              
6214             sub whinge {
6215 3     3 1 10 my ($self, @args) = @_;
6216 3         59 $self->{_warner}->whinge( @args );
6217 3         14 return;
6218             }
6219              
6220             1;
6221              
6222             __END__