• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OSGeo / gdal / 15764079719

19 Jun 2025 06:18PM UTC coverage: 71.06%. First build
15764079719

Pull #12610

github

web-flow
Merge ebc9fc7ca into 47eb1b6d9
Pull Request #12610: VRT expressions / gdal raster calc: Handle NoData

85 of 91 new or added lines in 3 files covered. (93.41%)

574850 of 808964 relevant lines covered (71.06%)

249805.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.6
/apps/gdalalg_raster_calc.cpp
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  "gdal raster calc" subcommand
5
 * Author:   Daniel Baston
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, ISciences LLC
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12

13
#include "gdalalg_raster_calc.h"
14

15
#include "../frmts/vrt/gdal_vrt.h"
16
#include "../frmts/vrt/vrtdataset.h"
17

18
#include "cpl_float.h"
19
#include "cpl_vsi_virtual.h"
20
#include "gdal_priv.h"
21
#include "gdal_utils.h"
22
#include "vrtdataset.h"
23

24
#include <algorithm>
25
#include <array>
26
#include <optional>
27

28
//! @cond Doxygen_Suppress
29

30
#ifndef _
31
#define _(x) (x)
32
#endif
33

34
struct GDALCalcOptions
35
{
36
    GDALDataType dstType{GDT_Unknown};
37
    bool checkSRS{true};
38
    bool checkExtent{true};
39
};
40

41
static bool MatchIsCompleteVariableNameWithNoIndex(const std::string &str,
192✔
42
                                                   size_t from, size_t to)
43
{
44
    if (to < str.size())
192✔
45
    {
46
        // If the character after the end of the match is:
47
        // * alphanumeric or _ : we've matched only part of a variable name
48
        // * [ : we've matched a variable that already has an index
49
        // * ( : we've matched a function name
50
        if (std::isalnum(str[to]) || str[to] == '_' || str[to] == '[' ||
240✔
51
            str[to] == '(')
90✔
52
        {
53
            return false;
61✔
54
        }
55
    }
56
    if (from > 0)
131✔
57
    {
58
        // If the character before the start of the match is alphanumeric or _,
59
        // we've matched only part of a variable name.
60
        if (std::isalnum(str[from - 1]) || str[from - 1] == '_')
75✔
61
        {
62
            return false;
3✔
63
        }
64
    }
65

66
    return true;
128✔
67
}
68

69
/**
70
 *  Add a band subscript to all instances of a specified variable that
71
 *  do not already have such a subscript. For example, "X" would be
72
 *  replaced with "X[3]" but "X[1]" would be left untouched.
73
 */
74
static std::string SetBandIndices(const std::string &origExpression,
132✔
75
                                  const std::string &variable, int band,
76
                                  bool &expressionChanged)
77
{
78
    std::string expression = origExpression;
132✔
79
    expressionChanged = false;
132✔
80

81
    std::string::size_type seekPos = 0;
132✔
82
    auto pos = expression.find(variable, seekPos);
132✔
83
    while (pos != std::string::npos)
304✔
84
    {
85
        auto end = pos + variable.size();
172✔
86

87
        if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end))
172✔
88
        {
89
            // No index specified for variable
90
            expression = expression.substr(0, pos + variable.size()) + '[' +
216✔
91
                         std::to_string(band) + ']' + expression.substr(end);
324✔
92
            expressionChanged = true;
108✔
93
        }
94

95
        seekPos = end;
172✔
96
        pos = expression.find(variable, seekPos);
172✔
97
    }
98

99
    return expression;
132✔
100
}
101

102
static bool PosIsFunctionArgument(const std::string &expression, size_t pos)
40✔
103
{
104
    // If this position is a function argument, we should be able to
105
    // scan backwards for a ( and find only variable names, literals or commas.
106
    while (pos != 0)
40✔
107
    {
108
        const char c = expression[pos];
32✔
109
        if (c == '(')
32✔
110
        {
111
            pos--;
8✔
112
            break;
8✔
113
        }
114
        if (!(isspace(c) || isalnum(c) || c == ',' || c == '.' || c == '[' ||
24✔
115
              c == ']' || c == '_'))
116
        {
117
            return false;
4✔
118
        }
119
        pos--;
20✔
120
    }
121

122
    // Now what we've found the (, the preceding character should be part of a
123
    // value function name
124
    while (pos != 0)
16✔
125
    {
126
        const char c = expression[pos];
8✔
127
        if (isalnum(c) || c == '_')
8✔
128
        {
129
            return true;
8✔
130
        }
131
        if (!isspace(c))
×
132
        {
133
            return false;
×
134
        }
135
        pos--;
×
136
    }
137

138
    return false;
8✔
139
}
140

141
/**
142
 *  Replace X by X[1],X[2],...X[n]
143
 */
144
static std::string
145
SetBandIndicesFlattenedExpression(const std::string &origExpression,
16✔
146
                                  const std::string &variable, int nBands)
147
{
148
    std::string expression = origExpression;
16✔
149

150
    std::string::size_type seekPos = 0;
16✔
151
    auto pos = expression.find(variable, seekPos);
16✔
152
    while (pos != std::string::npos)
36✔
153
    {
154
        auto end = pos + variable.size();
20✔
155

156
        if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end) &&
40✔
157
            PosIsFunctionArgument(expression, pos))
20✔
158
        {
159
            std::string newExpr = expression.substr(0, pos);
8✔
160
            for (int i = 1; i <= nBands; ++i)
24✔
161
            {
162
                if (i > 1)
16✔
163
                    newExpr += ',';
8✔
164
                newExpr += variable;
16✔
165
                newExpr += '[';
16✔
166
                newExpr += std::to_string(i);
16✔
167
                newExpr += ']';
16✔
168
            }
169
            const size_t oldExprSize = expression.size();
8✔
170
            newExpr += expression.substr(end);
8✔
171
            expression = std::move(newExpr);
8✔
172
            end += expression.size() - oldExprSize;
8✔
173
        }
