File Coverage

blib/lib/Astro/Catalog/IO/JCMT.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Astro::Catalog::IO::JCMT;
2              
3             =head1 NAME
4              
5             Astro::Catalog::IO::JCMT - JCMT catalogue I/O for Astro::Catalog
6              
7             =head1 SYNOPSIS
8              
9             $cat = Astro::Catalog::IO::JCMT->_read_catalog( \@lines );
10             $arrref = Astro::Catalog::IO::JCMT->_write_catalog( $cat, %options );
11             $filename = Astro::Catalog::IO::JCMT->_default_file();
12              
13             =head1 DESCRIPTION
14              
15             This class provides read and write methods for catalogues in the JCMT
16             pointing catalogue format. The methods are not public and should, in general,
17             only be called from the C C and C
18             methods.
19              
20             =cut
21              
22 1     1   6262771 use 5.006;
  1         10  
  1         61  
23 1     1   7 use warnings;
  1         1  
  1         104  
24 1     1   62 use warnings::register;
  1         2  
  1         478  
25 1     1   8 use Carp;
  1         2  
  1         260  
26 1     1   5 use strict;
  1         9  
  1         57  
27              
28 1     1   1113 use Astro::Telescope;
  1         42184  
  1         42  
29 1     1   720 use Astro::Coords;
  0            
  0            
