File Coverage

blib/lib/Astro/Catalog/IO/TST.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Astro::Catalog::IO::TST;
2              
3             =head1 NAME
4              
5             Astro::Catalog::IO::TST - Standard Tab Separated Table format I/O
6              
7             =head1 SYNOPSIS
8              
9             $cat = Astro::Catalog::IO::TST->_read_catalog( \@lines );
10             \@lines = Astro::Catalog::IO::TST->_write_catalog( $cat );
11              
12             =head1 DESCRIPTION
13              
14             Performs IO for catalogues that use the standard Tab Separated Table
15             (TST) format. The TST format is commonly returned by astronomical catalogue
16             servers that use the Astronomical Catalogue Library (ACL) format, although
17             it is also perfectly reasonable to read and write this format to disk.
18              
19             =cut
20              
21 1     1   5989780 use 5.006;
  1         6  
  1         84  
22 1     1   14 use strict;
  1         2  
  1         92  
23 1     1   65 use warnings;
  1         3  
  1         103  
24 1     1   6 use warnings::register;
  1         2  
  1         405  
25 1     1   7 use vars qw/ $VERSION $DEBUG /;
  1         2  
  1         134  
26 1     1   13 use Carp;
  1         8  
  1         200  
27 1     1   1288 use Data::Dumper;
  1         8266  
  1         115  
28              
29 1     1   945 use Astro::Catalog;
  0            
  0            