174

175
        seekPos = end;
20✔
176
        pos = expression.find(variable, seekPos);
20✔
177
    }
178

179
    return expression;
16✔
180
}
181

182
struct SourceProperties
183
{
184
    int nBands{0};
185
    int nX{0};
186
    int nY{0};
187
    std::array<double, 6> gt{};
188
    std::unique_ptr<OGRSpatialReference, OGRSpatialReferenceReleaser> srs{
189
        nullptr};
190
    std::vector<std::optional<double>> noData{};
191
    GDALDataType eDT{GDT_Unknown};
192
};
193

194
static std::optional<SourceProperties>
195
UpdateSourceProperties(SourceProperties &out, const std::string &dsn,
142✔
196
                       const GDALCalcOptions &options)
197
{
198
    SourceProperties source;
284✔
199
    bool srsMismatch = false;
142✔
200
    bool extentMismatch = false;
142✔
201
    bool dimensionMismatch = false;
142✔
202

203
    {
204
        std::unique_ptr<GDALDataset> ds(
205
            GDALDataset::Open(dsn.c_str(), GDAL_OF_RASTER));
142✔
206

207
        if (!ds)
142✔
208
        {
209
            CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
×
210
                     dsn.c_str());
211
            return std::nullopt;
×
212
        }
213

214
        source.nBands = ds->GetRasterCount();
142✔
215
        source.nX = ds->GetRasterXSize();
142✔
216
        source.nY = ds->GetRasterYSize();
142✔
217
        source.noData.resize(source.nBands);
142✔
218

219
        if (options.checkExtent)
142✔
220
        {
221
            ds->GetGeoTransform(source.gt.data());
138✔
222
        }
223

224
        if (options.checkSRS && out.srs)
142✔
225
        {
226
            const OGRSpatialReference *srs = ds->GetSpatialRef();
54✔
227
            srsMismatch = srs && !srs->IsSame(out.srs.get());
54✔
228
        }
229

230
        // Store the source data type if it is the same for all bands in the source
231
        bool bandsHaveSameType = true;
142✔
232
        for (int i = 1; i <= source.nBands; ++i)
362✔
233
        {
234
            GDALRasterBand *band = ds->GetRasterBand(i);
220✔
235

236
            if (i == 1)
220✔
237
            {
238
                source.eDT = band->GetRasterDataType();
142✔
239
            }
240
            else if (bandsHaveSameType &&
156✔
241
                     source.eDT != band->GetRasterDataType())
78✔
242
            {
243
                source.eDT = GDT_Unknown;
×
NEW
244
                bandsHaveSameType = false;
×
245
            }
246

247
            int success;
248
            double noData = band->GetNoDataValue(&success);
220✔
249
            if (success)
220✔
250
            {
251
                source.noData[i - 1] = noData;
25✔
252
            }
253
        }
254
    }
255

256
    if (source.nX != out.nX || source.nY != out.nY)
142✔
257
    {
258
        dimensionMismatch = true;
3✔
259
    }
260

261
    if (source.gt[0] != out.gt[0] || source.gt[2] != out.gt[2] ||
284✔
262
        source.gt[3] != out.gt[3] || source.gt[4] != out.gt[4])
284✔
263
    {
264
        extentMismatch = true;
5✔
265
    }
266
    if (source.gt[1] != out.gt[1] || source.gt[5] != out.gt[5])
142✔
267
    {
268
        // Resolutions are different. Are the extents the same?
269
        double xmaxOut = out.gt[0] + out.nX * out.gt[1] + out.nY * out.gt[2];
8✔
270
        double yminOut = out.gt[3] + out.nX * out.gt[4] + out.nY * out.gt[5];
8✔
271

272
        double xmax =
273
            source.gt[0] + source.nX * source.gt[1] + source.nY * source.gt[2];
8✔
274
        double ymin =
275
            source.gt[3] + source.nX * source.gt[4] + source.nY * source.gt[5];
8✔
276

277
        // Max allowable extent misalignment, expressed as fraction of a pixel
278
        constexpr double EXTENT_RTOL = 1e-3;
8✔
279

280
        if (std::abs(xmax - xmaxOut) > EXTENT_RTOL * std::abs(source.gt[1]) ||
11✔
281
            std::abs(ymin - yminOut) > EXTENT_RTOL * std::abs(source.gt[5]))
3✔
282
        {
283
            extentMismatch = true;
5✔
284
        }
285
    }
286

287
    if (options.checkExtent && extentMismatch)
142✔
288
    {
289
        CPLError(CE_Failure, CPLE_AppDefined,
1✔
290
                 "Input extents are inconsistent.");
291
        return std::nullopt;
1✔
292
    }
293

294
    if (!options.checkExtent && dimensionMismatch)
141✔
295
    {
296
        CPLError(CE_Failure, CPLE_AppDefined,
1✔
297
                 "Inputs do not have the same dimensions.");
298
        return std::nullopt;
1✔
299
    }
300

301
    // Find a common resolution
302
    if (source.nX > out.nX)
140✔
303
    {
304
        auto dx = CPLGreatestCommonDivisor(out.gt[1], source.gt[1]);
1✔
305
        if (dx == 0)
1✔
306
        {
307
            CPLError(CE_Failure, CPLE_AppDefined,
×
308
                     "Failed to find common resolution for inputs.");
309
            return std::nullopt;
×
310
        }
311
        out.nX = static_cast<int>(
1✔
312
            std::round(static_cast<double>(out.nX) * out.gt[1] / dx));
1✔
313
        out.gt[1] = dx;
1✔
314
    }
315
    if (source.nY > out.nY)
