/******************************************************************************
 *
 * Project:  LCP Driver
 * Purpose:  FARSITE v.4 Landscape file (.lcp) reader for GDAL
 * Author:   Chris Toney
 *
 ******************************************************************************
 * Copyright (c) 2008, Chris Toney
 * Copyright (c) 2008-2011, Even Rouault <even dot rouault at mines-paris dot org>
 * Copyright (c) 2013, Kyle Shannon <kyle at pobox dot com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "cpl_port.h"
#include "cpl_string.h"
#include "gdal_frmts.h"
#include "ogr_spatialref.h"
#include "rawdataset.h"

CPL_CVSID("$Id: lcpdataset.cpp b2723bb9ee29fb36de5c3afec9e9a6b757ef743c 2018-05-10 21:21:26 +0200 Even Rouault $")

constexpr size_t LCP_HEADER_SIZE = 7316;
constexpr int LCP_MAX_BANDS = 10;
constexpr int LCP_MAX_PATH = 256;
constexpr int LCP_MAX_DESC = 512;
constexpr int LCP_MAX_CLASSES = 100;

/************************************************************************/
/* ==================================================================== */
/*                              LCPDataset                              */
/* ==================================================================== */
/************************************************************************/

class LCPDataset final: public RawDataset
{
    VSILFILE    *fpImage;       // image data file.
    char        pachHeader[LCP_HEADER_SIZE];

    CPLString   osPrjFilename{};
    char        *pszProjection;

    int bHaveProjection{};

    static CPLErr ClassifyBandData( GDALRasterBand *poBand,
                                    GInt32 *pnNumClasses,
                                    GInt32 *panClasses );

    CPL_DISALLOW_COPY_ASSIGN(LCPDataset)

  public:
    LCPDataset();
    ~LCPDataset() override;

    char **GetFileList(void) override;

    CPLErr GetGeoTransform( double * ) override;

    static int          Identify( GDALOpenInfo * );
    static GDALDataset *Open( GDALOpenInfo * );
    static GDALDataset *CreateCopy( const char * pszFilename,
                                    GDALDataset *poSrcDS,
                                    int bStrict, char ** papszOptions,
                                    GDALProgressFunc pfnProgress,
                                    void * pProgressData );
    const char *GetProjectionRef(void) override;
};

/************************************************************************/
/*                            LCPDataset()                              */
/************************************************************************/

LCPDataset::LCPDataset() :
    fpImage(nullptr),
    pszProjection(CPLStrdup( "" ))
{
    memset( pachHeader, 0, sizeof(pachHeader) );
    bHaveProjection = FALSE;
}

/************************************************************************/
/*                            ~LCPDataset()                             */
/************************************************************************/

LCPDataset::~LCPDataset()

{
    FlushCache();
    if( fpImage != nullptr )
    {
        if( VSIFCloseL( fpImage ) != 0 )
        {
            CPLError( CE_Failure, CPLE_FileIO, "I/O error" );
        }
    }
    CPLFree(pszProjection);
}

/************************************************************************/
/*                          GetGeoTransform()                           */
/************************************************************************/

CPLErr LCPDataset::GetGeoTransform( double * padfTransform )
{
    double dfEast = 0.0;
    double dfWest = 0.0;
    double dfNorth = 0.0;
    double dfSouth = 0.0;
    double dfCellX = 0.0;
    double dfCellY = 0.0;

    memcpy( &dfEast, pachHeader + 4172, sizeof(double) );
    memcpy( &dfWest, pachHeader + 4180, sizeof(double) );
    memcpy( &dfNorth, pachHeader + 4188, sizeof(double) );
    memcpy( &dfSouth, pachHeader + 4196, sizeof(double) );
    memcpy( &dfCellX, pachHeader + 4208, sizeof(double) );
    memcpy( &dfCellY, pachHeader + 4216, sizeof(double) );
    CPL_LSBPTR64(&dfEast);
    CPL_LSBPTR64(&dfWest);
    CPL_LSBPTR64(&dfNorth);
    CPL_LSBPTR64(&dfSouth);
    CPL_LSBPTR64(&dfCellX);
    CPL_LSBPTR64(&dfCellY);

    padfTransform[0] = dfWest;
    padfTransform[3] = dfNorth;
    padfTransform[1] = dfCellX;
    padfTransform[2] = 0.0;

    padfTransform[4] = 0.0;
    padfTransform[5] = -1 * dfCellY;

    return CE_None;
}

/************************************************************************/
/*                              Identify()                              */
/************************************************************************/

int LCPDataset::Identify( GDALOpenInfo * poOpenInfo )

{
/* -------------------------------------------------------------------- */
/*      Verify that this is a FARSITE v.4 LCP file                      */
/* -------------------------------------------------------------------- */
    if( poOpenInfo->nHeaderBytes < 50 )
        return FALSE;

    /* check if first three fields have valid data */
    if( (CPL_LSBSINT32PTR(poOpenInfo->pabyHeader) != 20
          && CPL_LSBSINT32PTR(poOpenInfo->pabyHeader) != 21)
        || (CPL_LSBSINT32PTR(poOpenInfo->pabyHeader+4) != 20
          && CPL_LSBSINT32PTR(poOpenInfo->pabyHeader+4) != 21)
        || (CPL_LSBSINT32PTR(poOpenInfo->pabyHeader+8) < -90
             || CPL_LSBSINT32PTR(poOpenInfo->pabyHeader+8) > 90) )
    {
        return FALSE;
    }

    return TRUE;
}

/************************************************************************/
/*                            GetFileList()                             */
/************************************************************************/

char **LCPDataset::GetFileList()

{
    char **papszFileList = GDALPamDataset::GetFileList();

    if( bHaveProjection )
    {
        papszFileList = CSLAddString( papszFileList, osPrjFilename );
    }

    return papszFileList;
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

GDALDataset *LCPDataset::Open( GDALOpenInfo * poOpenInfo )

{
/* -------------------------------------------------------------------- */
/*      Verify that this is a FARSITE LCP file                          */
/* -------------------------------------------------------------------- */
    if( !Identify( poOpenInfo ) || poOpenInfo->fpL == nullptr )
        return nullptr;

/* -------------------------------------------------------------------- */
/*      Confirm the requested access is supported.                      */
/* -------------------------------------------------------------------- */
    if( poOpenInfo->eAccess == GA_Update )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "The LCP driver does not support update access to existing"
                  " datasets." );
        return nullptr;
    }
/* -------------------------------------------------------------------- */
/*      Create a corresponding GDALDataset.                             */
/* -------------------------------------------------------------------- */
    LCPDataset  *poDS = new LCPDataset();
    poDS->fpImage = poOpenInfo->fpL;
    poOpenInfo->fpL = nullptr;

