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

OSGeo / gdal / 15715506568

17 Jun 2025 06:40PM UTC coverage: 71.052%. First build
15715506568

Pull #12551

github

web-flow
Merge 095f7911b into 7a290749b
Pull Request #12551: gdal raster calc: add a --dialect=muparser|builtin, where builtin can be used to compute a single band from all bands of a single input dataset

151 of 170 new or added lines in 3 files covered. (88.82%)

571728 of 804660 relevant lines covered (71.05%)

251318.17 hits per line

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

92.53
/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,
127✔
42
                                                   size_t from, size_t to)
43
{
44
    if (to < str.size())
127✔
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] == '[' ||
148✔
51
            str[to] == '(')
52✔
52
        {
53
            return false;
45✔
54
        }
55
    }
56
    if (from > 0)
82✔
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] == '_')
45✔
61
        {
62
            return false;
3✔
63
        }
64
    }
65

66
    return true;
79✔
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,
95✔
75
                                  const std::string &variable, int band,
76
                                  bool &expressionChanged)
77
{
78
    std::string expression = origExpression;
95✔
79
    expressionChanged = false;
95✔
80

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

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

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

99
    return expression;
95✔
100
}
101

102
/**
103
 *  Replace X by X[1],X[2],...X[n]
104
 */
105
static std::string
106
SetBandIndicesFlattenedExpression(const std::string &origExpression,
4✔
107
                                  const std::string &variable, int nBands)
108
{
109
    std::string expression = origExpression;
4✔
110

111
    std::string::size_type seekPos = 0;
4✔
112
    auto pos = expression.find(variable, seekPos);
4✔
113
    while (pos != std::string::npos)
8✔
114
    {
115
        auto end = pos + variable.size();
4✔
116

117
        if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end))
4✔
118
        {
119
            std::string newExpr = expression.substr(0, pos);
4✔
120
            for (int i = 1; i <= nBands; ++i)
12✔
121
            {
122
                if (i > 1)
8✔
123
                    newExpr += ',';
4✔
124
                newExpr += variable;
8✔
125
                newExpr += '[';
8✔
126
                newExpr += std::to_string(i);
8✔
127
                newExpr += ']';
8✔
128
            }
129
            const size_t oldExprSize = expression.size();
4✔
130
            newExpr += expression.substr(end);
4✔
131
            expression = std::move(newExpr);
4✔
132
            end += expression.size() - oldExprSize;
4✔
133
        }
134

135
        seekPos = end;
4✔
136
        pos = expression.find(variable, seekPos);
4✔
137
    }
138

139
    return expression;
4✔
140
}
141

142
struct SourceProperties
143
{
144
    int nBands{0};
145
    int nX{0};
146
    int nY{0};
147
    std::array<double, 6> gt{};
148
    std::unique_ptr<OGRSpatialReference, OGRSpatialReferenceReleaser> srs{
149
        nullptr};
150
    GDALDataType eDT{GDT_Unknown};
151
};
152

153
static std::optional<SourceProperties>
154
UpdateSourceProperties(SourceProperties &out, const std::string &dsn,
107✔
155
                       const GDALCalcOptions &options)
156
{
157
    SourceProperties source;
214✔
158
    bool srsMismatch = false;
107✔
159
    bool extentMismatch = false;
107✔
160
    bool dimensionMismatch = false;
107✔
161

162
    {
163
        std::unique_ptr<GDALDataset> ds(
164
            GDALDataset::Open(dsn.c_str(), GDAL_OF_RASTER));
107✔
165

166
        if (!ds)
107✔
167
        {
168
            CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
×
169
                     dsn.c_str());
170
            return std::nullopt;
×
171
        }
172

173
        source.nBands = ds->GetRasterCount();
107✔
174
        source.nX = ds->GetRasterXSize();
107✔
175
        source.nY = ds->GetRasterYSize();
107✔
176

177
        if (options.checkExtent)
107✔
178
        {
179
            ds->GetGeoTransform(source.gt.data());
103✔
180
        }
181

182
        if (options.checkSRS && out.srs)
107✔
183
        {
184
            const OGRSpatialReference *srs = ds->GetSpatialRef();
50✔
185
            srsMismatch = srs && !srs->IsSame(out.srs.get());
50✔
186
        }
187

188
        // Store the source data type if it is the same for all bands in the source
189
        for (int i = 0; i < source.nBands; ++i)
286✔
190
        {
191
            if (i == 0)
179✔
192
            {
193
                source.eDT = ds->GetRasterBand(1)->GetRasterDataType();
107✔
194
            }
195
            else if (source.eDT !=
144✔
196
                     ds->GetRasterBand(i + 1)->GetRasterDataType())
72✔
197
            {
NEW
198
                source.eDT = GDT_Unknown;
×
NEW
199
                break;
×
200
            }
201
        }
202
    }
203

204
    if (source.nX != out.nX || source.nY != out.nY)
107✔
205
    {
206
        dimensionMismatch = true;
3✔
207
    }
208

209
    if (source.gt[0] != out.gt[0] || source.gt[2] != out.gt[2] ||
214✔
210
        source.gt[3] != out.gt[3] || source.gt[4] != out.gt[4])
