// $Id: DMSPHeader.java,v 1.4 2004/12/07 23:27:09 edavis Exp $
package ucar.nc2.iosp.dmsp;

import ucar.nc2.Attribute;
import ucar.nc2.Dimension;

import java.io.IOException;
import java.util.*;
import java.text.SimpleDateFormat;
import java.text.ParseException;

/**
 * A description
 * <p/>
 * User: edavis
 * Date: Aug 6, 2004
 * Time: 3:38:11 PM
 */
public class DMSPHeader
{
  private String[] header = null;
  private HashMap headerInfo = new HashMap();
  private int headerSizeInBytes = 0;
  private int headerSizeInBytesGuess = 5000;

  private ucar.unidata.io.RandomAccessFile raFile;
  private ucar.nc2.NetcdfFile ncFile;
  private long actualSize;

  // File information
  private String fileIdAttName = "fileId";
  private Attribute fileIdAtt = null;

  private String datasetIdAttName = "datasetId";
  private Attribute datasetIdAtt = null;

  private int recordSizeInBytes = 0;

  private int numHeaderRecords = 0;
  private int numDataRecords = 0;

  private String numDataRecordsDimName = "numScans";
  private Dimension numDataRecordsDim = null;

  private int numArtificialDataRecords = 0;
  private int numRecords = 0;

  // Processing information
  private String suborbitHistoryAttName = "suborbitHistory";
  private Attribute suborbitHistoryAtt = null;

  private String processingSystemAttName = "processingSystem";
  private Attribute processingSystemAtt = null;

  private Date processingDate = null;

  private String processingDateAttName = "processingDate";
  private Attribute processingDateAtt = null;

  // Satellite information
  private String spacecraftIdAttName = "spacecraftId";
  private Attribute spacecraftIdAtt = null;

  private String noradIdAttName = "noradId";
  private Attribute noradIdAtt = null;

  // Orbit information
  private String startDateAttName = "startDate";
  private Attribute startDateAtt = null;
  private Date startDate = null;

  private String endDateAttName = "endDate";
  private Attribute endDateAtt = null;
  private Date endDate = null;

  private String startDateLocalAttName = "startDateLocal";
  private Attribute startDateLocalAtt = null;

  private String startTimeLocalAttName = "startTimeLocal";
  private Attribute startTimeLocalAtt = null;

  private String startLatitudeAttName = "startLatitude";
  private Attribute startLatitudeAtt = null;

  private String startLongitudeAttName = "startLongitude";
  private Attribute startLongitudeAtt = null;

  private String endLatitudeAttName = "endLatitude";
  private Attribute endLatitudeAtt = null;

  private String endLongitudeAttName = "endLongitude";
  private Attribute endLongitudeAtt = null;

  private String startSubsolarCoordsAttName = "startSubsolarCoords";
  private Attribute startSubsolarCoordsAtt = null;

  private String endSubsolarCoordsAttName = "endSubsolarCoords";
  private Attribute endSubsolarCoordsAtt = null;

  private String startLunarCoordsAttName = "startLunarCoords";
  private Attribute startLunarCoordsAtt = null;

  private String endLunarCoordsAttName = "endLunarCoords";
  private Attribute endLunarCoordsAtt = null;

  private String ascendingNodeAttName = "ascendingNode";
  private Attribute ascendingNodeAtt = null;

  private String nodeHeadingAttName = "nodeHeading";
  private Attribute nodeHeadingAtt = null;

  // Sensor information
  private int numSamplesPerBand = 0;
  private String numSamplesPerBandDimName = "numSamplesPerScan";
  private Dimension numSamplesPerBandDim = null;

  private String nominalResolutionAttName = "nominalResolution";
  private Attribute nominalResolutionAtt = null;

  private String bandsPerScanlineAttName = "bandsPerScanline";
  private Attribute bandsPerScanlineAtt = null;

  private String bytesPerSampleAttName = "bytesPerSample";
  private Attribute bytesPerSampleAtt = null;

  private String byteOffsetBand1AttName = "byteOffsetBand1";
  private Attribute byteOffsetBand1Att = null;

  private String byteOffsetBand2AttName = "byteOffsetBand2";
  private Attribute byteOffsetBand2Att = null;

  private String band1AttName = "band1";
  private Attribute band1Att = null;

  private String band2AttName = "band2";
  private Attribute band2Att = null;

  private String bandOrganizationAttName = "bandOrganization";
  private Attribute bandOrganizationAtt = null;

  private String thermalOffsetAttName = "thermalOffset";
  private Attribute thermalOffsetAtt = null;

  private String thermalScaleAttName = "thermalScale";
  private Attribute thermalScaleAtt = null;

  private String percentDaylightAttName = "percentDaylight";
  private Attribute percentDaylightAtt = null;

  private String percentFullMoonAttName = "percentFullMoon";
  private Attribute percentFullMoonAtt = null;

  private String percentTerminatorEvidentAttName = "percentTerminatorEvident";
  private Attribute percentTerminatorEvidentAtt = null;

  // QC information
  private String qcFlagsAttName = "qcFlags";
  private Attribute qcFlagsAtt = null;


