LCOV - code coverage report
Current view: top level - apps - gdalalg_vector_clean_coverage.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 98 128 76.6 %
Date: 2025-08-01 10:10:57 Functions: 7 7 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             : *
       3             :  * Project:  GDAL
       4             :  * Purpose:  "gdal vector clean-coverage" 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_clean_coverage.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             : #include "ogrsf_frmts.h"
      21             : 
      22             : #include <cinttypes>
      23             : 
      24             : #ifndef _
      25             : #define _(x) (x)
      26             : #endif
      27             : 
      28             : //! @cond Doxygen_Suppress
      29             : 
      30          36 : GDALVectorCleanCoverageAlgorithm::GDALVectorCleanCoverageAlgorithm(
      31          36 :     bool standaloneStep)
      32             :     : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
      33          36 :                                       standaloneStep)
      34             : {
      35          36 :     AddActiveLayerArg(&m_activeLayer);
      36             :     AddArg("snapping-distance", 0, _("Distance tolerance for snapping nodes"),
      37          72 :            &m_opts.snappingTolerance)
      38          36 :         .SetMinValueIncluded(0);
      39             : 
      40             :     AddArg("merge-strategy", 0,
      41             :            _("Algorithm to assign overlaps to neighboring polygons"),
      42          72 :            &m_opts.mergeStrategy)
      43         180 :         .SetChoices({"longest-border", "max-area", "min-area", "min-index"});
      44             : 
      45             :     AddArg("maximum-gap-width", 0, _("Maximum width of a gap to be closed"),
      46          72 :            &m_opts.maximumGapWidth)
      47          36 :         .SetMinValueIncluded(0);
      48          36 : }
      49             : 
      50             : #if defined HAVE_GEOS &&                                                       \
      51             :     (GEOS_VERSION_MAJOR > 3 ||                                                 \
      52             :      (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 14))
      53             : 
      54             : class GDALVectorCleanCoverageOutputDataset
      55             :     : public GDALVectorNonStreamingAlgorithmDataset
      56             : {
      57             :   public:
      58          14 :     GDALVectorCleanCoverageOutputDataset(
      59             :         const GDALVectorCleanCoverageAlgorithm::Options &opts)
      60          14 :         : m_opts(opts)
      61             :     {
      62          14 :         m_poGeosContext = OGRGeometry::createGEOSContext();
      63          14 :     }
      64             : 
      65             :     ~GDALVectorCleanCoverageOutputDataset() override;
      66             : 
      67          13 :     GEOSCoverageCleanParams *GetCoverageCleanParams() const
      68             :     {
      69             :         GEOSCoverageCleanParams *params =
      70          13 :             GEOSCoverageCleanParams_create_r(m_poGeosContext);
      71             : 
      72          13 :         if (!params)
      73             :         {
      74           0 :             CPLError(CE_Failure, CPLE_AppDefined,
      75             :                      "Failed to create coverage clean parameters");
      76           0 :             return nullptr;
      77             :         }
      78             : 
      79          13 :         if (!GEOSCoverageCleanParams_setSnappingDistance_r(
      80          13 :                 m_poGeosContext, params, m_opts.snappingTolerance))
      81             :         {
      82           0 :             CPLError(CE_Failure, CPLE_AppDefined,
      83             :                      "Failed to set snapping tolerance");
      84           0 :             GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
      85           0 :             return nullptr;
      86             :         }
      87             : 
      88          13 :         if (!GEOSCoverageCleanParams_setGapMaximumWidth_r(
      89          13 :                 m_poGeosContext, params, m_opts.maximumGapWidth))
      90             :         {
      91           0 :             CPLError(CE_Failure, CPLE_AppDefined,
      92             :                      "Failed to set maximum gap width");
      93           0 :             GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
      94           0 :             return nullptr;
      95             :         }
      96             : 
      97             :         int mergeStrategy;
      98          13 :         if (m_opts.mergeStrategy == "longest-border")
      99             :         {
     100          10 :             mergeStrategy = GEOS_MERGE_LONGEST_BORDER;
     101             :         }
     102           3 :         else if (m_opts.mergeStrategy == "max-area")
     103             :         {
     104           1 :             mergeStrategy = GEOS_MERGE_MAX_AREA;
     105             :         }
     106           2 :         else if (m_opts.mergeStrategy == "min-area")
     107             :         {
     108           1 :             mergeStrategy = GEOS_MERGE_MIN_AREA;
     109             :         }
     110           1 :         else if (m_opts.mergeStrategy == "min-index")
     111             :         {
     112           1 :             mergeStrategy = GEOS_MERGE_MIN_INDEX;
     113             :         }
     114             :         else
     115             :         {
     116           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     117             :                      "Unknown overlap merge strategy: %s",
     118           0 :                      m_opts.mergeStrategy.c_str());
     119           0 :             GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
     120           0 :             return nullptr;
     121             :         }
     122             : 
     123          13 :         if (!GEOSCoverageCleanParams_setOverlapMergeStrategy_r(
     124          13 :                 m_poGeosContext, params, mergeStrategy))
     125             :         {
     126           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     127             :                      "Failed to set overlap merge strategy");
     128           0 :             GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
     129           0 :             return nullptr;
     130             :         }
     131             : 
     132          13 :         return params;
     133             :     }
     134             : 
     135          13 :     bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) override
     136             :     {
     137          26 :         std::vector<OGRFeatureUniquePtr> features;
     138          26 :         std::vector<GEOSGeometry *> geoms;
     139             : 
     140          13 :         GEOSCoverageCleanParams *params = GetCoverageCleanParams();
     141             : 
     142             :         // Copy features from srcLayer into dstLayer, converting
     143             :         // their geometries to GEOS
     144          57 :         for (auto &feature : srcLayer)
     145             :         {
     146          47 :             const OGRGeometry *fgeom = feature->GetGeometryRef();
     147             : 
     148             :             const auto eFGType =
     149          47 :                 fgeom ? wkbFlatten(fgeom->getGeometryType()) : wkbUnknown;
     150          47 :             if (eFGType != wkbPolygon && eFGType != wkbMultiPolygon &&
     151           3 :                 eFGType != wkbCurvePolygon && eFGType != wkbMultiSurface)
     152             :             {
     153           3 :                 for (auto &geom : geoms)
     154             :                 {
     155           0 :                     GEOSGeom_destroy_r(m_poGeosContext, geom);
     156             :                 }
     157           3 :                 GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
     158           3 :                 CPLError(CE_Failure, CPLE_AppDefined,
     159             :                          "Coverage cleaning can only be performed on "
     160             :                          "polygonal geometries. Feature %" PRId64
     161             :                          " does not have one",
     162           3 :                          static_cast<int64_t>(feature->GetFID()));
     163           3 :                 return false;
     164             :             }
     165             : 
     166             :             GEOSGeometry *geosGeom =
     167          44 :                 fgeom->exportToGEOS(m_poGeosContext, false);
     168          44 :             if (!geosGeom)
     169             :             {
     170             :                 // should not happen normally
     171           0 :                 for (auto &geom : geoms)
     172             :                 {
     173           0 :                     GEOSGeom_destroy_r(m_poGeosContext, geom);
     174             :                 }
     175           0 :                 GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
     176           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     177             :                          "Geometry of feature %" PRId64
     178             :                          " failed to convert to GEOS",
     179           0 :                          static_cast<int64_t>(feature->GetFID()));
     180           0 :                 return false;
     181             :             }
     182          44 :             geoms.push_back(geosGeom);
     183             : 
     184          44 :             feature->SetGeometry(nullptr);  // free some memory
     185          44 :             feature->SetFDefnUnsafe(dstLayer.GetLayerDefn());
     186             : 
     187          44 :             features.push_back(std::move(feature));
     188             :         }
     189             : 
     190             :         // Perform coverage cleaning
     191          10 :         GEOSGeometry *coll = GEOSGeom_createCollection_r(
     192             :             m_poGeosContext, GEOS_GEOMETRYCOLLECTION, geoms.data(),
     193          10 :             static_cast<unsigned int>(geoms.size()));
     194             : 
     195          10 :         if (coll == nullptr)
     196             :         {
     197           0 :             for (auto &geom : geoms)
     198             :             {
     199           0 :                 GEOSGeom_destroy_r(m_poGeosContext, geom);
     200             :             }
     201           0 :             GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
     202           0 :             return false;
     203             :         }
     204             : 
     205             :         GEOSGeometry *geos_result =
     206          10 :             GEOSCoverageCleanWithParams_r(m_poGeosContext, coll, params);
     207          10 :         GEOSGeom_destroy_r(m_poGeosContext, coll);
     208          10 :         GEOSCoverageCleanParams_destroy_r(m_poGeosContext, params);
     209             : 
     210          10 :         if (geos_result == nullptr)
     211             :         {
     212           0 :             return false;
     213             :         }
     214             : 
     215          10 :         m_papoGeosResults = GEOSGeom_releaseCollection_r(
     216             :             m_poGeosContext, geos_result, &m_nGeosResultSize);
     217          10 :         GEOSGeom_destroy_r(m_poGeosContext, geos_result);
     218          10 :         CPLAssert(features.size() == m_nGeosResultSize);
     219             : 
     220             :         // Create features with the modified geometries
     221          54 :         for (size_t i = 0; i < features.size(); i++)
     222             :         {
     223          44 :             GEOSGeometry *dstGeom = m_papoGeosResults[i];
     224             : 
     225             :             std::unique_ptr<OGRGeometry> poSimplified(
     226          44 :                 OGRGeometryFactory::createFromGEOS(m_poGeosContext, dstGeom));
     227          44 :             GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
     228          44 :             m_papoGeosResults[i] = nullptr;
     229             : 
     230          44 :             if (poSimplified == nullptr)
     231             :             {
     232           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     233             :                          "Failed to convert result from GEOS");
     234           0 :                 return false;
     235             :             }
     236          88 :             poSimplified->assignSpatialReference(
     237          44 :                 dstLayer.GetLayerDefn()->GetGeomFieldDefn(0)->GetSpatialRef());
     238          44 :             features[i]->SetGeometry(std::move(poSimplified));
     239             : 
     240          44 :             if (dstLayer.CreateFeature(features[i].get()) != CE_None)
     241             :             {
     242           0 :                 return false;
     243             :             }
     244          44 :             features[i].reset();
     245             :         }
     246             : 
     247          10 :         return true;
     248             :     }
     249             : 
     250             :   private:
     251             :     CPL_DISALLOW_COPY_ASSIGN(GDALVectorCleanCoverageOutputDataset)
     252             : 
     253             :     const GDALVectorCleanCoverageAlgorithm::Options &m_opts;
     254             :     GEOSContextHandle_t m_poGeosContext{nullptr};
     255             :     GEOSGeometry **m_papoGeosResults{nullptr};
     256             :     unsigned int m_nGeosResultSize{0};
     257             : };
     258             : 
     259          28 : GDALVectorCleanCoverageOutputDataset::~GDALVectorCleanCoverageOutputDataset()
     260             : {
     261          14 :     if (m_poGeosContext != nullptr)
     262             :     {
     263          58 :         for (size_t i = 0; i < m_nGeosResultSize; i++)
     264             :         {
     265          44 :             GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
     266             :         }
     267          14 :         GEOSFree_r(m_poGeosContext, m_papoGeosResults);
     268          14 :         finishGEOS_r(m_poGeosContext);
     269             :     }
     270          28 : }
     271             : 
     272          14 : bool GDALVectorCleanCoverageAlgorithm::RunStep(GDALPipelineStepRunContext &)
     273             : {
     274          14 :     auto poSrcDS = m_inputDataset[0].GetDatasetRef();
     275             :     auto poDstDS =
     276          28 :         std::make_unique<GDALVectorCleanCoverageOutputDataset>(m_opts);
     277             : 
     278          14 :     bool bFoundActiveLayer = false;
     279             : 
     280          27 :     for (auto &&poSrcLayer : poSrcDS->GetLayers())
     281             :     {
     282          20 :         if (m_activeLayer.empty() ||
     283           4 :             m_activeLayer == poSrcLayer->GetDescription())
     284             :         {
     285          13 :             if (!poDstDS->AddProcessedLayer(*poSrcLayer))
     286             :             {
     287           3 :                 return false;
     288             :             }
     289          10 :             bFoundActiveLayer = true;
     290             :         }
     291             :         else
     292             :         {
     293           3 :             poDstDS->AddPassThroughLayer(*poSrcLayer);
     294             :         }
     295             :     }
     296             : 
     297          11 :     if (!bFoundActiveLayer)
     298             :     {
     299           1 :         ReportError(CE_Failure, CPLE_AppDefined,
     300             :                     "Specified layer '%s' was not found",
     301             :                     m_activeLayer.c_str());
     302           1 :         return false;
     303             :     }
     304             : 
     305          10 :     m_outputDataset.Set(std::move(poDstDS));
     306             : 
     307          10 :     return true;
     308             : }
     309             : 
     310             : #else
     311             : 
     312             : bool GDALVectorCleanCoverageAlgorithm::RunStep(GDALPipelineStepRunContext &)
     313             : {
     314             :     ReportError(CE_Failure, CPLE_AppDefined,
     315             :                 "%s requires GDAL to be built against version 3.14 or later of "
     316             :                 "the GEOS library.",
     317             :                 NAME);
     318             :     return false;
     319             : }
     320             : #endif  // HAVE_GEOS
     321             : 
     322             : GDALVectorCleanCoverageAlgorithmStandalone::
     323             :     ~GDALVectorCleanCoverageAlgorithmStandalone() = default;
     324             : 
     325             : //! @endcond

Generated by: LCOV version 1.14