Coverage Report

Created: 2026-07-14 06:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_reproject.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  "reproject" step of "raster pipeline"
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_reproject.h"
14
#include "gdalalg_raster_write.h"
15
16
#include "gdal_alg.h"
17
#include "gdal_priv.h"
18
#include "gdal_utils.h"
19
#include "gdalwarper.h"
20
21
#include <cmath>
22
23
//! @cond Doxygen_Suppress
24
25
#ifndef _
26
0
#define _(x) (x)
27
#endif
28
29
/************************************************************************/
30
/*     GDALRasterReprojectAlgorithm::GDALRasterReprojectAlgorithm()     */
31
/************************************************************************/
32
33
GDALRasterReprojectAlgorithm::GDALRasterReprojectAlgorithm(bool standaloneStep)
34
0
    : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
35
0
                                      standaloneStep)
36
0
{
37
38
0
    AddArg(GDAL_ARG_NAME_INPUT_CRS, 's', _("Input CRS"), &m_srcCrs)
39
0
        .SetIsCRSArg()
40
0
        .AddHiddenAlias("s_srs")
41
0
        .AddHiddenAlias("src-crs");
42
43
0
    AddArg("like", 0,
44
0
           _("Dataset to use as a template for target bounds, CRS, size and "
45
0
             "nodata"),
46
0
           &m_likeDataset, GDAL_OF_RASTER)
47
0
        .SetMetaVar("DATASET");
48
49
0
    AddArg(GDAL_ARG_NAME_OUTPUT_CRS, 'd', _("Output CRS"), &m_dstCrs)
50
0
        .SetIsCRSArg()
51
0
        .AddHiddenAlias("t_srs")
52
0
        .AddHiddenAlias("dst-crs");
53
54
0
    GDALRasterReprojectUtils::AddResamplingArg(this, m_resampling);
55
56
0
    AddArg("resolution", 0, _("Target resolution (in destination CRS units)"),
57
0
           &m_resolution)
58
0
        .SetMinCount(2)
59
0
        .SetMaxCount(2)
60
0
        .SetMinValueExcluded(0)
61
0
        .SetRepeatedArgAllowed(false)
62
0
        .SetDisplayHintAboutRepetition(false)
63
0
        .SetMetaVar("<xres>,<yres>")
64
0
        .SetMutualExclusionGroup("resolution-size");
65
66
0
    AddArg("size", 0, _("Target size in pixels"), &m_size)
67
0
        .SetMinCount(2)
68
0
        .SetMaxCount(2)
69
0
        .SetMinValueIncluded(0)
70
0
        .SetRepeatedArgAllowed(false)
71
0
        .SetDisplayHintAboutRepetition(false)
72
0
        .SetMetaVar("<width>,<height>")
73
0
        .SetMutualExclusionGroup("resolution-size");
74
75
0
    auto &arg = AddBBOXArg(&m_bbox,
76
0
                           _("Target bounding box (in destination CRS units)"));
77
78
0
    arg.AddValidationAction(
79
0
        [this, &arg]()
80
0
        {
81
            // Validate it's not empty
82
0
            const std::vector<double> &bbox = arg.Get<std::vector<double>>();
83
0
            if ((bbox[0] >= bbox[2]) || (bbox[1] >= bbox[3]))
84
0
            {
85
0
                ReportError(CE_Failure, CPLE_AppDefined,
86
0
                            "Invalid bounding box specified");
87
0
                return false;
88
0
            }
89
0
            else
90
0
            {
91
0
                return true;
92
0
            }
93
0
        });
94
95
0
    AddArg("bbox-crs", 0, _("CRS of target bounding box"), &m_bboxCrs)
96
0
        .SetIsCRSArg()
97
0
        .AddHiddenAlias("bbox_srs");
98
99
0
    AddArg("target-aligned-pixels", 0,
100
0
           _("Round target extent to target resolution"),
101
0
           &m_targetAlignedPixels)
102
0
        .AddHiddenAlias("tap")
103
0
        .SetCategory(GAAC_ADVANCED);
104
0
    AddArg("input-nodata", 0,
105
0
           _("Set nodata values for input bands ('None' to unset)."),
106
0
           &m_srcNoData)
107
0
        .SetMinCount(1)
108
0
        .AddHiddenAlias("src-nodata")
109
0
        .SetRepeatedArgAllowed(false)
110
0
        .SetCategory(GAAC_ADVANCED);
111
0
    AddArg("output-nodata", 0,
112
0
           _("Set nodata values for output bands ('None' to unset)."),
113
0
           &m_dstNoData)
114
0
        .SetMinCount(1)
115
0
        .AddHiddenAlias("dst-nodata")
116
0
        .SetRepeatedArgAllowed(false)
117
0
        .SetCategory(GAAC_ADVANCED);
118
0
    AddArg("add-alpha", 0,
119
0
           _("Adds an alpha mask band to the destination when the source "
120
0
             "raster have none."),
121
0
           &m_addAlpha)
122
0
        .SetCategory(GAAC_ADVANCED);
123
124
0
    GDALRasterReprojectUtils::AddWarpOptTransformOptErrorThresholdArg(
125
0
        this, m_warpOptions, m_transformOptions, m_errorThreshold);
126
127
0
    AddNumThreadsArg(&m_numThreads, &m_numThreadsStr);
128
0
}
129
130
/************************************************************************/
131
/*             GDALRasterReprojectUtils::AddResamplingArg()             */
132
/************************************************************************/
133
134
/*static */ void
135
GDALRasterReprojectUtils::AddResamplingArg(GDALAlgorithm *alg,
136
                                           std::string &resampling)
