File Coverage

blib/lib/Astro/Catalog.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Astro::Catalog;
2              
3             # ---------------------------------------------------------------------------
4              
5             #+
6             # Name:
7             # Astro::Catalog
8              
9             # Purposes:
10             # Generic catalogue object
11              
12             # Language:
13             # Perl module
14              
15             # Description:
16             # This module provides a generic astronomical catalogue object
17              
18             # Authors:
19             # Alasdair Allan (aa@astro.ex.ac.uk)
20              
21             # Revision:
22             # $Id: Catalog.pm,v 1.59 2007/10/31 22:10:48 cavanagh Exp $
23              
24             # Copyright:
25             # Copyright (C) 2002 University of Exeter. All Rights Reserved.
26              
27             #-
28              
29             # ---------------------------------------------------------------------------
30              
31             =head1 NAME
32              
33             Astro::Catalog - A generic API for stellar catalogues
34              
35             =head1 SYNOPSIS
36              
37             $catalog = new Astro::Catalog( Stars => \@array );
38             $catalog = new Astro::Catalog( Format => 'Cluster', File => $file_name );
39             $catalog = new Astro::Catalog( Format => 'JCMT', Data => $scalar );
40             $catalog = new Astro::Catalog( Format => 'Simple', Data => \*STDIN );
41             $catalog = new Astro::Catalog( Format => 'VOTable', Data => \@lines );
42              
43             =head1 DESCRIPTION
44              
45             Stores generic meta-data about an astronomical catalogue. Takes a hash
46             with an array reference as an argument. The array should contain a list
47             of Astro::Catalog::Item objects. Alternatively it takes a catalogue
48             format and either the name of a catalogue file or a reference to a
49             scalar, glob or array.
50              
51             =head1 FORMATS
52              
53             For input the C module understands Cluster, Simple,
54             JCMT, TST, STL, GaiaPick, the UKIRT internal Bright Star catalogue
55             format and (a very simple parsing) of VOTable.
56              
57             The module can output all of these formats except TST (which is input only).
58              
59             =cut
60              
61              
62             # L O A D M O D U L E S --------------------------------------------------
63              
64 35     35   493897 use 5.006;
  35         144  
  35         2179  
65 35     35   334 use strict;
  35         254  
  35         1885  
66 35     35   272 use warnings;
  35         162  
  35         1908  
67 35     35   430 use warnings::register;
  35         85  
  35         7189  
68 35     35   209 use vars qw/ $VERSION $DEBUG /;
  35         78  
  35         3025  
69              
70 35     35   29663 use Astro::Coords;
  0            
  0            