  public Attribute getFileIdAtt() { return( this.fileIdAtt ); }
  public Attribute getDatasetIdAtt() { return( this.datasetIdAtt ); }
  public int getNumHeaderRecords() { return( this.numHeaderRecords ); }
  public int getNumDataRecords() { return( this.numDataRecords ); }
  public Dimension getNumDataRecordsDim() { return( this.numDataRecordsDim ); }
  public Dimension getNumSamplesPerBandDim() { return( this.numSamplesPerBandDim ); }
  public int getRecordSizeInBytes() { return( this.recordSizeInBytes ); }

  public Attribute getSuborbitHistoryAtt() { return( this.suborbitHistoryAtt ); }
  public Attribute getProcessingSystemAtt() { return( this.processingSystemAtt ); }
  public Attribute getProcessingDateAtt() { return( this.processingDateAtt ); }
  public Attribute getStartDateAtt() { return( this.startDateAtt ); }


  /** Check basic DMSP file validity of given random access file. */
  boolean isValidFile( ucar.unidata.io.RandomAccessFile raFile )
  {
    // @todo This method should not be called if read() has or will be called on this instance.
    this.raFile = raFile;
    try
    {
      this.actualSize = raFile.length();
    }
    catch ( IOException e )
    {
      return( false );
    }
    try
    {
      this.readHeaderFromFile( raFile );

      this.handleFileInformation();
      this.handleProcessingInformation();
      this.handleSatelliteInformation();
      this.handleSensorInformation();
    }
    catch ( IOException e )
    {
      return( false );
    }
    return( true );
  }

  void read( ucar.unidata.io.RandomAccessFile raFile, ucar.nc2.NetcdfFile ncFile )
          throws IOException
  {
    this.raFile = raFile;
    this.ncFile = ncFile;
    actualSize = this.raFile.length();

    // Read the header from the file
    this.readHeaderFromFile( raFile );

    // Deal with file/record information.
    this.handleFileInformation();

    this.ncFile.addAttribute( null, fileIdAtt );
    this.ncFile.addAttribute( null, datasetIdAtt );
    this.ncFile.addDimension( null, numDataRecordsDim );

    // Deal with processing/history information.
    this.handleProcessingInformation();

    this.ncFile.addAttribute( null, suborbitHistoryAtt);
    this.ncFile.addAttribute( null, processingSystemAtt);
    this.ncFile.addAttribute( null, processingDateAtt);

    // Deal with satellite information
    this.handleSatelliteInformation();

    this.ncFile.addAttribute( null, spacecraftIdAtt );
    this.ncFile.addAttribute( null, noradIdAtt );

    // Deal with orbit information
    this.handleOrbitInformation();

    this.ncFile.addAttribute( null, startDateAtt);
    this.ncFile.addAttribute( null, endDateAtt);
    this.ncFile.addAttribute( null, startDateLocalAtt);
    this.ncFile.addAttribute( null, startTimeLocalAtt);
    this.ncFile.addAttribute( null, startLatitudeAtt);
    this.ncFile.addAttribute( null, startLongitudeAtt);
    this.ncFile.addAttribute( null, endLatitudeAtt);
    this.ncFile.addAttribute( null, endLongitudeAtt);
    this.ncFile.addAttribute( null, startSubsolarCoordsAtt);
    this.ncFile.addAttribute( null, endSubsolarCoordsAtt);
    this.ncFile.addAttribute( null, startLunarCoordsAtt);
    this.ncFile.addAttribute( null, endLunarCoordsAtt);
    this.ncFile.addAttribute( null, ascendingNodeAtt);
    this.ncFile.addAttribute( null, nodeHeadingAtt);

    // Deal with sensor information
    this.handleSensorInformation();

    this.ncFile.addDimension( null, numSamplesPerBandDim );
    this.ncFile.addAttribute( null, nominalResolutionAtt );
    this.ncFile.addAttribute( null, bandsPerScanlineAtt );
    this.ncFile.addAttribute( null, bytesPerSampleAtt );
    this.ncFile.addAttribute( null, byteOffsetBand1Att );
    this.ncFile.addAttribute( null, byteOffsetBand2Att );
    this.ncFile.addAttribute( null, band1Att );
    this.ncFile.addAttribute( null, band2Att );
    this.ncFile.addAttribute( null, bandOrganizationAtt );
    this.ncFile.addAttribute( null, thermalOffsetAtt );
    this.ncFile.addAttribute( null, thermalScaleAtt );
    this.ncFile.addAttribute( null, percentDaylightAtt );
    this.ncFile.addAttribute( null, percentFullMoonAtt );
    this.ncFile.addAttribute( null, percentTerminatorEvidentAtt );

    // Deal with QC information
    this.handleQCInformation();

    this.ncFile.addAttribute( null, qcFlagsAtt );

    // Add some general metadata in global attributes.
    this.ncFile.addAttribute( null, new Attribute( "title",
                                                   new StringBuffer("NGDC archived ")
                                                   .append( datasetIdAtt.getStringValue())
                                                   .append( " data with start time ")
                                                   .append( startDateAtt.getStringValue())
                                                   .toString()));
    this.ncFile.addAttribute( null, new Attribute( "Convention", "_Coordinates"));

    // Add some THREDDS specific metadata in global attributes.
    this.ncFile.addAttribute( null, new Attribute( "thredds_creator", "DOD/USAF/SMC > Space and Missile Systems Center (SMC), U.S. Air Force, U.S. Department of Defense"));
    this.ncFile.addAttribute( null, new Attribute( "thredds_contributor", "DOC/NOAA/NESDIS/NGDC > National Geophysical Data Center, NESDIS, NOAA, U.S. Department of Commerce"));
    this.ncFile.addAttribute( null, new Attribute( "thredds_contributor_role", "archive"));
    this.ncFile.addAttribute( null, new Attribute( "thredds_publisher", "DOC/NOAA/NESDIS/NGDC > National Geophysical Data Center, NESDIS, NOAA, U.S. Department of Commerce"));
    this.ncFile.addAttribute( null, new Attribute( "thredds_publisher_url", "http://dmsp.ngdc.noaa.gov/"));
    this.ncFile.addAttribute( null, new Attribute( "thredds_publisher_email", "ngdc.dmsp@noaa.gov"));
    this.ncFile.addAttribute( null, new Attribute( "thredds_summary",
                                                   new StringBuffer("This dataset contains data from the DMSP ").append( spacecraftIdAtt.getStringValue())
                                                   .append( " satellite OLS instrument and includes both visible smooth and thermal smooth imagery with 2.7km resolution.")
                                                   .append( " The start time for this data is ").append( startDateAtt.getStringValue())
                                                   .append( " and the northerly equatorial crossing longitude is ").append( startLongitudeAtt.getNumericValue())
                                                   .append( ".  The DMSP satellite is a polar-orbiting satellite crossing the equator, depending on the satellite, at either dawn/dusk or noon/midnight.")
                                                   .append( " This data is in the NOAA/NGDC DMSP archive format.")
                                                   .toString()));
    this.ncFile.addAttribute( null, new Attribute( "thredds_history", ""));
    this.ncFile.addAttribute( null, new Attribute( "thredds_timeCoverage_start", startDateAtt.getStringValue()));
    this.ncFile.addAttribute( null, new Attribute( "thredds_timeCoverage_end", endDateAtt.getStringValue()));
    this.ncFile.addAttribute( null, new Attribute( "thredds_geospatialCoverage",
                                                   new StringBuffer("Polar orbit with northerly equatorial crossing at longitude ")
                                                   .append( ascendingNodeAtt.getNumericValue()).append( ".")
                                                   .toString()));


    // Set position of random access file to just after header record(s).
    this.raFile.seek( this.headerSizeInBytes );
  }

