File Coverage

blib/lib/Astro/FITS/HdrTrans/ACSIS.pm
Criterion Covered Total %
statement 189 271 69.7
branch 44 146 30.1
condition 38 108 35.1
subroutine 31 31 100.0
pod 19 19 100.0
total 321 575 55.8


line stmt bran cond sub pod time code
1             package Astro::FITS::HdrTrans::ACSIS;
2              
3             =head1 NAME
4              
5             Astro::FITS::HdrTrans::ACSIS - class for translation of JCMT ACSIS headers
6              
7             =head1 SYNOPSIS
8              
9             use Astro::FITS::HdrTrans::ACSIS;
10              
11             =head1 DESCRIPTION
12              
13             This class provides a set of translations for ACSIS at JCMT.
14              
15             =cut
16              
17 10     10   26334339 use 5.006;
  10         46  
18 10     10   53 use warnings;
  10         23  
  10         389  
19 10     10   54 use strict;
  10         32  
  10         281  
20 10     10   63 use Carp;
  10         19  
  10         938  
21              
22 10     10   7994 use Astro::Coords;
  10         5634243  
  10         389  
23 10     10   7029 use Astro::Telescope;
  10         46384  
  10         349  
24 10     10   156 use DateTime;
  10         22  
  10         218  
25 10     10   54 use DateTime::TimeZone;
  10         28  
  10         243  
26              
27             # inherit from the Base translation class and not HdrTrans
28             # itself (which is just a class-less wrapper)
29 10     10   53 use base qw/ Astro::FITS::HdrTrans::JCMT /;
  10         23  
  10         6292  
30              
31             # Use the FITS standard DATE-OBS handling
32             #use Astro::FITS::HdrTrans::FITS;
33              
34             # Speed of light in km/s.
35 10     10   106 use constant CLIGHT => 2.99792458e5;
  10         20  
  10         706  
36              
37 10     10   63 use vars qw/ $VERSION /;
  10         31  
  10         29782  