214✔
211
    {
212
        extentMismatch = true;
5✔
213
    }
214
    if (source.gt[1] != out.gt[1] || source.gt[5] != out.gt[5])
107✔
215
    {
216
        // Resolutions are different. Are the extents the same?
217
        double xmaxOut = out.gt[0] + out.nX * out.gt[1] + out.nY * out.gt[2];
8✔
218
        double yminOut = out.gt[3] + out.nX * out.gt[4] + out.nY * out.gt[5];
8✔
219

220
        double xmax =
221
            source.gt[0] + source.nX * source.gt[1] + source.nY * source.gt[2];
8✔
222
        double ymin =
223
            source.gt[3] + source.nX * source.gt[4] + source.nY * source.gt[5];
8✔
224

225
        // Max allowable extent misalignment, expressed as fraction of a pixel
226
        constexpr double EXTENT_RTOL = 1e-3;
8✔
227

228
        if (std::abs(xmax - xmaxOut) > EXTENT_RTOL * std::abs(source.gt[1]) ||
11✔
229
            std::abs(ymin - yminOut) > EXTENT_RTOL * std::abs(source.gt[5]))
3✔
230
        {
231
            extentMismatch = true;
5✔
232
        }
233
    }
234

235
    if (options.checkExtent && extentMismatch)
107✔
236
    {
237
        CPLError(CE_Failure, CPLE_AppDefined,
1✔
238
                 "Input extents are inconsistent.");
239
        return std::nullopt;
1✔
240
    }
241

242
    if (!options.checkExtent && dimensionMismatch)
106✔
243
    {
244
        CPLError(CE_Failure, CPLE_AppDefined,
1✔
245
                 "Inputs do not have the same dimensions.");
246
        return std::nullopt;
1✔
247
    }
248

249
    // Find a common resolution
250
    if (source.nX > out.nX)
105✔
251
    {
252
        auto dx = CPLGreatestCommonDivisor(out.gt[1], source.gt[1]);
1✔
253
        if (dx == 0)
1✔
254
        {
255
            CPLError(CE_Failure, CPLE_AppDefined,
×
256
                     "Failed to find common resolution for inputs.");
257
            return std::nullopt;
×
258
        }
259
        out.nX = static_cast<int>(
1✔
260
            std::round(static_cast<double>(out.nX) * out.gt[1] / dx));
1✔
261
        out.gt[1] = dx;
1✔
262
    }
263
    if (source.nY > out.nY)
105✔
264
    {
265
        auto dy = CPLGreatestCommonDivisor(out.gt[5], source.gt[5]);
1✔
266
        if (dy == 0)
1✔
267
        {
268
            CPLError(CE_Failure, CPLE_AppDefined,
×
269
                     "Failed to find common resolution for inputs.");
270
            return std::nullopt;
×
271
        }
272
        out.nY = static_cast<int>(
1✔
273
            std::round(static_cast<double>(out.nY) * out.gt[5] / dy));
1✔
274
        out.gt[5] = dy;
1✔
275
    }
276

277
    if (srsMismatch)
105✔
278
    {
279
        CPLError(CE_Failure, CPLE_AppDefined,
1✔
280
                 "Input spatial reference systems are inconsistent.");
281
        return std::nullopt;
1✔
282
    }
283

284
    return source;
104✔
285
}
286

287
static bool IsSumAllSources(const std::string &expression,
83✔
288
                            const std::map<std::string, std::string> &sources)
289
{
290
    bool ok = false;
83✔
291
    std::string modExpression;
166✔
292
    const char *pszSep = "+ ";
83✔
293
    if (cpl::starts_with(expression, "sum(") && expression.back() == ')')
83✔
294
    {
295
        modExpression = expression.substr(4, expression.size() - 5);
5✔
296
        pszSep = ", ";
5✔
297
    }
298
    else
299
    {
300
        modExpression = expression;
78✔
301
    }
302
    const CPLStringList aosTokens(
303
        CSLTokenizeString2(modExpression.c_str(), pszSep, 0));
83✔
304
    if (static_cast<size_t>(aosTokens.size()) == sources.size())
83✔
305
    {
306
        std::set<std::string> foundSources;
47✔
307
        for (int i = 0; i < aosTokens.size(); ++i)
106✔
308
        {
309
            if (cpl::contains(sources, aosTokens[i]) &&
86✔
310
                !cpl::contains(foundSources, aosTokens[i]))
86✔
311
            {
312
                foundSources.insert(aosTokens[i]);
27✔
313
            }
314
        }
315
        ok = foundSources.size() == sources.size();
47✔
316
    }
317
    return ok;
166✔
318
}
319

