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   2277 use 5.008;
  20         86  
4              
5 20     20   117 use strict;
  20         46  
  20         393  
6 20     20   93 use warnings;
  20         54  
  20         693  
7              
8 20     20   9233 use Astro::App::Satpass2::Locale qw{ __localize };
  20         58  
  20         1205  
9 20     20   8923 use Astro::App::Satpass2::Macro::Command;
  20         67  
  20         611  
10 20     20   8767 use Astro::App::Satpass2::Macro::Code;
  20         68  
  20         659  
11 20     20   9191 use Astro::App::Satpass2::ParseTime;
  20         60  
  20         960  
12 20         3982 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   142 };
  20         51  
20              
21 20     20   19936 use Astro::Coord::ECI 0.077; # This needs at least 0.049.
  20         276992  
  20         768  
22 20     20   10623 use Astro::Coord::ECI::Moon 0.077;
  20         122439  
  20         794  
23 20     20   9986 use Astro::Coord::ECI::Star 0.077;
  20         182252  
  20         892  
24 20     20   10766 use Astro::Coord::ECI::Sun 0.077;
  20         75997  
  20         825  
25 20     20   32270 use Astro::Coord::ECI::TLE 0.077 qw{:constants}; # This needs at least 0.059.
  20         1138531  
  20         5491  
26 20     20   12815 use Astro::Coord::ECI::TLE::Set 0.077;
  20         56417  
  20         928  
27             # The following includes @CARP_NOT.
28 20     20   165 use Astro::Coord::ECI::Utils 0.112 qw{ :all }; # This needs at least 0.112.
  20         801  
  20         9645  
29              
30             {
31             local $@ = undef;
32 20   50     58 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   191 } || 0;
  20         48  
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   12591 use Attribute::Handlers;
  20         103077  
  20         149  
49 20     20   724 use Clone ();
  20         60  
  20         287  
50 20     20   131 use Cwd ();
  20         42  
  20         402  
51 20     20   130 use File::Glob qw{ :glob };
  20         59  
  20         5128  
52 20     20   168 use File::HomeDir;
  20         41  
  20         1167  
53 20     20   162 use File::Spec;
  20         65  
  20         723  
54 20     20   17221 use File::Temp;
  20         228230  
  20         1560  
55 20     20   189 use Getopt::Long 2.33;
  20         317  
  20         628  
56 20     20   3106 use IO::File 1.14;
  20         448  
  20         2808  
57 20     20   161 use IO::Handle;
  20         68  
  20         722  
58 20     20   120 use POSIX qw{ floor };
  20         87  
  20         201  
59 20     20   1570 use Scalar::Util 1.26 qw{ blessed isdual openhandle };
  20         423  
  20         1063  
60 20     20   14376 use Text::Abbrev;
  20         904  
  20         1031  
61 20     20   148 use Text::ParseWords (); # Used only for {level1} stuff.
  20         46  
  20         404  
62              
63 20     20   99 use constant ASTRO_SPACETRACK_VERSION => 0.105;
  20         53  
  20         1188  
64 20     20   148 use constant DEFAULT_STDOUT_LAYERS => ':encoding(utf-8)';
  20         58  
  20         1897  
65              
66             BEGIN {
67             eval {
68 20 50       293 load_package( 'Time::y2038' )
69             and Time::y2038->import();
70 20         1348 1;
71             }
72 20 50   20   79 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         68 my $x = undef;
86 20         1572 bless \$x, 'Null';
87 20     20   147 };
  20         45  
88             # The canonical way to see if $rslt actually contains the above is
89             # NULL_REF eq ref $rslt
90 20     20   155 use constant NULL_REF => ref NULL;
  20         41  
  20         1092  
91              
92 20     20   164 use constant SUN_CLASS_DEFAULT => 'Astro::Coord::ECI::Sun';
  20         53  
  20         9362  
93              
94             our $VERSION = '0.052';
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   194 }
  20         81  
  20         166  
210              
211             sub Tokenize : ATTR(CODE,RAWDATA) {
212 19     19 0 33645 my ( undef, undef, $code, $name, $data ) = @_;
213 19         117 my $opt = _attr_hash( $name, $data, qw{ expand_tilde|expand-tilde! } );
214             exists $opt->{expand_tilde}
215 19 50       164 or $opt->{expand_tilde} = 1;
216 19         89 $attr{$code}{$name} = $opt;
217 19         64 return;
218 20     20   20789 }
  20         79  
  20         104  
219              
220             sub Tweak : ATTR(CODE,RAWDATA) {
221 266     266 0 7692 my ( undef, undef, $code, $name, $data ) = @_;
222 266         670 $attr{$code}{$name} = _attr_hash( $name, $data,
223             qw{ completion=s unsatisfied! } );
224 266         802 return;
225 20     20   19577 }
  20         51  
  20         104  
226              
227             sub Verb : ATTR(CODE,RAWDATA) {
228 1239     1239 0 2179558 my ( undef, undef, $code, $name, $data ) = @_;
229 1239         2645 $attr{$code}{$name} = _attr_list( $data );
230 1239         3486 return;
231 20     20   19343 }
  20         73  
  20         136  
232              
233             sub _attr_hash {
234 285     285   739 my ( $name, $arg, @legal ) = @_;
235 285         1009 my $gol = Getopt::Long::Parser->new();
236 285         5135 my %opt;
237             $gol->getoptionsfromarray(
238             _attr_list( $arg ),
239             \%opt,
240             @legal,
241 285 50       564 ) or do {
242 0         0 require Carp;
243 0         0 Carp::croak( "Bad $name option" );
244             };
245 285         94564 return \%opt;
246             }
247              
248             sub _attr_list {
249 1524 50   1524   4247 defined( local $_ = $_[0] )
250             or return [];
251 1524         5700 s/ \A \s+ //smx;
252 1524         15411 return [ split qr< \s+ >smx ];
253             }
254              
255             sub __get_attr {
256 1235     1235   2755 my ( undef, $code, $name, $dflt ) = @_; # $pkg unused
257 1235 50       2637 defined $code
258             or return \%attr;
259             defined $name
260 1235 50       2251 or return $attr{$code};
261             exists $attr{$code}{$name}
262 1235 100       5120 and return $attr{$code}{$name};
263 628         2467 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 6993 my ( $class, %args ) = @_;
445 7 50       41 ref $class and $class = ref $class;
446 7         20 my $self = {};
447 7         31 $self->{bodies} = [];
448 7         23 $self->{macro} = {};
449             $self->{sky} = [
450 7         75 SUN_CLASS_DEFAULT->new (),
451             Astro::Coord::ECI::Moon->new (),
452             ];
453 7         3207 $self->{sky_class} = { %sky_class };
454             $self->{_help_module} = {
455 7         78 '' => __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         15 and $self->{_help_module}{iridium} = 'Astro::Coord::ECI::TLE::Iridium';
467 7         20 bless $self, $class;
468 7         46 $self->_frame_push(initial => []);
469 7         55 $self->set(stdout => select());
470              
471 7         114 foreach my $name ( keys %static ) {
472 301 50       801 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         107 );
478              
479 7         45 foreach my $name ( qw{ formatter time_parser } ) {
480 14         68 $self->set( $name => delete $args{$name} );
481             }
482              
483 7         149 $self->set( %args );
484              
485 7         91 return $self;
486             }
487              
488             sub add {
489 1     1 1 52 my ( $self, @bodies ) = @_;
490 1         33 foreach my $body ( @bodies ) {
491 1 50       56 embodies( $body, 'Astro::Coord::ECI::TLE' )
492             or $self->wail(
493             'Arguments must represent Astro::Coord::ECI::TLE objects' );
494             }
495 1         92 push @{ $self->{bodies} }, @bodies;
  1         49  
496 1         32 return $self;
497             }
498              
499             sub alias : Verb() {
500 5     5 1 18 my ( undef, undef, @args ) = __arguments( @_ ); # Invocant, $opt unused
501              
502 5 100       18 if ( @args ) {
503 2         14 Astro::Coord::ECI::TLE->alias( @args );
504 2         56 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         11 return $output;
512             }
513 20     20   50106 }
  20         61  
  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 19 my ( $self, $opt, @args ) = __arguments( @_ );
519 3         30 $self->_apply_boolean_default(
520             $opt, 0, qw{ horizon transit twilight quarter } );
521              
522 3         25 my $almanac_start = $self->__parse_time(
523             shift @args, $self->_get_day_midnight());
524 3   50     18 my $almanac_end = $self->__parse_time (shift @args || '+1');
525              
526 3 50       14 $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         19 my $sta = $self->station();
532              
533 3         1180 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       27 or return $self->__wail( 'No bodies selected' );
541              
542 3         9 foreach my $body ( @sky ) {
543 6 50       512024 $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         52 );
552 6         720 push @almanac, $body->almanac_hash(
553             $almanac_start, $almanac_end);
554             }
555              
556             # Record number of events found
557              
558 3         949266 @almanac = grep { $opt->{$_->{almanac}{event}} } @almanac;
  27         91  
559 3         20 $self->{events} += @almanac;
560              
561             # Localize the event descriptions if appropriate.
562              
563 3         20 _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         23 sort { $a->{time} <=> $b->{time} }
  41         101  
570             @almanac
571             ], $opt );
572              
573 20     20   11150 }
  20         82  
  20         139  
574             sub _almanac_localize {
575 9     9   38 my @almanac = @_;
576 9         34 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         145 );
583             }
584 9         39 return;
585             }
586              
587             sub begin : Verb() Tweak( -unsatisfied ) {
588 5     5 1 27 my ( $self, $opt, @args ) = __arguments( @_ );
589             $self->_frame_push(
590 5 50       52 begin => @args ? \@args : $self->{frame}[-1]{args});
591 5         27 $self->{frame}[-1]{level1} = $opt->{level1};
592 5         17 return;
593 20     20   8187 }
  20         90  
  20         150  
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   8112 and __PACKAGE__->MODIFY_CODE_ATTRIBUTES(
600             \&begin,
601             'Verb( level1! )',
602             );
603             }
604              
605             sub cd : Verb() {
606 2     2 1 119 my ( $self, undef, $dir ) = __arguments( @_ ); # $opt unused
607 2 100       44 if (defined($dir)) {
608 1 50       73 chdir $dir or $self->wail("Can not cd to $dir: $!");
609             } else {
610 1 50       47 chdir File::HomeDir->my_home()
611             or $self->wail("Can not cd to home: $!");
612             }
613 2         108 return;
614 20     20   209 }
  20         70  
  20         120  
615              
616             sub choose : Verb( epoch=s ) {
617 2     2 1 10 my ( $self, $opt, @args ) = __arguments( @_ );
618              
619 2 50       17 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       14 if ( @args ) {
629 2 50       4 my @bodies = @{ $self->__choose( \@args, $self->{bodies} ) }
  2         18  
630             or return $self->__wail( 'No bodies chosen' );
631 2         7 @{ $self->{bodies} } = @bodies;
  2         15  
632             }
633 2         6 return;
634 20     20   7214 }
  20         121  
  20         122  
635              
636             sub clear : Verb() {
637 5     5 1 67 my ( $self ) = __arguments( @_ ); # $opt, @args unused
638 5         52 @{$self->{bodies}} = ();
  5         112  
639 5         53 return;
640 20     20   5225 }
  20         72  
  20         123  