/* -------------------------------------------------------------------- */
/*      Read the header and extract some information.                   */
/* -------------------------------------------------------------------- */
   if (VSIFSeekL( poDS->fpImage, 0, SEEK_SET ) < 0 ||
       VSIFReadL( poDS->pachHeader, 1, LCP_HEADER_SIZE, poDS->fpImage )
       != LCP_HEADER_SIZE)
   {
       CPLError( CE_Failure, CPLE_FileIO, "File too short" );
       delete poDS;
       return nullptr;
   }

   const int nWidth = CPL_LSBSINT32PTR( poDS->pachHeader + 4164);
   const int nHeight = CPL_LSBSINT32PTR( poDS->pachHeader + 4168);

   poDS->nRasterXSize = nWidth;
   poDS->nRasterYSize = nHeight;

   if (!GDALCheckDatasetDimensions(poDS->nRasterXSize, poDS->nRasterYSize))
   {
       delete poDS;
       return nullptr;
   }

   // Crown fuels = canopy height, canopy base height, canopy bulk density.
   // 21 = have them, 20 = do not have them
   const bool bHaveCrownFuels =
       CPL_TO_BOOL( CPL_LSBSINT32PTR (poDS->pachHeader + 0) - 20 );
   // Ground fuels = duff loading, coarse woody.
   const bool bHaveGroundFuels =
       CPL_TO_BOOL( CPL_LSBSINT32PTR (poDS->pachHeader + 4) - 20 );

   int nBands = 0;
   if( bHaveCrownFuels )
   {
       if( bHaveGroundFuels )
           nBands = 10;
       else
           nBands = 8;
   }
   else
   {
       if( bHaveGroundFuels )
           nBands = 7;
       else
           nBands = 5;
   }

   // Add dataset-level metadata.

   int nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 8);
   char szTemp[32] = { '\0' };
   snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
   poDS->SetMetadataItem( "LATITUDE", szTemp );

   nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 4204);
   if ( nTemp == 0 )
      poDS->SetMetadataItem( "LINEAR_UNIT", "Meters" );
   if ( nTemp == 1 )
      poDS->SetMetadataItem( "LINEAR_UNIT", "Feet" );

   poDS->pachHeader[LCP_HEADER_SIZE-1] = '\0';
   poDS->SetMetadataItem( "DESCRIPTION", poDS->pachHeader + 6804 );

/* -------------------------------------------------------------------- */
/*      Create band information objects.                                */
/* -------------------------------------------------------------------- */

   const int iPixelSize = nBands * 2;

   if (nWidth > INT_MAX / iPixelSize)
   {
       CPLError( CE_Failure, CPLE_AppDefined,  "Int overflow occurred");
       delete poDS;
       return nullptr;
   }

#ifdef CPL_LSB
   const bool bNativeOrder = true;
#else
   const bool bNativeOrder = false;