320
/** Create XML nodes for one or more derived bands resulting from the evaluation
321
 *  of a single expression
322
 *
323
 * @param root VRTDataset node to which the band nodes should be added
324
 * @param bandType the type of the band(s) to create
325
 * @param nXOut Number of columns in VRT dataset
326
 * @param nYOut Number of rows in VRT dataset
327
 * @param expression Expression for which band(s) should be added
328
 * @param dialect Expression dialect
329
 * @param flatten Generate a single band output raster per expression, even if
330
 *                input datasets are multiband.
331
 * @param pixelFunctionArguments Pixel function arguments.
332
 * @param sources Mapping of source names to DSNs
333
 * @param sourceProps Mapping of source names to properties
334
 * @param fakeSourceFilename If not empty, used instead of real input filenames.
335
 * @return true if the band(s) were added, false otherwise
336
 */
337
static bool
338
CreateDerivedBandXML(CPLXMLNode *root, int nXOut, int nYOut,
83✔
339
                     GDALDataType bandType, const std::string &expression,
340
                     const std::string &dialect, bool flatten,
341
                     const std::vector<std::string> &pixelFunctionArguments,
342
                     const std::map<std::string, std::string> &sources,
343
                     const std::map<std::string, SourceProperties> &sourceProps,
344
                     const std::string &fakeSourceFilename)
345
{
346
    const bool bIsSumAllSources = IsSumAllSources(expression, sources);
83✔
347

348
    int nOutBands = 1;  // By default, each expression produces a single output
83✔
349
                        // band. When processing the expression below, we may
350
                        // discover that the expression produces multiple bands,
351
                        // in which case this will be updated.
352

353
    for (int nOutBand = 1; nOutBand <= nOutBands; nOutBand++)
184✔
354
    {
355
        // Copy the expression for each output band, because we may modify it
356
        // when adding band indices (e.g., X -> X[1]) to the variables in the
357
        // expression.
358
        std::string bandExpression = expression;
104✔
359

360
        CPLXMLNode *band = CPLCreateXMLNode(root, CXT_Element, "VRTRasterBand");
104✔
361
        CPLAddXMLAttributeAndValue(band, "subClass", "VRTDerivedRasterBand");
104✔
362
        const char *pszDataType = nullptr;
104✔
363
        if (bandType != GDT_Unknown)
104✔
364
        {
365
            pszDataType = GDALGetDataTypeName(bandType);
7✔
366
        }
367
        CPLAddXMLAttributeAndValue(band, "dataType",
104✔
368
                                   pszDataType ? pszDataType : "Float64");
369

370
        for (const auto &[source_name, dsn] : sources)
234✔
371
        {
372
            auto it = sourceProps.find(source_name);
133✔
373
            CPLAssert(it != sourceProps.end());
133✔
374
            const auto &props = it->second;
133✔
375

376
            if (!flatten)
133✔
377
            {
378
                bool expressionUsesAllBands = false;
119✔
379
                if (dialect == "builtin")
119✔
380
                {
381
                    expressionUsesAllBands = true;
24✔
382
                }
383
                else
384
                {
385
                    const int nDefaultInBand = std::min(props.nBands, nOutBand);
95✔
386

387
                    CPLString expressionBandVariable;
95✔
388
                    expressionBandVariable.Printf("%s[%d]", source_name.c_str(),
389
                                                  nDefaultInBand);
95✔
390

391
                    bandExpression =
392
                        SetBandIndices(bandExpression, source_name,
190✔
393
                                       nDefaultInBand, expressionUsesAllBands);
95✔
394
                }
395

396
                if (expressionUsesAllBands)
119✔
397
                {
398
                    if (nOutBands <= 1)
91✔
399
                    {
400
                        nOutBands = props.nBands;
60✔
401
                    }
402
                    else if (props.nBands != 1 && props.nBands != nOutBands)
31✔
403
                    {
404
                        CPLError(CE_Failure, CPLE_AppDefined,
3✔
405
                                 "Expression cannot operate on all bands of "
406
                                 "rasters with incompatible numbers of bands "
407
                                 "(source %s has %d bands but expected to have "
408
                                 "1 or %d bands).",
409
                                 source_name.c_str(), props.nBands, nOutBands);
3✔
410
                        return false;
3✔
411
                    }
412
                }
413
            }
414
            else if (dialect != "builtin")
14✔
415
            {
416
                bandExpression = SetBandIndicesFlattenedExpression(
4✔
417
                    bandExpression, source_name, props.nBands);
4✔
418
            }
419

420
            // Create a <SimpleSource> for each input band that is used in
421
            // the expression.
422
            for (int nInBand = 1; nInBand <= props.nBands; nInBand++)
367✔
423
            {
424
                CPLString inBandVariable;
237✔
425
                if (dialect == "builtin")
237✔
426
                {
427
                    if (!flatten && props.nBands >= 2 && nInBand != nOutBand)
64✔
428
                        continue;
11✔
429
                }
430
                else
431
                {
432
                    inBandVariable.Printf("%s[%d]", source_name.c_str(),
433
                                          nInBand);
173✔
434
                    if (!flatten && bandExpression.find(inBandVariable) ==
173✔
435
                                        std::string::npos)
436
                    {
437
                        continue;
67✔
438
                    }
439
                }
440

441
                CPLXMLNode *source =
442
                    CPLCreateXMLNode(band, CXT_Element, "SimpleSource");
159✔
443
                if (!inBandVariable.empty())
159✔
444
                {
445
                    CPLAddXMLAttributeAndValue(source, "name",
106✔
446
                                               inBandVariable.c_str());
447
                }
448

449
                CPLXMLNode *sourceFilename =
450
                    CPLCreateXMLNode(source, CXT_Element, "SourceFilename");
159✔
451
                if (fakeSourceFilename.empty())
159✔
452
                {
453
                    CPLAddXMLAttributeAndValue(sourceFilename, "relativeToVRT",
122✔
454
                                               "0");
455
                    CPLCreateXMLNode(sourceFilename, CXT_Text, dsn.c_str());
122✔
456
                }
457
                else
458
                {
459
                    CPLCreateXMLNode(sourceFilename, CXT_Text,
37✔
460
                                     fakeSourceFilename.c_str());
461
                }
462

463
                CPLXMLNode *sourceBand =
464
                    CPLCreateXMLNode(source, CXT_Element, "SourceBand");
159✔
465
                CPLCreateXMLNode(sourceBand, CXT_Text,
159✔
466
                                 std::to_string(nInBand).c_str());
318✔
467

468
                if (fakeSourceFilename.empty())
159✔
469
                {
470
                    CPLXMLNode *srcRect =
471
                        CPLCreateXMLNode(source, CXT_Element, "SrcRect");
122✔
472
                    CPLAddXMLAttributeAndValue(srcRect, "xOff", "0");
122✔
473
                    CPLAddXMLAttributeAndValue(srcRect, "yOff", "0");
122✔
474
                    CPLAddXMLAttributeAndValue(
122✔
475
                        srcRect, "xSize", std::to_string(props.nX).c_str());
244✔
476
                    CPLAddXMLAttributeAndValue(
122✔
477
                        srcRect, "ySize", std::to_string(props.nY).c_str());
244✔
478

479
                    CPLXMLNode *dstRect =
480
                        CPLCreateXMLNode(source, CXT_Element, "DstRect");
122✔
481
                    CPLAddXMLAttributeAndValue(dstRect, "xOff", "0");
122✔
482
                    CPLAddXMLAttributeAndValue(dstRect, "yOff", "0");
122✔
483
                    CPLAddXMLAttributeAndValue(dstRect, "xSize",
122✔
484
                                               std::to_string(nXOut).c_str());
244✔
485
                    CPLAddXMLAttributeAndValue(dstRect, "ySize",
122✔
486
                                               std::to_string(nYOut).c_str());
244✔
487
                }
488
            }
489
        }
490

491
        CPLXMLNode *pixelFunctionType =
492
            CPLCreateXMLNode(band, CXT_Element, "PixelFunctionType");
101✔
493
        if (dialect == "builtin")
101✔
494
        {
495
            CPLCreateXMLNode(pixelFunctionType, CXT_Text, expression.c_str());
24✔
496
            if (!pixelFunctionArguments.empty())
24✔
497
            {
498
                CPLXMLNode *arguments = CPLCreateXMLNode(
4✔
499
                    band, CXT_Element, "PixelFunctionArguments");
500
                const CPLStringList args(pixelFunctionArguments);
8✔
501
                for (const auto &[key, value] : cpl::IterateNameValue(args))
8✔
502
                {
503
                    CPLAddXMLAttributeAndValue(arguments, key, value);
4✔
504
                }
505
            }
506
        }
507
        else if (bIsSumAllSources)
77✔
508
        {
509
            CPLCreateXMLNode(pixelFunctionType, CXT_Text, "sum");
21✔
510
        }
511
        else
512
        {
513
            CPLCreateXMLNode(pixelFunctionType, CXT_Text, "expression");
56✔
514
            CPLXMLNode *arguments =
515
                CPLCreateXMLNode(band, CXT_Element, "PixelFunctionArguments");
56✔
516

517
            // Add the expression as a last step, because we may modify the
518
            // expression as we iterate through the bands.
519
            CPLAddXMLAttributeAndValue(arguments, "expression",
56✔
520
                                       bandExpression.c_str());
521
            CPLAddXMLAttributeAndValue(arguments, "dialect", "muparser");
56✔
522
        }
523
    }
524

525
    return true;
80✔
526
}
527

