LCOV - code coverage report
Current view: top level - apps - gdalalg_raster_clip.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 173 173 100.0 %
Date: 2026-06-03 12:46:18 Functions: 3 3 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  GDAL
       4             :  * Purpose:  "clip" step of "raster pipeline", or "gdal raster clip" standalone
       5             :  * Author:   Even Rouault <even dot rouault at spatialys.com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #include "gdalalg_raster_clip.h"
      14             : 
      15             : #include "gdal_priv.h"
      16             : #include "gdal_utils.h"
      17             : 
      18             : #include <algorithm>
      19             : #include <cmath>
      20             : 
      21             : //! @cond Doxygen_Suppress
      22             : 
      23             : #ifndef _
      24             : #define _(x) (x)
      25             : #endif
      26             : 
      27             : /************************************************************************/
      28             : /*          GDALRasterClipAlgorithm::GDALRasterClipAlgorithm()          */
      29             : /************************************************************************/
      30             : 
      31         129 : GDALRasterClipAlgorithm::GDALRasterClipAlgorithm(bool standaloneStep)
      32             :     : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
      33         129 :                                       standaloneStep)
      34             : {
      35         129 :     constexpr const char *EXCLUSION_GROUP = "bbox-window-geometry-like";
      36         129 :     AddBBOXArg(&m_bbox, _("Clipping bounding box as xmin,ymin,xmax,ymax"))
      37         129 :         .SetMutualExclusionGroup(EXCLUSION_GROUP);
      38         258 :     AddArg("bbox-crs", 0, _("CRS of clipping bounding box"), &m_bboxCrs)
      39         258 :         .SetIsCRSArg()
      40         129 :         .AddHiddenAlias("bbox_srs");
      41             : 
      42             :     AddArg("window", 0, _("Raster window as col,line,width,height in pixels"),
      43         258 :            &m_window)
      44         129 :         .SetRepeatedArgAllowed(false)
      45         129 :         .SetMinCount(4)
      46         129 :         .SetMaxCount(4)
      47         129 :         .SetDisplayHintAboutRepetition(false)
      48         258 :         .SetMutualExclusionGroup(EXCLUSION_GROUP)
      49             :         .AddValidationAction(
      50          41 :             [this]()
      51             :             {
      52          14 :                 CPLAssert(m_window.size() == 4);
      53          14 :                 if (m_window[2] <= 0 || m_window[3] <= 0)
      54             :                 {
      55           2 :                     CPLError(CE_Failure, CPLE_AppDefined,
      56             :                              "Value of 'window' should be "
      57             :                              "col,line,width,height with "
      58             :                              "width > 0 and height > 0");
      59           2 :                     return false;
      60             :                 }
      61          12 :                 return true;
      62         129 :             });
      63             : 
      64         258 :     AddArg("geometry", 0, _("Clipping geometry (WKT or GeoJSON)"), &m_geometry)
      65         129 :         .SetMutualExclusionGroup(EXCLUSION_GROUP);
      66         258 :     AddArg("geometry-crs", 0, _("CRS of clipping geometry"), &m_geometryCrs)
      67         258 :         .SetIsCRSArg()
      68         129 :         .AddHiddenAlias("geometry_srs");
      69             :     AddArg("like", 0, _("Dataset to use as a template for bounds"),
      70         258 :            &m_likeDataset, GDAL_OF_RASTER | GDAL_OF_VECTOR)
      71         258 :         .SetMetaVar("DATASET")
      72         129 :         .SetMutualExclusionGroup(EXCLUSION_GROUP);
      73             :     AddArg("like-sql", 0, ("SELECT statement to run on the 'like' dataset"),
      74         258 :            &m_likeSQL)
      75         258 :         .SetMetaVar("SELECT-STATEMENT")
      76         129 :         .SetMutualExclusionGroup("sql-where");
      77             :     AddArg("like-layer", 0, ("Name of the layer of the 'like' dataset"),
      78         258 :            &m_likeLayer)
      79         129 :         .SetMetaVar("LAYER-NAME");
      80             :     AddArg("like-where", 0, ("WHERE SQL clause to run on the 'like' dataset"),
      81         258 :            &m_likeWhere)
      82         258 :         .SetMetaVar("WHERE-EXPRESSION")
      83         129 :         .SetMutualExclusionGroup("sql-where");
      84             :     AddArg("only-bbox", 0,
      85             :            _("For 'geometry' and 'like', only consider their bounding box"),
      86         129 :            &m_onlyBBOX);
      87             :     AddArg("allow-bbox-outside-source", 0,
      88             :            _("Allow clipping box to include pixels outside input dataset"),
      89         129 :            &m_allowExtentOutsideSource);
      90             :     // Note: this would be a use case for multiple exclusion groups because it is not
      91             :     //       compatible with "window"
      92             :     AddArg("add-alpha", 0,
      93             :            _("Adds an alpha mask band to the destination when the source "
      94             :              "raster have none."),
      95         129 :            &m_addAlpha);
      96         129 : }
      97             : 
      98             : /************************************************************************/
      99             : /*                  GDALRasterClipAlgorithm::RunStep()                  */
     100             : /************************************************************************/
     101             : 
     102          36 : bool GDALRasterClipAlgorithm::RunStep(GDALPipelineStepRunContext &)
     103             : {
     104          36 :     auto poSrcDS = m_inputDataset[0].GetDatasetRef();
     105          36 :     CPLAssert(poSrcDS);
     106          36 :     CPLAssert(m_outputDataset.GetName().empty());
     107          36 :     CPLAssert(!m_outputDataset.GetDatasetRef());
     108             : 
     109          36 :     if (!m_window.empty())
     110             :     {
     111           4 :         if (m_addAlpha)
     112             :         {
     113           1 :             ReportError(CE_Failure, CPLE_NotSupported,
     114             :                         "'alpha' argument is not supported with 'window'");
     115           1 :             return false;
     116             :         }
     117             : 
     118           3 :         CPLStringList aosOptions;
     119           3 :         aosOptions.AddString("-of");
     120           3 :         aosOptions.AddString("VRT");
     121             : 
     122           3 :         aosOptions.AddString("-srcwin");
     123           3 :         aosOptions.AddString(CPLSPrintf("%d", m_window[0]));
     124           3 :         aosOptions.AddString(CPLSPrintf("%d", m_window[1]));
     125           3 :         aosOptions.AddString(CPLSPrintf("%d", m_window[2]));
     126           3 :         aosOptions.AddString(CPLSPrintf("%d", m_window[3]));
     127             : 
     128           3 :         if (!m_allowExtentOutsideSource)
     129             :         {
     130             :             // Unless we've specifically allowed the bounding box to extend beyond
     131             :             // the source raster, raise an error.
     132           3 :             aosOptions.AddString("-epo");
     133             :         }
     134             : 
     135             :         GDALTranslateOptions *psOptions =
     136           3 :             GDALTranslateOptionsNew(aosOptions.List(), nullptr);
     137             : 
     138           3 :         GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
     139           3 :         auto poRetDS = GDALDataset::FromHandle(
     140             :             GDALTranslate("", hSrcDS, psOptions, nullptr));
     141           3 :         GDALTranslateOptionsFree(psOptions);
     142             : 
     143           3 :         const bool bOK = poRetDS != nullptr;
     144           3 :         if (bOK)
     145             :         {
     146           3 :             m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
     147             :         }
     148             : 
     149           3 :         return bOK;
     150             :     }
     151             : 
     152          32 :     GDALGeoTransform gt;
     153          32 :     if (poSrcDS->GetGeoTransform(gt) != CE_None)
     154             :     {
     155           1 :         ReportError(
     156             :             CE_Failure, CPLE_NotSupported,
     157             :             "Clipping is not supported on a raster without a geotransform");
     158           1 :         return false;
     159             :     }
     160          31 :     if (!gt.IsAxisAligned())
     161             :     {
     162           1 :         ReportError(CE_Failure, CPLE_NotSupported,
     163             :                     "Clipping is not supported on a raster whose geotransform "
     164             :                     "has rotation terms");
     165           1 :         return false;
     166             :     }
     167             : 
     168          60 :     auto [poClipGeom, errMsg] = GetClipGeometry();
     169          30 :     if (!poClipGeom)
     170             :     {
     171           3 :         ReportError(CE_Failure, CPLE_AppDefined, "%s", errMsg.c_str());
     172           3 :         return false;
     173             :     }
     174             : 
     175          27 :     auto poLikeDS = m_likeDataset.GetDatasetRef();
     176          28 :     if (!poClipGeom->getSpatialReference() && poLikeDS &&
     177           1 :         poLikeDS->GetLayerCount() == 0)
     178             :     {
     179           1 :         ReportError(CE_Failure, CPLE_AppDefined,
     180             :                     "Dataset '%s' has no CRS. Its bounds cannot be used.",
     181           1 :                     poLikeDS->GetDescription());
     182           1 :         return false;
     183             :     }
     184             : 
     185          52 :     CPLStringList aosOptions;
     186          26 :     aosOptions.AddString("-of");
     187          26 :     aosOptions.AddString("VRT");
     188             : 
     189          26 :     OGREnvelope env;
     190          26 :     poClipGeom->getEnvelope(&env);
     191             : 
     192          26 :     if (m_onlyBBOX)
     193             :     {
     194           2 :         auto poPoly = std::make_unique<OGRPolygon>(env);
     195           1 :         poPoly->assignSpatialReference(poClipGeom->getSpatialReference());
     196           1 :         poClipGeom = std::move(poPoly);
     197             :     }
     198             : 
     199          26 :     const bool bBottomUpRaster = gt.yscale > 0;
     200             : 
     201          26 :     if (poClipGeom->IsRectangle() && !m_addAlpha && !bBottomUpRaster)
     202             :     {
     203          13 :         aosOptions.AddString("-projwin");
     204          13 :         aosOptions.AddString(CPLSPrintf("%.17g", env.MinX));
     205          13 :         aosOptions.AddString(CPLSPrintf("%.17g", env.MaxY));
     206          13 :         aosOptions.AddString(CPLSPrintf("%.17g", env.MaxX));
     207          13 :         aosOptions.AddString(CPLSPrintf("%.17g", env.MinY));
     208             : 
     209          13 :         auto poClipGeomSRS = poClipGeom->getSpatialReference();
     210          13 :         if (poClipGeomSRS)
     211             :         {
     212           4 :             const char *const apszOptions[] = {"FORMAT=WKT2", nullptr};
     213           8 :             const std::string osWKT = poClipGeomSRS->exportToWkt(apszOptions);
     214           4 :             aosOptions.AddString("-projwin_srs");
     215           4 :             aosOptions.AddString(osWKT.c_str());
     216             :         }
     217             : 
     218          13 :         if (m_allowExtentOutsideSource)
     219             :         {
     220           3 :             aosOptions.AddString("--no-warn-about-outside-window");
     221             :         }
     222             :         else
     223             :         {
     224             :             // Unless we've specifically allowed the bounding box to extend beyond
     225             :             // the source raster, raise an error.
     226          10 :             aosOptions.AddString("-epo");
     227             :         }
     228             : 
     229             :         GDALTranslateOptions *psOptions =
     230          13 :             GDALTranslateOptionsNew(aosOptions.List(), nullptr);
     231             : 
     232          13 :         GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
     233          13 :         auto poRetDS = GDALDataset::FromHandle(
     234             :             GDALTranslate("", hSrcDS, psOptions, nullptr));
     235          13 :         GDALTranslateOptionsFree(psOptions);
     236             : 
     237          13 :         const bool bOK = poRetDS != nullptr;
     238          13 :         if (bOK)
     239             :         {
     240          11 :             m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
     241             :         }
     242             : 
     243          13 :         return bOK;
     244             :     }
     245             :     else
     246             :     {
     247          13 :         if (bBottomUpRaster)
     248             :         {
     249           1 :             gt.yorig += gt.yscale * poSrcDS->GetRasterYSize();
     250           1 :             gt.yscale = -gt.yscale;
     251             :         }
     252             : 
     253             :         {
     254             :             auto poClipGeomInSrcSRS =
     255          26 :                 std::unique_ptr<OGRGeometry>(poClipGeom->clone());
     256          13 :             if (poClipGeom->getSpatialReference() && poSrcDS->GetSpatialRef())
     257           7 :                 poClipGeomInSrcSRS->transformTo(poSrcDS->GetSpatialRef());
     258          13 :             poClipGeomInSrcSRS->getEnvelope(&env);
     259             :         }
     260             : 
     261          13 :         OGREnvelope rasterEnv;
     262          13 :         poSrcDS->GetExtent(&rasterEnv, nullptr);
     263          13 :         if (!m_allowExtentOutsideSource && !rasterEnv.Contains(env))
     264             :         {
     265           1 :             ReportError(CE_Failure, CPLE_AppDefined,
     266             :                         "Clipping geometry is partially or totally outside the "
     267             :                         "extent of the raster. You can set the "
     268             :                         "'allow-bbox-outside-source' argument to proceed.");
     269           1 :             return false;
     270             :         }
     271             : 
     272          12 :         if (m_addAlpha)
     273             :         {
     274           2 :             aosOptions.AddString("-dstalpha");
     275             :         }
     276             : 
     277          12 :         aosOptions.AddString("-cutline");
     278          12 :         aosOptions.AddString(poClipGeom->exportToWkt());
     279             : 
     280          12 :         aosOptions.AddString("-wo");
     281          12 :         aosOptions.AddString("CUTLINE_ALL_TOUCHED=YES");
     282             : 
     283          12 :         auto poClipGeomSRS = poClipGeom->getSpatialReference();
     284          12 :         if (poClipGeomSRS)
     285             :         {
     286           7 :             const char *const apszOptions[] = {"FORMAT=WKT2", nullptr};
     287          14 :             const std::string osWKT = poClipGeomSRS->exportToWkt(apszOptions);
     288           7 :             aosOptions.AddString("-cutline_srs");
     289           7 :             aosOptions.AddString(osWKT.c_str());
     290             :         }
     291             : 
     292          12 :         constexpr double REL_EPS_PIXEL = 1e-3;
     293          12 :         const double dfMinX =
     294          12 :             gt.xorig +
     295          12 :             floor((env.MinX - gt.xorig) / gt.xscale + REL_EPS_PIXEL) *
     296          12 :                 gt.xscale;
     297          12 :         const double dfMinY =
     298          12 :             gt.yorig +
     299          12 :             ceil((env.MinY - gt.yorig) / gt.yscale - REL_EPS_PIXEL) * gt.yscale;
     300          12 :         const double dfMaxX =
     301          12 :             gt.xorig +
     302          12 :             ceil((env.MaxX - gt.xorig) / gt.xscale - REL_EPS_PIXEL) * gt.xscale;
     303          12 :         const double dfMaxY =
     304          12 :             gt.yorig +
     305          12 :             floor((env.MaxY - gt.yorig) / gt.yscale + REL_EPS_PIXEL) *
     306          12 :                 gt.yscale;
     307             : 
     308          12 :         aosOptions.AddString("-te");
     309          12 :         aosOptions.AddString(CPLSPrintf("%.17g", dfMinX));
     310             :         aosOptions.AddString(
     311          12 :             CPLSPrintf("%.17g", bBottomUpRaster ? dfMaxY : dfMinY));
     312          12 :         aosOptions.AddString(CPLSPrintf("%.17g", dfMaxX));
     313             :         aosOptions.AddString(
     314          12 :             CPLSPrintf("%.17g", bBottomUpRaster ? dfMinY : dfMaxY));
     315             : 
     316          12 :         aosOptions.AddString("-tr");
     317          12 :         aosOptions.AddString(CPLSPrintf("%.17g", gt.xscale));
     318          12 :         aosOptions.AddString(CPLSPrintf("%.17g", std::fabs(gt.yscale)));
     319             : 
     320             :         GDALWarpAppOptions *psOptions =
     321          12 :             GDALWarpAppOptionsNew(aosOptions.List(), nullptr);
     322             : 
     323          12 :         GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
     324          12 :         auto poRetDS = GDALDataset::FromHandle(
     325             :             GDALWarp("", nullptr, 1, &hSrcDS, psOptions, nullptr));
     326          12 :         GDALWarpAppOptionsFree(psOptions);
     327             : 
     328          12 :         const bool bOK = poRetDS != nullptr;
     329          12 :         if (bOK)
     330             :         {
     331          12 :             m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
     332             :         }
     333             : 
     334          12 :         return bOK;
     335             :     }
     336             : }
     337             : 
     338             : GDALRasterClipAlgorithmStandalone::~GDALRasterClipAlgorithmStandalone() =
     339             :     default;
     340             : 
     341             : //! @endcond

Generated by: LCOV version 1.14