140✔
316
    {
317
        auto dy = CPLGreatestCommonDivisor(out.gt[5], source.gt[5]);
1✔
318
        if (dy == 0)
1✔
319
        {
320
            CPLError(CE_Failure, CPLE_AppDefined,
×
321
                     "Failed to find common resolution for inputs.");
322
            return std::nullopt;
×
323
        }
324
        out.nY = static_cast<int>(
1✔
325
            std::round(static_cast<double>(out.nY) * out.gt[5] / dy));
1✔
326
        out.gt[5] = dy;
1✔
327
    }
328

329
    if (srsMismatch)
140✔
330
    {
331
        CPLError(CE_Failure, CPLE_AppDefined,
1✔
332
                 "Input spatial reference systems are inconsistent.");
333
        return std::nullopt;
1✔
334
    }
335

336
    return source;
139✔
337
}
338

339
/** Create XML nodes for one or more derived bands resulting from the evaluation
340
 *  of a single expression
341
 *
342
 * @param root VRTDataset node to which the band nodes should be added
343
 * @param bandType the type of the band(s) to create
344
 * @param nXOut Number of columns in VRT dataset
345
 * @param nYOut Number of rows in VRT dataset
346
 * @param expression Expression for which band(s) should be added
347
 * @param dialect Expression dialect
348
 * @param flatten Generate a single band output raster per expression, even if
349
 *                input datasets are multiband.
350
 * @param noDataText nodata value to use for the created band, or "none", or ""
351
 * @param pixelFunctionArguments Pixel function arguments.
352
 * @param sources Mapping of source names to DSNs
353
 * @param sourceProps Mapping of source names to properties
354
 * @param fakeSourceFilename If not empty, used instead of real input filenames.
355
 * @return true if the band(s) were added, false otherwise
356
 */
357
static bool
358
CreateDerivedBandXML(CPLXMLNode *root, int nXOut, int nYOut,
108✔
359
                     GDALDataType bandType, const std::string &expression,
360
                     const std::string &dialect, bool flatten,
361
                     const std::string &noDataText,
362
                     const std::vector<std::string> &pixelFunctionArguments,
363
                     const std::map<std::string, std::string> &sources,
364
                     const std::map<std::string, SourceProperties> &sourceProps,
365
                     const std::string &fakeSourceFilename)
