LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/s101 - ogrs101readermultipoint.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 177 189 93.7 %
Date: 2026-05-29 23:25:07 Functions: 10 10 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  S-101 driver
       4             :  * Purpose:  Implements OGRS101Reader
       5             :  * Author:   Even Rouault <even dot rouault at spatialys.com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2026, Even Rouault <even dot rouault at spatialys.com>
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #include "ogr_s101.h"
      14             : #include "ogrs101readerconstants.h"
      15             : 
      16             : #include <memory>
      17             : 
      18             : /************************************************************************/
      19             : /*                       GetMultiPointLayerName()                       */
      20             : /************************************************************************/
      21             : 
      22             : /* static */ std::string
      23         306 : OGRS101Reader::GetMultiPointLayerName(const OGRSpatialReference &oSRS)
      24             : {
      25         306 :     return oSRS.GetAxesCount() == 2
      26             :                ? "MultiPoint2D"
      27         918 :                : CPLSPrintf("MultiPoint3D_%s", LaunderCRSName(oSRS).c_str());
      28             : }
      29             : 
      30             : /************************************************************************/
      31             : /*                    CreateMultiPointFeatureDefns()                    */
      32             : /************************************************************************/
      33             : 
      34             : /** Create the feature definition(s) for the MultiPoint layer(s)
      35             :  *
      36             :  * There is a layer per CRS used by multipoints.
      37             :  */
      38         347 : bool OGRS101Reader::CreateMultiPointFeatureDefns()
      39             : {
      40         347 :     bool bError = false;
      41             : 
      42             :     m_oMapCRSIdToMultiPointRecordIdx =
      43         347 :         CreateMapCRSIdToRecordIdxForMultiPoints(bError);
      44         347 :     if (bError)
      45           2 :         return false;
      46         426 :     for (const auto &[nCRSId, anRecordIdx] : m_oMapCRSIdToMultiPointRecordIdx)
      47             :     {
      48          81 :         const auto &oSRS = m_oMapSRS[nCRSId];
      49          81 :         const bool bIs2D = nCRSId == HORIZONTAL_CRS_ID;
      50             :         auto poFDefn = OGRFeatureDefnRefCountedPtr::makeInstance(
      51          81 :             GetMultiPointLayerName(oSRS).c_str());
      52          81 :         poFDefn->SetGeomType(bIs2D ? wkbMultiPoint : wkbMultiPoint25D);
      53         162 :         poFDefn->GetGeomFieldDefn(0)->SetSpatialRef(
      54         162 :             OGRSpatialReferenceRefCountedPtr::makeClone(&oSRS).get());
      55          81 :         poFDefn->GetGeomFieldDefn(0)->SetCoordinatePrecision(
      56          81 :             m_coordinatePrecision);
      57             :         {
      58         162 :             OGRFieldDefn oFieldDefn(OGR_FIELD_NAME_RECORD_ID, OFTInteger);
      59          81 :             poFDefn->AddFieldDefn(&oFieldDefn);
      60             :         }
      61             :         {
      62         162 :             OGRFieldDefn oFieldDefn(OGR_FIELD_NAME_RECORD_VERSION, OFTInteger);
      63          81 :             poFDefn->AddFieldDefn(&oFieldDefn);
      64             :         }
      65          81 :         if (!InferFeatureDefn(m_oMultiPointRecordIndex, MRID_FIELD, INAS_FIELD,
      66          81 :                               anRecordIdx, *poFDefn, m_oMapFieldDomains))
      67             :         {
      68           0 :             return false;
      69             :         }
      70          81 :         m_oMapMultiPointFeatureDefn[nCRSId] = std::move(poFDefn);
      71             :     }
      72             : 
      73         345 :     return true;
      74             : }
      75             : 
      76             : /************************************************************************/
      77             : /*                    GetCRSIdForMultiPointRecord()                     */
      78             : /************************************************************************/
      79             : 
      80             : /** Return the CRS id for a given MultiPoint record, or INVALID_CRS_ID on error */
      81             : OGRS101Reader::CRSId
      82         162 : OGRS101Reader::GetCRSIdForMultiPointRecord(const DDFRecord *poRecord,
      83             :                                            int iRecord, int nRecordID) const
      84             : {
      85         162 :     if (nRecordID < 0)
      86          96 :         nRecordID = poRecord->GetIntSubfield(MRID_FIELD, 0, RCID_SUBFIELD, 0);
      87             : 
      88          10 :     const auto GetErrorContext = [iRecord, nRecordID]()
      89             :     {
      90           5 :         if (iRecord >= 0)
      91           5 :             return CPLSPrintf("Record index=%d of MRID", iRecord);
      92             :         else
      93           0 :             return CPLSPrintf("Record ID=%d of MRID", nRecordID);
      94         162 :     };
      95             : 
      96         162 :     if (poRecord->FindField(C3IL_FIELD))
      97             :     {
      98             :         const CRSId nVCID =
      99          23 :             poRecord->GetIntSubfield(C3IL_FIELD, 0, VCID_SUBFIELD, 0);
     100          23 :         if (nVCID == HORIZONTAL_CRS_ID)
     101             :         {
     102           2 :             CPL_IGNORE_RET_VAL(EMIT_ERROR_OR_WARNING(
     103             :                 CPLSPrintf("%s: VCID subfield = %d of C3IL "
     104             :                            "field points to a non-3D CRS.",
     105             :                            GetErrorContext(), static_cast<int>(nVCID))));
     106           2 :             return INVALID_CRS_ID;
     107             :         }
     108          21 :         else if (!cpl::contains(m_oMapSRS, nVCID))
     109             :         {
     110           2 :             CPL_IGNORE_RET_VAL(EMIT_ERROR_OR_WARNING(
     111             :                 CPLSPrintf("%s: Unknown value %d for VCID subfield of C3IL "
     112             :                            "field.",
     113             :                            GetErrorContext(), static_cast<int>(nVCID))));
     114           2 :             return INVALID_CRS_ID;
     115             :         }
     116             :         else
     117             :         {
     118          19 :             return nVCID;
     119             :         }
     120             :     }
     121         139 :     else if (poRecord->FindField(C2IL_FIELD))
     122             :     {
     123         138 :         return HORIZONTAL_CRS_ID;
     124             :     }
     125             :     else
     126             :     {
     127           1 :         CPL_IGNORE_RET_VAL(EMIT_ERROR_OR_WARNING(
     128             :             CPLSPrintf("%s: No C2IL or C3IL field found.", GetErrorContext())));
     129           1 :         return INVALID_CRS_ID;
     130             :     }
     131             : }
     132             : 
     133             : /************************************************************************/
     134             : /*              CreateMapCRSIdToRecordIdxForMultiPoints()               */
     135             : /************************************************************************/
     136             : 
     137             : /** Browse through m_oMultiPointRecordIndex to identify which record belongs to
     138             :  * each CRS and create a map from each CRS id to the record indices that use
     139             :  * it.
     140             :  */
     141             : std::map<OGRS101Reader::CRSId, std::vector<int>>
     142         347 : OGRS101Reader::CreateMapCRSIdToRecordIdxForMultiPoints(bool &bError) const
     143             : {
     144         694 :     std::map<OGRS101Reader::CRSId, std::vector<int>> map;
     145             : 
     146         347 :     const int nRecords = m_oMultiPointRecordIndex.GetCount();
     147         441 :     for (int iRecord = 0; iRecord < nRecords; ++iRecord)
     148             :     {
     149          96 :         const auto poRecord = m_oMultiPointRecordIndex.GetByIndex(iRecord);
     150             :         const CRSId nCRSId = GetCRSIdForMultiPointRecord(poRecord, iRecord,
     151          96 :                                                          /* nRecordID = */ -1);
     152          96 :         if (nCRSId == INVALID_CRS_ID)
     153             :         {
     154           5 :             if (m_bStrict)
     155             :             {
     156           2 :                 bError = true;
     157           2 :                 return {};
     158             :             }
     159             :         }
     160             :         else
     161             :         {
     162          91 :             map[nCRSId].push_back(iRecord);
     163             :         }
     164             :     }
     165             : 
     166         345 :     return map;
     167             : }
     168             : 
     169             : /************************************************************************/
     170             : /*                       ReadMultiPointGeometry()                       */
     171             : /************************************************************************/
     172             : 
     173             : std::unique_ptr<OGRMultiPoint>
     174         512 : OGRS101Reader::ReadMultiPointGeometry(const DDFRecord *poRecord, int iRecord,
     175             :                                       int nRecordID,
     176             :                                       const OGRSpatialReference *poSRS) const
     177             : {
     178         512 :     const bool bIs3D = poRecord->FindField(C3IL_FIELD) != nullptr;
     179         512 :     const char *pszCoordFieldName = bIs3D ? C3IL_FIELD : C2IL_FIELD;
     180        1024 :     const auto apoCoordFields = poRecord->GetFields(pszCoordFieldName);
     181         512 :     if (apoCoordFields.empty())
     182           0 :         return nullptr;
     183             : 
     184        1024 :     auto poMP = std::make_unique<OGRMultiPoint>();
     185         512 :     poMP->assignSpatialReference(poSRS);
     186        1022 :     for (const auto *poCoordField : apoCoordFields)
     187             :     {
     188             :         const int nCoordCount =
     189         128 :             bIs3D && poCoordField->GetParts().size() == 2
     190         640 :                 ? poCoordField->GetParts()[1]->GetRepeatCount()
     191         384 :                 : poCoordField->GetRepeatCount();
     192             : 
     193        1543 :         for (int iPnt = 0; iPnt < nCoordCount; ++iPnt)
     194             :         {
     195             :             auto poPoint = ReadPointGeometryInternal(
     196             :                 poRecord, iRecord, nRecordID, iPnt, poSRS, bIs3D, poCoordField,
     197        1033 :                 MRID_FIELD);
     198        1033 :             if (!poPoint)
     199           2 :                 return nullptr;
     200        1031 :             poMP->addGeometry(std::move(poPoint));
     201             :         }
     202             :     }
     203         510 :     return poMP;
     204             : }
     205             : 
     206             : /************************************************************************/
     207             : /*                       FillFeatureMultiPoint()                        */
     208             : /************************************************************************/
     209             : 
     210             : /** Fill the content of the provided feature from the identified record
     211             :  * (of m_oMultiPointRecordIndex).
     212             :  */
     213         348 : bool OGRS101Reader::FillFeatureMultiPoint(const DDFRecordIndex &oIndex,
     214             :                                           int iRecord,
     215             :                                           OGRFeature &oFeature) const
     216             : {
     217         348 :     const auto poRecord = oIndex.GetByIndex(iRecord);
     218         348 :     CPLAssert(poRecord);
     219             : 
     220             :     const OGRSpatialReference *poSRS =
     221         348 :         oFeature.GetDefnRef()->GetGeomFieldDefn(0)->GetSpatialRef();
     222             :     auto poMP =
     223         696 :         ReadMultiPointGeometry(poRecord, iRecord, /* nRecordID = */ -1, poSRS);
     224         348 :     if (poMP)
     225             :     {
     226         346 :         oFeature.SetGeometry(std::move(poMP));
     227             :     }
     228           2 :     else if (m_bStrict)
     229           2 :         return false;
     230             : 
     231         692 :     return FillFeatureAttributes(oIndex, iRecord, INAS_FIELD, oFeature) &&
     232         346 :            FillFeatureWithNonAttrAssocSubfields(poRecord, iRecord, INAS_FIELD,
     233         346 :                                                 oFeature);
     234             : }
     235             : 
     236             : /************************************************************************/
     237             : /*                   ProcessUpdateRecordMultiPoint()                    */
     238             : /************************************************************************/
     239             : 
     240             : /** Updates the geometry part of poTargetRecord with poUpdateRecord */
     241          33 : bool OGRS101Reader::ProcessUpdateRecordMultiPoint(
     242             :     const DDFRecord *poUpdateRecord, DDFRecord *poTargetRecord) const
     243             : {
     244          33 :     return ProcessUpdatePointList(poUpdateRecord, poTargetRecord,
     245          33 :                                   /* bIs3DAllowed = */ true);
     246             : }
     247             : 
     248             : /************************************************************************/
     249             : /*                       ProcessUpdatePointList()                       */
     250             : /************************************************************************/
     251             : 
     252             : /** Updates the point list of poTargetRecord with poUpdateRecord */
     253          36 : bool OGRS101Reader::ProcessUpdatePointList(const DDFRecord *poUpdateRecord,
     254             :                                            DDFRecord *poTargetRecord,
     255             :                                            bool bIs3DAllowed) const
     256             : {
     257          36 :     const auto poIDField = poUpdateRecord->GetField(0);
     258          36 :     CPLAssert(poIDField);
     259             : 
     260             :     // Record name
     261             :     const RecordName nRCNM =
     262          36 :         poUpdateRecord->GetIntSubfield(poIDField, RCNM_SUBFIELD, 0);
     263             : 
     264             :     // Record identifier
     265             :     const int nRCID =
     266          36 :         poUpdateRecord->GetIntSubfield(poIDField, RCID_SUBFIELD, 0);
     267             : 
     268             :     // Coordinate Control field
     269          36 :     const auto poControlField = poUpdateRecord->FindField(COCC_FIELD);
     270          36 :     if (!poControlField)
     271           0 :         return true;
     272             : 
     273             :     // Number of Coordinates
     274          36 :     constexpr const char *NCOR_SUBFIELD = "NCOR";
     275             :     const int nNCOR =
     276          36 :         poUpdateRecord->GetIntSubfield(poControlField, NCOR_SUBFIELD, 0);
     277             : 
     278             :     // Coordinate Update Instruction
     279          36 :     constexpr const char *COUI_SUBFIELD = "COUI";
     280             :     const int nCOUI =
     281          36 :         poUpdateRecord->GetIntSubfield(poControlField, COUI_SUBFIELD, 0);
     282             : 
     283          36 :     bool bIs3D = false;
     284          36 :     const DDFField *poUpdateField = nullptr;
     285          36 :     DDFField *poTargetField = nullptr;
     286          36 :     if (nCOUI == INSTRUCTION_DELETE)
     287             :     {
     288          11 :         if ((poTargetField = poTargetRecord->FindField(C2IL_FIELD)) != nullptr)
     289             :         {
     290          11 :             poUpdateField = poUpdateRecord->FindField(C2IL_FIELD);
     291             :         }
     292           0 :         else if (bIs3DAllowed && (poTargetField = poTargetRecord->FindField(
     293             :                                       C3IL_FIELD)) != nullptr)
     294             :         {
     295           0 :             poUpdateField = poUpdateRecord->FindField(C3IL_FIELD);
     296           0 :             bIs3D = true;
     297             :         }
     298             :         else
     299             :         {
     300           0 :             return EMIT_ERROR_OR_WARNING(
     301             :                 CPLSPrintf("%s, RCNM=%d, RCID=%d: missing %s field in "
     302             :                            "target record",
     303             :                            m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID,
     304             :                            bIs3DAllowed ? "C2IL / C3IL" : "C2IL"));
     305             :         }
     306             :     }
     307          25 :     else if ((poUpdateField = poUpdateRecord->FindField(C2IL_FIELD)) != nullptr)
     308             :     {
     309          23 :         if ((poTargetField = poTargetRecord->FindField(C2IL_FIELD)) == nullptr)
     310             :         {
     311           0 :             return EMIT_ERROR_OR_WARNING(CPLSPrintf(
     312             :                 "%s, RCNM=%d, RCID=%d: cannot find C2IL field in target "
     313             :                 "record",
     314             :                 m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID));
     315             :         }
     316             :     }
     317           4 :     else if (bIs3DAllowed &&
     318           2 :              (poUpdateField = poUpdateRecord->FindField(C3IL_FIELD)) != nullptr)
     319             :     {
     320           2 :         if ((poTargetField = poTargetRecord->FindField(C3IL_FIELD)) == nullptr)
     321             :         {
     322           0 :             return EMIT_ERROR_OR_WARNING(CPLSPrintf(
     323             :                 "%s, RCNM=%d, RCID=%d: cannot find C3IL field in target "
     324             :                 "record",
     325             :                 m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID));
     326             :         }
     327           2 :         bIs3D = true;
     328             :     }
     329             :     else
     330             :     {
     331           0 :         return EMIT_ERROR_OR_WARNING(
     332             :             CPLSPrintf("%s, RCNM=%d, RCID=%d: missing %s field in update "
     333             :                        "record",
     334             :                        m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID,
     335             :                        bIs3DAllowed ? "C2IL / C3IL" : "C2IL"));
     336             :     }
     337             : 
     338          36 :     const char *pszCoordFieldName = bIs3D ? C3IL_FIELD : C2IL_FIELD;
     339         108 :     if ((poUpdateField && poUpdateRecord->GetFields(C2IL_FIELD).size() > 1) ||
     340          72 :         poTargetRecord->GetFields(C2IL_FIELD).size() > 1)
     341             :     {
     342           0 :         return EMIT_ERROR_OR_WARNING(
     343             :             CPLSPrintf("%s: only one instance of %s supported for update",
     344             :                        m_osFilename.c_str(), pszCoordFieldName));
     345             :     }
     346             : 
     347             :     // Coordinate Index (1-based)
     348          36 :     constexpr const char *COIX_SUBFIELD = "COIX";
     349             :     const int nCOIX =
     350          36 :         poUpdateRecord->GetIntSubfield(poControlField, COIX_SUBFIELD, 0);
     351             : 
     352             :     const int nTargetRepeatCount =
     353           2 :         (bIs3D && poTargetField->GetParts().size() == 2)
     354          38 :             ? poTargetField->GetParts()[1]->GetRepeatCount()
     355          34 :             : poTargetField->GetRepeatCount();
     356             : 
     357             :     // Check that start index and count is consistent with update and
     358             :     // target list of points
     359          36 :     const int nMaxCOIXAllowed =
     360          36 :         nTargetRepeatCount + (nCOUI == INSTRUCTION_INSERT ? 1 : 0);
     361          36 :     if (nCOIX <= 0 || nCOIX > nMaxCOIXAllowed)
     362             :     {
     363           4 :         return EMIT_ERROR_OR_WARNING(CPLSPrintf(
     364             :             "%s, RCNM=%d, RCID=%d: invalid COIX = %d. Must be in [1,%d].",
     365             :             m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID, nCOIX,
     366             :             nMaxCOIXAllowed));
     367             :     }
     368             : 
     369             :     const int nUpdateRepeatCount =
     370          55 :         !poUpdateField ? 0
     371           2 :         : (bIs3D && poUpdateField->GetParts().size() == 2)
     372          25 :             ? poUpdateField->GetParts()[1]->GetRepeatCount()
     373          21 :             : poUpdateField->GetRepeatCount();
     374             : 
     375          32 :     if (poUpdateField && nNCOR != nUpdateRepeatCount)
     376             :     {
     377           2 :         return EMIT_ERROR_OR_WARNING(
     378             :             CPLSPrintf("%s, RCNM=%d, RCID=%d: invalid NCOR = %d. Expected %d",
     379             :                        m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID,
     380             :                        nNCOR, nUpdateRepeatCount));
     381             :     }
     382          32 :     else if (poUpdateField && nCOUI == INSTRUCTION_DELETE &&
     383           2 :              !EMIT_ERROR_OR_WARNING(
     384             :                  CPLSPrintf("%s, RCNM=%d, RCID=%d: unexpected %s field in "
     385             :                             "update record in COUI = %d (delete) mode",
     386             :                             m_osFilename.c_str(), static_cast<int>(nRCNM),
     387             :                             nRCID, pszCoordFieldName, nCOUI)))
     388             :     {
     389           1 :         return false;
     390             :     }
     391             : 
     392          29 :     if (nCOUI == INSTRUCTION_UPDATE || nCOUI == INSTRUCTION_DELETE)
     393             :     {
     394          23 :         if (nCOIX + nNCOR > nTargetRepeatCount + 1)
     395             :         {
     396           3 :             return EMIT_ERROR_OR_WARNING(CPLSPrintf(
     397             :                 "%s, RCNM=%d, RCID=%d: invalid COIX = %d and/or NCOR=%d",
     398             :                 m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID, nCOIX,
     399             :                 nNCOR));
     400             :         }
     401             :     }
     402           6 :     else if (nCOUI != INSTRUCTION_INSERT)
     403             :     {
     404           2 :         return EMIT_ERROR_OR_WARNING(CPLSPrintf(
     405             :             "%s, RCNM=%d, RCID=%d: invalid COUI = %d", m_osFilename.c_str(),
     406             :             static_cast<int>(nRCNM), nRCID, nCOUI));
     407             :     }
     408             : 
     409             :     struct Coord
     410             :     {
     411             :         int Y = 0;
     412             :         int X = 0;
     413             :         int Z = 0;
     414             : 
     415          90 :         void Read(const DDFRecord *poRecord, const DDFField *poField, int i,
     416             :                   bool bIs3D)
     417             :         {
     418          90 :             Y = poRecord->GetIntSubfield(poField, YCOO_SUBFIELD, i);
     419          90 :             X = poRecord->GetIntSubfield(poField, XCOO_SUBFIELD, i);
     420          90 :             if (bIs3D)
     421           6 :                 Z = poRecord->GetIntSubfield(poField, ZCOO_SUBFIELD, i);
     422          90 :         }
     423             :     };
     424             : 
     425          48 :     std::vector<Coord> asTarget;
     426             :     // Ingest the existing/target record
     427          83 :     for (int i = 0; i < nTargetRepeatCount; ++i)
     428             :     {
     429          59 :         Coord c;
     430          59 :         c.Read(poTargetRecord, poTargetField, i, bIs3D);
     431          59 :         asTarget.push_back(c);
     432             :     }
     433             : 
     434             :     // Apply the update record
     435          48 :     std::vector<Coord> asUpdate;
     436          24 :     if (poUpdateField)
     437             :     {
     438          48 :         for (int i = 0; i < nUpdateRepeatCount; ++i)
     439             :         {
     440          31 :             Coord c;
     441          31 :             c.Read(poUpdateRecord, poUpdateField, i, bIs3D);
     442          31 :             asUpdate.push_back(c);
     443             :         }
     444             :     }
     445             : 
     446          24 :     const auto oTargetBeginIter = asTarget.begin() + (nCOIX - 1);
     447          24 :     if (nCOUI == INSTRUCTION_INSERT)
     448             :     {
     449           4 :         asTarget.insert(oTargetBeginIter, asUpdate.begin(), asUpdate.end());
     450             :     }
     451          20 :     else if (nCOUI == INSTRUCTION_UPDATE)
     452             :     {
     453          13 :         std::copy(asUpdate.begin(), asUpdate.end(), oTargetBeginIter);
     454             :     }
     455             :     else
     456             :     {
     457           7 :         CPLAssert(nCOUI == INSTRUCTION_DELETE);
     458           7 :         asTarget.erase(oTargetBeginIter, oTargetBeginIter + nNCOR);
     459             :     }
     460             : 
     461             :     // Compose raw target field
     462          24 :     std::string s;
     463          24 :     if (bIs3D)
     464             :     {
     465           2 :         const DDFRecord *poRecord =
     466           2 :             poUpdateRecord ? poUpdateRecord : poTargetRecord;
     467           2 :         const DDFField *poField = poUpdateField ? poUpdateField : poTargetField;
     468           2 :         AppendUInt8(s, static_cast<uint8_t>(poRecord->GetIntSubfield(
     469             :                            poField, VCID_SUBFIELD, 0)));
     470             :     }
     471             : 
     472          88 :     for (const auto &c : asTarget)
     473             :     {
     474          64 :         AppendInt32(s, c.Y);
     475          64 :         AppendInt32(s, c.X);
     476          64 :         if (bIs3D)
     477           4 :             AppendInt32(s, c.Z);
     478             :     }
     479          24 :     AppendUInt8(s, DDF_FIELD_TERMINATOR);
     480             : 
     481          24 :     poTargetRecord->SetFieldRaw(poTargetField, s.data(),
     482          24 :                                 static_cast<int>(s.size()));
     483             : 
     484          24 :     return true;
     485             : }

Generated by: LCOV version 1.14