File Coverage

blib/lib/Astro/App/Satpass2.pm
Criterion Covered Total %
statement 1645 2453 67.0
branch 616 1338 46.0
condition 163 419 38.9
subroutine 275 348 79.0
pod 62 66 93.9
total 2761 4624 59.7


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