#endif

   // TODO(schwehr): Explain the 2048.
   char* pszList = static_cast<char *>( CPLMalloc(2048) );
   pszList[0] = '\0';

   for( int iBand = 1; iBand <= nBands; iBand++ )
   {
        GDALRasterBand  *poBand = new RawRasterBand(
            poDS, iBand, poDS->fpImage, LCP_HEADER_SIZE + ((iBand-1)*2),
            iPixelSize, iPixelSize * nWidth, GDT_Int16, bNativeOrder,
            RawRasterBand::OwnFP::NO );

        poDS->SetBand(iBand, poBand);

        switch ( iBand ) {
        case 1:
           poBand->SetDescription("Elevation");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4224);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ELEVATION_UNIT", szTemp );

           if ( nTemp == 0 )
              poBand->SetMetadataItem( "ELEVATION_UNIT_NAME", "Meters" );
           if ( nTemp == 1 )
              poBand->SetMetadataItem( "ELEVATION_UNIT_NAME", "Feet" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 44);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ELEVATION_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 48);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ELEVATION_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 52);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ELEVATION_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 4244 + 255) = '\0';
           poBand->SetMetadataItem( "ELEVATION_FILE", poDS->pachHeader + 4244 );

           break;

        case 2:
           poBand->SetDescription("Slope");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4226);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "SLOPE_UNIT", szTemp );

           if ( nTemp == 0 )
              poBand->SetMetadataItem( "SLOPE_UNIT_NAME", "Degrees" );
           if ( nTemp == 1 )
              poBand->SetMetadataItem( "SLOPE_UNIT_NAME", "Percent" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 456);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "SLOPE_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 460);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "SLOPE_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 464);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "SLOPE_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 4500 + 255) = '\0';
           poBand->SetMetadataItem( "SLOPE_FILE", poDS->pachHeader + 4500 );

           break;

        case 3:
           poBand->SetDescription("Aspect");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4228);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ASPECT_UNIT", szTemp );

           if ( nTemp == 0 )
              poBand->SetMetadataItem( "ASPECT_UNIT_NAME", "Grass categories" );
           if ( nTemp == 1 )
              poBand->SetMetadataItem( "ASPECT_UNIT_NAME", "Grass degrees" );
           if ( nTemp == 2 )
              poBand->SetMetadataItem( "ASPECT_UNIT_NAME", "Azimuth degrees" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 868);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ASPECT_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 872);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ASPECT_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 876);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "ASPECT_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 4756 + 255) = '\0';
           poBand->SetMetadataItem( "ASPECT_FILE", poDS->pachHeader + 4756 );

           break;

        case 4:
        {
           poBand->SetDescription("Fuel models");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4230);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "FUEL_MODEL_OPTION", szTemp );

           if ( nTemp == 0 )
              poBand->SetMetadataItem(
                  "FUEL_MODEL_OPTION_DESC",
                  "no custom models AND no conversion file needed" );
           if ( nTemp == 1 )
              poBand->SetMetadataItem(
                  "FUEL_MODEL_OPTION_DESC",
                  "custom models BUT no conversion file needed" );
           if ( nTemp == 2 )
              poBand->SetMetadataItem(
                  "FUEL_MODEL_OPTION_DESC",
                  "no custom models BUT conversion file needed" );
           if ( nTemp == 3 )
              poBand->SetMetadataItem(
                  "FUEL_MODEL_OPTION_DESC",
                  "custom models AND conversion file needed" );

           const int nMinFM = CPL_LSBSINT32PTR (poDS->pachHeader + 1280);
           snprintf( szTemp, sizeof(szTemp), "%d", nMinFM);
           poBand->SetMetadataItem( "FUEL_MODEL_MIN", szTemp );

           const int nMaxFM = CPL_LSBSINT32PTR (poDS->pachHeader + 1284);
           snprintf( szTemp, sizeof(szTemp), "%d", nMaxFM);
           poBand->SetMetadataItem( "FUEL_MODEL_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 1288);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "FUEL_MODEL_NUM_CLASSES", szTemp );

           if (nTemp > 0 && nTemp <= 100) {
              strcpy(pszList, "");
              for ( int i = 0; i <= nTemp; i++ ) {
                  const int nTemp2 =
                      CPL_LSBSINT32PTR( poDS->pachHeader + (1292+(i*4)) );
                  if ( nTemp2 >= nMinFM && nTemp2 <= nMaxFM ) {
                     snprintf( szTemp, sizeof(szTemp), "%d", nTemp2);
                     strcat(pszList, szTemp);
                     if (i < nTemp )
                        strcat(pszList, ",");
                  }
              }
           }
           poBand->SetMetadataItem( "FUEL_MODEL_VALUES", pszList );

           *(poDS->pachHeader + 5012 + 255) = '\0';
           poBand->SetMetadataItem( "FUEL_MODEL_FILE",
                                    poDS->pachHeader + 5012 );

           break;
        }
        case 5:
           poBand->SetDescription("Canopy cover");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4232);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CANOPY_COV_UNIT", szTemp );

           if ( nTemp == 0 )
              poBand->SetMetadataItem( "CANOPY_COV_UNIT_NAME",
                                       "Categories (0-4)" );
           if ( nTemp == 1 )
              poBand->SetMetadataItem( "CANOPY_COV_UNIT_NAME", "Percent" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 1692);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CANOPY_COV_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 1696);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CANOPY_COV_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 1700);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CANOPY_COV_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 5268 + 255) = '\0';
           poBand->SetMetadataItem( "CANOPY_COV_FILE", poDS->pachHeader + 5268 );

           break;

        case 6:
           if(bHaveCrownFuels) {
              poBand->SetDescription("Canopy height");

              nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4234);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CANOPY_HT_UNIT", szTemp );

              if ( nTemp == 1 )
                 poBand->SetMetadataItem( "CANOPY_HT_UNIT_NAME", "Meters" );
              if ( nTemp == 2 )
                 poBand->SetMetadataItem( "CANOPY_HT_UNIT_NAME", "Feet" );
              if ( nTemp == 3 )
                 poBand->SetMetadataItem( "CANOPY_HT_UNIT_NAME", "Meters x 10" );
              if ( nTemp == 4 )
                 poBand->SetMetadataItem( "CANOPY_HT_UNIT_NAME", "Feet x 10" );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2104);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CANOPY_HT_MIN", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2108);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CANOPY_HT_MAX", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2112);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CANOPY_HT_NUM_CLASSES", szTemp );

              *(poDS->pachHeader + 5524 + 255) = '\0';
              poBand->SetMetadataItem( "CANOPY_HT_FILE",
                                       poDS->pachHeader + 5524 );
           }
           else
           {
              poBand->SetDescription("Duff");

              nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4240);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "DUFF_UNIT", szTemp );

              if ( nTemp == 1 )
                 poBand->SetMetadataItem( "DUFF_UNIT_NAME", "Mg/ha" );
              if ( nTemp == 2 )
                 poBand->SetMetadataItem( "DUFF_UNIT_NAME", "t/ac" );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3340);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "DUFF_MIN", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3344);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "DUFF_MAX", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3348);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "DUFF_NUM_CLASSES", szTemp );

              *(poDS->pachHeader + 6292 + 255) = '\0';
              poBand->SetMetadataItem( "DUFF_FILE", poDS->pachHeader + 6292 );
           }
           break;

        case 7:
           if(bHaveCrownFuels) {
              poBand->SetDescription("Canopy base height");

              nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4236);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CBH_UNIT", szTemp );

              if ( nTemp == 1 )
                 poBand->SetMetadataItem( "CBH_UNIT_NAME", "Meters" );
              if ( nTemp == 2 )
                 poBand->SetMetadataItem( "CBH_UNIT_NAME", "Feet" );
              if ( nTemp == 3 )
                 poBand->SetMetadataItem( "CBH_UNIT_NAME", "Meters x 10" );
              if ( nTemp == 4 )
                 poBand->SetMetadataItem( "CBH_UNIT_NAME", "Feet x 10" );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2516);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CBH_MIN", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2520);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CBH_MAX", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2524);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CBH_NUM_CLASSES", szTemp );

              *(poDS->pachHeader + 5780 + 255) = '\0';
              poBand->SetMetadataItem( "CBH_FILE", poDS->pachHeader + 5780 );
           }
           else
           {
              poBand->SetDescription( "Coarse woody debris" );

              nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4242);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CWD_OPTION", szTemp );

              //if ( nTemp == 1 )
              //   poBand->SetMetadataItem( "CWD_UNIT_DESC", "?" );
              //if ( nTemp == 2 )
              //   poBand->SetMetadataItem( "CWD_UNIT_DESC", "?" );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3752);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CWD_MIN", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3756);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CWD_MAX", szTemp );

              nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3760);
              snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
              poBand->SetMetadataItem( "CWD_NUM_CLASSES", szTemp );

              *(poDS->pachHeader + 6548 + 255) = '\0';
              poBand->SetMetadataItem( "CWD_FILE", poDS->pachHeader + 6548 );
           }
           break;

        case 8:
           poBand->SetDescription("Canopy bulk density");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4238);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CBD_UNIT", szTemp );

           if ( nTemp == 1 )
              poBand->SetMetadataItem( "CBD_UNIT_NAME", "kg/m^3" );
           if ( nTemp == 2 )
              poBand->SetMetadataItem( "CBD_UNIT_NAME", "lb/ft^3" );
           if ( nTemp == 3 )
              poBand->SetMetadataItem( "CBD_UNIT_NAME", "kg/m^3 x 100" );
           if ( nTemp == 4 )
              poBand->SetMetadataItem( "CBD_UNIT_NAME", "lb/ft^3 x 1000" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2928);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CBD_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2932);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CBD_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 2936);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CBD_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 6036 + 255) = '\0';
           poBand->SetMetadataItem( "CBD_FILE", poDS->pachHeader + 6036 );

           break;

        case 9:
           poBand->SetDescription("Duff");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4240);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "DUFF_UNIT", szTemp );

           if ( nTemp == 1 )
              poBand->SetMetadataItem( "DUFF_UNIT_NAME", "Mg/ha" );
           if ( nTemp == 2 )
              poBand->SetMetadataItem( "DUFF_UNIT_NAME", "t/ac" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3340);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "DUFF_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3344);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "DUFF_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3348);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "DUFF_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 6292 + 255) = '\0';
           poBand->SetMetadataItem( "DUFF_FILE", poDS->pachHeader + 6292 );

           break;

        case 10:
           poBand->SetDescription("Coarse woody debris");

           nTemp = CPL_LSBUINT16PTR (poDS->pachHeader + 4242);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CWD_OPTION", szTemp );

           //if ( nTemp == 1 )
           //   poBand->SetMetadataItem( "CWD_UNIT_DESC", "?" );
           //if ( nTemp == 2 )
           //   poBand->SetMetadataItem( "CWD_UNIT_DESC", "?" );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3752);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CWD_MIN", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3756);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CWD_MAX", szTemp );

           nTemp = CPL_LSBSINT32PTR (poDS->pachHeader + 3760);
           snprintf( szTemp, sizeof(szTemp), "%d", nTemp);
           poBand->SetMetadataItem( "CWD_NUM_CLASSES", szTemp );

           *(poDS->pachHeader + 6548 + 255) = '\0';
           poBand->SetMetadataItem( "CWD_FILE", poDS->pachHeader + 6548 );

           break;
        }
   }

/* -------------------------------------------------------------------- */
/*      Try to read projection file.                                    */
/* -------------------------------------------------------------------- */
    char * const pszDirname = CPLStrdup(CPLGetPath(poOpenInfo->pszFilename));
    char * const pszBasename = CPLStrdup(CPLGetBasename(poOpenInfo->pszFilename));

    poDS->osPrjFilename = CPLFormFilename( pszDirname, pszBasename, "prj" );
    VSIStatBufL sStatBuf;
    int nRet = VSIStatL( poDS->osPrjFilename, &sStatBuf );

    if( nRet != 0 && VSIIsCaseSensitiveFS(poDS->osPrjFilename))
    {
        poDS->osPrjFilename = CPLFormFilename( pszDirname, pszBasename, "PRJ" );
        nRet = VSIStatL( poDS->osPrjFilename, &sStatBuf );
    }

    if( nRet == 0 )
    {
        char** papszPrj = CSLLoad( poDS->osPrjFilename );

        CPLDebug( "LCP", "Loaded SRS from %s", poDS->osPrjFilename.c_str() );

        OGRSpatialReference oSRS;
        if( oSRS.importFromESRI( papszPrj ) == OGRERR_NONE )
        {
            CPLFree( poDS->pszProjection );
            oSRS.exportToWkt( &(poDS->pszProjection) );
            poDS->bHaveProjection = TRUE;
        }

        CSLDestroy(papszPrj);
    }

    CPLFree( pszDirname );
    CPLFree( pszBasename );