528
static bool ParseSourceDescriptors(const std::vector<std::string> &inputs,
88✔
529
                                   std::map<std::string, std::string> &datasets,
530
                                   std::string &firstSourceName,
531
                                   bool requireSourceNames)
532
{
533
    for (size_t iInput = 0; iInput < inputs.size(); iInput++)
198✔
534
    {
535
        const std::string &input = inputs[iInput];
115✔
536
        std::string name;
115✔
537

538
        const auto pos = input.find('=');
115✔
539
        if (pos == std::string::npos)
115✔
540
        {
541
            if (requireSourceNames && inputs.size() > 1)
46✔
542
            {
543
                CPLError(CE_Failure, CPLE_AppDefined,
1✔
544
                         "Inputs must be named when more than one input is "
545
                         "provided.");
546
                return false;
1✔
547
            }
548
            name = "X";
45✔
549
            if (iInput > 0)
45✔
550
            {
551
                name += std::to_string(iInput);
2✔
552
            }
553
        }
554
        else
555
        {
556
            name = input.substr(0, pos);
69✔
557
        }
558

559
        // Check input name is legal
560
        for (size_t i = 0; i < name.size(); ++i)
249✔
561
        {
562
            const char c = name[i];
138✔
563
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
138✔
564
            {
565
                // ok
566
            }
567
            else if (c == '_' || (c >= '0' && c <= '9'))
20✔
568
            {
569
                if (i == 0)
19✔
570
                {
571
                    // Reserved constants in MuParser start with an underscore
572
                    CPLError(
2✔
573
                        CE_Failure, CPLE_AppDefined,
574
                        "Name '%s' is illegal because it starts with a '%c'",
575
                        name.c_str(), c);
576
                    return false;
2✔
577
                }
578
            }
579
            else
580
            {
581
                CPLError(CE_Failure, CPLE_AppDefined,
1✔
582
                         "Name '%s' is illegal because character '%c' is not "
583
                         "allowed",
584
                         name.c_str(), c);
585
                return false;
1✔
586
            }
587
        }
588

589
        std::string dsn =
590
            (pos == std::string::npos) ? input : input.substr(pos + 1);
111✔
591
        if (datasets.find(name) != datasets.end())
111✔
592
        {
593
            CPLError(CE_Failure, CPLE_AppDefined,
1✔
594
                     "An input with name '%s' has already been provided",
595
                     name.c_str());
596
            return false;
1✔
597
        }
598
        datasets[name] = std::move(dsn);
110✔
599

600
        if (iInput == 0)
110✔
601
        {
602
            firstSourceName = std::move(name);
84✔
603
        }
604
    }
605

606
    return true;
83✔
607
}
608

