package Astro::FITS::HdrTrans::SPEX;

=head1 NAME

Astro::FITS::HdrTrans::SPEX - IRTF SPEX translations

=head1 SYNOPSIS

  use Astro::FITS::HdrTrans::SPEX;

  %gen = Astro::FITS::HdrTrans::SPEX->translate_from_FITS( %hdr );

=head1 DESCRIPTION

This class provides a generic set of translations that are specific to
the SPEX camera of the IRTF.

=cut

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

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

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 = (
                 # Value in headers is too imprecise
                 DEC_SCALE           => (-0.1182/3600.0),
                 DETECTOR_READ_TYPE  => 'NDSTARE',
                 GAIN                => 13.0,
                 OBSERVATION_MODE    => 'imaging',
                 NSCAN_POSITIONS     => 1,
                 # Value in headers is too imprecise
                 RA_SCALE            => (-0.116/3600.0),
                 ROTATION            => -1.03,
                 SPEED_GAIN => 'Normal',
                );

# NULL mappings used to override base class implementations
my @NULL_MAP = qw/ /;

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

my %UNIT_MAP = (
                EXPOSURE_TIME        => "ITIME",
                FILTER               => "GFLT",
                OBJECT               => 'OBJECT',
               );


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

=head1 METHODS

=over 4

=item B<this_instrument>

The name of the instrument required to match (case insensitively)
against the INSTRUME/INSTRUMENT keyword to allow this class to
translate the specified headers. Called by the default
C<can_translate> method.

  $inst = $class->this_instrument();

Returns "INGRID".

=cut

sub this_instrument {
  return qr/^SPEX/i;
}

=back

=head1 COMPLEX CONVERSIONS

=over 4

=cut

sub to_AIRMASS_START {
  my $self = shift;
  my $FITS_headers = shift;
  my $airmass = 1.0;
  if ( defined( $FITS_headers->{AIRMASS} ) ) {
    $airmass = $FITS_headers->{AIRMASS};
  }
  return $airmass;
}

sub to_AIRMASS_END {
  my $self = shift;
  my $FITS_headers = shift;
  my $airmass = 1.0;
  if ( defined( $FITS_headers->{AIRMASS} ) ) {
    $airmass = $FITS_headers->{AIRMASS};
  }
  return $airmass;
}

sub from_AIRMASS_END {
  my $self = shift;
  my $generic_headers = shift;
  "AMEND",  $generic_headers->{ "AIRMASS_END" };
}

# Convert from sexagesimal d:m:s to decimal degrees.
sub to_DEC_BASE {
  my $self = shift;
  my $FITS_headers = shift;
  my $dec = 0.0;
  my $sexa = $FITS_headers->{"DECBASE"};
  if ( defined( $sexa ) ) {
    $dec = $self->dms_to_degrees( $sexa );
  }
  return $dec;
}

# Assume that the initial offset is 0.0, i.e. the base is the
# source position.  This also assumes that the reference pixel
# is unchanged in the group, as is created in the conversion
# script.  The other headers are measured in sexagesimal, but 
# the offsets are in arcseconds.
sub to_DEC_TELESCOPE_OFFSET {
  my $self = shift;
  my $FITS_headers = shift;
  my $offset;
  my $base = $self->to_DEC_BASE($FITS_headers);

  # Convert from sexagesimal d:m:s to decimal degrees.
  my $sexadec = $FITS_headers->{DEC};
  if ( defined( $sexadec ) ) {
    my $dec = $self->dms_to_degrees( $sexadec );

    # The offset is arcseconds with respect to the base position.
    $offset = 3600.0 * ( $dec - $base );
  } else {
    $offset = 0.0;
  }
  return $offset;
}

sub to_DR_RECIPE {
  my $self = shift;
  my $FITS_headers = shift;
  my $recipe = "JITTER_SELF_FLAT";
  if ( $self->to_OBSERVATION_TYPE($FITS_headers) eq "DARK" ) {
    $recipe = "REDUCE_DARK";
  } elsif (  $self->to_STANDARD($FITS_headers) ) {
    $recipe = "JITTER_SELF_FLAT_APHOT";
  }
  return $recipe;
}


sub to_NUMBER_OF_EXPOSURES {
  my $self = shift;
  my $FITS_headers = shift;
  my $coadds = 1;
  if ( defined $FITS_headers->{CO_ADDS} ) {
    $coadds = $FITS_headers->{CO_ADDS};
  }
   
}

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

  # Allow for the UKIRT convention of the final offset to 0,0, and a
  # default dither pattern of 5.
  my $noffsets = 6;

  # The number of gripu members appears to be given by keyword LOOP.
  if ( defined $FITS_headers->{NOFFSETS} ) {
    $noffsets = $FITS_headers->{NOFFSETS};
  }

  return $noffsets;
}