/* -------------------------------------------------------------------- */
/*      Initialize any PAM information.                                 */
/* -------------------------------------------------------------------- */
    poDS->SetDescription( poOpenInfo->pszFilename );
    poDS->TryLoadXML();

/* -------------------------------------------------------------------- */
/*      Check for external overviews.                                   */
/* -------------------------------------------------------------------- */
    poDS->oOvManager.Initialize(
        poDS, poOpenInfo->pszFilename, poOpenInfo->GetSiblingFiles() );

    CPLFree(pszList);

    return poDS;
}

/************************************************************************/
/*                          ClassifyBandData()                          */
/*  Classify a band and store 99 or fewer unique values.  If there are  */
/*  more than 99 unique values, then set pnNumClasses to -1 as a flag   */
/*  that represents this.  These are legacy values in the header, and   */
/*  while we should never deprecate them, we could possibly not         */
/*  calculate them by default.                                          */
/************************************************************************/

CPLErr LCPDataset::ClassifyBandData( GDALRasterBand *poBand,
                                     GInt32 *pnNumClasses,
                                     GInt32 *panClasses )
{
    if( pnNumClasses == nullptr )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid pointer for panClasses" );
        return CE_Failure;
    }

    if( panClasses == nullptr )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid pointer for panClasses" );
        *pnNumClasses = -1;
        return CE_Failure;
    }

    if( poBand == nullptr )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid band passed to ClassifyBandData()" );
        *pnNumClasses = -1;
        memset( panClasses, 0, 400 );
        return CE_Failure;
    }

    const int nXSize = poBand->GetXSize();
    const int nYSize = poBand->GetYSize();
    double dfMax = 0.0;
    double dfDummy = 0.0;
    poBand->GetStatistics( FALSE, TRUE, &dfDummy, &dfMax, &dfDummy, &dfDummy );

    const int nSpan = static_cast<int>( dfMax );
    GInt16 *panValues =
        static_cast<GInt16 *>( CPLMalloc( sizeof( GInt16 ) * nXSize ) );
    GByte *pabyFlags =
        static_cast<GByte *>( CPLMalloc( sizeof( GByte ) * nSpan + 1 ) );
    memset( pabyFlags, 0, nSpan + 1 );

    int nFound = 0;
    bool bTooMany = false;
    CPLErr eErr = CE_None;
    for( int iLine = 0; iLine < nYSize; iLine++ )
    {
        eErr = poBand->RasterIO( GF_Read, 0, iLine, nXSize, 1,
                                 panValues, nXSize, 1,
                                 GDT_Int16, 0, 0, nullptr );
        for( int iPixel = 0; iPixel < nXSize; iPixel++ )
        {
            if( panValues[iPixel] == -9999 )
            {
                continue;
            }
            if( nFound > 99 )
            {
                CPLDebug( "LCP",
                          "Found more that 100 unique values in "
                          "band %d.  Not 'classifying' the data.",
                          poBand->GetBand() );
                nFound = -1;
                bTooMany = true;
                break;
            }
            if( bTooMany )
            {
                break;
            }
            if( pabyFlags[panValues[iPixel]] == 0 )
            {
                pabyFlags[panValues[iPixel]] = 1;
                nFound++;
            }
        }
    }
    CPLAssert( nFound <= 100 );

    // The classes are always padded with a leading 0.  This was for aligning
    // offsets, or making it a 1-based array instead of 0-based.
    panClasses[0] = 0;
    for( int j = 0, nIndex = 1; j < nSpan + 1; j++ )
    {
        if( pabyFlags[j] == 1 )
        {
            panClasses[nIndex++] = j;
        }
    }
    *pnNumClasses = nFound;
    CPLFree( reinterpret_cast<void *>( pabyFlags ) );
    CPLFree( reinterpret_cast<void *>( panValues ) );

    return eErr;
}

/************************************************************************/
/*                          CreateCopy()                                */
/************************************************************************/

GDALDataset *LCPDataset::CreateCopy( const char * pszFilename,
                                     GDALDataset * poSrcDS,
                                     int bStrict, char ** papszOptions,
                                     GDALProgressFunc pfnProgress,
                                     void * pProgressData )

{
/* -------------------------------------------------------------------- */
/*      Verify input options.                                           */
/* -------------------------------------------------------------------- */
    const int nBands = poSrcDS->GetRasterCount();
    if( nBands != 5 && nBands != 7 && nBands != 8 && nBands != 10 )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "LCP driver doesn't support %d bands.  Must be 5, 7, 8 "
                  "or 10 bands.", nBands );
        return nullptr;
    }

    GDALDataType eType = poSrcDS->GetRasterBand( 1 )->GetRasterDataType();
    if( eType != GDT_Int16 && bStrict )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "LCP only supports 16-bit signed integer data types." );
        return nullptr;
    }
    else if( eType != GDT_Int16 )
    {
        CPLError( CE_Warning, CPLE_AppDefined,
                  "Setting data type to 16-bit integer." );
    }