609
static bool ReadFileLists(std::vector<std::string> &inputs)
70✔
610
{
611
    for (std::size_t i = 0; i < inputs.size(); i++)
165✔
612
    {
613
        const auto &input = inputs[i];
95✔
614
        if (input[0] == '@')
95✔
615
        {
616
            auto f =
617
                VSIVirtualHandleUniquePtr(VSIFOpenL(input.c_str() + 1, "r"));
2✔
618
            if (!f)
2✔
619
            {
620
                CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
×
621
                         input.c_str() + 1);
×
622
                return false;
×
623
            }
624
            std::vector<std::string> sources;
2✔
625
            while (const char *filename = CPLReadLineL(f.get()))
6✔
626
            {
627
                sources.push_back(filename);
4✔
628
            }
4✔
629
            inputs.erase(inputs.begin() + static_cast<int>(i));
2✔
630
            inputs.insert(inputs.end(), sources.begin(), sources.end());
2✔
631
        }
632
    }
633

634
    return true;
70✔
635
}
636

637
/** Creates a VRT datasource with one or more derived raster bands containing
638
 *  results of an expression.
639
 *
640
 * To make this work with muparser (which does not support vector types), we
641
 * do a simple parsing of the expression internally, transforming it into
642
 * multiple expressions with explicit band indices. For example, for a two-band
643
 * raster "X", the expression "X + 3" will be transformed into "X[1] + 3" and
644
 * "X[2] + 3". The use of brackets is for readability only; as far as the
645
 * expression engine is concerned, the variables "X[1]" and "X[2]" have nothing
646
 * to do with each other.
647
 *
648
 * @param inputs A list of sources, expressed as NAME=DSN
649
 * @param expressions A list of expressions to be evaluated
650
 * @param dialect Expression dialect
651
 * @param flatten Generate a single band output raster per expression, even if
652
 *                input datasets are multiband.
653
 * @param pixelFunctionArguments Pixel function arguments.
654
 * @param options flags controlling which checks should be performed on the inputs
655
 * @param[out] maxSourceBands Maximum number of bands in source dataset(s)
656
 * @param fakeSourceFilename If not empty, used instead of real input filenames.
657
 *
658
 * @return a newly created VRTDataset, or nullptr on error
659
 */
660
static std::unique_ptr<GDALDataset> GDALCalcCreateVRTDerived(
88✔
661
    const std::vector<std::string> &inputs,
662
    const std::vector<std::string> &expressions, const std::string &dialect,
663
    bool flatten,
664
    const std::vector<std::vector<std::string>> &pixelFunctionArguments,
665
    const GDALCalcOptions &options, int &maxSourceBands,
666
    const std::string &fakeSourceFilename = std::string())
667
{
668
    if (inputs.empty())
88✔
669
    {
670
        return nullptr;
×
671
    }
672

673
    std::map<std::string, std::string> sources;
176✔
674
    std::string firstSource;
176✔
675
    bool requireSourceNames = dialect != "builtin";
88✔
676
    if (!ParseSourceDescriptors(inputs, sources, firstSource,
88✔
677
                                requireSourceNames))
678
    {
679
        return nullptr;
5✔
680
    }
681

682
    // Use the first source provided to determine properties of the output
683
    const char *firstDSN = sources[firstSource].c_str();
83✔
684

685
    maxSourceBands = 0;
83✔
686

687
    // Read properties from the first source
688
    SourceProperties out;
166✔
689
    {
690
        std::unique_ptr<GDALDataset> ds(
691
            GDALDataset::Open(firstDSN, GDAL_OF_RASTER));
83✔
692

693
        if (!ds)
83✔
694
        {
695
            CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
×
696
                     firstDSN);
697
            return nullptr;
×
698
        }
699

700
        out.nX = ds->GetRasterXSize();
83✔
701
        out.nY = ds->GetRasterYSize();
83✔
702
        out.nBands = 1;
83✔
703
        out.srs.reset(ds->GetSpatialRef() ? ds->GetSpatialRef()->Clone()
83✔
704
                                          : nullptr);
705
        ds->GetGeoTransform(out.gt.data());
83✔
706
    }