  /**
   * Read the header information from the file into name/value pairs.
   *
   * @param raFile - the file to be read.
   * @throws IOException if any problems reading the file (or validating the file).
   */
  private void readHeaderFromFile( ucar.unidata.io.RandomAccessFile raFile )
          throws IOException
  {
    long pos = 0;
    raFile.seek( pos );

    // Read in first record.
    this.headerSizeInBytes = raFile.length() > this.headerSizeInBytesGuess ?
                             this.headerSizeInBytesGuess : (int) raFile.length();
    byte[] b = new byte[this.headerSizeInBytes];
    if ( raFile.read( b ) != this.headerSizeInBytes ) throw new IOException( "Invalid DMSP file: could not read first " + this.headerSizeInBytes + " bytes.");
    String fullHeader = new String( b );
    // Make sure header starts with the proper item.
    if ( ! fullHeader.startsWith( HeaderInfoTitle.FILE_ID.toString() ) )
    {
      throw new IOException( "Invalid DMSP file: header does not start with \"" + HeaderInfoTitle.FILE_ID.toString() + "\"." );
    }
    // Make sure header contains end-of-header marker.
    int endOfHeaderIndex = fullHeader.indexOf( HeaderInfoTitle.END_HEADER.toString() );
    if ( endOfHeaderIndex == -1 )
    {
      throw new IOException( "Invalid DMSP file: header does not end with \"" + HeaderInfoTitle.END_HEADER.toString() + "\"." );
    }

    // Drop the end-of-header marker and the line feed ('\n') proceeding it.
    header = fullHeader.substring( 0, endOfHeaderIndex - 1 ).split( "\n" );

    int lineSeperatorIndex = 0;
    String curHeaderLine = null;
    String curHeaderTitle = null;
    String curHeaderValue = null;
    for ( int i = 0; i < this.header.length; i++ )
    {
      curHeaderLine = this.header[i].trim();
      lineSeperatorIndex = curHeaderLine.indexOf( ':' );
      if ( lineSeperatorIndex == -1 ) throw new IOException( "Invalid DMSP file: header line <" + curHeaderLine + "> contains no seperator <:>." );
      if ( lineSeperatorIndex == 0 ) throw new IOException( "Invalid DMSP file: header line <" + curHeaderLine + "> contains no title." );
      if ( lineSeperatorIndex == curHeaderLine.length() - 1 ) throw new IOException( "Invalid DMSP file: header line <" + curHeaderLine + "> contains no value." );

      curHeaderTitle = curHeaderLine.substring( 0, lineSeperatorIndex ).trim();
      if ( HeaderInfoTitle.getTitle( curHeaderTitle ) == null ) throw new IOException( "Invalid DMSP file: header line <" + curHeaderLine + "> contains invalid title." );

      curHeaderValue = curHeaderLine.substring( lineSeperatorIndex + 1 ).trim();
      if ( curHeaderValue.equals( "")) throw new IOException( "Invalid DMSP file: header line <" + curHeaderLine + "> contains no value." );

      headerInfo.put( curHeaderTitle, curHeaderValue );
    }
  }