/* -------------------------------------------------------------------- */
/*      What schema do we have (ground/crown fuels)                     */
/* -------------------------------------------------------------------- */

    const bool bHaveCrownFuels = nBands == 8 || nBands == 10;
    const bool bHaveGroundFuels = nBands == 7 || nBands == 10;

    // Since units are 'configurable', we should check for user
    // defined units.  This is a bit cumbersome, but the user should
    // be allowed to specify none to get default units/options.  Use
    // default units every chance we get.
    GInt16 panMetadata[LCP_MAX_BANDS] = {
        0, // 0 ELEVATION_UNIT
        0, // 1 SLOPE_UNIT
        2, // 2 ASPECT_UNIT
        0, // 3 FUEL_MODEL_OPTION
        1, // 4 CANOPY_COV_UNIT
        3, // 5 CANOPY_HT_UNIT
        3, // 6 CBH_UNIT
        3, // 7 CBD_UNIT
        1, // 8 DUFF_UNIT
        0, // 9 CWD_OPTION
    };

    // Check the units/options for user overrides.
    const char *pszTemp
        = CSLFetchNameValueDef( papszOptions, "ELEVATION_UNIT", "METERS" );
    if( STARTS_WITH_CI(pszTemp, "METER") )
    {
        panMetadata[0] = 0;
    }
    else if( EQUAL( pszTemp, "FEET" ) || EQUAL( pszTemp, "FOOT" ) )
    {
        panMetadata[0] = 1;
    }
    else
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid value (%s) for ELEVATION_UNIT.",
                  pszTemp );
        return nullptr;
    }

    pszTemp = CSLFetchNameValueDef( papszOptions, "SLOPE_UNIT", "DEGREES" );
    if( EQUAL( pszTemp, "DEGREES" ) )
    {
        panMetadata[1] = 0;
    }
    else if( EQUAL( pszTemp, "PERCENT" ) )
    {
        panMetadata[1] = 1;
    }
    else
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid value (%s) for SLOPE_UNIT.",
                  pszTemp );
        return nullptr;
    }

    pszTemp = CSLFetchNameValueDef( papszOptions, "ASPECT_UNIT",
                                    "AZIMUTH_DEGREES" );
    if( EQUAL( pszTemp, "GRASS_CATEGORIES" ) )
    {
        panMetadata[2] = 0;
    }
    else if( EQUAL( pszTemp, "GRASS_DEGREES" ) )
    {
        panMetadata[2] = 1;
    }
    else if( EQUAL( pszTemp, "AZIMUTH_DEGREES" ) )
    {
        panMetadata[2] = 2;
    }
    else
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid value (%s) for ASPECT_UNIT.",
                  pszTemp );
        return nullptr;
    }

    pszTemp = CSLFetchNameValueDef( papszOptions, "FUEL_MODEL_OPTION",
                                    "NO_CUSTOM_AND_NO_FILE" );
    if( EQUAL( pszTemp, "NO_CUSTOM_AND_NO_FILE" ) )
    {
        panMetadata[3] = 0;
    }
    else if( EQUAL( pszTemp, "CUSTOM_AND_NO_FILE" ) )
    {
        panMetadata[3] = 1;
    }
    else if( EQUAL( pszTemp, "NO_CUSTOM_AND_FILE" ) )
    {
        panMetadata[3] = 2;
    }
    else if( EQUAL( pszTemp, "CUSTOM_AND_FILE" ) )
    {
        panMetadata[3] = 3;
    }
    else
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid value (%s) for FUEL_MODEL_OPTION.",
                  pszTemp );
        return nullptr;
    }

    pszTemp = CSLFetchNameValueDef( papszOptions, "CANOPY_COV_UNIT",
                                    "PERCENT" );
    if( EQUAL( pszTemp, "CATEGORIES" ) )
    {
        panMetadata[4] = 0;
    }
    else if( EQUAL( pszTemp, "PERCENT" ) )
    {
        panMetadata[4] = 1;
    }
    else
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Invalid value (%s) for CANOPY_COV_UNIT.",
                  pszTemp );
        return nullptr;
    }

    if( bHaveCrownFuels )
    {
        pszTemp = CSLFetchNameValueDef( papszOptions, "CANOPY_HT_UNIT",
                                        "METERS_X_10" );
        if( EQUAL( pszTemp, "METERS" ) || EQUAL( pszTemp, "METER" ) )
        {
            panMetadata[5] = 1;
        }
        else if( EQUAL( pszTemp, "FEET" ) || EQUAL( pszTemp, "FOOT" ) )
        {
            panMetadata[5] = 2;
        }
        else if( EQUAL( pszTemp, "METERS_X_10" )  ||
                 EQUAL( pszTemp, "METER_X_10" ) )
        {
            panMetadata[5] = 3;
        }
        else if( EQUAL( pszTemp, "FEET_X_10" ) ||
                 EQUAL( pszTemp, "FOOT_X_10" ) )
        {
            panMetadata[5] = 4;
        }
        else
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Invalid value (%s) for CANOPY_HT_UNIT.",
                      pszTemp );
            return nullptr;
        }

        pszTemp = CSLFetchNameValueDef( papszOptions, "CBH_UNIT",
                                        "METERS_X_10" );
        if( EQUAL( pszTemp, "METERS" ) || EQUAL( pszTemp, "METER" ) )
        {
            panMetadata[6] = 1;
        }
        else if( EQUAL( pszTemp, "FEET" ) || EQUAL( pszTemp, "FOOT" ) )
        {
            panMetadata[6] = 2;
        }
        else if( EQUAL( pszTemp, "METERS_X_10" ) ||
                 EQUAL( pszTemp, "METER_X_10" ) )
        {
            panMetadata[6] = 3;
        }
        else if( EQUAL( pszTemp, "FEET_X_10" ) ||
                 EQUAL( pszTemp, "FOOT_X_10" ) )
        {
            panMetadata[6] = 4;
        }
        else
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Invalid value (%s) for CBH_UNIT.",
                      pszTemp );
            return nullptr;
        }

        pszTemp = CSLFetchNameValueDef( papszOptions, "CBD_UNIT",
                                        "KG_PER_CUBIC_METER_X_100" );
        if( EQUAL( pszTemp, "KG_PER_CUBIC_METER" ) )
        {
            panMetadata[7] = 1;
        }
        else if( EQUAL( pszTemp, "POUND_PER_CUBIC_FOOT" ) )
        {
            panMetadata[7] = 2;
        }
        else if( EQUAL( pszTemp, "KG_PER_CUBIC_METER_X_100" ) )
        {
            panMetadata[7] = 3;
        }
        else if( EQUAL( pszTemp, "POUND_PER_CUBIC_FOOT_X_1000" ) )
        {
            panMetadata[7] = 4;
        }
        else
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Invalid value (%s) for CBD_UNIT.",
                      pszTemp );
            return nullptr;
        }
    }

    if( bHaveGroundFuels )
    {
        pszTemp = CSLFetchNameValueDef( papszOptions, "DUFF_UNIT",
                                        "MG_PER_HECTARE_X_10" );
        if( EQUAL( pszTemp, "MG_PER_HECTARE_X_10" ) )
        {
            panMetadata[8] = 1;
        }
        else if ( EQUAL( pszTemp, "TONS_PER_ACRE_X_10" ) )
        {
            panMetadata[8] = 2;
        }
        else
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Invalid value (%s) for DUFF_UNIT.",
                      pszTemp );
            return nullptr;
        }

        panMetadata[9] = 1;
    }

    // Calculate the stats for each band.  The binary file carries along
    // these metadata for display purposes(?).
    bool bCalculateStats = CPLFetchBool(papszOptions, "CALCULATE_STATS", true);

    const bool bClassifyData = CPLFetchBool(papszOptions, "CLASSIFY_DATA", true);

    // We should have stats if we classify, we'll get them anyway.
    if( bClassifyData && !bCalculateStats )
    {
        CPLError( CE_Warning, CPLE_AppDefined,
                  "Ignoring request to not calculate statistics, "
                  "because CLASSIFY_DATA was set to ON" );
        bCalculateStats = true;
    }

    pszTemp = CSLFetchNameValueDef( papszOptions, "LINEAR_UNIT",
                                    "SET_FROM_SRS" );
    int nLinearUnits = 0;
    bool bSetLinearUnits = false;
    if( EQUAL( pszTemp, "SET_FROM_SRS" ) )
    {
        bSetLinearUnits = true;
    }
    else if( STARTS_WITH_CI(pszTemp, "METER") )
    {
        nLinearUnits = 0;
    }
    else if( EQUAL( pszTemp, "FOOT" ) || EQUAL( pszTemp, "FEET" ) )
    {
        nLinearUnits = 1;
    }
    else if( STARTS_WITH_CI(pszTemp, "KILOMETER") )
    {
        nLinearUnits = 2;
    }
    bool bCalculateLatitude = true;
    int nLatitude = 0;
    if( CSLFetchNameValue( papszOptions, "LATITUDE" ) != nullptr )
    {
        bCalculateLatitude = false;
        nLatitude = atoi( CSLFetchNameValue( papszOptions, "LATITUDE" ) );
        if( nLatitude > 90 || nLatitude < -90 )
        {
            CPLError( CE_Failure, CPLE_OpenFailed,
                      "Invalid value (%d) for LATITUDE.", nLatitude );
            return nullptr;
        }
    }

    // If no latitude is supplied, attempt to extract the central latitude
    // from the image.  It must be set either manually or here, otherwise
    // we fail.
    double adfSrcGeoTransform[6] = { 0.0 };
    poSrcDS->GetGeoTransform( adfSrcGeoTransform );
    OGRSpatialReference oSrcSRS;
    const char *pszWkt = poSrcDS->GetProjectionRef();
    double dfLongitude = 0.0;
    double dfLatitude = 0.0;

    const int nYSize = poSrcDS->GetRasterYSize();

    if( !bCalculateLatitude )
    {
        dfLatitude = nLatitude;
    }
    else if( !EQUAL( pszWkt, "" ) )
    {
        oSrcSRS.importFromWkt( pszWkt );
        OGRSpatialReference oDstSRS;
        oDstSRS.importFromEPSG( 4269 );
        OGRCoordinateTransformation *poCT
            = reinterpret_cast<OGRCoordinateTransformation *>(
                OGRCreateCoordinateTransformation( &oSrcSRS, &oDstSRS ) );
        if( poCT != nullptr )
        {
            dfLatitude =
                adfSrcGeoTransform[3] + adfSrcGeoTransform[5] * nYSize / 2;
            const int nErr = static_cast<int>(
                poCT->Transform( 1, &dfLongitude, &dfLatitude ) );
            if( !nErr )
            {
                dfLatitude = 0.0;
                // For the most part, this is an invalid LCP, but it is a
                // changeable value in Flammap/Farsite, etc.  We should
                // probably be strict here all the time.
                CPLError( CE_Failure, CPLE_AppDefined,
                          "Could not calculate latitude from spatial "
                          "reference and LATITUDE was not set." );
                return nullptr;
            }
        }
        OGRCoordinateTransformation::DestroyCT( poCT );
    }
    else
    {
        // See comment above on failure to transform.
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Could not calculate latitude from spatial reference "
                  "and LATITUDE was not set." );
        return nullptr;
    }
    // Set the linear units if the metadata item was not already set, and we
    // have an SRS.
    if( bSetLinearUnits && !EQUAL( pszWkt, "" ) )
    {
        const char *pszUnit = oSrcSRS.GetAttrValue( "UNIT", 0 );
        if( pszUnit == nullptr )
        {
            if( bStrict )
            {
                CPLError( CE_Failure, CPLE_AppDefined,
                          "Could not parse linear unit." );
                return nullptr;
            }
            else
            {
                CPLError( CE_Warning, CPLE_AppDefined,
                          "Could not parse linear unit, using meters" );
                nLinearUnits = 0;
            }
        }
        else
        {
            CPLDebug( "LCP", "Setting linear unit to %s", pszUnit );
            if( EQUAL( pszUnit, "meter" ) || EQUAL( pszUnit, "metre" ) )
            {
                nLinearUnits = 0;
            }
            else if( EQUAL( pszUnit, "feet" ) || EQUAL( pszUnit, "foot" ) )
            {
                nLinearUnits = 1;
            }
            else if( STARTS_WITH_CI(pszUnit, "kilomet") )
            {
                nLinearUnits = 2;
            }
            else
            {
                if( bStrict )
                nLinearUnits = 0;
            }
            pszUnit = oSrcSRS.GetAttrValue( "UNIT", 1 );
            if( pszUnit != nullptr )
            {
                const double dfScale = CPLAtof( pszUnit );
                if( dfScale != 1.0 )
                {
                    if( bStrict )
                    {
                        CPLError( CE_Failure, CPLE_AppDefined,
                                  "Unit scale is %lf (!=1.0). It is not "
                                  "supported.", dfScale );
                        return nullptr;
                    }
                    else
                    {
                        CPLError( CE_Warning, CPLE_AppDefined,
                                  "Unit scale is %lf (!=1.0). It is not "
                                  "supported, ignoring.", dfScale );
                    }
                }
            }
        }
    }
    else if( bSetLinearUnits )
    {
        // This can be defaulted if it is not a strict creation.
        if( bStrict )
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Could not parse linear unit from spatial reference "
                      "and LINEAR_UNIT was not set." );
            return nullptr;
        }
        else
        {
            CPLError( CE_Warning, CPLE_AppDefined,
                      "Could not parse linear unit from spatial reference "
                      "and LINEAR_UNIT was not set, defaulting to meters." );
            nLinearUnits = 0;
        }
    }

    const char *pszDescription =
        CSLFetchNameValueDef( papszOptions, "DESCRIPTION",
                              "LCP file created by GDAL." );

    // Loop through and get the stats for the bands if we need to calculate
    // them.  This probably should be done when we copy the data over to the
    // destination dataset, since we load the values into memory, but this is
    // much simpler code using GDALRasterBand->GetStatistics().  We also may
    // need to classify the data (number of unique values and a list of those
    // values if the number of unique values is > 100.  It is currently unclear
    // how these data are used though, so we will implement that at some point
    // if need be.
    double *padfMin =
        static_cast<double *>( CPLMalloc( sizeof( double ) * nBands ) );
    double *padfMax =
        static_cast<double *>( CPLMalloc( sizeof( double ) * nBands ) );

    // Initialize these arrays to zeros
    GInt32 *panFound =
        static_cast<GInt32 *>( VSIMalloc2( sizeof( GInt32 ), nBands ) );
    memset( panFound, 0, sizeof( GInt32 ) * nBands );
    GInt32 *panClasses =
        static_cast<GInt32 *>(
            VSIMalloc3( sizeof( GInt32 ), nBands, LCP_MAX_CLASSES ) );
    memset( panClasses, 0, sizeof( GInt32 ) * nBands * LCP_MAX_CLASSES );

    CPLErr eErr = CE_None;
    if( bCalculateStats )
    {

        for( int i = 0; i < nBands; i++ )
        {
            GDALRasterBand *poBand = poSrcDS->GetRasterBand( i + 1 );
            double dfDummy = 0.0;
            eErr = poBand->GetStatistics( FALSE, TRUE, &padfMin[i],
                                          &padfMax[i], &dfDummy, &dfDummy );
            if( eErr != CE_None )
            {
                CPLError( CE_Warning, CPLE_AppDefined, "Failed to properly "
                                                       "calculate statistics "
                                                       "on band %d", i );
                padfMin[i] = 0.0;
                padfMax[i] = 0.0;
            }

            // See comment above.
            if( bClassifyData )
            {
                eErr = ClassifyBandData( poBand, panFound+ i,
                                         panClasses + ( i * LCP_MAX_CLASSES ) );
                if ( eErr != CE_None )
                {
                  CPLError( CE_Warning, CPLE_AppDefined,
                            "Failed to classify band data on band %d.", i );
                }
            }
        }
    }

    VSILFILE *fp = VSIFOpenL( pszFilename, "wb" );
    if( fp == nullptr )
    {
        CPLError( CE_Failure, CPLE_OpenFailed,
                  "Unable to create lcp file %s.", pszFilename );
        CPLFree( padfMin );
        CPLFree( padfMax );
        CPLFree( panFound );
        CPLFree( panClasses );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Write the header                                                */
/* -------------------------------------------------------------------- */

    GInt32 nTemp = bHaveCrownFuels ? 21 : 20;
    CPL_LSBPTR32( &nTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));
    nTemp = bHaveGroundFuels ? 21 : 20;
    CPL_LSBPTR32( &nTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));

    const int nXSize = poSrcDS->GetRasterXSize();
    nTemp = static_cast<GInt32>( dfLatitude + 0.5 );
    CPL_LSBPTR32( &nTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));
    dfLongitude = adfSrcGeoTransform[0] + adfSrcGeoTransform[1] * nXSize;
    CPL_LSBPTR64( &dfLongitude );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfLongitude, 8, 1, fp ));
    dfLongitude = adfSrcGeoTransform[0];
    CPL_LSBPTR64( &dfLongitude );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfLongitude, 8, 1, fp ));
    dfLatitude = adfSrcGeoTransform[3];
    CPL_LSBPTR64( &dfLatitude );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfLatitude, 8, 1, fp ));
    dfLatitude = adfSrcGeoTransform[3] + adfSrcGeoTransform[5] * nYSize;
    CPL_LSBPTR64( &dfLatitude );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfLatitude, 8, 1, fp ));

    // Swap the two classification arrays if we are writing them, and they need
    // to be swapped.
