Coverage Report

Created: 2026-02-14 09:00

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