  /**
   * Parse the file header information about the file (e.g., file ID, dataset ID,
   * record size, number of records) and create netCDF attributes and dimensions
   * where appropriate.
   *
   * @throws IOException if any problems reading the file (or validating the file).
   */
  private void handleFileInformation()
          throws IOException
  {
    fileIdAtt = new Attribute( this.fileIdAttName,
                               (String) headerInfo.get( HeaderInfoTitle.FILE_ID.toString() ) );
    datasetIdAtt = new Attribute( this.datasetIdAttName,
                                  (String) headerInfo.get( HeaderInfoTitle.DATA_SET_ID.toString() ) );
    recordSizeInBytes = Integer.parseInt( (String) headerInfo.get( HeaderInfoTitle.RECORD_BYTES.toString() ) );
    numRecords = Integer.parseInt( (String) headerInfo.get( HeaderInfoTitle.NUM_RECORDS.toString() ) );
    numHeaderRecords = Integer.parseInt( (String) headerInfo.get( HeaderInfoTitle.NUM_HEADER_RECORDS.toString() ) );
    numDataRecords = Integer.parseInt( (String) headerInfo.get( HeaderInfoTitle.NUM_DATA_RECORDS.toString() ) );
    numDataRecordsDim = new Dimension( this.numDataRecordsDimName,
                                       numDataRecords, true, false, false );
    numArtificialDataRecords = Integer.parseInt( (String) headerInfo.get( HeaderInfoTitle.NUM_ARTIFICIAL_DATA_RECORDS.toString() ) );
    if ( numHeaderRecords + numDataRecordsDim.getLength() + numArtificialDataRecords != numRecords )
    {
      throw new IOException( "Invalid DMSP file: the number of header records <" + this.numHeaderRecords + ">, data records <" + this.numDataRecordsDim.getLength() + ">, and artificial data records <" + this.numArtificialDataRecords + "> is not equal to total records <" + this.numRecords + ">." );
    }
    this.headerSizeInBytes = this.numHeaderRecords * this.recordSizeInBytes;
    if ( numRecords * this.recordSizeInBytes != this.actualSize )
    {
      throw new IOException( "Invalid DMSP file: the number of records <" + this.numRecords + "> times the record size <" + this.recordSizeInBytes + "> does not equal the size of the file <" + this.actualSize + ">." );
    }
  }

  /**
   * Parse the processing/history information from the header.
   *
   * @throws IOException if any problems reading the file (or validating the file).
   */
  private void handleProcessingInformation()
          throws IOException
  {
    suborbitHistoryAtt = new Attribute( this.suborbitHistoryAttName,
                                        (String) headerInfo.get( HeaderInfoTitle.SUBORBIT_HISTORY.toString() ) );
    processingSystemAtt = new Attribute( this.processingSystemAttName,
                                         (String) headerInfo.get( HeaderInfoTitle.PROCESSING_SYSTEM.toString() ) );
    String processingDateString = (String) headerInfo.get( HeaderInfoTitle.PROCESSING_DATE.toString() );
    try
    {
      processingDate = DateFormatHandler.ALT_DATE_TIME.getDateFromDateTimeString( processingDateString );
    }
    catch ( ParseException e )
    {
      throw new IOException( "Invalid DMSP file: processing date string <" + processingDateString + "> not parseable: " + e.getMessage() );
    }
    processingDateAtt = new Attribute(
            this.processingDateAttName,
            DateFormatHandler.ISO_DATE_TIME.getDateTimeStringFromDate( processingDate ) );
  }

  /**
   * Parse the satellite information from the header.
   */
  private void handleSatelliteInformation()
  {
    spacecraftIdAtt = new Attribute(
            this.spacecraftIdAttName,
            (String) headerInfo.get( HeaderInfoTitle.SPACECRAFT_ID.toString() ) );
    noradIdAtt = new Attribute(
            this.noradIdAttName,
            (String) headerInfo.get( HeaderInfoTitle.NORAD_ID.toString() ) );
  }

