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