File Coverage

blib/lib/Astro/Coord/ECI/TLE/Set.pm
Criterion Covered Total %
statement 136 180 75.5
branch 41 86 47.6
condition 11 26 42.3
subroutine 18 21 85.7
pod 14 14 100.0
total 220 327 67.2


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Astro::Coord::ECI::TLE::Set - Represent a set of data for the same ID.
4              
5             =head1 SYNOPSIS
6              
7             use Astro::SpaceTrack;
8             use Astro::Coord::ECI::TLE;
9             use Astro::Coord::ECI::TLE::Set;
10             use Astro::Coord::ECI::Utils qw{rad2deg};
11              
12             # Get orbital data on the International Space Station and
13             # related NASA stuff.
14             my $st = Astro::SpaceTrack->new(
15             username => $me,
16             password => $secret,
17             );
18             my $rslt = $st->retrieve( qw{ -last5 25544 } );
19             $rslt->is_success
20             or die "Unable to get data: ", $rslt->status_line;
21            
22             # We aggregate the data because we retrieved the last five orbital
23             # elements for the body. The Set object will select the correct one for
24             # the given time.
25             my @sats = Astro::Coord::ECI::TLE::Set->aggregate (
26             Astro::Coord::ECI::TLE->parse ($rslt->content));
27             my $now = time ();
28            
29             # Display current International Space Station (etc)
30             # position in terms of latitude, longitude, and altitude.
31             # Like all position methods, geodetic() returns angles in
32             # radians and distances in kilometers.
33             print join ("\t", qw{OID Latitude Longitude Altitude}
34             ), "\n";
35             foreach my $tle (@sats) {
36             my ($lat, $long, $alt) = $tle->universal($now)
37             ->geodetic();
38             print join ("\t", $tle->get ('id'),
39             rad2deg($lat), rad2deg($long), $alt),
40             "\n";
41             }
42              
43             =head1 DESCRIPTION
44              
45             This module is intended to represent a set of orbital elements,
46             representing the same NORAD ID at different points in time. It
47             can contain any number of objects of class Astro::Coord::ECI::TLE
48             (or any subclass thereof) provided all contents are of the same
49             class and represent the same NORAD ID.
50              
51             In addition to the methods documented here, an
52             Astro::Coord::ECI::TLE::Set supports all methods provided by the
53             currently-selected member object, through Perl's AUTOLOAD mechanism.
54             In this way, the object is almost a plug-compatible replacement for
55             an Astro::Coord::ECI::TLE object, but it uses the orbital elements
56             appropriate to the time given. The weasel word 'almost' is expanded
57             on in the L section,
58             below.
59              
60             When the first member object is added via the add() method, it becomes
61             the currently-selected object. The select() method can be used to
62             select the member that best represents the time passed to the select
63             method. In addition, certain method calls that are delegated to the
64             currently-selected member object can cause a new member to be selected
65             before the delegation is done. These include 'universal', 'dynamical',
66             and any Astro::Coord::ECI::TLE orbital propagation model.
67              
68             There may be cases where the member class does not want to use the
69             normal delegation mechanism. In this case, it needs to define a
70             _nodelegate_xxxx method, where xxxx is the name of the method that
71             is not to be delegated to. The _nodelegate method is called with the
72             same calling sequence as the original method, but the first argument
73             is a reference to the Astro::Coord::ECI::TLE::Set object, not the
74             member object. Use of this mechanism constitutes a pledge that the
75             _nodelegate method does not make use of any private interfaces to the
76             member objects.
77              
78             =head2 Incompatibilities with Astro::Coord::ECI::TLE
79              
80             =head3 Inheritance
81              
82             Astro::Coord::ECI::TLE::Set is not a member of the Astro::Coord::ECI
83             inheritance hierarchy, so $set->isa ('Astro::Coord::ECI') is false.
84              
85             =head3 Calling semantics for delegated behaviors
86              
87             In general, when Astro::Coord::ECI::TLE::Set delegates functionality
88             to a member object, that object's method receives a reference to the
89             member object as its first argument. That is, if $set is the
90             Astro::Coord::ECI::TLE::Set object and $tle is the relevant member
91             object, $set->method (...) becomes $tle->method (...) from the point
92             of view of the called method.
93              
94             If the member class wishes to see the Astro::Coord::ECI::TLE::Set
95             object as the first argument of method xxxx, it defines method
96             _nodelegate_xxxx, which is called as though by $set->_nodelegate_xxx
97             (...). The _nodelegate_xxx method must use only the public interface
98             to the $tle object (whatever its class). A cheap way to get this
99             method is
100              
101             *_nodelegate_xxxx = \&xxxx;
102              
103             but nothing says the _nodelegate_xxxx method B be defined this
104             way.
105              
106             The C and C methods are special-cased in the
107             AUTOLOAD code so that a select() is done before they are called.
108              
109             =head3 Calling semantics for static behaviors
110              
111             Some Astro::Coord::ECI methods (e.g. universal()) will instantiate an
112             object for you if you call them statically. This will not work with
113             Astro::Coord:ECI::TLE::Set; that is,
114             Astro::Coord::ECI::TLE::Set->universal () is an error.
115              
116             =head3 Return semantics for delegated behaviors
117              
118             In general, when behavior is delegated to a member object, the return
119             is whatever the delegated method returns. This means that, for methods
120             that return the object they are called on (e.g. universal()) you get
121             back a reference to the member object, not a reference to the
122             containing Astro::Coord::ECI::TLE::Set object.
123              
124             =head2 Methods
125              
126             The following methods should be considered public:
127              
128             =over
129              
130             =cut
131              
132             package Astro::Coord::ECI::TLE::Set;
133              
134 5     5   3981 use strict;
  5         12  
  5         151  
