LCOV - code coverage report
Current view: top level - frmts/raw - snodasdataset.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 227 245 92.7 %
Date: 2024-11-21 22:18:42 Functions: 16 16 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  SNODAS driver
       4             :  * Purpose:  Implementation of SNODASDataset
       5             :  * Author:   Even Rouault, <even dot rouault at spatialys.com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2011, Even Rouault <even dot rouault at spatialys.com>
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #include "cpl_string.h"
      14             : #include "gdal_frmts.h"
      15             : #include "ogr_srs_api.h"
      16             : #include "rawdataset.h"
      17             : 
      18             : /************************************************************************/
      19             : /* ==================================================================== */
      20             : /*                            SNODASDataset                             */
      21             : /* ==================================================================== */
      22             : /************************************************************************/
      23             : 
      24             : class SNODASRasterBand;
      25             : 
      26             : class SNODASDataset final : public RawDataset
      27             : {
      28             :     CPLString osDataFilename{};
      29             :     bool bGotTransform;
      30             :     double adfGeoTransform[6];
      31             :     bool bHasNoData;
      32             :     double dfNoData;
      33             :     bool bHasMin;
      34             :     double dfMin;
      35             :     int bHasMax;
      36             :     double dfMax;
      37             :     OGRSpatialReference m_oSRS{};
      38             : 
      39             :     friend class SNODASRasterBand;
      40             : 
      41             :     CPL_DISALLOW_COPY_ASSIGN(SNODASDataset)
      42             : 
      43             :     CPLErr Close() override;
      44             : 
      45             :   public:
      46             :     SNODASDataset();
      47             :     ~SNODASDataset() override;
      48             : 
      49             :     CPLErr GetGeoTransform(double *padfTransform) override;
      50             : 
      51           1 :     const OGRSpatialReference *GetSpatialRef() const override
      52             :     {
      53           1 :         return &m_oSRS;
      54             :     }
      55             : 
      56             :     char **GetFileList() override;
      57             : 
      58             :     static GDALDataset *Open(GDALOpenInfo *);
      59             :     static int Identify(GDALOpenInfo *);
      60             : };
      61             : 
      62             : /************************************************************************/
      63             : /* ==================================================================== */
      64             : /*                            SNODASRasterBand                          */
      65             : /* ==================================================================== */
      66             : /************************************************************************/
      67             : 
      68             : class SNODASRasterBand final : public RawRasterBand
      69             : {
      70             :     CPL_DISALLOW_COPY_ASSIGN(SNODASRasterBand)
      71             : 
      72             :   public:
      73             :     SNODASRasterBand(VSILFILE *fpRaw, int nXSize, int nYSize);
      74             : 
      75           6 :     ~SNODASRasterBand() override
      76           3 :     {
      77           6 :     }
      78             : 
      79             :     double GetNoDataValue(int *pbSuccess = nullptr) override;
      80             :     double GetMinimum(int *pbSuccess = nullptr) override;
      81             :     double GetMaximum(int *pbSuccess = nullptr) override;
      82             : };
      83             : 
      84             : /************************************************************************/
      85             : /*                         SNODASRasterBand()                           */
      86             : /************************************************************************/
      87             : 
      88           3 : SNODASRasterBand::SNODASRasterBand(VSILFILE *fpRawIn, int nXSize, int nYSize)
      89             :     : RawRasterBand(fpRawIn, 0, 2, nXSize * 2, GDT_Int16, !CPL_IS_LSB, nXSize,
      90           3 :                     nYSize, RawRasterBand::OwnFP::YES)
      91             : {
      92           3 : }
      93             : 
      94             : /************************************************************************/
      95             : /*                          GetNoDataValue()                            */
      96             : /************************************************************************/
      97             : 
      98           1 : double SNODASRasterBand::GetNoDataValue(int *pbSuccess)
      99             : {
     100           1 :     SNODASDataset *poGDS = reinterpret_cast<SNODASDataset *>(poDS);
     101           1 :     if (pbSuccess)
     102           1 :         *pbSuccess = poGDS->bHasNoData;
     103             : 
     104           1 :     if (poGDS->bHasNoData)
     105           1 :         return poGDS->dfNoData;
     106             : 
     107           0 :     return RawRasterBand::GetNoDataValue(pbSuccess);
     108             : }
     109             : 
     110             : /************************************************************************/
     111             : /*                            GetMinimum()                              */
     112             : /************************************************************************/
     113             : 
     114           1 : double SNODASRasterBand::GetMinimum(int *pbSuccess)
     115             : {
     116           1 :     SNODASDataset *poGDS = reinterpret_cast<SNODASDataset *>(poDS);
     117           1 :     if (pbSuccess)
     118           1 :         *pbSuccess = poGDS->bHasMin;
     119             : 
     120           1 :     if (poGDS->bHasMin)
     121           1 :         return poGDS->dfMin;
     122             : 
     123           0 :     return RawRasterBand::GetMinimum(pbSuccess);
     124             : }
     125             : 
     126             : /************************************************************************/
     127             : /*                            GetMaximum()                             */
     128             : /************************************************************************/
     129             : 
     130           1 : double SNODASRasterBand::GetMaximum(int *pbSuccess)
     131             : {
     132           1 :     SNODASDataset *poGDS = reinterpret_cast<SNODASDataset *>(poDS);
     133           1 :     if (pbSuccess)
     134           1 :         *pbSuccess = poGDS->bHasMax;
     135             : 
     136           1 :     if (poGDS->bHasMax)
     137           1 :         return poGDS->dfMax;
     138             : 
     139           0 :     return RawRasterBand::GetMaximum(pbSuccess);
     140             : }
     141             : 
     142             : /************************************************************************/
     143             : /* ==================================================================== */
     144             : /*                            SNODASDataset                             */
     145             : /* ==================================================================== */
     146             : /************************************************************************/
     147             : 
     148             : /************************************************************************/
     149             : /*                           SNODASDataset()                            */
     150             : /************************************************************************/
     151             : 
     152           3 : SNODASDataset::SNODASDataset()
     153             :     : bGotTransform(false), bHasNoData(false), dfNoData(0.0), bHasMin(false),
     154           3 :       dfMin(0.0), bHasMax(false), dfMax(0.0)
     155             : {
     156           3 :     m_oSRS.SetFromUserInput(SRS_WKT_WGS84_LAT_LONG);
     157           3 :     m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     158             : 
     159           3 :     adfGeoTransform[0] = 0.0;
     160           3 :     adfGeoTransform[1] = 1.0;
     161           3 :     adfGeoTransform[2] = 0.0;
     162           3 :     adfGeoTransform[3] = 0.0;
     163           3 :     adfGeoTransform[4] = 0.0;
     164           3 :     adfGeoTransform[5] = 1.0;
     165           3 : }
     166             : 
     167             : /************************************************************************/
     168             : /*                           ~SNODASDataset()                           */
     169             : /************************************************************************/
     170             : 
     171           6 : SNODASDataset::~SNODASDataset()
     172             : 
     173             : {
     174           3 :     SNODASDataset::Close();
     175           6 : }
     176             : 
     177             : /************************************************************************/
     178             : /*                              Close()                                 */
     179             : /************************************************************************/
     180             : 
     181           6 : CPLErr SNODASDataset::Close()
     182             : {
     183           6 :     CPLErr eErr = CE_None;
     184           6 :     if (nOpenFlags != OPEN_FLAGS_CLOSED)
     185             :     {
     186           3 :         if (SNODASDataset::FlushCache(true) != CE_None)
     187           0 :             eErr = CE_Failure;
     188             : 
     189           3 :         if (GDALPamDataset::Close() != CE_None)
     190           0 :             eErr = CE_Failure;
     191             :     }
     192           6 :     return eErr;
     193             : }
     194             : 
     195             : /************************************************************************/
     196             : /*                          GetGeoTransform()                           */
     197             : /************************************************************************/
     198             : 
     199           1 : CPLErr SNODASDataset::GetGeoTransform(double *padfTransform)
     200             : 
     201             : {
     202           1 :     if (bGotTransform)
     203             :     {
     204           1 :         memcpy(padfTransform, adfGeoTransform, sizeof(double) * 6);
     205           1 :         return CE_None;
     206             :     }
     207             : 
     208           0 :     return GDALPamDataset::GetGeoTransform(padfTransform);
     209             : }
     210             : 
     211             : /************************************************************************/
     212             : /*                            GetFileList()                             */
     213             : /************************************************************************/
     214             : 
     215           2 : char **SNODASDataset::GetFileList()
     216             : 
     217             : {
     218           2 :     char **papszFileList = GDALPamDataset::GetFileList();
     219             : 
     220           2 :     papszFileList = CSLAddString(papszFileList, osDataFilename);
     221             : 
     222           2 :     return papszFileList;
     223             : }
     224             : 
     225             : /************************************************************************/
     226             : /*                            Identify()                                */
     227             : /************************************************************************/
     228             : 
     229       51413 : int SNODASDataset::Identify(GDALOpenInfo *poOpenInfo)
     230             : {
     231       51413 :     if (poOpenInfo->nHeaderBytes == 0)
     232       47800 :         return FALSE;
     233             : 
     234        3613 :     return STARTS_WITH_CI(reinterpret_cast<char *>(poOpenInfo->pabyHeader),
     235             :                           "Format version: NOHRSC GIS/RS raster file v1.1");
     236             : }
     237             : 
     238             : /************************************************************************/
     239             : /*                                Open()                                */
     240             : /************************************************************************/
     241             : 
     242           3 : GDALDataset *SNODASDataset::Open(GDALOpenInfo *poOpenInfo)
     243             : 
     244             : {
     245           3 :     if (!Identify(poOpenInfo) || poOpenInfo->fpL == nullptr)
     246           0 :         return nullptr;
     247             : 
     248             :     /* -------------------------------------------------------------------- */
     249             :     /*      Confirm the requested access is supported.                      */
     250             :     /* -------------------------------------------------------------------- */
     251           3 :     if (poOpenInfo->eAccess == GA_Update)
     252             :     {
     253           0 :         CPLError(CE_Failure, CPLE_NotSupported,
     254             :                  "The SNODAS driver does not support update access to existing"
     255             :                  " datasets.");
     256           0 :         return nullptr;
     257             :     }
     258             : 
     259           3 :     int nRows = -1;
     260           3 :     int nCols = -1;
     261           6 :     CPLString osDataFilename;
     262           3 :     bool bIsInteger = false;
     263           3 :     bool bIs2Bytes = false;
     264           3 :     double dfNoData = 0;
     265           3 :     bool bHasNoData = false;
     266           3 :     double dfMin = 0;
     267           3 :     bool bHasMin = false;
     268           3 :     double dfMax = 0;
     269           3 :     bool bHasMax = false;
     270           3 :     double dfMinX = 0.0;
     271           3 :     double dfMinY = 0.0;
     272           3 :     double dfMaxX = 0.0;
     273           3 :     double dfMaxY = 0.0;
     274           3 :     bool bHasMinX = false;
     275           3 :     bool bHasMinY = false;
     276           3 :     bool bHasMaxX = false;
     277           3 :     bool bHasMaxY = false;
     278           3 :     bool bNotProjected = false;
     279           3 :     bool bIsWGS84 = false;
     280           6 :     CPLString osDataUnits;
     281           6 :     CPLString osDescription;
     282           3 :     int nStartYear = -1;
     283           3 :     int nStartMonth = -1;
     284           3 :     int nStartDay = -1;
     285           3 :     int nStartHour = -1;
     286           3 :     int nStartMinute = -1;
     287           3 :     int nStartSecond = -1;
     288           3 :     int nStopYear = -1;
     289           3 :     int nStopMonth = -1;
     290           3 :     int nStopDay = -1;
     291           3 :     int nStopHour = -1;
     292           3 :     int nStopMinute = -1;
     293           3 :     int nStopSecond = -1;
     294             : 
     295           3 :     const char *pszLine = nullptr;
     296         324 :     while ((pszLine = CPLReadLine2L(poOpenInfo->fpL, 1024, nullptr)) != nullptr)
     297             :     {
     298             :         char **papszTokens =
     299         321 :             CSLTokenizeStringComplex(pszLine, ":", TRUE, FALSE);
     300         321 :         if (CSLCount(papszTokens) != 2)
     301             :         {
     302           3 :             CSLDestroy(papszTokens);
     303           3 :             continue;
     304             :         }
     305         318 :         if (papszTokens[1][0] == ' ')
     306         318 :             memmove(papszTokens[1], papszTokens[1] + 1,
     307         318 :                     strlen(papszTokens[1] + 1) + 1);
     308             : 
     309         318 :         if (EQUAL(papszTokens[0], "Data file pathname"))
     310             :         {
     311           3 :             osDataFilename = papszTokens[1];
     312             :         }
     313         315 :         else if (EQUAL(papszTokens[0], "Description"))
     314             :         {
     315           3 :             osDescription = papszTokens[1];
     316             :         }
     317         312 :         else if (EQUAL(papszTokens[0], "Data units"))
     318             :         {
     319           3 :             osDataUnits = papszTokens[1];
     320             :         }
     321             : 
     322         309 :         else if (EQUAL(papszTokens[0], "Start year"))
     323           3 :             nStartYear = atoi(papszTokens[1]);
     324         306 :         else if (EQUAL(papszTokens[0], "Start month"))
     325           3 :             nStartMonth = atoi(papszTokens[1]);
     326         303 :         else if (EQUAL(papszTokens[0], "Start day"))
     327           3 :             nStartDay = atoi(papszTokens[1]);
     328         300 :         else if (EQUAL(papszTokens[0], "Start hour"))
     329           3 :             nStartHour = atoi(papszTokens[1]);
     330         297 :         else if (EQUAL(papszTokens[0], " Start minute"))
     331           0 :             nStartMinute = atoi(papszTokens[1]);
     332         297 :         else if (EQUAL(papszTokens[0], "Start second"))
     333           3 :             nStartSecond = atoi(papszTokens[1]);
     334             : 
     335         294 :         else if (EQUAL(papszTokens[0], "Stop year"))
     336           3 :             nStopYear = atoi(papszTokens[1]);
     337         291 :         else if (EQUAL(papszTokens[0], "Stop month"))
     338           3 :             nStopMonth = atoi(papszTokens[1]);
     339         288 :         else if (EQUAL(papszTokens[0], "Stop day"))
     340           3 :             nStopDay = atoi(papszTokens[1]);
     341         285 :         else if (EQUAL(papszTokens[0], "Stop hour"))
     342           3 :             nStopHour = atoi(papszTokens[1]);
     343         282 :         else if (EQUAL(papszTokens[0], "Stop minute"))
     344           3 :             nStopMinute = atoi(papszTokens[1]);
     345         279 :         else if (EQUAL(papszTokens[0], "Stop second"))
     346           3 :             nStopSecond = atoi(papszTokens[1]);
     347             : 
     348         276 :         else if (EQUAL(papszTokens[0], "Number of columns"))
     349             :         {
     350           3 :             nCols = atoi(papszTokens[1]);
     351             :         }
     352         273 :         else if (EQUAL(papszTokens[0], "Number of rows"))
     353             :         {
     354           3 :             nRows = atoi(papszTokens[1]);
     355             :         }
     356         270 :         else if (EQUAL(papszTokens[0], "Data type"))
     357             :         {
     358           3 :             bIsInteger = EQUAL(papszTokens[1], "integer");
     359             :         }
     360         267 :         else if (EQUAL(papszTokens[0], "Data bytes per pixel"))
     361             :         {
     362           3 :             bIs2Bytes = EQUAL(papszTokens[1], "2");
     363             :         }
     364         264 :         else if (EQUAL(papszTokens[0], "Projected"))
     365             :         {
     366           3 :             bNotProjected = EQUAL(papszTokens[1], "no");
     367             :         }
     368         261 :         else if (EQUAL(papszTokens[0], "Horizontal datum"))
     369             :         {
     370           3 :             bIsWGS84 = EQUAL(papszTokens[1], "WGS84");
     371             :         }
     372         258 :         else if (EQUAL(papszTokens[0], "No data value"))
     373             :         {
     374           3 :             bHasNoData = true;
     375           3 :             dfNoData = CPLAtofM(papszTokens[1]);
     376             :         }
     377         255 :         else if (EQUAL(papszTokens[0], "Minimum data value"))
     378             :         {
     379           3 :             bHasMin = true;
     380           3 :             dfMin = CPLAtofM(papszTokens[1]);
     381             :         }
     382         252 :         else if (EQUAL(papszTokens[0], "Maximum data value"))
     383             :         {
     384           3 :             bHasMax = true;
     385           3 :             dfMax = CPLAtofM(papszTokens[1]);
     386             :         }
     387         249 :         else if (EQUAL(papszTokens[0], "Minimum x-axis coordinate"))
     388             :         {
     389           3 :             bHasMinX = true;
     390           3 :             dfMinX = CPLAtofM(papszTokens[1]);
     391             :         }
     392         246 :         else if (EQUAL(papszTokens[0], "Minimum y-axis coordinate"))
     393             :         {
     394           3 :             bHasMinY = true;
     395           3 :             dfMinY = CPLAtofM(papszTokens[1]);
     396             :         }
     397         243 :         else if (EQUAL(papszTokens[0], "Maximum x-axis coordinate"))
     398             :         {
     399           3 :             bHasMaxX = true;
     400           3 :             dfMaxX = CPLAtofM(papszTokens[1]);
     401             :         }
     402         240 :         else if (EQUAL(papszTokens[0], "Maximum y-axis coordinate"))
     403             :         {
     404           3 :             bHasMaxY = true;
     405           3 :             dfMaxY = CPLAtofM(papszTokens[1]);
     406             :         }
     407             : 
     408         318 :         CSLDestroy(papszTokens);
     409             :     }
     410             : 
     411           3 :     CPL_IGNORE_RET_VAL(VSIFCloseL(poOpenInfo->fpL));
     412           3 :     poOpenInfo->fpL = nullptr;
     413             : 
     414             :     /* -------------------------------------------------------------------- */
     415             :     /*      Did we get the required keywords?  If not we return with        */
     416             :     /*      this never having been considered to be a match. This isn't     */
     417             :     /*      an error!                                                       */
     418             :     /* -------------------------------------------------------------------- */
     419           3 :     if (nRows == -1 || nCols == -1 || !bIsInteger || !bIs2Bytes)
     420           0 :         return nullptr;
     421             : 
     422           3 :     if (!bNotProjected || !bIsWGS84)
     423           0 :         return nullptr;
     424             : 
     425           3 :     if (osDataFilename.empty())
     426           0 :         return nullptr;
     427             : 
     428           3 :     if (!GDALCheckDatasetDimensions(nCols, nRows))
     429           0 :         return nullptr;
     430             : 
     431             :     /* -------------------------------------------------------------------- */
     432             :     /*      Open target binary file.                                        */
     433             :     /* -------------------------------------------------------------------- */
     434           3 :     const char *pszPath = CPLGetPath(poOpenInfo->pszFilename);
     435           3 :     osDataFilename = CPLFormFilename(pszPath, osDataFilename, nullptr);
     436             : 
     437           3 :     VSILFILE *fpRaw = VSIFOpenL(osDataFilename, "rb");
     438             : 
     439           3 :     if (fpRaw == nullptr)
     440           0 :         return nullptr;
     441             : 
     442             :     /* -------------------------------------------------------------------- */
     443             :     /*      Create a corresponding GDALDataset.                             */
     444             :     /* -------------------------------------------------------------------- */
     445           6 :     auto poDS = std::make_unique<SNODASDataset>();
     446             : 
     447           3 :     poDS->nRasterXSize = nCols;
     448           3 :     poDS->nRasterYSize = nRows;
     449           3 :     poDS->osDataFilename = std::move(osDataFilename);
     450           3 :     poDS->bHasNoData = bHasNoData;
     451           3 :     poDS->dfNoData = dfNoData;
     452           3 :     poDS->bHasMin = bHasMin;
     453           3 :     poDS->dfMin = dfMin;
     454           3 :     poDS->bHasMax = bHasMax;
     455           3 :     poDS->dfMax = dfMax;
     456           3 :     if (bHasMinX && bHasMinY && bHasMaxX && bHasMaxY)
     457             :     {
     458           3 :         poDS->bGotTransform = true;
     459           3 :         poDS->adfGeoTransform[0] = dfMinX;
     460           3 :         poDS->adfGeoTransform[1] = (dfMaxX - dfMinX) / nCols;
     461           3 :         poDS->adfGeoTransform[2] = 0.0;
     462           3 :         poDS->adfGeoTransform[3] = dfMaxY;
     463           3 :         poDS->adfGeoTransform[4] = 0.0;
     464           3 :         poDS->adfGeoTransform[5] = -(dfMaxY - dfMinY) / nRows;
     465             :     }
     466             : 
     467           3 :     if (!osDescription.empty())
     468           3 :         poDS->SetMetadataItem("Description", osDescription);
     469           3 :     if (!osDataUnits.empty())
     470           3 :         poDS->SetMetadataItem("Data_Units", osDataUnits);
     471           3 :     if (nStartYear != -1 && nStartMonth != -1 && nStartDay != -1 &&
     472           3 :         nStartHour != -1 && nStartMinute != -1 && nStartSecond != -1)
     473           0 :         poDS->SetMetadataItem(
     474             :             "Start_Date",
     475             :             CPLSPrintf("%04d/%02d/%02d %02d:%02d:%02d", nStartYear, nStartMonth,
     476           0 :                        nStartDay, nStartHour, nStartMinute, nStartSecond));
     477           3 :     if (nStopYear != -1 && nStopMonth != -1 && nStopDay != -1 &&
     478           3 :         nStopHour != -1 && nStopMinute != -1 && nStopSecond != -1)
     479           6 :         poDS->SetMetadataItem("Stop_Date",
     480             :                               CPLSPrintf("%04d/%02d/%02d %02d:%02d:%02d",
     481             :                                          nStopYear, nStopMonth, nStopDay,
     482           3 :                                          nStopHour, nStopMinute, nStopSecond));
     483             : 
     484             :     /* -------------------------------------------------------------------- */
     485             :     /*      Create band information objects.                                */
     486             :     /* -------------------------------------------------------------------- */
     487           6 :     auto poBand = std::make_unique<SNODASRasterBand>(fpRaw, nCols, nRows);
     488           3 :     if (!poBand->IsValid())
     489           0 :         return nullptr;
     490           3 :     poDS->SetBand(1, std::move(poBand));
     491             : 
     492             :     /* -------------------------------------------------------------------- */
     493             :     /*      Initialize any PAM information.                                 */
     494             :     /* -------------------------------------------------------------------- */
     495           3 :     poDS->SetDescription(poOpenInfo->pszFilename);
     496           3 :     poDS->TryLoadXML();
     497             : 
     498             :     /* -------------------------------------------------------------------- */
     499             :     /*      Check for overviews.                                            */
     500             :     /* -------------------------------------------------------------------- */
     501           3 :     poDS->oOvManager.Initialize(poDS.get(), poOpenInfo->pszFilename);
     502             : 
     503           3 :     return poDS.release();
     504             : }
     505             : 
     506             : /************************************************************************/
     507             : /*                       GDALRegister_SNODAS()                          */
     508             : /************************************************************************/
     509             : 
     510        1595 : void GDALRegister_SNODAS()
     511             : 
     512             : {
     513        1595 :     if (GDALGetDriverByName("SNODAS") != nullptr)
     514         302 :         return;
     515             : 
     516        1293 :     GDALDriver *poDriver = new GDALDriver();
     517             : 
     518        1293 :     poDriver->SetDescription("SNODAS");
     519        1293 :     poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
     520        1293 :     poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
     521        1293 :                               "Snow Data Assimilation System");
     522        1293 :     poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/snodas.html");
     523        1293 :     poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "hdr");
     524        1293 :     poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
     525             : 
     526        1293 :     poDriver->pfnOpen = SNODASDataset::Open;
     527        1293 :     poDriver->pfnIdentify = SNODASDataset::Identify;
     528             : 
     529        1293 :     GetGDALDriverManager()->RegisterDriver(poDriver);
     530             : }

Generated by: LCOV version 1.14