  private void handleOrbitInformation() throws IOException
  {
    // Read the start date UTC information.
    String time = (String) headerInfo.get(  HeaderInfoTitle.START_TIME_UTC.toString());
    String startDateTimeUTC = (String) headerInfo.get(  HeaderInfoTitle.START_DATE_UTC.toString())
                              + "T" + time.substring( 0, time.indexOf( '.')+4)
                              + "GMT";
    try
    {
      this.startDate = DateFormatHandler.ISO_DATE_TIME.getDateFromDateTimeString( startDateTimeUTC);
    }
    catch ( ParseException e )
    {
      throw new IOException( "Invalid DMSP file: start date/time string <" + startDateTimeUTC + "> not parseable: " + e.getMessage() );
    }
    this.startDateAtt = new Attribute( this.startDateAttName,
                                       DateFormatHandler.ISO_DATE_TIME.getDateTimeStringFromDate( this.startDate));

    // Read the end date UTC information.
    time = (String) headerInfo.get(  HeaderInfoTitle.END_TIME_UTC.toString());
    String endDateTimeUTC = (String) headerInfo.get(  HeaderInfoTitle.END_DATE_UTC.toString())
                            + "T" + time.substring( 0, time.indexOf( '.')+4)
                            + "GMT";
    try
    {
      this.endDate = DateFormatHandler.ISO_DATE_TIME.getDateFromDateTimeString( endDateTimeUTC);
    }
    catch ( ParseException e )
    {
      throw new IOException( "Invalid DMSP file: end date/time string <" + endDateTimeUTC + "> not parseable: " + e.getMessage() );
    }
    this.endDateAtt = new Attribute( this.endDateAttName,
                                       DateFormatHandler.ISO_DATE_TIME.getDateTimeStringFromDate( this.endDate));

    // Read the local start/end date/time
    this.startDateLocalAtt = new Attribute( this.startDateLocalAttName,
                                            (String) HeaderInfoTitle.START_DATE_LOCAL.toString());
    this.startTimeLocalAtt = new Attribute( this.startTimeLocalAttName,
                                            (String) HeaderInfoTitle.START_TIME_LOCAL.toString());

    // Read the start latitude/longitude information.
    String startLatLon = (String) headerInfo.get(  HeaderInfoTitle.START_LAT_LON.toString());
    String[] latLon = startLatLon.split( " ");
    Double lat, lon;
    if ( latLon.length != 2) throw new IOException( "Invalid DMSP file: start lat/lon <" + startLatLon + "> invalid.");
    try
    {
      lat = Double.valueOf( latLon[0]);
      lon = Double.valueOf( latLon[1]);
    }
    catch ( NumberFormatException e )
    {
      throw new IOException( "Invalid DMSP file: start lat/lon string <" + startLatLon + "> not parseable: " + e.getMessage() );
    }
    this.startLatitudeAtt = new Attribute( this.startLatitudeAttName, lat);
    this.startLongitudeAtt = new Attribute( this.startLongitudeAttName, lon);

    // Read the end latitude/longitude information.
    String endLatLon = (String) headerInfo.get(  HeaderInfoTitle.END_LAT_LON.toString());
    latLon = endLatLon.split( " ");
    if ( latLon.length != 2) throw new IOException( "Invalid DMSP file: end lat/lon <" + endLatLon + "> invalid.");
    try
    {
      lat = Double.valueOf( latLon[0]);
      lon = Double.valueOf( latLon[1]);
    }
    catch ( NumberFormatException e )
    {
      throw new IOException( "Invalid DMSP file: end lat/lon string <" + endLatLon + "> not parseable: " + e.getMessage() );
    }
    this.endLatitudeAtt = new Attribute( this.endLatitudeAttName, lat);
    this.endLongitudeAtt = new Attribute( this.endLongitudeAttName, lon);

    // Read the start sub-solar coordinates.
    this.startSubsolarCoordsAtt = new Attribute( this.startSubsolarCoordsAttName,
                                                 (String) headerInfo.get( HeaderInfoTitle.START_SUBSOLAR_COORD.toString()));

    // Read the end sub-solar coordinates.
    this.endSubsolarCoordsAtt = new Attribute( this.endSubsolarCoordsAttName,
                                               (String) headerInfo.get( HeaderInfoTitle.END_SUBSOLAR_COORD.toString()));

    // Read the start lunar coordinates.
    this.startLunarCoordsAtt = new Attribute( this.startLunarCoordsAttName,
                                              (String) headerInfo.get( HeaderInfoTitle.START_LUNAR_COORD.toString()));

    // Read the end lunar coordinates.
    this.endLunarCoordsAtt = new Attribute( this.endLunarCoordsAttName,
                                            (String) headerInfo.get( HeaderInfoTitle.END_LUNAR_COORD.toString()));

    // Read the ascending node.
    Double ascendingNode = Double.valueOf( (String) headerInfo.get( HeaderInfoTitle.ASCENDING_NODE.toString()) );
    this.ascendingNodeAtt = new Attribute( this.ascendingNodeAttName, ascendingNode);

    Double nodeHeading = Double.valueOf( (String) headerInfo.get( HeaderInfoTitle.NODE_HEADING.toString()) );
    this.nodeHeadingAtt = new Attribute( this.nodeHeadingAttName, nodeHeading);
  }