38              
39             $VERSION = "1.64";
40              
41             # Cache UTC definition
42             our $UTC = DateTime::TimeZone->new( name => 'UTC' );
43              
44             # in each class we have three sets of data.
45             # - constant mappings
46             # - unit mappings
47             # - complex mappings
48              
49             # for a constant mapping, there is no FITS header, just a generic
50             # header that is constant
51             my %CONST_MAP = (
52             INST_DHS => 'ACSIS',
53             );
54              
55             # unit mapping implies that the value propagates directly
56             # to the output with only a keyword name change
57              
58             my %UNIT_MAP = (
59             AIRMASS_END => 'AMEND',
60             AMBIENT_TEMPERATURE=> 'ATSTART',
61             AZIMUTH_END => 'AZEND',
62             BACKEND => 'BACKEND',
63             BANDWIDTH_MODE => 'BWMODE',
64             CHOP_ANGLE => 'CHOP_PA',
65             CHOP_COORDINATE_SYSTEM => 'CHOP_CRD',
66             CHOP_FREQUENCY => 'CHOP_FRQ',
67             CHOP_THROW => 'CHOP_THR',
68             ELEVATION_END => 'ELEND',
69             FRONTEND => 'INSTRUME',
70             NUMBER_OF_CYCLES => 'NUM_CYC',
71             SWITCH_MODE => 'SW_MODE',
72             SPECIES => 'MOLECULE',
73             VELOCITY_TYPE => 'DOPPLER',
74             );
75              
76             # Create the translation methods
77             __PACKAGE__->_generate_lookup_methods( \%CONST_MAP, \%UNIT_MAP );
78              
79             =head1 METHODS
80              
81             =over 4
82              
83             =item B<can_translate>
84              
85             Returns true if the supplied headers can be handled by this class.
86              
87             $cando = $class->can_translate( \%hdrs );
88              
89             For this class, the method will return true if the B<BACKEND> header exists
90             and matches 'ACSIS'.
91              
92             Can also match translated GSD files.
93              
94             =cut
95              
96             sub can_translate {
97 21     21 1 63 my $self = shift;
98 21         41 my $headers = shift;
99              
100 21 100 66     188 if ( exists $headers->{BACKEND} &&
    50 100        
    50 66        
      66        
      33        
      33        
      33        
      33        
101             defined $headers->{BACKEND} &&
102             $headers->{BACKEND} =~ /^ACSIS/i
103             ) {
104 2         240 return 1;
105              
106             # BACKEND will discriminate between DAS files converted to ACSIS format
107             # from GSD format directly (handled by Astro::FITS::HdrTrans::JCMT_GSD).
108             } elsif ( exists $headers->{BACKEND} &&
109             defined $headers->{BACKEND} &&
110             $headers->{BACKEND} =~ /^DAS/i &&
111             ! (exists $headers->{'GSDFILE'} && exists $headers->{'SCA#'})) {
112             # Do not want to confuse with reverse conversion
113             # of JCMT_GSD data headers which will have a defined
114             # BACKEND header of DAS.
115 0         0 return 1;
116             } elsif ( exists $headers->{INST_DHS} &&
117             defined $headers->{INST_DHS} &&
118             $headers->{INST_DHS} eq 'ACSIS') {
119             # This is for the reverse conversion of DAS data
120 0         0 return 1;
121             } else {
122 19         849 return 0;
123             }
124             }
125              
126             =back
127              
128             =head1 COMPLEX CONVERSIONS
129              
130             These methods are more complicated than a simple mapping. We have to
131             provide both from- and to-FITS conversions All these routines are
132             methods and the to_ routines all take a reference to a hash and return
133             the translated value (a many-to-one mapping) The from_ methods take a
134             reference to a generic hash and return a translated hash (sometimes
135             these are many-to-many)
136              
137             =over 4
138              
139             =item B<to_DR_RECIPE>
140              
141             Usually simply copies the RECIPE header. If the header is undefined,
142             initially set the recipe to REDUCE_SCIENCE. If the observation type
143             is skydip and the RECIPE header is "REDUCE_SCIENCE", actually use
144             REDUCE_SKYDIP. If a skydip is not being done and the STANDARD header
145             is true, then the recipe is set to REDUCE_STANDARD. If the INBEAM
146             header is "POL", the recipe name has "_POL" appended if it is a
147             science observation. "REDUCE_SCIENCE" is translated to
148             "REDUCE_SCIENCE_GRADIENT".
149              
150             =cut
151              
152             sub to_DR_RECIPE {
153 1     1 1 3 my $class = shift;
154 1         2 my $FITS_headers = shift;
155              
156 1         5 my $dr = $FITS_headers->{RECIPE};
157 1 50       88 if ( defined( $dr ) ) {
158 1         4 $dr = uc( $dr );
159             } else {
160 0         0 $dr = 'REDUCE_SCIENCE';
161             }
162              
163 1         5 my $obstype = lc( $class->to_OBSERVATION_TYPE( $FITS_headers ) );
164 1         5 my $pol = $class->to_POLARIMETER( $FITS_headers );
165 1         36 my $standard = $class->to_STANDARD( $FITS_headers );
166 1         4 my $utdate = $class->to_UTDATE( $FITS_headers );
167 1         39 my $freq_sw = $class->_is_FSW( $FITS_headers );
168              
169 1 50       5 if ($utdate < 20080701) {
170 1 50 33     5 if ($obstype eq 'skydip' && $dr eq 'REDUCE_SCIENCE') {
171 0         0 $dr = "REDUCE_SKYDIP";
172             }
173             }
174              
175 1         6 my $is_sci = ( $obstype =~ /science|raster|scan|grid|jiggle/ );
176              
177 1 50 33     6 if ( $standard && $is_sci ) {
178 0         0 $dr = "REDUCE_STANDARD";
179             }
180              
181             # Append unless we have already appended
182 1 0 33     5 if ( $utdate > 20081115 && $pol && $is_sci ) {
      33        
183 0 0       0 $dr .= "_POL" unless $dr =~ /_POL$/;
184             }
185              
186 1 50       4 if ( $dr eq 'REDUCE_SCIENCE' ) {
187 0 0       0 $dr .= '_' . ($freq_sw ? 'FSW' : 'GRADIENT');
188             }
189              
190 1         3 return $dr;
191             }
192              
193             =item B<from_DR_RECIPE>
194              
195             Returns DR_RECIPE unless we have a skydip.
196              
197             =cut
198              
199             sub from_DR_RECIPE {
200 1     1 1 3 my $class = shift;
201 1         5 my $generic_headers = shift;
202 1         3 my $dr = $generic_headers->{DR_RECIPE};
203 1         2 my $ut = $generic_headers->{UTDATE};
204 1 50 33     10 if (defined $ut && $ut < 20080615) {
205 1 50 33     8 if (defined $dr && $dr eq 'REDUCE_SKYDIP') {
206 0         0 $dr = 'REDUCE_SCIENCE';
207             }
208             }
209 1         11 return ("RECIPE" => $dr);
210             }
211              
212             =item B<to_POLARIMETER>
213              
214             If the polarimeter is in the beam, as denoted by the INBEAM header
215             containing "POL", then this returns true. Otherwise, return false.
216              
217             =cut
218              
219             sub to_POLARIMETER {
220 2     2 1 4 my $class = shift;
221 2         4 my $FITS_headers = shift;
222              
223 2         8 my $inbeam = $FITS_headers->{INBEAM};
224 2         108 my $utdate = $class->to_UTDATE( $FITS_headers );
225              
226 2 0 33     88 if ( $utdate > 20081115 &&
      33        
227             defined( $inbeam ) &&
228             $inbeam =~ /pol/i ) {
229 0         0 return 1;
230             }
231 2         7 return 0;
232             }
233              
234             =item B<from_POLARIMETER>
235              
236             If the POLARIMETER header is true, then return "POL" for the INBEAM
237             header. Otherwise, return undef.
238              
239             =cut
240              
241             sub from_POLARIMETER {
242 1     1 1 12 my $class = shift;
243 1         3 my $generic_headers = shift;
244              
245 1         3 my $pol = $generic_headers->{POLARIMETER};
246              
247 1 50       4 if ( $pol ) {
248 0         0 return ( "INBEAM" => "POL" );
249             }
250              
251 1         15 return ( "INBEAM" => undef );
252             }
253              
254             =item B<to_REFERENCE_LOCATION>
255              
256             Creates a string representing the location of the reference spectrum
257             to the nearest hundredth of a degree. It takes the form
258             system_longitude_latitude where system will normally be J2000 or GAL.
259             If the string cannot be evaluated (such as missing headers), the
260             returned value is undefined.
261              
262             =cut
263              
264             sub to_REFERENCE_LOCATION {
265 1     1 1 3 my $self = shift;
266 1         2 my $FITS_headers = shift;
267              
268             # Set the returned value in case something goes awry.
269 1         3 my $ref_location = undef;
270              
271             # Assume that the co-ordinate system is the same for the BASE
272             # co-ordinates as the offset to the reference spectrum.
273 1         3 my ( $system, $base_lon, $base_lat );
274              
275             $system = defined( $FITS_headers->{'TRACKSYS'} ) ?
276 1 50       4 $FITS_headers->{'TRACKSYS'} :
277             undef;
278 1 50       62 $system =~ s/\s+$// if defined( $system );
279              
280             # Obtain the base location's longitude in decimal degrees.
281             $base_lon = defined( $FITS_headers->{'BASEC1'} ) ?
282 1 50       5 $FITS_headers->{'BASEC1'} :
283             undef;
284              
285             # Obtain the base location's latitude in decimal degrees.
286             $base_lat = defined( $FITS_headers->{'BASEC2'} ) ?
287 1 50       50 $FITS_headers->{'BASEC2'} :
288             undef;
289              
290             # Derive the reference position's longitude.
291 1         48 my $ref_lon = undef;
292 1 50 33     18 if ( defined( $system ) && defined( $base_lon ) ) {
293              
294             # The value of SKYREFX has the form
295             # [OFFSET] <longitude_offset_in_arcsec> [<co-ordinate system>]
296             #
297             # Assume for now that the TRACKSYS and co-ordinate system are the
298             # same.
299 0 0       0 if ( defined( $FITS_headers->{'SKYREFX'} ) ) {
300 0         0 my $ref_x = $FITS_headers->{'SKYREFX'};
301 0         0 my @comps = split( /\s+/, $ref_x );
302 0         0 my $offset_lon = $comps[1] / 3600.0;
303              
304             # Two decimal places should permit sufficient fuzziness.
305 0         0 $ref_lon = sprintf( "%.2f", $base_lon + $offset_lon );
306             }
307             }
308              
309             # Derive the reference position's latitude.
310 1         2 my $ref_lat = undef;
311 1 50 33     4 if ( defined( $system ) && defined( $base_lat ) ) {
312              
313             # The value of SKYREFY has the form
314             # [OFFSET] <latitude_offset_in_arcsec> [<co-ordinate system>]
315             #
316             # Assume for now that the TRACKSYS and co-ordinate system are the
317             # same.
318 0 0       0 if ( defined( $FITS_headers->{'SKYREFY'} ) ) {
319 0         0 my $ref_y = $FITS_headers->{'SKYREFY'};
320 0         0 my @comps = split( /\s+/, $ref_y );
321 0         0 my $offset_lat = $comps[1] / 3600.0;
322 0         0 $ref_lat = sprintf( "%.2f", $base_lat + $offset_lat );
323             }
324             }
325              
326             # Form the string comprising the three elements.
327 1 50 33     5 if ( defined( $ref_lon ) && defined( $ref_lat ) ) {
328 0         0 $ref_location = $system . "_" . $ref_lon . "_" . $ref_lat;
329             }
330              
331 1         4 return $ref_location;
332             }
333              
334              
335             =item B<to_SAMPLE_MODE>
336              
337             If the SAM_MODE value is either 'raster' or 'scan', return
338             'scan'. Otherwise, return the value in lowercase.
339              
340             =cut
341              
342             sub to_SAMPLE_MODE {
343 1     1 1 3 my $self = shift;
344 1         2 my $FITS_headers = shift;
345              
346 1         2 my $sam_mode;
347 1 50 33     4 if( defined( $FITS_headers->{'SAM_MODE'} ) &&
348             uc( $FITS_headers->{'SAM_MODE'} ) eq 'RASTER' ) {
349 0         0 $sam_mode = 'scan';
350             } else {
351 1         162 $sam_mode = lc( $FITS_headers->{'SAM_MODE'} );
352             }
353 1         68 return $sam_mode;
354             }
355              
356             =item B<to_SURVEY>
357              
358             Checks the value of the SURVEY header and uses that. If it's
359             undefined, then use the PROJECT header to determine an appropriate
360             survey.
361              
362             =cut
363              
364             sub to_SURVEY {
365 1     1 1 3 my $self = shift;
366 1         3 my $FITS_headers = shift;
367              
368 1         2 my $survey;
369              
370 1 50       4 if( defined( $FITS_headers->{'SURVEY'} ) ) {
371 0         0 $survey = $FITS_headers->{'SURVEY'};
372             } else {
373              
374 1         77 my $project = $FITS_headers->{'PROJECT'};
375 1 50       69 if( defined( $project ) ) {
376 1 50       6 if( $project =~ /JLS([GNS])/ ) {
377 0 0       0 if( $1 eq 'G' ) {
    0          
    0          
378 0         0 $survey = 'GBS';
379             } elsif( $1 eq 'N' ) {
380 0         0 $survey = 'NGS';
381             } elsif( $1 eq 'S' ) {
382 0         0 $survey = 'SLS';
383             }
384             }
385             }
386             }
387              
388 1         3 return $survey;
389              
390             }
391              
392             =item B<to_EXPOSURE_TIME>
393              
394             Uses the to_UTSTART and to_UTEND functions to calculate the exposure
395             time. Returns the exposure time as a scalar, not as a Time::Seconds
396             object.
397              
398             =cut
399              
400             sub to_EXPOSURE_TIME {
401 1     1 1 3 my $self = shift;
402 1         2 my $FITS_headers = shift;
403              
404             # force date headers to be standardized
405 1         5 $self->_fix_dates( $FITS_headers );
406              
407 1         2 my $return;
408 1 50 33     3 if ( exists( $FITS_headers->{'DATE-OBS'} ) &&
409             exists( $FITS_headers->{'DATE-END'} ) ) {
410 1         53 my $start = $self->to_UTSTART( $FITS_headers );
411 1         9 my $end = $self->to_UTEND( $FITS_headers );
412 1         5 my $duration = $end - $start;
413 1         39 $return = $duration->seconds;
414             }
415 1         43 return $return;
416             }
417              
418             =item B<to_INSTRUMENT>
419              
420             Converts the C<INSTRUME> header into the C<INSTRUMENT> header. If the
421             C<INSTRUME> header begins with "HARP" or "FE_HARP", then the
422             C<INSTRUMENT> header will be set to "HARP".
423              
424             =cut
425              
426             sub to_INSTRUMENT {
427 1     1 1 2 my $self = shift;
428 1         3 my $FITS_headers = shift;
429 1         2 my $return;
430 1 50       4 if ( exists( $FITS_headers->{'INSTRUME'} ) ) {
431 1 50 33     29 if ( $FITS_headers->{'INSTRUME'} =~ /^HARP/ ||
432             $FITS_headers->{'INSTRUME'} =~ /^FE_HARP/ ) {
433 0         0 $return = "HARP";
434             } else {
435 1         138 $return = $FITS_headers->{'INSTRUME'};
436             }
437             }
438 1         67 return $return;
439             }
440              
441             =item B<to_OBSERVATION_ID>
442              
443             Converts the C<OBSID> header directly into the C<OBSERVATION_ID>
444             generic header, or if that header does not exist, converts the
445             C<BACKEND>, C<OBSNUM>, and C<DATE-OBS> headers into C<OBSERVATION_ID>.
446              
447             =cut
448              
449             sub to_OBSERVATION_ID {
450 1     1 1 2 my $self = shift;
451 1         2 my $FITS_headers = shift;
452 1         3 my $return;
453 1 50 33     5 if ( exists( $FITS_headers->{'OBSID'} ) &&
454             defined( $FITS_headers->{'OBSID'} ) ) {
455 1         97 $return = $FITS_headers->{'OBSID'};
456             } else {
457 0         0 $self->_fix_dates( $FITS_headers );
458              
459 0         0 my $backend = lc( $self->to_BACKEND( $FITS_headers ) );
460 0         0 my $obsnum = $self->to_OBSERVATION_NUMBER( $FITS_headers );
461 0         0 my $dateobs = $self->to_UTSTART( $FITS_headers );
462              
463 0 0 0     0 if ( defined( $backend ) &&
      0        
464             defined( $obsnum ) &&
465             defined( $dateobs ) ) {
466 0         0 my $datetime = $dateobs->datetime;
467 0         0 $datetime =~ s/-//g;
468 0         0 $datetime =~ s/://g;
469              
470 0         0 $return = join '_', $backend, $obsnum, $datetime;
471             }
472             }
473              
474 1         81 return $return;
475             }
476              
477             =item B<to_OBSERVATION_MODE>
478              
479             Concatenates the SAM_MODE, SW_MODE, and OBS_TYPE header keywords into
480             the OBSERVATION_MODE generic header, with spaces removed and joined
481             with underscores. For example, if SAM_MODE is 'jiggle ', SW_MODE is
482             'chop ', and OBS_TYPE is 'science ', then the OBSERVATION_MODE generic
483             header will be 'jiggle_chop_science'.
484              
485             =cut
486              
487             sub to_OBSERVATION_MODE {
488 1     1 1 3 my $self = shift;
489 1         3 my $FITS_headers = shift;
490              
491 1         2 my $return;
492 1 50 33     4 if ( exists( $FITS_headers->{'SAM_MODE'} ) &&
      33        
493             exists( $FITS_headers->{'SW_MODE'} ) &&
494             exists( $FITS_headers->{'OBS_TYPE'} ) ) {
495 1         80 my $sam_mode = $FITS_headers->{'SAM_MODE'};
496 1         74 $sam_mode =~ s/\s//g;
497 1 50       4 $sam_mode = "raster" if $sam_mode eq "scan";
498 1         4 my $sw_mode = $FITS_headers->{'SW_MODE'};
499 1         66 $sw_mode =~ s/\s//g;
500              
501             # handle OBS_TYPE missing
502 1         4 my $obs_type = $FITS_headers->{'OBS_TYPE'};
503 1 50       68 $obs_type = "science" unless $obs_type;
504 1         3 $obs_type =~ s/\s//g;
505              
506 1 50       8 $return = ( ( $obs_type =~ /science/i )
507             ? join '_', $sam_mode, $sw_mode
508             : join '_', $sam_mode, $sw_mode, $obs_type );
509             }
510 1         4 return $return;
511             }
512              
513             =item B<to_OBSERVATION_TYPE>
514              
515             Returns the type of observation that was done. If the OBS_TYPE header
516             matches /science/, the SAM_MODE header is used: if SAM_MODE matches
517             /raster/, then return 'raster'. If SAM_MODE matches /grid/, then
518             return 'grid'. If SAM_MODE matches /jiggle/, then return 'jiggle'.
519              
520             If the OBS_TYPE header matches /focus/, then return 'focus'. If the
521             OBS_TYPE header matches /pointing/, then return 'pointing'.
522              
523             If none of the above options hold, then return undef.
524              
525             =cut
526              
527             sub to_OBSERVATION_TYPE {
528 2     2 1 5 my $self = shift;
529 2         3 my $FITS_headers = shift;
530              
531 2         3 my $return;
532 2         7 my $ot = $FITS_headers->{OBS_TYPE};
533              
534             # Sometimes we lack OBS_TYPE. In that case we have to assume SCIENCE
535             # even though the headers are broken. (eg 20080509#18 RxWD)
536 2 50       142 $ot = "science" unless $ot;
537              
538 2 50       7 if ( $ot ) {
539 2         5 my $obs_type = lc( $ot );
540              
541 2 50       12 if ( $obs_type =~ /science/ ) {
    50          
    0          
    0          
542              
543 0 0       0 if ( defined( $FITS_headers->{'SAM_MODE'} ) ) {
544              
545 0         0 my $sam_mode = $FITS_headers->{'SAM_MODE'};
546              
547 0 0       0 if ( $sam_mode =~ /raster|scan/ ) {
    0          
    0          
548 0         0 $return = "raster";
549             } elsif ( $sam_mode =~ /grid/ ) {
550 0         0 $return = "grid";
551             } elsif ( $sam_mode =~ /jiggle/ ) {
552 0         0 $return = "jiggle";
553             } else {
554 0         0 croak "Unexpected sample mode: '$sam_mode'";
555             }
556             }
557             } elsif ( $obs_type =~ /focus/ ) {
558 2         5 $return = "focus";
559             } elsif ( $obs_type =~ /pointing/ ) {
560 0         0 $return = "pointing";
561             } elsif ( $obs_type =~ /skydip/) {
562 0         0 $return = "skydip";
563             } else {
564 0         0 croak "Unexpected OBS_TYPE of '$obs_type'\n";
565             }
566             }
567              
568 2         5 return $return;
569             }
570              
571              
572             =item B<to_REST_FREQUENCY>
573              
574             Uses an C<Starlink::AST::FrameSet> object to determine the
575             frequency. If such an object is not passed in, then the rest frequency
576             is set to zero.
577              
578             Returns the rest frequency in Hz.
579              
580             =cut
581              
582             sub to_REST_FREQUENCY {
583 1     1 1 2 my $self = shift;
584 1         2 my $FITS_headers = shift;
585 1         3 my $frameset = shift;
586              
587 1         3 my $return;
588              
589 1 50 33     6 if ( defined( $frameset ) &&
    50 33        
      33        
590             UNIVERSAL::isa( $frameset, "Starlink::AST::FrameSet" ) ) {
591             # in some rare cases restfreq is not set in the frameset
592 0         0 eval {
593 0         0 my $frequency = $frameset->Get( "restfreq" );
594 0         0 $return = $frequency * 1_000_000_000;
595             };
596             } elsif ( exists( $FITS_headers->{'RESTFREQ'} ) ||
597             ( exists( $FITS_headers->{'SUBHEADERS'} ) &&
598             exists( $FITS_headers->{'SUBHEADERS'}->[0]->{'RESTFREQ'} ) ) ) {
599              
600             $return = exists( $FITS_headers->{'RESTFREQ'} ) ?
601             $FITS_headers->{'RESTFREQ'} :
602 0 0       0 $FITS_headers->{'SUBHEADERS'}->[0]->{'RESTFREQ'};
603 0         0 $return *= 1_000_000_000;
604             }
605              
606 1         91 return $return;
607             }
608              
609             =item B<to_SYSTEM_VELOCITY>
610              
611             Converts the DOPPLER and SPECSYS headers into one combined
612             SYSTEM_VELOCITY header. The first three characters of each specific
613             header are used and concatenated. For example, if DOPPLER is 'radio'
614             and SPECSYS is 'LSR', then the resulting SYSTEM_VELOCITY generic
615             header will be 'RADLSR'. The results are always returned in capital
616             letters.
617              
618             =cut
619              
620             sub to_SYSTEM_VELOCITY {
621 1     1 1 4 my $self = shift;
622 1         3 my $FITS_headers = shift;
623 1         3 my $frameset = shift;
624              
625 1         2 my $return;
626 1 50 33     5 if ( exists( $FITS_headers->{'DOPPLER'} ) && defined $FITS_headers->{DOPPLER} ) {
627 0         0 my $doppler = uc( $FITS_headers->{'DOPPLER'} );
628              
629 0 0 0     0 if ( defined( $frameset ) &&
630             UNIVERSAL::isa( $frameset, "Starlink::AST::FrameSet" ) ) {
631             # Sometimes we have frequency axis (rare)
632 0         0 eval {
633 0         0 my $sourcevrf = uc( $frameset->Get( "sourcevrf" ) );
634 0         0 $return = substr( $doppler, 0, 3 ) . substr( $sourcevrf, 0, 3 );
635             };
636             }
637 0 0       0 if (!defined $return) {
638 0 0       0 if ( exists( $FITS_headers->{'SPECSYS'} ) ) {
639 0         0 my $specsys = uc( $FITS_headers->{'SPECSYS'} );
640 0         0 $return = substr( $doppler, 0, 3 ) . substr( $specsys, 0, 3 );
641             } else {
642 0         0 my $specsys = '';
643 0 0       0 if ( $doppler eq 'RADIO' ) {
    0          
644 0         0 $specsys = 'LSRK';
645             } elsif ( $doppler eq 'OPTICAL' ) {
646 0         0 $specsys = 'HELIOCENTRIC';
647             }
648 0         0 $return = substr( $doppler, 0, 3 ) . substr( $specsys, 0, 3 );
649             }
650             }
651             }
652 1         102 return $return;
653             }
654              
655             =item B<to_TRANSITION>
656              
657             Converts the TRANSITI header to the TRANSITION generic header.
658              
659             This would be a unit mapping except that we would like to tidy up
660             some whitespace issues.
661              
662             =cut
663              
664             sub to_TRANSITION {
665 1     1 1 3 my $self = shift;
666 1         2 my $FITS_headers = shift;
667              
668 1         5 my $transition = $FITS_headers->{'TRANSITI'};
669              
670 1 50       73 return undef unless defined $transition;
671              
672             # Remove leading and trailing spaces.
673 1         5 $transition =~ s/^ *//;
674 1         6 $transition =~ s/ *$//;
675             # Remove duplicated spaces.
676 1         8 $transition =~ s/ +/ /g;
677              
678 1         3 return $transition;
679             }
680              
681             =item B<from_TRANSITION>
682              
683             Converts TRANSITION back to TRANSITI.
684              
685             =cut
686              
687             sub from_TRANSITION {
688 1     1 1 2 my $self = shift;
689 1         3 my $generic_headers = shift;
690              
691 1         4 my $transition = $generic_headers->{'TRANSITION'};
692              
693 1 50       5 if (defined $transition) {
694             # Restore whitespace issue to allow comparison of untranslated header.
695 1         5 $transition =~ s/ - / - /;
696             }
697              
698 1         33 return (TRANSITI => $transition);
699             }
700              
701             =item B<to_VELOCITY>
702              
703             Converts the ZSOURCE header into an appropriate system velocity,
704             depending on the value of the DOPPLER header. If the DOPPLER header is
705             'redshift', then the VELOCITY generic header will be returned
706             as a redshift. If the DOPPLER header is 'optical', then the
707             VELOCITY generic header will be returned as an optical
708             velocity. If the DOPPLER header is 'radio', then the VELOCITY
709             generic header will be returned as a radio velocity. Note that
710             calculating the radio velocity from the zeropoint (which is the
711             ZSOURCE header) gives accurates results only if the radio velocity is
712             a small fraction (~0.01) of the speed of light.
713              
714             =cut
715              
716             sub to_VELOCITY {
717 1     1 1 3 my $self = shift;
718 1         3 my $FITS_headers = shift;
719 1         3 my $frameset = shift;
720              
721 1         3 my $velocity = 0;
722 1 50 33     7 if ( defined( $frameset ) &&
723             UNIVERSAL::isa( $frameset, "Starlink::AST::FrameSet" ) ) {
724              
725 0         0 my $sourcesys = "VRAD";
726 0 0       0 if ( defined( $FITS_headers->{'DOPPLER'} ) ) {
727 0 0       0 if ( $FITS_headers->{'DOPPLER'} =~ /rad/i ) {
    0          
    0          
728 0         0 $sourcesys = "VRAD";
729             } elsif ( $FITS_headers->{'DOPPLER'} =~ /opt/i ) {
730 0         0 $sourcesys = "VOPT";
731             } elsif ( $FITS_headers->{'DOPPLER'} =~ /red/i ) {
732 0         0 $sourcesys = "REDSHIFT";
733             }
734             }
735             # Sometimes we do not have a spec frame (broken files)
736 0         0 eval {
737 0         0 $frameset->Set( sourcesys => $sourcesys );
738 0         0 $velocity = $frameset->Get( "sourcevel" );
739             };
740             } else {
741              
742             # We weren't passed a frameset, so try using other headers.
743 1 50 33     4 if ( exists( $FITS_headers->{'DOPPLER'} ) &&
      33        
744             ( exists( $FITS_headers->{'ZSOURCE'} ) ||
745             exists( $FITS_headers->{'SUBHEADERS'}->[0]->{'ZSOURCE'} ) ) ) {
746 0         0 my $doppler = uc( $FITS_headers->{'DOPPLER'} );
747             my $zsource = exists( $FITS_headers->{'ZSOURCE'} ) ?
748             $FITS_headers->{'ZSOURCE'} :
749 0 0       0 $FITS_headers->{'SUBHEADERS'}->[0]->{'ZSOURCE'};
750              
751 0 0       0 if ( $doppler eq 'REDSHIFT' ) {
    0          
    0          
752 0         0 $velocity = $zsource;
753             } elsif ( $doppler eq 'OPTICAL' ) {
754 0         0 $velocity = $zsource * CLIGHT;
755             } elsif ( $doppler eq 'RADIO' ) {
756 0         0 $velocity = ( CLIGHT * $zsource ) / ( 1 + $zsource );
757             }
758             }
759             }
760              
761 1         104 return $velocity;
762             }
763              
764             =item B<to_SUBSYSTEM_IDKEY>
765              
766             =cut
767              
768             sub to_SUBSYSTEM_IDKEY {
769 1     1 1 3 my $self = shift;
770 1         4 my $FITS_headers = shift;
771              
772             # Try the general headers first
773 1         8 my $general = $self->SUPER::to_SUBSYSTEM_IDKEY( $FITS_headers );
774 1 50       5 return ( defined $general ? $general : "SUBSYSNR" );
775             }
776              
777              
778             =item B<_is_FSW>
779              
780             Helper function to determine if we are doing frequency switch.
781              
782             =cut
783              
784             sub _is_FSW {
785 1     1   3 my $class = shift;
786 1         2 my $FITS_headers = shift;
787              
788 1         6 my $fsw = $FITS_headers->{SW_MODE};
789              
790 1 50 33     84 if ( defined( $fsw ) &&
791             $fsw =~ /freqsw/i ) {
792 0         0 return 1;
793             }
794 1         3 return 0;
795             }
796              
797             =back
798              
799             =head1 SEE ALSO
800              
801             C<Astro::FITS::HdrTrans>, C<Astro::FITS::HdrTrans::Base>
802              
803             =head1 AUTHORS
804              
805             Tim Jenness E<lt>t.jenness@jach.hawaii.eduE<gt>,
806             Brad Cavanagh E<lt>b.cavanagh@jach.hawaii.eduE<gt>.
807              
808             =head1 COPYRIGHT
809              
810             Copyright (C) 2016 East Asian Observatory.
811             Copyright (C) 2007-2013, 2016 Science and Technology Facilities Council.
812             Copyright (C) 2005-2007 Particle Physics and Astronomy Research Council.
813             All Rights Reserved.
814              
815             This program is free software; you can redistribute it and/or modify it under
816             the terms of the GNU General Public License as published by the Free Software
817             Foundation; either version 2 of the License, or (at your option) any later
818             version.
819              
820             This program is distributed in the hope that it will be useful,but WITHOUT ANY
821             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
822             PARTICULAR PURPOSE. See the GNU General Public License for more details.
823              
824             You should have received a copy of the GNU General Public License along with
825             this program; if not, write to the Free Software Foundation, Inc., 59 Temple
826             Place,Suite 330, Boston, MA 02111-1307, USA
827              
828             =cut
829              
830             1;