File Coverage

blib/lib/Astro/Catalog/Query/SkyCat.pm
Criterion Covered Total %
statement 27 27 100.0
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 36 36 100.0


line stmt bran cond sub pod time code
1             package Astro::Catalog::Query::SkyCat;
2              
3             =head1 NAME
4              
5             Astro::Catalog::Query::SkyCat - Generate SkyCat catalogue query clients
6              
7             =head1 SYNOPSIS
8              
9             =head1 DESCRIPTION
10              
11             On load, automatically parse the SkyCat server specification file
12             from C<~/.skycat/skycat.cfg>, if available, and dynamically
13             generate query classes that can send queries to each catalog
14             server and parse the results.
15              
16             =cut
17              
18 2     2   9002331 use 5.006;
  2         15  
  2         299  
19 2     2   20 use strict;
  2         3  
  2         117  
20 2     2   72 use warnings;
  2         4  
  2         110  
21 2     2   12 use warnings::register;
  2         4  
  2         976  
22 2     2   12 use vars qw/ $VERSION $FOLLOW_DIRS $DEBUG /;
  2         4  
  2         198  
23              
24 2     2   5115 use Data::Dumper;
  2         15760  
  2         217  
25 2     2   18 use Carp;
  2         4  
  2         130  
26 2     2   13 use File::Spec;
  2         4  
  2         63  
27              
28 2     2   13 use base qw/ Astro::Catalog::Transport::REST /;
  2         4  
  2         1842  