135 5     5   28 use warnings;
  5         12  
  5         156  
136              
137 5     5   27 use Astro::Coord::ECI::Utils qw{ :params :ref max @CARP_NOT };
  5         9  
  5         745  
138 5     5   36 use Carp;
  5         12  
  5         485  
139              
140             our @CARP_NOT = qw{
141             Astro::Coord::ECI::TLE::Iridium
142             Astro::Coord::ECI::TLE
143             Astro::Coord::ECI
144             };
145              
146             our $VERSION = '0.129';
147              
148 5     5   36 use constant ERR_NOCURRENT => <
  5         9  
  5         11138  
149             Error - Can not call %s because there is no current member. Be
150             sure you called add() after instantiating or calling clear().
151             eod
152              
153             =item $set = Astro::Coord::ECI::TLE::Set->new ()
154              
155             This method instantiates a new set. Any arguments are passed to the
156             add() method.
157              
158             =cut
159              
160             sub new {
161 11     11 1 467 my ($class, @args) = @_;
162 11 50       30 $class = ref $class if ref $class;
163 11         39 my $self = {
164             current => undef, # Current member
165             members => [], # [effective, TLE].
166             };
167 11         23 bless $self, $class;
168 11 100       41 $self->add (@args) if @args;
169 11         26 return $self;
170             }
171              
172             =item $set->add ($member ...);
173              
174             This method adds members to the set. The initial member may be any
175             initialized member of the Astro::Coord::ECI::TLE class, or any subclass
176             thereof. Subsequent members must be the same class as the initial
177             member, and represent the same NORAD ID. If not, an exception is thrown.
178             If a prospective member has the same effective date as a current member,
179             the prospective member is silently ignored. If a member does not have an
180             effective date, the epoch is used as a proxy for the effective date.
181              
182             The first member added becomes the current member for the purpose
183             of delegating method calls. Adding subsequent members does not
184             change the current member, though it may be appropriate to call
185             select() after adding.
186              
187             =cut
188              
189             sub add {
190 11     11 1 27 my ($self, @args) = @_;
191 11         16 my ($id, %ep, $class);
192 11         21 foreach (@{$self->{members}}) {
  11         34  
193 1         4 my ($effective, $tle) = @$_;
194 1   33     6 $id ||= $tle->get ('id');
195 1   33     5 $class ||= ref $tle;
196 1         4 $effective = $tle->get('effective');
197 1 50       5 defined $effective or $effective = $tle->get('epoch');
198 1         3 $ep{$effective} = $tle;
199             }
200 11 100       25 foreach my $tle (map {__instance( $_, __PACKAGE__ ) ?
  14         37  
201             $_->members : $_} @args) {
202 14         39 my $aid = $tle->get ('id');
203 14 100       36 if (defined $id) {
204 4 50       11 __instance( $tle, $class ) or croak <
205 0         0 Error - Additional member of @{[__PACKAGE__]} must be a
206             subclass of $class
207             eod
208 4 50       22 $aid == $id or croak <
209             Error - NORAD ID mismatch. Trying to add ID $aid to set defined
210             as ID $id.
211             eod
212             } else {
213 10 50       25 __instance( $tle, 'Astro::Coord::ECI::TLE' ) or croak <
214 0         0 Error - First member of @{[__PACKAGE__]} must be a subclass
215             of Astro::Coord::ECI::TLE.
216             eod
217 10         24 $class = ref $tle;
218 10         13 $id = $aid;
219 10         22 $self->{current} = $tle;
220             }
221 14         35 my $aep = $tle->get ('effective');
222 14 100       42 defined $aep or $aep = $tle->get('epoch');
223 14 50       33 next if $ep{$aep};
224 14         51 $ep{$aep} = $tle;
225             }
226 11         27 @{$self->{members}} = sort {$a->[0] <=> $b->[0]}
  4         19  
227 11         31 map {[$_, $ep{$_}]} keys %ep;
  15         52  
228 11         43 return $self;
229             }
230              
231             =item @sets = Astro::Coord::ECI::TLE::Set->aggregate ($tle ...);
232              
233             This method aggregates the given Astro::Coord::ECI::TLE objects into
234             sets by NORAD ID. If there is only one object with a given NORAD ID, it
235             is simply returned intact, B made into a set with one member.
236              
237             If you should for some reason want sets with one member, do
238              
239             $Astro::Coord::ECI::TLE::Set::Singleton = 1;
240              
241             before you call aggregate(). Actually, any value that Perl will
242             interpret as true will work. You might want a 'local' in front of all
243             this.
244              
245             Optionally, the first argument may be a hash reference. The hash
246             contains options that modify the function of this method. The only
247             option at the moment is
248              
249             select => $time
250              
251             which causes the object best representing the given time to be selected
252             in any Astro::Coord::ECI::TLE::Set objects.
253              
254             =cut
255              
256             our $Singleton = 0;
257              
258             sub aggregate {
259 3     3 1 11 my ($class, @args) = @_;
260 3 50       48 $class = ref $class if ref $class;
261 3 50       88 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
262 3         11 my %data;
263             my @rslt;
264 3         8 foreach my $tle ( @args ) {
265 8         23 my $model = $tle->get( 'model' );
266 8         20 my $id = $tle->get ('id');
267 8 50 33     30 if ( '' eq $id && 'null' eq $model ) {
268 0         0 push @rslt, $tle;
269             } else {
270 8   100     36 $data{$id} ||= [];
271 8         12 push @{$data{$id}}, $tle;
  8         22  
272             }
273             }
274 3 100       11 my $limit = $Singleton ? 0 : 1;
275 3         18 foreach my $id (sort keys %data) {
276 5         9 my $items = @{$data{$id}};
  5         10  
277 5 100       15 if ($items > $limit) {
278 4         9 my $set = $class->new(@{$data{$id}});
  4         16  
279             exists $opt->{select}
280 4 50       13 and $set->select($opt->{select});
281 4         11 push @rslt, $set;
282             } else {
283 1         3 push @rslt, @{$data{$id}};
  1         3  
284             }
285             }
286 3         16 return @rslt;
287             }
288              
289             =item $set->can ($method);
290              
291             This method checks to see if the object can execute the given method.
292             If so, it returns a code reference to the subroutine; otherwise it
293             returns undef.
294              
295             This override to UNIVERSAL::can is necessary because we want to return
296             true for member class methods, but we execute them by autoloading, so
297             they are not in our namespace.
298              
299             =cut
300              
301             sub can {
302 8     8 1 26 my ($self, $method) = @_;
303 8         14 my $rslt = eval {$self->SUPER::can($method)};
  8         34  
304 8 50       21 $@ and return;
305 8 100       28 $rslt and return $rslt;
306              
307 5         9 return eval { ## no critic (RequireCheckingReturnValueOfEval)
308 5         97 $self->{current}->can($method)
309             };
310             }
311              
312             =item $set->clear ();
313              
314             This method removes all members from the set, allowing it to be
315             reloaded with a different NORAD ID.
316              
317             =cut
318              
319             sub clear {
320 2     2 1 291 my $self = shift;
321 2         4 $self->{current} = undef;
322 2         5 @{$self->{members}} = ();
  2         14  
323 2         5 return $self;
324             }
325              
326             =item $value = $set->get( $name );
327              
328             This method returns the value of the named attribute.
329              
330             If the attribute name is C<'tle'>, it returns the concatenated TLE data
331             of all TLEs in the set. Otherwise it simply returns the named attribute
332             of the selected C object.
333              
334             =cut
335              
336             {
337             my %override = (
338             tle => sub {
339             ## my ( $self, $name ) = @_;
340             my ( $self ) = @_; # Name unused
341             my $output;
342             foreach my $body ( $self->members() ) {
343             $output .= $body->get( 'tle' );
344             }
345             return $output;
346             },
347             );
348              
349             sub get {
350 0     0 1 0 my ( $self, $name ) = @_;
351             $override{$name}
352 0 0       0 and return $override{$name}->( $self, $name );
353 0         0 return $self->select()->get( $name );
354             }
355             }
356              
357             =item $time = $set->max_effective_date(...);
358              
359             This method extends the L
360             C method appropriately for sets of elements.
361              
362             If there are arguments, their maximum is taken, the appropriate member
363             element is set, and C is called on that element,
364             passing the date used to select the element. If there are no arguments,
365             C is called on the current element, with no
366             arguments. If the set has no members, the maximum of the arguments is
367             returned (or C if there are no arguments).
368              
369             =cut
370              
371             sub max_effective_date {
372 3     3 1 12 my ($self, @args) = @_;
373 3 50       6 @{ $self->{members} } or return max(@args);
  3         8  
374 3 50       11 if (@args) {
375 3         8 my $effective = max @args;
376 3         8 my $tle = $self->select($effective);
377 3         10 return $tle->max_effective_date($effective);
378             } else {
379 0         0 return $self->{current}->max_effective_date();
380             }
381             }
382              
383             =item @tles = $set->members ();
384              
385             This method returns all members of the set, in ascending order by
386             effective date.
387              
388             =cut
389              
390             sub members {
391 4     4 1 13 my $self = shift;
392 4         7 return ( map { $_->[1] } @{ $self->{members} } );
  4         14  
  4         13  
393             }
394              
395             =item $set->represents($class)
396              
397             If the set has a current member, this method returns true if the current
398             member represents the given class, or the class name of the current
399             member if no argument is given.
400              
401             If the set has no current member, an exception is thrown.
402              
403             See the Astro::Coord::ECI represents() method for the details of the
404             behavior if the set has a current member.
405              
406             Normally we would just let AUTOLOAD take care of this, but it turned out
407             to be handy to be able to call UNIVERSAL::can on this method.
408              
409             =cut
410              
411             sub represents {
412 6     6 1 341 my ($self, $class) = @_;
413 6 100       301 $self->{current} or croak sprintf ERR_NOCURRENT, 'represents';
414 4         16 return $self->{current}->represents($class);
415             }
416              
417             =item $set->select ($time);
418              
419             This method selects the member object that best represents the given
420             time, and returns that member. If called without an argument or with an
421             undefined argument, it simply returns the currently-selected member.
422              
423             The 'best representative' member for a given time is chosen by
424             considering all members in the set, ordered by ascending effective date.
425             If all epochs are after the given time, the earliest effective date is
426             chosen. If some epochs are on or before the given time, the latest
427             effective date that is not after the given time is chosen.
428              
429             The 'best representative' algorithm tries to select the element set that
430             would actually be current at the given time. If no element set is
431             current (i.e. all are in the future at the given time) we take the
432             earliest, to minimize peeking into the future. This is done even if that
433             member's 'backdate' attribute is false.
434              
435             =cut
436              
437             sub select : method { ## no critic (ProhibitBuiltInHomonyms)
438 16     16 1 162 my ($self, $time) = @_;
439 16 100       39 if (defined $time) {
440 13 50       21 croak <{members}};
  13         33  
441             Error - Can not select a member object until you have added members.
442             eod
443 13         22 my ($effective, $current);
444 13         18 foreach (@{$self->{members}}) {
  13         28  
445 26 100 100     100 ($effective, $current) = @$_
446             unless defined $effective && $_->[0] > $time;
447             }
448 13         29 $self->{current} = $current;
449             }
450 16         42 return $self->{current};
451             }
452              
453             =item $set->set ($name => $value ...);
454              
455             This method iterates over the individual name-value pairs. If the name
456             is an attribute of the object's model (that is, if is_model_attribute ()
457             returns true), it calls set_selected($name, $value). Otherwise, it calls
458             set_all($name, $value). If the set has no members, this method simply
459             returns.
460              
461             =cut
462              
463             sub set {
464 1     1 1 559 my ($self, @args) = @_;
465 1 50       6 return $self unless $self->{current};
466 1         3 while (@args) {
467 1         3 my $name = shift @args;
468 1 50       6 if ($self->{current}->is_model_attribute ($name)) {
469 0         0 $self->set_selected ($name, shift @args);
470             } else {
471 1         4 $self->set_all ($name, shift @args);
472             }
473             }
474 1         3 return $self;
475             }
476              
477             =item $set->set_all ($name => $value ...);
478              
479             This method sets the given attribute values on all members of the set.
480             It is not an error to call this on an object with no members, but
481             neither does it accomplish anything useful.
482              
483             =cut
484              
485             sub set_all {
486 1     1 1 4 my ($self, @args) = @_;
487 1         2 foreach my $member (@{$self->{members}}) {
  1         3  
488 2         7 $member->[1]->set (@args);
489             }
490 1         4 return $self;
491             }
492              
493             =item $set->set_selected ($name => $value ...);
494              
495             This method sets the given attribute values on the currently-selected
496             member of the set. It is an error to call this on an object with no
497             members.
498              
499             =cut
500              
501             sub set_selected {
502 0     0 1 0 my ($self, @args) = @_;
503             my $delegate = $self->{current} or
504 0 0       0 croak sprintf ERR_NOCURRENT, 'set_selected';
505 0         0 return $delegate->set (@args);
506             }
507              
508             =item $valid = $set->validate($options, $time ...);
509              
510             This method calls C on each of the members of the set,
511             removing from the set any members that fail to validate. The number of
512             members remaining in the set is returned.
513              
514             The $options argument is itself optional. If passed, it is a reference
515             to a hash of option names and values. See the
516             L C method for
517             the details.
518              
519             Each member of the set will be validated at the time it would first be
520             used for computations (if that is defined) and at the time its successor
521             in the set (if any) would first be used for computation. In addition,
522             each member will be validated at any of the C<$time> arguments that
523             happens to fall between these two times.
524              
525             If a member is removed, validate() will call itself recursively to
526             ensure that the new set is still valid.
527              
528             =cut
529              
530             sub validate {
531 0     0 1 0 my ( $self, @args ) = @_;
532 0 0       0 my $opt = HASH_REF eq ref $args[0] ? shift @args : {};
533              
534 0         0 my @members = map { [ @{ $_ } ] } @{ $self->{members} };
  0         0  
  0         0  
  0         0  
535 0 0       0 $members[0][1]->get('backdate') and $members[0][0] = undef;
536 0         0 foreach my $inx (0 .. $#members - 1) {
537 0         0 $members[$inx][2] = $members[$inx + 1][0];
538             }
539              
540 0         0 my @valid;
541 0         0 foreach ( @members ) {
542 0         0 my ($start, $tle, $end) = @{ $_ };
  0         0  
543 0         0 my @check = grep { defined $_ } $start, $end;
  0         0  
544 0         0 foreach my $time ( @args ) {
545 0 0 0     0 defined $end and $time > $end and next;
546 0 0 0     0 defined $start and $time < $start and next;
547 0         0 push @check, $time;
548             }
549 0 0       0 $tle->validate($opt, @check) and push @valid, [$start, $tle];
550             }
551              
552 0 0       0 @valid == @members and return @members;
553              
554 0 0       0 @valid or do {
555 0         0 $self->clear();
556 0         0 return 0;
557             };
558              
559 0 0       0 defined $valid[0][0]
560             or $valid[0][0] = $valid[0][1]->get('effective');
561 0 0       0 defined $valid[0][0]
562             or $valid[0][0] = $valid[0][1]->get('epoch');
563              
564 0         0 my $time;
565 0 0       0 $self->{current} and $time = $self->{current}->get('epoch');
566              
567 0         0 $self->{members} = \@valid;
568              
569 0 0       0 defined $time and $self->select($time);
570              
571 0         0 return $self->validate($opt, @args);
572             }
573              
574             # The AUTOLOAD routine does not define methods, it simply
575             # simulates them. This is because there is no good way to
576             # get rid of the routines if we end up representing a
577             # different class.
578              
579             my %selector = map {$_ => 1} qw{dynamical universal};
580              
581             sub AUTOLOAD {
582 12     12   189 my @args = @_;
583 12         22 my $self = $args[0];
584 12         13 our $AUTOLOAD;
585 12         77 (my $routine = $AUTOLOAD) =~ s/.*:://;
586             my $delegate = $self->{current} or
587 12 50       39 croak sprintf ERR_NOCURRENT, $routine;
588 12 50 33     40 if (@args > 1 && ($selector{$routine} ||
      66        
589             $delegate->is_valid_model ($routine))) {
590 5         14 $self->select ($args[1]);
591 5         7 $delegate = $self->{current};
592             }
593 12         19 my $coderef;
594 12 100       86 if ($coderef = $delegate->can ("_nodelegate_$routine")) {
    50          
595             } elsif ($coderef = $delegate->can ($routine)) {
596             #### splice @args, 0, 1, $delegate; # Not $_[0] = $delegate!!!
597 11         20 $args[0] = $delegate;
598             } else {
599 0         0 croak <
600             Error - Can not call $routine because it is not supported by
601 0         0 class @{[ref $delegate]}
602             eod
603             }
604 12         32 return $coderef->(@args);
605             }
606              
607             sub DESTROY {
608 11     11   6302 my $self = shift;
609 11         21 $self = undef;
610 11         248 return;
611             }
612              
613             1;
614             __END__