LCOV - code coverage report
Current view: top level - apps - gdalalg_vector_check_geometry.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 143 148 96.6 %
Date: 2025-12-01 18:11:08 Functions: 8 10 80.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             : *
       3             :  * Project:  GDAL
       4             :  * Purpose:  "gdal vector check-geometry" subcommand
       5             :  * Author:   Daniel Baston
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2025, ISciences LLC
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #include "gdalalg_vector_check_geometry.h"
      14             : 
      15             : #include "cpl_error.h"
      16             : #include "gdal_priv.h"
      17             : #include "gdalalg_vector_geom.h"
      18             : #include "ogr_geometry.h"
      19             : #include "ogr_geos.h"
      20             : 
      21             : #include <cinttypes>
      22             : 
      23             : #ifndef _
      24             : #define _(x) (x)
      25             : #endif
      26             : 
      27             : //! @cond Doxygen_Suppress
      28             : 
      29          61 : GDALVectorCheckGeometryAlgorithm::GDALVectorCheckGeometryAlgorithm(
      30          61 :     bool standaloneStep)
      31             :     : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
      32          61 :                                       standaloneStep)
      33             : {
      34             :     AddArg("include-field", 0,
      35          61 :            _("Fields from input layer to include in output"), &m_includeFields);
      36             : 
      37             :     AddArg("include-valid", 0,
      38             :            _("Include valid inputs in output, with empty geometry"),
      39          61 :            &m_includeValid);
      40             : 
      41             :     AddArg("geometry-field", 0, _("Name of geometry field to check"),
      42          61 :            &m_geomField);
      43          61 : }
      44             : 
      45             : #ifdef HAVE_GEOS
      46             : 
      47             : class GDALInvalidLocationLayer final : public GDALVectorPipelineOutputLayer
      48             : {
      49             :   private:
      50             :     static constexpr const char *ERROR_DESCRIPTION_FIELD = "error";
      51             : 
      52             :   public:
      53          18 :     GDALInvalidLocationLayer(OGRLayer &layer,
      54             :                              const std::vector<int> &srcFieldIndices,
      55             :                              bool bSingleLayerOutput, int srcGeomField,
      56             :                              bool skipValid)
      57          18 :         : GDALVectorPipelineOutputLayer(layer),
      58          20 :           m_defn(OGRFeatureDefn::CreateFeatureDefn(
      59             :               bSingleLayerOutput ? "error_location"
      60          20 :                                  : std::string("error_location_")
      61           2 :                                        .append(layer.GetDescription())
      62           2 :                                        .c_str())),
      63          18 :           m_geosContext(OGRGeometry::createGEOSContext()),
      64          36 :           m_srcGeomField(srcGeomField), m_skipValid(skipValid)
      65             :     {
      66          18 :         m_defn->Reference();
      67             : 
      68          18 :         if (!srcFieldIndices.empty())
      69             :         {
      70           1 :             const OGRFeatureDefn &srcDefn = *layer.GetLayerDefn();
      71           1 :             m_srcFieldMap.resize(srcDefn.GetFieldCount(), -1);
      72           1 :             int iDstField = 0;
      73           3 :             for (int iSrcField : srcFieldIndices)
      74             :             {
      75           2 :                 m_defn->AddFieldDefn(srcDefn.GetFieldDefn(iSrcField));
      76           2 :                 m_srcFieldMap[iSrcField] = iDstField++;
      77             :             }
      78             :         }
      79             : 
      80             :         auto poDescriptionFieldDefn =
      81          36 :             std::make_unique<OGRFieldDefn>(ERROR_DESCRIPTION_FIELD, OFTString);
      82          18 :         m_defn->AddFieldDefn(std::move(poDescriptionFieldDefn));
      83             : 
      84          36 :         m_defn->GetGeomFieldDefn(0)->SetSpatialRef(
      85          18 :             m_srcLayer.GetLayerDefn()
      86          18 :                 ->GetGeomFieldDefn(m_srcGeomField)
      87          18 :                 ->GetSpatialRef());
      88          18 :     }
      89             : 
      90             :     ~GDALInvalidLocationLayer() override;
      91             : 
      92           0 :     int TestCapability(const char *) const override
      93             :     {
      94           0 :         return false;
      95             :     }
      96             : 
      97           5 :     const OGRFeatureDefn *GetLayerDefn() const override
      98             :     {
      99           5 :         return m_defn;
     100             :     }
     101             : 
     102           6 :     std::unique_ptr<OGRFeature> CreateFeatureFromLastError() const
     103             :     {
     104           6 :         auto poErrorFeature = std::make_unique<OGRFeature>(m_defn);
     105             : 
     106          12 :         std::string msg = CPLGetLastErrorMsg();
     107             : 
     108             :         // Trim GEOS exception name
     109           6 :         const auto subMsgPos = msg.find(": ");
     110           6 :         if (subMsgPos != std::string::npos)
     111             :         {
     112           6 :             msg = msg.substr(subMsgPos + strlen(": "));
     113             :         }
     114             : 
     115             :         // Trim newline from end of GEOS exception message
     116           6 :         if (!msg.empty() && msg.back() == '\n')
     117             :         {
     118           3 :             msg.pop_back();
     119             :         }
     120             : 
     121           6 :         poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD, msg.c_str());
     122             : 
     123           6 :         CPLErrorReset();
     124             : 
     125          12 :         return poErrorFeature;
     126             :     }
     127             : 
     128          36 :     void TranslateFeature(
     129             :         std::unique_ptr<OGRFeature> poSrcFeature,
     130             :         std::vector<std::unique_ptr<OGRFeature>> &apoOutputFeatures) override
     131             :     {
     132             :         const OGRGeometry *poGeom =
     133          36 :             poSrcFeature->GetGeomFieldRef(m_srcGeomField);
     134          36 :         std::unique_ptr<OGRFeature> poErrorFeature;
     135             : 
     136          36 :         if (poGeom)
     137             :         {
     138          36 :             if (poGeom->getDimension() < 1)
     139             :             {
     140           1 :                 CPLErrorOnce(CE_Warning, CPLE_AppDefined,
     141             :                              "Point geometry passed to 'gdal vector "
     142             :                              "check-geometry'. Point geometries are "
     143             :                              "always valid/simple.");
     144             :             }
     145             :             else
     146             :             {
     147          35 :                 auto eType = wkbFlatten(poGeom->getGeometryType());
     148          35 :                 GEOSGeometry *poGeosGeom = poGeom->exportToGEOS(m_geosContext);
     149             : 
     150          35 :                 if (!poGeosGeom)
     151             :                 {
     152             :                     // Try to find a useful message / coordinate from
     153             :                     // GEOS exception message.
     154           6 :                     poErrorFeature = CreateFeatureFromLastError();
     155             : 
     156           6 :                     if (eType == wkbPolygon)
     157             :                     {
     158             :                         const OGRLinearRing *poRing =
     159           4 :                             poGeom->toPolygon()->getExteriorRing();
     160           4 :                         if (poRing != nullptr && !poRing->IsEmpty())
     161             :                         {
     162           3 :                             auto poPoint = std::make_unique<OGRPoint>();
     163           3 :                             poRing->StartPoint(poPoint.get());
     164           3 :                             poErrorFeature->SetGeometry(std::move(poPoint));
     165             :                         }
     166             :                         else
     167             :                         {
     168             :                             // TODO get a point from somewhere else?
     169             :                         }
     170             :                     }
     171             :                 }
     172             :                 else
     173             :                 {
     174          29 :                     char *pszReason = nullptr;
     175          29 :                     GEOSGeometry *location = nullptr;
     176          29 :                     char ret = 1;
     177          29 :                     bool warnAboutGeosVersion = false;
     178          29 :                     bool checkedSimple = false;
     179             : 
     180          29 :                     if (eType == wkbPolygon || eType == wkbMultiPolygon ||
     181          10 :                         eType == wkbCurvePolygon || eType == wkbMultiSurface ||
     182             :                         eType == wkbGeometryCollection)
     183             :                     {
     184          21 :                         ret = GEOSisValidDetail_r(m_geosContext, poGeosGeom, 0,
     185             :                                                   &pszReason, &location);
     186             :                     }
     187             : 
     188          29 :                     if (eType == wkbLineString || eType == wkbMultiLineString ||
     189          23 :                         eType == wkbCircularString ||
     190          21 :                         eType == wkbCompoundCurve ||
     191           8 :                         (ret == 1 && eType == wkbGeometryCollection))
     192             :                     {
     193          10 :                         checkedSimple = true;
     194             : #if GEOS_VERSION_MAJOR > 3 ||                                                  \
     195             :     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 14)
     196          10 :                         ret = GEOSisSimpleDetail_r(m_geosContext, poGeosGeom, 1,
     197             :                                                    &location);
     198             : #else
     199             :                         ret = GEOSisSimple_r(m_geosContext, poGeosGeom);
     200             :                         warnAboutGeosVersion = true;
     201             : #endif
     202             :                     }
     203             : 
     204          29 :                     GEOSGeom_destroy_r(m_geosContext, poGeosGeom);
     205          29 :                     if (ret == 0)
     206             :                     {
     207          21 :                         if (warnAboutGeosVersion)
     208             :                         {
     209           0 :                             CPLErrorOnce(
     210             :                                 CE_Warning, CPLE_AppDefined,
     211             :                                 "Detected a non-simple linear geometry, but "
     212             :                                 "cannot output self-intersection points "
     213             :                                 "because GEOS library version is < 3.14.");
     214             :                         }
     215             : 
     216          21 :                         poErrorFeature = std::make_unique<OGRFeature>(m_defn);
     217          21 :                         if (pszReason == nullptr)
     218             :                         {
     219           8 :                             if (checkedSimple)
     220             :                             {
     221           8 :                                 poErrorFeature->SetField(
     222             :                                     ERROR_DESCRIPTION_FIELD,
     223             :                                     "self-intersection");
     224             :                             }
     225             :                         }
     226             :                         else
     227             :                         {
     228          13 :                             poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD,
     229             :                                                      pszReason);
     230          13 :                             GEOSFree_r(m_geosContext, pszReason);
     231             :                         }
     232             : 
     233          21 :                         if (location != nullptr)
     234             :                         {
     235             :                             std::unique_ptr<OGRGeometry> poErrorGeom(
     236             :                                 OGRGeometryFactory::createFromGEOS(
     237          21 :                                     m_geosContext, location));
     238          21 :                             GEOSGeom_destroy_r(m_geosContext, location);
     239             : 
     240          42 :                             poErrorGeom->assignSpatialReference(
     241          42 :                                 m_srcLayer.GetLayerDefn()
     242          21 :                                     ->GetGeomFieldDefn(m_srcGeomField)
     243          42 :                                     ->GetSpatialRef());
     244             : 
     245          21 :                             poErrorFeature->SetGeometry(std::move(poErrorGeom));
     246             :                         }
     247             :                     }
     248           8 :                     else if (ret == 2)
     249             :                     {
     250           0 :                         poErrorFeature = CreateFeatureFromLastError();
     251             :                     }
     252             :                 }
     253             :             }
     254             :         }
     255             : 
     256          36 :         if (!poErrorFeature && !m_skipValid)
     257             :         {
     258           3 :             poErrorFeature = std::make_unique<OGRFeature>(m_defn);
     259             :             // TODO Set geometry to POINT EMPTY ?
     260             :         }
     261             : 
     262          36 :         if (poErrorFeature)
     263             :         {
     264          30 :             if (!m_srcFieldMap.empty())
     265             :             {
     266           2 :                 poErrorFeature->SetFieldsFrom(
     267           1 :                     poSrcFeature.get(), m_srcFieldMap.data(), false, false);
     268             :             }
     269          30 :             poErrorFeature->SetFID(poSrcFeature->GetFID());
     270          30 :             apoOutputFeatures.push_back(std::move(poErrorFeature));
     271             :         }
     272          36 :     }
     273             : 
     274             :     CPL_DISALLOW_COPY_ASSIGN(GDALInvalidLocationLayer)
     275             : 
     276             :   private:
     277             :     std::vector<int> m_srcFieldMap{};
     278             :     OGRFeatureDefn *const m_defn;
     279             :     const GEOSContextHandle_t m_geosContext;
     280             :     const int m_srcGeomField;
     281             :     const bool m_skipValid;
     282             : };
     283             : 
     284          36 : GDALInvalidLocationLayer::~GDALInvalidLocationLayer()
     285             : {
     286          18 :     m_defn->Release();
     287          18 :     finishGEOS_r(m_geosContext);
     288          36 : }
     289             : 
     290             : #endif
     291             : 
     292          21 : bool GDALVectorCheckGeometryAlgorithm::RunStep(GDALPipelineStepRunContext &)
     293             : {
     294             : #ifdef HAVE_GEOS
     295          21 :     auto poSrcDS = m_inputDataset[0].GetDatasetRef();
     296          21 :     CPLAssert(poSrcDS);
     297          21 :     CPLAssert(m_outputDataset.GetName().empty());
     298          21 :     CPLAssert(!m_outputDataset.GetDatasetRef());
     299             : 
     300          21 :     const bool bSingleLayerOutput = m_inputLayerNames.empty()
     301          23 :                                         ? poSrcDS->GetLayerCount() == 1
     302           2 :                                         : m_inputLayerNames.size() == 1;
     303             : 
     304          42 :     auto outDS = std::make_unique<GDALVectorPipelineOutputDataset>(*poSrcDS);
     305          40 :     for (auto &&poSrcLayer : poSrcDS->GetLayers())
     306             :     {
     307          24 :         if (m_inputLayerNames.empty() ||
     308           0 :             std::find(m_inputLayerNames.begin(), m_inputLayerNames.end(),
     309          24 :                       poSrcLayer->GetDescription()) != m_inputLayerNames.end())
     310             :         {
     311          22 :             const auto poSrcLayerDefn = poSrcLayer->GetLayerDefn();
     312          22 :             if (poSrcLayerDefn->GetGeomFieldCount() == 0)
     313             :             {
     314           2 :                 if (m_inputLayerNames.empty())
     315           1 :                     continue;
     316           1 :                 ReportError(CE_Failure, CPLE_AppDefined,
     317             :                             "Specified layer '%s' has no geometry field",
     318           1 :                             poSrcLayer->GetDescription());
     319           3 :                 return false;
     320             :             }
     321             : 
     322             :             const int geomFieldIndex =
     323          20 :                 m_geomField.empty()
     324          20 :                     ? 0
     325           2 :                     : poSrcLayerDefn->GetGeomFieldIndex(m_geomField.c_str());
     326             : 
     327          20 :             if (geomFieldIndex == -1)
     328             :             {
     329           1 :                 ReportError(CE_Failure, CPLE_AppDefined,
     330             :                             "Specified geometry field '%s' does not exist in "
     331             :                             "layer '%s'",
     332           1 :                             m_geomField.c_str(), poSrcLayer->GetDescription());
     333           1 :                 return false;
     334             :             }
     335             : 
     336          19 :             std::vector<int> includeFieldIndices;
     337          21 :             for (const auto &fieldName : m_includeFields)
     338             :             {
     339             :                 auto iSrcField =
     340           3 :                     poSrcLayerDefn->GetFieldIndex(fieldName.c_str());
     341           3 :                 if (iSrcField == -1)
     342             :                 {
     343           1 :                     ReportError(
     344             :                         CE_Failure, CPLE_AppDefined,
     345             :                         "Specified field '%s' does not exist in layer '%s'",
     346           1 :                         fieldName.c_str(), poSrcLayer->GetDescription());
     347           1 :                     return false;
     348             :                 }
     349           2 :                 includeFieldIndices.push_back(iSrcField);
     350             :             }
     351             : 
     352          36 :             outDS->AddLayer(*poSrcLayer,
     353          18 :                             std::make_unique<GDALInvalidLocationLayer>(
     354             :                                 *poSrcLayer, includeFieldIndices,
     355             :                                 bSingleLayerOutput, geomFieldIndex,
     356          36 :                                 !m_includeValid));
     357             :         }
     358             :     }
     359             : 
     360          18 :     m_outputDataset.Set(std::move(outDS));
     361             : 
     362          18 :     return true;
     363             : #else
     364             :     ReportError(CE_Failure, CPLE_AppDefined,
     365             :                 "%s requires GDAL to be built against the GEOS library.", NAME);
     366             :     return false;
     367             : #endif
     368             : }
     369             : 
     370             : GDALVectorCheckGeometryAlgorithmStandalone::
     371             :     ~GDALVectorCheckGeometryAlgorithmStandalone() = default;
     372             : 
     373             : //! @endcond

Generated by: LCOV version 1.14