File Coverage

blib/lib/Astro/FITS/HdrTrans/JCMT.pm
Criterion Covered Total %
statement 159 173 91.9
branch 41 62 66.1
condition 23 42 54.7
subroutine 23 23 100.0
pod 14 14 100.0
total 260 314 82.8


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Astro::FITS::HdrTrans::JCMT - class combining common behaviour for modern JCMT instruments
4              
5             =cut
6              
7             package Astro::FITS::HdrTrans::JCMT;
8              
9 13     13   20820421 use strict;
  13         70  
  13         618  
10 13     13   116 use warnings;
  13         93  
  13         1020  
11              
12 13     13   13109 use Astro::Coords;
  13         2535648  
  13         380  
13 13     13   2272 use Astro::Telescope;
  13         19998  
  13         571  
14 13     13   91 use DateTime;
  13         28  
  13         422  
15 13     13   76 use DateTime::TimeZone;
  13         48  
  13         791  
16              
17             our $VERSION = '1.66';
18              
19 13     13   78 use base qw/ Astro::FITS::HdrTrans::JAC /;
  13         27  
  13         9741  
20              
21             # Unit mapping implies that the value propogates directly
22             # to the output with only a keyword name change.
23             my %UNIT_MAP =
24             (
25             AIRMASS_START => 'AMSTART',
26             AZIMUTH_START => 'AZSTART',
27             ELEVATION_START => 'ELSTART',
28             FILENAME => 'FILE_ID',
29             DR_RECIPE => "RECIPE",
30             HUMIDITY => 'HUMSTART',
31             LATITUDE => 'LAT-OBS',
32             LONGITUDE => 'LONG-OBS',
33             OBJECT => 'OBJECT',
34             OBSERVATION_NUMBER => 'OBSNUM',
35             PROJECT => 'PROJECT',
36             SCAN_PATTERN => 'SCAN_PAT',
37             STANDARD => 'STANDARD',
38             TAI_UTC_CORRECTION => 'DTAI',
39             UT1_UTC_CORRECTION => 'DUT1',
40             WIND_BLIND => 'WND_BLND',
41             X_APERTURE => 'INSTAP_X',
42             Y_APERTURE => 'INSTAP_Y',
43             );
44              
45             my %CONST_MAP = ();
46              
47             # Create the translation methods
48             __PACKAGE__->_generate_lookup_methods( \%CONST_MAP, \%UNIT_MAP );
49              
50             our $COORDS;
51              
52             =head1 METHODS
53              
54             =over 4
55              
56             =item B<translate_from_FITS>
57              
58             This routine overrides the base class implementation to enable the
59             caches to be cleared for target location.
60              
61             This means that some conversion methods (in particular those using time in
62             a base class) may not work properly outside the context of a full translation
63             unless they have been subclassed locally.
64              
65             Date fixups are handled in a super class.
66              
67             =cut
68              
69             sub translate_from_FITS {
70 4     4 1 9 my $class = shift;
71 4         8 my $headers = shift;
72              
73             # clear cache
74 4         52 $COORDS = undef;
75              
76             # Go to the base class
77 4         44 return $class->SUPER::translate_from_FITS( $headers, @_ );
78             }
79              
80             =item B<to_UTDATE>
81              
82             Converts the date in a date-obs header into a number of form YYYYMMDD.
83              
84             =cut
85              
86             sub to_UTDATE {
87 4     4 1 10 my $class = shift;
88 4         8 my $FITS_headers = shift;
89              
90 4         22 $class->_fix_dates( $FITS_headers );
91 4         29 return $class->SUPER::to_UTDATE( $FITS_headers, @_ );
92             }
93              
94             =item B<to_UTEND>
95              
96             Converts UT date in a date-end header into C<Time::Piece> object
97              
98             =cut
99              
100             sub to_UTEND {
101 5     5 1 15 my $class = shift;
102 5         15 my $FITS_headers = shift;
103              
104 5         30 $class->_fix_dates( $FITS_headers );
105 5         58 return $class->SUPER::to_UTEND( $FITS_headers, @_ );
106             }
107              
108             =item B<to_UTSTART>
109              
110             Converts UT date in a date-obs header into C<Time::Piece> object.
111              
112             =cut
113              
114             sub to_UTSTART {
115 9     9 1 27 my $class = shift;
116 9         16 my $FITS_headers = shift;
117              
118 9         40 $class->_fix_dates( $FITS_headers );
119 9         77 return $class->SUPER::to_UTSTART( $FITS_headers, @_ );
120             }
121              
122             =item B<to_RA_BASE>
123              
124             Uses the elevation, azimuth, telescope name, and observation start
125             time headers (ELSTART, AZSTART, TELESCOP, and DATE-OBS headers,
126             respectively) to calculate the base RA.
127              
128             Returns the RA in degrees.
129              
130             =cut
131              
132             sub to_RA_BASE {
133 4     4 1 13 my $self = shift;
134 4         8 my $FITS_headers = shift;
135              
136 4         21 my $coords = $self->_calc_coords( $FITS_headers );
137 4 50       17 return undef unless defined $coords;
138 4         41 return $coords->ra( format => 'deg' );
139             }
140              
141             =item B<to_DEC_BASE>
142              
143             Uses the elevation, azimuth, telescope name, and observation start
144             time headers (ELSTART, AZSTART, TELESCOP, and DATE-OBS headers,
145             respectively) to calculate the base declination.
146              
147             Returns the declination in degrees.
148              
149             =cut
150              
151             sub to_DEC_BASE {
152 4     4 1 11 my $self = shift;
153 4         10 my $FITS_headers = shift;
154              
155 4         34 my $coords = $self->_calc_coords( $FITS_headers );
156              
157 4 50       29 return undef unless defined $coords;
158 4         28 return $coords->dec( format => 'deg' );
159             }
160              
161             =item B<to_TAU>
162              
163             Use the average WVM tau measurements.
164              
165             =cut
166              
167             sub to_TAU {
168 4     4 1 12 my $self = shift;
169 4         10 my $FITS_headers = shift;
170              
171 4         11 my $tau = undef;
172 4         10 for my $src (qw/ TAU225 WVMTAU /) {
173 6         14 my $st = $src . "ST";
174 6         13 my $en = $src . "EN";
175              
176 6         20 my @startvals = $self->via_subheader_undef_check( $FITS_headers, $st );
177 6         29 my @endvals = $self->via_subheader_undef_check( $FITS_headers, $en );
178 6         18 my $startval = $startvals[0];
179 6         13 my $endval = $endvals[-1];
180              
181 6   66     50 my $have_start = ((defined $startval) and ($startval != 0.0));
182 6   66     31 my $have_end = ((defined $endval) and ($endval != 0.0));
183 6 100 66     39 if ($have_start and $have_end) {
    50          
    50          
184 4         41 $tau = ($startval + $endval) / 2;
185 4         18 last;
186             } elsif ($have_start) {
187 0         0 $tau = $startval;
188             } elsif ($have_end) {
189 0         0 $tau = $endval;
190             }
191             }
192 4         13 return $tau;
193             }
194              
195             =item B<to_SEEING>
196              
197             Use the average seeing measurements.
198              
199             =cut
200              
201             sub to_SEEING {
202 4     4 1 12 my $self = shift;
203 4         10 my $FITS_headers = shift;
204              
205 4         26 my $seeing = 0.0;
206              
207              
208 4         20 my @startvals = $self->via_subheader_undef_check( $FITS_headers, "SEEINGST" );
209 4         19 my @endvals = $self->via_subheader_undef_check( $FITS_headers, "SEEINGEN" );
210 4         10 my $startval = $startvals[0];
211 4         11 my $endval = $endvals[-1];
212              
213 4 100 66     56 if (defined $startval && defined $endval) {
    50          
    50          
214 3         28 $seeing = ($startval + $endval) / 2;
215             } elsif (defined $startval) {
216 0         0 $seeing = $startval;
217             } elsif (defined $endval) {
218 0         0 $seeing = $endval;
219             }
220              
221 4         14 return $seeing;
222             }
223              
224              
225              
226              
227             =item B<to_OBSERVATION_ID_SUBSYSTEM>
228              
229             Returns the subsystem observation IDs associated with the header.
230             Returns a reference to an array. Will be empty if the OBSIDSS header
231             is missing.
232              
233             =cut
234              
235             sub to_OBSERVATION_ID_SUBSYSTEM {
236 4     4 1 13 my $self = shift;
237 4         11 my $FITS_headers = shift;
238             # Try multiple headers since the database is different to the file
239 4         9 my @obsidss;
240 4         11 for my $h (qw/ OBSIDSS OBSID_SUBSYSNR /) {
241 5         20 my @found = $self->via_subheader( $FITS_headers, $h );
242 5 100       21 if (@found) {
243 3         10 @obsidss = @found;
244 3         10 last;
245             }
246             }
247 4         9 my @all;
248 4 100       18 if (@obsidss) {
249             # Remove duplicates
250 3         8 my %seen;
251 3         9 @all = grep { ! $seen{$_}++ } @obsidss;
  3         20  
252             }
253 4         17 return \@all;
254             }
255              
256             =item B<to_SUBSYSTEM_IDKEY>
257              
258             =cut
259              
260             sub to_SUBSYSTEM_IDKEY {
261 4     4 1 12 my $self = shift;
262 4         10 my $FITS_headers = shift;
263              
264 4         12 for my $try ( qw/ OBSIDSS OBSID_SUBSYSNR / ) {
265 5         20 my @results = $self->via_subheader( $FITS_headers, $try );
266 5 100       28 return $try if @results;
267             }
268 1         6 return;
269             }
270              
271             =item B<to_DOME_OPEN>
272              
273             Uses the roof and door status at start and end of observation headers
274             to generate a combined value which, if true, confirms that the dome
275             was fully open throughout. (Unless it closed and reopened during
276             the observation.)
277              
278             =cut
279              
280             sub to_DOME_OPEN {
281 4     4 1 11 my $self = shift;
282 4         9 my $FITS_headers = shift;
283              
284 4         13 my ($n_open, $n_closed, $n_other) = (0, 0, 0);
285              
286 4         12 foreach my $header (qw/DOORSTST DOORSTEN ROOFSTST ROOFSTEN/) {
287 16         51 foreach my $value ($self->via_subheader($FITS_headers, $header)) {
288 12 100       89 if ($value =~ /^open$/i) {
    50          
289 4         10 $n_open ++;
290             }
291             elsif ($value =~ /^closed$/i) {
292 8         22 $n_closed ++;
293             }
294             else {
295 0         0 $n_other ++;
296             }
297             }
298             }
299              
300 4 100 33     76 if ($n_open and not ($n_closed or $n_other)) {
      66        
301 1         6 return 1;
302             }
303              
304 3 100 33     25 if ($n_closed and not ($n_open or $n_other)) {
      66        
305 2         9 return 0;
306             }
307              
308 1         4 return undef;
309             }
310              
311             =item B<from_DOME_OPEN>
312              
313             Converts the DOME_OPEN value back to individual roof and door
314             status headers.
315              
316             =cut
317              
318             sub from_DOME_OPEN {
319 2     2 1 6 my $self = shift;
320 2         4 my $generic_headers = shift;
321              
322 2         6 my $value = undef;
323              
324 2 100       8 if (exists $generic_headers->{'DOME_OPEN'}) {
325 1         3 my $dome = $generic_headers->{'DOME_OPEN'};
326 1 50       4 if (defined $dome) {
327 1 50       4 $value = $dome ? 'Open' : 'Closed';
328             }
329             }
330              
331 2         5 return map {$_ => $value} qw/DOORSTST DOORSTEN ROOFSTST ROOFSTEN/;
  8         32  
332             }
333              
334             =item B<to_REMOTE>
335              
336             Convert between the JCMT's OPER_LOC header and a standardised 'REMOTE value'.
337              
338             REMOTE = 1
339             LOCAL = 0
340              
341             If not defined or has a different value, return 'undef'
342             =cut
343              
344             sub to_REMOTE {
345 4     4 1 9 my $self = shift;
346 4         10 my $FITS_headers = shift;
347 4         8 my $remote;
348 4 50       55 if (exists( $FITS_headers->{'REMOTE'})) {
349 0         0 $remote = $FITS_headers->{'REMOTE'};
350             } else {
351 4         60 $remote = ''
352             }
353 4 50       22 if (uc($remote) =~ /REMOTE/) {
    50          
354 0         0 $remote = 1;
355             } elsif (uc($remote) =~ /LOCAL/) {
356 0         0 $remote = 0;
357             } else {
358 4         11 $remote = undef;
359             }
360              
361 4         12 return $remote;
362             }
363              
364              
365             =item B<from_REMOTE>
366              
367             Converts the REMOTE value back to the OPER_LOC header
368             if REMOTE=1, oper_loc='REMOTE'
369             if REMOTE=0, oper_loc='LOCAL'
370             if REMOTE is anything else, return undef;
371              
372             =cut
373              
374             sub from_REMOTE {
375 2     2 1 6 my $self = shift;
376 2         4 my $generic_headers = shift;
377              
378 2         5 my $value = undef;
379              
380 2 50       8 if (exists $generic_headers->{'REMOTE'}) {
381 0         0 my $remote = $generic_headers->{'REMOTE'};
382 0 0       0 if (defined $remote) {
383 0 0       0 $value = $remote ? 'REMOTE' : 'LOCAL';
384             }
385             }
386              
387 2         29 return (OPER_LOC => $value);
388             }
389              
390              
391              
392             =back
393              
394             =head1 PRIVATE METHODS
395              
396             =over 4
397              
398             =item B<_calc_coords>
399              
400             Function to calculate the coordinates at the start of the observation by using
401             the elevation, azimuth, telescope, and observation start time. Caches
402             the result if it's already been calculated.
403              
404             Returns an Astro::Coords object.
405              
406             =cut
407              
408             sub _calc_coords {
409 8     8   18 my $self = shift;
410 8         35 my $FITS_headers = shift;
411              
412             # Force dates to be standardized
413 8         46 $self->_fix_dates( $FITS_headers );
414              
415             # Here be dragons. Possibility that cache will not be cleared properly
416             # if a user comes in outside of the translate_from_FITS() method.
417 8 100 66     62 if ( defined( $COORDS ) &&
418             UNIVERSAL::isa( $COORDS, "Astro::Coords" ) ) {
419 4         16 return $COORDS;
420             }
421              
422 4         20 my $telescope = $FITS_headers->{'TELESCOP'};
423              
424             # We can try DATE-OBS and AZEL START or DATE-END and AZEL END
425 4         356 my ($dateobs, $az, $el);
426              
427 4         40 my @keys = ( { date => "DATE-OBS", az => "AZSTART", el => "ELSTART" },
428             { date => "DATE-END", az => "AZEND", el => "ELEND" } );
429              
430 4         14 for my $keys_to_try ( @keys ) {
431              
432             # We might have subheaders, especially for the AZEL
433             # values so we read into arrays and check them.
434              
435 5         44 my @dateobs = $self->via_subheader( $FITS_headers, $keys_to_try->{date} );
436 5         58 my @azref = $self->via_subheader( $FITS_headers, $keys_to_try->{az} );
437 5         28 my @elref = $self->via_subheader( $FITS_headers, $keys_to_try->{el} );
438              
439             # try to ensure that we use the same index everywhere
440 5         15 my $idx;
441 5         23 ($idx, $dateobs) = _middle_value(\@dateobs, $idx);
442 5         16 ($idx, $az) = _middle_value(\@azref, $idx);
443 5         16 ($idx, $el) = _middle_value(\@elref, $idx);
444              
445             # if we have a set of values we can stop looking
446 5 100 66     65 last if (defined $dateobs && defined $az && defined $el);
      66        
447             }
448              
449             # only proceed if we have a defined value
450 4 50 33     114 if (defined $dateobs && defined $telescope
      33        
      33        
451             && defined $az && defined $el ) {
452 4         80 my $coords = new Astro::Coords( az => $az,
453             el => $el,
454             units => 'degrees',
455             );
456 4         1623 $coords->telescope( new Astro::Telescope( $telescope ) );
457              
458             # convert ISO date to object
459 4         78165 my $dt = Astro::FITS::HdrTrans::Base->_parse_iso_date( $dateobs );
460 4 50       15 return unless defined $dt;
461              
462 4         32 $coords->datetime( $dt );
463              
464 4         1417 $COORDS = $coords;
465 4         45 return $COORDS;
466             }
467              
468 0         0 return undef;
469             }
470              
471             =item B<_middle_value>
472              
473             Returns the value from the middle of an array reference. If that is
474             not defined we start from the beginning until we find a defined
475             value. Return undef if we can not find anything.
476              
477             =cut
478              
479             sub _middle_value {
480 15     15   33 my $arr = shift;
481 15         23 my $idx = shift;
482              
483 15 100       54 $idx = int ((scalar @$arr) / 2) unless defined $idx;
484              
485 15 100       55 return ($idx, $arr->[$idx]) if (defined $arr->[$idx]);
486              
487             # No luck scan them all
488 2         6 for my $idx (0..$#$arr) {
489 0         0 my $val = $arr->[$idx];
490 0 0       0 return ($idx, $val) if defined $val;
491             }
492 2         6 return (undef, undef);
493             }
494              
495             =back
496              
497             =head1 SEE ALSO
498              
499             C<Astro::FITS::HdrTrans>,
500             C<Astro::FITS::HdrTrans::Base>,
501             C<Astro::FITS::HdrTrans::JAC>.
502              
503             =head1 AUTHORS
504              
505             Anubhav E<lt>a.agarwal@jach.hawawii.eduE<gt>,
506             Brad Cavanagh E<lt>b.cavanagh@jach.hawaii.eduE<gt>,
507             Tim Jenness E<lt>t.jenness@jach.hawaii.eduE<gt>.
508              
509             =head1 COPYRIGHT
510              
511             Copyright (C) 2009, 2011, 2012, 2014 Science and Technology Facilities Council.
512             Copyright (C) 2016 East Asian Observatory.
513             All Rights Reserved.
514              
515             This program is free software; you can redistribute it and/or modify it under
516             the terms of the GNU General Public License as published by the Free Software
517             Foundation; either version 2 of the License, or (at your option) any later
518             version.
519              
520             This program is distributed in the hope that it will be useful,but WITHOUT ANY
521             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
522             PARTICULAR PURPOSE. See the GNU General Public License for more details.
523              
524             You should have received a copy of the GNU General Public License along with
525             this program; if not, write to the Free Software Foundation, Inc., 59 Temple
526             Place,Suite 330, Boston, MA 02111-1307, USA
527              
528             =cut
529              
530             1;