sub to_OBSERVATION_TYPE {
  my $self = shift;
  my $FITS_headers = shift;
  my $type = "OBJECT";
  if ( defined $FITS_headers->{OBJECT} && defined $FITS_headers->{GFLT}) {
    my $object = uc( $FITS_headers->{OBJECT} );
    my $filter = uc( $FITS_headers->{GFLT} );
    if ( $filter =~ /blank/i ) {
      $type = "DARK";
    } elsif ( $object =~ /flat/i ) {
      $type = "FLAT";
    }
  }
  return $type;
}

# Convert from sexagesimal h:m:s to decimal degrees then to decimal
# hours.
sub to_RA_BASE {
  my $self = shift;
  my $FITS_headers = shift;
  my $ra = 0.0;
  my $sexa = $FITS_headers->{"RABASE"};
  if ( defined( $sexa ) ) {
    $ra = $self->hms_to_degrees( $sexa );
  }
  return $ra;
}

# Assume that the initial offset is 0.0, i.e. the base is the
# source position.  This also assumes that the reference pixel
# is unchanged in the group, as is created in the conversion
# script.  The other headers are measured in sexagesimal, but
# the offsets are in arcseconds.
sub to_RA_TELESCOPE_OFFSET {
  my $self = shift;
  my $FITS_headers = shift;
  my $offset;

  # Base RA is in degrees.
  my $base = $self->to_RA_BASE($FITS_headers);

  # Convert from sexagesimal right ascension h:m:s and declination
  # d:m:s to decimal degrees.
  my $sexara = $FITS_headers->{RA};
  my $sexadec = $FITS_headers->{DEC};
  if ( defined( $base ) && defined( $sexara ) && defined( $sexadec ) ) {
    my $dec = $self->dms_to_degrees( $sexadec );
    my $ra = $self->hms_to_degrees( $sexara );

    # The offset is arcseconds with respect to the base position.
    $offset = 3600.0 * ( $ra - $base ) * $self->cosdeg( $dec );
  } else {
    $offset = 0.0;
  }
  return $offset;
}

# Take a pragmatic way of defining a standard.  Not perfect, but
# should suffice unitl we know all the names.
sub to_STANDARD {
  my $self = shift;
  my $FITS_headers = shift;
  my $standard = 0;
  my $object = $FITS_headers->{"OBJECT"};
  if ( defined( $object ) && $object =~ /^FS/ ) {
    $standard = 1;
  }
  return $standard;
}

# Allow for multiple occurences of the date, the first being valid and
# the second is blank.
sub to_UTDATE {
  my $self = shift;
  my $FITS_headers = shift;
  my $utdate;
  if ( exists $FITS_headers->{"DATE-OBS"} ) {
    $utdate = $FITS_headers->{"DATE-OBS"};

    # This is a kludge to work with old data which has multiple values of
    # the DATE keyword with the last value being blank (these were early
    # SPEX data).  Return the first value, since the last value can be
    # blank.
    if ( ref( $utdate ) eq 'ARRAY' ) {
      $utdate = $utdate->[0];
    }
  } elsif (exists $FITS_headers->{'DATE_OBS'}) {
    $utdate = $FITS_headers->{'DATE_OBS'};
  }
  $utdate =~ s/-//g if $utdate;
  return $utdate;
}

# Derive from the start time, plus the exposure time and some
# allowance for the read time taken from 
# http://irtfweb.ifa.hawaii.edu/~spex
# http://irtfweb.ifa.hawaii.edu/Facility/spex/work/array_params/array_params.html
sub to_UTEND {
  my $self = shift;
  my $FITS_headers = shift;
  my $utend = $self->to_UTSTART($FITS_headers);
  if ( defined $FITS_headers->{ITIME} && defined $FITS_headers->{NDR} ) {
    $utend += ( $FITS_headers->{ITIME} * $FITS_headers->{NDR}) ;
  }
  return $utend;
}

sub to_UTSTART {
  my $self = shift;
  my $FITS_headers = shift;
  my $base = $self->to_UTDATE( $FITS_headers );
  return unless defined $base;
  if (exists $FITS_headers->{TIME_OBS}) {
    my $ymd = substr($base,0,4). "-". substr($base,4,2)."-". substr($base,6,2);
    my $iso = $ymd. "T" . $FITS_headers->{TIME_OBS};
    return $self->_parse_iso_date( $iso );
  }
  return;
}

sub to_X_LOWER_BOUND {
  my $self = shift;
  my $FITS_headers = shift;
  my @bounds = $self->get_bounds($FITS_headers);
  return $bounds[ 0 ];
}

# Specify the reference pixel, which is normally near the frame centre.
sub to_X_REFERENCE_PIXEL{
  my $self = shift;
  my $FITS_headers = shift;
  my $xref;

  # Use the average of the bounds to define the centre and dimension.
  my @bounds = $self->get_bounds($FITS_headers);
  my $xdim = $bounds[ 2 ] - $bounds[ 0 ] + 1;
  my $xmid = $self->nint( ( $bounds[ 2 ] + $bounds[ 0 ] ) / 2 );

  # SPEX is at the centre for a sub-array along an axis but offset slightly
  # for a sub-array to avoid the joins between the four sub-array sections
  # of the frame.  Ideally these should come through the headers...
  if ( $xdim == 512 ) {
    $xref = $xmid - 36;
  } else {
    $xref = $xmid;
  }
  return $xref;
}