366
{
367
    int nOutBands = 1;  // By default, each expression produces a single output
108✔
368
                        // band. When processing the expression below, we may
369
                        // discover that the expression produces multiple bands,
370
                        // in which case this will be updated.
371

372
    for (int nOutBand = 1; nOutBand <= nOutBands; nOutBand++)
237✔
373
    {
374
        // Copy the expression for each output band, because we may modify it
375
        // when adding band indices (e.g., X -> X[1]) to the variables in the
376
        // expression.
377
        std::string bandExpression = expression;
133✔
378

379
        CPLXMLNode *band = CPLCreateXMLNode(root, CXT_Element, "VRTRasterBand");
133✔
380
        CPLAddXMLAttributeAndValue(band, "subClass", "VRTDerivedRasterBand");
133✔
381
        if (bandType == GDT_Unknown)
133✔
382
        {
383
            bandType = GDT_Float64;
96✔
384
        }
385
        CPLAddXMLAttributeAndValue(band, "dataType",
133✔
386
                                   GDALGetDataTypeName(bandType));
387

388
        std::optional<double> dstNoData;
133✔
389
        bool autoSelectNoDataValue = false;
133✔
390
        if (noDataText.empty())
133✔
391
        {
392
            autoSelectNoDataValue = true;
122✔
393
        }
394
        else if (noDataText != "none")
11✔
395
        {
396
            char *end;
397
            dstNoData = CPLStrtod(noDataText.c_str(), &end);
11✔
398
            if (end != noDataText.c_str() + noDataText.size())
11✔
399
            {
NEW
400
                CPLError(CE_Failure, CPLE_AppDefined,
×
401
                         "Invalid NoData value: %s", noDataText.c_str());
NEW
402
                return false;
×
403
            }
404
        }
405

406
        for (const auto &[source_name, dsn] : sources)
303✔
407
        {
408
            auto it = sourceProps.find(source_name);
174✔
409
            CPLAssert(it != sourceProps.end());
174✔
410
            const auto &props = it->second;
174✔
411

412
            bool expressionAppliedPerBand = false;
174✔
413
            if (dialect == "builtin")
174✔
414
            {
415
                expressionAppliedPerBand = !flatten;
42✔
416
            }
417
            else
418
            {
419
                const int nDefaultInBand = std::min(props.nBands, nOutBand);
132✔
420

421
                if (flatten)
132✔
422
                {
423
                    bandExpression = SetBandIndicesFlattenedExpression(
16✔
424
                        bandExpression, source_name, props.nBands);
16✔
425
                }
426

427
                bandExpression =
428
                    SetBandIndices(bandExpression, source_name, nDefaultInBand,
264✔
429
                                   expressionAppliedPerBand);
132✔
430
            }
431

432
            if (expressionAppliedPerBand)
174✔
433
            {
434
                if (nOutBands <= 1)
132✔
435
                {
436
                    nOutBands = props.nBands;
93✔
437
                }
438
                else if (props.nBands != 1 && props.nBands != nOutBands)
39✔
439
                {
440
                    CPLError(CE_Failure, CPLE_AppDefined,
3✔
441
                             "Expression cannot operate on all bands of "
442
                             "rasters with incompatible numbers of bands "
443
                             "(source %s has %d bands but expected to have "
444
                             "1 or %d bands).",
445
                             source_name.c_str(), props.nBands, nOutBands);
3✔
446
                    return false;
4✔
447
                }
448
            }
449

450
            // Create a source for each input band that is used in
451
            // the expression.
452
            for (int nInBand = 1; nInBand <= props.nBands; nInBand++)
461✔
453
            {
454
                CPLString inBandVariable;
290✔
455
                if (dialect == "builtin")
290✔
456
                {
457
                    if (!flatten && props.nBands >= 2 && nInBand != nOutBand)
72✔
458
                        continue;
11✔
459
                }
460
                else
461
                {
462
                    inBandVariable.Printf("%s[%d]", source_name.c_str(),
463
                                          nInBand);
218✔
464
                    if (bandExpression.find(inBandVariable) ==
218✔
465
                        std::string::npos)
466
                    {
467
                        continue;
75✔
468
                    }
469
                }
470

471
                const std::optional<double> &srcNoData =
472
                    props.noData[nInBand - 1];
204✔
473

474
                CPLXMLNode *source = CPLCreateXMLNode(
204✔
475
                    band, CXT_Element,
476
                    srcNoData.has_value() ? "ComplexSource" : "SimpleSource");
204✔
477
                if (!inBandVariable.empty())
204✔
478
                {
479
                    CPLAddXMLAttributeAndValue(source, "name",
143✔
480
                                               inBandVariable.c_str());
481
                }
482

483
                CPLXMLNode *sourceFilename =
484
                    CPLCreateXMLNode(source, CXT_Element, "SourceFilename");
204✔
485
                if (fakeSourceFilename.empty())
204✔
486
                {
487
                    CPLAddXMLAttributeAndValue(sourceFilename, "relativeToVRT",
145✔
488
                                               "0");
489
                    CPLCreateXMLNode(sourceFilename, CXT_Text, dsn.c_str());
145✔
490
                }
491
                else
492
                {
493
                    CPLCreateXMLNode(sourceFilename, CXT_Text,
59✔
494
                                     fakeSourceFilename.c_str());
495
                }
496

497
                CPLXMLNode *sourceBand =
498
                    CPLCreateXMLNode(source, CXT_Element, "SourceBand");
204✔
499
                CPLCreateXMLNode(sourceBand, CXT_Text,
204✔
500
                                 std::to_string(nInBand).c_str());
408✔
501

502
                if (srcNoData.has_value())
204✔
503
                {
504
                    CPLXMLNode *srcNoDataNode =
505
                        CPLCreateXMLNode(source, CXT_Element, "NODATA");
25✔
506
                    std::string srcNoDataText =
507
                        CPLSPrintf("%.17g", srcNoData.value());
50✔
508
                    CPLCreateXMLNode(srcNoDataNode, CXT_Text,
25✔
509
                                     srcNoDataText.c_str());
510

511
                    if (autoSelectNoDataValue && !dstNoData.has_value())
25✔
512
                    {
513
                        dstNoData = srcNoData;
6✔
514
                    }
515
                }
516

517
                if (fakeSourceFilename.empty())
204✔
518
                {
519
                    CPLXMLNode *srcRect =
520
                        CPLCreateXMLNode(source, CXT_Element, "SrcRect");
145✔
521
                    CPLAddXMLAttributeAndValue(srcRect, "xOff", "0");
145✔
522
                    CPLAddXMLAttributeAndValue(srcRect, "yOff", "0");
145✔
523
                    CPLAddXMLAttributeAndValue(
145✔
524
                        srcRect, "xSize", std::to_string(props.nX).c_str());
290✔
525
                    CPLAddXMLAttributeAndValue(
145✔
526
                        srcRect, "ySize", std::to_string(props.nY).c_str());
290✔
527

528
                    CPLXMLNode *dstRect =
529
                        CPLCreateXMLNode(source, CXT_Element, "DstRect");
145✔
530
                    CPLAddXMLAttributeAndValue(dstRect, "xOff", "0");
145✔
531
                    CPLAddXMLAttributeAndValue(dstRect, "yOff", "0");
145✔
532
                    CPLAddXMLAttributeAndValue(dstRect, "xSize",
145✔
533
                                               std::to_string(nXOut).c_str());
290✔
534
                    CPLAddXMLAttributeAndValue(dstRect, "ySize",
145✔
535
                                               std::to_string(nYOut).c_str());
290✔
536
                }
537
            }
538

539
            if (dstNoData.has_value())
171✔
540
            {
541
                if (!GDALIsValueExactAs(dstNoData.value(), bandType))
25✔
542
                {
543
                    CPLError(
1✔
544
                        CE_Failure, CPLE_AppDefined,
545
                        "Band output type %s cannot represent NoData value %g",
546
                        GDALGetDataTypeName(bandType), dstNoData.value());
1✔
547
                    return false;
1✔
548
                }
549

550
                CPLXMLNode *noDataNode =
551
                    CPLCreateXMLNode(band, CXT_Element, "NoDataValue");
24✔
552
                CPLString dstNoDataText =
553
                    CPLSPrintf("%.17g", dstNoData.value());
48✔
554
                CPLCreateXMLNode(noDataNode, CXT_Text, dstNoDataText.c_str());
24✔
555
            }
556
        }
557

558
        CPLXMLNode *pixelFunctionType =
559
            CPLCreateXMLNode(band, CXT_Element, "PixelFunctionType");
129✔
560
        CPLXMLNode *arguments =
561
            CPLCreateXMLNode(band, CXT_Element, "PixelFunctionArguments");
129✔
562

563
        if (dialect == "builtin")
129✔
564
        {
565
            CPLCreateXMLNode(pixelFunctionType, CXT_Text, expression.c_str());
28✔
566
        }
567
        else
568
        {
569
            CPLCreateXMLNode(pixelFunctionType, CXT_Text, "expression");
101✔
570
            CPLAddXMLAttributeAndValue(arguments, "dialect", "muparser");
101✔
571
            // Add the expression as a last step, because we may modify the
572
            // expression as we iterate through the bands.
573
            CPLAddXMLAttributeAndValue(arguments, "expression",
101✔
574
                                       bandExpression.c_str());
575
        }
576

577
        if (!pixelFunctionArguments.empty())
129✔
578
        {
579
            const CPLStringList args(pixelFunctionArguments);
20✔
580
            for (const auto &[key, value] : cpl::IterateNameValue(args))
20✔
581
            {
582
                CPLAddXMLAttributeAndValue(arguments, key, value);
10✔
583
            }
584
        }
585
    }
586

587
    return true;
104✔
588
}
589

