File Coverage

blib/lib/Astro/App/Satpass2.pm
Criterion Covered Total %
statement 1679 2483 67.6
branch 630 1346 46.8
condition 168 423 39.7
subroutine 277 353 78.4
pod 59 63 93.6
total 2813 4668 60.2


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