641              
642             sub dispatch {
643 289     289 1 808 my ($self, $verb, @args) = @_;
644              
645 289 50       697 defined $verb or return;
646              
647 289         1020 my $unsatisfied = $self->_in_unsatisfied_if();
648              
649 289 100       760 if ( $self->{macro}{$verb} ) {
650 19 50       48 $unsatisfied
651             and return;
652 19         67 return $self->_macro( $verb, @args );
653             }
654              
655 270         412 my $code;
656 270         577 $verb =~ s/ \A core [.] //smx;
657 270 100 66     1291 $code = $self->can($verb)
658             and $self->__get_attr($code, 'Verb')
659             or $self->wail("Unknown interactive method '$verb'");
660              
661 269         561 my $rslt;
662             $unsatisfied
663             and not $self->__get_attr( $code, Tweak => {} )->{unsatisfied}
664 269 100 100     1429 or $rslt = $code->( $self, @args );
665              
666 261 100       37330 defined $rslt
667             and $rslt =~ s/ (?
668              
669 261         443 foreach my $code (
670 261 100       1585 reverse @{ delete( $self->{frame}[-1]{post_dispatch} ) || [] }
671             ) {
672 23         36 my $append;
673 23 100       54 defined( $append = $code->( $self ) )
674             and $rslt .= $append;
675             }
676 261         1119 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   64 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       6 or return;
707              
708             my @bodies = @{
709 1 50       4 $self->__choose( { invert => 1 }, \@args, $self->{bodies} ) }
  1         18  
710             or return $self->__wail( 'No bodies left' );
711              
712 1         11 @{ $self->{bodies} } = @bodies;
  1         5  
713              
714 1         4 return;
715 20     20   14831 }
  20         90  
  20         118  
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   9898 }
  20         57  
  20         97  
753              
754             sub echo : Verb( n! ) {
755 44     44 1 177 my ( undef, $opt, @args ) = __arguments( @_ ); # Invocant unused
756 44         166 my $output = join( ' ', @args );
757 44 50       156 $opt->{n} or $output .= "\n";
758 44         138 return $output;
759 20     20   6081 }
  20         70  
  20         125  
760              
761             sub else : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
762 2     2 1 12 my ( $self ) = __arguments( @_ ); # $opt, @args unused
763              
764 2         24 @{ $self->{frame} } > 1
765             and 'begin' eq $self->{frame}[-1]{type}
766             and 'if' eq $self->{frame}[-2]{type}
767 2 50 33     7 or $self->wail( 'Else without if ... then begin' );
      33        
768              
769 2 50       12 $self->{frame}[-1]{in_else}++
770             and $self->wail( 'Only one else may follow an if' );
771              
772 2         12 return $self->_twiddle_condition( ! $self->{frame}[-2]{condition} );
773 20     20   6577 }
  20         51  
  20         150  
774              
775             sub _twiddle_condition {
776 4     4   10 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     14 0
790             );
791              
792             $self->{frame}[-1]{condition} =
793 4         10 $self->{frame}[-2]{condition} = $cond;
794              
795 4         14 return;
796             }
797              
798             sub end : Verb() Tweak( -unsatisfied ) {
799 5     5 1 52 my ( $self ) = __arguments( @_ ); # $opt, @args unused
800              
801 5 50       58 $self->{frame}[-1]{type} eq 'begin'
802             or $self->wail( 'End without begin' );
803 5         53 $self->_frame_pop();
804 5         25 return;
805 20     20   8564 }
  20         78  
  20         117  
806              
807             sub error : Verb() {
808 1     1 1 6 my ( $self, undef, @arg ) = __arguments( @_ );
809             @arg
810 1 50       7 or push @arg, 'An error has occurred';
811 1         7 $self->wail( @arg );
812 0         0 return;
813 20     20   5874 }
  20         56  
  20         142  
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 768 my ($self, @args) = @_;
823 264         668 my $accum;
824             my $in;
825 264         0 my $extern;
826 264 100       712 if ( CODE_REF eq ref $args[0] ) {
827 13         30 $extern = shift @args;
828             $in = sub {
829 21     21   41 my ( $prompt ) = @_;
830 21 100       67 @args and return shift @args;
831 8         34 return $extern->( $prompt );
832 13         70 };
833             } else {
834 251     502   1222 $in = sub { return shift @args };
  502         1693  
835             }
836 264         685 @args = map { split qr{ (?<= \n ) }smx, $_ } @args;
  265         3164  
837 264         848 while ( defined ( local $_ = $in->( $self->get( 'prompt' ) ) ) ) {
838 280 50       810 $self->{echo} and $self->whinge($self->get( 'prompt' ), $_);
839 280 100       900 m/ \A \s* [#] /smx and next;
840 277         659 my $stdout = $self->{frame}[-1]{stdout};
841             my ($args, $redirect) = $self->__tokenize(
842 277         1322 { 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       1134 $self->{execute_filter}->( $self, $args ) or next;
857 267 100       1063 @{ $args } or next;
  267         791  
858 266 100       636 if ($redirect->{'>'}) {
859 1         15 my ( $mode, $name ) = map { $redirect->{'>'}{$_} } qw{ mode name };
  2         8  
860 1         3 my $fh;
861             $stdout = sub {
862 1     1   6 my ( $output ) = @_;
863 1   33     9 $fh ||= $self->_file_opener( $name, $mode );
864 1         14 $fh->print( $output );
865 1         26 return;
866 1         6 };
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         448 my $frame_depth = $#{$self->{frame}};
  266         667  
875 266         760 $self->{frame}[-1]{localout} = $stdout;
876              
877 266         761 my $output = $self->dispatch( @$args );
878              
879 256         1047 $#{$self->{frame}} >= $frame_depth
880 256 100       406 and delete $self->{frame}[ $frame_depth ]{localout};
881              
882 256 100       1258 $self->_execute_output( $output,
883             defined $stdout ? $stdout : \$accum );
884              
885 256 100       1559 $extern and last;
886             }
887 250         1500 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   677 my ( undef, $output, $stdout ) = @_; # Invocant unused
916 256 100       586 defined $output or return;
917 152         444 my $ref = ref $stdout;
918 152 50       602 if ( !defined $stdout ) {
    100          
    100          
    50          
919 0         0 return $output;
920             } elsif ( SCALAR_REF eq $ref ) {
921 149         446 $$stdout .= $output;
922             } elsif ( CODE_REF eq $ref ) {
923 2         7 $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         281 return;
930             }
931              
932             sub exit : method Verb() { ## no critic (ProhibitBuiltInHomonyms)
933 1     1 1 6 my ( $self ) = __arguments( @_ ); # $opt, @args unused
934              
935 1         13 $self->_frame_pop(1); # Leave only the inital frame.
936              
937 1         3 eval { ## no critic (RequireCheckingReturnValueOfEval)
938 20     20   22716 no warnings qw{exiting};
  20         45  
  20         1935  
939 1         10 last SATPASS2_EXECUTE;
940             };
941 0         0 $self->whinge("$@Exiting Perl");
942 0         0 exit;
943              
944 20     20   161 }
  20         54  
  20         104  
945              
946             sub export : Verb() {
947 4     4 1 34 my ( $self, undef, $name, @args ) = __arguments( @_ ); # $opt unused
948 4 100       25 if ($mutator{$name}) {
949 1 50       15 @args and $self->set ($name, shift @args);
950 1         7 $self->{exported}{$name} = 1;
951             } else {
952 3 100       46 @args or return $self->wail( 'You must specify a value' );
953 2         24 $self->{exported}{$name} = shift @args;
954             }
955 3         11 return;
956 20     20   6163 }
  20         91  
  20         111  
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   14110 }
  20         57  
  20         113  
1035              
1036             sub formatter : Verb() Tweak( -completion _readline_complete_subcommand ) {
1037 9 50   9 1 41 splice @_, ( HASH_REF eq ref $_[1] ? 2 : 1 ), 0, 'formatter';
1038 9         45 goto &_helper_handler;
1039 20     20   5394 }
  20         54  
  20         119  
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   10809 }
  20         57  
  20         110  
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   9332 }
  20         72  
  20         119  
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   6372 }
  20         47  
  20         123  
1137              
1138             sub get {
1139 927     927 1 2300 my ($self, $name) = @_;
1140 927         2799 $self->_attribute_exists( $name );
1141 927         2926 $self->_deprecation_notice( attribute => $name );
1142 927         3378 return $accessor{$name}->($self, $name);
1143             }
1144              
1145             sub height : Verb( debug! ) {
1146 0     0 1 0 return _height_us( __arguments( @_ ) );
1147 20     20   6227 }
  20         47  
  20         102  
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   13776 }
  20         54  
  20         101  
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 7 my ( $self, @args ) = @_;
1477             @args
1478 2 50       8 or $self->wail( 'Arguments required' );
1479              
1480 2         39 @{ $self->{frame} } > 1
1481             and 'begin' eq $self->{frame}[-1]{type}
1482             and 'if' eq $self->{frame}[-2]{type}
1483 2 50 33     4 or $self->wail( 'Elsif without if ... then begin' );
      33        
1484              
1485 2         12 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         9 return $self->__infix_engine( \%define, \@ctx, @args );
1495 20     20   44580 }
  20         84  
  20         142  
1496              
1497             sub if : method Verb() Tweak( -unsatisfied ) { ## no critic (ProhibitBuiltInHomonyms)
1498 22     22 1 73 my ( $self, @args ) = @_;
1499             @args
1500 22 50       75 or $self->wail( 'Arguments required' );
1501 22         88 my @ctx = ( {
1502             dispatch => 1,
1503             value => [],
1504             } );
1505 22         81 return $self->__infix_engine( \%define, \@ctx, @args );
1506 20     20   6576 }
  20         80  
  20         113  
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   13034 }
  20         60  
  20         2639  