590
static bool ParseSourceDescriptors(const std::vector<std::string> &inputs,
113✔
591
                                   std::map<std::string, std::string> &datasets,
592
                                   std::string &firstSourceName,
593
                                   bool requireSourceNames)
594
{
595
    for (size_t iInput = 0; iInput < inputs.size(); iInput++)
258✔
596
    {
597
        const std::string &input = inputs[iInput];
150✔
598
        std::string name;
150✔
599

600
        const auto pos = input.find('=');
150✔
601
        if (pos == std::string::npos)
150✔
602
        {
603
            if (requireSourceNames && inputs.size() > 1)
59✔
604
            {
605
                CPLError(CE_Failure, CPLE_AppDefined,
1✔
606
                         "Inputs must be named when more than one input is "
607
                         "provided.");
608
                return false;
1✔
609
            }
610
            name = "X";
58✔
611
            if (iInput > 0)
58✔
612
            {
613
                name += std::to_string(iInput);
2✔
614
            }
615
        }
616
        else
617
        {
618
            name = input.substr(0, pos);
91✔
619
        }
620

621
        // Check input name is legal
622
        for (size_t i = 0; i < name.size(); ++i)
319✔
623
        {
624
            const char c = name[i];
173✔
625
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
173✔
626
            {
627
                // ok
628
            }
629
            else if (c == '_' || (c >= '0' && c <= '9'))
20✔
630
            {
631
                if (i == 0)
19✔
632
                {
633
                    // Reserved constants in MuParser start with an underscore
634
                    CPLError(
2✔
635
                        CE_Failure, CPLE_AppDefined,
636
                        "Name '%s' is illegal because it starts with a '%c'",
637
                        name.c_str(), c);
638
                    return false;
2✔
639
                }
640
            }
641
            else
642
            {
643
                CPLError(CE_Failure, CPLE_AppDefined,
1✔
644
                         "Name '%s' is illegal because character '%c' is not "
645
                         "allowed",
646
                         name.c_str(), c);
647
                return false;
1✔
648
            }
649
        }
650

651
        std::string dsn =
652
            (pos == std::string::npos) ? input : input.substr(pos + 1);
146✔
653
        if (datasets.find(name) != datasets.end())
146✔
654
        {
655
            CPLError(CE_Failure, CPLE_AppDefined,
1✔
656
                     "An input with name '%s' has already been provided",
657
                     name.c_str());
658
            return false;
1✔
659
        }
660
        datasets[name] = std::move(dsn);
145✔
661

662
        if (iInput == 0)
145✔
663
        {
664
            firstSourceName = std::move(name);
109✔
665
        }
666
    }
667

668
    return true;
108✔
669
}
670

671
static bool ReadFileLists(const std::vector<GDALArgDatasetValue> &inputDS,
83✔
672
                          std::vector<std::string> &inputFilenames)
673
{
674
    for (const auto &dsVal : inputDS)
194✔
675
    {
676
        const auto &input = dsVal.GetName();
111✔
677
        if (!input.empty() && input[0] == '@')
111✔
678
        {
679
            auto f =
680
                VSIVirtualHandleUniquePtr(VSIFOpenL(input.c_str() + 1, "r"));
2✔
681
            if (!f)
2✔
682
            {
683
                CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
×
684
                         input.c_str() + 1);
×
685
                return false;
×
686
            }
687
            while (const char *filename = CPLReadLineL(f.get()))
6✔
688
            {
689
                inputFilenames.push_back(filename);
4✔
690
            }
4✔
691
        }
692
        else
693
        {
694
            inputFilenames.push_back(input);
109✔
695
        }
696
    }
697

698
    return true;
83✔
699
}
700

701
/** Creates a VRT datasource with one or more derived raster bands containing
702
 *  results of an expression.
703
 *
704
 * To make this work with muparser (which does not support vector types), we
705
 * do a simple parsing of the expression internally, transforming it into
706
 * multiple expressions with explicit band indices. For example, for a two-band
707
 * raster "X", the expression "X + 3" will be transformed into "X[1] + 3" and
708
 * "X[2] + 3". The use of brackets is for readability only; as far as the
709
 * expression engine is concerned, the variables "X[1]" and "X[2]" have nothing
710
 * to do with each other.
711
 *
712
 * @param inputs A list of sources, expressed as NAME=DSN
713
 * @param expressions A list of expressions to be evaluated
714
 * @param dialect Expression dialect
715
 * @param flatten Generate a single band output raster per expression, even if
716
 *                input datasets are multiband.
717
 * @param noData NoData values to use for output bands, or "none", or ""
718
 * @param pixelFunctionArguments Pixel function arguments.
719
 * @param options flags controlling which checks should be performed on the inputs
720
 * @param[out] maxSourceBands Maximum number of bands in source dataset(s)
721
 * @param fakeSourceFilename If not empty, used instead of real input filenames.
722
 *
723
 * @return a newly created VRTDataset, or nullptr on error
724
 */
725
static std::unique_ptr<GDALDataset> GDALCalcCreateVRTDerived(
113✔
726
    const std::vector<std::string> &inputs,
727
    const std::vector<std::string> &expressions, const std::string &dialect,
728
    bool flatten, const std::string &noData,
729
    const std::vector<std::vector<std::string>> &pixelFunctionArguments,
730
    const GDALCalcOptions &options, int &maxSourceBands,
731
    const std::string &fakeSourceFilename = std::string())
