1package Tk::AstroCatalog; 2 3=head1 NAME 4 5Tk::SourceCatalog - creates a self-standing sources catalog widget 6 7=head1 SYNOPSIS 8 9 use Tk::AstroCatalog; 10 11 $cat = new Tk::AstroCatalog($MW, $addCmd, $upDate, $onDestroy); 12 13=head1 DESCRIPTION 14 15Tk::AstroCatalog creates a non-editable text widget, displaying 16sources from a default catalog or user-selected catalog file. 17 18=cut 19 20use strict; 21use Math::Trig qw/pi/; 22use Carp; 23use Astro::Catalog; 24use Astro::Catalog::Item; 25use Astro::Coords 0.12; 26use Tk; 27use Tk::FileSelect; 28 29my $locateBug = 0; 30my $BUSY = 0; 31my @COLOR_LIST = ( 32 '#ffAAAA', '#00ff00', '#ff55ff', '#ffff00', '#00ffff', 33 '#ff00ff', '#ffffff', '#ff5555', '#55ff55', '#55ffff', '#ffff55'); 34my $COLOR_INDEX = 0; 35 36our $VERSION = '4.36'; 37 38# Kluge - this is the format of the catalog to be read 39# Needs to be given as an option on the FileSelect widget. 40our $FORMAT = 'JCMT'; 41 42=head1 PUBLIC METHODS 43 44Methods available in this class: 45 46=head2 Constructor 47 48=over 4 49 50=item new 51 52Create a new Tk::AstroCatalog object. A new catalog object will be 53created. Callbacks must be specified for -addCmd and -upDate; a 54warning is issued for -onDestroy when it is missing. 55 56 $cat = new Tk::AstroCatalog($MW, 57 -addCmd => $addCmd, 58 -upDate => $upDate, 59 -onDestroy => $onDestroy); 60 61Additionally a pre-existing Astro::Catalog object can be supplied 62using the "-catalog" option. 63 64 $cat = new Tk::AstroCatalog($MW, 65 -addCmd => $addCmd, 66 -upDate => $upDate 67 -catalog => $cat); 68 69The "-transient" option can be used if only a single value is required 70from the widget. Default behaviour is for the widget to be 71permanent. The "-transient" button does not have a "Done" button on 72the screen (ie no button to close the window without a selection) 73 74The "-addCmd" callback is triggered whenever a source is selected 75from the widget. If the widget is transient the widget will be 76closed after the first add is triggered. 77 78The "-onDestroy" callback is triggered when the "Done" button is 79pressed. 80 81The "-upDate" method is triggered whenever the contents of the 82catalog widget are refreshed/updated. 83 84It makes more sense for this widget to work like Tk::FileSelect 85when used in transient mode since we want to get the answer back 86rather than enter an event loop. 87 88The "-customColumns" method can be used to add additional columns 89to the display. This is an array of hashes specifying the 90title, width and generator function for each column. This generating 91function will be called with an Astro::Catalog::Item and must 92return a string of the given width. 93 94 -customColumns => [{ 95 title => 'Example', 96 width => 7, 97 generator => sub { 98 my $item = shift; 99 return sprintf('%7s', 'test'); 100 }}, 101 ] 102 103=cut 104 105sub new { 106 my $class = shift; 107 croak "CatWin usage: Missing args \n" unless (@_); 108 my $MW = shift; 109 my %defaults = ( 110 -default => 'defaults', 111 -transient => 0, 112 @_); 113 114 croak "Tk::AstroCatalog -addCmd option missing \n" unless exists $defaults{'-addCmd'}; 115 croak "Tk::AstroCatalog -upDate option missing \n" unless exists $defaults{'-upDate'}; 116 warn "Tk::AstroCatalog -onDestroy option missing \n" unless exists $defaults{'-onDestroy'}; 117 118 my $self = {}; 119 120 if (exists $defaults{'-catalog'}) { 121 $self->{CatClass} = ref($defaults{'-catalog'}); 122 $self->{Catalog} = $defaults{'-catalog'}; 123 } 124 else { 125 # use default settings 126 $self->{CatClass} = 'Astro::Catalog'; 127 $self->{Catalog} = $self->{CatClass}->new(); 128 } 129 130 $self->{UpDate} = undef; 131 $self->{Reset} = undef; 132 $self->{AddCommand} = undef; 133 $self->{Toplevel} = $MW->Toplevel; 134 $self->{Selected} = []; 135 $self->{Text} = undef; 136 $self->{File} = 'default'; 137 $self->{Transient} = $defaults{'-transient'}; 138 $self->{RefLabel} = ''; 139 140 if (exists $defaults{'-customColumns'}) { 141 # Store whole hash rather than just generator function 142 # in case we want to add other ways of specifying custom columns. 143 my $cols = $self->{CustomColumns} = $defaults{'-customColumns'}; 144 croak "Tk::AstroCatalog -customColumns must be an array ref" 145 unless 'ARRAY' eq ref $cols; 146 147 my $headings = ''; 148 foreach my $col (@$cols) { 149 $headings .= sprintf('%-' . $col->{'width'} . 's ', $col->{'title'}); 150 } 151 152 $self->{CustomHeadings} = $headings; 153 $self->{CustomWidth} = length($headings); 154 } 155 else { 156 $self->{CustomColumns} = undef; 157 $self->{CustomHeadings} = ''; 158 $self->{CustomWidth} = 0; 159 } 160 161 bless $self, $class; 162 $self->Reset($defaults{'-onDestroy'}) if exists $defaults{'-onDestroy'}; 163 $self->AddCommand($defaults{'-addCmd'}); 164 $self->UpDate($defaults{'-upDate'}); 165 166 $self->makeCatalog(); 167 return $self; 168} 169 170=back 171 172=head2 Data manipulation functions 173 174=over 4 175 176=item Catalog 177 178Returns and sets the Astro::Catalog object. 179 180 $catalog = $cat->Catalog(); 181 $cat->Catalog(new Astro::Catalog(...)); 182 183=cut 184 185sub Catalog { 186 my $self = shift; 187 if (@_) { 188 my $cat = shift; 189 if (UNIVERSAL::isa($cat,'Astro::Catalog')) { 190 $self->{Catalog} = $cat; 191 } 192 else { 193 croak "Tk::AstroCatalog: Catalog must be of type Astro::Catalog \n"; 194 } 195 } 196 return $self->{Catalog}; 197} 198 199=item AddCommand 200 201Returns and sets the AddCommand callback code for the catalog 202 203 $addCommand = $cat->AddCommand(); 204 $cat->AddCommand($addCommand); 205 206=cut 207 208sub AddCommand { 209 my $self = shift; 210 if (@_) { 211 my $cmd = shift; 212 if (ref($cmd) eq 'CODE') { 213 $self->{AddCommand} = $cmd; 214 } 215 else { 216 croak "CatWin: AddCommand must be of type Code Ref \n"; 217 } 218 } 219 return $self->{AddCommand}; 220} 221 222=item UpDate 223 224Returns and sets the UpDate callback code for the catalog 225 226 $update = $cat->UpDate(); 227 $cat->UpDate($update); 228 229Called whenever the contents of the text widget are redisplayed. 230The first argument will be the current object. 231 232=cut 233 234sub UpDate { 235 my $self = shift; 236 if (@_) { 237 my $cmd = shift; 238 if (ref($cmd) eq 'CODE') { 239 $self->{upDate} = $cmd; 240 } 241 else { 242 croak "CatWin: upDate must be of type Code Ref \n"; 243 } 244 } 245 return $self->{upDate}; 246} 247 248=item Reset 249 250Returns and sets the onDestroy callback code for the catalog 251 252 $reset = $cat->Reset(); 253 $cat->Reset($reset); 254 255=cut 256 257sub Reset { 258 my $self = shift; 259 if (@_) { 260 my $cmd = shift; 261 if (ref($cmd) eq 'CODE') { 262 $self->{Reset} = $cmd; 263 } 264 else { 265 croak "CatWin: Reset must be of type Code Ref \n"; 266 } 267 } 268 return $self->{Reset}; 269} 270 271=item Toplevel 272 273Returns and sets the name of the Toplevel 274 275 $toplevel = $cat->Toplevel(); 276 $cat->Toplevel($top); 277 278=cut 279 280sub Toplevel { 281 my $self = shift; 282 if (@_) { 283 $self->{Toplevel} = shift; 284 } 285 return $self->{Toplevel}; 286} 287 288=item Transient 289 290Returns and sets whether the widget should be destroyed after the 291next Add. 292 293 $toplevel = $cat->Transient(); 294 $cat->Transient($top); 295 296=cut 297 298sub Transient { 299 my $self = shift; 300 if (@_) { 301 $self->{Transient} = shift; 302 } 303 return $self->{Transient}; 304} 305 306=item Text 307 308Returns and sets the name of the Text 309 310 $text = $cat->Text(); 311 $cat->Text($text); 312 313=cut 314 315sub Text { 316 my $self = shift; 317 if (@_) { 318 my $cat = shift; 319 if (UNIVERSAL::isa($cat,'Tk::Frame')) { 320 $self->{Text} = $cat; 321 } 322 else { 323 croak "CatWin: Text widget must be of type Tk::Frame \n"; 324 } 325 } 326 return $self->{Text}; 327} 328 329=item RefLabel 330 331Configure the text displayed in the reference label widget. 332Usually a summary of the reference position. 333 334 $self->RefLabel 335 336Returns a reference to a scalar that can be used to associate 337the value with a widget. 338 339=cut 340 341sub RefLabel { 342 my $self = shift; 343 if (@_) { 344 $self->{RefLabel} = shift; 345 } 346 return \$self->{RefLabel}; 347} 348 349=item CatClass 350 351Returns and sets the name of the CatClass 352 353 $class = $cat->CatClass(); 354 $cat->CatClass($class); 355 356=cut 357 358sub CatClass { 359 my $self = shift; 360 if (@_) { 361 $self->{CatClass} = shift; 362 } 363 return $self->{CatClass}; 364} 365 366=item Selected 367 368Returns the Selected array or the indexed value of this array 369 370 @selected = $cat->Selected(); 371 $value = $cat->Selected($index); 372 373=cut 374 375sub Selected { 376 my $self = shift; 377 if (@_) { 378 my $index = shift; 379 if (@_) { 380 $self->{Selected}->[$index] = shift; 381 } 382 return $self->{Selected}->[$index]; 383 } 384 return $self->{Selected}; 385} 386 387=item file 388 389Returns and sets the File name 390 391 $file = $cat->file(); 392 $cat->file($filename); 393 394=cut 395 396sub file { 397 my $self = shift; 398 if (@_) { 399 $self->{File} = shift; 400 } 401 return $self->{File}; 402} 403 404=item makeCatalog 405 406makeCatalog creates a window that displays the 407contents of a catalog and allows the user to select as 408many entries as the user wishes. 409 410 $catalog = $cat->makeCatalog(); 411 $catalog = $cat->makeCatalog($selected); 412 413=cut 414 415sub makeCatalog { 416 my $self = shift; 417 my $selected = $self->{Selected}; 418 my $Top = $self->Toplevel; 419 $Top->geometry('+600+437'); 420 $Top->title('Source Plot: Catalog Window'); 421 $Top->resizable(0, 0); 422 423 print "made the catalog window\n" if $locateBug; 424 425 my @Sources; 426 my $topFrame = $Top->Frame( 427 -relief => 'groove', 428 -borderwidth => 2, 429 -width => 50 430 )->pack(-padx => 10, -fill => 'x', -ipady => 3, -pady => 10); 431 432 # create the header 433 my $headFrame = $topFrame->Frame( 434 -relief => 'flat', 435 -borderwidth => 2 436 )->grid(-row => 0, -sticky => 'nsew', -ipadx => 3); 437 my $head = $topFrame->Text( 438 -wrap => 'none', 439 -relief => 'flat', 440 -foreground => 'midnightblue', 441 -width => 90 + $self->{'CustomWidth'}, 442 -height => 1, 443 -font => '-*-Courier-Medium-R-Normal--*-120-*-*-*-*-*-*', 444 -takefocus => 0 445 )->grid(-sticky => 'ew', -row => 0); 446 my $title = sprintf "%5s %-16s %-12s %-13s %-4s %-3s %-3s %-5s %s%s", 447 'Index', 'Name', 'Ra', 'Dec', 'Epoc', 'Az', 'El', 'Dist', 448 $self->{'CustomHeadings'}, "Comment"; 449 $head->insert('end', $title); 450 $head->configure(-state => 'disabled'); 451 452 print "just about to make the scrollable text\n" if $locateBug; 453 454 # create the text scrollable window 455 my $T = $topFrame->Scrolled('Text', 456 -scrollbars => 'e', 457 -wrap => 'none', 458 -width => 100 + $self->{'CustomWidth'}, 459 -height => 15, 460 -font => '-*-Courier-Medium-R-Normal--*-120-*-*-*-*-*-*', 461 -setgrid => 1, 462 )->grid(-sticky => 'nsew'); 463 $T->bindtags(qw/widget_demo/); # remove all bindings but dummy "widget_demo" 464 $self->Text($T); 465 print "just before creating the done button\n" if $locateBug; 466 467 # KLUGE with a global reference label for now 468 my $RefLabel = $topFrame->Label( 469 -textvariable => $self->RefLabel, -width => 64, 470 )->grid(-sticky=>'nsew',-row=>2); 471 472 # Create button frame 473 my $buttonF2 = $Top->Frame->pack(-padx => 10, -fill => 'x'); 474 my $buttonF = $Top->Frame->pack(-padx => 10, -pady => 10); 475 476 # create the Done button if we are not transient 477 unless ($self->Transient) { 478 my $dBut = $buttonF->Button( 479 -text => 'Done', 480 -command => sub{ $self->destroy } 481 )->pack(-side=>'right'); 482 } 483 484 # create the Add button 485 my $addBut = $buttonF->Button( 486 -text=>'Add', 487 -relief => 'raised', 488 -width => 7, 489 -command => sub { 490 my $callback = $self->AddCommand; 491 my $selected = $self->Selected; 492 # turn off tags 493 foreach my $one (@$selected) { 494 # KLUGE source does not have index attribute 495 $T->tag('configure', 'd' . $one->{index}, -foreground => 'blue'); 496 } 497 $callback->($selected); 498 499 if ($self->Transient) { 500 # game over (should be a sub) 501 $self->destroy; 502 } 503 })->pack(-side => 'right', -padx => 20); 504 505 # create the Search button 506 my $searchBut; 507 $searchBut = $buttonF->Button( 508 -text => 'Search', 509 -relief => 'raised', 510 -width => 7, 511 -command => sub { 512 $searchBut->configure(-state => 'disabled'); 513 $self->getSource($self->Toplevel->Toplevel,$searchBut); 514 })->pack(-side => 'right'); 515 516 # declared for the catalog file 517 my $catEnt; 518 519 # create the Rescan button 520 my $rescanBut = $buttonF->Button( 521 -text=>'Rescan', 522 -relief => 'raised', 523 -width => 7, 524 -command => sub { 525 $self->file($catEnt->get); 526 # reset current array to original list 527 $self->Catalog->reset_list; 528 $self->fillWithSourceList ('full'); 529 })->pack(-side => 'right', -padx => '20'); 530 531 # create the Sort menu 532 my $sortmenu = $buttonF->Menubutton(-text => 'Sort by', -relief => 'raised', -width => 7); 533 $sortmenu->command(-label => 'Unsorted', -command => sub { 534 $self->Catalog->sort_catalog('unsorted'); 535 $self->fillWithSourceList('full'); 536 }); 537 $sortmenu->command(-label => 'Id', -command => sub { 538 $self->Catalog->sort_catalog('id'); 539 $self->fillWithSourceList('full'); 540 }); 541 $sortmenu->command(-label => 'Ra', -command => sub { 542 $self->Catalog->sort_catalog('ra'); 543 $self->fillWithSourceList('full'); 544 }); 545 $sortmenu->command(-label => 'Dec', -command => sub { 546 $self->Catalog->sort_catalog('dec'); 547 $self->fillWithSourceList('full'); 548 }); 549 $sortmenu->command(-label => 'Az', -command => sub { 550 $self->Catalog->sort_catalog('az'); 551 $self->fillWithSourceList('full'); 552 }); 553 $sortmenu->command(-label => 'El', -command => sub { 554 $self->Catalog->sort_catalog('el'); 555 $self->fillWithSourceList('full'); 556 }); 557 # add sort by distance if we have a reference position 558 if ($self->Catalog->reference) { 559 $sortmenu->command(-label => 'Distance', -command => sub { 560 $self->Catalog->sort_catalog('distance'); 561 $self->fillWithSourceList('full'); 562 }); 563 $sortmenu->command(-label => 'Distance in Az', -command => sub { 564 $self->Catalog->sort_catalog('distance_az'); 565 $self->fillWithSourceList('full'); 566 }); 567 } 568 569 $sortmenu->pack(-side => 'right', -padx => '20'); 570 571 # create the catalog menu button 572 my $catB = $buttonF2->Menubutton(-text => 'Catalogs', -relief => 'raised', -width => 8); 573 $catB->command(-label => 'Default Catalog', -command => sub { 574 $self->file('default'); 575 $catEnt->delete('0', 'end'); 576 $catEnt->insert(0, $self->file); 577 # No filename for default 578 $self->Catalog($self->CatClass->new( 579 Format => $FORMAT, 580 )); 581 $self->fillWithSourceList('full'); 582 }); 583 $catB->command(-label => 'File Catalog', -command => sub { 584 my $dir; 585 chomp($dir = `pwd`); 586 my $win = $Top->FileSelect(-directory => $dir);; 587 my $file = $win->Show; 588 if (defined $file && $file ne '') { 589 $catEnt->delete('0', 'end'); 590 $catEnt->insert('0', $file); 591 592 # Get the current catalogue properties [should be a sub] 593 my $oldcat = $self->Catalog; 594 my ($refc, $canobs); 595 if (defined $oldcat) { 596 $refc = $oldcat->reference; 597 $canobs = $oldcat->auto_filter_observability; 598 } 599 600 $self->file($file); 601 $self->Catalog($self->CatClass->new( 602 File =>$self->file, 603 Format => $FORMAT 604 )); 605 606 # Propagate previous info 607 $self->Catalog->reference($refc) if defined $refc; 608 $self->Catalog->auto_filter_observability($canobs); 609 $self->Catalog->reset_list; 610 611 $self->fillWithSourceList('full'); 612 } 613 }); 614 $catB->pack(-side => 'left', -padx => 10); 615 616 # Create the catalog file label 617 $buttonF2->Label( 618 -text => "Catalog file:", 619 )->pack(-side => 'left'); 620 $catEnt = $buttonF2->Entry( 621 -relief => 'sunken', 622 -width => 37 623 )->pack(-side => 'left', -padx => 10); 624 $catEnt->bind('<KeyPress-Return>' => sub { 625 # Get the current catalogue properties [should be a sub] 626 my $oldcat = $self->Catalog; 627 my ($refc, $canobs); 628 if (defined $oldcat) { 629 $refc = $oldcat->reference; 630 $canobs = $oldcat->auto_filter_observability; 631 } 632 633 $self->file($catEnt->get); 634 if ($catEnt->get eq 'default') { 635 $self->Catalog($self->CatClass->new( 636 Format => $FORMAT 637 )); 638 } 639 else { 640 $self->Catalog($self->CatClass->new( 641 File => $self->file, 642 Format => $FORMAT 643 )); 644 } 645 # Propagate previous info 646 $self->Catalog->reference($refc) if defined $refc; 647 $self->Catalog->auto_filter_observability($canobs); 648 $self->Catalog->reset_list; 649 650 $self->fillWithSourceList('full'); 651 }); 652 $catEnt->insert(0, $self->file); 653 654 print "made it past all the buttons and just about to fill...\n" if $locateBug; 655 # if we do not have a catalog yet create one 656 unless ($self->Catalog) { 657 $self->file($catEnt->get); 658 $self->Catalog($self->CatClass->new( 659 File => $self->file, 660 Format => $FORMAT 661 )); 662 } 663 $self->fillWithSourceList('full'); 664 665 return $self; 666} 667 668=item destroy 669 670Remove the widget from display. Leaves calling the 671Reset handler to the DESTROY method. 672 673=cut 674 675sub destroy { 676 my $self = shift; 677 my $Top = $self->Toplevel; 678 $Top->destroy() if defined $Top && Exists($Top); 679} 680 681=item DESTROY 682 683Object destructor. Triggers when the object is destroyed. 684Guarantees to destroy the Toplevel widget and does trigger 685the onDestroy callback. 686 687=cut 688 689sub DESTROY { 690 my $self = shift; 691 my $callback = $self->Reset; 692 $callback->() if defined $callback; 693 my $Top = $self->Toplevel; 694 $Top->destroy() if defined $Top && Exists($Top); 695} 696 697=item fillWithSourceList 698 699Fills a text widget with the list of current sources 700 701 $cat->fillWithSourceList(); 702 $cat->fillWithSourceList($text, $selected, $task, $index); 703 $cat->fillWithSourceList($text, $selected, $task); 704 $cat->fillWithSourceList($text, $selected); 705 706Also triggers the UpDate method. 707 708=cut 709 710sub fillWithSourceList { 711 my (@bold, @normal); 712 my $self = shift; 713 my $T = $self->Text; 714 my $selected = $self->Selected; 715 my $task = shift; 716 my $index = shift; 717 my @entered = (); 718 my ($line, $itag); 719 720 # Retrieve the objects 721 # forcing the reference time 722 $self->Catalog->force_ref_time; 723 my @stars = $self->Catalog->stars; 724 my @sources = map {$_->coords} @stars; 725 726 # Enable infobox for access 727 $T->configure(-state => 'normal'); 728 729 # Clear the existing widgets 730 if (defined $task && $task eq 'full') { 731 $T->delete('1.0', 'end'); 732 foreach my $source (@sources) { 733 # KLUGE source does not have index attribute 734 if (exists $source->{index} && defined $source->{index}) { 735 $T->tagDelete('d' . $source->{index}); 736 } 737 } 738 739 # And clear the current selection 740 @$selected = (); 741 } 742 743 # Set up display styles 744 if ($T->depth > 1) { 745 @bold = (-background => "#eeeeee", -relief => 'raised', -borderwidth => 1); 746 @normal = (-background => undef, -relief => 'flat'); 747 } 748 else { 749 @bold = (-foreground => 'white', -background => 'black'); 750 @normal = (-foreground => undef, -background => undef); 751 } 752 $T->tag(qw/configure normal -foreground blue/); 753 $T->tag(qw/configure inactive -foreground black/); 754 $T->tag(qw/configure selected -foreground red/); 755 foreach (@COLOR_LIST) { 756 $T->tag('configure', $_, -foreground => $_); 757 } 758 759 # Get a reference coordinate from the object 760 my $ref = $self->Catalog->reference; 761 762 # write the label 763 if ($ref) { 764 my ($az, $el) = $ref->azel(); 765 my $summary = sprintf("%-15s Az: %3.0f El: %3.0f", 766 $ref->name, $az->degrees, $el->degrees); 767 $self->RefLabel("Reference position: $summary"); 768 } 769 else { 770 # blank it 771 $self->RefLabel(''); 772 } 773 774 # Insert the current values 775 if (defined $task && $task eq 'full') { 776 my $len = @sources; 777 for ($index = 0; $index < $len; $index ++) { 778 my $source = $sources[$index]; 779 # KLUGE source does not have index attribute 780 $source->{index} = $index; 781 # KLUGE - source summary should add az, el and we should 782 # add distance 783 my $distance = " --- "; 784 if ($ref) { 785 my $d = $ref->distance($source); 786 if (defined $d) { 787 $distance = sprintf("%5.0f", $d->degrees); 788 } 789 else { 790 $distance = " Inf"; 791 } 792 } 793 my $custom = ''; 794 if ($self->{'CustomColumns'}) { 795 $custom = join(' ', map {$_->{'generator'}->($stars[$index])} 796 @{$self->{'CustomColumns'}}) . ' '; 797 } 798 $line = sprintf("%-4d %s %3.0f %3.0f %s %s%s", 799 $index, 800 $source->summary(), 801 $source->az(format=>'d'), 802 $source->el(format=>'d'), 803 $distance, 804 $custom, 805 $source->comment 806 ); 807 if ($self->isWithin ($source, @$selected)) { 808 $self->inswt("$line\n","d$index",'selected'); 809 } 810 else { 811 # KLUGE - source does not really have active or color attributes 812 # KLUGE2 - "active" is never set! 813 if ($source->{active}) { 814 if ($source->{color} ne '') { 815 $self->inswt("$line\n", "d$index", $source->{color}); 816 } 817 else { 818 $self->inswt("$line\n", "d$index", 'normal'); 819 } 820 } 821 else { 822 $self->inswt("$line\n", "d$index", 'inactive'); 823 } 824 } 825 } 826 827 $len = @sources; 828 for ($itag = 0; $itag < $len; $itag ++) { 829 my $dtag = "d$itag"; 830 $T->tag('bind', $dtag, '<Any-Enter>' => sub { 831 shift->tag('configure', $dtag, @bold); 832 }); 833 $T->tag('bind', $dtag, '<Any-Leave>' => sub { 834 shift->tag('configure', $dtag, @normal); 835 }); 836 $T->tag('bind', $dtag, '<ButtonRelease-1>' => sub { 837 unless ($BUSY) { 838 unless ($self->isWithin($sources[substr($dtag, 1, 99)], @$selected) ) { 839 shift->tag('configure', $dtag, -foreground => 'red'); 840 push (@$selected, $sources[substr($dtag, 1, 99)]); 841 } 842 else { 843 # KLUGE - no color support in class 844 if ($sources[substr($dtag, 1, 99)]->{color} ne '') { 845 shift->tag('configure', $dtag, -foreground => $sources[substr($dtag, 1, 99)]->color()); 846 } 847 else { 848 shift->tag('configure', $dtag, -foreground => 'blue'); 849 } 850 $self->remove($sources[substr($dtag, 1, 99)], $selected); 851 } 852 } 853 }); 854 $T->tag('bind', $dtag, '<Double-1>' => sub { 855 $BUSY = 1; 856 my $source = $sources[substr($dtag, 1, 99)]; 857 push (@$selected, $source); 858 my $T = shift; 859 my $callback = $self->AddCommand; 860 # turn off tags 861 foreach $source (@$selected) { 862 # KLUGE source does not have index attribute 863 $T->tag('configure', 'd' . $source->{index}, -foreground => 'blue'); 864 } 865 print " ref(@$selected) is selected \n" if $locateBug; 866 my @array = [1..2]; 867 $callback->($selected); 868 $BUSY = 0; 869 @$selected = (); 870 871 $self->destroy if $self->Transient; 872 }); 873 } 874 } 875 876 $T->mark(qw/set insert 1.0/); 877 878 # Disable access to infobox 879 $T->configure(-state => 'disabled'); 880 881 # Trigger an update callback 882 $self->UpDate->($self); 883} 884 885=item color 886 887Returns a color from @COLOR_LIST and increments the latter's index 888 889 $color = $cat->color(); 890 891=cut 892 893sub getColor { 894 my $color = $COLOR_LIST[$COLOR_INDEX]; 895 my $len = @COLOR_LIST; 896 $COLOR_INDEX++; 897 $COLOR_INDEX = $COLOR_INDEX % $len; 898 return $color; 899} 900 901=item error 902 903Displays an error message in Tk 904 905 $cat->error('Error message'); 906 907=cut 908 909sub error { 910 my $MW = shift; 911 my $errWin = $MW->Toplevel(-borderwidth => 10); 912 $errWin->title('Observation Log Error!'); 913 $errWin->resizable(0,0); 914 $errWin->Button( 915 -text => 'Ok', 916 -command => sub { 917 destroy $errWin; 918 })->pack(-side=>'bottom'); 919 my $message = shift; 920 $errWin->Label ( 921 -text => "\nError!\n\n " . $message . " \n", 922 -relief => 'sunken' 923 )->pack(-side => 'bottom', -pady => 10); 924 $errWin->title(shift) if @_; 925 $MW->update; 926 $errWin->grab; 927} 928 929=item inswt 930 931inswt inserts text into a given text widget and applies 932one or more tags to that text. 933 934Parameters: 935 $text - Text to insert (it's inserted at the "insert" mark) 936 $args - One or more tags to apply to text. If this is empty 937 then all tags are removed from the text. 938 939 $cat->inswt($text, $args); 940 941=cut 942 943sub inswt { 944 my $self = shift; 945 my $w = $self->Text; 946 my ($text, @args) = @_; 947 my $start = $w->index('insert'); 948 949 $w->insert('insert', $text); 950 foreach my $tag ($w->tag('names', $start)) { 951 $w->tag('remove', $tag, $start, 'insert'); 952 } 953 foreach my $i (@args) { 954 $w->tag('add', $i, $start, 'insert'); 955 } 956} 957 958=item getSource 959 960getSource prompts the user to enter source coords and name 961and filters the catalog based on the input provided. 962 963Takes the new top level widget to use, and the search button 964to be re-activated when this window closes. 965 966 $obj = $cat->getSource($toplevel, $search_button); 967 968=cut 969 970sub getSource { 971 my $self = shift; 972 my $Top = shift; 973 my $searchButton = shift; 974 my @Epocs = ('RJ', 'RB'); 975 my %distances = ( 976 '15 degrees' => 15.0, 977 '5 degrees' => 5.0, 978 '1 degree' => 1.0, 979 '30\'' => 0.5, 980 '15\'' => 0.25, 981 '5\'' => 1.0 / 12, 982 '1\'' => 1.0 / 60, 983 '30\'\'' => 0.5 / 60, 984 '15\'\'' => 0.25 / 60, 985 '5\'\'' => 1.0 / 12 / 60, 986 '1\'\'' => 1.0 / 3600, 987 ); 988 my $name; 989 990 $Top->title('Source Plot'); 991 $Top->resizable(0,0); 992 my $topFrame = $Top->Frame( 993 -relief => 'groove', -borderwidth => 2, -width => 50 994 )->pack(-padx => 10, -fill => 'x', -ipady => 10, -pady => 10); 995 996 $topFrame->Label ( 997 -text => "Name:" 998 )->grid(-column=>0, -row=>0); 999 my $nameEnt = $topFrame->Entry( 1000 -relief=>'sunken', -width=>15 1001 )->grid(-column => 1, -row => 0, -padx => 10, -pady => 3); 1002 1003 $topFrame->Label ( 1004 -text => "Ra:" 1005 )->grid(-column => 0, -row => 1); 1006 my $raEnt = $topFrame->Entry( 1007 -relief => 'sunken', -width => 15 1008 )->grid(-column => 1, -row => 1, -padx => 10, -pady => 3); 1009 1010 $topFrame->Label ( 1011 -text => "Dec:" 1012 )->grid(-column => 0, -row => 2); 1013 my $decEnt = $topFrame->Entry( 1014 -relief => 'sunken', -width => 15 1015 )->grid(-column => 1, -row => 2, -padx => 10, -pady => 3); 1016 1017 $topFrame->Label( 1018 -text => 'Distance:' 1019 )->grid(-column => 0, -row => 3); 1020 my $distEnt = '1\''; 1021 my $distB = $topFrame->Menubutton( 1022 -text => $distEnt, -relief => 'raised', -width => 15); 1023 foreach my $dist (sort {$distances{$b} <=> $distances{$a}} keys %distances) { 1024 $distB->command(-label => $dist, -command => sub { 1025 $distB->configure(-text => $dist); 1026 $distEnt = $dist; 1027 }); 1028 } 1029 $distB->grid(-column => 1, -row => 3, -padx => 10, -pady => 5, -sticky => 'w'); 1030 1031 $topFrame->Label ( 1032 -text => "Epoc:" 1033 )->grid(-column => 0, -row => 4, -padx => 5, -pady => 5); 1034 my $epocEnt = 'RJ'; 1035 my $epocB = $topFrame->Menubutton( 1036 -text => $epocEnt, -relief => 'raised', -width => 15); 1037 foreach $name (@Epocs) { 1038 $epocB->command(-label => $name, -command => sub { 1039 $epocB->configure(-text => $name); 1040 $epocEnt = $name; 1041 }); 1042 } 1043 $epocB->grid(-column => 1, -row => 4, -padx => 10, -pady => 5, -sticky => 'w'); 1044 1045 my $buttonF = $Top->Frame->pack(-padx => 10, -pady => 10); 1046 $buttonF->Button( 1047 -text => 'Ok', 1048 -command => sub { 1049 my $name = $nameEnt->get(); undef $name if $name eq ''; 1050 my $ra = $raEnt->get(); undef $ra if $ra eq ''; 1051 my $dec = $decEnt->get(); undef $dec if $dec eq ''; 1052 1053 my $dec_tol = pi * $distances{$distEnt} / 180; 1054 my $ra_tol = $dec_tol * 15; 1055 1056 # Filter by name if a name was specified. 1057 1058 $self->Catalog()->filter_by_id($name) if defined $name; 1059 1060 # Use Astro::Catalog's coordinate filter by distance 1061 # if possible. 1062 1063 if (defined $ra and defined $dec) { 1064 my $coord = new Astro::Coords(ra => $ra, dec => $dec, 1065 type => $epocEnt eq 'RB' ? 'B1950' : 'J2000'); 1066 1067 $self->Catalog()->filter_by_distance( 1068 $dec_tol, $coord); 1069 } 1070 elsif (defined $ra or defined $dec) { 1071 # Searching by RA or Dec alone isn't implemented 1072 # by Astro::Catalog, so use a callback filter. 1073 1074 $ra = Astro::Coords::Angle::Hour->new( 1075 $ra, range => '2PI')->radians() 1076 if defined $ra; 1077 $dec = Astro::Coords::Angle->new($dec)->radians() 1078 if defined $dec; 1079 1080 $self->Catalog()->filter_by_cb(sub { 1081 my $item = shift; 1082 my $coord = $item->coords(); 1083 my ($item_ra, $item_dec) = map {$_->radians()} 1084 $epocEnt eq 'RB' 1085 ? $coord->radec1950() 1086 : $coord->radec(); 1087 1088 return ((! defined $ra or 1089 abs($item_ra - $ra) <= $ra_tol) 1090 and (! defined $dec or 1091 abs($item_dec - $dec) <= $dec_tol)); 1092 }); 1093 } 1094 1095 $self->fillWithSourceList('full'); 1096 $Top->destroy(); 1097 })->pack(-side=>'right'); 1098 $buttonF->Button( 1099 -text => 'Cancel', 1100 -command => sub { 1101 $Top->destroy(); 1102 })->pack(-side=>'right'); 1103 1104 $Top->bind('<Destroy>', sub { 1105 my $widget = shift; 1106 return unless $widget == $Top; 1107 $searchButton->configure(-state => 'normal'); 1108 }); 1109 1110 $Top->update; 1111 $Top->grab; 1112 return; 1113} 1114 1115=item isWithin 1116 1117isWithin returns a boolean value as to whether an element is 1118within the array specified. 1119 1120 $obj = $cat->isWithin($element, @array); 1121 1122=cut 1123 1124sub isWithin { 1125 my $self = shift; 1126 my $element = shift; 1127 my @array = @_; 1128 my $len = @array; 1129 foreach (@array) { 1130 # KLUGE - need an isEqual method rather than this. Will break 1131 # for none RA/Dec coordinates. Had to remove epoch check 1132 if ($element->name() eq $_->name() 1133 && $element->ra() eq $_->ra() 1134 && $element->dec() eq $_->dec()) { 1135 return 1; 1136 } 1137 } 1138 return 0; 1139} 1140 1141=item remove 1142 1143Removes the item passed from the array specified. 1144 1145 $obj = $cat->remove($element, @array); 1146 1147=cut 1148 1149sub remove { 1150 my $self = shift; 1151 my $element = shift; 1152 my $array = shift; 1153 my $len = @$array; 1154 my @temp; 1155 my $flag = 0; 1156 1157 # KLUGE - epcc no longer required 1158 for (my $index = 0; $index < $len; $index++) { 1159 if ($element->name() eq $$array[$index]->name() 1160 && $element->ra() eq $$array[$index]->ra() 1161 && $element->dec() eq $$array[$index]->dec()) { 1162 $flag = -1; 1163 } 1164 else { 1165 $temp[$index+$flag] = $$array[$index]; 1166 } 1167 } 1168 @$array = @temp; 1169} 1170 11711; 1172 1173__END__ 1174 1175=back 1176 1177=head1 SEE ALSO 1178 1179L<Astro::Catalog>, L<Astro::Catalog::Item>, L<Astro::Coords> 1180 1181=head1 COPYRIGHT 1182 1183Copyright (C) 2013 Science & Technology Facilities Council. 1184Copyright (C) 1999-2002,2004 Particle Physics and Astronomy Research Council. 1185All Rights Reserved. 1186 1187=head1 AUTHOR 1188 1189Major subroutines and layout originally designed by Casey Best 1190(University of Victoria) with modifications to create independent 1191composite widget by Tim Jenness and Pam Shimek (University of 1192Victoria) 1193 1194Revamped for Astro::Catalog by Tim Jenness. 1195 1196=cut 1197