1563              
1564             sub _in_unsatisfied_if {
1565 290     290   642 my ( $self ) = @_;
1566 290 50       442 return @{ $self->{frame} } ? $self->{frame}[-1]{unsatisfied_if} : 0;
  290         1154  
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   80 my ( $self, $def, $ctx, @tokens ) = @_;
1604             @tokens
1605 24 50       92 or $self->wail( 'Nothing to compute' );
1606 24         34 my $rslt;
1607 24         54 while ( @tokens ) {
1608 50         132 $rslt = $self->_infix_engine_dispatch( $def, $ctx, \@tokens );
1609             }
1610             $def->{done}
1611 24 50       103 and $def->{done}->( $self, $def, $ctx, \@tokens );
1612 24         84 return $rslt;
1613             }
1614              
1615             sub _infix_engine_dispatch {
1616 61     61   116 my ( $self, $def, $ctx, $tokens ) = @_;
1617 61 50       78 @{ $tokens }
  61         119  
1618             or return;
1619 61         85 my $tkn = shift @{ $tokens };
  61         104  
1620 61 100       164 if ( my $info = $def->{oper}{$tkn} ) {
    50          
1621             $info->{validation}
1622 57 100       213 and $def->{vld}{ $info->{validation} }->(
1623             $self, $def, $ctx, $tkn, $tokens );
1624 57         144 return $info->{handler}->( $self, $def, $ctx, $tokens );
1625             } elsif ( $def->{val} ) {
1626 4         12 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 59 my ( $self, $opt, @args ) = __arguments( @_ );
1653              
1654             @args
1655             and not $opt->{choose}
1656 7 50 33     62 and $opt->{choose} = \@args;
1657 7         98 my @bodies = $self->__choose( $opt->{choose}, $self->{bodies} );
1658              
1659             @bodies
1660 7 100       101 and return $self->__format_data(
1661             list => \@bodies, $opt );
1662              
1663             $self->{warn_on_empty}
1664 2 50       38 and $self->whinge( 'The observing list is empty' );
1665              
1666 2         54 return;
1667 20     20   21140 }
  20         1305  
  20         1344  
1668              
1669             sub load : Verb( verbose! ) {
1670 6     6 1 56 my ( $self, $opt, @names ) = __arguments( @_ );
1671 6 50       47 @names or $self->wail( 'No file names specified' );
1672              
1673 6         31 my $attrs = {
1674             illum => $self->get( 'illum' ),
1675             model => $self->get( 'model' ),
1676             sun => $self->_sky_object( 'sun' ),
1677             };
1678              
1679 6         722 foreach my $fn ( @names ) {
1680 6 50       25 $opt->{verbose} and warn "Loading $fn\n";
1681 6         49 my $data = $self->_file_reader( $fn, { glob => 1 } );
1682 5         326 $self->__add_to_observing_list(
1683             Astro::Coord::ECI::TLE->parse( $attrs, $data ) );
1684             }
1685 5         34 return;
1686 20     20   6665 }
  20         42  
  20         1367  
1687              
1688             sub localize : Verb( all|except! ) {
1689 1     1 1 16 my ( $self, $opt, @args ) = __arguments( @_ );
1690              
1691 1         10 foreach my $name ( @args ) {
1692 2         22 $self->_attribute_exists( $name );
1693             }
1694              
1695 1 50       7 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         4 foreach my $name ( @args ) {
1701 2         13 $self->_localize( $name );
1702             }
1703              
1704 1         3 return;
1705 20     20   10865 }
  20         1414  
  20         1446  
1706              
1707             sub _localize {
1708 2     2   6 my ( $self, $key ) = @_;
1709              
1710             my $val = exists $self->{$key} ?
1711 2 50       8 $self->{$key} :
1712             $self->get( $key );
1713 2 50 33     12 my $clone = ( blessed( $val ) && $val->can( 'clone' ) ) ?
    50          
1714             $val->clone() :
1715             ref $val ? Clone::clone( $val ) : $val;
1716              
1717 2         6 $self->{frame}[-1]{local}{$key} = $val;
1718 2 50       5 if ( exists $self->{$key} ) {
1719 2         5 $self->{$key} = $clone;
1720             } else {
1721 0         0 $self->set( $key => $clone );
1722             }
1723              
1724 2         5 return;
1725             }
1726              
1727             sub location : Verb( dump! ) {
1728 3     3 1 13 my ( $self, $opt ) = __arguments( @_ );
1729 3         27 return $self->__format_data(
1730             location => $self->station(), $opt );
1731 20     20   7303 }
  20         1178  
  20         1284  
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   5726 no strict qw{ refs };
  20         44  
  20         6869  
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 103 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
1770 29         77 my $cmd;
1771 29 50       117 if (!@args) {
    100          
1772 0         0 $cmd = 'brief';
1773             } elsif ( $self->{frame}[-1]{level1} ) {
1774 8 50       47 if ($mac_cmd{$args[0]}) {
    50          
1775 0         0 $cmd = $mac_cmd{shift @args};
1776             } elsif (@args > 1) {
1777 8         17 $cmd = 'define';
1778             } else {
1779 0         0 $cmd = 'list';
1780             }
1781             } else {
1782 21 50       83 defined( $cmd = $mac_cmd{ $args[0] } )
1783             or $cmd = $args[0];
1784 21         41 shift @args;
1785             }
1786              
1787 29 50       167 my $code = $self->can( "_macro_sub_$cmd" )
1788             or $self->wail( "Subcommand '$cmd' unknown" );
1789 29         109 return $code->( $self, @args );
1790 20     20   179 }
  20         51  
  20         98  
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   11 my ( $self, undef, @args ) = __arguments( @_ );
1797 3         8 my $output;
1798 3 50       10 foreach my $name (sort @args ? @args : keys %{$self->{macro}}) {
  3         15  
1799 1 50       11 $self->{macro}{$name} and $output .= $name . "\n";
1800             }
1801 3         10 return $output;
1802 20     20   6996 }
  20         51  
  20         130  
1803              
1804             sub _macro_sub_define : Verb( completion=s@ ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1805 16     16   51 my ( $self, $opt, $name, @args ) = __arguments( @_ );
1806 16         38 my $output;
1807 16 50       49 defined $name
1808             or return $self->__wail( 'You must provide a name for the macro' );
1809             @args
1810 16 50       82 or return $self->__wail( 'You must provide a definition for the macro' );
1811              
1812 16 50 33     109 $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         164 );
1829 16         73 return $output;
1830 20     20   8847 }
  20         49  
  20         82  
1831              
1832             sub _macro_define_generator {
1833 9     9   24 my ( $self, @args ) = @_; # $self if Macro object
1834 9         17 my $output;
1835 9         21 foreach my $macro ( @args ) {
1836 9 50       36 if ( my $comp = $self->completion() ) {
1837 0         0 $output .= "macro define \\\n " .
1838             "--completion '@$comp' \\\n " .
1839             "$macro \\\n ";
1840             } else {
1841 9         29 $output .= "macro define $macro \\\n ";
1842             }
1843 9         38 $output .= join( " \\\n ", map { quoter( $_ ) } $self->def() ) .
  17         45  
1844             "\n";
1845             }
1846 9         37 return $output;
1847             }
1848              
1849             sub _macro_sub_delete : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1850 1     1   5 my ( $self, undef, @args ) = __arguments( @_ );
1851 1         4 my $output;
1852 1 50       12 foreach my $name (@args ? @args : keys %{$self->{macro}}) {
  0         0  
1853 1         20 delete $self->{macro}{$name};
1854             }
1855 1         5 return $output;
1856 20     20   9136 }
  20         57  
  20         144  
1857              
1858             sub _macro_sub_list : Verb() Tweak( -completion _macro_list_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
1859 9     9   32 my ( $self, undef, @args ) = __arguments( @_ );
1860 9         21 my $output;
1861 9 100       32 foreach my $name (sort @args ? @args : keys %{$self->{macro}}) {
  1         40  
1862 9 50       39 $self->{macro}{$name}
1863             or next;
1864 9         50 $output .= $self->{macro}{$name}->generator( $name );
1865             }
1866 9         27 return $output;
1867 20     20   6883 }
  20         45  
  20         97  
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   10683 }
  20         45  
  20         142  
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   10333 }
  20         40  
  20         98  
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 127 my ( $self, $opt, @args ) = __arguments( @_ );
1945              
1946             $opt->{ephemeris}
1947 20 100       125 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     113 }->{$opt->{_template}};
1953              
1954 20         118 $self->_apply_boolean_default(
1955             $opt, 0, qw{ horizon illumination transit appulse } );
1956 20         83 $self->_apply_boolean_default( $opt, 0, qw{ am pm } );
1957 20 50 66     146 $opt->{am} or $opt->{pm} or $opt->{am} = $opt->{pm} = 1;
1958 20         136 my $pass_start = $self->__parse_time (
1959             shift @args, $self->_get_day_noon());
1960 20   100     129 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         100 my $sta = $self->station();
1965             my @bodies = $self->__choose( $opt->{choose}, $self->{bodies} )
1966 20 50       8122 or $self->wail( 'No bodies selected' );
1967 20   50     143 my $pass_step = shift @args || 60;
1968              
1969             # Decide which model to use.
1970              
1971 20         102 my $model = $self->{model};
1972              
1973             # Set the station for the objects in the sky.
1974              
1975 20         48 foreach my $body ( @{ $self->{sky} } ) {
  20         75  
1976 41         2190 $body->set( station => $sta );
1977             }
1978              
1979             # Pick up horizon and appulse distance.
1980              
1981 20         1196 my $horizon = deg2rad ($self->{horizon});
1982 20         120 my $appulse = deg2rad ($self->{appulse});
1983 20         115 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       98 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         120 $opt->{brightest} = $self->{pass_variant} & PASS_VARIANT_BRIGHTEST;
2002             }
2003 20         65 my $pass_variant = $self->{pass_variant};
2004              
2005             # Foreach body to be modelled
2006              
2007 20         39 my @accumulate; # For chronological output.
2008 20         104 foreach my $tle ( $self->_aggregate( \@bodies ) ) {
2009              
2010             {
2011 39 50       2637 my $mdl = $tle->get('inertial') ? $model :
  39         172  
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       1393 );
2028             }
2029              
2030             eval {
2031             push @accumulate, $self->_pass_select_event( $opt, $tle->pass (
2032 39         349 $pass_start, $pass_end, $self->{sky} ) );
2033 39         291 1;
2034 39 50       14411 } 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         125 @accumulate = $self->__pass_filter_am_pm( $opt, @accumulate );
2041              
2042             $opt->{chronological}
2043 20 100       102 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         84 $self->{events} += @accumulate;
2050              
2051 20 100       76 if ( $opt->{almanac} ) {
2052 4         11 my %almanac;
2053 4         43 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     493 $almanac{$noon}{$illum} ||= do {
2057 6         21 my @day;
2058              
2059             my @events = grep { {
2060             horizon => 1,
2061             twilight => 1,
2062             }->{$_->{almanac}{event}}
2063 36         1056816 } $illum->almanac_hash(
2064 6         45 $self->_get_day_midnight( $pass->{time} ) );
2065              
2066 6         88 _almanac_localize( @events );
2067              
2068 6         25 foreach my $evt ( @events ) {
2069 24         51 $evt->{event} = 'almanac';
2070 24 100       79 my $pm = $evt->{time} >= $noon ? 1 : 0;
2071 24         43 push @{ $day[$pm] }, $evt;
  24         55  
2072             }
2073              
2074 6         62 \@day;
2075             };
2076              
2077 6 50       33 $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       36 if ( $opt->{ephemeris} ) {
2082 3         24 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time}
2083 3         22 } @{ $pass->{events} }, @{ $almanac{$noon}{$illum}[$pm] };
  26         52  
  3         14  
  3         29  
2084             }
2085             }
2086              
2087 4 100       38 unless( $opt->{ephemeris} ) {
2088 2         8 foreach my $pass ( @accumulate ) {
2089             $pass->{_pm}
2090 3 50       14 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         8 foreach my $pass ( reverse @accumulate ) {
2101             $pass->{_pm}
2102 3 50       13 and next;
2103 3         20 my $illum = $pass->{body}->get( 'illum' );
2104 3         51 my $noon = $self->_get_day_noon( $pass->{time} );
2105 3 50       165 $almanac{$noon}{$illum}[0]
2106             or next;
2107 3         13 @{ $pass->{events} } = sort { $a->{time} <=> $b->{time} }
  26         51  
2108 3         10 @{ $pass->{events} },
2109 3         6 @{ $almanac{$noon}{$illum}[0] };
  3         19  
2110 3         30 $almanac{$noon}{$illum}[0] = undef;
2111             }
2112             }
2113             }
2114              
2115             return $self->__format_data(
2116 20         133 $opt->{_template} => \@accumulate, $opt );
2117              
2118 20     20   26125 }
  20         63  
  20         91  
2119              
2120             sub __pass_filter_am_pm {
2121 20     20   89 my ( $self, $opt, @accumulate ) = @_;
2122 20   50     77 $opt ||= {};
2123             $opt->{am} xor $opt->{pm}
2124 20 100 75     222 or return @accumulate;
2125             return (
2126 6         15 map { $_->[0] }
2127 12   50     152 grep { $opt->{am} xor $_->[1] }
2128 2         5 map { [
2129             $_,
2130             $_->{time} >= $self->_get_day_noon( $_->{time} )
2131 12         350 ] } @accumulate
2132             );
2133             }
2134              
2135             sub __pass_options {
2136 20     20   79 my ( $self, $opt ) = @_;
2137             return [
2138 20         125 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   48618880 my ( undef, $opt, @passes ) = @_; # Invocant unused
2167 39         122 my @rslt;
2168 39         166 foreach my $pass ( @passes ) {
2169 38         241 @{ $pass->{events} } = grep {
2170             _pass_select_event_code( $opt, $_->{event} )
2171 38 50       83 } @{ $pass->{events} }
  136         359  
  38         147  
2172             and push @rslt, $pass;
2173             }
2174             return @rslt
2175 39         143 }
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   274 my ( $opt, $event ) = @_;
2187 136 50 33     415 isdual( $event )
2188             or $event !~ m/ \D /smx
2189             or return 1;
2190 136 50       275 $event == PASS_EVENT_NONE
2191             and return 1;
2192 136   66     686 return defined $selector[ $event ] && $opt->{ $selector[ $event ] };
2193             }
2194             }
2195              
2196             sub perl : Tokenize( -noexpand_tilde ) : Verb( eval! setup! ) {
2197 2     2 1 19 my ( $self, $opt, $file, @args ) = __arguments( @_ );
2198 2 50       15 defined $file
2199             or $self->wail( 'At least one argument is required' );
2200             $opt->{setup}
2201 2 50 0     18 and push @{ $self->{_perl} ||= [] }, [ $opt, $file, @args ];
  0         0  
2202 2         13 local @ARGV = ( $self, map { $self->expand_tilde( $_ ) } @args );
  0         0  
2203             $opt->{eval}
2204 2 100       34 or local $0 = $self->expand_tilde( $file );
2205              
2206             my $data = $opt->{eval} ?
2207 2 100       45 $file :
2208             $self->_file_reader( $file, { glob => 1 } );
2209 2         224 my $rslt = eval $data; ## no critic (BuiltinFunctions::ProhibitStringyEval)
2210 2 100       38 $@
2211             and $self->wail( "Failed to eval '$file': $@" );
2212 1 50       36 instance( $rslt, 'Astro::App::Satpass2' )
2213             or return $rslt;
2214 0         0 return;
2215 20     20   17705 }
  20         45  
  20         89  