732
{
733
    if (inputs.empty())
113✔
734
    {
735
        return nullptr;
×
736
    }
737

738
    std::map<std::string, std::string> sources;
226✔
739
    std::string firstSource;
226✔
740
    bool requireSourceNames = dialect != "builtin";
113✔
741
    if (!ParseSourceDescriptors(inputs, sources, firstSource,
113✔
742
                                requireSourceNames))
743
    {
744
        return nullptr;
5✔
745
    }
746

747
    // Use the first source provided to determine properties of the output
748
    const char *firstDSN = sources[firstSource].c_str();
108✔
749

750
    maxSourceBands = 0;
108✔
751

752
    // Read properties from the first source
753
    SourceProperties out;
216✔
754
    {
755
        std::unique_ptr<GDALDataset> ds(
756
            GDALDataset::Open(firstDSN, GDAL_OF_RASTER));
108✔
757

758
        if (!ds)
108✔
759
        {
760
            CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
×
761
                     firstDSN);
762
            return nullptr;
×
763
        }
764

765
        out.nX = ds->GetRasterXSize();
108✔
766
        out.nY = ds->GetRasterYSize();
108✔
767
        out.nBands = 1;
108✔
768
        out.srs.reset(ds->GetSpatialRef() ? ds->GetSpatialRef()->Clone()
108✔
769
                                          : nullptr);
770
        ds->GetGeoTransform(out.gt.data());
108✔
771
    }
772

773
    CPLXMLTreeCloser root(CPLCreateXMLNode(nullptr, CXT_Element, "VRTDataset"));
216✔
774

775
    maxSourceBands = 0;
108✔
776

777
    // Collect properties of the different sources, and verity them for
778
    // consistency.
779
    std::map<std::string, SourceProperties> sourceProps;
216✔
780
    for (const auto &[source_name, dsn] : sources)
247✔
781
    {
782
        // TODO avoid opening the first source twice.
783
        auto props = UpdateSourceProperties(out, dsn, options);
142✔
784
        if (props.has_value())
142✔
785
        {
786
            maxSourceBands = std::max(maxSourceBands, props->nBands);
139✔
787
            sourceProps[source_name] = std::move(props.value());
139✔
788
        }
789
        else
790
        {
791
            return nullptr;
3✔
792
        }
793
    }
794

795
    size_t iExpr = 0;
105✔
796
    for (const auto &origExpression : expressions)
209✔
797
    {
798
        GDALDataType bandType = options.dstType;
108✔
799

800
        // If output band type has not been specified, set it equal to the
801
        // input band type for certain pixel functions, if the inputs have
802
        // a consistent band type.
803
        if (bandType == GDT_Unknown && dialect == "builtin" &&
158✔
804
            (origExpression == "min" || origExpression == "max" ||
72✔
805
             origExpression == "mode"))
22✔
806
        {
807
            for (const auto &[_, props] : sourceProps)
12✔
808
            {
809
                if (bandType == GDT_Unknown)
6✔
810
                {
811
                    bandType = props.eDT;
6✔
812
                }
813
                else if (props.eDT == GDT_Unknown || props.eDT != bandType)
×
814
                {
815
                    bandType = GDT_Unknown;
×
816
                    break;
×
817
                }
818
            }
819
        }
820

821
        if (!CreateDerivedBandXML(root.get(), out.nX, out.nY, bandType,
108✔
822
                                  origExpression, dialect, flatten, noData,
823
                                  pixelFunctionArguments[iExpr], sources,
108✔
824
                                  sourceProps, fakeSourceFilename))
825
        {
826
            return nullptr;
4✔
827
        }
828
        ++iExpr;
104✔
829
    }
830

831
    //CPLDebug("VRT", "%s", CPLSerializeXMLTree(root.get()));
832

833
    auto ds = fakeSourceFilename.empty()
101✔
834
                  ? std::make_unique<VRTDataset>(out.nX, out.nY)
835
                  : std::make_unique<VRTDataset>(1, 1);
202✔
836
    if (ds->XMLInit(root.get(), "") != CE_None)
101✔
837
    {
838
        return nullptr;
×
839
    };
840
    ds->SetGeoTransform(out.gt.data());
101✔
841
    if (out.srs)
101✔
842
    {
843
        ds->SetSpatialRef(out.srs.get());
52✔
844
    }
845

846
    return ds;
101✔
847
}
848

849
/************************************************************************/
850
/*          GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm()          */
851
/************************************************************************/
852

853
GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm(bool standaloneStep) noexcept
102✔
854
    : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
855
                                      ConstructorOptions()
306✔
856
                                          .SetStandaloneStep(standaloneStep)
102✔
857
                                          .SetAddDefaultArguments(false)
102✔
858
                                          .SetAutoOpenInputDatasets(false)
102✔
859
                                          .SetInputDatasetMetaVar("INPUTS")
204✔
860
                                          .SetInputDatasetMaxCount(INT_MAX))