  /**
   * Parse the sensor information from the header.
   */
  private void handleSensorInformation()
  {
    numSamplesPerBand = Integer.parseInt( (String) headerInfo.get( HeaderInfoTitle.SAMPLES_PER_BAND.toString()) );
    numSamplesPerBandDim = new Dimension(
            this.numSamplesPerBandDimName,
            numSamplesPerBand, true);

    // Read nominal resolution information
    nominalResolutionAtt = new Attribute( nominalResolutionAttName,
                                          (String) headerInfo.get( HeaderInfoTitle.NOMINAL_RESOLUTION.toString()));

    // Read bands per scanlin information.
    bandsPerScanlineAtt = new Attribute( bandsPerScanlineAttName,
                                         Integer.valueOf( (String) headerInfo.get( HeaderInfoTitle.BANDS_PER_SCANLINE.toString())));

    // Read bytes per smaple information
    bytesPerSampleAtt = new Attribute( bytesPerSampleAttName,
                                       Integer.valueOf( (String) headerInfo.get( HeaderInfoTitle.BYTES_PER_SAMPLE.toString())));;

    // Read byte offset for band 1 information.
    byteOffsetBand1Att = new Attribute( byteOffsetBand1AttName,
                                        Integer.valueOf( (String) headerInfo.get( HeaderInfoTitle.BYTE_OFFSET_BAND_1.toString())));

    // Read byte offset for band 2 information.
    byteOffsetBand2Att = new Attribute( byteOffsetBand2AttName,
                                        Integer.valueOf( (String) headerInfo.get( HeaderInfoTitle.BYTE_OFFSET_BAND_2.toString())));

    // Band 1 description
    band1Att = new Attribute( band1AttName, (String) headerInfo.get( HeaderInfoTitle.BAND_1.toString()));

    // Band 2 description
    band2Att = new Attribute( band2AttName, (String) headerInfo.get( HeaderInfoTitle.BAND_2.toString()));

    // Band organization
    bandOrganizationAtt = new Attribute( bandOrganizationAttName,
                                         (String) headerInfo.get( HeaderInfoTitle.ORGANIZATION.toString()));

    // thermal offset
    thermalOffsetAtt = new Attribute( thermalOffsetAttName,
                                      (String) headerInfo.get( HeaderInfoTitle.THERMAL_OFFSET.toString()));

    // thermal scale
    thermalScaleAtt = new Attribute( thermalScaleAttName,
                                      (String) headerInfo.get( HeaderInfoTitle.THERMAL_SCALE.toString()));

    // percent daylight
    percentDaylightAtt = new Attribute( percentDaylightAttName,
                                        Double.valueOf( (String) headerInfo.get( HeaderInfoTitle.PERCENT_DAYLIGHT.toString())));

    // percent full moon
    percentFullMoonAtt = new Attribute( percentFullMoonAttName,
                                        Double.valueOf( (String) headerInfo.get( HeaderInfoTitle.PERCENT_FULL_MOON.toString())));

    // percent terminator evident
    percentTerminatorEvidentAtt = new Attribute( percentTerminatorEvidentAttName,
                                        Double.valueOf( (String) headerInfo.get( HeaderInfoTitle.PERCENT_TERMINATOR_EVIDENT.toString())));
  }

  /**
   * Parse the sensor information from the header.
   */
  private void handleQCInformation()
  {
    // QC flags
    qcFlagsAtt = new Attribute( qcFlagsAttName,
                                (String) headerInfo.get( HeaderInfoTitle.QC_FLAGS.toString()));

  }

  /**
   * Return a string containing the header name/value pairs.
   *
   * @return a string of the file header name/value pairs.
   */
  protected String headerInfoDump()
  {
    StringBuffer retVal = new StringBuffer( );

    Iterator it = this.headerInfo.keySet().iterator();
    String curHeaderTitle = null;
    String curHeaderValue = null;
    while ( it.hasNext() )
    {
      curHeaderTitle = (String) it.next();
      curHeaderValue = (String) this.headerInfo.get( curHeaderTitle );
      retVal.append( curHeaderTitle );
      retVal.append( ":::::" );
      retVal.append( curHeaderValue );
      retVal.append( ":::::\n" );
    }

    return( retVal.toString() );
  }