2216              
2217             sub phase : Verb( choose=s@ ) {
2218 1     1 1 12 my ( $self, $opt, @args ) = __arguments( @_ );
2219              
2220 1         11 my $time = $self->__parse_time (shift @args, time );
2221              
2222             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
2223 1 50       14 or $self->wail( 'No bodies selected' );
2224             return $self->__format_data(
2225             phase => [
2226 1         7 map { { body => $_->universal( $time ), time => $time } }
2227 1         6 grep { $_->can( 'phase' ) }
  2         18  
2228             @sky
2229             ], $opt );
2230 20     20   7543 }
  20         48  
  20         104  
2231              
2232             sub position : Verb( choose=s@ questionable|spare! quiet! ) {
2233 4     4 1 28925 my ( $self, $opt, $time ) = __arguments( @_ );
2234              
2235 4 50       27 if ( defined $time ) {
2236 4         19 $time = $self->__parse_time($time);
2237             } else {
2238 0         0 $time = time;
2239             }
2240              
2241             # Define the observing station.
2242              
2243 4         24 my $sta = $self->station();
2244 4         1627 $sta->universal( $time );
2245              
2246             my @list = $self->__choose( { bodies => 1, sky => 1 },
2247 4         283 $opt->{choose} );
2248              
2249 4         14 my @good;
2250 4         23 my $horizon = deg2rad ($self->{horizon});
2251 4         37 foreach my $body (@list) {
2252 13 100       63 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         148 );
2262             $body->get('inertial')
2263 4 50       1015 and $body->set( model => $self->{model} );
2264             }
2265             eval {
2266 13         44 $body->universal ($time);
2267 10         3591 push @good, $body;
2268 10         56 1;
2269 13 100       468 } or do {
2270 3 50       2442 $@ =~ m/ \Q$interrupted\E /smxo and $self->wail($@);
2271 3 50       34 $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         39 }, $opt );
2284 20     20   11362 }
  20         61  
  20         132  
2285              
2286             sub pwd : Verb() {
2287 1     1 1 8215 return Cwd::cwd() . "\n";
2288 20     20   4631 }
  20         63  
  20         84  
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 45 my ( $self, $opt, @args ) = __arguments( @_ );
2295              
2296 1         54 my $start = $self->__parse_time (
2297             $args[0], $self->_get_day_midnight() );
2298 1   50     44 my $end = $self->__parse_time ($args[1] || '+30');
2299              
2300 1         19 $self->_apply_boolean_default( $opt, 0, map { "q$_" } 0 .. 3 );
  4         45  
2301              
2302             my @sky = $self->__choose( $opt->{choose}, $self->{sky} )
2303 1 50       27 or $self->wail( 'No bodies selected' );
2304              
2305 1         13 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         15 foreach my $body ( @sky ) {
2313 2 50       40 next unless $body->can ('next_quarter_hash');
2314 2         32 $body->universal ($start);
2315              
2316 2         2740 while (1) {
2317 7         40 my $hash = $body->next_quarter_hash();
2318 7 100       203676 $hash->{time} > $end and last;
2319 5 50       49 $opt->{$quarter_name[$hash->{almanac}{detail}]}
2320             or next;
2321 5         27 push @almanac, $hash;
2322             }
2323             }
2324              
2325             # Localize the event descriptions if appropriate.
2326              
2327 1         6 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         35 );
2335             }
2336              
2337             # Record number of events found
2338              
2339 1         9 $self->{events} += @almanac;
2340              
2341             # Sort and display the quarter-phase information.
2342              
2343             return $self->__format_data(
2344             almanac => [
2345 1         38 sort { $a->{time} <=> $b->{time} }
  9         62  
2346             @almanac
2347             ], $opt );
2348              
2349 20     20   12053 }
  20         48  
  20         153  
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   22751 }
  20         82  
  20         146  
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 478 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
2554              
2555 72         309 while (@args) {
2556 351         1003 my ( $name, $value ) = splice @args, 0, 2;
2557 351         1039 $self->_attribute_exists( $name );
2558 351 100       817 if ( _is_interactive() ) {
2559 28 100       165 $nointeractive{$name}
2560             and $self->wail(
2561             "Attribute '$name' may not be set interactively");
2562 27 50 66     199 defined $value and $value eq 'undef'
2563             and $value = undef;
2564             }
2565 350 50       1130 if ( $mutator{$name} ) {
2566 350         923 $self->_deprecation_notice( attribute => $name );
2567 350         1289 $mutator{$name}->($self, $name, $value);
2568             } else {
2569 0         0 $self->wail("Read-only attribute '$name'");
2570             }
2571             }
2572 71         214 return;
2573 20     20   13850 }
  20         84  
  20         164  
2574              
2575             sub _set_almanac_horizon {
2576 7     7   33 my ( $self, $name, $value ) = @_;
2577 7         38 my $parsed = $self->__parse_angle( { accept => 1 }, $value );
2578 7 50       149 my $internal = looks_like_number( $parsed ) ? deg2rad( $parsed ) :
2579             $parsed;
2580 7         87 my $eci = Astro::Coord::ECI->new();
2581 7         1041 $eci->set( $name => $internal ); # To validate.
2582 7         291 $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   151 my ( $self, $name, $value ) = @_;
2601 31         123 my $angle = $self->__parse_angle( $value );
2602 31 100       154 if ( my $code = $validate{$name} ) {
2603 15 0       75 defined $angle or $self->weep(
    50          
2604             "$name angle is undef for value ", defined $value ? $value : 'undef' );
2605 15 50       86 $code->( $angle )
2606             or $self->wail( "Value $value is invalid for $name" );
2607             }
2608 31         145 $self->{"_$name"} = deg2rad( $angle );
2609 31         347 return ( $self->{$name} = $angle );
2610             }
2611             }
2612              
2613             sub _set_angle_or_undef {
2614 21     21   81 my ( $self, $name, $value ) = @_;
2615 21 100 66     134 defined $value and 'undef' ne $value and goto &_set_angle;
2616 15         82 return ( $self->{$name} = undef );
2617             }
2618              
2619             sub _set_code_ref {
2620 11 50   11   60 CODE_REF eq ref $_[2]
2621             or $_[0]->wail( "Attribute $_[1] must be a code reference" );
2622 11         54 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   106 my ( $self, %arg ) = @_;
2643 14         64 my $old = $self->{$arg{name}};
2644 14         29 my $obj;
2645 14 50       63 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       55 if ( defined $arg{default} ) {
2653             defined $arg{value}
2654             and '' ne $arg{value}
2655 14 50 33     95 or $arg{value} = $arg{default};
2656             }
2657 14 50 33     121 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         126 my ( $pkg, @args ) = $self->__parse_class_and_args( $arg{value} );
2665             my $cls = $self->load_package(
2666 14 50       77 { fatal => 'wail' }, $pkg, @{ $arg{prefix} || [] } );
  14         102  
2667 14 50 33     286 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       162 $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     108 "Can not instantiate object from '$arg{value}'" );
2681             }
2682             defined $arg{class}
2683             and not $obj->isa( $arg{class} )
2684 14 50 66     136 and $self->wail( "$arg{name} must be of class $arg{class}" );
2685             blessed( $old )
2686             and not $arg{nocopy}
2687 14 0 33     83 and $old->can( 'copy' )
      33        