#ifdef CPL_MSB
    if( bClassifyData )
    {
        GDALSwapWords( panFound, 2, nBands, 2 );
        GDALSwapWords( panClasses, 2, LCP_MAX_CLASSES, 2 );
    }
#endif

    if( bCalculateStats )
    {
        for( int i = 0; i < nBands; i++ )
        {
            // If we don't have Crown fuels, but do have Ground fuels, we
            // have to 'fast forward'.
            if( i == 5 && !bHaveCrownFuels && bHaveGroundFuels )
            {
                CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 3340, SEEK_SET ));
            }
            nTemp = static_cast<GInt32>(padfMin[i]);
            CPL_LSBPTR32( &nTemp );
            CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));
            nTemp = static_cast<GInt32>(padfMax[i]);
            CPL_LSBPTR32( &nTemp );
            CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));
            if( bClassifyData )
            {
                // These two arrays were swapped in their entirety above.
                CPL_IGNORE_RET_VAL(VSIFWriteL( panFound + i, 4, 1, fp ));
                CPL_IGNORE_RET_VAL(
                    VSIFWriteL(
                        panClasses + ( i * LCP_MAX_CLASSES ), 4, 100, fp ));
            }
            else
            {
                nTemp = -1;
                CPL_LSBPTR32( &nTemp );
                CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));
                CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 400, SEEK_CUR ));
            }
        }
    }
    else
    {
        CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 4164, SEEK_SET ));
    }
    CPLFree( padfMin );
    CPLFree( padfMax );
    CPLFree( panFound );
    CPLFree( panClasses );

    // Should be at one of 3 locations, 2104, 3340, or 4164.
    CPLAssert( VSIFTellL( fp ) == 2104  ||
               VSIFTellL( fp ) == 3340  ||
               VSIFTellL( fp ) == 4164 );
    CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 4164, SEEK_SET ));

    /* Image size */
    nTemp = static_cast<GInt32>(nXSize);
    CPL_LSBPTR32( &nTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));
    nTemp = static_cast<GInt32>(nYSize);
    CPL_LSBPTR32( &nTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));

    // X and Y boundaries.
    // Max x.
    double dfTemp = adfSrcGeoTransform[0] + adfSrcGeoTransform[1] * nXSize;
    CPL_LSBPTR64( &dfTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfTemp, 8, 1, fp ));
    // Min x.
    dfTemp = adfSrcGeoTransform[0];
    CPL_LSBPTR64( &dfTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfTemp, 8, 1, fp ));
    // Max y.
    dfTemp = adfSrcGeoTransform[3];
    CPL_LSBPTR64( &dfTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfTemp, 8, 1, fp ));
    // Min y.
    dfTemp = adfSrcGeoTransform[3] + adfSrcGeoTransform[5] * nYSize;
    CPL_LSBPTR64( &dfTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfTemp, 8, 1, fp ));

    nTemp = nLinearUnits;
    CPL_LSBPTR32( &nTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &nTemp, 4, 1, fp ));

    // Resolution.
    // X resolution.
    dfTemp = adfSrcGeoTransform[1];
    CPL_LSBPTR64( &dfTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfTemp, 8, 1, fp ));
    // Y resolution.
    dfTemp = fabs( adfSrcGeoTransform[5] );
    CPL_LSBPTR64( &dfTemp );
    CPL_IGNORE_RET_VAL(VSIFWriteL( &dfTemp, 8, 1, fp ));