  /**
   * Return the header information for this file as a String.
   *
   * @return the string form (NGDC DMSP archive format) of the header information.
   */
  public String toString()
  {
    StringBuffer retVal = new StringBuffer();

    retVal.append( HeaderInfoTitle.FILE_ID.toString() );
    retVal.append( ": ");
    retVal.append( this.fileIdAtt.getStringValue());
    retVal.append( "\n");

    retVal.append( HeaderInfoTitle.DATA_SET_ID.toString() );
    retVal.append( ": " );
    retVal.append( this.datasetIdAtt.getStringValue() );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.RECORD_BYTES.toString() );
    retVal.append( ": " );
    retVal.append( this.recordSizeInBytes );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.NUM_HEADER_RECORDS.toString() );
    retVal.append( ": " );
    retVal.append( this.numHeaderRecords );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.NUM_RECORDS.toString() );
    retVal.append( ": " );
    retVal.append( this.numRecords );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.SUBORBIT_HISTORY.toString() );
    retVal.append( ": " );
    retVal.append( this.suborbitHistoryAtt.getStringValue() );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.PROCESSING_SYSTEM.toString() );
    retVal.append( ": " );
    retVal.append( this.processingSystemAtt.getStringValue() );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.PROCESSING_DATE.toString() );
    retVal.append( ": " );
    retVal.append( DateFormatHandler.ALT_DATE_TIME.getDateTimeStringFromDate( this.processingDate ) );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.SPACECRAFT_ID.toString() );
    retVal.append( ": " );
    retVal.append( this.spacecraftIdAtt.getStringValue() );
    retVal.append( "\n" );

    retVal.append( HeaderInfoTitle.NORAD_ID.toString() );
    retVal.append( ": " );
    retVal.append( this.noradIdAtt.getStringValue() );
    retVal.append( "\n" );

    return ( retVal.toString() );
  }

  /**
   * Class for dealing with date/time formats (from the header and for nc file).
   */
  static class DateFormatHandler
  {
    // Available date format handlers.
    public final static DateFormatHandler ISO_DATE = new DateFormatHandler( "yyyy-MM-dd");
    public final static DateFormatHandler ISO_TIME = new DateFormatHandler( "HH:mm:ss.SSSz" );
    public final static DateFormatHandler ISO_DATE_TIME = new DateFormatHandler( "yyyy-MM-dd\'T\'HH:mm:ss.SSSz" );
    public final static DateFormatHandler ALT_DATE_TIME = new DateFormatHandler( "EEE MMM dd HH:mm:ss yyyy" );

    private String dateTimeFormatString = null;

    private DateFormatHandler( String dateTimeFormatString )
    {
      this.dateTimeFormatString = dateTimeFormatString;
    }

    public String getDateTimeFormatString() { return( this.dateTimeFormatString ); }

    /**
     * Return a java.util.Date given a date string using the date/time format string.
     *
     * @param dateTimeString - date/time string to be used to set java.util.Date.
     * @return The java.util.Date set by the given date/time string.
     */
    public Date getDateFromDateTimeString( String dateTimeString )
            throws ParseException
    {
      Date theDate = null;

      SimpleDateFormat dateFormat = new SimpleDateFormat( this.dateTimeFormatString, Locale.US );
      dateFormat.setTimeZone( TimeZone.getTimeZone( "GMT" ) );

      theDate = dateFormat.parse( dateTimeString );

      return ( theDate );
    }

    /**
     * Return the date/time string that represents the given a java.util.Date
     * in the format of this DataFormatHandler.
     *
     * @param date - the Date to be formatted into a date/time string.
     * @return The date/time string formatted from the given Date.
     */
    public String getDateTimeStringFromDate( Date date )
    {
      SimpleDateFormat dateFormat = new SimpleDateFormat( this.dateTimeFormatString, Locale.US );
      dateFormat.setTimeZone( TimeZone.getTimeZone( "GMT" ) );

      String dateString = dateFormat.format( date );

      return ( dateString );
    }
  }

  /**
   * Contains information about the NGDC DMSP archive file format's header information.
   */
  static class HeaderInfoTitle
  {
    private static java.util.HashMap hash = new java.util.HashMap( 20 );

    public final static HeaderInfoTitle FILE_ID = new HeaderInfoTitle( "file ID");                  // /dmsp/moby-1-3/subscriptions/IBAMA/1353226646955.tmp
    public final static HeaderInfoTitle DATA_SET_ID = new HeaderInfoTitle( "data set ID" );              // DMSP F14 OLS LS & TS
    public final static HeaderInfoTitle RECORD_BYTES = new HeaderInfoTitle( "record bytes" );             // 3040
    public final static HeaderInfoTitle NUM_HEADER_RECORDS = new HeaderInfoTitle( "number of header records" ); // 1
    public final static HeaderInfoTitle NUM_RECORDS = new HeaderInfoTitle( "number of records" );        // 692
    public final static HeaderInfoTitle SUBORBIT_HISTORY = new HeaderInfoTitle( "suborbit history" );         // F14200307192230.OIS (1,691)
    public final static HeaderInfoTitle PROCESSING_SYSTEM = new HeaderInfoTitle( "processing system" );        // v2.1b
    public final static HeaderInfoTitle PROCESSING_DATE = new HeaderInfoTitle( "processing date" );          // Sat Jul 19 19:33:23 2003
    public final static HeaderInfoTitle SPACECRAFT_ID = new HeaderInfoTitle( "spacecraft ID" );            // F14
    public final static HeaderInfoTitle NORAD_ID = new HeaderInfoTitle( "NORAD ID" );                 // 24753
    public final static HeaderInfoTitle START_DATE_UTC = new HeaderInfoTitle( "start date UTC" );           // 2003-07-19
    public final static HeaderInfoTitle START_TIME_UTC = new HeaderInfoTitle( "start time UTC" );           // 22:30:31.37112
    public final static HeaderInfoTitle END_DATE_UTC = new HeaderInfoTitle( "end date UTC" );             // 2003-07-19
    public final static HeaderInfoTitle END_TIME_UTC = new HeaderInfoTitle( "end time UTC" );             // 22:35:21.83694
    public final static HeaderInfoTitle START_DATE_LOCAL = new HeaderInfoTitle( "start date local" );         // 2003-07-19
    public final static HeaderInfoTitle START_TIME_LOCAL = new HeaderInfoTitle( "start time local" );         // 19:52:42.03518
    public final static HeaderInfoTitle START_LAT_LON = new HeaderInfoTitle( "start lat,lon" );            // 0.00 320.54
    public final static HeaderInfoTitle END_LAT_LON = new HeaderInfoTitle( "end lat,lon" );              // 16.99 316.69
    public final static HeaderInfoTitle START_SUBSOLAR_COORD = new HeaderInfoTitle( "start sub-solar coord" );    // 20.87 202.37
    public final static HeaderInfoTitle END_SUBSOLAR_COORD = new HeaderInfoTitle( "end sub-solar coord" );      // 20.87 201.16
    public final static HeaderInfoTitle START_LUNAR_COORD = new HeaderInfoTitle( "start lunar coord" );        // UNKNOWN
    public final static HeaderInfoTitle END_LUNAR_COORD = new HeaderInfoTitle( "end lunar coord" );          // UNKNOWN
    public final static HeaderInfoTitle ASCENDING_NODE = new HeaderInfoTitle( "ascending node" );           // 320.55
    public final static HeaderInfoTitle NODE_HEADING = new HeaderInfoTitle( "node heading" );             // 8.64
    public final static HeaderInfoTitle EPHEMERIS_SOURCE = new HeaderInfoTitle( "ephemeris source" );         // NORAD
    public final static HeaderInfoTitle NUM_DATA_RECORDS = new HeaderInfoTitle( "number of data records" );   // 691
    public final static HeaderInfoTitle NUM_ARTIFICIAL_DATA_RECORDS = new HeaderInfoTitle( "number of artificial data records" ); // 0
    public final static HeaderInfoTitle NOMINAL_RESOLUTION = new HeaderInfoTitle( "nominal resolution" );                // 2.7 km
    public final static HeaderInfoTitle BANDS_PER_SCANLINE = new HeaderInfoTitle( "bands per scanline" );                // 2
    public final static HeaderInfoTitle SAMPLES_PER_BAND = new HeaderInfoTitle( "samples per band" );                  // 1465
    public final static HeaderInfoTitle BYTES_PER_SAMPLE = new HeaderInfoTitle( "bytes per sample" );                  // 1
    public final static HeaderInfoTitle BYTE_OFFSET_BAND_1 = new HeaderInfoTitle( "byte offset band 1" );                // 96
    public final static HeaderInfoTitle BYTE_OFFSET_BAND_2 = new HeaderInfoTitle( "byte offset band 2" );                // 1568
    public final static HeaderInfoTitle BAND_1 = new HeaderInfoTitle( "band 1" );                            // OLS Visible .4-1.1um
    public final static HeaderInfoTitle BAND_2 = new HeaderInfoTitle( "band 2" );                            // OLS Thermal 10.5-12.6um
    public final static HeaderInfoTitle ORGANIZATION = new HeaderInfoTitle( "organization" );                      // band interleaved by line
    public final static HeaderInfoTitle THERMAL_OFFSET = new HeaderInfoTitle( "thermal offset" );                    // 190.00 K
    public final static HeaderInfoTitle THERMAL_SCALE = new HeaderInfoTitle( "thermal scale" );                     // 0.47
    public final static HeaderInfoTitle QC_FLAGS = new HeaderInfoTitle( "QC flags" );                          // 0=not QC'ed  1=artificial  2=bad vis
    public final static HeaderInfoTitle PERCENT_DAYLIGHT = new HeaderInfoTitle( "% daylight" );                        // 0.0
    public final static HeaderInfoTitle PERCENT_FULL_MOON = new HeaderInfoTitle( "% full moon" );                       // 57.8
    public final static HeaderInfoTitle PERCENT_TERMINATOR_EVIDENT = new HeaderInfoTitle( "% terminator evident" );              // 0.0
    public final static HeaderInfoTitle END_HEADER = new HeaderInfoTitle( "end header" );


    private String HeaderInfoTitle;

    private HeaderInfoTitle( String title )
    {
      this.HeaderInfoTitle = title;
      hash.put( title, this );
    }

    /**
     * Find the HeaderInfoTitle that matches this title.
     *
     * @param title
     * @return HeaderInfoTitle or null if no match.
     */
    public static HeaderInfoTitle getTitle( String title )
    {
      if ( title == null )
      {
        return null;
      }
      return (HeaderInfoTitle) hash.get( title );
    }

    /**
     * Return the string title.
     */
    public String toString()
    {
      return HeaderInfoTitle;
    }

  }
}

/*
 * $Log: DMSPHeader.java,v $
 * Revision 1.4  2004/12/07 23:27:09  edavis
 * Add a time variable and various global and variable attributes to comply with the "_Coordinates" convention.
 *
 * Revision 1.3  2004/10/13 15:25:47  edavis
 * Add some general metadata as global attributes (includes some THREDDS specific metadata).
 *
 * Revision 1.2  2004/10/06 21:25:28  edavis
 * Fix parsing of "start time UTC" and "end time UTC" attributes.
 *
 * Revision 1.1  2004/09/30 17:52:06  edavis
 * New DMSP IOServiceProvider.
 *
 */