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