#ifdef CPL_MSB
    GDALSwapWords( panMetadata, 2, LCP_MAX_BANDS, 2 );
#endif
    CPL_IGNORE_RET_VAL(VSIFWriteL( panMetadata, 2, LCP_MAX_BANDS, fp ));

    // Write the source filenames.
    char **papszFileList = poSrcDS->GetFileList();
    if( papszFileList != nullptr )
    {
        for( int i = 0; i < nBands; i++ )
        {
            if( i == 5 && !bHaveCrownFuels && bHaveGroundFuels )
            {
                CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 6292, SEEK_SET ));
            }
            CPL_IGNORE_RET_VAL(VSIFWriteL( papszFileList[0], 1,
                        CPLStrnlen( papszFileList[0], LCP_MAX_PATH ), fp ));
            CPL_IGNORE_RET_VAL(
                VSIFSeekL( fp, 4244 + ( 256 * ( i+1 ) ), SEEK_SET ));
        }
    }
    // No file list, mem driver, etc.
    else
    {
        CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 6804, SEEK_SET ));
    }
    CSLDestroy( papszFileList );
    // Should be at location 5524, 6292 or 6804.
    CPLAssert( VSIFTellL( fp ) == 5524 ||
               VSIFTellL( fp ) == 6292 ||
               VSIFTellL( fp ) == 6804 );
    CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 6804, SEEK_SET ));

    // Description .
    CPL_IGNORE_RET_VAL(
        VSIFWriteL(
            pszDescription, 1, CPLStrnlen( pszDescription, LCP_MAX_DESC ),
            fp ));

    // Should be at or below location 7316, all done with the header.
    CPLAssert( VSIFTellL( fp ) <= 7316 );
    CPL_IGNORE_RET_VAL(VSIFSeekL( fp, 7316, SEEK_SET ));