707

708
    CPLXMLTreeCloser root(CPLCreateXMLNode(nullptr, CXT_Element, "VRTDataset"));
166✔
709

710
    maxSourceBands = 0;
83✔
711

712
    // Collect properties of the different sources, and verity them for
713
    // consistency.
714
    std::map<std::string, SourceProperties> sourceProps;
166✔
715
    for (const auto &[source_name, dsn] : sources)
187✔
716
    {
717
        // TODO avoid opening the first source twice.
718
        auto props = UpdateSourceProperties(out, dsn, options);
107✔
719
        if (props.has_value())
107✔
720
        {
721
            maxSourceBands = std::max(maxSourceBands, props->nBands);
104✔
722
            sourceProps[source_name] = std::move(props.value());
104✔
723
        }
724
        else
725
        {
726
            return nullptr;
3✔
727
        }
728
    }
729

730
    size_t iExpr = 0;
80✔
731
    for (const auto &origExpression : expressions)
160✔
732
    {
733
        GDALDataType bandType = options.dstType;
83✔
734

735
        // If output band type has not been specified, set it equal to the
736
        // input band type for certain pixel functions, if the inputs have
737
        // a consistent band type.
738
        if (bandType == GDT_Unknown && dialect == "builtin" &&
125✔
739
            (origExpression == "min" || origExpression == "max" ||
60✔
740
             origExpression == "mode"))
18✔
741
        {
742
            for (const auto &[_, props] : sourceProps)
12✔
743
            {
744
                if (bandType == GDT_Unknown)
6✔
745
                {
746
                    bandType = props.eDT;
6✔
747
                }
NEW
748
                else if (props.eDT == GDT_Unknown || props.eDT != bandType)
×
749
                {
NEW
750
                    bandType = GDT_Unknown;
×
NEW
751
                    break;
×
752
                }
753
            }
754
        }
755

756
        if (!CreateDerivedBandXML(root.get(), out.nX, out.nY, bandType,
83✔
757
                                  origExpression, dialect, flatten,
758
                                  pixelFunctionArguments[iExpr], sources,
83✔
759
                                  sourceProps, fakeSourceFilename))
760
        {
761
            return nullptr;
3✔
762
        }
763
        ++iExpr;
80✔
764
    }
765

766
    //CPLDebug("VRT", "%s", CPLSerializeXMLTree(root.get()));
767

768
    auto ds = fakeSourceFilename.empty()
77✔
769
                  ? std::make_unique<VRTDataset>(out.nX, out.nY)
770
                  : std::make_unique<VRTDataset>(1, 1);
154✔
771
    if (ds->XMLInit(root.get(), "") != CE_None)
77✔
772
    {
773
        return nullptr;
×
774
    };
775
    ds->SetGeoTransform(out.gt.data());
77✔
776
    if (out.srs)
77✔
777
    {
778
        ds->SetSpatialRef(out.srs.get());
48✔
779
    }
780

781
    return ds;
77✔
782
}
783

784
/************************************************************************/
785
/*          GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm()          */
786
/************************************************************************/
787

788
GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm() noexcept
70✔
789
    : GDALAlgorithm(NAME, DESCRIPTION, HELP_URL)
