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