/* -------------------------------------------------------------------- */
/*      Loop over image, copying image data.                            */
/* -------------------------------------------------------------------- */

    GInt16 *panScanline =
        static_cast<GInt16 *>( VSIMalloc3( 2, nBands, nXSize ) );

    if( !pfnProgress( 0.0, nullptr, pProgressData ) )
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL( fp ));
        VSIFree( panScanline );
        return nullptr;
    }
    for( int iLine = 0; iLine < nYSize; iLine++ )
    {
        for( int iBand = 0; iBand < nBands; iBand++ )
        {
            GDALRasterBand * poBand = poSrcDS->GetRasterBand( iBand+1 );
            eErr = poBand->RasterIO( GF_Read, 0, iLine, nXSize, 1,
                                     panScanline + iBand, nXSize, 1, GDT_Int16,
                                     nBands * 2, nBands * nXSize * 2, nullptr );
            // Not sure what to do here.
            if( eErr != CE_None )
            {
                CPLError( CE_Warning, CPLE_AppDefined, "Error reported in "
                                                       "RasterIO" );
            }
        }
#ifdef CPL_MSB
        GDALSwapWords( panScanline, 2, nBands * nXSize, 2 );
#endif
        CPL_IGNORE_RET_VAL(VSIFWriteL( panScanline, 2, nBands * nXSize, fp ));

        if( !pfnProgress( iLine / static_cast<double>(nYSize),
                          nullptr, pProgressData ) )
        {
            VSIFree( reinterpret_cast<void *>( panScanline ) );
            CPL_IGNORE_RET_VAL(VSIFCloseL( fp ));
            return nullptr;
        }
    }
    VSIFree( panScanline );
    CPL_IGNORE_RET_VAL(VSIFCloseL( fp ));
    if( !pfnProgress( 1.0, nullptr, pProgressData ) )
    {
        return nullptr;
    }

    // Try to write projection file.  *Most* landfire data follows ESRI
    // style projection files, so we use the same code as the AAIGrid driver.
    const char *pszOriginalProjection = poSrcDS->GetProjectionRef();
    if( !EQUAL( pszOriginalProjection, "" ) )
    {
        OGRSpatialReference oSRS;

        char * const pszDirname = CPLStrdup( CPLGetPath(pszFilename) );
        char * const pszBasename = CPLStrdup( CPLGetBasename(pszFilename) );

        char *pszPrjFilename =
            CPLStrdup( CPLFormFilename( pszDirname, pszBasename, "prj" ) );
        fp = VSIFOpenL( pszPrjFilename, "wt" );
        if (fp != nullptr)
        {
            oSRS.importFromWkt( pszOriginalProjection );
            oSRS.morphToESRI();
            char *pszESRIProjection = nullptr;
            oSRS.exportToWkt( &pszESRIProjection );
            CPL_IGNORE_RET_VAL(
                VSIFWriteL( pszESRIProjection, 1,
                            strlen(pszESRIProjection), fp ) );

            CPL_IGNORE_RET_VAL(VSIFCloseL( fp ));
            CPLFree( pszESRIProjection );
        }
        else
        {
            CPLError( CE_Failure, CPLE_FileIO,
                      "Unable to create file %s.", pszPrjFilename );
        }
        CPLFree( pszDirname );
        CPLFree( pszBasename );
        CPLFree( pszPrjFilename );
    }
    return static_cast<GDALDataset *>( GDALOpen( pszFilename, GA_ReadOnly ) );
}

/************************************************************************/
/*                          GetProjectionRef()                          */
/************************************************************************/

const char *LCPDataset::GetProjectionRef()

{
    return pszProjection;
}

/************************************************************************/
/*                         GDALRegister_LCP()                           */
/************************************************************************/

void GDALRegister_LCP()

{
    if( GDALGetDriverByName( "LCP" ) != nullptr )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "LCP" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME,
                               "FARSITE v.4 Landscape File (.lcp)" );
    poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "lcp" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "frmt_lcp.html" );

    poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );

    poDriver->SetMetadataItem( GDAL_DMD_CREATIONDATATYPES, "Int16" );
    poDriver->SetMetadataItem( GDAL_DMD_CREATIONOPTIONLIST,
"<CreationOptionList>"
"   <Option name='ELEVATION_UNIT' type='string-select' default='METERS' description='Elevation units'>"
"       <Value>METERS</Value>"
"       <Value>FEET</Value>"
"   </Option>"
"   <Option name='SLOPE_UNIT' type='string-select' default='DEGREES' description='Slope units'>"
"       <Value>DEGREES</Value>"
"       <Value>PERCENT</Value>"
"   </Option>"
"   <Option name='ASPECT_UNIT' type='string-select' default='AZIMUTH_DEGREES'>"
"       <Value>GRASS_CATEGORIES</Value>"
"       <Value>AZIMUTH_DEGREES</Value>"
"       <Value>GRASS_DEGREES</Value>"
"   </Option>"
"   <Option name='FUEL_MODEL_OPTION' type='string-select' default='NO_CUSTOM_AND_NO_FILE'>"
"       <Value>NO_CUSTOM_AND_NO_FILE</Value>"
"       <Value>CUSTOM_AND_NO_FILE</Value>"
"       <Value>NO_CUSTOM_AND_FILE</Value>"
"       <Value>CUSTOM_AND_FILE</Value>"
"   </Option>"
"   <Option name='CANOPY_COV_UNIT' type='string-select' default='PERCENT'>"
"       <Value>CATEGORIES</Value>"
"       <Value>PERCENT</Value>"
"   </Option>"
"   <Option name='CANOPY_HT_UNIT' type='string-select' default='METERS_X_10'>"
"       <Value>METERS</Value>"
"       <Value>FEET</Value>"
"       <Value>METERS_X_10</Value>"
"       <Value>FEET_X_10</Value>"
"   </Option>"
"   <Option name='CBH_UNIT' type='string-select' default='METERS_X_10'>"
"       <Value>METERS</Value>"
"       <Value>FEET</Value>"
"       <Value>METERS_X_10</Value>"
"       <Value>FEET_X_10</Value>"
"   </Option>"
"   <Option name='CBD_UNIT' type='string-select' default='KG_PER_CUBIC_METER_X_100'>"
"       <Value>KG_PER_CUBIC_METER</Value>"
"       <Value>POUND_PER_CUBIC_FOOT</Value>"
"       <Value>KG_PER_CUBIC_METER_X_100</Value>"
"       <Value>POUND_PER_CUBIC_FOOT_X_1000</Value>"
"   </Option>"
"   <Option name='DUFF_UNIT' type='string-select' default='MG_PER_HECTARE_X_10'>"
"       <Value>MG_PER_HECTARE_X_10</Value>"
"       <Value>TONS_PER_ACRE_X_10</Value>"
"   </Option>"
// Kyle does not think we need to override this, but maybe?
// "   <Option name='CWD_OPTION' type='boolean' default='FALSE' description='Override logic for setting the coarse woody presence'/>" */
"   <Option name='CALCULATE_STATS' type='boolean' default='YES' description='Write the stats to the lcp'/>"
"   <Option name='CLASSIFY_DATA' type='boolean' default='YES' description='Write the stats to the lcp'/>"
"   <Option name='LINEAR_UNIT' type='string-select' default='SET_FROM_SRS' description='Set the linear units in the lcp'>"
"       <Value>SET_FROM_SRS</Value>"
"       <Value>METER</Value>"
"       <Value>FOOT</Value>"
"       <Value>KILOMETER</Value>"
"   </Option>"
"   <Option name='LATITUDE' type='int' default='' description='Set the latitude for the dataset, this overrides the driver trying to set it programmatically in EPSG:4269'/>"
"   <Option name='DESCRIPTION' type='string' default='LCP file created by GDAL' description='A short description of the lcp file'/>"
"</CreationOptionList>" );

    poDriver->pfnOpen = LCPDataset::Open;
    poDriver->pfnCreateCopy = LCPDataset::CreateCopy;
    poDriver->pfnIdentify = LCPDataset::Identify;

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