70✔
790
{
791
    m_supportsStreamedOutput = true;
70✔
792

793
    AddProgressArg();
70✔
794

795
    AddArg(GDAL_ARG_NAME_INPUT, 'i', _("Input raster datasets"), &m_inputs)
140✔
796
        .SetPositional()
70✔
797
        .SetRequired()
70✔
798
        .SetMinCount(1)
70✔
799
        .SetAutoOpenDataset(false)
70✔
800
        .SetMetaVar("INPUTS");
70✔
801

802
    AddOutputFormatArg(&m_format, /* bStreamAllowed = */ true,
803
                       /* bGDALGAllowed = */ true);
70✔
804
    AddOutputDatasetArg(&m_outputDataset, GDAL_OF_RASTER);
70✔
805
    AddCreationOptionsArg(&m_creationOptions);
70✔
806
    AddOverwriteArg(&m_overwrite);
70✔
807
    AddOutputDataTypeArg(&m_type);
70✔
808

809
    AddArg("no-check-srs", 0,
810
           _("Do not check consistency of input spatial reference systems"),
811
           &m_NoCheckSRS);
70✔
812
    AddArg("no-check-extent", 0, _("Do not check consistency of input extents"),
813
           &m_NoCheckExtent);
70✔
814

815
    AddArg("calc", 0, _("Expression(s) to evaluate"), &m_expr)
140✔
816
        .SetRequired()
70✔
817
        .SetPackedValuesAllowed(false)
70✔
818
        .SetMinCount(1)
70✔
819
        .SetAutoCompleteFunction(
820
            [this](const std::string &currentValue)
4✔
821
            {
822
                std::vector<std::string> ret;
4✔
823
                if (m_dialect == "builtin")
2✔
824
                {
825
                    if (currentValue.find('(') == std::string::npos)
1✔
826
                        return VRTDerivedRasterBand::GetPixelFunctionNames();
1✔
827
                }
828
                return ret;
1✔
829
            });
70✔
830

831
    AddArg("dialect", 0, _("Expression dialect"), &m_dialect)
140✔
832
        .SetDefault(m_dialect)
70✔
833
        .SetChoices("muparser", "builtin");
70✔
834

835
    AddArg("flatten", 0,
836
           _("Generate a single band output raster per expression, even if "
837
             "input datasets are multiband"),
838
           &m_flatten);
70✔
839

840
    // This is a hidden option only used by test_gdalalg_raster_calc_expression_rewriting()
841
    // for now
842
    AddArg("no-check-expression", 0,
843
           _("Whether to skip expression validity checks for virtual format "
844
             "output"),
845
           &m_noCheckExpression)
140✔
846
        .SetHidden();
70✔
847

848
    AddValidationAction(
70✔
849
        [this]()
134✔
850
        {
851
            return m_noCheckExpression || !IsGDALGOutput() ||
75✔
852
                   RunImpl(nullptr, nullptr);
75✔
853
        });
854
}
70✔
855

856
/************************************************************************/
857
/*                GDALRasterCalcAlgorithm::RunImpl()                    */
858
/************************************************************************/
859

860
bool GDALRasterCalcAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
70✔
861
                                      void *pProgressData)