71             use Astro::Catalog::Item;
72             use Time::Piece qw/ :override /;
73             use Carp;
74              
75             $VERSION = "4.31";
76             $DEBUG = 0;
77              
78              
79             # C O N S T R U C T O R ----------------------------------------------------
80              
81             =head1 METHODS
82              
83             =head2 Constructor
84              
85             =over 4
86              
87             =item B
88              
89             Create a new instance from a hash of options
90              
91             $catalog = new Astro::Catalog( Stars => \@array );
92             $catalog = new Astro::Catalog( Format => 'Cluster', File => $file_name );
93             $catalog = new Astro::Catalog( Format => 'JCMT', Data => $scalar );
94              
95             returns a reference to an C object. See the C method
96             for a list of allowed arguments.
97              
98             =cut
99              
100             sub new {
101             my $proto = shift;
102             my $class = ref($proto) || $proto;
103              
104             # bless the query hash into the class
105             my $block = bless { ALLSTARS => [],
106             CURRENT => undef, # undefined until we copy
107             ERRSTR => '',
108             ORIGIN => 'UNKNOWN',
109             COORDS => undef,
110             RADIUS => undef,
111             REFPOS => undef,
112             REFTIME => undef,
113             FIELDDATE => undef,
114             AUTO_OBSERVE => 0,
115             PREFERRED_MAG_TYPE => undef,
116             IDS => {},
117             }, $class;
118              
119             # If we have arguments configure the object
120             # Note that configuration can result in a new object
121             $block = $block->configure( @_ ) if @_;
122              
123             return $block;
124              
125             }
126              
127             # O U P T U T ------------------------------------------------------------
128              
129             =back
130              
131             =head2 Output Methods
132              
133             =over 4
134              
135             =item B
136              
137             Will serialise the catalogue object in a variety of file formats using
138             pluggable IO, see the C classes
139              
140             $catalog->write_catalog(
141             File => $file_name, Format => $file_type, [%opts] )
142             or die $catalog->errstr;
143              
144             returns true on sucess and false if the write failed (the reason
145             can be obtained using the C method). The C<%opts> are optional
146             arguments and are dependent on the output format chosen. Current
147             valid output formats are 'Simple', 'Cluster', 'JCMT' and 'VOTable'.
148              
149             The File argument can refer to a file name on disk (simple scalar),
150             a glob (eg \*STDOUT), an IO::Handle object (for example something
151             returned by the File::Temp constructor) a reference to a scalar
152             (\$content) or reference to an array. For the last two options,
153             the contents of the catalogue file are stored in the scalar or in
154             the array (a line per array entry with no new lines).
155              
156             =cut
157              
158             sub write_catalog {
159             my $self = shift;
160              
161             # grab the argument list
162             my %args = @_;
163              
164             # Go through hash and downcase all keys
165             %args = _normalize_hash( %args );
166              
167             # unless we have a Filename forget it...
168             my $file;
169             unless( $args{file} ) {
170             croak ( 'Usage: _write_catalog( File => $catalog, Format => $format');
171             } else {
172             $file = $args{file};
173             }
174              
175             # default to cluster format if no filenames supplied
176             $args{format} = 'Cluster' unless ( defined $args{format} );
177              
178             # Need to read the IO class
179             my $ioclass = _load_io_plugin( $args{format} );
180             return unless defined $ioclass;
181              
182             # remove the two handled hash options and pass the rest
183             delete $args{file};
184             delete $args{format};
185              
186             # call the io plugin's _write_catalog function
187             my $lines = $ioclass->_write_catalog( $self, %args );
188              
189             # Play it defensively - make sure we add the newlines
190             chomp @$lines;
191              
192             #use Data::Dumper;
193             #print Dumper(@$lines);
194              
195             # If we have a reference then we do not need to open or close
196             # files - simpler to deal with each case in turn. This has the
197             # side effect of repeating the join() in 3 separate places.
198             # Probably better than creating a large scalar for the one time
199             # when we do not need it.
200              
201             my $retval = 1;
202             if (ref($file)) {
203             # If we are storing in a reference to a scalar or reference
204             # to an array, just do the copy and return early. We do not
205             if (ref($file) eq 'SCALAR') {
206             # Copy single string to scalar
207             $$file = join("\n", @$lines) ."\n";
208             } elsif (ref($file) eq 'ARRAY') {
209             # Just copy the lines into the output array
210             @$file = @$lines;
211             } elsif (ref($file) eq 'GLOB' || $file->can("print") ) {
212             # GLOB - so print the full string to the file handle and flush
213             $retval = print $file join("\n", @$lines) ."\n";
214             autoflush $file 1; # We need to make sure we write the lines
215             } else {
216             croak "Can not write catalogue to reference of type ".
217             ref($file)."\n";
218             }
219              
220             } else {
221             # A file name
222             my $status = open my $fh, ">$file";
223             if (!$status) {
224             $self->errstr(__PACKAGE__ .": Error creating catalog file $file: $!" );
225             return;
226             }
227              
228             # write to file
229             $retval = print $fh join("\n", @$lines) ."\n";
230              
231             # close file
232             $status = close($fh);
233             if (!$status) {
234             $self->errstr(__PACKAGE__.": Error closing catalog file $file: $!");
235             return;
236             }
237             }
238              
239             # everything okay
240             return $retval;
241             }
242              
243             # A C C E S S O R --------------------------------------------------------
244              
245             =back
246              
247             =head2 Accessor Methods
248              
249             =over 4
250              
251             =item B
252              
253             Return (or set) the origin of the data. For example, USNOA2, GSC
254             for catalogue queries, or 'JCMT' for the JCMT pointing catalogue.
255             No constraint is placed on the content of this parameter.
256              
257             $catalog->origin( 'JCMT' );
258             $origin = $catalog->origin();
259              
260             =cut
261              
262             sub origin {
263             my $self = shift;
264             if (@_) {
265             $self->{ORIGIN} = shift;
266             }
267             return $self->{ORIGIN};
268             }
269              
270             =item B
271              
272             Error string associated with any error. Can only be trusted immediately
273             after a call that sets it (eg write_catalog).
274              
275             =cut
276              
277             sub errstr {
278             my $self = shift;
279             if (@_) {
280             $self->{ERRSTR} = shift;
281             }
282             return $self->{ERRSTR};
283             }
284              
285             =item B
286              
287             Set or return the preferred magnitude type to be returned from the
288             Astro::Catalog::Item->get_magnitude() method.
289              
290             my $type = $catalog->preferred_magnitude_type;
291             $catalog->preferred_magnitude_type( 'MAG_ISO' );
292              
293             =cut
294              
295             sub preferred_magnitude_type {
296             my $self = shift;
297             if( @_ ) {
298             my $type = shift;
299             $self->{PREFERRED_MAG_TYPE} = $type;
300             }
301             return $self->{PREFERRED_MAG_TYPE};
302             }
303              
304             =item B
305              
306             Return the number of stars in the catalogue (post filter).
307              
308             $num = $catalog->sizeof();
309              
310             =cut
311              
312             sub sizeof {
313             my $self = shift;
314             return scalar( @{$self->stars} );
315             }
316              
317             =item B
318              
319             Returns the total number of stars in the catalogue without filtering.
320              
321             =cut
322              
323             sub sizeoffull {
324             my $self = shift;
325             return scalar( @{ $self->allstars} );
326             }
327              
328             =item B
329              
330             Push a new star (or stars) onto the end of the C object
331              
332             $catalog->pushstar( @stars );
333              
334             returns the number of stars now in the Catalog object (even if no
335             arguments were supplied). The method guarantees that the stars are
336             pushed onto the internal original list and the filtered/sorted
337             version.
338              
339             Currently no check is made to make sure that the star is already
340             on one of the two lists.
341              
342             =cut
343              
344             sub pushstar {
345             my $self = shift;
346              
347             my $allref = $self->allstars;
348              
349             # push onto the original array
350             push( @$allref, @_ );
351              
352             # Update the IDs hash.
353             foreach my $star ( @_ ) {
354             if( defined( $star->id ) ) {
355             $self->{IDS}->{$star->id}++;
356             }
357             }
358              
359             # And push onto the copy ONLY IF WE HAVE A COPY
360             # We do not want to force a copy unnecsarily by using scalar context
361             if ($self->_have_copy) {
362             # push the new item onto the stack
363             my $ref = $self->stars;
364             push( @$ref, @_);
365             }
366             return;
367             }
368              
369             =item B
370              
371             Pop a star from the end of the C object. This forces
372             a copy of the array if one has not already been made (ie the original
373             version is unchanged).
374              
375             $star = $catalog->popstar();
376              
377             the method deletes the star and returns the deleted C
378             object.
379              
380             =cut
381              
382             sub popstar {
383             my $self = shift;
384              
385             my $star = pop( @{$self->stars} );
386             if( defined( $star->id ) ) {
387             $self->{IDS}->{$star->id}--;
388             }
389              
390             # pop the star out of the stack
391             return $star;
392             }
393              
394             =item B
395              
396             Return C objects that have the given ID. This forces
397             a copy of the array if one has not already been made (ie the original
398             version is unchanged).
399              
400             @stars = $catalog->popstarbyid( $id );
401              
402             The method deletes the stars and returns the deleted C
403             objects. If no star exists with the given ID, the method returns an empty list.
404              
405             If called in scalar context this method returns an array reference, and if
406             called in list context returns an array of C objects.
407              
408             This is effectively an inverse filter (see C for complementary
409             method).
410              
411             =cut
412              
413             sub popstarbyid {
414             my $self = shift;
415              
416             # Return undef if they didn't pass an ID.
417             return () unless @_;
418              
419             my $id = shift;
420              
421             # Return if we know that that star doesn't exist.
422             return () if ( ! defined( $self->{IDS} ) );
423             return () if ( ! defined( $self->{IDS}->{$id} ) );
424             return () if ( ! $self->{IDS}->{$id} );
425              
426             my @matched;
427             my @unmatched;
428             my $matched;
429             my @stars = $self->stars;
430             while ( @stars ) {
431             my $item = pop @stars;
432             if( defined( $item ) && defined( $item->id ) ) {
433             if( $item->id eq $id ) {
434             push @matched, $item;
435             $self->{IDS}->{$id}--;
436             last if ( 0 == $self->{IDS}->{$id} );
437             } else {
438             push @unmatched, $item;
439             }
440             } else {
441             push @unmatched, $item;
442             }
443             }
444              
445             push @unmatched, @stars;
446             @{ $self->stars } = @unmatched;
447              
448             return ( wantarray ? @matched : \@matched );
449              
450             }
451              
452             =item B
453              
454             Return all the stars in the catalog in their original ordering and without
455             filtering.
456              
457             @allstars = $catalog->allstars();
458             $ref = $catalog->allstars();
459              
460             In list context returns all the stars, in scalar context returns a reference
461             to the internal array. This allows the primary array to be modified in place
462             so use this with care.
463              
464             Addendum: This is pretty much for internal use only, but if you do this
465              
466             $catalog->allstars( @stars );
467              
468             you repalce the stars array with the array passed. Don't do this, it's bad!
469              
470             =cut
471              
472             sub allstars {
473             my $self = shift;
474              
475             if (@_) {
476             @{$self->{ALLSTARS}} = @_;
477             }
478              
479             return (wantarray ? @{ $self->{ALLSTARS} } : $self->{ALLSTARS} );
480             }
481              
482             =item B
483              
484             Return a list of all the C objects that are currently
485             valid and in the current order. This method may well return different
486             stars to the C method depending on the current sort in scope.
487              
488             @stars = $catalog->stars();
489              
490             in list context the copy of the array is returned, while in scalar
491             context a reference to the array is return. In scalar context, the
492             referenced array will always be that of the current list of valid
493             stars. If the current list is empty the primary list will be copied
494             into the current array so that it can be modified independently of the
495             original list. This may cost you a lot of memory. Note that changes to
496             the array ordering or content may be lost in this case whenever the
497             C method is used.
498              
499             =cut
500              
501             sub stars {
502             my $self = shift;
503              
504             # If we have a defined CURRENT array we just do whatever is needed
505             return ( wantarray ? @{ $self->{CURRENT} } : $self->{CURRENT} )
506             if $self->_have_copy;
507              
508             # If we are in list context we do not want to force a copy if
509             # we have never copied. Just return the original list.
510             # By this point we know that CURRENT is not defined
511             if (wantarray) {
512             return $self->allstars;
513             } else {
514             # scalar context so we are forced to copy the array from allstars
515             @{ $self->{CURRENT} } = $self->allstars;
516             return $self->{CURRENT};
517             }
518              
519             }
520              
521              
522             =item B
523              
524             Return the C object at index $index
525              
526             $star = $catalog->starbyindex( $index );
527              
528             the first star is at index 0 (not 1). Returns undef if no arguments
529             are provided.
530              
531             =cut
532              
533             sub starbyindex {
534             my $self = shift;
535              
536             # return unless we have arguments
537             return () unless @_;
538              
539             my $index = shift;
540              
541             return $self->stars->[$index];
542             }
543              
544             =item B
545              
546             Set the field centre and radius of the catalogue (if appropriate)
547              
548             $catalog->fieldcentre( RA => $ra,
549             Dec => $dec,
550             Radius => $radius,
551             Coords => new Astro::Coords()
552             );
553              
554             RA and Dec must be given together or as Coords.
555             Coords (an Astro::Coords object) supercedes RA/Dec.
556              
557             =cut
558              
559             sub fieldcentre {
560             my $self = shift;
561              
562             # return unless we have arguments
563             return () unless @_;
564              
565             # grab the argument list and normalize hash
566             my %args = _normalize_hash( @_ );
567              
568             if (defined $args{coords}) {
569             $self->set_coords($args{coords});
570             } elsif ( defined $args{ra} && defined $args{dec}) {
571             my $c = new Astro::Coords( type => 'J2000',
572             ra => $args{ra},
573             dec => $args{dec},
574             );
575             $self->set_coords($c);
576             }
577              
578             # set field radius
579             if ( defined $args{radius} ) {
580             $self->set_radius($args{radius});
581             }
582              
583             }
584              
585             =item B
586              
587             Set the field centre radius. Must be in arcminutes.
588              
589             $catalog->set_radius( $radius );
590              
591             =cut
592              
593             sub set_radius {
594             my $self = shift;
595             my $r = shift;
596             $self->{RADIUS} = $r;
597             return;
598             }
599              
600             =item B
601              
602             Set the field centre coordinates with an C object.
603              
604             $catalog->set_coords( $c );
605              
606             =cut
607              
608             sub set_coords {
609             my $self = shift;
610             my $c = shift;
611             croak "Coords must be an Astro::Coords"
612             unless UNIVERSAL::isa($c, "Astro::Coords");
613             $self->{COORDS} = $c;
614             }
615              
616             =item B
617              
618             Return the C object associated with the field centre.
619              
620             $c = $catalog->get_coords();
621              
622             =cut
623              
624             sub get_coords {
625             my $self = shift;
626             return $self->{COORDS};
627             }
628              
629             =item B
630              
631             Return the RA of the catalogue field centre in sexagesimal,
632             space-separated format. Returns undef if no coordinate supplied.
633              
634             $ra = $catalog->get_ra();
635              
636             =cut
637              
638             sub get_ra {
639             my $self = shift;
640             my $c = $self->get_coords;
641             return unless defined $c;
642             my $ra = $c->ra;
643             if( UNIVERSAL::isa( $ra, "Astro::Coords::Angle" ) ) {
644             $ra->str_delim( ' ' );
645             $ra->str_ndp( 2 );
646             return "$ra";
647             } else {
648             $ra = $c->ra( format => 's' );
649             $ra =~ s/:/ /g;
650             $ra =~ s/^\s*//;
651             return $ra;
652             }
653             }
654              
655             =item B
656              
657             Return the Dec of the catalogue field centre in sexagesimal
658             space-separated format with leading sign.
659              
660             $dec = $catalog->get_dec();
661              
662             =cut
663              
664             sub get_dec {
665             my $self = shift;
666             my $c = $self->get_coords;
667             return unless defined $c;
668             my $dec = $c->dec;
669             if( UNIVERSAL::isa( $dec, "Astro::Catalog::Angle" ) ) {
670             $dec->str_delim( ' ' );
671             $dec->str_ndp( 2 );
672             $dec = "$dec";
673             $dec = ( substr( $dec, 0, 1 ) eq '-' ? '' : '+' ) . $dec;
674             return $dec;
675             } else {
676             $dec = $c->dec( format => 's' );
677             $dec =~ s/:/ /g;
678             $dec =~ s/^\s*//;
679             # prepend sign if there is no sign
680             $dec = (substr($dec,0,1) eq '-' ? '' : '+' ) . $dec;
681             return $dec;
682             }
683             }
684              
685             =item B
686              
687             Return the radius of the catalogue from the field centre
688              
689             $radius = $catalog->get_radius();
690              
691             =cut
692              
693             sub get_radius {
694             my $self = shift;
695             return $self->{RADIUS};
696             }
697              
698             =item B
699              
700             If set this must contain an C object that can be
701             used as a reference position. When a reference is supplied
702             distances will be calculated from each catalog target to the
703             reference. It will also be possible to sort by distance.
704              
705             $ref = $catalog->reference;
706             $catalog->reference( $c );
707              
708             If a reference position is not specified explicitly the field
709             centre will be used instead (if defined).
710              
711             =cut
712              
713             sub reference {
714             my $self = shift;
715             if (@_) {
716             my $val = shift;
717             if (defined $val) {
718             if (UNIVERSAL::isa($val, "Astro::Coords")) {
719             $self->{REFPOS} = $val;
720             } else {
721             croak "Must supply reference as a Astro::Coords object";
722             }
723             } else {
724             $self->{REFPOS} = undef;
725             }
726             }
727              
728             # default to field centre
729             return (defined $self->{REFPOS} ? $self->{REFPOS} : $self->get_coords );
730             }
731              
732             =item B
733              
734             The reference time used for coordinate calculations. Extracted
735             from the reference coordinate object if one exists and no override
736             has been specified. If neither a default setting has been made
737             and no reference exists the current time is returned.
738              
739             $reftime = $src->reftime();
740              
741             $src->reftime( $newtime );
742              
743             Time must be a C object. This is only really important
744             for moving objects such as planets or asteroids or for occasions when
745             you are calcualting azimuth or elevation.
746              
747             =cut
748              
749             sub reftime {
750             my $self = shift;
751             if (@_) {
752             my $val = shift;
753             if (defined $val) {
754             if (UNIVERSAL::isa($val, "Time::Piece")) {
755             $self->{REFTIME} = $val;
756             } else {
757             croak "Must supply start time with a Time::Piece object";
758             }
759             } else {
760             $self->{REFTIME} = undef;
761             }
762             }
763              
764             # if we have no default ask for a coordinate object
765             my $retval = $self->{REFTIME};
766              
767             if (!$retval) {
768             my $ref = $self->reference;
769             if ($ref) {
770             # retrieve it from the coordinate object
771             $retval = $ref->datetime;
772             } else {
773             # else we just say "now"
774             $retval = gmtime();
775             }
776             }
777             return $retval;
778             }
779              
780             =item B
781              
782             The observation date/time of the field.
783              
784             $fielddate = $src->fielddate;
785              
786             $src->fielddate( $date );
787              
788             Date must be a C object. This defaults to the current
789             time when the C object was instantiated.
790              
791             =cut
792              
793             sub fielddate {
794             my $self = shift;
795              
796             if( @_ ) {
797             my $val = shift;
798             if( defined( $val ) ) {
799             if( UNIVERSAL::isa( $val, "Time::Piece" ) ) {
800             $self->{FIELDDATE} = $val;
801             } else {
802             croak "Must supply field date as a Time::Piece object";
803             }
804             }
805             }
806              
807             return $self->{FIELDDATE};
808             }
809              
810             =item B
811              
812             If this flag is true, a reset_list will automatically remove targets
813             that are not observable (as determined by C
814             which will be invoked).
815              
816             Default is false.
817              
818             =cut
819              
820             sub auto_filter_observability {
821             my $self = shift;
822             if (@_) {
823             $self->{AUTO_OBSERVE} = shift;
824             }
825             return $self->{AUTO_OBSERVE};
826             }
827              
828              
829             # C O N F I G U R E -------------------------------------------------------
830              
831             =back
832              
833             =head2 General Methods
834              
835             =over 4
836              
837             =item B
838              
839             Configures the object from multiple pieces of information.
840              
841             $newcat = $catalog->configure( %options );
842              
843             Takes a hash as argument with the list of keywords. Supported options
844             are:
845              
846             Format => Format of supplied catalog
847             File => File name for catalog on disk. Not used if 'Data' supplied.
848             Data => Contents of catalogue, either as a scalar variable,
849             reference to array of lines or reference to glob (file handle).
850             This key is used in preference to 'File' if both are present
851              
852             Stars => Array of Astro::Catalog::Item objects. Supercedes all other options.
853             ReadOpt => Reference to hash of options to be forwarded onto the
854             format specific catalogue reader. See the IO documentation
855             for details.
856              
857             If Format is supplied without any other options, a default file is requested
858             from the class implementing the formatted read. If no default file is
859             forthcoming the method croaks.
860              
861             If no options are specified the method does nothing, assumes you will
862             be supplying stars at a later time.
863              
864             The options are case-insensitive.
865              
866             Note that in some cases (when reading a catalogue) this method will
867             act as a constructor. In any case, always returns a catalog object
868             (either the same one that went in or a modified one).
869              
870             API uncertainty - in principal Data is not needed since File
871             could be overloaded (in a similar way to write_catalog).
872              
873             =cut
874              
875             sub configure {
876             my $self = shift;
877              
878             # return unless we have arguments
879             return $self unless @_;
880              
881             # grab the argument list
882             my %args = @_;
883              
884             # Go through hash and downcase all keys
885             %args = _normalize_hash( %args );
886              
887             # Check for deprecation
888             if ( exists $args{cluster} ) {
889             warnings::warnif("deprecated",
890             "Cluster option now deprecated. Use Format=>'Cluster',File=>file instead");
891             $args{file} = $args{cluster};
892             $args{format} = 'Cluster';
893             }
894              
895             # Define the actual catalogue
896             # ---------------------------
897              
898             # Stars has priority
899             if ( defined $args{stars} ) {
900              
901             # grab the array reference and stuff it into the object
902             $self->pushstar( @{ $args{stars} } );
903              
904             # Make sure we do not loop over this later
905             delete( $args{stars} );
906              
907             } elsif ( defined $args{format} ) {
908              
909             # Need to read the IO class
910             my $ioclass = _load_io_plugin( $args{format} );
911             return unless defined $ioclass;
912              
913             # Now read the catalog (overwriting $self)
914             print "# READING CATALOG $ioclass \n" if $DEBUG;
915             $self = $ioclass->read_catalog( File => $args{file},
916             Data => $args{data},
917             ReadOpt => $args{readopt} );
918              
919             croak "Error reading catalog of class $ioclass\n"
920             unless defined $self;
921              
922             # Remove used args
923             delete $args{format};
924             delete $args{file};
925             delete $args{data};
926             delete $args{readopt};
927             }
928              
929             # Define the field centre if provided
930             # -----------------------------------
931             $self->fieldcentre( %args );
932              
933             # Remove field centre args
934             delete $args{ra};
935             delete $args{dec};
936             delete $args{coords};
937              
938              
939             # Loop over any remaining args
940             for my $key ( keys %args ) {
941             my $method = lc($key);
942             $self->$method( $args{$key} ) if $self->can($method);
943             }
944              
945             if( ! defined( $self->fielddate ) ) {
946             my $date = gmtime;
947             $self->fielddate( $date );
948             }
949              
950             return $self;
951             }
952              
953             =item B
954              
955             Forces the star list to return to the original unsorted, unfiltered catalogue
956             list.
957              
958             $catalog->reset_list();
959              
960             If C is true, the list will be immediately
961             filtered for observability.
962              
963             =cut
964              
965             sub reset_list {
966             my $self = shift;
967              
968             # Simply need to clear the CURRENT
969             $self->{CURRENT} = undef;
970              
971             # and filter automatically if required
972             $self->filter_by_observability
973             if $self->auto_filter_observability;
974              
975             return;
976             }
977              
978             =item B
979              
980             Force the specified reference time into the coordinate object
981             associated with each star (in the current list). This ensures that
982             calculations on the catalogue entries are all calculated for the same
983             time.
984              
985             $catalog->force_ref_time();
986              
987             After this, the times in the coordinate objects will be set and will
988             no longer reflect current time (if they had it originally).
989              
990             =cut
991              
992             sub force_ref_time {
993             my $self = shift;
994             my $reftime = $self->reftime;
995             for my $star (@{$self->stars}) {
996             my $c = $star->coords;
997             next unless defined $c;
998              
999             # Force the time (since we can not tell if the ref time is the
1000             # current time then we can not know whether we need to override
1001             # the coords objects or not
1002             $c->datetime( $reftime );
1003             }
1004             }
1005              
1006             =item B
1007              
1008             Calculate the X and Y positions for every item in the catalog, if they
1009             have an RA and Dec.
1010              
1011             $catalog->calc_xy( $frameset );
1012              
1013             The supplied argument must be a Starlink::AST::FrameSet.
1014              
1015             =cut
1016              
1017             sub calc_xy {
1018             my $self = shift;
1019             my $frameset = shift;
1020              
1021             if( ! UNIVERSAL::isa( $frameset, "Starlink::AST::FrameSet" ) ) {
1022             croak "Argument to calc_xy() must be a Starlink::AST::FrameSet object";
1023             }
1024              
1025             # Loop through the items, obtaining the RA and Dec in radians for
1026             # each item.
1027             my @ras;
1028             my @decs;
1029             foreach my $item ( $self->stars ) {
1030             my ( $ra, $dec ) = $item->coords->radec();
1031             push @ras, $ra->radians;
1032             push @decs, $dec->radians;
1033             }
1034              
1035             # Do the calculations;
1036             my( $xref, $yref ) = $frameset->Tran2( \@ras, \@decs, 0 );
1037              
1038             # Loop through the items, pushing in the X and Y values.
1039             my $i = 0;
1040             foreach my $item ( $self->stars ) {
1041             $item->x( $xref->[$i] );
1042             $item->y( $yref->[$i] );
1043             $i++;
1044             }
1045             }
1046              
1047             =back
1048              
1049             =head2 Filters
1050              
1051             All these filters work on a copy of the full star list. The filters are
1052             cumulative.
1053              
1054             =over 4
1055              
1056             =item B
1057              
1058             Generate a filtered catalogue where only those targets that are
1059             observable are present (assumes that the current state of the
1060             coordinate objects is correct but will use the reference time returned
1061             by C). ie the object is returned to its original state and
1062             then immediately filtered by observability. Any stars without
1063             coordinates are also filtered. Starts from the current star list
1064             (which may already have been filtered).
1065              
1066             @new = $catalog->filter_by_observability();
1067              
1068             Returns the newly selected stars (as if the C method was called
1069             immediately, unless called in a non-list context.
1070              
1071             =cut
1072              
1073             sub filter_by_observability {
1074             my $self = shift;
1075              
1076             $self->force_ref_time;
1077             my $ref = $self->stars;
1078              
1079             # For each star, extract the coordinate object and, if defined
1080             # check for observability
1081             @$ref = grep { $_->coords->isObservable } grep { $_->coords; } @$ref;
1082             return $self->stars if wantarray;
1083             }
1084              
1085             =item B
1086              
1087             Given a source name filter the source list such that the
1088             supplied ID is a substring of the star ID (case insensitive).
1089              
1090             @stars = $catalog->filter_by_id( "IRAS" );
1091              
1092             Would result in a catalog with all the stars with "IRAS"
1093             in their name. This is just a convenient alternative to C
1094             and is equivalent to
1095              
1096             @stars = $catalog->filter_by_cb( sub { $_[0]->id =~ /IRAS/i; } );
1097              
1098             A regular expression can be supplied explicitly using qr//:
1099              
1100             @stars = $catalog->filter_by_id( qr/^IRAS/i );
1101              
1102             See C for a similar method that returns stars
1103             that are an exact match to ID and removes them from the current
1104             list.
1105              
1106             =cut
1107              
1108             sub filter_by_id {
1109             my $self = shift;
1110             my $id = shift;
1111              
1112             # Convert to regex if required
1113             if (not ref($id)) {
1114             $id = quotemeta( $id );
1115             $id = qr/$id/i;
1116             }
1117              
1118             return $self->filter_by_cb( sub { $_[0]->id =~ $id; });
1119              
1120             }
1121              
1122             =item B
1123              
1124             Retrieve all targets that are within the specified distance of the
1125             reference position.
1126              
1127             @selected = $catalog->filter_by_distance( $radius, $refpos );
1128              
1129             The radius is in radians. The reference position defaults to
1130             the value returned by the C method if none supplied.
1131              
1132             API uncertainty:
1133              
1134             - Should the radius default to the get_radius() method?
1135             - Should this method take hash arguments?
1136             - Should there be a units argument? (radians, arcmin, arcsec, degrees)
1137              
1138             =cut
1139              
1140             sub filter_by_distance {
1141             my $self = shift;
1142             croak "Must be at least one argument"
1143             unless scalar(@_) > 0;
1144              
1145             # Read the arguments
1146             my $radius = shift;
1147             my $refpos = shift;
1148             $refpos = $self->reference if not defined $refpos;
1149              
1150             croak "Reference position not defined"
1151             if not defined $refpos;
1152              
1153             croak "Reference must be an Astro::Coords object"
1154             unless UNIVERSAL::isa( $refpos, "Astro::Coords" );
1155              
1156             # Calculate distance and throw away outliers
1157             return $self->filter_by_cb( sub {
1158             my $star = shift;
1159             my $c = $star->coords;
1160             return if not defined $c;
1161             my $dist = $refpos->distance( $c );
1162             return if not defined $dist;
1163             return $dist < $radius;
1164             } );
1165             }
1166              
1167             =item B
1168              
1169             Filter the star list using the given the supplied callback (reference
1170             to a subroutine). The callback should expect a star object and should
1171             return a boolean.
1172              
1173             @selected = $catalog->filter_by_cb( sub { $_[0]->id == "HLTau" } );
1174             @selected = $catalog->filter_by_cb( sub { $_[0]->id =~ /^IRAS/;} );
1175              
1176             =cut
1177              
1178             sub filter_by_cb {
1179             my $self = shift;
1180             my $cb = shift;
1181              
1182             croak "Callback has to be a reference to a subroutine"
1183             unless ref($cb) eq "CODE";
1184              
1185             # Get reference to array (force copy)
1186             my $ref = $self->stars;
1187              
1188             @$ref = grep { $cb->( $_ ); } @$ref;
1189             return $self->stars;
1190             }
1191              
1192             =back
1193              
1194             =head2 Sorting
1195              
1196             The following routines are available for sorting the star catalogue.
1197             The sort applies to the current source list and not the original source list.
1198             This is the case even if no filters have been applied (ie the original
1199             unsorted catalogue is always available).
1200              
1201             =over 4
1202              
1203             =item B
1204              
1205             Sort the catalog.
1206              
1207             $catalog->sort_catalog( $mode );
1208              
1209             where mode can be one of
1210              
1211             "unsorted"
1212             "id"
1213             "ra"
1214             "dec"
1215             "az"
1216             "el"
1217              
1218             and
1219              
1220             "distance"
1221             "distance_az"
1222              
1223             if a reference position is available. "az" and "el" require that the
1224             star coordinates have an associated telescope and that the reference
1225             time is correct.
1226              
1227             If mode is a code reference, that will be passed to the sort
1228             routine directly. Note that the callback must expect $a and
1229             $b to be set.
1230              
1231             The method C is invoked prior to sorting
1232             unless the mode is "id". "name" is a synonym for "id".
1233              
1234             Currently the C option simply forces a C
1235             since there is currently no tracking of the applied filters.
1236             It should be possible to step through the original list and
1237             the current filtered list and end up with a filtered but
1238             unsorted list. This is not implemented.
1239              
1240             Pre-canned sorts are optimized because the values are precalculated
1241             prior to doing the sort rather than calculated each time through
1242             the sort.
1243              
1244             =cut
1245              
1246             sub sort_catalog {
1247             my $self = shift;
1248             my $mode = shift;
1249              
1250             # unsort is a kluge at the moment
1251             if ($mode =~ /^unsort/i) {
1252             $self->reset_list;
1253             return;
1254             }
1255              
1256             # For reference time unless we are in id/name mode
1257             $self->force_ref_time
1258             unless ($mode =~ /^(id|name)/i);
1259              
1260             # Get the star list
1261             my $stars = $self->stars;
1262              
1263             # If we have a code ref we cannot optimize so just do it
1264             if (ref($mode)) {
1265              
1266             # Just sort it all
1267             @$stars = sort $mode, @$stars;
1268              
1269             } else {
1270              
1271             # see if we have a reference object
1272             my $ref = $self->reference;
1273              
1274             # down case
1275             my $sort = lc($mode);
1276              
1277             # to try to speed up all the queries, rather than
1278             # calculating the dynamic values during the sort we should
1279             # do it outside the sort. Create an array of hashes for the
1280             # sorting
1281             my @unsorted = map {
1282             my $c = $_->coords;
1283             return () unless defined $c;
1284             my %calc = (
1285             object => $_,
1286             );
1287             $calc{ra} = $c->ra_app if $sort eq 'ra';
1288             $calc{dec} = $c->dec_app if $sort eq 'dec';
1289             $calc{az} = $c->az if $sort eq 'az';
1290             $calc{el} = $c->el if $sort eq 'el';
1291             $calc{id} = $_->id if ( $sort eq 'id' || $sort eq 'name' );
1292              
1293             if ($ref && $sort eq 'distance') {
1294             $calc{distance} = $ref->distance( $c );
1295             $calc{distance} = "Inf" unless defined $calc{distance};
1296             }
1297             if ($ref && $sort eq 'distance_az') {
1298             my $az = $c->az(format => 'deg');
1299             my $ref_az = $ref->az(format => 'deg');
1300             if (defined $az and defined $ref_az) {
1301             $calc{'distance'} = abs($az - $ref_az);
1302             }
1303             else {
1304             $calc{'distance'} = 'Inf';
1305             }
1306             }
1307             \%calc;
1308             } @$stars;
1309              
1310             # Array to hold the sorted hashes
1311             my @rSources;
1312              
1313             # Now do the sort
1314             if ($sort =~ /(name|id)/) {
1315             @rSources = sort by_id @unsorted;
1316             } elsif ($sort =~ /ra/) {
1317             @rSources = sort by_ra @unsorted;
1318             } elsif ($sort =~ /dec/) {
1319             @rSources = sort by_dec @unsorted;
1320             } elsif ($sort =~ /az/ and $sort !~ /dist/) { # Avoid accidentally
1321             # matching in distance_az
1322             # mode but why are these
1323             # regexps anyway?
1324             @rSources = sort { $a->{az} <=> $b->{az} } @unsorted;
1325             } elsif ($sort =~ /el/) {
1326             # reverse sort
1327             @rSources = sort { $b->{el} <=> $a->{el} } @unsorted;
1328             } elsif ($sort =~ /dist/) {
1329             @rSources = sort by_dist @unsorted;
1330             } else {
1331             croak "Unknown sort type: $sort";
1332             }
1333              
1334             # extract the objects in the right order
1335             @$stars = map { $_->{object} } @rSources;
1336             }
1337             }
1338              
1339             =back
1340              
1341             =begin __PRIVATE_METHODS__
1342              
1343             =head3 Internal sort optimizers.
1344              
1345             =over 4
1346              
1347             =item by_id
1348              
1349             Internal routine to sort the entries in a source catalog by ID.
1350              
1351             sort by_id @sources;
1352              
1353             Returns -1,0,1
1354              
1355             =cut
1356              
1357             sub by_id
1358             {
1359             my $b2 = $b->{id};
1360             my $a2 = $a->{id};
1361              
1362             # only compare if the ID is defined and has length
1363             if (defined $a2 && defined $b2 &&
1364             length($a2) > 0 && length($b2) > 0) {
1365             $a2 = uc($a2);
1366             $b2 = uc($b2);
1367             } else {
1368             return -1;
1369             }
1370              
1371             ($a2 cmp $b2);
1372             }
1373              
1374             =item by_ra
1375              
1376             Internal routine to sort the entries in a source catalog by RA
1377             (actually sorts by apparent RA).
1378              
1379             sort by_ra @sources;
1380              
1381             Returns -1,0,1
1382              
1383             =cut
1384              
1385             sub by_ra
1386             {
1387             return $a->{ra} <=> $b->{ra};
1388             }
1389              
1390             =item by_dec
1391              
1392             Internal routine to sort the entries in a source catalog by Dec.
1393             (actually uses apparent Dec)
1394              
1395             sort by_dec @sources;
1396              
1397             Returns -1,0,1
1398              
1399             =cut
1400              
1401             sub by_dec
1402             {
1403             return $a->{dec} <=> $b->{dec};
1404             }
1405              
1406             =item by_dist
1407              
1408             Sorts by distance from a reference position.
1409              
1410             "Inf" is handled as being a long way off even though it is included
1411             in the search results.
1412              
1413             =cut
1414              
1415             sub by_dist {
1416             my $a2 = $a->{distance};
1417             my $b2 = $b->{distance};
1418              
1419             # need to trap for Inf
1420             if ($a2 eq 'Inf' && $b2 eq 'Inf') {
1421             # they are the same
1422             return 0;
1423             } elsif ($a2 eq 'Inf') {
1424             # A is larger than B
1425             return 1;
1426             } elsif ($b2 eq 'Inf') {
1427             return -1;
1428             }
1429              
1430             $a2 <=> $b2;
1431             }
1432              
1433             =back
1434              
1435             =head2 Private methods
1436              
1437             These methods and functions are for internal use only.
1438              
1439             =over 4
1440              
1441             =item B<_have_copy>
1442              
1443             Internal method indicating whether we have a copy of the stars array
1444             or whether we are using the original version.
1445              
1446             $havecopy = $catalog->_have_copy;
1447              
1448             =cut
1449              
1450             sub _have_copy {
1451             my $self = shift;
1452             return (defined $self->{CURRENT} );
1453             }
1454              
1455             =item B<_normalize_hash>
1456              
1457             Given a hash, returns a new hash with each key down cased. If a
1458             key is duplicated after downcasing a warning is issued if the keys
1459             contain differing values.
1460              
1461             %n = _normalize_hash( %args );
1462              
1463             =cut
1464              
1465             sub _normalize_hash {
1466             my %args = @_;
1467              
1468             my %out;
1469              
1470             for my $key ( keys %args ) {
1471             my $outkey = lc($key);
1472             if (exists $out{$outkey} && $out{$outkey} ne $args{$key}) {
1473             warnings::warnif("Key '$outkey' supplied more than once with differing values. Ignoring second version");
1474             next;
1475             }
1476              
1477             # Store the key in the new hash
1478             $out{$outkey} = $args{$key};
1479              
1480             }
1481              
1482             return %out;
1483             }
1484              
1485             =item B<_load_io_plugin>
1486              
1487             Given a file format, load the corresponding IO class. In general the
1488             IO class is lower case except for the first letter. JCMT and VOTable
1489             are the exception. All plugins are in hierarchy C.
1490              
1491             Returns the class name on successful load. If the class can not be found
1492             a warning is issued and false is returned.
1493              
1494             =cut
1495              
1496             sub _load_io_plugin {
1497             my $format = shift;
1498              
1499             # Force case
1500             $format = ucfirst( lc( $format ) );
1501              
1502             # Horrible kluge since I prefer "JCMT" to "Jcmt".
1503             # Maybe we should not try to fudge case at all?
1504             # Getting out of hand - maybe we should special case Cluster
1505             # and assume uppercase elsewhere.
1506             $format = 'JCMT' if $format eq 'Jcmt';
1507             $format = 'TST' if $format eq 'Tst';
1508             $format = 'VOTable' if $format eq 'Votable';
1509             $format = 'STL' if $format eq 'Stl';
1510             $format = 'GaiaPick' if $format eq 'Gaiapick';
1511             $format = 'UKIRTBS' if $format eq 'Ukirtbs';
1512             $format = 'SExtractor' if $format eq 'Sextractor';
1513             $format = 'FINDOFF' if $format eq 'Findoff';
1514             $format = 'FITSTable' if $format eq 'Fitstable';
1515             $format = 'LCOGTFITSTable' if $format eq 'Lcogtfitstable';
1516             $format = 'RITMatch' if $format eq 'Ritmatch';
1517             $format = 'XY' if $format eq 'Xy';
1518             $format = 'ASSM' if $format eq 'Assm';
1519              
1520             my $class = "Astro::Catalog::IO::" . $format;
1521              
1522             # For some reason eval require does not work for us. Use string eval
1523             # instead.
1524             # eval { require $class; };
1525             eval "use $class;";
1526             if ($@) {
1527             warnings::warnif("Error reading IO plugin $class: $@");
1528             return;
1529             } else {
1530             return $class;
1531             }
1532              
1533             }
1534              
1535             # T I M E A T T H E B A R --------------------------------------------
1536              
1537             =back
1538              
1539             =end __PRIVATE_METHODS__
1540              
1541             =head1 COPYRIGHT
1542              
1543             Copyright (C) 2001 University of Exeter. All Rights Reserved.
1544             Some modificiations Copyright (C) 2003 Particle Physics and Astronomy
1545             Research Council. All Rights Reserved.
1546              
1547             This program was written as part of the eSTAR project and is free software;
1548             you can redistribute it and/or modify it under the terms of the GNU Public
1549             License.
1550              
1551              
1552             =head1 AUTHORS
1553              
1554             Alasdair Allan Eaa@astro.ex.ac.ukE,
1555             Tim Jenness Etjenness@cpan.orgE
1556             Tim Lister Etlister@lcogt.netE
1557              
1558             =cut
1559              
1560             # L A S T O R D E R S ------------------------------------------------------
1561              
1562             1;