2688             and $old->copy( $obj );
2689 14         56 $self->{$arg{name}} = $obj;
2690 14         106 return $arg{value};
2691             }
2692              
2693             sub _set_distance_meters {
2694 9 100   9   92 return ( $_[0]{$_[1]} = defined $_[2] ?
2695             ( $_[0]->__parse_distance( $_[2], '0m' ) * 1000 ) : $_[2] );
2696             }
2697              
2698             sub _set_ellipsoid {
2699 7     7   44 my ($self, $name, $val) = @_;
2700 7         74 Astro::Coord::ECI->set (ellipsoid => $val);
2701 7         374 return ($self->{$name} = $val);
2702             }
2703              
2704             sub _set_formatter {
2705 7     7   46 my ( $self, $name, $val ) = @_;
2706 7         51 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   88 my ( $self, $name, $val ) = @_;
2717 24         84 $self->get( 'formatter' )->$name( $val );
2718 24         86 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   51 my ( $self, $name, $class ) = @_;
2737 7         18 my $want_class = 'Astro::Coord::ECI';
2738 7 50       29 ref $class and $self->wail( "$name must not be a reference" );
2739 7 50       29 if ( defined $class ) {
2740 7         61 $self->load_package( { fatal => 'wail' }, $class );
2741 7 50       99 $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         26 $self->{$name} = $class;
2747 7         39 $self->{_help_module}{$name} = $class;
2748 7         22 foreach my $body ( @{ $self->{bodies} } ) {
  7         40  
2749 0         0 $body->set( $name => $class );
2750             }
2751 7         29 return;
2752             }
2753              
2754             sub _set_model {
2755 7     7   36 my ( $self, $name, $val ) = @_;
2756 7 50       77 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         56 foreach my $body ( @{ $self->{bodies} } ) {
  7         37  
2760 0         0 $body->set( model => $val );
2761             }
2762 7         36 return ( $self->{$name} = $val );
2763             }
2764              
2765             sub _set_output_layers {
2766 7     7   31 my ( $self, $name, $val ) = @_;
2767              
2768 7 50 33     126 if ( defined $val && '' ne $val ) {
2769 7 50   7   54 open my $fh, ">$val", File::Spec->devnull()
  7         35  
  7         90  
  7         461  
2770             or $self->wail( "Invalid $name value '$val'" );
2771 7         84684 close $fh;
2772             }
2773 7         97 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   46 my ( $self, $name, $val ) = @_;
2798 8 100       67 if ( $val =~ m/ \A (?: 0 x? ) [0-9]* \z /smx ) {
    50          
2799 7         43 $val = oct $val;
2800             } elsif ( $val !~ m/ \A [0-9]+ \z /smx ) {
2801 1         16 my @args = split qr{ [^\w-] }smx, $val;
2802 1         7 foreach ( @args ) {
2803 1         21 s/ \A (?! - ) /-/smx;
2804             }
2805 1   33     19 $go ||= Getopt::Long::Parser->new();
2806 1         31 $val = $self->get( $name );
2807             $go->getoptionsfromarray( \@args,
2808 0     0   0 none => sub { $val = PASS_VARIANT_NONE },
2809 1 50       14 map { $_ => sub {
2810 1     1   694 my ( $name, $value ) = @_;
2811 1         9 my $mask = $variant_def{$name};
2812 1 50       11 if ( $value ) {
2813 0         0 $val |= $mask;
2814             } else {
2815 1         4 $val &= ~ $mask;
2816             }
2817 1         3 return;
2818             }
2819 5         55 } @option_names )
2820             or $self->wail( "Invalid $name value '$val'" );
2821             }
2822 8         134 return ( $self->{$name} = $val );
2823             }
2824              
2825             sub _show_pass_variant {
2826 1     1   4 my ( $self, $name ) = @_;
2827 1         6 my $val = $self->get( $name );
2828 1         4 my @options;
2829 1         5 foreach my $key ( keys %variant_def ) {
2830 5 50       13 $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 2611 my ( $self, $variant ) = @_;
2840 138 50       510 $variant_def{$variant}
2841             or $self->wail( "Invalid pass_variant name '$variant'" );
2842 138         395 my $val = $self->get( 'pass_variant' ) & $variant_def{$variant};
2843 138         383 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   47 my ($self, $name, $val) = @_;
2864             $self->{frame}
2865 15 50       74 and $self->{frame}[-1]{$name} = $val;
2866 15         66 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   39 my ( $self, $name, $val ) = @_;
2877              
2878 7 50 33     77 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         48 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   46 my ( $self, $name, $val ) = @_;
2900 14 50 66     77 defined $val and $val eq 'undef' and $val = undef;
2901 14         114 $self->{time_parser}->$name( $val );
2902 14         31 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   65 my ($self, $name, $val) = @_;
2918 9 50       84 if (my $key = $twilight_abbr{lc $val}) {
2919 9         37 $self->{$name} = $key;
2920 9         35 $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   38 my ( $self, $name, $val ) = @_;
2933 7         35 $self->_set_formatter_attribute( $name, $val );
2934 7         54 $self->_set_time_parser_attribute( $name, $val );
2935 7         22 return $val;
2936             }
2937              
2938             sub _set_unmodified {
2939 165     165   619 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   58 my ($self, $name, $val) = @_;
2951             # TODO warn if $val is true but not '1'.
2952 7 50       31 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         27 return ($self->{$name} = $val);
2957             }
2958              
2959             sub show : Verb( changes! deprecated! readonly! ) {
2960 23     23 1 130 my ( $self, $opt, @args ) = __arguments( @_ );
2961              
2962 23         127 foreach my $name ( qw{ deprecated readonly } ) {
2963 46 50       190 exists $opt->{$name} or $opt->{$name} = 1;
2964             }
2965 23         50 my $output;
2966              
2967 23 50       88 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         53 foreach my $name (@args) {
2986 23 50       81 exists $shower{$name}
2987             or $self->wail("No such attribute as '$name'");
2988              
2989 23         138 my @val = $shower{$name}->( $self, $name );
2990 23 50       79 if ( $opt->{changes} ) {
2991 20     20   72646 no warnings qw{ uninitialized };
  20         51  
  20         2928  
2992 0 0       0 $static{$name} eq $val[-1] and next;
2993             }
2994              
2995 23 50       66 exists $mutator{$name} or unshift @val, '#';
2996 23         120 $output .= quoter( @val ) . "\n";
2997             }
2998 23         81 return $output;
2999 20     20   171 }
  20         54  
  20         132  
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   13 my ( $self, $name ) = @_;
3010 2         11 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   54 my ($self, $name) = @_;
3032 20         53 my $val = $self->get( $name );
3033 20         93 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   10873 use constant SPY2DPS => 3600 * 365.24219 * SECSPERDAY;
  20         43  
  20         8909  
3041              
3042             # Given a body in the sky, encodes it in 'sky add' format
3043             sub _sky_list_body {
3044 8     8   24 my ( $body ) = @_;
3045 8 50       28 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         57 my ( $ra, $dec, $rng, $pmra, $pmdec, $vr ) = $body->position();
3050 1         16 $rng /= PARSEC;
3051 1         10 $pmra = rad2deg( $pmra / 24 * 360 * cos( $ra ) ) * SPY2DPS;
3052 1         8 $pmdec = rad2deg( $pmdec ) * SPY2DPS;
3053 1         9 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         241 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 69 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3064              
3065 12   50     49 my $verb = lc ( shift @args || 'list' );
3066              
3067 12 50       104 if ( my $code = $self->can( "_sky_sub_$verb") ) {
3068 12         49 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   164 }
  20         48  
  20         136  
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   75 my ( $self, $name, %opt ) = @_;
3098             defined $opt{fatal}
3099 12 100       69 or $opt{fatal} = 1;
3100 12 100       490 if ( my $info = $self->{sky_class}{ fold_case( $name ) } ) {
    50          
3101 10         29 my ( $class, @attr ) = @{ $info };
  10         60  
3102 10         124 return $class->new( @attr );
3103             } elsif ( $opt{fatal} ) {
3104 0         0 $self->weep( "No class defined for $name" );
3105             }
3106 2         7 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   26 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3114 5 50       58 my $name = shift @args
3115             or $self->wail( 'You did not specify what to add' );
3116 5 50       28 defined $self->_find_in_sky( $name )
3117             and return;
3118 5 100       17 if ( my $obj = $self->_sky_object( $name, fatal => 0 ) ) {
3119 3         273 push @{ $self->{sky} }, $obj;
  3         9  
3120             } else {
3121 2 100       18 @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       15 my $rng = @args ?
3127             $self->__parse_distance( shift @args, '1pc' ) :
3128             10000 * PARSEC;
3129 1 50       4 my $pmra = @args ? do {
3130 1         4 my $angle = shift @args;
3131 1 50       16 $angle =~ s/ s \z //smxi
3132             or $angle *= 24 / 360 / cos( $ra );
3133 1         17 deg2rad( $angle / SPY2DPS );
3134             } : 0;
3135 1 50       17 my $pmdec = @args ? deg2rad( shift( @args ) / SPY2DPS ) : 0;
3136 1 50       7 my $pmrec = @args ? shift @args : 0;
3137 1         7 push @{ $self->{sky} }, Astro::Coord::ECI::Star->new(
3138             debug => $self->{debug},
3139 1         7 name => $name,
3140             sun => $self->_sky_object( 'sun' ),
3141             )->position( $ra, $dec, $rng, $pmra, $pmdec, $pmrec );
3142             }
3143 4         2016 return;
3144 20     20   17386 }
  20         46  
  20         115  
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   13758 }
  20         75  
  20         111  
3201              
3202             sub _sky_sub_clear : Verb() { ## no critic (ProhibitUnusedPrivateSubroutines)
3203 1     1   7 my ( $self ) = __arguments( @_ ); # $opt and args unused
3204 1         13 @{ $self->{sky} } = ();
  1         8  
3205 1         6 return;
3206 20     20   5089 }
  20         59  
  20         95  
3207              
3208             sub _sky_sub_drop : Verb() Tweak( -completion _sky_body_complete ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3209 1     1   10 my ( $self, undef, @args ) = __arguments( @_ ); # $opt unused
3210 1 50       21 @args or $self->wail(
3211             'You must specify at least one name to drop' );
3212 1         9 foreach my $name ( @args ) {
3213 1         12 $self->_drop_from_sky( $name );
3214             }
3215 1         7 return;
3216 20     20   5746 }
  20         53  
  20         104  
3217              
3218             sub _sky_sub_list : Verb( verbose! ) { ## no critic (ProhibitUnusedPrivateSubroutines)
3219 5     5   20 my ( $self, $opt ) = __arguments( @_ ); # args unused
3220 5         21 my $output;
3221 5         15 foreach my $body (
3222 8         56 map { $_->[1] }
3223 4         81 sort { $a->[0] cmp $b->[0] }
3224 8   33     144 map { [ lc( $_->get( 'name' ) || $_->get( 'id' ) ), $_ ] }
3225 5         26 @{$self->{sky}}
3226             ) {
3227 8         33 $output .= _sky_list_body( $body );
3228 8 50       44 if ( $opt->{verbose} ) {
3229 0         0 $output .= "# Class: @{[ ref $body ]}\n";
  0         0  
3230             }
3231             }
3232 5 100       12 unless (@{$self->{sky}}) {
  5         21  
3233             $self->{warn_on_empty}
3234 1 50       7 and $self->whinge( 'The sky is empty' );
3235             }
3236 5         28 return $output;
3237 20     20   8927 }
  20         105  
  20         141  
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   6886 }
  20         69  
  20         101  
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   9030 }
  20         58  
  20         117  
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   8197 }
  20         79  
  20         97  
3294              
3295             sub source : Verb( optional! ) {
3296 8     8 1 38 my ( $self, $opt, $src, @args ) = __arguments( @_ );
3297              
3298 8         18 my $output;
3299 8 100       37 my $reader = $self->_file_reader( $src, $opt )
3300             or return;
3301              
3302 6         14 my @level1_cache;
3303 6         13 my $level1_context = {};
3304             my $fetcher = $opt->{level1} ? sub {
3305             @level1_cache
3306 21 100   21   51 and return shift @level1_cache;
3307 19         36 my $buffer = $reader->();
3308 19         92 @level1_cache = $self->_rewrite_level1_command(
3309             $buffer, $level1_context );
3310 19         63 return shift @level1_cache;
3311 6 100       37 } : $reader;
3312              
3313 6         29 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         26 $self->{frame}[-1]{level1} = $opt->{level1};
3318 6         12 my $err;
3319 6 50       12 my $ok = eval { while ( defined( my $input = $fetcher->() ) ) {
  6         15  
3320 13 100       77 if ( defined ( my $buffer = $self->execute( $fetcher,
3321             $input ) ) ) {
3322 2         15 $output .= $buffer;
3323             }
3324             }
3325 6         30 1;
3326             } or $err = $@;
3327              
3328 6         29 $self->_frame_pop( $frames );
3329 6 50       20 $ok or $self->whinge( $err );
3330              
3331 6 100       30 $opt->{level1} and $self->_rewrite_level1_macros();
3332 6         113 return $output;
3333 20     20   9671 }
  20         47  
  20         90  
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   6043 no warnings qw{ uninitialized };
  20         53  
  20         16605  
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   190 }
  20         51  
  20         101  
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   7430 }
  20         66  
  20         100  
3466              
3467             sub station {
3468 34     34 1 165 my ( $self ) = @_;
3469              
3470             defined $self->{height}
3471             and defined $self->{latitude}
3472             and defined $self->{longitude}
3473 34 50 33     446 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     175 $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 22 my ( $self, $opt, @args ) = __arguments( @_ );
3493              
3494 3 100       25 @args or @args = qw{show};
3495              
3496 3   50     19 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         6 my $output;
3504              
3505 3 100 66     39 if ($verb eq 'add' || $verb eq 'drop') {
    100 33        
    50          
3506              
3507 1         16 Astro::Coord::ECI::TLE->status ($verb, @args);
3508 1         19 foreach my $tle (@{$self->{bodies}}) {
  1         13  
3509 1 50       8 $tle->get ('id') == $args[0] and $tle->rebless ();
3510             }
3511              
3512             } elsif ($verb eq 'clear') {
3513              
3514 1         12 Astro::Coord::ECI::TLE->status ($verb, @args);
3515 1         16 foreach my $tle (@{$self->{bodies}}) {
  1         10  
3516 2         134 $tle->rebless ();
3517             }
3518              
3519             } elsif ($verb eq 'show' || $verb eq 'list') {
3520              
3521 1         6 my @data = Astro::Coord::ECI::TLE->status( 'show', @args );
3522 1 50       18 @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   13 '__encode_operational_status' ) ) || sub { return $_[2] };
  0         0  
3528              
3529 1         6 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         231 return $output;
3542              
3543 20     20   15184 }
  20         75  
  20         115  
3544              
3545             sub system : method Verb() { ## no critic (ProhibitBuiltInHomonyms)
3546 4     4 1 23 my ( $self, undef, $verb, @args ) = __arguments( @_ ); # $opt unused
3547              
3548             @args = map {
3549 4         19 bsd_glob( $_, GLOB_NOCHECK | GLOB_BRACE | GLOB_QUOTE )
  8         306  
3550             } @args;
3551 4         19 my $stdout = $self->{frame}[-1]{localout};
3552 4         21 my @exported = keys %{ $self->{exported} };
  4         24  
3553 4         15 local @ENV{@exported} = map { $mutator{$_} ? $self->get( $_ ) :
3554 5 100       48 $self->{exported}{$_} } @exported;
3555 4 50 33     37 if ( defined $stdout && -t $stdout ) {
3556 0         0 CORE::system {$verb} $verb, @args;
  0         0  
3557 0         0 return;
3558             } else {
3559 4         78 $self->load_package( { fatal => 'wail' }, 'IPC::System::Simple' );
3560 4         35 return IPC::System::Simple::capturex( $verb, @args );
3561             }
3562 20     20   8888 }
  20         47  
  20         166  
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         10  
  0         0  
3566 1 50       13 $have_time_hires->() or $self->wail( 'Time::HiRes not available' );
3567 1         8 $self->_dispatch_check( time => $args[0] );
3568 1         5 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   6 return sprintf "%.3f seconds\n", Time::HiRes::time() - $start;
3575             },
3576 1 50       5 );
3577 1         10 return $self->dispatch( @args );
3578 20     20   7638 }
  20         49  
  20         110  
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   6145 }
  20         45  
  20         156  
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     20 and $opt->{choose} = \@args;
3590              
3591 4         26 my $bodies = $self->__choose( $opt->{choose}, $self->{bodies} );
3592 4         13 @{ $bodies } = map { $_->[0] }
  5         125  
3593 1 50       47 sort { $a->[1] <=> $b->[1] || $a->[2] <=> $b->[2] }
3594 5         48 map { [ $_, $_->get( 'id' ), $_->get( 'epoch' ) ] }
3595 4         12 @{ $bodies };
  4         10  
3596 4         14 my $tplt_name = delete $opt->{_template};
3597 4         19 return $self->__format_data( $tplt_name => $bodies, $opt );
3598 20     20   7996 }
  20         43  
  20         101  
3599              
3600             sub __tle_options {
3601 4     4   14 my ( $self, $opt ) = @_;
3602             return [
3603 4         27 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         11 foreach my $name ( @args ) {
3612 1         8 delete $self->{exported}{$name};
3613             }
3614 1         3 return;
3615 20     20   6372 }
  20         81  
  20         86  
3616              
3617             sub validate : Verb( quiet! ) {
3618 1     1 1 14 my ( $self, $opt, @args ) = __arguments( @_ );
3619              
3620 1         29 my $pass_start = $self->__parse_time (
3621             shift @args, $self->_get_day_noon());
3622 1   50     17 my $pass_end = $self->__parse_time (shift @args || '+7');
3623 1 50       9 $pass_start >= $pass_end
3624             and $self->wail( 'End time must be after start time' );
3625              
3626 1 50       4 @{ $self->{bodies} }
  1         11  
3627             or $self->wail( 'No bodies selected' );
3628              
3629             # Validate each body.
3630              
3631 1         3 my @valid;
3632 1         14 foreach my $tle ( $self->_aggregate( $self->{bodies} ) ) {
3633 2 100       1670 $tle->validate( $opt, $pass_start, $pass_end )
3634             and push @valid, $tle->members();
3635             }
3636              
3637 1         708 $self->{bodies} = \@valid;
3638              
3639 1         4 return;
3640 20     20   7696 }
  20         48  
  20         99  
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   5277 }
  20         46  
  20         90  
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   54 my ( $self, $code ) = @_;
3665 23   50     30 push @{ $self->{frame}[-1]{post_dispatch} ||= [] }, $code;
  23         117  
3666 23         54 return;
3667             }
3668              
3669             # $self->_aggregate( $list_ref );
3670              
3671             sub __add_to_observing_list {
3672 5     5   14192 my ( $self, @args ) = @_;
3673 5         30 foreach my $body ( @args ) {
3674 10 50       182 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         123 push @{ $self->{bodies} }, @args;
  5         19  
3682 5         18 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   89 my ( $self, $bodies ) = @_;
3690 27         122 local $Astro::Coord::ECI::TLE::Set::Singleton = $self->{singleton};
3691 27         70 return Astro::Coord::ECI::TLE::Set->aggregate ( @{ $bodies } );
  27         290  
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   176 my ( $self, $opt, $invert, @keys ) = @_;
3703 44         91 my $state = my $found = 0;
3704 44         122 foreach my $key ( @keys ) {
3705 136 100       324 if ( exists $opt->{$key} ) {
3706 8         16 $found++;
3707             $invert
3708 8 50       24 and $opt->{$key} = ( ! $opt->{$key} );
3709 8 100       28 $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     229 "-$_" } @keys );
  0         0  
3716 44         102 my $default = $state < 2;
3717 44         82 foreach my $key ( @keys ) {
3718             exists $opt->{$key}
3719 136 100       382 or $opt->{$key} = $default;
3720             }
3721 44         95 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   2782 my ( $self, $name, %arg ) = @_;
3737             exists $accessor{$name}
3738             and ( ! $level1_attr{$name} || $self->{frame}[-1]{level1} )
3739 1280 50 33     6448 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   89 my ( $self, $name ) = @_;
3783 43         269 my ( $attr, $sub ) = split qr{ [.] }smx, $name, 2;
3784 43 100       188 $accessor{$attr}
3785             or return NULL;
3786 9         28 my $rslt = $self->get( $attr );
3787 9 100       27 if ( defined $sub ) {
3788             $rslt
3789 2 50 33     22 and my $code = $special{$attr}
3790             or return NULL;
3791 2         8 $rslt = $code->( $rslt, $sub );
3792             }
3793 9         22 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   285 my ( $self, @args ) = @_;
3840 45 100       275 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
3841 45         137 my $choice = shift @args;
3842 45 100       166 defined $choice
3843             or $choice = [];
3844 45 50       186 ARRAY_REF eq ref $choice
3845             or $self->weep( 'Choice invalid' );
3846 45         162 my @rslt;
3847             my @selector;
3848 45         95 foreach my $sel ( @{ $choice } ) {
  45         160  
3849 5         16 my $ref = ref $sel;
3850 5 50       27 my $code = $chooser{$ref}
3851             or $self->weep( "$ref not supported as chooser" );
3852 5         27 push @selector, $code->( $sel );
3853             }
3854              
3855             $opt->{bodies}
3856             and push @args,
3857 45 100       188 $self->_aggregate( $self->{bodies} );
3858             $opt->{sky}
3859 45 100       840 and push @args, $self->{sky};
3860              
3861 45 100       114 @args = map { ARRAY_REF eq ref $_ ? @{ $_ } : $_ } @args;
  51         209  
  43         198  
3862              
3863             not @selector
3864 45 100       384 and return wantarray ? @args : \@args;
    100          
3865              
3866 5         17 foreach my $tle ( @args ) {
3867 10 50       31 ARRAY_REF eq ref $tle
3868             and $self->weep( 'Schwartzian-transform objects not supported' );
3869              
3870 10         20 my $match = $opt->{invert};
3871 10         19 my $context = {};
3872 10         23 foreach my $sel ( @selector ) {
3873 10 100       24 $sel->( $tle, $context )
3874             or next;
3875 4         15 $match = !$match;
3876 4         10 last;
3877             }
3878              
3879 10 100       45 $match and push @rslt, $tle;
3880             }
3881              
3882 5 100       46 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   2786 my ( $self, $type, $name, $repl ) = @_;
3926 1277 50       3236 $deprecate{$type} or return;
3927 1277 50       3378 $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   8 my ( $self, $name ) = @_;
3954 1 50       17 defined( my $inx = $self->_find_in_sky( $name ) )
3955             or return;
3956 1         34 return splice @{ $self->{sky} }, $inx, 1;
  1         10  
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     12 $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       194 if ( $mode =~ m/ \A (?: [+>] | [|] - ) /smx ) {
3979              
3980 1         5 my $layers = $self->get( 'output_layers' );
3981 1 50 33     8 if ( defined $layers && '' ne $layers ) {
3982 1 50       15 binmode $fh, $layers
3983             or $self->wail(
3984             "Unable to set '$layers' on $name: $!" );
3985             }
3986             }
3987              
3988 1         68 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   3741 my ( $self, $file, $opt ) = @_;
4021              
4022 25 100       131 if ( openhandle( $file ) ) {
4023             $opt->{glob}
4024 2 100   1   15 or return sub { return scalar <$file> };
  1         28  
4025 1         6 local $/ = undef;
4026 1         27 return scalar <$file>;
4027             }
4028              
4029 23         62 my $ref = ref $file;
4030 23 50       141 my $code = $self->can( "_file_reader_$ref" )
4031             or $self->wail( sprintf "Opening a $ref ref is unsupported" );
4032              
4033 23         125 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   56 my ( $self, $file, $opt ) = @_;
4042              
4043 14 50       79 defined $file
4044             and chomp $file;
4045              
4046 14 50 33     151 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       140 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     96 my $encoding = $opt->{encoding} || 'utf-8';
4068             my $fh = IO::File->new(
4069             $self->expand_tilde( $file ),
4070             "<:encoding($encoding)",
4071 14 100       99 ) or do {
4072 3 100       446 $opt->{optional} and return;
4073 2         46 $self->wail( "Failed to open $file: $!" );
4074             };
4075             $opt->{glob}
4076 11 100   16   3256 or return sub { return scalar <$fh> };
  16         344  
4077 7         47 local $/ = undef;
4078 7         376 return scalar <$fh>;
4079             }
4080             }
4081              
4082             sub _file_reader__validate_url {
4083 14     14   51 my ( undef, $url ) = @_; # Invocant unused
4084              
4085 14 50       93 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   19 my ( undef, $file, $opt ) = @_; # Invocant unused
4109              
4110 5         13 my $inx = 0;
4111             $opt->{glob}
4112 5 100   11   39 or return sub { return $file->[$inx++] };
  11         27  
4113 1         3 my $buffer;
4114 1         2 foreach ( @{ $file } ) {
  1         4  
4115 5         14 $buffer .= $_;
4116 5 50       18 $buffer =~ m/ \n \z /smx
4117             or $buffer .= "\n";
4118             }
4119 1         6 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       12 or return $file;
4126 1         2 my $buffer;
4127 1         2 local $_;
4128 1         44 while ( defined( $_ = $file->() ) ) {
4129 5         28 $buffer .= $_;
4130 5 50       20 $buffer =~ m/ \n \z /smx
4131             or $buffer .= "\n";
4132             }
4133 1         9 return $buffer;
4134             }
4135              
4136             sub _file_reader_SCALAR { ## no critic (ProhibitUnusedPrivateSubroutines)
4137 2     2   7 my ( $self, $file, $opt ) = @_;
4138              
4139             $opt->{glob}
4140 2 100       9 and return ${ $file };
  1         6  
4141 1 50       5 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   924 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   19 my ( $self, $name ) = @_;
4155              
4156 6         80 my $re = qr/ \A \Q$name\E \z /smxi;
4157 6         17 foreach my $inx ( 0 .. $#{ $self->{sky} } ) {
  6         31  
4158 8 100       170 $self->{sky}[$inx]->get( 'name' ) =~ $re
4159             and return $inx;
4160             }
4161 5         111 return;
4162             }
4163              
4164             # Documented in POD
4165              
4166             sub __format_data {
4167 41     41   4546 my ( $self, $action, $data, $opt ) = @_;
4168 41         293 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   161 my ( $self, $type, $args, $opt ) = @_;
4189 59   50     169 $args ||= [];
4190 59   100     254 $opt ||= {};
4191 59   100     99 my $frames = scalar @{$self->{frame} ||= []};
  59         324  
4192 59 100       223 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       171 $prior->{condition};
4199             #### defined $stdout or $stdout = select();
4200 59         213 my ( undef, $filename, $line ) = caller;
4201 59         953 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     118 unsatisfied_if => $prior->{unsatisfied_if} || ! $condition,
      100        
4213             };
4214 59         188 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   150 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     244 @{$self->{frame}} - 1;
  27         80  
4244 53         98 while (@{$self->{frame}} > $frames) {
  105         291  
4245 52 50       94 my $frame = pop @{$self->{frame}}
  52         180  
4246             or $self->weep( 'No frame to pop' );
4247 52   50     156 my $local = $frame->{local} || {};
4248 52         110 foreach my $name ( keys %{ $local } ) {
  52         182  
4249 2         14 my $value = $local->{$name};
4250 2 100 66     45 if ( exists $self->{$name} && !$force_set{$name} ) {
4251 1         16 $self->{$name} = $value;
4252             } else {
4253 1         28 $self->set( $name, $value );
4254             }
4255             }
4256 52         121 foreach my $key (qw{macro}) {
4257 52   50     183 my $info = $frame->{$key} || {};
4258 52         88 foreach my $name ( keys %{ $info } ) {
  52         150  
4259 19         58 $self->{$key}{$name} = $info->{ $name };
4260             }
4261             }
4262 52         354 ($frame->{spacetrack} && %{$frame->{spacetrack}})
4263 52 50 33     155 and $self->_get_spacetrack()->set(%{$frame->{spacetrack}});
  0         0  
4264             }
4265 53 50       143 if (delete $self->{pending}) {
4266 0         0 $self->wail('Input ended on continued line');
4267             }
4268 53         139 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   71 foreach my $name ( @_ ) {
4275 20         108 $force_set{$name} = 1;
4276             }
4277 20         54 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   156 my ( $self, $opt ) = @_;
4353 41   50     161 $opt ||= {};
4354 41 50 33     378 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   9 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         4 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   104351 no warnings qw{ once };
  20         56  
  20         16136  
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         12 };
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   183 no strict qw{ refs };
  20         45  
  20         15957  
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   171 no strict qw{ refs };
  20         59  
  20         34974  
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   40 my ( $self ) = @_;
4677             exists $self->{spacetrack}
4678 7 50       55 or $self->{spacetrack} = $self->_get_spacetrack_default();
4679 7         36 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   23 my ( $self ) = @_;
4690 7 50       32 $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   43 my ( $self, $day ) = @_;
4701 10 100       56 defined $day
4702             or $day = time;
4703 10         38 my $gmt = $self->get( 'formatter' )->gmt();
4704 10 50       88 my @time = $gmt ? gmtime( $day ) : localtime( $day );
4705 10         38 $time[0] = $time[1] = $time[2] = 0;
4706 10         25 $time[5] += 1900;
4707 10 50       77 return $gmt ? greg_time_gm(@time) : greg_time_local(@time);
4708             }
4709              
4710             sub _get_day_noon {
4711 42     42   107 my ( $self, $day ) = @_;
4712 42 100       151 defined $day
4713             or $day = time;
4714 42         143 my $gmt = $self->get( 'formatter' )->gmt();
4715 42 50       256 my @time = $gmt ? gmtime( $day ) : localtime( $day );
4716 42         129 $time[0] = $time[1] = 0;
4717 42         82 $time[2] = 12;
4718 42         109 $time[5] += 1900;
4719 42 50       214 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   22 my ( $self, $attribute ) = @_;
4729 9 50       21 my $object = $self->get( $attribute )
4730             or $self->wail( "No $attribute object available" );
4731 9         17 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   55 my ( $self, $opt, $name, $method, @args ) = __arguments( @_ );
4768              
4769             exists $opt->{raw}
4770 9 50       42 or $opt->{raw} = ( ! _is_interactive() );
4771              
4772 9 50       27 defined $method
4773             or $self->wail( 'No method name specified' );
4774              
4775 9 50       23 'config' eq $method
4776             and return $self->_helper_config_handler( $name => $opt );
4777              
4778 9         26 my $object = $self->_helper_get_object( $name );
4779 9 50 33     71 $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     88 and @args = $parse_input{$name}{$method}->( $self, $opt, @args );
      66        
4786             delete $opt->{raw}
4787 9 100       50 and return $object->$method( @args );
4788 5         26 my @rslt = $object->decode( $method, @args );
4789              
4790 5 100       79 instance( $rslt[0], ref $object ) and return;
4791 2 50       10 ref $rslt[0] and return $rslt[0];
4792 2         12 return quoter( $name, $method, @rslt ) . "\n";
4793 20     20   187 }
  20         73  
  20         140  
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   13118 use constant INTERACTIVE_CALLER => __PACKAGE__ . '::dispatch';
  20         54  
  20         3141  
4866             sub _is_interactive {
4867 364     364   567 my $level = 0;
4868 364         2302 while ( my @info = caller( $level ) ) {
4869 1520 100       3121 INTERACTIVE_CALLER eq $info[3]
4870             and return $level;
4871 1482         6296 $level++;
4872             }
4873 326         694 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   133199 %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   45 my ($self, $name, @args) = @_;
4935 19 50       45 $self->{macro}{$name} or $self->wail("No such macro as '$name'");
4936 19         67 my $frames = $self->_frame_push(macro => [@args]);
4937             my $macro = $self->{frame}[-1]{macro}{$name} =
4938 19         58 delete $self->{macro}{$name};
4939 19         34 my $output;
4940             my $err;
4941 19 100       30 my $ok = eval {
4942 19         71 $output = $macro->execute( $name, @args );
4943 18         41 1;
4944             } or $err = $@;
4945 19         95 $self->_frame_pop($frames);
4946 19 100       42 $ok or $self->wail($err);
4947 18         61 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   11 my @parts = @_;
4960 3         7 my $angle = 0;
4961 3         7 my $circle = 1;
4962 3         6 my $places;
4963 3         9 foreach ( @parts ) {
4964 9         14 my ( $part, $size ) = @{ $_ };
  9         19  
4965 9 50       20 defined $part or last;
4966 9         16 $circle *= $size;
4967 9         18 $angle = $angle * $size + $part;
4968 9 50       25 $places = $part =~ m/ [.] ( [0-9]+ ) /smx ? length $1 : 0;
4969             }
4970 3         15 $angle *= 360 / $circle;
4971 3 50       36 if ( my $mag = sprintf '%d', $circle / 360 ) {
4972 3         9 $places += length $mag;
4973             }
4974 3         54 return sprintf( '%.*f', $places, $angle ) + 0;
4975             }
4976              
4977             # Documented in POD
4978              
4979             sub __parse_angle {
4980 40     40   110 my ( $self, @args ) = @_;
4981 40 100       143 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
4982 40         92 my ( $angle ) = @args;
4983 40 100       120 defined $angle or return;
4984              
4985 33 100       309 if ( $angle =~ m/ : /smx ) {
    100          
4986              
4987 2         16 my ($h, $m, $s) = split ':', $angle;
4988 2         13 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         11 my ( $sgn, $deg, $min, $sec ) = ( $1, $2, $3, $4 );
5000 1         8 $angle = _parse_angle_parts(
5001             [ $deg => 360 ],
5002             [ $min => 60 ],
5003             [ $sec => 60 ],
5004             );
5005 1 50 33     8 $sgn and '-' eq $sgn and return -$angle;
5006 1         4 return $angle;
5007             }
5008              
5009             $opt->{accept}
5010 30 50 66     279 or looks_like_number( $angle )
5011             or $self->wail( "Invalid angle '$angle'" );
5012              
5013 30         126 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   15 my ($self, $string, $dfdist) = @_;
5030 3 50       14 defined $dfdist or $dfdist = 'km';
5031 3 50       36 my $dfunits = $dfdist =~ s/ ( [[:alpha:]]+ ) \z //smx ? $1 : 'km';
5032 3 50       36 my $units = lc (
5033             $string =~ s/ \s* ( [[:alpha:]]+ ) \z //smx ? $1 : $dfunits );
5034 3 50       18 $units{$units}
5035             or $self->wail( "Units of '$units' are unknown" );
5036 3 50       18 $string ne '' or $string = $dfdist;
5037 3 50       18 looks_like_number ($string)
5038             or $self->wail( "'$string' is not a number" );
5039 3         20 return $string * $units{$units};
5040             }
5041             }
5042              
5043             # Documented in POD
5044              
5045             sub __parse_time {
5046 55     55   1983 my ($self, $time, $default) = @_;
5047             my $pt = $self->{time_parser}
5048 55 50       215 or $self->wail( 'No time parser available' );
5049 55 50       302 $self->{time_parser}->can( 'station' )
5050             and $self->_set_time_parser_attribute(
5051             station => $self->station() );
5052 55 50       263 if ( defined( my $time = $pt->parse( $time, $default ) ) ) {
5053 55         140 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   701 my ( $self ) = @_;
5064             defined ( my $pt = $self->{time_parser} )
5065 332 100       1116 or return;
5066 311         1429 $pt->reset();
5067 311         601 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   3 my $sec = shift;
5077 1         7 $sec *= 12 / PI;
5078 1         7 my $hr = floor( $sec );
5079 1         3 $sec = ( $sec - $hr ) * 60;
5080 1         10 my $min = floor( $sec );
5081 1         8 $sec = ( $sec - $min ) * 60;
5082 1         18 my $rslt = sprintf '%2d:%02d:%02d', $hr, $min, floor( $sec + .5 );
5083 1         7 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   55 my ( $self, $in, $error ) = @_;
5096             $in and defined( my $more = $in->(
5097             my $prompt = $self->get( 'continuation_prompt' ) ) )
5098 15 100 66     104 or do {
5099 1 50       3 $error or return;
5100 1 50       4 ref $error eq CODE_REF
5101             and return $error->();
5102 1         4 $self->wail( $error );
5103             };
5104 14 50       105 $self->{echo} and $self->whinge( $prompt, $more );
5105 14 100       106 $more =~ m/ \n \z /smx or $more .= "\n";
5106 14         39 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   42 my ( undef, $buffer, $context ) = @_; # Invocant unused
5199              
5200 19         28 my $command = delete $context->{command};
5201              
5202 19 100       47 defined $buffer
5203             or return $buffer;
5204 12 50       62 $buffer =~ m/ \A \s* \z /sxm
5205             and return $buffer;
5206 12 50       56 $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       28 if ( ! defined $command ) {
5212 12 100       42 $buffer =~ m/ \A \s* ( \w+ ) /sxm
5213             or return $buffer;
5214 11         25 $command = $1;
5215             }
5216 11         22 my $append = '';
5217 11 100       84 $buffer =~ s/ ( \s* \\? \n ) //sxm
5218             and $append = $1;
5219             $append =~ m/ \\ /sxm
5220 11 50       30 and $context->{command} = $command;
5221              
5222 11   66     43 my $handler = $level1_requote{$command} || $level1_requote{''};
5223 11         25 my ( $this_quote, $start_pos );
5224 11         90 while ( $buffer =~ m/ (?: \A | (?
5225             ) {
5226 22 100       80 if ( ! defined $start_pos ) {
    100          
5227 9         30 $start_pos = $+[0] - 1;
5228 9         78 $this_quote = $1;
5229             } elsif ( $1 eq $this_quote ) {
5230 9         25 my $length = $+[0] - $start_pos;
5231 9         35 local $_ = substr $buffer, $start_pos + 1, $length - 2;
5232 9         31 $handler->{$this_quote}->();
5233 9         29 substr $buffer, $start_pos, $length, $_;
5234 9         28 pos( $buffer ) = $start_pos + length $_;
5235 9         39 $start_pos = undef;
5236             }
5237             }
5238              
5239 11 100       55 my $code = $level1_map{$command}
5240             or return $buffer . $append;
5241              
5242 3         14 my @rslt = $code->( $buffer );
5243 3         14 $rslt[-1] .= $append;
5244 3         13 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         11 foreach ( @{ $args } ) {
  8         24  
5367 8 100 100     89 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         25 push @rslt, $code->( $1, $_ );
5372 7         16 $rewrote++;
5373             } else {
5374 1         7 push @rslt, $_;
5375             }
5376             }
5377              
5378 8 100       32 return $rewrote ? \@rslt : $args;
5379             }
5380              
5381             sub _rewrite_level1_macros {
5382 4     4   9 my ( $self ) = @_;
5383              
5384 4         8 foreach my $macro ( values %{ $self->{macro} } ) {
  4         13  
5385 8         24 $macro->__level1_rewrite();
5386             }
5387              
5388 4         5 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   93 my ( $self, $name, $opt ) = @_;
5439 24         99 $opt->{_template} = $name;
5440             my $code = sub {
5441 5     5   3027 my ( $opt_name, $opt_value ) = @_;
5442 5 50       87 $opt->{_template} = $opt_value ? "${name}_$opt_name" : $name;
5443 5         44 return;
5444 24         159 };
5445 24         236 my $re = qr< \A \Q$name\E _ ( \w+ ) \z >smx;
5446 24         62 my @rslt;
5447 24         79 my $fmtr = $self->get( 'formatter' );
5448 24 50       186 if ( $fmtr->can( '__list_templates' ) ) {
5449 24         122 foreach ( $fmtr->__list_templates() ) {
5450 672 100       2002 $_ =~ $re
5451             or next;
5452 44         213 push @rslt, "$1!", $code;
5453             }
5454             }
5455 24         343 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   75242 my ($self, @parms) = @_;
5582 381         1139 local $self->{_case_mod} = undef;
5583 381 100       1251 my $opt = HASH_REF eq ref $parms[0] ? shift @parms : {};
5584 381         680 my $in = $opt->{in};
5585 381         774 my $buffer = shift @parms;
5586 381 100       1520 $buffer =~ m/ \n \z /smx or $buffer .= "\n";
5587 381   100     973 my $args = shift @parms || [];
5588 381         794 my @rslt = ( {} );
5589 381         705 my $absquote; # True if inside ''
5590             my $relquote; # True if inside "" (and not in '')
5591 381         642 my $len = length $buffer;
5592 381         674 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         899 while ($inx < $len) {
5601 6312         10610 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     37410 if ( $absquote ) {
    100 100        
    100 100        
    100 66        
    100 66        
    100          
    100          
    100          
    100          
    100          
5609 621 50       1225 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         98 $absquote = undef;
5619             } else {
5620 587         883 $rslt[-1]{token} .= $char;
5621 587 100       1040 if ( $inx >= $len ) {
5622 2         16 $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         33 my $next = substr $buffer, $inx++, 1;
5633 10 100       46 if ( $inx >= $len ) { # At end of line
    100          
5634 2 50       30 if ( $relquote ) { # Inside ""
5635 0         0 $buffer .= $self->_read_continuation( $in,
5636             'Unclosed double quote' );
5637             } else { # Between tokens
5638 2         32 $buffer .= $self->_read_continuation( $in,
5639             'Dangling continuation' );
5640 2 50       24 $opt->{single} or push @rslt, {}; # New token
5641             }
5642 2         14 $len = length $buffer;
5643             } elsif ( $relquote ) {
5644 7 100       36 if ( my $code = $case_ctl{$next} ) {
5645 6         20 $code->( $self );
5646             } else {
5647 1   33     5 $rslt[-1]{token} .= $escape{$next} || $next;
5648             }
5649             } else {
5650 1         2 $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         125 $rslt[-1]{token} .= ''; # Empty string, to force defined.
5660 35         81 $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         104 $rslt[-1]{token} .= ''; # Empty string, to force defined.
5668             ( $relquote = !$relquote )
5669 44 100       145 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         1907 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         156 my $name = substr $buffer, $inx++, 1;
5684 72         141 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     355 if ($name eq '{' && $inx < $len) {
    100          
5691 34         59 $brkt = 1;
5692 34         57 $name = '';
5693 34         50 my $nest = 1;
5694 34         77 while ($inx < $len) {
5695 369         520 $char = substr $buffer, $inx++, 1;
5696 369 50       731 if ($char eq '{') {
    100          
5697 0         0 $nest++;
5698             } elsif ($char eq '}') {
5699 33 50       92 --$nest or last;
5700             }
5701 336         555 $name .= $char;
5702             }
5703 34 100       83 $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         65 pos( $buffer ) = $inx;
5716 21 50       482 if ( $buffer =~ m/ \G ( \w* (?: [.] \w+ )? ) /smxgc ) {
5717 21         68 $name .= $1;
5718 21         44 $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         119 my ($indirect, $value);
5726 71 100       188 $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       375 if ($name =~ m/ (.*?) ( [:]? [\-\+\=\?] | [:] ) (.*) /smx) {
5732 28         127 my ($name, $flag, $rest) = ($1, $2, $3);
5733              
5734             # First we do indirection if that was required.
5735              
5736 28 50       63 $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         101 $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         152 my $mod = __tokenize(
5752             $self,
5753             { single => 1, noredirect => 1, in => $in },
5754             $rest, $args);
5755 28         78 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         66 $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       75 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       112 if ($flag eq '+') {
    100          
    100          
    100          
    100          
    50          
5774 4 100       17 $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       41 if ($flag eq '') {
5783 10         39 my @pos = split ':', $mod, 2;
5784 10         22 foreach ( @pos ) {
5785 18         50 s/ \A \s+ //smx;
5786             }
5787 10 50       27 @pos > 2
5788             and $self->wail(
5789             'Substring expansion has extra arguments' );
5790 10         26 foreach ( @pos ) {
5791 18 50       69 m/ \A [-+]? [0-9]+ \z /smx
5792             or $self->wail(
5793             'Substring expansion argument non-numeric'
5794             );
5795             }
5796 10 100       31 if (ref $value) {
5797 4 50       11 if (@pos > 1) {
5798 4         15 $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         18 $value = [@$value[$pos[0] .. $pos[1]]];
5804             } else {
5805             # We want to disable warnings if we slop
5806             # outside the string.
5807 20     20   203 no warnings qw{substr};
  20         50  
  20         52719  
5808 6 100       27 $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         8 $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     67 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         25 $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         3 $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       103 $indirect
5869             and $name = $self->_tokenize_var(
5870             $name, $args, $relquote, $indirect);
5871 43         142 $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       244 ref $value
    100          
5878             or $value = defined $value ? [ $value ] : [];
5879              
5880             # If we are inside quotes
5881 69 100       147 if ( $relquote ) {
5882             # do case modification
5883             # NOTE that the argument list is modified in-place.
5884 12         19 $self->_case_mod( @{ $value } );
  12         31  
5885             } else {
5886             # otherwise do word splitting
5887 57         83 $value = [ map { split qr{ \s+ }smx } @{ $value } ];
  71         452  
  57         125  
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       149 if ( @{ $value } ) {
  69         202  
5896 58         110 foreach ( @$value ) {
5897 86         194 $rslt[-1]{token} .= $_;
5898 86         183 push @rslt, {};
5899             }
5900 58         109 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       69 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         30 my $next = substr $buffer, $inx++, 1;
5924 11 50       26 $next =~ m/ \s /smx and next;
5925 11 100       27 if ($next eq $char) {
5926 6         13 $rslt[-1]{mode} .= $next;
5927 6 100       33 length $rslt[-1]{mode} > 2
5928             and $self->wail(
5929             "Syntax error near $rslt[-1]{mode}");
5930             } else {
5931 5         10 --$inx;
5932 5         39 $rslt[-1]{token} = '';
5933 5         12 last;
5934             }
5935             }
5936 5 100       25 if ( '<<' eq $rslt[-1]{mode} ) { # Heredoc
5937 4         8 delete $rslt[-1]{redirect};
5938 4         13 delete $rslt[-1]{type};
5939 4         8 delete $rslt[-1]{mode};
5940 4         14 my $quote = '';
5941 4         12 while ( $inx < $len ) {
5942 62         105 my $next = substr $buffer, $inx++, 1;
5943 62 100       110 if ( $next =~ m/ \s /smx ) {
5944 2 50       12 $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     154 or $rslt[-1]{token} .= $next;
      66        
5951             $quote
5952             and $next eq $quote
5953 60 100 100     190 and $rslt[-1]{token} ne ''
      100        
5954             and last;
5955             }
5956             }
5957 4 100       28 $quote and $rslt[-1]{token} =~ s/ . \z //sxm;
5958 4         9 my $terminator = $rslt[-1]{token};
5959 4         17 my $look_for = $terminator . "\n";
5960 4         9 $rslt[-1]{token} = '';
5961 4         10 $rslt[-1]{expand} = $quote ne q<'>;
5962 4         7 while ( 1 ) {
5963 9         37 my $buffer = $self->_read_continuation( $in,
5964             "Here doc terminator $terminator not found" );
5965 9 100       31 $buffer eq $look_for and last;
5966 5         11 $rslt[-1]{token} .= $buffer;
5967             }
5968 4 100       33 if ( $quote ne q<'> ) {
5969             $rslt[-1]{token} = __tokenize(
5970             $self,
5971             { single => 1, noredirect => 1, in => $in },
5972 3         45 $rslt[-1]{token}, $args
5973             );
5974             }
5975 4         11 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         8523 $self->_case_mod( $char );
5985 3744         5572 $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         57 $rslt[-1]{tilde}++;
5992 12         61 $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         19 last;
5998              
5999             # Else we just put it in the token.
6000             } else {
6001 829         1993 $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     15211 if ( $inx >= $len && ( $absquote || $relquote ) ) {
      100        
6007 2 50       10 $buffer .= $self->_read_continuation( $in,
6008             $absquote ? 'Unclosed single quote' :
6009             'Unclosed double quote'
6010             );
6011 2         7 $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       790 $absquote and $self->wail( 'Unclosed terminal single quote' );
6023 376 50       709 $relquote and $self->wail( 'Unclosed terminal double quote' );
6024              
6025             # Replace leading punctuation with the corresponding method.
6026              
6027             shift @rslt
6028 376   100     1535 while @rslt && ! defined $rslt[0]{token};
6029 376 50 66     2783 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         777 my (@tokens, %redir);
6048 376         587 my $expand_tildes = 1;
6049 376 100 100     3233 if ( defined $rslt[0]{token}
6050             and my $kode = $self->can( $rslt[0]{token} ) ) {
6051 252 100       983 if ( my $hash = $self->__get_attr( $kode, 'Tokenize' ) ) {
6052 2         11 $expand_tildes = $hash->{expand_tilde};
6053             }
6054             }
6055 376         893 foreach (@rslt) {
6056 1318 100       2681 exists $_->{token} or next;
6057 966 100 66     2638 if ($_->{redirect}) {
    100          
6058 1 50       3 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       8 $_->{token}),
6068             };
6069             }
6070             } elsif ( $expand_tildes && $_->{tilde} ) {
6071 12         96 push @tokens, $self->expand_tilde( $_->{token} );
6072             } else {
6073 953         1930 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     1102 ($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       771 $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         2197 return (\@tokens, \%redir);
6092             }
6093              
6094             # Retrieve the value of a variable.
6095             sub _tokenize_var {
6096 74     74   386 my ($self, $name, $args, $relquote, $indirect) = @_;
6097              
6098 74 0 33     297 defined $name and $name ne ''
    50          
6099             or return $indirect ? '' : undef;
6100              
6101 74 100       234 $special{$name} and do {
6102 19         93 my $val = $special{$name}->($args, $relquote);
6103 19 50 33     78 return ($indirect && ref $val) ? '' : $val;
6104             };
6105              
6106 55 100       208 $name !~ m/ \D /smx
6107             and return $args->[$name - 1];
6108              
6109 40         125 my $value = $self->_attribute_value( $name );
6110 40 100       128 NULL_REF eq ref $value
6111             or return $value;
6112              
6113             exists $self->{exported}{$name}
6114 34 100       114 and return $self->{exported}{$name};
6115              
6116             defined $ENV{$name}
6117 32 100       109 and return $ENV{$name};
6118              
6119 14         28 foreach my $frame ( reverse @{ $self->{frame} } ) {
  14         50  
6120             defined $frame->{define}{$name}
6121 17 100       60 and return $frame->{define}{$name};
6122             }
6123              
6124 11         29 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   4998 my $self = shift;
6134 3756         5969 foreach ( @_ ) {
6135             $self->{_case_mod}{case}
6136 3759 100       7084 and $_ = $self->{_case_mod}{case}->( $self, $_ );
6137 3759         4878 my $code;
6138             $code = delete $self->{_case_mod}{single}
6139 3759 100       7718 and $_ = $code->( $self, $_ );
6140             }
6141 3756         5197 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 95 my ($self, @args) = @_;
6153 18         183 $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       4 if ( $self->get( 'error_out' ) ) {
6163 1         6 $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 13 my ($self, @args) = @_;
6189 3         22 $self->{_warner}->whinge( @args );
6190 3         10 return;
6191             }
6192              
6193             1;
6194              
6195             __END__