306✔
861
{
862
    AddRasterInputArgs(false, false);
102✔
863
    if (standaloneStep)
102✔
864
    {
865
        AddProgressArg();
81✔
866
        AddRasterOutputArgs(false);
81✔
867
    }
868

869
    AddOutputDataTypeArg(&m_type);
102✔
870

871
    AddArg("no-check-srs", 0,
872
           _("Do not check consistency of input spatial reference systems"),
873
           &m_noCheckSRS);
102✔
874
    AddArg("no-check-extent", 0, _("Do not check consistency of input extents"),
875
           &m_noCheckExtent);
102✔
876

877
    AddArg("propagate-nodata", 0,
878
           _("Whether to set pixels to the output NoData value if any of the "
879
             "input pixels is NoData"),
880
           &m_propagateNoData);
102✔
881

882
    AddArg("calc", 0, _("Expression(s) to evaluate"), &m_expr)
204✔
883
        .SetRequired()
102✔
884
        .SetPackedValuesAllowed(false)
102✔
885
        .SetMinCount(1)
102✔
886
        .SetAutoCompleteFunction(
887
            [this](const std::string &currentValue)
4✔
888
            {
889
                std::vector<std::string> ret;
4✔
890
                if (m_dialect == "builtin")
2✔
891
                {
892
                    if (currentValue.find('(') == std::string::npos)
1✔
893
                        return VRTDerivedRasterBand::GetPixelFunctionNames();
1✔
894
                }
895
                return ret;
1✔
896
            });
102✔
897

898
    AddArg("dialect", 0, _("Expression dialect"), &m_dialect)
204✔
899
        .SetDefault(m_dialect)
102✔
900
        .SetChoices("muparser", "builtin");
102✔
901

902
    AddArg("flatten", 0,
903
           _("Generate a single band output raster per expression, even if "
904
             "input datasets are multiband"),
905
           &m_flatten);
102✔
906

907
    AddNodataArg(&m_nodata, true);
102✔
908

909
    // This is a hidden option only used by test_gdalalg_raster_calc_expression_rewriting()
910
    // for now
911
    AddArg("no-check-expression", 0,
912
           _("Whether to skip expression validity checks for virtual format "
913
             "output"),
914
           &m_noCheckExpression)
204✔
915
        .SetHidden();
102✔
916

917
    AddValidationAction(
102✔
918
        [this]()
164✔
919
        {
920
            GDALPipelineStepRunContext ctxt;
87✔
921
            return m_noCheckExpression || !IsGDALGOutput() || RunStep(ctxt);
87✔
922
        });
923
}
102✔
924

925
/************************************************************************/
926
/*                  GDALRasterCalcAlgorithm::RunImpl()                  */
927
/************************************************************************/
928

929
bool GDALRasterCalcAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
78✔
930
                                      void *pProgressData)
931
{
932
    GDALPipelineStepRunContext stepCtxt;
78✔
933
    stepCtxt.m_pfnProgress = pfnProgress;
78✔
934
    stepCtxt.m_pProgressData = pProgressData;
78✔
935
    return RunPreStepPipelineValidations() && RunStep(stepCtxt);
78✔
936
}
937

938
/************************************************************************/
939
/*                GDALRasterCalcAlgorithm::RunStep()                    */
940
/************************************************************************/
941