29              
30              
31             $VERSION = '4.31';
32             $DEBUG = 0;
33              
34             # Controls whether we follow 'directory' config entries and recursively
35             # expand those. Default to false at the moment.
36             $FOLLOW_DIRS = 0;
37              
38             # This is the name of the config file that was used to generate
39             # the content in %CONFIG. Can be different to the contents ofg_file
40             # if that
41             my $CFG_FILE;
42              
43             # This is the content of the config file
44             # organized as a hash indexed by remote server shortname
45             # this has the advantage of removing duplicates
46             my %CONFIG;
47              
48             =head1 METHODS
49              
50             =head2 Constructor
51              
52             =over 4
53              
54             =item B
55              
56             Simple constructor. Forces read of config file if one can be found and
57             the config has not been read previously. If no config file can be located
58             the query object can not be instantiated since it will not know the
59             location of any servers.
60              
61             $q = new Astro::Catalog::Query::SkyCat( catalog => 'gsc', %options );
62             $q = new Astro::Catalog::Query::SkyCat( catalog => 'gsc@eso', %options );
63              
64             The C field must be present, otherwise the new object will
65             not know which remote server to use and which options are mandatory
66             in the query. Note that the remote catalog can not be changed after
67             the object is instantiated. In general it is probably not wise to
68             try to change the remote host via either the C or
69             C methods unless you know what you are doing. Modifying your
70             C file is safer.
71              
72             Currently only one config file is supported at any given time.
73             If a config file is changed (see the C class method)
74             the current config is overwritten automatically.
75              
76             It is not possible to override the catalog file in the
77             constructor. Use the C class method instead.
78              
79             Obviously a config per object can be supported but this is
80             probably not that helpful. This will be reconsidered if demand
81             is high.
82              
83             =cut
84              
85             sub new {
86             my $proto = shift;
87             my $class = ref($proto) || $proto;
88              
89             # Instantiate via base class
90             my $block = $class->SUPER::new( @_ );
91              
92             return $block;
93             }
94              
95             =back
96              
97             =head2 Accessor methods
98              
99             =over 4
100              
101             =item B<_selected_catalog>
102              
103             Catalog name selected by the user and currently configured for
104             this object. Not to be used outside this class..
105              
106             =cut
107              
108             sub _selected_catalog {
109             my $self = shift;
110             if (@_) {
111             # The class has to be configured as a hash!!!
112             $self->{SKYCAT_CATALOG} = shift;
113             }
114             return $self->{SKYCAT_CATALOG};
115             }
116              
117             =back
118              
119             =head2 General methods
120              
121             =over 4
122              
123             =item C
124              
125             Configure the object. This calls the base class configure , after it has
126             made sure that a sky cat config file has been read (otherwise we will
127             not be able to vet the incoming arguments.
128              
129             =cut
130              
131             sub configure {
132             my $self = shift;
133              
134             # load a config if we do not have one read yet
135             # Note that this may force a remote URL read via directory
136             # directives even though we do not have a user agent configured...
137             $self->_load_config() unless %CONFIG;
138              
139             # Error if we have no config yet
140             croak "Error instantiating SkyCat object since no config was located"
141             unless %CONFIG;
142              
143             # Now we need to configure this object based on the
144             # supplied catalog name. This is not really a public interface
145             # let's call it a protected interface available to subclases
146             # even though we are not technically a subclass...
147             my %args = Astro::Catalog::_normalize_hash(@_);
148              
149             croak "A remote service catalog name must be provided using a 'catalog' key"
150             unless exists $args{catalog};
151              
152             # case-insensitive
153             my $cat = lc($args{catalog});
154              
155             # if we have an entry in %CONFIG then we can use it directly
156             # else we may have a root name without a remote server
157             if (! exists $CONFIG{$cat}) {
158             my $name = $cat;
159             # clear it and look for another
160             $cat = undef;
161              
162             # if name does not include an @ we probably have a generic catalog
163             # and just need to choose a random specific version
164             if ($name !~ /\@/) {
165              
166             # look through the catalog
167             for my $rmt (keys %CONFIG) {
168             if ($rmt =~ /^$name\@/) {
169             # a match
170             $cat = $rmt;
171             }
172             }
173             }
174             # No luck finding catalog name
175             croak "unable to find a remote service named $name"
176             unless defined $cat;
177             }
178              
179             # Now we know the details we need to store this somewhere in
180             # the object so that it won't get clobbered. Otherwise the
181             # super class configure will not be able to get the information
182             # it needs. We can not simply store this in options since configure
183             # does not know it is an allowed option...
184             $self->_selected_catalog( $cat );
185              
186             # delete catalog from list
187             delete $args{catalog};
188              
189             # Configure
190             $self->SUPER::configure( %args );
191              
192             }
193              
194             =item B<_build_query>
195              
196             Construct a query URL based on the options.
197              
198             $url = $q->_build_query();
199              
200             =cut
201              
202             sub _build_query {
203             my $self = shift;
204              
205             my $cat = $self->_selected_catalog();
206              
207             # Get the URL
208             my $url = $CONFIG{$cat}->{url};
209              
210             # Translate all the options to the internal skycat format
211             my %translated = $self->_translate_options();
212              
213             print "Translated query: ".Dumper(\%translated,$url) if $DEBUG;
214              
215             # Now for each token replace it in the URL
216             for my $key (keys %translated) {
217             my $tok = "%". $key;
218             croak "Token $tok is mandatory but was not specified"
219             unless defined $translated{$key};
220             $url =~ s/$tok/$translated{$key}/;
221             }
222              
223             print "Final URL: $url\n" if $DEBUG;
224              
225             return $url;
226             }
227              
228              
229             =item B<_parse_query>
230              
231             All the SkyCat servers return data in TST format.
232             Need to make sure that column information is passed
233             into the TST parser.
234              
235             =cut
236              
237             sub _parse_query {
238             my $self = shift;
239              
240             # Get the catalog info
241             my $cat = $self->_selected_catalog();
242              
243             # and extract formatting information needed by the TST parser
244             my %params;
245             for my $key (keys %{ $CONFIG{$cat} }) {
246             if ($key =~ /_col$/) {
247             print "FOUND column specified $key\n" if $DEBUG;
248             $params{$key} = $CONFIG{$cat}->{$key};
249             }
250             }
251              
252             # If this catalogue is a GSC, pass in a GSC parameter
253             $params{gsc} = 1 if $cat =~ /^gsc/i;
254              
255             print $self->{BUFFER} ."\n" if $DEBUG;
256              
257             # Make sure we set origin and field centre if we know it
258             my $newcat = new Astro::Catalog(
259             Format => 'TST', Data => $self->{BUFFER},
260             ReadOpt => \%params,
261             Origin => $CONFIG{$cat}->{long_name},
262             );
263              
264             # set the field centre
265             my %allow = $self->_get_allowed_options();
266             my %field;
267             for my $key ("ra","dec","radius") {
268             if (exists $allow{$key}) {
269             $field{$key} = $self->query_options($key);
270             }
271             }
272             $newcat->fieldcentre( %field );
273              
274             return $newcat;
275             }
276              
277             =item B<_get_allowed_options>
278              
279             This method declares which options can be configured by the user
280             of this service. Generated automatically by the skycat config
281             file and keyed to the requested catalog.
282              
283             =cut
284              
285             sub _get_allowed_options {
286             my $self = shift;
287             my $cat = $self->_selected_catalog();
288              
289             return %{ $CONFIG{$cat}->{allow} };
290              
291             }
292              
293             =item B<_get_default_options>
294              
295             Get the default options that are relevant for the selected
296             catalog.
297              
298             %defaults = $q->_get_default_options();
299              
300             =cut
301              
302             sub _get_default_options {
303             my $self = shift;
304              
305             # Global skycat defaults
306             my %defaults = (
307             # Target information
308             ra => undef,
309             dec => undef,
310             id => undef,
311              
312             # Limits
313             radmin => 0,
314             radmax => 5,
315             width => 10,
316             height => 10,
317              
318             magfaint => 100,
319             magbright => 0,
320              
321             nout => 20000,
322             cond => '',
323             );
324              
325             # Get allowed options
326             my %allow = $self->_get_allowed_options();
327              
328             # Trim the defaults (could do with hash slice?)
329             my %trim = map { $_ => $defaults{$_} } keys %allow;
330              
331             return %trim;
332             }
333              
334             =item B<_get_supported_init>
335              
336              
337              
338             =cut
339              
340             sub _get_supported_init {
341             croak "xxx - get supported init";
342             }
343              
344             =back
345              
346             =head2 Class methods
347              
348             These methods are not associated with any particular object.
349              
350             =over 4
351              
352             =item B
353              
354             Location of the skycat config file. Default location is
355             C<$SKYCAT_CFG>, if defined, else C<$HOME/.skycat/skycat.cfg>,
356             or C<$PERLPREFIX/etc/skycat.cfg> if there isn't a version
357             in the users home directory
358              
359             This could be made per-class if there is a demand for running
360             queries with different catalogs. This would also move the config
361             contents into the query object itself.
362              
363             =cut
364              
365             sub _set_cfg_file {
366             my $cfg_file;
367              
368             if( exists $ENV{SKYCAT_CFG} ) {
369             $cfg_file = $ENV{SKYCAT_CFG};
370             } elsif ( -f File::Spec->catfile( $ENV{HOME}, ".skycat", "skycat.cfg") ){
371             $cfg_file = File::Spec->catfile( $ENV{HOME}, ".skycat", "skycat.cfg");
372             } else {
373             # generate the default path to the $PERLPRFIX/etc/skycat.cfg file,
374             # this is a horrible hack, there is probably an elegant way to do
375             # this but I can't be bothered looking it up right now.
376             my $perlbin = $^X;
377             my ($volume, $dir, $file) = File::Spec->splitpath( $perlbin );
378             my @dirs = File::Spec->splitdir( $dir );
379             my @path;
380             foreach my $i ( 0 .. $#dirs-2 ) {
381             push @path, $dirs[$i];
382             }
383             my $directory = File::Spec->catdir( @path, 'etc' );
384              
385             # reset to the default
386             $cfg_file = File::Spec->catfile( $directory, "skycat.cfg" );
387              
388             # debugging and testing purposes
389             unless ( -f $cfg_file ) {
390             # use blib version!
391             $cfg_file = File::Spec->catfile( '.', 'etc', 'skycat.cfg' );
392             }
393             }
394             return $cfg_file;
395             }
396              
397             sub cfg_file {
398             my $class = shift;
399             my $cfg_file;
400             if (@_) {
401             $cfg_file = shift;
402             if( ( defined( $CFG_FILE ) &&
403             $cfg_file ne $CFG_FILE ) ||
404             ! defined( $CFG_FILE ) ) {
405              
406             # We were given a new config file, so load it.
407             $class->_load_config($cfg_file);
408             $CFG_FILE = $cfg_file;
409             }
410             }
411             if( ! defined( $CFG_FILE ) ) {
412             $CFG_FILE = _set_cfg_file;
413             }
414             return $CFG_FILE;
415             }
416              
417             =back
418              
419             =begin __PRIVATE_METHODS__
420              
421             =head2 Internal methods
422              
423             =over 4
424              
425             =item B<_load_config>
426              
427             Method to load the skycat config information into
428             the class and configure the modules.
429              
430             $q->_load_config() or die "Error loading config";
431              
432             The config file name is obtained from the C method.
433             Returns true if the file was read successfully and contained at
434             least one catalog server. Otherwise returns false.
435              
436             Requires an object to attach itself to (mainly for the useragent
437             remote directory follow up). The results of this load are
438             visible to all instances of this class.
439              
440             Usually called automatically from the constructor if a config
441             has not previously been read.
442              
443             =cut
444              
445             sub _load_config {
446             my $self = shift;
447             my $cfg = shift;
448              
449             print "SkyCat.pm: \$cfg = $cfg\n" if $DEBUG;
450              
451             if (!defined $cfg) {
452             $cfg = _set_cfg_file;
453             $self->cfg_file( $cfg );
454             }
455              
456             unless (-e $cfg) {
457             my $xcfg = (defined $cfg ? $cfg : "" );
458             return;
459             }
460              
461             my $fh;
462             unless (open $fh, "<$cfg") {
463             warnings::warnif( "Specified config file, $cfg, could not be opened: $!");
464             return;
465             }
466              
467             # Need to read the contents into an array
468             my @lines = <$fh>;
469              
470             # Process the config file and extract the raw content
471             my @configs = $self->_extract_raw_info( \@lines );
472              
473             print "Pre-filtering has \@configs " . @configs . " entries\n" if $DEBUG;
474              
475             # Close file
476             close( $fh ) or do {
477             warnings::warnif("Error closing config file, $cfg: $!");
478             return;
479             };
480              
481             # Get the token mapping for validation
482             my %map = $self->_token_mapping;
483              
484             # Currently we are only interested in catalog, namesvr and archive
485             # so throw everything else away
486             @configs = grep { $_->{serv_type} =~ /(namesvr|catalog|archive)/ } @configs;
487              
488             print "Post-filtering has \@configs " . @configs . " entries\n" if $DEBUG;
489              
490             # Process each entry. Mainly URL processing
491             for my $entry ( @configs ) {
492             # Skip if we have already analysed this server
493             if (exists $CONFIG{lc($entry->{short_name})}) {
494             print "Already know about " . $entry->{short_name} . "\n"
495             if $DEBUG;
496             next;
497             }
498              
499             print " Processing " . $entry->{short_name} . "\n" if $DEBUG;
500             print Dumper( $entry ) if( $DEBUG );
501              
502             # Extract info from the 'url'. We need to extract the following info:
503             # - Host name and port
504             # - remaining url path
505             # - all the CGI options including the static options
506             # Note that at the moment we do not do token replacement (the
507             # rest of the REST architecture expects to get the above
508             # information separately). This might well prove to be silly
509             # since we can trivially replace the tokens without having to
510             # reconstruct the url. Of course, this does allow us to provide
511             # mandatory keywords. $url =~ s/\%ra/$ra/;
512             if ($entry->{url} =~ m|^http:// # Standard http:// prefix
513             ([\w\.\-]+ # remote host
514             (?::\d+)?) # Optional port number
515             / # path separator
516             ([\w\/\-\.]+\?) # remaining URL path and ?
517             (.*) # CGI options without trailing space
518             |x) {
519             $entry->{remote_host} = $1;
520             $entry->{url_path} = $2;
521             my $options = $3;
522              
523             # if first character is & we append that to url_path since it
524             # is an empty argument
525             $entry->{url_path} .= "&" if $options =~ s/^\&//;
526              
527             # In general the options from skycat files are a real pain
528             # Most of them have nice blah=%blah format but there are some cases
529             # that do ?%ra%dec or coords=%ra %dec that just cause more trouble
530             # than they are worth given the standard URL constructor that we
531             # are attempting to inherit from REST
532             # Best idea is not to fight against it. Extract the host, path
533             # and options separately but simply use token replacement when it
534             # comes time to build the URL. This will require that the url
535             # is moved into its own method in REST.pm for subclassing.
536             # We still need to extract the tokens themselves so that we
537             # can generate an allowed options list.
538              
539             # tokens have the form %xxx but we have to make sure we allow
540             # %mime-type. Use the /g modifier to get all the matches
541             my @tokens = ( $options =~ /(\%[\w\-]+)/g);
542              
543             # there should always be tokens. No obvious way to reomve the anomaly
544             warnings::warnif( "No tokens found in $options!!!" )
545             unless @tokens;
546              
547             # Just need to make sure that these are acceptable tokens
548             # Get the lookup table and store that as the allowed options
549             my %allow;
550             for my $tok (@tokens) {
551             # only one token. See if we recognize it
552             my $strip = $tok;
553             $strip =~ s/%//;
554              
555             if (exists $map{$strip}) {
556             if (!defined $map{$strip}) {
557             warnings::warnif("Do not know how to process token $tok" );
558             } else {
559             $allow{ $map{$strip} } = $strip;
560             }
561             } else {
562              
563             warnings::warnif("Token $tok not currently recognized")
564             unless exists $map{$strip};
565             }
566              
567             }
568              
569             # Store them
570             $entry->{tokens} = \@tokens;
571             $entry->{allow} = \%allow;
572              
573             print Dumper( $entry ) if( $DEBUG );
574              
575             # And store this in the config. Only store it if we have
576             # tokens
577             $CONFIG{lc($entry->{short_name})} = $entry;
578              
579             } # if entry
580              
581             } # for loop
582              
583             # Debug
584             print Dumper(\%CONFIG) if $DEBUG;
585              
586             return;
587             }
588              
589             =item B<_extract_raw_info>
590              
591             Go through a skycat.cfg file and extract the raw unprocessed entries
592             into an array of hashes. The actual content of the file is passed
593             in as a reference to an array of lines.
594              
595             @entries = $q->_extract_raw_info( \@lines );
596              
597             This routine is separate from the main load routine to allow recursive
598             calls to remote directory entries.
599              
600             =cut
601              
602             sub _extract_raw_info {
603             my $self = shift;
604             my $lines = shift;
605              
606             # Now read in the contents
607             my $current; # Current server spec
608             my @configs; # Somewhere temporary to store the entries
609              
610             for my $line (@$lines) {
611              
612              
613             # Skip comment lines and blank lines
614             next if $line =~ /^\s*\#/;
615             next if $line =~ /^\s*$/;
616              
617             if ($line =~ /^(\w+):\s*(.*?)\s*$/) {
618             # This is content
619             my $key = $1;
620             my $value = $2;
621             # Assume that serv_type is always first
622             if ($key eq 'serv_type') {
623             # Store previous config if it contains something
624             # If it actually contains information on a serv_type of
625             # directory we can follow the URL and recursively expand
626             # the content
627             push(@configs, $self->_dir_check( $current ));
628              
629             # Clear the config and store the serv_type
630             $current = { $key => $value };
631              
632             } else {
633             # Just store the key value pair
634             $current->{$key} = $value;
635             }
636              
637             } else {
638             # do not know what this line signifies since it is
639             # not a comment and not a content line
640             warnings::warnif("Unexpected line in config file: $line\n");
641             }
642              
643             }
644              
645             # Last entry will still be in %$current so store it if it contains
646             # something.
647             push(@configs, $self->_dir_check( $current ));
648              
649             # Return the entries
650             return @configs;
651             }
652              
653             =item B<_dir_check>
654              
655             If the supplied hash reference has content, look at the content
656             and decide whether you simply want to keep that content or
657             follow up directory specifications by doing a remote URL call
658             and expanding that directory specification to many more remote
659             catalogue server configs.
660              
661             @configs = $q->_dir_check( \%current );
662              
663             Returns the supplied argument, additional configs derived from
664             that argument or nothing at all.
665              
666             Do not follow a 'directory' link if we have already followed a link with
667             the same short name. This prevents infinite recursion when the catalog
668             pointed to by 'catalogs@eso' itself contains a reference to 'catalogs@eso'.
669              
670             =cut
671              
672             my %followed_dirs;
673             sub _dir_check {
674             my $self = shift;
675             my $current = shift;
676              
677             if (defined $current && %$current) {
678             if ($current->{serv_type} eq 'directory') {
679             # Get the content of the URL unless we are not
680             # reading directories
681             if ($FOLLOW_DIRS && defined $current->{url} &&
682             !exists $followed_dirs{$current->{short_name}}) {
683             print "Following directory link to ". $current->{short_name}.
684             "[".$current->{url}."]\n"
685             if $DEBUG;
686              
687             # Indicate that we have followed this link
688             $followed_dirs{$current->{short_name}} = $current->{url};
689              
690             # Retrieve the url, pass that array to the raw parser and then
691             # return any new configs to our caller
692             # Must force scalar context to get array ref
693             # back rather than a simple list.
694             return $self->_extract_raw_info(scalar $self->_get_directory_url( $current->{url} ));
695             }
696             } else {
697             # Not a 'directory' so this is a simple config entry. Simply return it.
698             return ($current);
699             }
700             }
701              
702             # return empty list since we have no value
703             return ();
704             }
705              
706              
707             =item B<_get_directory_url>
708              
709             Returns the content of the remote directory URL supplied as
710             argument. In scalar context returns reference to array of lines. In
711             list context returns the lines in a list.
712              
713             \@lines = $q->_get_directory_url( $url );
714             @lines = $q->_get_directory__url( $url );
715              
716             If we have an error retrieving the file, just return an empty
717             array (ie skip it).
718              
719             =cut
720              
721             sub _get_directory_url {
722             my $self = shift;
723             my $url = shift;
724              
725             # Call the base class to get the actual content
726             my $content = '';
727             eval {
728             $content = $self->_fetch_url( $url );
729             };
730              
731             # Need an array
732             my @lines;
733             @lines = split("\n", $content) if defined $content;
734              
735             if (wantarray) {
736             return @lines;
737             } else {
738             return \@lines;
739             }
740             }
741              
742             =item B<_token_mapping>
743              
744             Provide a mapping of tokens found in SkyCat config files to the
745             internal values used generically by Astro::Catalog::Query classes.
746              
747             %map = $class->_token_mappings;
748              
749             Keys are skycat tokens.
750              
751             =cut
752              
753             sub _token_mapping {
754             return (
755             id => 'id',
756              
757             ra => 'ra',
758             dec => 'dec',
759              
760             # Arcminutes
761             r1 => 'radmin',
762             r2 => 'radmax',
763             w => 'width',
764             h => 'height',
765              
766             n => 'nout',
767              
768             # which filter???
769             m1 => 'magfaint',
770             m2 => 'magbright',
771              
772             # Is this a conditional?
773             cond => 'cond',
774              
775             # Not Yet Supported
776             cols => undef,
777             'mime-type' => undef,
778             ws => undef,
779             );
780             }
781              
782             =back
783              
784             =head2 Translations
785              
786             SkyCat specific translations from the internal format to URL format
787             go here.
788              
789             RA/Dec must match format described in
790             http://vizier.u-strasbg.fr/doc/asu.html
791             (at least for GSC) ie hh:mm:ss.s+/-dd:mm:ss
792             or decimal degrees.
793              
794             =over 4
795              
796             =cut
797              
798             sub _from_dec {
799             my $self = shift;
800             my $dec = $self->query_options("dec");
801             my %allow = $self->_get_allowed_options();
802              
803             # Need colons
804             $dec =~ s/\s+/:/g;
805              
806             # Need a + preprended
807             $dec = "+" . $dec if $dec !~ /^[\+\-]/;
808              
809             return ($allow{dec},$dec);
810             }
811              
812             sub _from_ra {
813             my $self = shift;
814             my $ra = $self->query_options("ra");
815             my %allow = $self->_get_allowed_options();
816              
817             # need colons
818             $ra =~ s/\s+/:/g;
819              
820             return ($allow{ra},$ra);
821             }
822              
823             =item B<_translate_one_to_one>
824              
825             Return a list of internal options (as defined in C<_get_allowed_options>)
826             that are known to support a one-to-one mapping of the internal value
827             to the external value.
828              
829             %one = $q->_translate_one_to_one();
830              
831             Returns a hash with keys and no values (this makes it easy to
832             check for the option).
833              
834             This method also returns, the values from the parent class.
835              
836             =cut
837              
838             sub _translate_one_to_one {
839             my $self = shift;
840             # convert to a hash-list
841             return ($self->SUPER::_translate_one_to_one,
842             map { $_, undef }(qw/
843             cond
844             /)
845             );
846             }
847              
848             =back
849              
850             =end __PRIVATE_METHODS__
851              
852             =head1 NOTES
853              
854             'directory' entries are not followed by default although the class
855             can be configured to do so by setting
856              
857             $Astro::Catalog::Query::SkyCat::FOLLOW_DIRS = 1;
858              
859             to true.
860              
861             This class could simply read the catalog config file and allow queries
862             on explicit servers directly rather than going to the trouble of
863             auto-generating a class per server. This has the advantage of allowing
864             a user to request USNO data from different servers rather than generating
865             a single USNO class. ie
866              
867             my $q = new Astro::Catalog::Query::SkyCat( catalog => 'usnoa@eso',
868             target => 'HL Tau',
869             radius => 5 );
870              
871             as opposed to
872              
873             my $q = new Astro::Catalog::Query::USNOA( target => 'HL Tau',
874             radius => 5 );
875              
876             What to do with catalogue mirrors is an open question. Of course,
877             convenience wrapper classes could be made available that simply delegate
878             the calls to the SkyCat class.
879              
880             =head1 SEE ALSO
881              
882             SkyCat FTP server. [URL goes here]
883              
884             SSN75 [http://www.starlink.rl.ac.uk/star/docs/ssn75.htx//ssn75.html]
885             by Clive Davenhall.
886              
887             =head1 BUGS
888              
889             At the very least for testing, an up-to-date skycat.cfg file
890             should be distributed with this module. Whether it should be
891             used by this module once installed is an open question (since
892             many people will not have a version in the standard location).
893              
894             =head1 COPYRIGHT
895              
896             Copyright (C) 2001-2003 University of Exeter and Particle Physics and
897             Astronomy Research Council. All Rights Reserved.
898              
899             This program was written as part of the eSTAR project and is free
900             software; you can redistribute it and/or modify it under the terms of
901             the GNU Public License.
902              
903             =head1 AUTHORS
904              
905             Tim Jenness Etjenness@cpan.orgE,
906             Alasdair Allan Eaa@astro.ex.ac.ukE
907              
908             =cut
909              
910             1;