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

Generated by: LCOV version 1.14