137
0
{
138
0
    alg->AddArg("resampling", 'r', _("Resampling method"), &resampling)
139
0
        .SetChoices("nearest", "bilinear", "cubic", "cubicspline", "lanczos",
140
0
                    "average", "rms", "mode", "min", "max", "med", "q1", "q3",
141
0
                    "sum")
142
0
        .SetDefault("nearest")
143
0
        .SetHiddenChoices("near");
144
0
}
145
146
/************************************************************************/
147
/*              AddWarpOptTransformOptErrorThresholdArg()               */
148
/************************************************************************/
149
150
/* static */
151
void GDALRasterReprojectUtils::AddWarpOptTransformOptErrorThresholdArg(
152
    GDALAlgorithm *alg, std::vector<std::string> &warpOptions,
153
    std::vector<std::string> &transformOptions, double &errorThreshold)
154
0
{
155
0
    {
156
0
        auto &arg =
157
0
            alg->AddArg("warp-option", 0, _("Warping option(s)"), &warpOptions)
158
0
                .AddAlias("wo")
159
0
                .SetMetaVar("<NAME>=<VALUE>")
160
0
                .SetCategory(GAAC_ADVANCED)
161
0
                .SetPackedValuesAllowed(false);
162
0
        arg.AddValidationAction([alg, &arg]()
163
0
                                { return alg->ParseAndValidateKeyValue(arg); });
164
0
        arg.SetAutoCompleteFunction(
165
0
            [](const std::string &currentValue)
166
0
            {
167
0
                std::vector<std::string> ret;
168
0
                GDALAlgorithm::AddOptionsSuggestions(GDALWarpGetOptionList(), 0,
169
0
                                                     currentValue, ret);
170
0
                return ret;
171
0
            });
172
0
    }
173
0
    {
174
0
        auto &arg = alg->AddArg("transform-option", 0, _("Transform option(s)"),
175
0
                                &transformOptions)
176
0
                        .AddAlias("to")
177
0
                        .SetMetaVar("<NAME>=<VALUE>")
178
0
                        .SetCategory(GAAC_ADVANCED)
179
0
                        .SetPackedValuesAllowed(false);
180
0
        arg.AddValidationAction([alg, &arg]()
181
0
                                { return alg->ParseAndValidateKeyValue(arg); });
182
0
        arg.SetAutoCompleteFunction(
183
0
            [](const std::string &currentValue)
184
0
            {
185
0
                std::vector<std::string> ret;
186
0
                GDALAlgorithm::AddOptionsSuggestions(
187
0
                    GDALGetGenImgProjTranformerOptionList(), 0, currentValue,
188
0
                    ret);
189
0
                return ret;
190
0
            });
191
0
    }
192
0
    alg->AddArg("error-threshold", 0, _("Error threshold"), &errorThreshold)
193
0
        .AddAlias("et")
194
0
        .SetMinValueIncluded(0)
195
0
        .SetCategory(GAAC_ADVANCED);
196
0
}
197
198
/************************************************************************/
199
/*          GDALRasterReprojectAlgorithm::CanHandleNextStep()           */
200
/************************************************************************/
201
202
bool GDALRasterReprojectAlgorithm::CanHandleNextStep(
203
    GDALPipelineStepAlgorithm *poNextStep) const