30             use Astro::Catalog;
31             use Astro::Catalog::Star;
32              
33             use base qw/ Astro::Catalog::IO::ASCII /;
34              
35             use vars qw/$VERSION $DEBUG /;
36              
37             $VERSION = '4.31';
38             $DEBUG = 0;
39              
40             # Name must be limited to 15 characters on write
41             use constant MAX_SRC_LENGTH => 15;
42              
43             # Default location for a JCMT catalog
44             my $defaultCatalog = "/local/progs/etc/poi.dat";
45              
46             # Planets appended to the catalogue
47             my @PLANETS = qw/ mercury mars uranus saturn jupiter venus neptune /;
48              
49             =over 4
50              
51             =item B
52              
53             Method to take a general target name and clean it up
54             so that it is suitable for writing in a JCMT source catalog.
55             This routine is used by the catalog writing code but can also
56             be used publically in order to make sure that a target name
57             to be written to the catalogue is guaranteed to match that used
58             in another location (e.g. when writing an a document to accompany
59             the catalogue which refers to targets within it).
60              
61             The source name can be truncated.
62              
63             $cleaned = Astro::Catalog::IO::JCMT->clean_target_name( $dirty );
64              
65             Will return undef if the argument is not defined.
66              
67             Punctuation such as "," and ";" are replaced with underscores.
68             ".", "()" and "+-" are allowed.
69              
70             =cut
71              
72             sub clean_target_name {
73             my $class = shift;
74             my $dirty = shift;
75             return unless defined $dirty;
76              
77             # Remove spaces [compress]
78             $dirty =~ s/\s+//g;
79              
80             # Remove disallowed characters
81             # and replace with dashes
82             $dirty =~ s/[,;:'"`]/-/g;
83              
84             # Truncate it to the allowed length
85             # Name must be limited to MAX_SRC_LENGTH characters
86             $dirty = substr($dirty,0,MAX_SRC_LENGTH);
87              
88             # Return the cleaned name
89             return $dirty;
90             }
91              
92              
93             =item B<_default_file>
94              
95             Returns the location of the default JCMT pointing catalogue at the
96             JCMT itself. This is purely for convenience of the caller when they
97             are at the JCMT and wish to use the default catalogue without having
98             to know explicitly where it is.
99              
100             $filename = Astro::Catalog::IO::JCMT->_default_file();
101              
102             Returns empty list/undef if the file is not available.
103              
104             If the environment variable ASTRO_CATALOG_JCMT is defined (and exists)
105             this will be used as the default.
106              
107             =cut
108              
109             sub _default_file {
110             my $class = shift;
111             return $ENV{ASTRO_CATALOG_JCMT}
112             if (exists $ENV{ASTRO_CATALOG_JCMT} && -e $ENV{ASTRO_CATALOG_JCMT});
113             return (-e $defaultCatalog ? $defaultCatalog : () );
114             }
115              
116             =item B<_read_catalog>
117              
118             Parses the catalogue lines and returns a new C
119             object containing the catalog entries.
120              
121             $cat = Astro::Catalog::IO::JCMT->_read_catalog( \@lines, %options );
122              
123             Supported options (with defaults) are:
124              
125             telescope => Name of telescope to associate with each coordinate entry
126             (defaults to JCMT). If the telescope option is specified
127             but is undef or empty string, no telescope is used.
128              
129             incplanets => Append planets to catalogue entries (default is true)
130              
131              
132             =cut
133              
134             sub _read_catalog {
135             my $class = shift;
136             my $lines = shift;
137              
138             # Default options
139             my %defaults = ( telescope => 'JCMT',
140             incplanets => 1);
141              
142             my %options = (%defaults, @_);
143              
144             croak "Must supply catalogue contents as a reference to an array"
145             unless ref($lines) eq 'ARRAY';
146              
147             # Create a new telescope to associate with this
148             my $tel;
149             $tel = new Astro::Telescope( $options{telescope} )
150             if $options{telescope};
151              
152             # Go through each line and parse it
153             # and store in the array if we had a successful read
154             my @stars = map { $class->_parse_line( $_, $tel); } @$lines;
155              
156             # Add planets if required
157             if ($options{incplanets}) {
158             # create coordinate objects for the planets
159             my @planets = map { new Astro::Coords(planet => $_) } @PLANETS;
160              
161             # And associate a telescope
162             if ($tel) {
163             for (@planets) {
164             $_->telescope($tel);
165             }
166             }
167              
168             # And create the star objects
169             push(@stars, map { new Astro::Catalog::Star(
170             field => 'JCMT',
171             id => $_->name,
172             coords => $_,
173             comment => 'Added automatically',
174             ) } @planets);
175              
176             }
177              
178             # Create the catalog object
179             return new Astro::Catalog( Stars => \@stars,
180             Origin => 'JCMT',
181             );
182              
183             }
184              
185             =item B<_write_catalog>
186              
187             Write the catalog to an array and return it. Returning a reference to
188             an array provides more flexibility to the caller.
189              
190             $ref = Astro::Catalog::IO::JCMT->_write_catalog( $cat );
191              
192             Spaces are removed from source names. The contents of the catalog
193             are sanity checked.
194              
195             =cut
196              
197             sub _write_catalog {
198             my $class = shift;
199             my $cat = shift;
200              
201             # Would make more sense to use the array ref here
202             my @sources = $cat->stars;
203              
204             # Counter for unknown targets
205             my $unk = 1;
206              
207             # Hash for storing target information
208             # so that we can search for duplicates
209             my %targets;
210              
211             # Create hash of all unique target names present
212             # after cleaning. We need this so that we can make sure
213             # a generated name derived from a duplication (with target mismatch)
214             # does not generate a name that already existed explicitly.
215             my %allnames = map { $class->clean_target_name($_->coords->name), undef }
216             @sources;
217              
218             # Loop over each source and extract catalog information
219             # Make sure that we remove unique entries
220             # BUT THAT WE RETAIN THE ORDER OF THE SOURCES IN THE CATALOG
221             # Hence an array for the information
222             my @processed;
223             for my $star (@sources) {
224              
225             # Extract the coordinate object
226             my $src = $star->coords;
227              
228             # Get the name but do not deal with undef yet
229             # in case the type is not valid
230             my $name = $src->name;
231              
232             # Somewhere to store the extracted information
233             my %srcdata;
234              
235             # Store the name (stripped of spaces) and
236             # treat srcdata{name} as the primary name from here on
237             $srcdata{name} = $class->clean_target_name( $name );
238              
239             # Store a comment
240             $srcdata{comment} = $star->comment;
241              
242             # prepopulate the default velocity settings
243             $srcdata{rv} = 'n/a';
244             $srcdata{vdefn} = 'RADIO';
245             $srcdata{vframe} = 'LSR';
246              
247             # Get the miscellaneous data.
248             my $misc = $star->misc;
249             if( defined( $misc ) ) {
250             $srcdata{vrange} = ( defined( $misc->{'velocity_range'} ) ?
251             sprintf( "%s", $misc->{'velocity_range'} ) :
252             "n/a" );
253             $srcdata{flux850} = ( defined( $misc->{'flux850'} ) ?
254             sprintf( "%s", $misc->{'flux850'} ) :
255             "n/a" );
256             } else {
257             $srcdata{vrange} = "n/a";
258             $srcdata{flux850} = "n/a";
259             }
260              
261             # Get the type of source
262             my $type = $src->type;
263             if ($type eq 'RADEC') {
264             $srcdata{system} = "RJ";
265              
266             # Need to get the space separated RA/Dec and the sign
267             $srcdata{long} = $src->ra(format => 'array');
268             $srcdata{lat} = $src->dec(format => 'array');
269              
270             # Get the velocity information
271             my $rv = $src->rv;
272             if ($rv) {
273             $srcdata{rv} = $rv;
274             $srcdata{vdefn} = $src->vdefn;
275             $srcdata{vframe} = $src->vframe;
276              
277             # JCMT compatibility
278             $srcdata{vframe} = "LSR" if $srcdata{vframe} eq 'LSRK';
279              
280             }
281              
282             } elsif ($type eq 'PLANET') {
283             # Planets are not supported in catalog form. Skip them
284             next;
285              
286             } elsif ($type eq 'FIXED') {
287             $srcdata{system} = "AZ";
288              
289             $srcdata{long} = $src->az(format => 'array');
290             $srcdata{lat} = $src->el(format => 'array');
291              
292             # Need to remove + sign from long/AZ since we are not expecting
293             # it in RA/DEC. This is probably a bug in Astro::Coords
294             shift(@{ $srcdata{long} } ) if $srcdata{long}->[0] eq '+';
295              
296             } else {
297             my $errname = ( defined $srcdata{name} ? $srcdata{name} : "");
298             warnings::warnif "Coordinate of type $type for target $errname not supported in JCMT catalog files\n";
299             next;
300             }
301              
302             # Generate a name if not defined
303             if (!defined $srcdata{name}) {
304             $srcdata{name} = "UNKNOWN$unk";
305             $unk++;
306             }
307              
308             # See if we already have this source and that it is really the
309             # same source Note that we do not see whether this name is the
310             # same as one of the derived names. Eg if CRL618 is in the
311             # pointing catalogue 3 times with identical coords and we add a
312             # new CRL618 with different coords then we trigger 3 warning
313             # messages rather than 1 because we do not check that CRL618_2 is
314             # the same as CRL618_1
315              
316             # Note that velocity specification is included in this comparison
317              
318             if (exists $targets{$srcdata{name}}) {
319             my $previous = $targets{$srcdata{name}};
320              
321             # Create stringified form of previous coordinate with same name
322             # and current coordinate
323             my $prevcoords = join(" ",@{$previous->{long}},@{$previous->{lat}},
324             $previous->{rv}, $previous->{vdefn}, $previous->{vframe});
325             my $curcoords = join(" ",@{$srcdata{long}},@{$srcdata{lat}},
326             $srcdata{rv}, $srcdata{vdefn}, $srcdata{vframe});
327              
328             if ($prevcoords eq $curcoords) {
329             # This is the same target so we can ignore it
330             } else {
331             # Make up a new name. Use the unknown counter for this since
332             # we probably have not used it before. Probably not the best
333             # approach and might have problems in edge cases but good
334             # enough for now
335             my $oldname = $srcdata{name};
336              
337             # loop for 100 times
338             my $count;
339             while (1) {
340             # protection loop
341             $count++;
342              
343             # Try to construct a new name based on a global counter
344             # rather than a counter that starts at 1 for each root
345             my $suffix = "_$unk";
346              
347             # increment $unk for next try
348             $unk++;
349              
350             # Abort if we have gone round too many times
351             # Making sure that $unk is incremented first
352             if ($count > 100) {
353             $srcdata{name} = substr($oldname,0,int(MAX_SRC_LENGTH/2)) .
354             int(rand(10000)+1000);
355             warn "Uncontrollable looping (or unfeasibly large number of duplicate sources with different coordinates). Panicked and generated random source name of $srcdata{name}.\n";
356             last;
357             }
358              
359             # Assume the old name will do fine
360             my $root = $oldname;
361              
362             # Do not want to truncate the _XX off the end later on
363             if (length($oldname) > MAX_SRC_LENGTH - length($suffix)) {
364             # This may well be confusing but we have no choice. Since
365             # _XX is unique the only time we will get a name clash by
366             # simply chopping the string is if we have a duplicate
367             # that is too long along with a target name that includes
368             # _XX amd matches the truncated source name!
369             $root = substr($oldname, 0, (MAX_SRC_LENGTH-length($suffix)) );
370              
371             }
372              
373             # Form the new name
374             my $newname = $root . $suffix;
375              
376             # check to see if this name is in the existing target list
377             next if exists $allnames{$newname};
378              
379             # Store it in the targets array and exit loop
380             $srcdata{name} = $newname;
381             last;
382             }
383              
384             # different target
385             warn "Found target with the same name [$oldname] but with different coordinates, renaming it to $srcdata{name}!\n";
386              
387             $targets{$srcdata{name}} = \%srcdata;
388              
389             # Store it in the array
390             push(@processed, \%srcdata);
391              
392             }
393              
394             } else {
395             # Store in hash for easy lookup for duplicates
396             $targets{$srcdata{name}} = \%srcdata;
397              
398             # Store it in the array
399             push(@processed, \%srcdata);
400              
401             }
402              
403             }
404              
405              
406             # Output array for new catalog lines
407             my @lines;
408              
409             # Write a header
410             push @lines, "*\n";
411             push @lines, "* Catalog written automatically by class ". __PACKAGE__ ."\n";
412             push @lines, "* on date " . gmtime . "UT\n";
413             push @lines, "* Origin of catalogue: ". $cat->origin ."\n";
414             push @lines, "*\n";
415              
416             # Now need to go through the targets and write them to disk
417             for my $src (@processed) {
418             my $name = $src->{name};
419             my $long = $src->{long};
420             my $lat = $src->{lat};
421             my $system = $src->{system};
422             my $comment = $src->{comment};
423             my $rv = $src->{rv};
424             my $vdefn = $src->{vdefn};
425             my $vframe = $src->{vframe};
426             my $vrange = $src->{vrange};
427             my $flux850 = $src->{flux850};
428              
429             $comment = '' unless defined $comment;
430              
431             # Velocity can not easily be done with a sprintf since it can be either
432             # a string or a 2 column number
433             if (lc($rv) eq 'n/a') {
434             $rv = ' n/a ';
435             } else {
436             my $sign = ( $rv >= 0 ? '+' : '-' );
437             my $val = $rv;
438             $val =~ s/^\s*[+-]\s*//;
439             $val =~ s/\s*$//;
440             $rv = $sign . ' '. sprintf('%6.1f',$val);
441             }
442              
443             # Name must be limited to MAX_SRC_LENGTH characters
444             # [this should be taken care of by clean_target_name but
445             # if we have appended _X....
446             $name = substr($name,0,MAX_SRC_LENGTH);
447              
448             push @lines,
449             sprintf("%-". MAX_SRC_LENGTH.
450             "s %02d %02d %06.3f %1s %02d %02d %04.1f %2s %s %5s %5s %-4s %s %s\n",
451             $name, @$long, @$lat, $system, $rv, $flux850, $vrange, $vframe, $vdefn, $comment);
452              
453             }
454              
455             return \@lines;
456             }
457              
458             =item B<_parse_line>
459              
460             Parse a line from a JCMT format catalogue and return a corresponding
461             C object. Returns empty list if the line can not
462             be parsed or refers to a comment line (so that map can be used in the
463             caller).
464              
465             $star = Astro::Catalog::IO::JCMT->_parse_line( $line, $tel );
466              
467             where C<$line> is the line to be parsed and (optional) C<$tel>
468             is an C object to be associated with the
469             coordinate objects.
470              
471             The line is parsed using a pattern match.
472              
473             =cut
474              
475             sub _parse_line {
476             my $class = shift;
477             my $line = shift;
478             my $tel = shift;
479             chomp $line;
480              
481             # Skip commented and blank lines
482             return if ($line =~ /^\s*[\*\%]/);
483             return if ($line =~ /^\s*$/);
484              
485             # Use a pattern match parser
486             my @match = ( $line =~ m/^(.*?) # Target name (non greedy)
487             \s* # optional trailing space
488             (\d{1,2}) # 1 or 2 digits [RA:h] [greedy]
489             \s+ # separator
490             (\d{1,2}) # 1 or 2 digits [RA:m]
491             \s+ # separator
492             (\d{1,2}(?:\.\d*)?) # 1|2 digits opt .fraction [RA:s]
493             # no capture on fraction
494             \s+
495             ([+-]?\s*\d{1,2}) # 1|2 digit [dec:d] inc sign
496             \s+
497             (\d{1,2}) # 1|2 digit [dec:m]
498             \s+
499             (\d{1,2}(?:\.\d*)?) # arcsecond (optional fraction)
500             # no capture on fraction
501             \s+
502             (RJ|RB|GA|AZ) # coordinate type
503             # most everything else is optional
504             # [sign]velocity, flux,vrange,vel_def,frame,comments
505             \s*
506             (n\/a|[+-]\s*\d+(?:\.\d*)?)? # velocity [8]
507             \s*
508             (n\/a|\d+(?:\.\d*)?)? # flux [9]
509             \s*
510             (n\/a|\d+(?:\.\d*)?)? # vel range [10]
511             \s*
512             ([\w\/]+)? # vel frame [11]
513             \s*
514             ([\w\/]+)? # vel defn [12]
515             \s*
516             (.*)$ # comment [13]
517             /xi);
518              
519             # Abort if we do not have matches for the first 8 fields
520             for (0..7) {
521             return unless defined $match[$_];
522             }
523              
524             # Read the values
525             my $target = $match[0];
526             my $ra = join(":",@match[1..3]);
527             my $dec = join(":",@match[4..6]);
528             $dec =~ s/\s//g; # remove space between the sign and number
529             my $epoc = $match[7];
530              
531             print "Creating a new source in _parse_line: $target\n" if $DEBUG;
532              
533             # need to translate JCMT epoch to normal epoch
534             my %coords;
535             $epoc = uc($epoc);
536             $coords{name} = $target;
537             if ($epoc eq 'RJ') {
538             $coords{ra} = $ra;
539             $coords{dec} = $dec;
540             $coords{type} = "j2000"
541             } elsif ($epoc eq 'RB') {
542             $coords{ra} = $ra;
543             $coords{dec} = $dec;
544             $coords{type} = "b1950";
545             } elsif ($epoc eq 'GA') {
546             $coords{long} = $ra;
547             $coords{lat} = $dec;
548             $coords{type} = "galactic";
549             } elsif ($epoc eq 'AZ') {
550             $coords{az} = $ra;
551             $coords{el} = $dec;
552             $coords{units} = 'sexagesimal';
553             } else {
554             warnings::warnif "Unknown coordinate type: '$epoc' for target $target. Ignoring line.";
555             return;
556             }
557              
558             # catalog comments are space delimited
559             my $ccol = 13;
560             my $cat_comm = (defined $match[$ccol] ? $match[$ccol] : '');
561              
562             # Replace multiple spaces in comment with single space
563             $cat_comm =~ s/\s+/ /g;
564              
565             # velocity
566             $coords{vdefn} = "RADIO";
567             $coords{vframe} = "LSR";
568             if (defined $match[8] && $match[8] !~ /n/) {
569             $match[8] =~ s/\s//g; # remove spaces
570             $coords{rv} = $match[8];
571             $coords{vdefn} = $match[12];
572             $coords{vframe} = $match[11];
573             }
574              
575             # create the source object
576             my $source = new Astro::Coords( %coords );
577              
578             unless (defined $source ) {
579             if ($DEBUG) {
580             print "failed to create source for '$target' and $ra and $dec and $epoc\n";
581             return;
582             } else {
583             croak "Error parsing line. Unable to create source date for target '$target' at RA '$ra' Dec '$dec' and Epoch '$epoc'\n";
584             }
585             }
586              
587             $source->telescope( $tel ) if $tel;
588             $source->comment($cat_comm);
589              
590             # Field name should simply be linked to the telescope
591             my $field = (defined $tel ? $tel->name : '' );
592              
593             my %misc;
594             # Grab the line's velocity range, if it isn't "n/a".
595             if( defined $match[10] && $match[10] !~ /n\/a/ ) {
596             $misc{'velocity_range'} = $match[10];
597             }
598              
599             # Grab the 850-micron flux, if it isn't "n/a".
600             if( defined $match[9] && $match[9] !~ /n\/a/ ) {
601             $misc{'flux850'} = $match[9];
602             }
603              
604             print "Created a new source in _parse_line: $target in field $field\n" if $DEBUG;
605              
606             # Now create the star object
607             return new Astro::Catalog::Star( id => $target,
608             coords => $source,
609             field => $field,
610             comment => $cat_comm,
611             misc => \%misc,
612             );
613              
614             }
615              
616              
617             =back
618              
619             =head1 NOTES
620              
621             Coordinates are stored as C objects inside
622             C objects.
623              
624              
625             =head1 GLOBAL VARIABLES
626              
627             The following global variables can be modified to control the state of the
628             module:
629              
630             =over 4
631              
632             =item $DEBUG
633              
634             Controls debugging messages. Default state is false.
635              
636             =back
637              
638             =head1 CONSTANTS
639              
640             The following constants are available for querying:
641              
642             =over 4
643              
644             =item MAX_SRC_LENGTH
645              
646             The maximum length of sourcenames writable to a JCMT source catalogue.
647              
648             =back
649              
650             =head1 COPYRIGHT
651              
652             Copyright (C) 1999-2003 Particle Physics and Astronomy Research Council.
653             All Rights Reserved.
654              
655             =head1 AUTHORS
656              
657             Tim Jenness Etjenness@cpan.orgE
658              
659             =cut
660              
661             1;