862
{
863
    CPLAssert(!m_outputDataset.GetDatasetRef());
70✔
864

865
    GDALCalcOptions options;
70✔
866
    options.checkExtent = !m_NoCheckExtent;
70✔
867
    options.checkSRS = !m_NoCheckSRS;
70✔
868
    if (!m_type.empty())
70✔
869
    {
870
        options.dstType = GDALGetDataTypeByName(m_type.c_str());
1✔
871
    }
872

873
    if (!ReadFileLists(m_inputs))
70✔
874
    {
875
        return false;
×
876
    }
877

878
    std::vector<std::vector<std::string>> pixelFunctionArgs;
140✔
879
    if (m_dialect == "builtin")
70✔
880
    {
881
        for (std::string &expr : m_expr)
23✔
882
        {
883
            const CPLStringList aosTokens(
884
                CSLTokenizeString2(expr.c_str(), "()",
885
                                   CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
12✔
886
            const char *pszFunction = aosTokens[0];
12✔
887
            const auto *pair =
888
                VRTDerivedRasterBand::GetPixelFunction(pszFunction);
12✔
889
            if (!pair)
12✔
890
            {
NEW
891
                ReportError(CE_Failure, CPLE_NotSupported,
×
892
                            "'%s' is a unknown builtin function", pszFunction);
NEW
893
                return false;
×
894
            }
895
            if (aosTokens.size() == 2)
12✔
896
            {
897
                std::vector<std::string> validArguments;
2✔
898
                AddOptionsSuggestions(pair->second.c_str(), 0, std::string(),
2✔
899
                                      validArguments);
900
                for (std::string &s : validArguments)
6✔
901
                {
902
                    if (!s.empty() && s.back() == '=')
4✔
903
                        s.pop_back();
4✔
904
                }
905

906
                const CPLStringList aosTokensArgs(CSLTokenizeString2(
907
                    aosTokens[1], ",",
908
                    CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
2✔
909
                for (const auto &[key, value] :
4✔
910
                     cpl::IterateNameValue(aosTokensArgs))
6✔
911
                {
912
                    if (std::find(validArguments.begin(), validArguments.end(),
2✔
913
                                  key) == validArguments.end())
2✔
914
                    {
NEW
915
                        if (validArguments.empty())
×
916
                        {
NEW
917
                            ReportError(
×
918
                                CE_Failure, CPLE_IllegalArg,
919
                                "'%s' is a unrecognized argument for builtin "
920
                                "function '%s'. It does not accept any "
921
                                "argument",
922
                                key, pszFunction);
923
                        }
924
                        else
925
                        {
NEW
926
                            std::string validArgumentsStr;
×
NEW
927
                            for (const std::string &s : validArguments)
×
928
                            {
NEW
929
                                if (!validArgumentsStr.empty())
×
NEW
930
                                    validArgumentsStr += ", ";
×
NEW
931
                                validArgumentsStr += '\'';
×
NEW
932
                                validArgumentsStr += s;
×
NEW
933
                                validArgumentsStr += '\'';
×
934
                            }
NEW
935
                            ReportError(
×
936
                                CE_Failure, CPLE_IllegalArg,
937
                                "'%s' is a unrecognized argument for builtin "
938
                                "function '%s'. Only %s %s supported",
939
                                key, pszFunction,
NEW
940
                                validArguments.size() == 1 ? "is" : "are",
×
941
                                validArgumentsStr.c_str());
942
                        }
NEW
943
                        return false;
×
944
                    }
945
                    CPL_IGNORE_RET_VAL(value);
2✔
946
                }
947
                pixelFunctionArgs.emplace_back(aosTokensArgs);
2✔
948
            }
949
            else
950
            {
951
                pixelFunctionArgs.push_back(std::vector<std::string>());
10✔
952
            }
953
            expr = pszFunction;
12✔
954
        }
955
    }
956
    else
957
    {
958
        pixelFunctionArgs.resize(m_expr.size());
59✔
959
    }
960

961
    int maxSourceBands = 0;
70✔
962
    auto vrt =
963
        GDALCalcCreateVRTDerived(m_inputs, m_expr, m_dialect, m_flatten,
70✔
964
                                 pixelFunctionArgs, options, maxSourceBands);
140✔
965
    if (vrt == nullptr)
70✔
966
    {
967
        return false;
11✔
968
    }
969

970
    if (!m_noCheckExpression)
59✔
971
    {
972
        const bool bIsVRT =
973
            m_format == "VRT" ||
116✔
974
            (m_format.empty() &&
45✔
975
             EQUAL(
50✔
976
                 CPLGetExtensionSafe(m_outputDataset.GetName().c_str()).c_str(),
977
                 "VRT"));
46✔
978
        const bool bIsGDALG =
979
            m_format == "GDALG" ||
116✔
980
            (m_format.empty() &&
45✔
981
             cpl::ends_with(m_outputDataset.GetName(), ".gdalg.json"));
25✔
982
        if (m_format == "stream" || bIsVRT || bIsGDALG)
46✔
983
        {
984
            // Try reading a single pixel to check formulas are valid.
985
            std::vector<GByte> dummyData(vrt->GetRasterCount());
18✔
986

987
            auto poGTIFFDrv = GetGDALDriverManager()->GetDriverByName("GTiff");
18✔
988
            std::string osTmpFilename;
18✔
989
            if (poGTIFFDrv)
18✔
990
            {
991
                std::string osFilename =
992
                    VSIMemGenerateHiddenFilename("tmp.tif");
36✔
993
                auto poDS = std::unique_ptr<GDALDataset>(
994
                    poGTIFFDrv->Create(osFilename.c_str(), 1, 1, maxSourceBands,
995
                                       GDT_Byte, nullptr));
36✔
996
                if (poDS)
18✔
997
                    osTmpFilename = std::move(osFilename);
18✔
998
            }
999
            if (!osTmpFilename.empty())
18✔
1000
            {
1001
                auto fakeVRT = GDALCalcCreateVRTDerived(
1002
                    m_inputs, m_expr, m_dialect, m_flatten, pixelFunctionArgs,
18✔
1003
                    options, maxSourceBands, osTmpFilename);
18✔
1004
                if (fakeVRT &&
36✔
1005
                    fakeVRT->RasterIO(GF_Read, 0, 0, 1, 1, dummyData.data(), 1,
18✔
1006
                                      1, GDT_Byte, vrt->GetRasterCount(),
1007
                                      nullptr, 0, 0, 0, nullptr) != CE_None)
18✔
1008
                {
1009
                    return false;
5✔
1010
                }
1011
            }
1012
            if (bIsGDALG)
13✔
1013
            {
1014
                return true;
1✔
1015
            }
1016
        }
1017
    }
1018

1019
    if (m_format == "stream")
53✔
1020
    {
1021
        m_outputDataset.Set(std::move(vrt));
11✔
1022
        return true;
11✔
1023
    }
1024

1025
    CPLStringList translateArgs;
84✔
1026
    if (!m_format.empty())
42✔
1027
    {
1028
        translateArgs.AddString("-of");
7✔
1029
        translateArgs.AddString(m_format.c_str());
7✔
1030
    }
1031
    for (const auto &co : m_creationOptions)
43✔
1032
    {
1033
        translateArgs.AddString("-co");
1✔
1034
        translateArgs.AddString(co.c_str());
1✔
1035
    }
1036

1037
    GDALTranslateOptions *translateOptions =
1038
        GDALTranslateOptionsNew(translateArgs.List(), nullptr);
42✔
1039
    GDALTranslateOptionsSetProgress(translateOptions, pfnProgress,
42✔
1040
                                    pProgressData);
1041

1042
    auto poOutDS =
1043
        std::unique_ptr<GDALDataset>(GDALDataset::FromHandle(GDALTranslate(
1044
            m_outputDataset.GetName().c_str(), GDALDataset::ToHandle(vrt.get()),
42✔
1045
            translateOptions, nullptr)));
84✔
1046
    GDALTranslateOptionsFree(translateOptions);
42✔
1047

1048
    const bool bOK = poOutDS != nullptr;
42✔
1049
    m_outputDataset.Set(std::move(poOutDS));
42✔
1050

1051
    return bOK;
42✔
1052
}
1053

1054
//! @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