204
0
{
205
0
    return poNextStep->GetName() == GDALRasterWriteAlgorithm::NAME &&
206
0
           poNextStep->GetOutputFormat() != "stream";
207
0
}
208
209
/************************************************************************/
210
/*               GDALRasterReprojectAlgorithm::RunStep()                */
211
/************************************************************************/
212
213
bool GDALRasterReprojectAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt)
214
0
{
215
0
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
216
0
    CPLAssert(poSrcDS);
217
0
    CPLAssert(m_outputDataset.GetName().empty());
218
0
    CPLAssert(!m_outputDataset.GetDatasetRef());
219
220
0
    CPLStringList aosOptions;
221
0
    std::string outputFilename;
222
223
0
    aosOptions.AddString("--invoked-from-gdal-algorithm");
224
225
    // --like provide defaults: override if not explicitly set
226
0
    if (auto poLikeDS = m_likeDataset.GetDatasetRef())
227
0
    {
228
0
        const auto poSpatialRef = poLikeDS->GetSpatialRef();
229
0
        if (poSpatialRef)
230
0
        {
231
0
            char *pszWKT = nullptr;
232
0
            poSpatialRef->exportToWkt(&pszWKT);
233
0
            m_dstCrs = pszWKT;
234
0
            CPLFree(pszWKT);
235
0
            GDALGeoTransform gt;
236
0
            if (poLikeDS->GetGeoTransform(gt) == CE_None)
237
0
            {
238
0
                if (gt.IsAxisAligned())
239
0
                {
240
0
                    if (m_resolution.empty())
241
0
                    {
242
0
                        m_resolution = {std::abs(gt[1]), std::abs(gt[5])};
243
0
                    }
244
0
                    const int nXSize = poLikeDS->GetRasterXSize();
245
0
                    const int nYSize = poLikeDS->GetRasterYSize();
246
0
                    if (m_size.empty())
247
0
                    {
248
0
                        m_size = {nXSize, nYSize};
249
0
                    }
250
0
                    if (m_bbox.empty())
251
0
                    {
252
0
                        double minX = gt.xorig;
253
0
                        double maxY = gt.yorig;
254
0
                        double maxX =
255
0
                            gt.xorig + nXSize * gt.xscale + nYSize * gt.xrot;
256
0
                        double minY =
257
0
                            gt.yorig + nXSize * gt.yrot + nYSize * gt.yscale;
258
0
                        if (minY > maxY)
259
0
                            std::swap(minY, maxY);
260
0
                        m_bbox = {minX, minY, maxX, maxY};
261
0
                        m_bboxCrs = m_dstCrs;
262
0
                    }
263
0
                }
264
0
                else
265
0
                {
266
0
                    ReportError(
267
0
                        CE_Warning, CPLE_AppDefined,
268
0
                        "Dataset provided with --like has a geotransform "
269
0
                        "with rotation. Ignoring it");
270
0
                }
271
0
            }
272
0
        }
273
0
    }
274
275
0
    if (ctxt.m_poNextUsableStep)
276
0
    {
277
0
        CPLAssert(CanHandleNextStep(ctxt.m_poNextUsableStep));
278
0
        outputFilename = ctxt.m_poNextUsableStep->GetOutputDataset().GetName();
279
0
        const auto &format = ctxt.m_poNextUsableStep->GetOutputFormat();
280
0
        if (!format.empty())
281
0
        {
282
0
            aosOptions.AddString("-of");
283
0
            aosOptions.AddString(format.c_str());
284
0
        }
285
286
0
        bool bFoundNumThreads = false;
287
0
        for (const std::string &co :
288
0
             ctxt.m_poNextUsableStep->GetCreationOptions())
289
0
        {
290
0
            aosOptions.AddString("-co");
291
0
            if (STARTS_WITH_CI(co.c_str(), "NUM_THREADS="))
292
0
                bFoundNumThreads = true;
293
0
            aosOptions.AddString(co.c_str());
294
0
        }
295
296
        // Forward m_numThreads to GeoTIFF driver if --co NUM_THREADS not
297
        // specified
298
0
        if (!bFoundNumThreads && m_numThreads > 1 &&
299
0
            (EQUAL(format.c_str(), "GTIFF") || EQUAL(format.c_str(), "COG") ||
300
0
             (format.empty() &&
301
0
              EQUAL(CPLGetExtensionSafe(outputFilename.c_str()).c_str(),
302
0
                    "tif"))))
303
0
        {
304
0
            aosOptions.AddString("-co");
305
0
            aosOptions.AddString(CPLSPrintf("NUM_THREADS=%d", m_numThreads));
306
0
        }
307
0
    }
308
0
    else
309
0
    {
310
0
        aosOptions.AddString("-of");
311
0
        aosOptions.AddString("VRT");
312
0
    }
313
0
    if (!m_srcCrs.empty())
314
0
    {
315
0
        aosOptions.AddString("-s_srs");
316
0
        aosOptions.AddString(m_srcCrs.c_str());
317
0
    }
318
0
    if (!m_dstCrs.empty())
319
0
    {
320
0
        aosOptions.AddString("-t_srs");
321
0
        aosOptions.AddString(m_dstCrs.c_str());
322
0
    }
323
0
    if (!m_resampling.empty())
324
0
    {
325
0
        aosOptions.AddString("-r");
326
0
        aosOptions.AddString(m_resampling.c_str());
327
0
    }
328
0
    if (!m_resolution.empty())
329
0
    {
330
0
        aosOptions.AddString("-tr");
331
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_resolution[0]));
332
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_resolution[1]));
333
0
    }