30             use Astro::Catalog::Star;
31             use Astro::Coords;
32              
33             use base qw/ Astro::Catalog::IO::ASCII /;
34              
35             $DEBUG = 0;
36             $VERSION = '4.31';
37              
38             =begin __PRIVATE_METHODS__
39              
40             =head1 Private Methods
41              
42             These methods are usually called automatically from the C
43             constructor.
44              
45             =over 4
46              
47             =item B<_read_catalog>
48              
49             Read contents of a TST catalogue (supplied as an array of lines) and
50             return a corresponding C object.
51              
52             $cat = Astro::Catalog::IO::TST->_read_catalog( \@lines, %opts );
53              
54             Generally assumes that the first three columns in the table
55             are ID, RA and Dec.
56              
57             The supplied options can be used to specify non-standard
58             column positions. eg
59              
60             ra_col => 5, dec_col => 6
61              
62             would indicate that "ra" and "dec" are in columns 5 and 6 rather than
63             columns 2 and 3. Note that all options passed in here are treated
64             identically to TST parameters. ie the parameters read from the
65             TST file are merged with the supplied parameters (with the supplied
66             parameters over-writing file content). If "id", "ra" and "dec" are
67             missing, they are assumed to be columns 0, 1 and 2 respectively.
68              
69              
70             =cut
71              
72             sub _read_catalog {
73             my $class = shift;
74             my $lines = shift;
75              
76             my %options = @_;
77              
78             # Are we in the main table yet?
79             my $intable;
80              
81             # Parameters
82             my %params;
83              
84             # Descriptive comments
85             my @descr;
86              
87             # General comments (usually ignored)
88             my @comments;
89              
90             # Column names in order
91             my @columns;
92              
93             # Actual star information
94             my @stars;
95              
96             # Column formats, types and units (optional)
97             my %extras;
98              
99             # Loop over each line
100             my $counter = 0;
101             for (@$lines) {
102              
103             # increment line counter
104             $counter++;
105              
106             # Make sure we have a copy since there is some processing
107             # of the line and we do not want the content of the supplied
108             # catalog to change from under the caller.
109             my $line = $_;
110             chomp($line);
111              
112             # Simply loop if there is no content
113             print "PARSING line $counter\n" if $DEBUG;
114             next unless $line =~ /\S/;
115              
116             # Look for comments
117             if ($line =~ /^\[EOD\]/) {
118             # Usually indicates that we can stop parsing.
119             # At the very least this means end of data.
120             # so reset $intable
121             print " FOUND EOD - no need to continue\n" if $DEBUG;
122             last;
123              
124             } elsif ($line =~ /^\s*\#/) {
125             # probably a comment although CURSA extensions allow
126             # some column information
127             if ($line =~ /^\s*\#column-(.*):/) {
128              
129             # Special key: usually units, types or formats
130             my $key = $1;
131              
132             #print " FOUND CURSA extension: $key\n" if $DEBUG;
133              
134             # Remove the crud
135             $line =~ s/^\s*\#column-$key://;
136              
137             my @content = $class->_parse_line( $line );
138              
139             # and store it for now
140             $extras{$key} = [] unless exists $extras{$key};
141             push(@{ $extras{$key} }, @content);
142              
143             warnings::warnif("CURSA-style parameters encountered whilst inside table!")
144             if $intable;
145              
146             } else {
147             # Standard comment - strip the hash
148             $line =~ s/^\s*\#//;
149             #print " FOUND standard comment\n" if $DEBUG;
150             push(@comments, $line);
151             }
152             } elsif ($line =~ /\t/) {
153             # Parse the line in standard manner
154             print " FOUND standard line\n" if $DEBUG;
155             my @content = $class->_parse_line( $line );
156             print Dumper(@content) if $DEBUG;
157              
158             # If the line includes tab characters it is probably a table
159             # entry. Either the header or the content or the separator
160             if ($intable) {
161             # Must be reading real content
162             warnings::warnif("Column mismatch: name count different to actual content!:\n$line\n") if @columns != @content;
163             print " FOUND content line\n" if $DEBUG;
164              
165             # Store the content in a hash indexed by the associated columns
166             # This will be a problem for degenerate headings!
167             my %star = map { lc($columns[$_]), $content[$_] } (0..$#content);
168              
169             push(@stars, \%star);
170             } elsif ( !@columns ) {
171             # We have read no column information so this must be
172             # the table description
173             print " FOUND table description line\n" if $DEBUG;
174             @columns = @content;
175              
176             #} elsif ($line =~ /^[-\t]+$/) { # This doesn't seem to parse the
177             # SuperCOSMOS TST separator, not
178             # sure what's going on here.
179              
180             } elsif ( $content[0] =~ /^[-]+$/ && $content[1] =~ /^[-]+$/ ) {
181             # this is probably safe enough, but its another un-Godly hack,
182             # sorry Tim, have a look at the SuperCOSMOS.pm module and turn
183             # on debugging in TST to see whats going on here during parsing.
184             #print " FOUND table separator line\n" if $DEBUG;
185             warnings::warnif("Table separator has already been encountered!")
186             if $intable;
187              
188             # separator, so next time around is a real table
189             $intable = 1;
190             } else {
191             # Should not get here
192             croak "Fatal parse error reading TST table, line '$line'";
193             }
194              
195             } elsif ($line =~ /^\w+:/) {
196             # A parameter is some characters ending in a colon
197             my ($key, $value) = $line =~ /^(\w+):\s*(.*?)\s*$/;
198              
199             if (defined $key && defined $value) {
200             $params{$key} = $value;
201             } else {
202             warnings::warnif("Error extracting parameter from line '$line'");
203             }
204              
205             warnings::warnif("Parameter specified after table has been started. This is non-standard!")
206             if $intable;
207              
208             } else {
209             # This is probably general description
210             push(@descr, $line);
211             }
212              
213             # loop around
214             }
215              
216             # First merge the supplied parameters into those read from the file
217             # itself. The supplied values override values read from the file
218             %params = (%params, %options);
219              
220             print Dumper( \@descr, \@comments, \@columns, \%params, \%extras, \@stars)
221             if $DEBUG;
222              
223             # Now we need to go through the parameters to see whether there are
224             # any _col parameters that we need to map to an "ra", "dec" and "id"
225             # field
226             for my $key (keys %params) {
227             next unless $key =~ /^(\w+)_col$/;
228             my $col = lc($1);
229              
230             # find the column name (noting that the column numbers start
231             # counting at 0)
232             my $colnum = $params{$key};
233              
234             # This is the translated name [either the name supplied
235             # directly or a column number
236             my $oldname;
237              
238             # it is possible that this number is actually a column name
239             if ($colnum =~ /[A-Za-z]/) {
240             # has a word character
241             $oldname = lc($colnum);
242             } else {
243             # Need to map column number to a name
244              
245             # Negative value indicates that we are not actually specifying
246             # a column
247             if ($colnum == -1) {
248             # should the entry "$col" be deleted from each star hash
249             # if it is present but has been designated -1 by a parameter?
250             next;
251             }
252              
253             # The old column name
254             $oldname = lc( $columns[$colnum]);
255             }
256              
257             # Insert new column into hash
258             # overwriting existing content if required without warning
259             # Assuming $star->{$oldname} actually exists
260             for my $star (@stars) {
261             $star->{$col} = $star->{$oldname} if exists $star->{$oldname};
262             }
263              
264             }
265              
266             # if we do not yet have id, ra or dec assume columns (0,1,2)
267             # Is this the correct thing to do? The spec in SSN/75 is vague
268             # on this since it seems to imply that the first 3 columns may be
269             # ra,dec and id without being called that and that *_col must be
270             # present as parameters if the first 3 columns are not id,ra,dec
271             # content. How do I know if they are id,ra and dec if they are not
272             # called that.
273             if (@stars) {
274             my $colnum = 0;
275             for my $key (qw/ id ra dec /) {
276             # only check first star
277             if (not exists $stars[0]->{$key}) {
278             # did not have it, loop over all
279             warnings::warnif("Guessing column $colnum contains $key");
280             for (@stars) {
281             $_->{$key} = $_->{lc($columns[$colnum])};
282             }
283             }
284             $colnum++;
285             }
286             }
287              
288             # Now convert the information into a star object
289              
290             # This is a back-of-the-envelope data dictionary from looking at
291             # USNO-A2, 2MASS, Bright Star Catalogues and SuperCOSMOS. Maps the
292             # Astro::Catalog::Star methods to different columns names
293             my %datadict = (
294             field => [ qw/ field /, qw/ fldno / ],
295             quality => [ qw/ qual /, qw/ qflg /, qw/ quality / ],
296             distance => [ "d'" ],
297             posangle => [ qw/ pa /, qw/ _r / ],
298             );
299              
300              
301             # precalculate EQUINOX (type for Astro::Coords at the moment
302             # since it can not deal with JXXXX.XX format)
303             my $type = $params{EQUINOX};
304             if (defined $type) {
305             if ($type =~ /(B1950|J2000)(\.0*)?$/) {
306             $type = $1;
307             } else {
308             warnings::warnif("Unsupported equinox '$type'. Defaulting to J2000");
309             $type = "J2000";
310             }
311             } else {
312             # default to J2000
313             $type = "J2000";
314             }
315              
316             for my $star (@stars) {
317              
318             my %construct;
319              
320             # Create some coordinates
321             # decimal degrees or sexagesimal hours/deg
322             if (exists $star->{ra} && exists $star->{dec}) {
323             my $units;
324             if ($star->{ra} =~ /:/) {
325             $units = "sex";
326             } else {
327             # must be decimal degrees
328             $units = "deg";
329             }
330              
331             my $c = new Astro::Coords( ra => $star->{ra},
332             dec => $star->{dec},
333             type => $type,
334             units => $units,
335             name => $star->{id}
336             );
337              
338             if (defined $c) {
339             $construct{coords} = $c;
340             } else {
341             warnings::warnif("Error instantiating coordinate object");
342             }
343              
344             }
345              
346             # DEBUGGING, prints out everything we've parsed from the catalogue
347             #
348             #print "\n\n\n" . Dumper( $star ) . "\n\n\n";
349              
350             # Assume that some field names are standardised. This is
351             # probably rubbish (whoever heard of standards!).
352             # Need to create a data dictionary with all the alternatives
353             # that are in use.
354             # Be very scared if we have to provide mapping routines
355             for my $starkey (keys %datadict) {
356             for my $colname (@{ $datadict{$starkey} }) {
357             if (exists $star->{$colname}) {
358             $construct{$starkey} = $star->{$colname};
359              
360             # stop looking
361             next;
362             }
363             }
364             }
365              
366             # In GSC, posangle has junk on the end. We know it should be
367             # a number
368             $construct{posangle} =~ s/\D+$// if exists $construct{posangle};
369              
370             # gsc flag requires some work
371             if (exists $star->{gsc}) {
372             $construct{gsc} = ( $star->{gsc} eq '+' ? "TRUE" : "FALSE");
373             } elsif ($params{gsc}) {
374             $construct{gsc} = "TRUE";
375             }
376              
377             # Magnitudes <- anything that ends in mag
378             # Assdume filter is in X_mag
379             # If no prefix assume R (yeah right) - we do not know the
380             # source of the catalog at this point so can not even guess
381             $construct{magnitudes} = {};
382             $construct{magerr} = {};
383             for my $key (keys %$star) {
384              
385             print "LOOPING KEY = $key\n" if $DEBUG;
386              
387             # Un-Goldy hack number #5 for the SuperCOSMOS catalogue, for some
388             # bloody stupid reason they've decided to label their magntitudes
389             # B_J, R_1, R_2 and I. God help me, if I ever find the guy responsible
390             # for this stupid idea. For now lets munge these here and cross our
391             # fingers.
392             if ( $key eq "b_j" ) {
393             $$star{bj_mag} = $star->{$key};
394             delete $star->{$key};
395             $key = "bj_mag";
396             }
397             if ( $key eq "r_1" ) {
398             $$star{r1_mag} = $star->{$key};
399             delete $star->{$key};
400             $key = "r1_mag" ;
401             }
402             if ( $key eq "r_2" ) {
403             $$star{r2_mag} = $star->{$key};
404             delete $star->{$key};
405             $key = "r2_mag" ;
406             }
407             if ( $key eq "i" ) {
408             $$star{i_mag} = $star->{$key};
409             delete $star->{$key};
410             $key = "i_mag" ;
411             }
412              
413             # drop through unless we have a magnitude
414             next unless $key =~ /^(.*?)_?mag$/; # non-greedy
415              
416             # No capture - assume R
417             my $filter = ( $1 ? uc($1) : "R" );
418              
419             # if the filter starts with e_ then it is probably an
420             # error in the magnitude
421             if ($filter =~ /^E_(\w)$/i) {
422             # error in magnitude
423             my $err = $1;
424             $construct{magerr}->{$err} = $star->{$key}
425             if $star->{$key} =~ /\d/;
426             print "Found Mag Error $err ... \n" if $DEBUG;
427             } elsif ($filter =~ /_/) {
428             # is this a color?
429             warnings::warnif "Found unrecognised filter string: $filter\n";
430             } else {
431             # Assume it is a filter
432             $construct{magnitudes}->{$filter} = $star->{$key};
433             print "Found filter $filter ...\n" if $DEBUG;
434             }
435             }
436              
437             my ( @fluxes, @colors );
438             foreach my $fkey ( keys %{$construct{magnitudes}} ) {
439             my $num;
440             if ( defined $construct{magerr}->{$fkey} ) {
441             $num = new Number::Uncertainty( Value => $construct{magnitudes}->{$fkey},
442             Error => $construct{magerr}->{$fkey} );
443             } else {
444             $num = new Number::Uncertainty( Value => $construct{magnitudes}->{$fkey} );
445             }
446             my $mag = new Astro::Flux( $num, 'mag', "$fkey" );
447             push @fluxes, $mag;
448             }
449             delete $construct{magnitudes};
450             delete $construct{magerr} if defined $construct{magerr};
451              
452             # Colors: Look for B-V
453             $construct{colours} = {};
454             for my $key (keys %$star) {
455              
456             next unless $key =~ /^(\w)-(\w)$/; # non-greedy
457             $construct{colours}->{uc($key)} = $star->{$key};
458             print "Found colour ".uc($key)." ... \n" if $DEBUG;
459             }
460             foreach my $ckey ( keys %{$construct{colours}} ) {
461             my @filters = split "-", $ckey;
462             my $color = new Astro::FluxColor( upper => new Astro::WaveBand( Filter => $filters[0] ),
463             lower => new Astro::WaveBand( Filter => $filters[1] ),
464             quantity => new Number::Uncertainty( Value => $construct{colours}->{$ckey} ) );
465             push @colors, $color;
466             }
467             delete $construct{colours};
468              
469             # build the fluxes object from the available data
470             if ( defined $fluxes[0] && defined $colors[0] ) {
471             $construct{fluxes} = new Astro::Fluxes( @fluxes, @colors );
472             } elsif ( defined $colors[0] ) {
473             $construct{fluxes} = new Astro::Fluxes( @colors );
474             } elsif ( defined $fluxes[0] ) {
475             $construct{fluxes} = new Astro::Fluxes( @fluxes );
476             } else {
477             delete $construct{fluxes} if defined $construct{fluxes};
478             }
479              
480             print Dumper( %construct ) . "\n" if $DEBUG;
481              
482             # Modify the array in place
483             $star = new Astro::Catalog::Star( id => $star->{id}, %construct );
484             }
485              
486             return new Astro::Catalog( Stars => \@stars);
487             }
488              
489             =item B<_write_catalog>
490              
491             Create an output catalogue in the TST format and return the lines
492             in an array.
493              
494             $ref = Astro::Catalog::IO::TST->_write_catalog( $catalog );
495              
496             Argument is an C object.
497              
498             =cut
499              
500             sub _write_catalog {
501             croak ( 'Usage: _write_catalog( $catalog, [%opts] ') unless scalar(@_) >= 1;
502             my $class = shift;
503             my $catalog = shift;
504              
505             my @output;
506              
507             # First, the header. We're only going to write the ID, RA, and Dec.
508             push @output, "Id\tra\tdec";
509             push @output, "--\t--\t---";
510              
511             # Now loop through the stars and push their respective IDs, RAs, and
512             # Decs onto the output array.
513             foreach my $star ( $catalog->stars ) {
514             my $output_string = "";
515              
516             $output_string .= $star->id;
517             $output_string .= "\t";
518             $output_string .= $star->coords->ra->string;
519             $output_string .= "\t";
520             $output_string .= $star->coords->dec->string;
521              
522             push @output, $output_string;
523              
524             }
525              
526             # And return!
527             return \@output;
528             }
529              
530             =item B<_parse_line>
531              
532             Internal routine for doing the tab delimited parsing.
533             Returns back the columns.
534              
535             @content = $class->_parse_line( $line );
536              
537             This routine is trivial but it seemed sensible to put it in a function
538             since the parse is done in more than one place in _read_catalog.
539             Whitespace around the column separators is stripped.
540              
541             =cut
542              
543             sub _parse_line {
544             my $class = shift;
545             my $line = shift;
546              
547             # Just so we do things correctly, add a ' ' to the
548             # end of a string if it ends in a tab. Otherwise for blank
549             # last column we end up being a column short
550             $line .= " " if $line =~ /\t$/;
551              
552             # Do the split on tab and then clean up each string
553             # Safer since \s include \t
554             my @cols = split(/\t/,$line);
555              
556             for (@cols) {
557             s/^\s*//;
558             s/\s*$//;
559             }
560              
561             return @cols;
562             }
563              
564              
565             =back
566              
567             =end __PRIVATE_METHODS__
568              
569             =head1 REVISION
570              
571             $Id: TST.pm,v 1.14 2006/03/16 00:15:13 cavanagh Exp $
572              
573             =head1 FORMAT
574              
575             The TST format is specified in a number of documents. For example
576             SSN/75 [http://www.starlink.rl.ac.uk/star/docs/ssn75.htx//ssn75.html]
577             by Clive Davenhall.
578              
579             =head1 SEE ALSO
580              
581             L, L.
582              
583             =head1 COPYRIGHT
584              
585             Copyright (C) 2003-2004 Particle Physics and Astronomy Research Council.
586             All Rights Reserved.
587              
588             This module is free software; you can redistribute it and/or modify it
589             under the terms of the GNU Public License.
590              
591             =head1 AUTHORS
592              
593             Alasdair Allan Eaa@astro.ex.ac.ukE
594             Tim Jenness Etjenness@cpan.orgE
595              
596             =cut
597              
598             1;
599