sub from_X_REFERENCE_PIXEL {
  my $self = shift;
  my $generic_headers = shift;
  "CRPIX1", $generic_headers->{"X_REFERENCE_PIXEL"};
}

sub to_X_UPPER_BOUND {
  my $self = shift;
  my $FITS_headers = shift;
  my @bounds = $self->get_bounds( $FITS_headers );
  return $bounds[ 2 ];
}

sub to_Y_LOWER_BOUND {
  my $self = shift;
  my $FITS_headers = shift;
  my @bounds = $self->get_bounds( $FITS_headers );
  return $bounds[ 1 ];
}

# Specify the reference pixel, which is normally near the frame centre.
sub to_Y_REFERENCE_PIXEL{
  my $self = shift;
  my $FITS_headers = shift;
  my $yref;

  # Use the average of the bounds to define the centre and dimension.
  my @bounds = $self->get_bounds($FITS_headers);
  my $ydim = $bounds[ 3 ] - $bounds[ 1 ] + 1;
  my $ymid = $self->nint( ( $bounds[ 3 ] + $bounds[ 1 ] ) / 2 );

  # SPEX is at the centre for a sub-array along an axis but offset slightly
  # for a sub-array to avoid the joins between the four sub-array sections
  # of the frame.  Ideally these should come through the headers...
  if ( $ydim == 512 ) {
    $yref = $ymid - 40;
  } else {
    $yref = $ymid;
  }

  return $yref;
}

sub from_Y_REFERENCE_PIXEL {
  my $self = shift;
  my $generic_headers = shift;
  "CRPIX2", $generic_headers->{"Y_REFERENCE_PIXEL"};
}

sub to_Y_UPPER_BOUND {
  my $self = shift;
  my $FITS_headers = shift;
  my @bounds = $self->get_bounds( $FITS_headers );
  return $bounds[ 3 ];
}

# Supplementary methods for the translations
# ------------------------------------------

# Converts a sky angle specified in d:m:s format into decimal degrees.
# Argument is the sexagesimal format angle.
sub dms_to_degrees {
  my $self = shift;
  my $sexa = shift;
  my $dms;
  if ( defined( $sexa ) ) {
    my @pos = split( /:/, $sexa );
    $dms = $pos[ 0 ] + $pos[ 1 ] / 60.0 + $pos [ 2 ] / 3600.;
  }
  return $dms;
}

sub get_bounds {
  my $self = shift;
  my $FITS_headers = shift;
  my @bounds = ( 1, 1, 512, 512 );
  if ( exists $FITS_headers->{ARRAY0} ) {
    my $boundlist = $FITS_headers->{ARRAY0};
    @bounds = split( ",", $boundlist );

    # Bounds count from zero.
    $bounds[ 0 ]++;
    $bounds[ 1 ]++;
  }
  return @bounds;
}
   
# Returns the UT date in yyyyMMdd format.
sub get_UT_date {
  my $self = shift;
  my $FITS_headers = shift;
  my $date = $FITS_headers->{"DATE-OBS"};
  $date =~ s/-//g;
  return $date;
}

# Returns the UT time of observation in decimal hours.
sub get_UT_hours {
  my $self = shift;
  my $FITS_headers = shift;
  if ( exists $FITS_headers->{"TIME-OBS"} && $FITS_headers->{"TIME-OBS"} =~ /:/ ) {
    my ($hour, $minute, $second) = split( /:/, $FITS_headers->{"TIME-OBS"} );
    return $hour + ($minute / 60) + ($second / 3600);
  } else {
    return $FITS_headers->{"TIME-OBS"};
  }
}

# Converts a sky angle specified in h:m:s format into decimal degrees.
# It takes no account of latitude.  Argument is the sexagesimal format angle.
sub hms_to_degrees {
  my $self = shift;
  my $sexa = shift;
  my $hms;
  if ( defined( $sexa ) ) {
    my @pos = split( /:/, $sexa );
    $hms = 15.0 * ( $pos[ 0 ] + $pos[ 1 ] / 60.0 + $pos [ 2 ] / 3600. );
  }
  return $hms;
}


=back

=head1 REVISION

 $Id: SOFI.pm 14879 2008-02-13 21:51:31Z timj $

=head1 SEE ALSO

C<Astro::FITS::HdrTrans>, C<Astro::FITS::HdrTrans::UKIRT>.

=head1 AUTHOR

Malcolm J. Currie E<lt>mjc@star.rl.ac.ukE<gt>,
Tim Jenness E<lt>t.jenness@jach.hawaii.eduE<gt>.

=head1 COPYRIGHT

Copyright (C) 2008 Science and Technology Facilities Council.
Copyright (C) 2003-2005 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;