334
0
    if (!m_size.empty())
335
0
    {
336
0
        aosOptions.AddString("-ts");
337
0
        aosOptions.AddString(CPLSPrintf("%d", m_size[0]));
338
0
        aosOptions.AddString(CPLSPrintf("%d", m_size[1]));
339
0
    }
340
0
    if (!m_bbox.empty())
341
0
    {
342
0
        aosOptions.AddString("-te");
343
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_bbox[0]));
344
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_bbox[1]));
345
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_bbox[2]));
346
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_bbox[3]));
347
0
    }
348
0
    if (!m_bboxCrs.empty())
349
0
    {
350
0
        aosOptions.AddString("-te_srs");
351
0
        aosOptions.AddString(m_bboxCrs.c_str());
352
0
    }
353
0
    if (m_targetAlignedPixels)
354
0
    {
355
0
        aosOptions.AddString("-tap");
356
0
    }
357
0
    if (!m_srcNoData.empty())
358
0
    {
359
0
        aosOptions.push_back("-srcnodata");
360
0
        std::string s;
361
0
        for (const std::string &v : m_srcNoData)
362
0
        {
363
0
            if (!s.empty())
364
0
                s += " ";
365
0
            s += v;
366
0
        }
367
0
        aosOptions.push_back(s);
368
0
    }
369
0
    if (!m_dstNoData.empty())
370
0
    {
371
0
        aosOptions.push_back("-dstnodata");
372
0
        std::string s;
373
0
        for (const std::string &v : m_dstNoData)
374
0
        {
375
0
            if (!s.empty())
376
0
                s += " ";
377
0
            s += v;
378
0
        }
379
0
        aosOptions.push_back(s);
380
0
    }
381
0
    if (m_addAlpha)
382
0
    {
383
0
        aosOptions.AddString("-dstalpha");
384
0
    }
385
386
0
    bool bFoundNumThreads = false;
387
0
    for (const std::string &opt : m_warpOptions)
388
0
    {
389
0
        aosOptions.AddString("-wo");
390
0
        if (STARTS_WITH_CI(opt.c_str(), "NUM_THREADS="))
391
0
            bFoundNumThreads = true;
392
0
        aosOptions.AddString(opt.c_str());
393
0
    }
394
0
    if (bFoundNumThreads)
395
0
    {
396
0
        if (GetArg(GDAL_ARG_NAME_NUM_THREADS)->IsExplicitlySet())
397
0
        {
398
0
            ReportError(CE_Failure, CPLE_AppDefined,
399
0
                        "--num-threads argument and NUM_THREADS warp options "
400
0
                        "are mutually exclusive.");
401
0
            return false;
402
0
        }
403
0
    }
404
0
    else
405
0
    {
406
0
        aosOptions.AddString("-wo");
407
0
        aosOptions.AddString(CPLSPrintf("NUM_THREADS=%d", m_numThreads));
408
0
    }
409
410
0
    for (const std::string &opt : m_transformOptions)
411
0
    {
412
0
        aosOptions.AddString("-to");
413
0
        aosOptions.AddString(opt.c_str());
414
0
    }
415
0
    if (std::isfinite(m_errorThreshold))
416
0
    {
417
0
        aosOptions.AddString("-et");
418
0
        aosOptions.AddString(CPLSPrintf("%.17g", m_errorThreshold));
419
0
    }
420
421
0
    bool bOK = false;
422
0
    GDALWarpAppOptions *psOptions =
423
0
        GDALWarpAppOptionsNew(aosOptions.List(), nullptr);
424
0
    if (psOptions)
425
0
    {
426
0
        if (ctxt.m_poNextUsableStep)
427
0
        {
428
0
            GDALWarpAppOptionsSetProgress(psOptions, ctxt.m_pfnProgress,
429
0
                                          ctxt.m_pProgressData);
430
0
        }
431
0
        GDALDatasetH hSrcDS = GDALDataset::ToHandle(poSrcDS);
432
0
        auto poRetDS = GDALDataset::FromHandle(GDALWarp(
433
0
            outputFilename.c_str(), nullptr, 1, &hSrcDS, psOptions, nullptr));
434
0
        GDALWarpAppOptionsFree(psOptions);
435
0
        bOK = poRetDS != nullptr;
436
0
        if (bOK)
437
0
        {
438
0
            m_outputDataset.Set(std::unique_ptr<GDALDataset>(poRetDS));
439
0
        }
440
0
    }
441
0
    return bOK;
442
0
}
443
444
GDALRasterReprojectAlgorithmStandalone::
445
0
    ~GDALRasterReprojectAlgorithmStandalone() = default;
446
447
//! @endcond