The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Astro::FITS::HdrTrans::JCMT_GSD_DB;

=head1 NAME

Astro::FITS::HdrTrans::JCMT_GSD_DB - JCMT GSD Database header translations

=head1 DESCRIPTION

Converts information contained in JCMT heterodyne database headers
to and from generic headers. See Astro::FITS::HdrTrans for a list of
generic headers.

=cut

use 5.006;
use warnings;
use strict;
use Carp;

use Time::Piece;

# Inherit from Base
use base qw/ Astro::FITS::HdrTrans::Base /;

use vars qw/ $VERSION /;

$VERSION = "1.50";

# for a constant mapping, there is no FITS header, just a generic
# header that is constant
my %CONST_MAP = (
                 INST_DHS         => 'HET_GSD',
                 COORDINATE_UNITS => 'decimal',
                 EQUINOX          => 'current',
                 TELESCOPE        => 'JCMT',
                );

# NULL mappings used to override base class implementations
my @NULL_MAP = ();

# unit mapping implies that the value propogates directly
# to the output with only a keyword name change

my %UNIT_MAP = (
                AMBIENT_TEMPERATURE => "TAMB",
                APERTURE => "APERTURE",
                AZIMUTH_START => "AZ",
                BACKEND => "BACKEND",
                BACKEND_SECTIONS => "NORSECT",
                CHOP_FREQUENCY => "CHOPFREQ",
                CHOP_THROW => "CHOPTHRW",
                COORDINATE_SYSTEM => "COORDCD",
                #        COORDINATE_TYPE => "C4LSC",
                CYCLE_LENGTH => "CYCLLEN",
                #        DEC_BASE => "",
                ELEVATION_START => "EL",
                FILENAME => "GSDFILE",
                FILTER => "FILTER",
                FREQUENCY_RESOLUTION => "FREQRES",
                FRONTEND => "FRONTEND",
                HUMIDITY => "HUMIDITY",
                NUMBER_OF_CYCLES => "NOCYCLES",
                NUMBER_OF_SUBSCANS => "NOSCANS",
                OBJECT => "OBJECT",
                OBSERVATION_MODE => "OBSMODE",
                OBSERVATION_NUMBER => "SCAN",
                PROJECT => "PROJID",
                #        RA_BASE => "C4RADATE",
                RECEIVER_TEMPERATURE => "TRX",
                ROTATION => "YPOSANG",
                REST_FREQUENCY => "RESTFRQ1",
                SEEING => "PHA",
                SWITCH_MODE => "SWMODE",
                SYSTEM_TEMPERATURE => "STSYS",
                TAU => "TAU",
                USER_AZ_CORRECTION => "UXPNT",
                USER_EL_CORRECTION => "UYPNT",
                VELOCITY => "VELOCITY",
                VELOCITY_REFERENCE_FRAME => "VREF",
                VELOCITY_TYPE => "VDEF",
                X_BASE => "XREF",
                Y_BASE => "YREF",
                X_DIM => "NOXPTS",
                Y_DIM => "NOYPTS",
                X_REQUESTED => "XSOURCE",
                Y_REQUESTED => "YSOURCE",
                X_SCALE => "DELTAX",
                Y_SCALE => "DELTAY",
               );

# Create the translation methods
__PACKAGE__->_generate_lookup_methods( \%CONST_MAP, \%UNIT_MAP, \@NULL_MAP );

=head1 METHODS

=over 4

=item B<can_translate>

Returns true if the supplied headers can be handled by this class.

  $cando = $class->can_translate( \%hdrs );

For this class, the method will return true if the "GSDFILE" header
exists and the "SCA#" header exist.

=cut

sub can_translate {
  my $self = shift;
  my $headers = shift;

  if (exists $headers->{GSDFILE} && exists $headers->{"SCA#"}) {
    return 1;
  }
  return 0;
}

=back

=head1 COMPLEX CONVERSIONS

These methods are more complicated than a simple mapping. We have to
provide both from- and to-FITS conversions All these routines are
methods and the to_ routines all take a reference to a hash and return
the translated value (a many-to-one mapping) The from_ methods take a
reference to a generic hash and return a translated hash (sometimes
these are many-to-many)

=over 4

=item B<to_INSTRUMENT>

Sets the C<INSTRUMENT> generic header. For RxA3i, sets the value
to RXA3. For RxB, sets the value to RXB3.

=cut

sub to_INSTRUMENT {
  my $self = shift;
  my $FITS_headers = shift;
  my $return;

  if ( exists( $FITS_headers->{'FRONTEND'} ) ) {
    $return = $FITS_headers->{'FRONTEND'};
    if ( $return =~ /^rxa3/i ) {
      $return = "RXA3";
    } elsif ( $return =~ /^rxb/i ) {
      $return = "RXB3";
    }
  }
  return $return;
}

=item B<to_OBSERVATION_ID>

Calculate a unique Observation ID.

=cut

# Note this routine is generic for JCMT heterodyne instrumentation.
# Would be completely generic if BACKEND was not used in preference to instrument.

sub to_OBSERVATION_ID {
  my $self = shift;
  my $FITS_headers = shift;
  my $backend = lc( $self->to_BACKEND( $FITS_headers ) );
  my $obsnum = $self->to_OBSERVATION_NUMBER( $FITS_headers );
  my $dateobs = $self->to_UTSTART( $FITS_headers );
  my $datetime = $dateobs->datetime;
  $datetime =~ s/-//g;
  $datetime =~ s/://g;

  my $obsid = join('_', $backend, $obsnum, $datetime);
  return $obsid;
}

=item B<to_UTDATE>

Translates the C<DATE_OBS> or C<LONGDATEOBS> header into a
YYYYMMDD integer.

=cut

sub to_UTDATE {
  my $self = shift;
  my $FITS_headers = shift;
  my $date = _sybase_convert_date( _date_header( $FITS_headers ), 1);
  return $date->strftime('%Y%m%d');
}

=item B<to_UTSTART>

Translates the DB date header into a C<Time::Piece> object.

=cut

sub to_UTSTART {
  my $self = shift;
  my $FITS_headers = shift;
  return _sybase_convert_date( _date_header( $FITS_headers ));
}

=item B<to_UTEND>

Translates the database date header into a C<Time::Piece> object and adds 
on the exposure time.

=cut

sub to_UTEND {
  my $self = shift;
  my $FITS_headers = shift;

  my $return = _sybase_convert_date( _date_header( $FITS_headers ) );
  return undef unless defined $return;
  my $expt = $self->to_EXPOSURE_TIME( $FITS_headers );

  $return += $expt;
  return $return;
}

=item B<to_BANDWIDTH_MODE>

Uses the NORSECT (number of backend sections), NOFCHAN (number of
frontend output channels) and NOBCHAN (number of channels) to form a
string that is of the format 250MHzx2048. To obtain this, the
bandwidth (250MHz in this example) is calculated as 125MHz * NORSECT /
NOFCHAN. The number of channels is taken directly and not manipulated
in any way.

If appropriate, the bandwidth may be given in GHz.

=cut

sub to_BANDWIDTH_MODE {
  my $self = shift;
  my $FITS_headers = shift;

  my $return;

  if ( exists( $FITS_headers->{'NORSECT'} ) && defined( $FITS_headers->{'NORSECT'} ) &&
       exists( $FITS_headers->{'NOFCHAN'} ) && defined( $FITS_headers->{'NOFCHAN'} ) &&
       exists( $FITS_headers->{'NOBCHAN'} ) && defined( $FITS_headers->{'NOBCHAN'} ) ) {

    my $bandwidth = 125 * $FITS_headers->{'NORSECT'} / $FITS_headers->{'NOFCHAN'};

    if ( $bandwidth >= 1000 ) {
      $bandwidth /= 1000;
      $return = sprintf( "%dGHzx%d", $bandwidth, $FITS_headers->{'NOBCHAN'} );
    } else {
      $return = sprintf( "%dMHzx%d", $bandwidth, $FITS_headers->{'NOBCHAN'} );
    }
  }

  return $return;

}


=item B<to_EXPOSURE_TIME>

=cut

sub to_EXPOSURE_TIME {
  my $self = shift;
  my $FITS_headers = shift;
  my $expt = 0;

  if ( exists( $FITS_headers->{'OBSMODE'} ) && defined( $FITS_headers->{'OBSMODE'} ) ) {

    my $obsmode = uc( $FITS_headers->{'OBSMODE'} );

    if ( $obsmode eq 'RASTER' ) {

      if ( exists( $FITS_headers->{'NSCAN'} ) && defined( $FITS_headers->{'NSCAN'} ) &&
           exists( $FITS_headers->{'CYCLLEN'} ) && defined( $FITS_headers->{'CYCLLEN'} ) &&
           exists( $FITS_headers->{'NOCYCPTS'} ) && defined( $FITS_headers->{'NOCYCPTS'} ) ) {

        my $nscan = $FITS_headers->{'NSCAN'};
        my $cycllen = $FITS_headers->{'CYCLLEN'};
        my $nocycpts = $FITS_headers->{'NOCYCPTS'};

        # raster.
        $expt = 15 + $nscan * $cycllen * ( 1 + 1 / sqrt( $nocycpts ) ) * 1.4;
      }
    } elsif ( $obsmode eq 'PATTERN' or $obsmode eq 'GRID' ) {

      my $swmode = '';

      if ( exists( $FITS_headers->{'SWMODE'} ) && defined( $FITS_headers->{'SWMODE'} ) ) {
        $swmode = $FITS_headers->{'SWMODE'};
      } else {
        $swmode = 'BEAMSWITCH';
      }

      if ( exists( $FITS_headers->{'NSCAN'} ) && defined( $FITS_headers->{'NSCAN'} ) &&
           exists( $FITS_headers->{'NCYCLE'} ) && defined( $FITS_headers->{'NCYCLE'} ) &&
           exists( $FITS_headers->{'CYCLLEN'} ) && defined( $FITS_headers->{'CYCLLEN'} ) ) {

        my $nscan = $FITS_headers->{'NSCAN'};
        my $ncycle = $FITS_headers->{'NCYCLE'};
        my $cycllen = $FITS_headers->{'CYCLLEN'};

        if ( $swmode eq 'POSITION_SWITCH' ) {

          # position switch pattern/grid.
          $expt = 6 + $nscan * $ncycle * $cycllen * 1.35;

        } elsif ( $swmode eq 'BEAMSWITCH' ) {

          # beam switch pattern/grid.
          $expt = 6 + $nscan * $ncycle * $cycllen * 1.35;

        } elsif ( $swmode eq 'CHOPPING' ) {
          if ( exists( $FITS_headers->{'FRONTEND'} ) && defined( $FITS_headers->{'FRONTEND'} ) ) {
            my $frontend = uc( $FITS_headers->{'FRONTEND'} );
            if ( $frontend eq 'RXA3I' ) {

              # fast frequency switch pattern/grid, receiver A.
              $expt = 15 + $nscan * $ncycle * $cycllen * 1.20;

            } elsif ( $frontend eq 'RXB' ) {

              # slow frequency switch pattern/grid, receiver B.
              $expt = 18 + $nscan * $ncycle * $cycllen * 1.60;

            }
          }
        }
      }
    } else {

      my $swmode;
      if ( exists( $FITS_headers->{'SWMODE'} ) && defined( $FITS_headers->{'SWMODE'} ) ) {
        $swmode = uc( $FITS_headers->{'SWMODE'} );
      } else {
        $swmode = 'BEAMSWITCH';
      }

      if ( exists( $FITS_headers->{'NSCAN'} ) && defined( $FITS_headers->{'NSCAN'} ) &&
           exists( $FITS_headers->{'NCYCLE'} ) && defined( $FITS_headers->{'NCYCLE'} ) &&
           exists( $FITS_headers->{'CYCLLEN'} ) && defined( $FITS_headers->{'CYCLLEN'} ) ) {

        my $nscan = $FITS_headers->{'NSCAN'};
        my $ncycle = $FITS_headers->{'NCYCLE'};
        my $cycllen = $FITS_headers->{'CYCLLEN'};

        if ( $swmode eq 'POSITION_SWITCH' ) {

          # position switch sample.
          $expt = 4.8 + $nscan * $ncycle * $cycllen * 1.10;

        } elsif ( $swmode eq 'BEAMSWITCH' ) {

          # beam switch sample.
          $expt = 4.8 + $nscan * $ncycle * $cycllen * 1.25;

        } elsif ( $swmode eq 'CHOPPING' ) {
          if ( exists( $FITS_headers->{'FRONTEND'} ) && defined( $FITS_headers->{'FRONTEND'} ) ) {
            my $frontend = uc( $FITS_headers->{'FRONTEND'} );
            if ( $frontend eq 'RXA3I' ) {

              # fast frequency switch sample, receiver A.
              $expt = 3 + $nscan * $ncycle * $cycllen * 1.10;

            } elsif ( $frontend eq 'RXB' ) {

              # slow frequency switch sample, receiver B.
              $expt = 3 + $nscan * $ncycle * $cycllen * 1.40;
            }
          }
        }
      }
    }
  }

  return $expt;
}

=item B<to_SYSTEM_VELOCITY>

Translate the C<VREF> and C<C12VDEF> headers into one combined header.

=cut

sub to_SYSTEM_VELOCITY {
  my $self = shift;
  my $FITS_headers = shift;
  my $return;
  if ( exists( $FITS_headers->{'VREF'} ) && defined( $FITS_headers->{'VREF'} ) &&
       exists( $FITS_headers->{'VDEF'} ) && defined( $FITS_headers->{'VDEF'} ) ) {
    $return = uc( substr( $FITS_headers->{'VDEF'}, 0, 3 ) . substr( $FITS_headers->{'VREF'}, 0, 3 ) );
  }
  return $return;
}

=back

=begin __PRIVATE

=over 4

=item B<_date_header>

Works out which header corresponds to the date field in sybase format and returns the value.

  $value = _date_header( $FITS_headers );

Returns undef if none found.

=cut

sub _date_header {
  my $FITS_headers = shift;
  for my $key (qw/ LONGDATEOBS DATE_OBS LONGDATE/ ) {
    if (exists $FITS_headers->{$key} && defined $FITS_headers->{$key}) {
      return $FITS_headers->{$key};
    }
  }
  return;
}

=item B<_sybase_convert_date>

Converts a sybase long date to Time::Piece object.

  $date = _sybase_convert_date( $string );

Optional flag can be set to true to drop Hours, minutes and seconds from parse.

  $utday = _sybase_convert_date( $string, 1);

Returns undef if no string is supplied.

=cut

sub _sybase_convert_date {
  my $longdate = shift;
  my $drophms = shift;
  return undef unless $longdate;

  # The UT header is in Sybase format, which is something like
  # "Mar 15 2002  7:04:35:234AM   ". We first need to remove the number
  # of milliseconds, then the whitespace at the end, then use the
  # "%b%t%d%t%Y%t%I:%M:%S%p" format.

  # General cleanup
  $longdate =~ s/:\d\d\d//;
  $longdate =~ s/\s*$//;

  my $return;
  if ($drophms) {
    $longdate =~ s/\s*\d\d?:\d\d:\d\d[A|P]M$//;
    $return = Time::Piece->strptime( $longdate,
                                     "%b %e %Y" );
  } else {
    $return = Time::Piece->strptime( $longdate,
                                     "%b %e %Y %l:%M:%S%p" );
  }
  return $return;
}

=back

=end __PRIVATE

=head1 REVISION

$Id$

=head1 AUTHOR

Brad Cavanagh E<lt>b.cavanagh@jach.hawaii.eduE<gt>,
Tim Jenness E<lt>t.jenness@jach.hawaii.eduE<gt>

=head1 COPYRIGHT

Copyright (C) 2008 Science and Technology Facilities Council.
Copyright (C) 2003-2007 Particle Physics and Astronomy Research Council.
All Rights Reserved.

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful,but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place,Suite 330, Boston, MA  02111-1307, USA

=cut

1;