LCOV - code coverage report
Current view: top level - frmts/raw - mffdataset.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 332 451 73.6 %
Date: 2025-09-10 17:48:50 Functions: 14 19 73.7 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  GView
       4             :  * Purpose:  Implementation of Atlantis MFF Support
       5             :  * Author:   Frank Warmerdam, warmerda@home.com
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2000, Frank Warmerdam
       9             :  * Copyright (c) 2008-2012, Even Rouault <even dot rouault at spatialys.com>
      10             :  *
      11             :  * SPDX-License-Identifier: MIT
      12             :  ****************************************************************************/
      13             : 
      14             : #include "atlsci_spheroid.h"
      15             : #include "cpl_string.h"
      16             : #include "gdal_frmts.h"
      17             : #include "ogr_spatialref.h"
      18             : #include "rawdataset.h"
      19             : 
      20             : #include <cctype>
      21             : #include <cmath>
      22             : #include <algorithm>
      23             : 
      24             : enum
      25             : {
      26             :     MFFPRJ_NONE,
      27             :     MFFPRJ_LL,
      28             :     MFFPRJ_UTM,
      29             :     MFFPRJ_UNRECOGNIZED
      30             : };
      31             : 
      32             : /************************************************************************/
      33             : /* ==================================================================== */
      34             : /*                              MFFDataset                              */
      35             : /* ==================================================================== */
      36             : /************************************************************************/
      37             : 
      38             : class MFFDataset final : public RawDataset
      39             : {
      40             :     int nGCPCount;
      41             :     GDAL_GCP *pasGCPList;
      42             : 
      43             :     OGRSpatialReference m_oSRS{};
      44             :     OGRSpatialReference m_oGCPSRS{};
      45             :     GDALGeoTransform m_gt{};
      46             :     char **m_papszFileList;
      47             : 
      48             :     void ScanForGCPs();
      49             :     void ScanForProjectionInfo();
      50             : 
      51             :     CPL_DISALLOW_COPY_ASSIGN(MFFDataset)
      52             : 
      53             :     CPLErr Close() override;
      54             : 
      55             :   public:
      56             :     MFFDataset();
      57             :     ~MFFDataset() override;
      58             : 
      59             :     char **papszHdrLines;
      60             : 
      61             :     VSILFILE **pafpBandFiles;
      62             : 
      63             :     char **GetFileList() override;
      64             : 
      65             :     int GetGCPCount() override;
      66             : 
      67           0 :     const OGRSpatialReference *GetGCPSpatialRef() const override
      68             :     {
      69           0 :         return m_oGCPSRS.IsEmpty() ? nullptr : &m_oGCPSRS;
      70             :     }
      71             : 
      72             :     const GDAL_GCP *GetGCPs() override;
      73             : 
      74           0 :     const OGRSpatialReference *GetSpatialRef() const override
      75             :     {
      76           0 :         return m_oSRS.IsEmpty() ? nullptr : &m_oSRS;
      77             :     }
      78             : 
      79             :     CPLErr GetGeoTransform(GDALGeoTransform &gt) const override;
      80             : 
      81             :     static GDALDataset *Open(GDALOpenInfo *);
      82             : };
      83             : 
      84             : /************************************************************************/
      85             : /* ==================================================================== */
      86             : /*                            MFFTiledBand                              */
      87             : /* ==================================================================== */
      88             : /************************************************************************/
      89             : 
      90             : class MFFTiledBand final : public GDALRasterBand
      91             : {
      92             :     friend class MFFDataset;
      93             : 
      94             :     VSILFILE *fpRaw;
      95             :     RawRasterBand::ByteOrder eByteOrder;
      96             : 
      97             :     CPL_DISALLOW_COPY_ASSIGN(MFFTiledBand)
      98             : 
      99             :   public:
     100             :     MFFTiledBand(MFFDataset *, int, VSILFILE *, int, int, GDALDataType,
     101             :                  RawRasterBand::ByteOrder);
     102             :     ~MFFTiledBand() override;
     103             : 
     104             :     CPLErr IReadBlock(int, int, void *) override;
     105             : };
     106             : 
     107             : /************************************************************************/
     108             : /*                            MFFTiledBand()                            */
     109             : /************************************************************************/
     110             : 
     111           2 : MFFTiledBand::MFFTiledBand(MFFDataset *poDSIn, int nBandIn, VSILFILE *fp,
     112             :                            int nTileXSize, int nTileYSize,
     113             :                            GDALDataType eDataTypeIn,
     114           2 :                            RawRasterBand::ByteOrder eByteOrderIn)
     115           2 :     : fpRaw(fp), eByteOrder(eByteOrderIn)
     116             : {
     117           2 :     poDS = poDSIn;
     118           2 :     nBand = nBandIn;
     119             : 
     120           2 :     eDataType = eDataTypeIn;
     121             : 
     122           2 :     nBlockXSize = nTileXSize;
     123           2 :     nBlockYSize = nTileYSize;
     124           2 : }
     125             : 
     126             : /************************************************************************/
     127             : /*                           ~MFFTiledBand()                            */
     128             : /************************************************************************/
     129             : 
     130           4 : MFFTiledBand::~MFFTiledBand()
     131             : 
     132             : {
     133           2 :     if (VSIFCloseL(fpRaw) != 0)
     134             :     {
     135           0 :         CPLError(CE_Failure, CPLE_FileIO, "I/O error");
     136             :     }
     137           4 : }
     138             : 
     139             : /************************************************************************/
     140             : /*                             IReadBlock()                             */
     141             : /************************************************************************/
     142             : 
     143           1 : CPLErr MFFTiledBand::IReadBlock(int nBlockXOff, int nBlockYOff, void *pImage)
     144             : 
     145             : {
     146           1 :     const int nTilesPerRow = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
     147           1 :     const int nWordSize = GDALGetDataTypeSizeBytes(eDataType);
     148           1 :     const int nBlockSize = nWordSize * nBlockXSize * nBlockYSize;
     149             : 
     150           1 :     const vsi_l_offset nOffset =
     151           1 :         nBlockSize *
     152           1 :         (nBlockXOff + static_cast<vsi_l_offset>(nBlockYOff) * nTilesPerRow);
     153             : 
     154           2 :     if (VSIFSeekL(fpRaw, nOffset, SEEK_SET) == -1 ||
     155           1 :         VSIFReadL(pImage, 1, nBlockSize, fpRaw) < 1)
     156             :     {
     157           0 :         CPLError(CE_Failure, CPLE_FileIO,
     158             :                  "Read of tile %d/%d failed with fseek or fread error.",
     159             :                  nBlockXOff, nBlockYOff);
     160           0 :         return CE_Failure;
     161             :     }
     162             : 
     163           1 :     if (eByteOrder != RawRasterBand::NATIVE_BYTE_ORDER && nWordSize > 1)
     164             :     {
     165           0 :         if (GDALDataTypeIsComplex(eDataType))
     166             :         {
     167           0 :             GDALSwapWords(pImage, nWordSize / 2, nBlockXSize * nBlockYSize,
     168             :                           nWordSize);
     169           0 :             GDALSwapWords(reinterpret_cast<GByte *>(pImage) + nWordSize / 2,
     170           0 :                           nWordSize / 2, nBlockXSize * nBlockYSize, nWordSize);
     171             :         }
     172             :         else
     173           0 :             GDALSwapWords(pImage, nWordSize, nBlockXSize * nBlockYSize,
     174             :                           nWordSize);
     175             :     }
     176             : 
     177           1 :     return CE_None;
     178             : }
     179             : 
     180             : /************************************************************************/
     181             : /*                      MFF Spheroids                                   */
     182             : /************************************************************************/
     183             : 
     184             : class MFFSpheroidList final : public SpheroidList
     185             : {
     186             :   public:
     187             :     MFFSpheroidList();
     188             : };
     189             : 
     190           6 : MFFSpheroidList ::MFFSpheroidList()
     191             : {
     192           6 :     num_spheroids = 18;
     193             : 
     194           6 :     epsilonR = 0.1;
     195           6 :     epsilonI = 0.000001;
     196             : 
     197           6 :     spheroids[0].SetValuesByRadii("SPHERE", 6371007.0, 6371007.0);
     198           6 :     spheroids[1].SetValuesByRadii("EVEREST", 6377304.0, 6356103.0);
     199           6 :     spheroids[2].SetValuesByRadii("BESSEL", 6377397.0, 6356082.0);
     200           6 :     spheroids[3].SetValuesByRadii("AIRY", 6377563.0, 6356300.0);
     201           6 :     spheroids[4].SetValuesByRadii("CLARKE_1858", 6378294.0, 6356621.0);
     202           6 :     spheroids[5].SetValuesByRadii("CLARKE_1866", 6378206.4, 6356583.8);
     203           6 :     spheroids[6].SetValuesByRadii("CLARKE_1880", 6378249.0, 6356517.0);
     204           6 :     spheroids[7].SetValuesByRadii("HAYFORD", 6378388.0, 6356915.0);
     205           6 :     spheroids[8].SetValuesByRadii("KRASOVSKI", 6378245.0, 6356863.0);
     206           6 :     spheroids[9].SetValuesByRadii("HOUGH", 6378270.0, 6356794.0);
     207           6 :     spheroids[10].SetValuesByRadii("FISHER_60", 6378166.0, 6356784.0);
     208           6 :     spheroids[11].SetValuesByRadii("KAULA", 6378165.0, 6356345.0);
     209           6 :     spheroids[12].SetValuesByRadii("IUGG_67", 6378160.0, 6356775.0);
     210           6 :     spheroids[13].SetValuesByRadii("FISHER_68", 6378150.0, 6356330.0);
     211           6 :     spheroids[14].SetValuesByRadii("WGS_72", 6378135.0, 6356751.0);
     212           6 :     spheroids[15].SetValuesByRadii("IUGG_75", 6378140.0, 6356755.0);
     213           6 :     spheroids[16].SetValuesByRadii("WGS_84", 6378137.0, 6356752.0);
     214           6 :     spheroids[17].SetValuesByRadii("HUGHES", 6378273.0, 6356889.4);
     215           6 : }
     216             : 
     217             : /************************************************************************/
     218             : /* ==================================================================== */
     219             : /*                              MFFDataset                              */
     220             : /* ==================================================================== */
     221             : /************************************************************************/
     222             : 
     223             : /************************************************************************/
     224             : /*                            MFFDataset()                             */
     225             : /************************************************************************/
     226             : 
     227           8 : MFFDataset::MFFDataset()
     228             :     : nGCPCount(0), pasGCPList(nullptr), m_papszFileList(nullptr),
     229           8 :       papszHdrLines(nullptr), pafpBandFiles(nullptr)
     230             : {
     231           8 :     m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     232           8 :     m_oGCPSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     233           8 : }
     234             : 
     235             : /************************************************************************/
     236             : /*                            ~MFFDataset()                            */
     237             : /************************************************************************/
     238             : 
     239          16 : MFFDataset::~MFFDataset()
     240             : 
     241             : {
     242           8 :     MFFDataset::Close();
     243          16 : }
     244             : 
     245             : /************************************************************************/
     246             : /*                              Close()                                 */
     247             : /************************************************************************/
     248             : 
     249          15 : CPLErr MFFDataset::Close()
     250             : {
     251          15 :     CPLErr eErr = CE_None;
     252          15 :     if (nOpenFlags != OPEN_FLAGS_CLOSED)
     253             :     {
     254           8 :         if (MFFDataset::FlushCache(true) != CE_None)
     255           0 :             eErr = CE_Failure;
     256             : 
     257           8 :         CSLDestroy(papszHdrLines);
     258           8 :         if (pafpBandFiles)
     259             :         {
     260           0 :             for (int i = 0; i < GetRasterCount(); i++)
     261             :             {
     262           0 :                 if (pafpBandFiles[i])
     263             :                 {
     264           0 :                     if (VSIFCloseL(pafpBandFiles[i]) != 0)
     265             :                     {
     266           0 :                         eErr = CE_Failure;
     267           0 :                         CPLError(CE_Failure, CPLE_FileIO, "I/O error");
     268             :                     }
     269             :                 }
     270             :             }
     271           0 :             CPLFree(pafpBandFiles);
     272             :         }
     273             : 
     274           8 :         if (nGCPCount > 0)
     275             :         {
     276           6 :             GDALDeinitGCPs(nGCPCount, pasGCPList);
     277             :         }
     278           8 :         CPLFree(pasGCPList);
     279           8 :         CSLDestroy(m_papszFileList);
     280             : 
     281           8 :         if (GDALPamDataset::Close() != CE_None)
     282           0 :             eErr = CE_Failure;
     283             :     }
     284          15 :     return eErr;
     285             : }
     286             : 
     287             : /************************************************************************/
     288             : /*                            GetFileList()                             */
     289             : /************************************************************************/
     290             : 
     291           3 : char **MFFDataset::GetFileList()
     292             : {
     293           3 :     char **papszFileList = RawDataset::GetFileList();
     294           3 :     papszFileList = CSLInsertStrings(papszFileList, -1, m_papszFileList);
     295           3 :     return papszFileList;
     296             : }
     297             : 
     298             : /************************************************************************/
     299             : /*                            GetGCPCount()                             */
     300             : /************************************************************************/
     301             : 
     302           0 : int MFFDataset::GetGCPCount()
     303             : 
     304             : {
     305           0 :     return nGCPCount;
     306             : }
     307             : 
     308             : /************************************************************************/
     309             : /*                          GetGeoTransform()                           */
     310             : /************************************************************************/
     311             : 
     312           0 : CPLErr MFFDataset::GetGeoTransform(GDALGeoTransform &gt) const
     313             : {
     314           0 :     gt = m_gt;
     315           0 :     return CE_None;
     316             : }
     317             : 
     318             : /************************************************************************/
     319             : /*                               GetGCP()                               */
     320             : /************************************************************************/
     321             : 
     322           0 : const GDAL_GCP *MFFDataset::GetGCPs()
     323             : 
     324             : {
     325           0 :     return pasGCPList;
     326             : }
     327             : 
     328             : /************************************************************************/
     329             : /*                            ScanForGCPs()                             */
     330             : /************************************************************************/
     331             : 
     332           7 : void MFFDataset::ScanForGCPs()
     333             : 
     334             : {
     335           7 :     int NUM_GCPS = 0;
     336             : 
     337           7 :     if (CSLFetchNameValue(papszHdrLines, "NUM_GCPS") != nullptr)
     338           4 :         NUM_GCPS = atoi(CSLFetchNameValue(papszHdrLines, "NUM_GCPS"));
     339           7 :     if (NUM_GCPS < 0)
     340           0 :         return;
     341             : 
     342           7 :     nGCPCount = 0;
     343           7 :     pasGCPList =
     344           7 :         static_cast<GDAL_GCP *>(VSICalloc(sizeof(GDAL_GCP), 5 + NUM_GCPS));
     345           7 :     if (pasGCPList == nullptr)
     346           0 :         return;
     347             : 
     348          42 :     for (int nCorner = 0; nCorner < 5; nCorner++)
     349             :     {
     350          35 :         const char *pszBase = nullptr;
     351          35 :         double dfRasterX = 0.0;
     352          35 :         double dfRasterY = 0.0;
     353             : 
     354          35 :         if (nCorner == 0)
     355             :         {
     356           7 :             dfRasterX = 0.5;
     357           7 :             dfRasterY = 0.5;
     358           7 :             pszBase = "TOP_LEFT_CORNER";
     359             :         }
     360          28 :         else if (nCorner == 1)
     361             :         {
     362           7 :             dfRasterX = GetRasterXSize() - 0.5;
     363           7 :             dfRasterY = 0.5;
     364           7 :             pszBase = "TOP_RIGHT_CORNER";
     365             :         }
     366          21 :         else if (nCorner == 2)
     367             :         {
     368           7 :             dfRasterX = GetRasterXSize() - 0.5;
     369           7 :             dfRasterY = GetRasterYSize() - 0.5;
     370           7 :             pszBase = "BOTTOM_RIGHT_CORNER";
     371             :         }
     372          14 :         else if (nCorner == 3)
     373             :         {
     374           7 :             dfRasterX = 0.5;
     375           7 :             dfRasterY = GetRasterYSize() - 0.5;
     376           7 :             pszBase = "BOTTOM_LEFT_CORNER";
     377             :         }
     378             :         else /* if( nCorner == 4 ) */
     379             :         {
     380           7 :             dfRasterX = GetRasterXSize() / 2.0;
     381           7 :             dfRasterY = GetRasterYSize() / 2.0;
     382           7 :             pszBase = "CENTRE";
     383             :         }
     384             : 
     385          35 :         char szLatName[40] = {'\0'};
     386          35 :         char szLongName[40] = {'\0'};
     387          35 :         snprintf(szLatName, sizeof(szLatName), "%s_LATITUDE", pszBase);
     388          35 :         snprintf(szLongName, sizeof(szLongName), "%s_LONGITUDE", pszBase);
     389             : 
     390          45 :         if (CSLFetchNameValue(papszHdrLines, szLatName) != nullptr &&
     391          10 :             CSLFetchNameValue(papszHdrLines, szLongName) != nullptr)
     392             :         {
     393          10 :             GDALInitGCPs(1, pasGCPList + nGCPCount);
     394             : 
     395          10 :             CPLFree(pasGCPList[nGCPCount].pszId);
     396             : 
     397          10 :             pasGCPList[nGCPCount].pszId = CPLStrdup(pszBase);
     398             : 
     399          20 :             pasGCPList[nGCPCount].dfGCPX =
     400          10 :                 CPLAtof(CSLFetchNameValue(papszHdrLines, szLongName));
     401          20 :             pasGCPList[nGCPCount].dfGCPY =
     402          10 :                 CPLAtof(CSLFetchNameValue(papszHdrLines, szLatName));
     403          10 :             pasGCPList[nGCPCount].dfGCPZ = 0.0;
     404             : 
     405          10 :             pasGCPList[nGCPCount].dfGCPPixel = dfRasterX;
     406          10 :             pasGCPList[nGCPCount].dfGCPLine = dfRasterY;
     407             : 
     408          10 :             nGCPCount++;
     409             :         }
     410             :     }
     411             : 
     412             :     /* -------------------------------------------------------------------- */
     413             :     /*      Collect standalone GCPs.  They look like:                       */
     414             :     /*                                                                      */
     415             :     /*      GCPn = row, col, lat, long                                      */
     416             :     /*      GCP1 = 1, 1, 45.0, -75.0                                        */
     417             :     /* -------------------------------------------------------------------- */
     418          11 :     for (int i = 0; i < NUM_GCPS; i++)
     419             :     {
     420           4 :         char szName[25] = {'\0'};
     421           4 :         snprintf(szName, sizeof(szName), "GCP%d", i + 1);
     422           4 :         if (CSLFetchNameValue(papszHdrLines, szName) == nullptr)
     423           0 :             continue;
     424             : 
     425           4 :         char **papszTokens = CSLTokenizeStringComplex(
     426           4 :             CSLFetchNameValue(papszHdrLines, szName), ",", FALSE, FALSE);
     427           4 :         if (CSLCount(papszTokens) == 4)
     428             :         {
     429           4 :             GDALInitGCPs(1, pasGCPList + nGCPCount);
     430             : 
     431           4 :             CPLFree(pasGCPList[nGCPCount].pszId);
     432           4 :             pasGCPList[nGCPCount].pszId = CPLStrdup(szName);
     433             : 
     434           4 :             pasGCPList[nGCPCount].dfGCPX = CPLAtof(papszTokens[3]);
     435           4 :             pasGCPList[nGCPCount].dfGCPY = CPLAtof(papszTokens[2]);
     436           4 :             pasGCPList[nGCPCount].dfGCPZ = 0.0;
     437           4 :             pasGCPList[nGCPCount].dfGCPPixel = CPLAtof(papszTokens[1]) + 0.5;
     438           4 :             pasGCPList[nGCPCount].dfGCPLine = CPLAtof(papszTokens[0]) + 0.5;
     439             : 
     440           4 :             nGCPCount++;
     441             :         }
     442             : 
     443           4 :         CSLDestroy(papszTokens);
     444             :     }
     445             : }
     446             : 
     447             : /************************************************************************/
     448             : /*                        ScanForProjectionInfo                         */
     449             : /************************************************************************/
     450             : 
     451           7 : void MFFDataset::ScanForProjectionInfo()
     452             : {
     453             :     const char *pszProjName =
     454           7 :         CSLFetchNameValue(papszHdrLines, "PROJECTION_NAME");
     455             :     const char *pszOriginLong =
     456           7 :         CSLFetchNameValue(papszHdrLines, "PROJECTION_ORIGIN_LONGITUDE");
     457             :     const char *pszSpheroidName =
     458           7 :         CSLFetchNameValue(papszHdrLines, "SPHEROID_NAME");
     459             : 
     460           7 :     if (pszProjName == nullptr)
     461             :     {
     462           1 :         m_oSRS.Clear();
     463           1 :         m_oGCPSRS.Clear();
     464           1 :         return;
     465             :     }
     466           6 :     else if ((!EQUAL(pszProjName, "utm")) && (!EQUAL(pszProjName, "ll")))
     467             :     {
     468           0 :         CPLError(CE_Warning, CPLE_AppDefined,
     469             :                  "Only utm and lat/long projections are currently supported.");
     470           0 :         m_oSRS.Clear();
     471           0 :         m_oGCPSRS.Clear();
     472           0 :         return;
     473             :     }
     474           6 :     MFFSpheroidList *mffEllipsoids = new MFFSpheroidList;
     475             : 
     476          12 :     OGRSpatialReference oProj;
     477           6 :     oProj.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     478           6 :     if (EQUAL(pszProjName, "utm"))
     479             :     {
     480             :         int nZone;
     481             : 
     482           6 :         if (pszOriginLong == nullptr)
     483             :         {
     484             :             // If origin not specified, assume 0.0.
     485           4 :             CPLError(
     486             :                 CE_Warning, CPLE_AppDefined,
     487             :                 "No projection origin longitude specified.  Assuming 0.0.");
     488           4 :             nZone = 31;
     489             :         }
     490             :         else
     491           2 :             nZone = 31 + static_cast<int>(floor(CPLAtof(pszOriginLong) / 6.0));
     492             : 
     493           6 :         if (nGCPCount >= 5 && pasGCPList[4].dfGCPY < 0)
     494           0 :             oProj.SetUTM(nZone, 0);
     495             :         else
     496           6 :             oProj.SetUTM(nZone, 1);
     497             : 
     498           6 :         if (pszOriginLong != nullptr)
     499           2 :             oProj.SetProjParm(SRS_PP_CENTRAL_MERIDIAN, CPLAtof(pszOriginLong));
     500             :     }
     501             : 
     502          12 :     OGRSpatialReference oLL;
     503           6 :     oLL.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     504           6 :     if (pszOriginLong != nullptr)
     505           2 :         oLL.SetProjParm(SRS_PP_LONGITUDE_OF_ORIGIN, CPLAtof(pszOriginLong));
     506             : 
     507           6 :     if (pszSpheroidName == nullptr)
     508             :     {
     509           4 :         CPLError(CE_Warning, CPLE_AppDefined,
     510             :                  "Unspecified ellipsoid.  Using wgs-84 parameters.\n");
     511             : 
     512           4 :         oProj.SetWellKnownGeogCS("WGS84");
     513           4 :         oLL.SetWellKnownGeogCS("WGS84");
     514             :     }
     515             :     else
     516             :     {
     517           2 :         if (mffEllipsoids->SpheroidInList(pszSpheroidName))
     518             :         {
     519           2 :             oProj.SetGeogCS(
     520             :                 "unknown", "unknown", pszSpheroidName,
     521             :                 mffEllipsoids->GetSpheroidEqRadius(pszSpheroidName),
     522             :                 mffEllipsoids->GetSpheroidInverseFlattening(pszSpheroidName));
     523           2 :             oLL.SetGeogCS(
     524             :                 "unknown", "unknown", pszSpheroidName,
     525             :                 mffEllipsoids->GetSpheroidEqRadius(pszSpheroidName),
     526             :                 mffEllipsoids->GetSpheroidInverseFlattening(pszSpheroidName));
     527             :         }
     528           0 :         else if (EQUAL(pszSpheroidName, "USER_DEFINED"))
     529             :         {
     530             :             const char *pszSpheroidEqRadius =
     531           0 :                 CSLFetchNameValue(papszHdrLines, "SPHEROID_EQUATORIAL_RADIUS");
     532             :             const char *pszSpheroidPolarRadius =
     533           0 :                 CSLFetchNameValue(papszHdrLines, "SPHEROID_POLAR_RADIUS");
     534           0 :             if ((pszSpheroidEqRadius != nullptr) &&
     535             :                 (pszSpheroidPolarRadius != nullptr))
     536             :             {
     537           0 :                 const double eq_radius = CPLAtof(pszSpheroidEqRadius);
     538           0 :                 const double polar_radius = CPLAtof(pszSpheroidPolarRadius);
     539           0 :                 oProj.SetGeogCS("unknown", "unknown", "unknown", eq_radius,
     540           0 :                                 eq_radius / (eq_radius - polar_radius));
     541           0 :                 oLL.SetGeogCS("unknown", "unknown", "unknown", eq_radius,
     542           0 :                               eq_radius / (eq_radius - polar_radius));
     543             :             }
     544             :             else
     545             :             {
     546           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     547             :                          "Radii not specified for user-defined ellipsoid. "
     548             :                          "Using wgs-84 parameters.");
     549           0 :                 oProj.SetWellKnownGeogCS("WGS84");
     550           0 :                 oLL.SetWellKnownGeogCS("WGS84");
     551             :             }
     552             :         }
     553             :         else
     554             :         {
     555           0 :             CPLError(CE_Warning, CPLE_AppDefined,
     556             :                      "Unrecognized ellipsoid.  Using wgs-84 parameters.");
     557           0 :             oProj.SetWellKnownGeogCS("WGS84");
     558           0 :             oLL.SetWellKnownGeogCS("WGS84");
     559             :         }
     560             :     }
     561             : 
     562             :     /* If a geotransform is sufficient to represent the GCP's (i.e. each */
     563             :     /* estimated gcp is within 0.25*pixel size of the actual value- this */
     564             :     /* is the test applied by GDALGCPsToGeoTransform), store the         */
     565             :     /* geotransform.                                                     */
     566           6 :     bool transform_ok = false;
     567             : 
     568           6 :     if (EQUAL(pszProjName, "LL"))
     569             :     {
     570           0 :         transform_ok = CPL_TO_BOOL(
     571           0 :             GDALGCPsToGeoTransform(nGCPCount, pasGCPList, m_gt.data(), 0));
     572             :     }
     573             :     else
     574             :     {
     575             :         OGRCoordinateTransformation *poTransform =
     576           6 :             OGRCreateCoordinateTransformation(&oLL, &oProj);
     577           6 :         bool bSuccess = true;
     578           6 :         if (poTransform == nullptr)
     579             :         {
     580           0 :             CPLErrorReset();
     581           0 :             bSuccess = FALSE;
     582             :         }
     583             : 
     584             :         double *dfPrjX =
     585           6 :             static_cast<double *>(CPLMalloc(nGCPCount * sizeof(double)));
     586             :         double *dfPrjY =
     587           6 :             static_cast<double *>(CPLMalloc(nGCPCount * sizeof(double)));
     588             : 
     589          20 :         for (int gcp_index = 0; gcp_index < nGCPCount; gcp_index++)
     590             :         {
     591          14 :             dfPrjX[gcp_index] = pasGCPList[gcp_index].dfGCPX;
     592          14 :             dfPrjY[gcp_index] = pasGCPList[gcp_index].dfGCPY;
     593             : 
     594          28 :             if (bSuccess && !poTransform->Transform(1, &(dfPrjX[gcp_index]),
     595          14 :                                                     &(dfPrjY[gcp_index])))
     596           0 :                 bSuccess = FALSE;
     597             :         }
     598             : 
     599           6 :         if (bSuccess)
     600             :         {
     601          20 :             for (int gcp_index = 0; gcp_index < nGCPCount; gcp_index++)
     602             :             {
     603          14 :                 pasGCPList[gcp_index].dfGCPX = dfPrjX[gcp_index];
     604          14 :                 pasGCPList[gcp_index].dfGCPY = dfPrjY[gcp_index];
     605             :             }
     606          12 :             transform_ok = CPL_TO_BOOL(
     607           6 :                 GDALGCPsToGeoTransform(nGCPCount, pasGCPList, m_gt.data(), 0));
     608             :         }
     609             : 
     610           6 :         if (poTransform)
     611           6 :             delete poTransform;
     612             : 
     613           6 :         CPLFree(dfPrjX);
     614           6 :         CPLFree(dfPrjY);
     615             :     }
     616             : 
     617           6 :     m_oSRS = oProj;
     618           6 :     m_oGCPSRS = std::move(oProj);
     619             : 
     620           6 :     if (!transform_ok)
     621             :     {
     622             :         /* transform is sufficient in some cases (slant range, standalone gcps)
     623             :          */
     624           4 :         m_gt = GDALGeoTransform();
     625           4 :         m_oSRS.Clear();
     626             :     }
     627             : 
     628           6 :     delete mffEllipsoids;
     629             : }
     630             : 
     631             : /************************************************************************/
     632             : /*                                Open()                                */
     633             : /************************************************************************/
     634             : 
     635       33903 : GDALDataset *MFFDataset::Open(GDALOpenInfo *poOpenInfo)
     636             : 
     637             : {
     638             :     /* -------------------------------------------------------------------- */
     639             :     /*      We assume the user is pointing to the header file.              */
     640             :     /* -------------------------------------------------------------------- */
     641       33903 :     if (poOpenInfo->nHeaderBytes < 17 || poOpenInfo->fpL == nullptr)
     642       30508 :         return nullptr;
     643             : 
     644        3395 :     if (!poOpenInfo->IsExtensionEqualToCI("hdr"))
     645        3377 :         return nullptr;
     646             : 
     647             :     /* -------------------------------------------------------------------- */
     648             :     /*      Load the .hdr file, and compress white space out around the     */
     649             :     /*      equal sign.                                                     */
     650             :     /* -------------------------------------------------------------------- */
     651          18 :     char **papszHdrLines = CSLLoad(poOpenInfo->pszFilename);
     652          18 :     if (papszHdrLines == nullptr)
     653           0 :         return nullptr;
     654             : 
     655             :     // Remove spaces.  e.g.
     656             :     // SPHEROID_NAME = CLARKE_1866 -> SPHEROID_NAME=CLARKE_1866
     657         577 :     for (int i = 0; papszHdrLines[i] != nullptr; i++)
     658             :     {
     659         559 :         int iDst = 0;
     660         559 :         char *pszLine = papszHdrLines[i];
     661             : 
     662       16291 :         for (int iSrc = 0; pszLine[iSrc] != '\0'; iSrc++)
     663             :         {
     664       15732 :             if (pszLine[iSrc] != ' ')
     665             :             {
     666       13778 :                 pszLine[iDst++] = pszLine[iSrc];
     667             :             }
     668             :         }
     669         559 :         pszLine[iDst] = '\0';
     670             :     }
     671             : 
     672             :     /* -------------------------------------------------------------------- */
     673             :     /*      Verify it is an MFF file.                                       */
     674             :     /* -------------------------------------------------------------------- */
     675          26 :     if (CSLFetchNameValue(papszHdrLines, "IMAGE_FILE_FORMAT") != nullptr &&
     676           8 :         !EQUAL(CSLFetchNameValue(papszHdrLines, "IMAGE_FILE_FORMAT"), "MFF"))
     677             :     {
     678           0 :         CSLDestroy(papszHdrLines);
     679           0 :         return nullptr;
     680             :     }
     681             : 
     682          18 :     if ((CSLFetchNameValue(papszHdrLines, "IMAGE_LINES") == nullptr ||
     683          28 :          CSLFetchNameValue(papszHdrLines, "LINE_SAMPLES") == nullptr) &&
     684          10 :         (CSLFetchNameValue(papszHdrLines, "no_rows") == nullptr ||
     685           0 :          CSLFetchNameValue(papszHdrLines, "no_columns") == nullptr))
     686             :     {
     687          10 :         CSLDestroy(papszHdrLines);
     688          10 :         return nullptr;
     689             :     }
     690             : 
     691             :     /* -------------------------------------------------------------------- */
     692             :     /*      Create a corresponding GDALDataset.                             */
     693             :     /* -------------------------------------------------------------------- */
     694          16 :     auto poDS = std::make_unique<MFFDataset>();
     695             : 
     696           8 :     poDS->papszHdrLines = papszHdrLines;
     697             : 
     698           8 :     poDS->eAccess = poOpenInfo->eAccess;
     699             : 
     700             :     /* -------------------------------------------------------------------- */
     701             :     /*      Set some dataset wide information.                              */
     702             :     /* -------------------------------------------------------------------- */
     703          11 :     if (CSLFetchNameValue(papszHdrLines, "no_rows") != nullptr &&
     704           3 :         CSLFetchNameValue(papszHdrLines, "no_columns") != nullptr)
     705             :     {
     706           0 :         poDS->nRasterXSize =
     707           0 :             atoi(CSLFetchNameValue(papszHdrLines, "no_columns"));
     708           0 :         poDS->nRasterYSize = atoi(CSLFetchNameValue(papszHdrLines, "no_rows"));
     709             :     }
     710             :     else
     711             :     {
     712          16 :         poDS->nRasterXSize =
     713           8 :             atoi(CSLFetchNameValue(papszHdrLines, "LINE_SAMPLES"));
     714           8 :         poDS->nRasterYSize =
     715           8 :             atoi(CSLFetchNameValue(papszHdrLines, "IMAGE_LINES"));
     716             :     }
     717             : 
     718           8 :     if (!GDALCheckDatasetDimensions(poDS->nRasterXSize, poDS->nRasterYSize))
     719             :     {
     720           0 :         return nullptr;
     721             :     }
     722             : 
     723           8 :     RawRasterBand::ByteOrder eByteOrder = RawRasterBand::NATIVE_BYTE_ORDER;
     724             : 
     725           8 :     const char *pszByteOrder = CSLFetchNameValue(papszHdrLines, "BYTE_ORDER");
     726           8 :     if (pszByteOrder)
     727             :     {
     728           3 :         eByteOrder = EQUAL(pszByteOrder, "LSB")
     729           3 :                          ? RawRasterBand::ByteOrder::ORDER_LITTLE_ENDIAN
     730             :                          : RawRasterBand::ByteOrder::ORDER_BIG_ENDIAN;
     731             :     }
     732             : 
     733             :     /* -------------------------------------------------------------------- */
     734             :     /*      Get some information specific to APP tiled files.               */
     735             :     /* -------------------------------------------------------------------- */
     736           8 :     int nTileXSize = 0;
     737           8 :     int nTileYSize = 0;
     738           8 :     const char *pszRefinedType = CSLFetchNameValue(papszHdrLines, "type");
     739           8 :     const bool bTiled = CSLFetchNameValue(papszHdrLines, "no_rows") != nullptr;
     740             : 
     741           8 :     if (bTiled)
     742             :     {
     743           3 :         if (CSLFetchNameValue(papszHdrLines, "tile_size_rows"))
     744           3 :             nTileYSize =
     745           3 :                 atoi(CSLFetchNameValue(papszHdrLines, "tile_size_rows"));
     746           3 :         if (CSLFetchNameValue(papszHdrLines, "tile_size_columns"))
     747           3 :             nTileXSize =
     748           3 :                 atoi(CSLFetchNameValue(papszHdrLines, "tile_size_columns"));
     749             : 
     750           3 :         if (nTileXSize <= 0 || nTileYSize <= 0 ||
     751           9 :             poDS->nRasterXSize - 1 > INT_MAX - nTileXSize ||
     752           3 :             poDS->nRasterYSize - 1 > INT_MAX - nTileYSize)
     753             :         {
     754           0 :             return nullptr;
     755             :         }
     756             :     }
     757             : 
     758             :     /* -------------------------------------------------------------------- */
     759             :     /*      Read the directory to find matching band files.                 */
     760             :     /* -------------------------------------------------------------------- */
     761          16 :     const std::string osTargetPath = CPLGetPathSafe(poOpenInfo->pszFilename);
     762             :     const std::string osTargetBase =
     763          16 :         CPLGetBasenameSafe(poOpenInfo->pszFilename);
     764             :     const CPLStringList aosDirFiles(
     765          16 :         VSIReadDir(CPLGetPathSafe(poOpenInfo->pszFilename).c_str()));
     766           8 :     if (aosDirFiles.empty())
     767             :     {
     768           0 :         return nullptr;
     769             :     }
     770             : 
     771           8 :     int nSkipped = 0;
     772           8 :     for (int nRawBand = 0; true; nRawBand++)
     773             :     {
     774             :         /* Find the next raw band file. */
     775             : 
     776          15 :         int i = 0;  // Used after for.
     777          87 :         for (; i < aosDirFiles.size(); i++)
     778             :         {
     779          80 :             if (!EQUAL(CPLGetBasenameSafe(aosDirFiles[i]).c_str(),
     780             :                        osTargetBase.c_str()))
     781          55 :                 continue;
     782             : 
     783          25 :             const std::string osExtension = CPLGetExtensionSafe(aosDirFiles[i]);
     784          25 :             if (osExtension.size() >= 2 &&
     785          25 :                 isdigit(static_cast<unsigned char>(osExtension[1])) &&
     786          58 :                 atoi(osExtension.c_str() + 1) == nRawBand &&
     787           8 :                 strchr("bBcCiIjJrRxXzZ", osExtension[0]) != nullptr)
     788           8 :                 break;
     789             :         }
     790             : 
     791          15 :         if (i == aosDirFiles.size())
     792           7 :             break;
     793             : 
     794             :         /* open the file for required level of access */
     795             :         const std::string osRawFilename =
     796           8 :             CPLFormFilenameSafe(osTargetPath.c_str(), aosDirFiles[i], nullptr);
     797             : 
     798           8 :         VSILFILE *fpRaw = nullptr;
     799           8 :         if (poOpenInfo->eAccess == GA_Update)
     800           0 :             fpRaw = VSIFOpenL(osRawFilename.c_str(), "rb+");
     801             :         else
     802           8 :             fpRaw = VSIFOpenL(osRawFilename.c_str(), "rb");
     803             : 
     804           8 :         if (fpRaw == nullptr)
     805             :         {
     806           0 :             CPLError(CE_Warning, CPLE_OpenFailed,
     807             :                      "Unable to open %s ... skipping.", osRawFilename.c_str());
     808           0 :             nSkipped++;
     809           0 :             continue;
     810             :         }
     811          16 :         poDS->m_papszFileList =
     812           8 :             CSLAddString(poDS->m_papszFileList, osRawFilename.c_str());
     813             : 
     814           8 :         GDALDataType eDataType = GDT_Unknown;
     815           8 :         const std::string osExt = CPLGetExtensionSafe(aosDirFiles[i]);
     816           8 :         if (pszRefinedType != nullptr)
     817             :         {
     818           0 :             if (EQUAL(pszRefinedType, "C*4"))
     819           0 :                 eDataType = GDT_CFloat32;
     820           0 :             else if (EQUAL(pszRefinedType, "C*8"))
     821           0 :                 eDataType = GDT_CFloat64;
     822           0 :             else if (EQUAL(pszRefinedType, "R*4"))
     823           0 :                 eDataType = GDT_Float32;
     824           0 :             else if (EQUAL(pszRefinedType, "R*8"))
     825           0 :                 eDataType = GDT_Float64;
     826           0 :             else if (EQUAL(pszRefinedType, "I*1"))
     827           0 :                 eDataType = GDT_Byte;
     828           0 :             else if (EQUAL(pszRefinedType, "I*2"))
     829           0 :                 eDataType = GDT_Int16;
     830           0 :             else if (EQUAL(pszRefinedType, "I*4"))
     831           0 :                 eDataType = GDT_Int32;
     832           0 :             else if (EQUAL(pszRefinedType, "U*2"))
     833           0 :                 eDataType = GDT_UInt16;
     834           0 :             else if (EQUAL(pszRefinedType, "U*4"))
     835           0 :                 eDataType = GDT_UInt32;
     836           0 :             else if (EQUAL(pszRefinedType, "J*1"))
     837             :             {
     838           0 :                 CPLError(
     839             :                     CE_Warning, CPLE_OpenFailed,
     840             :                     "Unable to open band %d because type J*1 is not handled. "
     841             :                     "Skipping.",
     842             :                     nRawBand + 1);
     843           0 :                 nSkipped++;
     844           0 :                 CPL_IGNORE_RET_VAL(VSIFCloseL(fpRaw));
     845           0 :                 continue;  // Does not support 1 byte complex.
     846             :             }
     847           0 :             else if (EQUAL(pszRefinedType, "J*2"))
     848           0 :                 eDataType = GDT_CInt16;
     849           0 :             else if (EQUAL(pszRefinedType, "K*4"))
     850           0 :                 eDataType = GDT_CInt32;
     851             :             else
     852             :             {
     853           0 :                 CPLError(
     854             :                     CE_Warning, CPLE_OpenFailed,
     855             :                     "Unable to open band %d because type %s is not handled. "
     856             :                     "Skipping.\n",
     857             :                     nRawBand + 1, pszRefinedType);
     858           0 :                 nSkipped++;
     859           0 :                 CPL_IGNORE_RET_VAL(VSIFCloseL(fpRaw));
     860           0 :                 continue;
     861             :             }
     862             :         }
     863           8 :         else if (STARTS_WITH_CI(osExt.c_str(), "b"))
     864             :         {
     865           7 :             eDataType = GDT_Byte;
     866             :         }
     867           1 :         else if (STARTS_WITH_CI(osExt.c_str(), "i"))
     868             :         {
     869           0 :             eDataType = GDT_UInt16;
     870             :         }
     871           1 :         else if (STARTS_WITH_CI(osExt.c_str(), "j"))
     872             :         {
     873           0 :             eDataType = GDT_CInt16;
     874             :         }
     875           1 :         else if (STARTS_WITH_CI(osExt.c_str(), "r"))
     876             :         {
     877           1 :             eDataType = GDT_Float32;
     878             :         }
     879           0 :         else if (STARTS_WITH_CI(osExt.c_str(), "x"))
     880             :         {
     881           0 :             eDataType = GDT_CFloat32;
     882             :         }
     883             :         else
     884             :         {
     885           0 :             CPLError(CE_Warning, CPLE_OpenFailed,
     886             :                      "Unable to open band %d because extension %s is not "
     887             :                      "handled.  Skipping.",
     888             :                      nRawBand + 1, osExt.c_str());
     889           0 :             nSkipped++;
     890           0 :             CPL_IGNORE_RET_VAL(VSIFCloseL(fpRaw));
     891           0 :             continue;
     892             :         }
     893             : 
     894           8 :         const int nBand = poDS->GetRasterCount() + 1;
     895             : 
     896           8 :         const int nPixelOffset = GDALGetDataTypeSizeBytes(eDataType);
     897           0 :         std::unique_ptr<GDALRasterBand> poBand;
     898             : 
     899           8 :         if (bTiled)
     900             :         {
     901           3 :             if (nTileXSize > INT_MAX / nTileYSize / nPixelOffset)
     902             :             {
     903           1 :                 CPLError(CE_Failure, CPLE_AppDefined, "Too large tile");
     904           1 :                 CPL_IGNORE_RET_VAL(VSIFCloseL(fpRaw));
     905           1 :                 return nullptr;
     906             :             }
     907             : 
     908           4 :             poBand = std::make_unique<MFFTiledBand>(poDS.get(), nBand, fpRaw,
     909             :                                                     nTileXSize, nTileYSize,
     910           2 :                                                     eDataType, eByteOrder);
     911             :         }
     912             :         else
     913             :         {
     914          10 :             if (nPixelOffset != 0 &&
     915           5 :                 poDS->GetRasterXSize() > INT_MAX / nPixelOffset)
     916             :             {
     917           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
     918             :                          "Int overflow occurred... skipping");
     919           0 :                 nSkipped++;
     920           0 :                 CPL_IGNORE_RET_VAL(VSIFCloseL(fpRaw));
     921           0 :                 continue;
     922             :             }
     923             : 
     924          10 :             poBand = RawRasterBand::Create(
     925           5 :                 poDS.get(), nBand, fpRaw, 0, nPixelOffset,
     926           5 :                 nPixelOffset * poDS->GetRasterXSize(), eDataType, eByteOrder,
     927           5 :                 RawRasterBand::OwnFP::YES);
     928             :         }
     929             : 
     930           7 :         poDS->SetBand(nBand, std::move(poBand));
     931           7 :     }
     932             : 
     933             :     /* -------------------------------------------------------------------- */
     934             :     /*      Check if we have bands.                                         */
     935             :     /* -------------------------------------------------------------------- */
     936           7 :     if (poDS->GetRasterCount() == 0)
     937             :     {
     938           0 :         if (nSkipped > 0 && poOpenInfo->eAccess)
     939             :         {
     940           0 :             CPLError(CE_Failure, CPLE_OpenFailed,
     941             :                      "Failed to open %d files that were apparently bands.  "
     942             :                      "Perhaps this dataset is readonly?",
     943             :                      nSkipped);
     944           0 :             return nullptr;
     945             :         }
     946             :         else
     947             :         {
     948           0 :             CPLError(CE_Failure, CPLE_OpenFailed,
     949             :                      "MFF header file read successfully, but no bands "
     950             :                      "were successfully found and opened.");
     951           0 :             return nullptr;
     952             :         }
     953             :     }
     954             : 
     955             :     /* -------------------------------------------------------------------- */
     956             :     /*      Set all information from the .hdr that isn't well know to be    */
     957             :     /*      metadata.                                                       */
     958             :     /* -------------------------------------------------------------------- */
     959          81 :     for (int i = 0; papszHdrLines[i] != nullptr; i++)
     960             :     {
     961          74 :         char *pszName = nullptr;
     962             : 
     963          74 :         const char *pszValue = CPLParseNameValue(papszHdrLines[i], &pszName);
     964          74 :         if (pszName == nullptr || pszValue == nullptr)
     965           3 :             continue;
     966             : 
     967          71 :         if (!EQUAL(pszName, "END") && !EQUAL(pszName, "FILE_TYPE") &&
     968          68 :             !EQUAL(pszName, "BYTE_ORDER") && !EQUAL(pszName, "no_columns") &&
     969          65 :             !EQUAL(pszName, "no_rows") && !EQUAL(pszName, "type") &&
     970          63 :             !EQUAL(pszName, "tile_size_rows") &&
     971          61 :             !EQUAL(pszName, "tile_size_columns") &&
     972          59 :             !EQUAL(pszName, "IMAGE_FILE_FORMAT") &&
     973          52 :             !EQUAL(pszName, "IMAGE_LINES") && !EQUAL(pszName, "LINE_SAMPLES"))
     974             :         {
     975          38 :             poDS->SetMetadataItem(pszName, pszValue);
     976             :         }
     977             : 
     978          71 :         CPLFree(pszName);
     979             :     }
     980             : 
     981             :     /* -------------------------------------------------------------------- */
     982             :     /*      Any GCPs in header file?                                        */
     983             :     /* -------------------------------------------------------------------- */
     984           7 :     poDS->ScanForGCPs();
     985           7 :     poDS->ScanForProjectionInfo();
     986           7 :     if (poDS->nGCPCount == 0)
     987           1 :         poDS->m_oGCPSRS.Clear();
     988             : 
     989             :     /* -------------------------------------------------------------------- */
     990             :     /*      Initialize any PAM information.                                 */
     991             :     /* -------------------------------------------------------------------- */
     992           7 :     poDS->SetDescription(poOpenInfo->pszFilename);
     993           7 :     poDS->TryLoadXML();
     994             : 
     995             :     /* -------------------------------------------------------------------- */
     996             :     /*      Check for overviews.                                            */
     997             :     /* -------------------------------------------------------------------- */
     998           7 :     poDS->oOvManager.Initialize(poDS.get(), poOpenInfo->pszFilename);
     999             : 
    1000           7 :     return poDS.release();
    1001             : }
    1002             : 
    1003             : /************************************************************************/
    1004             : /*                         GDALRegister_MFF()                           */
    1005             : /************************************************************************/
    1006             : 
    1007        2024 : void GDALRegister_MFF()
    1008             : 
    1009             : {
    1010        2024 :     if (GDALGetDriverByName("MFF") != nullptr)
    1011         283 :         return;
    1012             : 
    1013        1741 :     GDALDriver *poDriver = new GDALDriver();
    1014             : 
    1015        1741 :     poDriver->SetDescription("MFF");
    1016        1741 :     poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
    1017        1741 :     poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "Vexcel MFF Raster");
    1018        1741 :     poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/mff.html");
    1019        1741 :     poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "hdr");
    1020        1741 :     poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
    1021             : 
    1022        1741 :     poDriver->pfnOpen = MFFDataset::Open;
    1023             : 
    1024        1741 :     GetGDALDriverManager()->RegisterDriver(poDriver);
    1025             : }

Generated by: LCOV version 1.14