942
bool GDALRasterCalcAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt)
83✔
943
{
944
    CPLAssert(!m_outputDataset.GetDatasetRef());
83✔
945

946
    GDALCalcOptions options;
83✔
947
    options.checkExtent = !m_noCheckExtent;
83✔
948
    options.checkSRS = !m_noCheckSRS;
83✔
949
    if (!m_type.empty())
83✔
950
    {
951
        options.dstType = GDALGetDataTypeByName(m_type.c_str());
4✔
952
    }
953

954
    std::vector<std::string> inputFilenames;
166✔
955
    if (!ReadFileLists(m_inputDataset, inputFilenames))
83✔
956
    {
957
        return false;
×
958
    }
959

960
    std::vector<std::vector<std::string>> pixelFunctionArgs;
166✔
961
    if (m_dialect == "builtin")
83✔
962
    {
963
        for (std::string &expr : m_expr)
27✔
964
        {
965
            const CPLStringList aosTokens(
966
                CSLTokenizeString2(expr.c_str(), "()",
967
                                   CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
14✔
968
            const char *pszFunction = aosTokens[0];
14✔
969
            const auto *pair =
970
                VRTDerivedRasterBand::GetPixelFunction(pszFunction);
14✔
971
            if (!pair)
14✔
972
            {
973
                ReportError(CE_Failure, CPLE_NotSupported,
×
974
                            "'%s' is a unknown builtin function", pszFunction);
975
                return false;
×
976
            }
977
            if (aosTokens.size() == 2)
14✔
978
            {
979
                std::vector<std::string> validArguments;
2✔
980
                AddOptionsSuggestions(pair->second.c_str(), 0, std::string(),
2✔
981
                                      validArguments);
982
                for (std::string &s : validArguments)
6✔
983
                {
984
                    if (!s.empty() && s.back() == '=')
4✔
985
                        s.pop_back();
4✔
986
                }
987

988
                const CPLStringList aosTokensArgs(CSLTokenizeString2(
989
                    aosTokens[1], ",",
990
                    CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
2✔
991
                for (const auto &[key, value] :
4✔
992
                     cpl::IterateNameValue(aosTokensArgs))
6✔
993
                {
994
                    if (std::find(validArguments.begin(), validArguments.end(),
2✔
995
                                  key) == validArguments.end())
2✔
996
                    {
997
                        if (validArguments.empty())
×
998
                        {
999
                            ReportError(
×
1000
                                CE_Failure, CPLE_IllegalArg,
1001
                                "'%s' is a unrecognized argument for builtin "
1002
                                "function '%s'. It does not accept any "
1003
                                "argument",
1004
                                key, pszFunction);
1005
                        }
1006
                        else
1007
                        {
1008
                            std::string validArgumentsStr;
×
1009
                            for (const std::string &s : validArguments)
×
1010
                            {
1011
                                if (!validArgumentsStr.empty())
×
1012
                                    validArgumentsStr += ", ";
×
1013
                                validArgumentsStr += '\'';
×
1014
                                validArgumentsStr += s;
×
1015
                                validArgumentsStr += '\'';
×
1016
                            }
1017
                            ReportError(
×
1018
                                CE_Failure, CPLE_IllegalArg,
1019
                                "'%s' is a unrecognized argument for builtin "
1020
                                "function '%s'. Only %s %s supported",
1021
                                key, pszFunction,
1022
                                validArguments.size() == 1 ? "is" : "are",
×
1023
                                validArgumentsStr.c_str());
1024
                        }
1025
                        return false;
×
1026
                    }
1027
                    CPL_IGNORE_RET_VAL(value);
2✔
1028
                }
1029
                pixelFunctionArgs.emplace_back(aosTokensArgs);
2✔
1030
            }
1031
            else
1032
            {
1033
                pixelFunctionArgs.push_back(std::vector<std::string>());
12✔
1034
            }
1035
            expr = pszFunction;
14✔
1036
        }
1037
    }
1038
    else
1039
    {
1040
        pixelFunctionArgs.resize(m_expr.size());
70✔
1041
    }
1042

1043
    if (m_propagateNoData)
83✔
1044
    {
1045
        if (m_nodata == "none")
3✔
1046
        {
NEW
1047
            ReportError(CE_Failure, CPLE_AppDefined,
×
1048
                        "Output NoData value must be specified to use "
1049
                        "--propagate-nodata");
NEW
1050
            return false;
×
1051
        }
1052
        for (auto &args : pixelFunctionArgs)
6✔
1053
        {
1054
            args.push_back("propagateNoData=1");
3✔
1055
        }
1056
    }
1057

1058
    int maxSourceBands = 0;
83✔
1059
    auto vrt = GDALCalcCreateVRTDerived(inputFilenames, m_expr, m_dialect,
83✔
1060
                                        m_flatten, m_nodata, pixelFunctionArgs,
83✔
1061
                                        options, maxSourceBands);
166✔
1062
    if (vrt == nullptr)
83✔
1063
    {
1064
        return false;
12✔
1065
    }
1066

1067
    if (!m_noCheckExpression)
71✔
1068
    {
1069
        const bool bIsVRT =
1070
            m_format == "VRT" ||
142✔
1071
            (m_format.empty() &&
57✔
1072
             EQUAL(
54✔
1073
                 CPLGetExtensionSafe(m_outputDataset.GetName().c_str()).c_str(),
1074
                 "VRT"));
58✔
1075
        const bool bIsGDALG =
1076
            m_format == "GDALG" ||
142✔
1077
            (m_format.empty() &&
57✔
1078
             cpl::ends_with(m_outputDataset.GetName(), ".gdalg.json"));
27✔
1079
        if (!m_standaloneStep || m_format == "stream" || bIsVRT || bIsGDALG)
58✔
1080
        {
1081
            // Try reading a single pixel to check formulas are valid.
1082
            std::vector<GByte> dummyData(vrt->GetRasterCount());
30✔
1083

1084
            auto poGTIFFDrv = GetGDALDriverManager()->GetDriverByName("GTiff");
30✔
1085
            std::string osTmpFilename;
30✔
1086
            if (poGTIFFDrv)
30✔
1087
            {
1088
                std::string osFilename =
1089
                    VSIMemGenerateHiddenFilename("tmp.tif");
60✔
1090
                auto poDS = std::unique_ptr<GDALDataset>(
1091
                    poGTIFFDrv->Create(osFilename.c_str(), 1, 1, maxSourceBands,
1092
                                       GDT_Byte, nullptr));
60✔
1093
                if (poDS)
30✔
1094
                    osTmpFilename = std::move(osFilename);
30✔
1095
            }
1096
            if (!osTmpFilename.empty())
30✔
1097
            {
1098
                auto fakeVRT = GDALCalcCreateVRTDerived(
1099
                    inputFilenames, m_expr, m_dialect, m_flatten, m_nodata,
30✔
1100
                    pixelFunctionArgs, options, maxSourceBands, osTmpFilename);
30✔
1101
                if (fakeVRT &&
60✔
1102
                    fakeVRT->RasterIO(GF_Read, 0, 0, 1, 1, dummyData.data(), 1,
30✔
1103
                                      1, GDT_Byte, vrt->GetRasterCount(),
1104
                                      nullptr, 0, 0, 0, nullptr) != CE_None)
30✔
1105
                {
1106
                    return false;
5✔
1107
                }
1108
            }
1109
            if (bIsGDALG)
25✔
1110
            {
1111
                return true;
1✔
1112
            }
1113
        }
1114
    }
1115

1116
    if (m_format == "stream" || !m_standaloneStep)
65✔
1117
    {
1118
        m_outputDataset.Set(std::move(vrt));
23✔
1119
        return true;
23✔
1120
    }
1121

1122
    CPLStringList translateArgs;
84✔
1123
    if (!m_format.empty())
42✔
1124
    {
1125
        translateArgs.AddString("-of");
7✔
1126
        translateArgs.AddString(m_format.c_str());
7✔
1127
    }
1128
    for (const auto &co : m_creationOptions)
43✔
1129
    {
1130
        translateArgs.AddString("-co");
1✔
1131
        translateArgs.AddString(co.c_str());
1✔
1132
    }
1133

1134
    GDALTranslateOptions *translateOptions =
1135
        GDALTranslateOptionsNew(translateArgs.List(), nullptr);
42✔
1136
    GDALTranslateOptionsSetProgress(translateOptions, ctxt.m_pfnProgress,
42✔
1137
                                    ctxt.m_pProgressData);
1138

1139
    auto poOutDS =
1140
        std::unique_ptr<GDALDataset>(GDALDataset::FromHandle(GDALTranslate(
1141
            m_outputDataset.GetName().c_str(), GDALDataset::ToHandle(vrt.get()),
42✔
1142
            translateOptions, nullptr)));
84✔
1143
    GDALTranslateOptionsFree(translateOptions);
42✔
1144

1145
    const bool bOK = poOutDS != nullptr;
42✔
1146
    m_outputDataset.Set(std::move(poOutDS));
42✔
1147

1148
    return bOK;
42✔
1149
}
1150

1151
GDALRasterCalcAlgorithmStandalone::~GDALRasterCalcAlgorithmStandalone() =
1152
    default;
1153

1154
//! @endcond
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc