Line data Source code
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 <optional>
26 :
27 : //! @cond Doxygen_Suppress
28 :
29 : #ifndef _
30 : #define _(x) (x)
31 : #endif
32 :
33 : struct GDALCalcOptions
34 : {
35 : GDALDataType dstType{GDT_Unknown};
36 : bool checkSRS{true};
37 : bool checkExtent{true};
38 : };
39 :
40 234 : static bool MatchIsCompleteVariableNameWithNoIndex(const std::string &str,
41 : size_t from, size_t to)
42 : {
43 234 : if (to < str.size())
44 : {
45 : // If the character after the end of the match is:
46 : // * alphanumeric or _ : we've matched only part of a variable name
47 : // * [ : we've matched a variable that already has an index
48 : // * ( : we've matched a function name
49 300 : if (std::isalnum(str[to]) || str[to] == '_' || str[to] == '[' ||
50 104 : str[to] == '(')
51 : {
52 93 : return false;
53 : }
54 : }
55 141 : if (from > 0)
56 : {
57 : // If the character before the start of the match is alphanumeric or _,
58 : // we've matched only part of a variable name.
59 89 : if (std::isalnum(str[from - 1]) || str[from - 1] == '_')
60 : {
61 3 : return false;
62 : }
63 : }
64 :
65 138 : return true;
66 : }
67 :
68 : /**
69 : * Add a band subscript to all instances of a specified variable that
70 : * do not already have such a subscript. For example, "X" would be
71 : * replaced with "X[3]" but "X[1]" would be left untouched.
72 : */
73 138 : static std::string SetBandIndices(const std::string &origExpression,
74 : const std::string &variable, int band,
75 : bool &expressionChanged)
76 : {
77 138 : std::string expression = origExpression;
78 138 : expressionChanged = false;
79 :
80 138 : std::string::size_type seekPos = 0;
81 138 : auto pos = expression.find(variable, seekPos);
82 336 : while (pos != std::string::npos)
83 : {
84 198 : auto end = pos + variable.size();
85 :
86 198 : if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end))
87 : {
88 : // No index specified for variable
89 204 : expression = expression.substr(0, pos + variable.size()) + '[' +
90 306 : std::to_string(band) + ']' + expression.substr(end);
91 102 : expressionChanged = true;
92 : }
93 :
94 198 : seekPos = end;
95 198 : pos = expression.find(variable, seekPos);
96 : }
97 :
98 138 : return expression;
99 : }
100 :
101 72 : static bool PosIsAggregateFunctionArgument(const std::string &expression,
102 : size_t pos)
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 72 : while (pos != 0)
107 : {
108 64 : const char c = expression[pos];
109 64 : if (c == '(')
110 : {
111 24 : pos--;
112 24 : break;
113 : }
114 40 : if (!(isspace(c) || isalnum(c) || c == ',' || c == '.' || c == '[' ||
115 : c == ']' || c == '_'))
116 : {
117 4 : return false;
118 : }
119 36 : pos--;
120 : }
121 :
122 : // Now what we've found the (, the preceding characters should be an
123 : // aggregate function name
124 32 : if (pos < 2)
125 : {
126 8 : return false;
127 : }
128 :
129 24 : if (STARTS_WITH_CI(expression.c_str() + (pos - 2), "avg") ||
130 20 : STARTS_WITH_CI(expression.c_str() + (pos - 2), "sum") ||
131 52 : STARTS_WITH_CI(expression.c_str() + (pos - 2), "min") ||
132 8 : STARTS_WITH_CI(expression.c_str() + (pos - 2), "max"))
133 : {
134 20 : return true;
135 : }
136 :
137 4 : return false;
138 : }
139 :
140 : /**
141 : * Replace X by X[1],X[2],...X[n]
142 : */
143 : static std::string
144 32 : SetBandIndicesFlattenedExpression(const std::string &origExpression,
145 : const std::string &variable, int nBands)
146 : {
147 32 : std::string expression = origExpression;
148 :
149 32 : std::string::size_type seekPos = 0;
150 32 : auto pos = expression.find(variable, seekPos);
151 68 : while (pos != std::string::npos)
152 : {
153 36 : auto end = pos + variable.size();
154 :
155 72 : if (MatchIsCompleteVariableNameWithNoIndex(expression, pos, end) &&
156 36 : PosIsAggregateFunctionArgument(expression, pos))
157 : {
158 20 : std::string newExpr = expression.substr(0, pos);
159 68 : for (int i = 1; i <= nBands; ++i)
160 : {
161 48 : if (i > 1)
162 28 : newExpr += ',';
163 48 : newExpr += variable;
164 48 : newExpr += '[';
165 48 : newExpr += std::to_string(i);
166 48 : newExpr += ']';
167 : }
168 20 : const size_t oldExprSize = expression.size();
169 20 : newExpr += expression.substr(end);
170 20 : expression = std::move(newExpr);
171 20 : end += expression.size() - oldExprSize;
172 : }
173 :
174 36 : seekPos = end;
175 36 : pos = expression.find(variable, seekPos);
176 : }
177 :
178 32 : return expression;
179 : }
180 :
181 : struct SourceProperties
182 : {
183 : int nBands{0};
184 : int nX{0};
185 : int nY{0};
186 : GDALGeoTransform gt{};
187 : std::unique_ptr<OGRSpatialReference, OGRSpatialReferenceReleaser> srs{
188 : nullptr};
189 : std::vector<std::optional<double>> noData{};
190 : GDALDataType eDT{GDT_Unknown};
191 : };
192 :
193 : static std::optional<SourceProperties>
194 146 : UpdateSourceProperties(SourceProperties &out, const std::string &dsn,
195 : const GDALCalcOptions &options)
196 : {
197 292 : SourceProperties source;
198 146 : bool srsMismatch = false;
199 146 : bool extentMismatch = false;
200 146 : bool dimensionMismatch = false;
201 :
202 : {
203 : std::unique_ptr<GDALDataset> ds(
204 146 : GDALDataset::Open(dsn.c_str(), GDAL_OF_RASTER));
205 :
206 146 : if (!ds)
207 : {
208 0 : CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
209 : dsn.c_str());
210 0 : return std::nullopt;
211 : }
212 :
213 146 : source.nBands = ds->GetRasterCount();
214 146 : source.nX = ds->GetRasterXSize();
215 146 : source.nY = ds->GetRasterYSize();
216 146 : source.noData.resize(source.nBands);
217 :
218 146 : if (options.checkExtent)
219 : {
220 142 : ds->GetGeoTransform(source.gt);
221 : }
222 :
223 146 : if (options.checkSRS && out.srs)
224 : {
225 54 : const OGRSpatialReference *srs = ds->GetSpatialRef();
226 54 : srsMismatch = srs && !srs->IsSame(out.srs.get());
227 : }
228 :
229 : // Store the source data type if it is the same for all bands in the source
230 146 : bool bandsHaveSameType = true;
231 392 : for (int i = 1; i <= source.nBands; ++i)
232 : {
233 246 : GDALRasterBand *band = ds->GetRasterBand(i);
234 :
235 246 : if (i == 1)
236 : {
237 146 : source.eDT = band->GetRasterDataType();
238 : }
239 200 : else if (bandsHaveSameType &&
240 100 : source.eDT != band->GetRasterDataType())
241 : {
242 0 : source.eDT = GDT_Unknown;
243 0 : bandsHaveSameType = false;
244 : }
245 :
246 : int success;
247 246 : double noData = band->GetNoDataValue(&success);
248 246 : if (success)
249 : {
250 15 : source.noData[i - 1] = noData;
251 : }
252 : }
253 : }
254 :
255 146 : if (source.nX != out.nX || source.nY != out.nY)
256 : {
257 3 : dimensionMismatch = true;
258 : }
259 :
260 292 : if (source.gt[0] != out.gt[0] || source.gt[2] != out.gt[2] ||
261 292 : source.gt[3] != out.gt[3] || source.gt[4] != out.gt[4])
262 : {
263 5 : extentMismatch = true;
264 : }
265 146 : if (source.gt[1] != out.gt[1] || source.gt[5] != out.gt[5])
266 : {
267 : // Resolutions are different. Are the extents the same?
268 8 : double xmaxOut = out.gt[0] + out.nX * out.gt[1] + out.nY * out.gt[2];
269 8 : double yminOut = out.gt[3] + out.nX * out.gt[4] + out.nY * out.gt[5];
270 :
271 : double xmax =
272 8 : source.gt[0] + source.nX * source.gt[1] + source.nY * source.gt[2];
273 : double ymin =
274 8 : source.gt[3] + source.nX * source.gt[4] + source.nY * source.gt[5];
275 :
276 : // Max allowable extent misalignment, expressed as fraction of a pixel
277 8 : constexpr double EXTENT_RTOL = 1e-3;
278 :
279 13 : if (std::abs(xmax - xmaxOut) > EXTENT_RTOL * std::abs(source.gt[1]) ||
280 5 : std::abs(ymin - yminOut) > EXTENT_RTOL * std::abs(source.gt[5]))
281 : {
282 5 : extentMismatch = true;
283 : }
284 : }
285 :
286 146 : if (options.checkExtent && extentMismatch)
287 : {
288 1 : CPLError(CE_Failure, CPLE_AppDefined,
289 : "Input extents are inconsistent.");
290 1 : return std::nullopt;
291 : }
292 :
293 145 : if (!options.checkExtent && dimensionMismatch)
294 : {
295 1 : CPLError(CE_Failure, CPLE_AppDefined,
296 : "Inputs do not have the same dimensions.");
297 1 : return std::nullopt;
298 : }
299 :
300 : // Find a common resolution
301 144 : if (source.nX > out.nX)
302 : {
303 1 : auto dx = CPLGreatestCommonDivisor(out.gt[1], source.gt[1]);
304 1 : if (dx == 0)
305 : {
306 0 : CPLError(CE_Failure, CPLE_AppDefined,
307 : "Failed to find common resolution for inputs.");
308 0 : return std::nullopt;
309 : }
310 1 : out.nX = static_cast<int>(
311 1 : std::round(static_cast<double>(out.nX) * out.gt[1] / dx));
312 1 : out.gt[1] = dx;
313 : }
314 144 : if (source.nY > out.nY)
315 : {
316 1 : auto dy = CPLGreatestCommonDivisor(out.gt[5], source.gt[5]);
317 1 : if (dy == 0)
318 : {
319 0 : CPLError(CE_Failure, CPLE_AppDefined,
320 : "Failed to find common resolution for inputs.");
321 0 : return std::nullopt;
322 : }
323 1 : out.nY = static_cast<int>(
324 1 : std::round(static_cast<double>(out.nY) * out.gt[5] / dy));
325 1 : out.gt[5] = dy;
326 : }
327 :
328 144 : if (srsMismatch)
329 : {
330 1 : CPLError(CE_Failure, CPLE_AppDefined,
331 : "Input spatial reference systems are inconsistent.");
332 1 : return std::nullopt;
333 : }
334 :
335 143 : return source;
336 : }
337 :
338 : /** Create XML nodes for one or more derived bands resulting from the evaluation
339 : * of a single expression
340 : *
341 : * @param root VRTDataset node to which the band nodes should be added
342 : * @param bandType the type of the band(s) to create
343 : * @param nXOut Number of columns in VRT dataset
344 : * @param nYOut Number of rows in VRT dataset
345 : * @param expression Expression for which band(s) should be added
346 : * @param dialect Expression dialect
347 : * @param flatten Generate a single band output raster per expression, even if
348 : * input datasets are multiband.
349 : * @param noDataText nodata value to use for the created band, or "none", or ""
350 : * @param pixelFunctionArguments Pixel function arguments.
351 : * @param sources Mapping of source names to DSNs
352 : * @param sourceProps Mapping of source names to properties
353 : * @param fakeSourceFilename If not empty, used instead of real input filenames.
354 : * @return true if the band(s) were added, false otherwise
355 : */
356 : static bool
357 110 : CreateDerivedBandXML(CPLXMLNode *root, int nXOut, int nYOut,
358 : GDALDataType bandType, const std::string &expression,
359 : const std::string &dialect, bool flatten,
360 : const std::string &noDataText,
361 : const std::vector<std::string> &pixelFunctionArguments,
362 : const std::map<std::string, std::string> &sources,
363 : const std::map<std::string, SourceProperties> &sourceProps,
364 : const std::string &fakeSourceFilename)
365 : {
366 110 : int nOutBands = 1; // By default, each expression produces a single output
367 : // band. When processing the expression below, we may
368 : // discover that the expression produces multiple bands,
369 : // in which case this will be updated.
370 :
371 243 : for (int nOutBand = 1; nOutBand <= nOutBands; nOutBand++)
372 : {
373 : // Copy the expression for each output band, because we may modify it
374 : // when adding band indices (e.g., X -> X[1]) to the variables in the
375 : // expression.
376 137 : std::string bandExpression = expression;
377 :
378 137 : CPLXMLNode *band = CPLCreateXMLNode(root, CXT_Element, "VRTRasterBand");
379 137 : CPLAddXMLAttributeAndValue(band, "subClass", "VRTDerivedRasterBand");
380 137 : if (bandType == GDT_Unknown)
381 : {
382 98 : bandType = GDT_Float64;
383 : }
384 137 : CPLAddXMLAttributeAndValue(band, "dataType",
385 : GDALGetDataTypeName(bandType));
386 :
387 137 : std::optional<double> dstNoData;
388 137 : bool autoSelectNoDataValue = false;
389 137 : if (noDataText.empty())
390 : {
391 132 : autoSelectNoDataValue = true;
392 : }
393 5 : else if (noDataText != "none")
394 : {
395 : char *end;
396 5 : dstNoData = CPLStrtod(noDataText.c_str(), &end);
397 5 : if (end != noDataText.c_str() + noDataText.size())
398 : {
399 0 : CPLError(CE_Failure, CPLE_AppDefined,
400 : "Invalid NoData value: %s", noDataText.c_str());
401 0 : return false;
402 : }
403 : }
404 :
405 313 : for (const auto &[source_name, dsn] : sources)
406 : {
407 180 : auto it = sourceProps.find(source_name);
408 180 : CPLAssert(it != sourceProps.end());
409 180 : const auto &props = it->second;
410 :
411 180 : bool expressionAppliedPerBand = false;
412 180 : if (dialect == "builtin")
413 : {
414 42 : expressionAppliedPerBand = !flatten;
415 : }
416 : else
417 : {
418 138 : const int nDefaultInBand = std::min(props.nBands, nOutBand);
419 :
420 138 : if (flatten)
421 : {
422 32 : bandExpression = SetBandIndicesFlattenedExpression(
423 32 : bandExpression, source_name, props.nBands);
424 : }
425 :
426 : bandExpression =
427 276 : SetBandIndices(bandExpression, source_name, nDefaultInBand,
428 138 : expressionAppliedPerBand);
429 : }
430 :
431 180 : if (expressionAppliedPerBand)
432 : {
433 126 : if (nOutBands <= 1)
434 : {
435 85 : nOutBands = props.nBands;
436 : }
437 41 : else if (props.nBands != 1 && props.nBands != nOutBands)
438 : {
439 3 : CPLError(CE_Failure, CPLE_AppDefined,
440 : "Expression cannot operate on all bands of "
441 : "rasters with incompatible numbers of bands "
442 : "(source %s has %d bands but expected to have "
443 : "1 or %d bands).",
444 3 : source_name.c_str(), props.nBands, nOutBands);
445 4 : return false;
446 : }
447 : }
448 :
449 : // Create a source for each input band that is used in
450 : // the expression.
451 497 : for (int nInBand = 1; nInBand <= props.nBands; nInBand++)
452 : {
453 320 : CPLString inBandVariable;
454 320 : if (dialect == "builtin")
455 : {
456 72 : if (!flatten && props.nBands >= 2 && nInBand != nOutBand)
457 11 : continue;
458 : }
459 : else
460 : {
461 : inBandVariable.Printf("%s[%d]", source_name.c_str(),
462 248 : nInBand);
463 248 : if (bandExpression.find(inBandVariable) ==
464 : std::string::npos)
465 : {
466 79 : continue;
467 : }
468 : }
469 :
470 : const std::optional<double> &srcNoData =
471 230 : props.noData[nInBand - 1];
472 :
473 230 : CPLXMLNode *source = CPLCreateXMLNode(
474 : band, CXT_Element,
475 230 : srcNoData.has_value() ? "ComplexSource" : "SimpleSource");
476 230 : if (!inBandVariable.empty())
477 : {
478 169 : CPLAddXMLAttributeAndValue(source, "name",
479 : inBandVariable.c_str());
480 : }
481 :
482 : CPLXMLNode *sourceFilename =
483 230 : CPLCreateXMLNode(source, CXT_Element, "SourceFilename");
484 230 : if (fakeSourceFilename.empty())
485 : {
486 158 : CPLAddXMLAttributeAndValue(sourceFilename, "relativeToVRT",
487 : "0");
488 158 : CPLCreateXMLNode(sourceFilename, CXT_Text, dsn.c_str());
489 : }
490 : else
491 : {
492 72 : CPLCreateXMLNode(sourceFilename, CXT_Text,
493 : fakeSourceFilename.c_str());
494 : }
495 :
496 : CPLXMLNode *sourceBand =
497 230 : CPLCreateXMLNode(source, CXT_Element, "SourceBand");
498 230 : CPLCreateXMLNode(sourceBand, CXT_Text,
499 460 : std::to_string(nInBand).c_str());
500 :
501 230 : if (srcNoData.has_value())
502 : {
503 : CPLXMLNode *srcNoDataNode =
504 15 : CPLCreateXMLNode(source, CXT_Element, "NODATA");
505 : std::string srcNoDataText =
506 30 : CPLSPrintf("%.17g", srcNoData.value());
507 15 : CPLCreateXMLNode(srcNoDataNode, CXT_Text,
508 : srcNoDataText.c_str());
509 :
510 15 : if (autoSelectNoDataValue && !dstNoData.has_value())
511 : {
512 6 : dstNoData = srcNoData;
513 : }
514 : }
515 :
516 230 : if (fakeSourceFilename.empty())
517 : {
518 : CPLXMLNode *srcRect =
519 158 : CPLCreateXMLNode(source, CXT_Element, "SrcRect");
520 158 : CPLAddXMLAttributeAndValue(srcRect, "xOff", "0");
521 158 : CPLAddXMLAttributeAndValue(srcRect, "yOff", "0");
522 158 : CPLAddXMLAttributeAndValue(
523 316 : srcRect, "xSize", std::to_string(props.nX).c_str());
524 158 : CPLAddXMLAttributeAndValue(
525 316 : srcRect, "ySize", std::to_string(props.nY).c_str());
526 :
527 : CPLXMLNode *dstRect =
528 158 : CPLCreateXMLNode(source, CXT_Element, "DstRect");
529 158 : CPLAddXMLAttributeAndValue(dstRect, "xOff", "0");
530 158 : CPLAddXMLAttributeAndValue(dstRect, "yOff", "0");
531 158 : CPLAddXMLAttributeAndValue(dstRect, "xSize",
532 316 : std::to_string(nXOut).c_str());
533 158 : CPLAddXMLAttributeAndValue(dstRect, "ySize",
534 316 : std::to_string(nYOut).c_str());
535 : }
536 : }
537 :
538 177 : if (dstNoData.has_value())
539 : {
540 15 : if (!GDALIsValueExactAs(dstNoData.value(), bandType))
541 : {
542 1 : CPLError(
543 : CE_Failure, CPLE_AppDefined,
544 : "Band output type %s cannot represent NoData value %g",
545 1 : GDALGetDataTypeName(bandType), dstNoData.value());
546 1 : return false;
547 : }
548 :
549 : CPLXMLNode *noDataNode =
550 14 : CPLCreateXMLNode(band, CXT_Element, "NoDataValue");
551 : CPLString dstNoDataText =
552 28 : CPLSPrintf("%.17g", dstNoData.value());
553 14 : CPLCreateXMLNode(noDataNode, CXT_Text, dstNoDataText.c_str());
554 : }
555 : }
556 :
557 : CPLXMLNode *pixelFunctionType =
558 133 : CPLCreateXMLNode(band, CXT_Element, "PixelFunctionType");
559 : CPLXMLNode *arguments =
560 133 : CPLCreateXMLNode(band, CXT_Element, "PixelFunctionArguments");
561 :
562 133 : if (dialect == "builtin")
563 : {
564 28 : CPLCreateXMLNode(pixelFunctionType, CXT_Text, expression.c_str());
565 : }
566 : else
567 : {
568 105 : CPLCreateXMLNode(pixelFunctionType, CXT_Text, "expression");
569 105 : CPLAddXMLAttributeAndValue(arguments, "dialect", "muparser");
570 : // Add the expression as a last step, because we may modify the
571 : // expression as we iterate through the bands.
572 105 : CPLAddXMLAttributeAndValue(arguments, "expression",
573 : bandExpression.c_str());
574 : }
575 :
576 133 : if (!pixelFunctionArguments.empty())
577 : {
578 16 : const CPLStringList args(pixelFunctionArguments);
579 16 : for (const auto &[key, value] : cpl::IterateNameValue(args))
580 : {
581 8 : CPLAddXMLAttributeAndValue(arguments, key, value);
582 : }
583 : }
584 : }
585 :
586 106 : return true;
587 : }
588 :
589 115 : static bool ParseSourceDescriptors(const std::vector<std::string> &inputs,
590 : std::map<std::string, std::string> &datasets,
591 : std::string &firstSourceName,
592 : bool requireSourceNames)
593 : {
594 264 : for (size_t iInput = 0; iInput < inputs.size(); iInput++)
595 : {
596 154 : const std::string &input = inputs[iInput];
597 154 : std::string name;
598 :
599 154 : const auto pos = input.find('=');
600 154 : if (pos == std::string::npos)
601 : {
602 59 : if (requireSourceNames && inputs.size() > 1)
603 : {
604 1 : CPLError(CE_Failure, CPLE_AppDefined,
605 : "Inputs must be named when more than one input is "
606 : "provided.");
607 1 : return false;
608 : }
609 58 : name = "X";
610 58 : if (iInput > 0)
611 : {
612 2 : name += std::to_string(iInput);
613 : }
614 : }
615 : else
616 : {
617 95 : name = input.substr(0, pos);
618 : }
619 :
620 : // Check input name is legal
621 327 : for (size_t i = 0; i < name.size(); ++i)
622 : {
623 177 : const char c = name[i];
624 177 : if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
625 : {
626 : // ok
627 : }
628 20 : else if (c == '_' || (c >= '0' && c <= '9'))
629 : {
630 19 : if (i == 0)
631 : {
632 : // Reserved constants in MuParser start with an underscore
633 2 : CPLError(
634 : CE_Failure, CPLE_AppDefined,
635 : "Name '%s' is illegal because it starts with a '%c'",
636 : name.c_str(), c);
637 2 : return false;
638 : }
639 : }
640 : else
641 : {
642 1 : CPLError(CE_Failure, CPLE_AppDefined,
643 : "Name '%s' is illegal because character '%c' is not "
644 : "allowed",
645 : name.c_str(), c);
646 1 : return false;
647 : }
648 : }
649 :
650 : std::string dsn =
651 150 : (pos == std::string::npos) ? input : input.substr(pos + 1);
652 150 : if (datasets.find(name) != datasets.end())
653 : {
654 1 : CPLError(CE_Failure, CPLE_AppDefined,
655 : "An input with name '%s' has already been provided",
656 : name.c_str());
657 1 : return false;
658 : }
659 149 : datasets[name] = std::move(dsn);
660 :
661 149 : if (iInput == 0)
662 : {
663 111 : firstSourceName = std::move(name);
664 : }
665 : }
666 :
667 110 : return true;
668 : }
669 :
670 84 : static bool ReadFileLists(const std::vector<GDALArgDatasetValue> &inputDS,
671 : std::vector<std::string> &inputFilenames)
672 : {
673 197 : for (const auto &dsVal : inputDS)
674 : {
675 113 : const auto &input = dsVal.GetName();
676 113 : if (!input.empty() && input[0] == '@')
677 : {
678 : auto f =
679 2 : VSIVirtualHandleUniquePtr(VSIFOpenL(input.c_str() + 1, "r"));
680 2 : if (!f)
681 : {
682 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s",
683 0 : input.c_str() + 1);
684 0 : return false;
685 : }
686 6 : while (const char *filename = CPLReadLineL(f.get()))
687 : {
688 4 : inputFilenames.push_back(filename);
689 4 : }
690 : }
691 : else
692 : {
693 111 : inputFilenames.push_back(input);
694 : }
695 : }
696 :
697 84 : return true;
698 : }
699 :
700 : /** Creates a VRT datasource with one or more derived raster bands containing
701 : * results of an expression.
702 : *
703 : * To make this work with muparser (which does not support vector types), we
704 : * do a simple parsing of the expression internally, transforming it into
705 : * multiple expressions with explicit band indices. For example, for a two-band
706 : * raster "X", the expression "X + 3" will be transformed into "X[1] + 3" and
707 : * "X[2] + 3". The use of brackets is for readability only; as far as the
708 : * expression engine is concerned, the variables "X[1]" and "X[2]" have nothing
709 : * to do with each other.
710 : *
711 : * @param inputs A list of sources, expressed as NAME=DSN
712 : * @param expressions A list of expressions to be evaluated
713 : * @param dialect Expression dialect
714 : * @param flatten Generate a single band output raster per expression, even if
715 : * input datasets are multiband.
716 : * @param noData NoData values to use for output bands, or "none", or ""
717 : * @param pixelFunctionArguments Pixel function arguments.
718 : * @param options flags controlling which checks should be performed on the inputs
719 : * @param[out] maxSourceBands Maximum number of bands in source dataset(s)
720 : * @param fakeSourceFilename If not empty, used instead of real input filenames.
721 : *
722 : * @return a newly created VRTDataset, or nullptr on error
723 : */
724 115 : static std::unique_ptr<GDALDataset> GDALCalcCreateVRTDerived(
725 : const std::vector<std::string> &inputs,
726 : const std::vector<std::string> &expressions, const std::string &dialect,
727 : bool flatten, const std::string &noData,
728 : const std::vector<std::vector<std::string>> &pixelFunctionArguments,
729 : const GDALCalcOptions &options, int &maxSourceBands,
730 : const std::string &fakeSourceFilename = std::string())
731 : {
732 115 : if (inputs.empty())
733 : {
734 0 : return nullptr;
735 : }
736 :
737 230 : std::map<std::string, std::string> sources;
738 230 : std::string firstSource;
739 115 : bool requireSourceNames = dialect != "builtin";
740 115 : if (!ParseSourceDescriptors(inputs, sources, firstSource,
741 : requireSourceNames))
742 : {
743 5 : return nullptr;
744 : }
745 :
746 : // Use the first source provided to determine properties of the output
747 110 : const char *firstDSN = sources[firstSource].c_str();
748 :
749 110 : maxSourceBands = 0;
750 :
751 : // Read properties from the first source
752 220 : SourceProperties out;
753 : {
754 : std::unique_ptr<GDALDataset> ds(
755 110 : GDALDataset::Open(firstDSN, GDAL_OF_RASTER));
756 :
757 110 : if (!ds)
758 : {
759 0 : CPLError(CE_Failure, CPLE_AppDefined, "Failed to open %s",
760 : firstDSN);
761 0 : return nullptr;
762 : }
763 :
764 110 : out.nX = ds->GetRasterXSize();
765 110 : out.nY = ds->GetRasterYSize();
766 110 : out.nBands = 1;
767 110 : out.srs.reset(ds->GetSpatialRef() ? ds->GetSpatialRef()->Clone()
768 : : nullptr);
769 110 : ds->GetGeoTransform(out.gt);
770 : }
771 :
772 220 : CPLXMLTreeCloser root(CPLCreateXMLNode(nullptr, CXT_Element, "VRTDataset"));
773 :
774 110 : maxSourceBands = 0;
775 :
776 : // Collect properties of the different sources, and verity them for
777 : // consistency.
778 220 : std::map<std::string, SourceProperties> sourceProps;
779 253 : for (const auto &[source_name, dsn] : sources)
780 : {
781 : // TODO avoid opening the first source twice.
782 146 : auto props = UpdateSourceProperties(out, dsn, options);
783 146 : if (props.has_value())
784 : {
785 143 : maxSourceBands = std::max(maxSourceBands, props->nBands);
786 143 : sourceProps[source_name] = std::move(props.value());
787 : }
788 : else
789 : {
790 3 : return nullptr;
791 : }
792 : }
793 :
794 107 : size_t iExpr = 0;
795 213 : for (const auto &origExpression : expressions)
796 : {
797 110 : GDALDataType bandType = options.dstType;
798 :
799 : // If output band type has not been specified, set it equal to the
800 : // input band type for certain pixel functions, if the inputs have
801 : // a consistent band type.
802 160 : if (bandType == GDT_Unknown && dialect == "builtin" &&
803 72 : (origExpression == "min" || origExpression == "max" ||
804 22 : origExpression == "mode"))
805 : {
806 12 : for (const auto &[_, props] : sourceProps)
807 : {
808 6 : if (bandType == GDT_Unknown)
809 : {
810 6 : bandType = props.eDT;
811 : }
812 0 : else if (props.eDT == GDT_Unknown || props.eDT != bandType)
813 : {
814 0 : bandType = GDT_Unknown;
815 0 : break;
816 : }
817 : }
818 : }
819 :
820 110 : if (!CreateDerivedBandXML(root.get(), out.nX, out.nY, bandType,
821 : origExpression, dialect, flatten, noData,
822 110 : pixelFunctionArguments[iExpr], sources,
823 : sourceProps, fakeSourceFilename))
824 : {
825 4 : return nullptr;
826 : }
827 106 : ++iExpr;
828 : }
829 :
830 : //CPLDebug("VRT", "%s", CPLSerializeXMLTree(root.get()));
831 :
832 103 : auto ds = fakeSourceFilename.empty()
833 : ? std::make_unique<VRTDataset>(out.nX, out.nY)
834 206 : : std::make_unique<VRTDataset>(1, 1);
835 103 : if (ds->XMLInit(root.get(), "") != CE_None)
836 : {
837 0 : return nullptr;
838 : };
839 103 : ds->SetGeoTransform(out.gt);
840 103 : if (out.srs)
841 : {
842 52 : ds->SetSpatialRef(out.srs.get());
843 : }
844 :
845 103 : return ds;
846 : }
847 :
848 : /************************************************************************/
849 : /* GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm() */
850 : /************************************************************************/
851 :
852 106 : GDALRasterCalcAlgorithm::GDALRasterCalcAlgorithm(bool standaloneStep) noexcept
853 : : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
854 318 : ConstructorOptions()
855 106 : .SetStandaloneStep(standaloneStep)
856 106 : .SetAddDefaultArguments(false)
857 106 : .SetAutoOpenInputDatasets(false)
858 212 : .SetInputDatasetMetaVar("INPUTS")
859 318 : .SetInputDatasetMaxCount(INT_MAX))
860 : {
861 106 : AddRasterInputArgs(false, false);
862 106 : if (standaloneStep)
863 : {
864 85 : AddProgressArg();
865 85 : AddRasterOutputArgs(false);
866 : }
867 :
868 106 : AddOutputDataTypeArg(&m_type);
869 :
870 : AddArg("no-check-srs", 0,
871 : _("Do not check consistency of input spatial reference systems"),
872 106 : &m_noCheckSRS);
873 : AddArg("no-check-extent", 0, _("Do not check consistency of input extents"),
874 106 : &m_noCheckExtent);
875 :
876 : AddArg("propagate-nodata", 0,
877 : _("Whether to set pixels to the output NoData value if any of the "
878 : "input pixels is NoData"),
879 106 : &m_propagateNoData);
880 :
881 212 : AddArg("calc", 0, _("Expression(s) to evaluate"), &m_expr)
882 106 : .SetRequired()
883 106 : .SetPackedValuesAllowed(false)
884 106 : .SetMinCount(1)
885 : .SetAutoCompleteFunction(
886 4 : [this](const std::string ¤tValue)
887 : {
888 4 : std::vector<std::string> ret;
889 2 : if (m_dialect == "builtin")
890 : {
891 1 : if (currentValue.find('(') == std::string::npos)
892 1 : return VRTDerivedRasterBand::GetPixelFunctionNames();
893 : }
894 1 : return ret;
895 106 : });
896 :
897 212 : AddArg("dialect", 0, _("Expression dialect"), &m_dialect)
898 106 : .SetDefault(m_dialect)
899 106 : .SetChoices("muparser", "builtin");
900 :
901 : AddArg("flatten", 0,
902 : _("Generate a single band output raster per expression, even if "
903 : "input datasets are multiband"),
904 106 : &m_flatten);
905 :
906 106 : AddNodataArg(&m_nodata, true);
907 :
908 : // This is a hidden option only used by test_gdalalg_raster_calc_expression_rewriting()
909 : // for now
910 : AddArg("no-check-expression", 0,
911 : _("Whether to skip expression validity checks for virtual format "
912 : "output"),
913 212 : &m_noCheckExpression)
914 106 : .SetHidden();
915 :
916 106 : AddValidationAction(
917 166 : [this]()
918 : {
919 88 : GDALPipelineStepRunContext ctxt;
920 88 : return m_noCheckExpression || !IsGDALGOutput() || RunStep(ctxt);
921 : });
922 106 : }
923 :
924 : /************************************************************************/
925 : /* GDALRasterCalcAlgorithm::RunImpl() */
926 : /************************************************************************/
927 :
928 79 : bool GDALRasterCalcAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
929 : void *pProgressData)
930 : {
931 79 : GDALPipelineStepRunContext stepCtxt;
932 79 : stepCtxt.m_pfnProgress = pfnProgress;
933 79 : stepCtxt.m_pProgressData = pProgressData;
934 79 : return RunPreStepPipelineValidations() && RunStep(stepCtxt);
935 : }
936 :
937 : /************************************************************************/
938 : /* GDALRasterCalcAlgorithm::RunStep() */
939 : /************************************************************************/
940 :
941 84 : bool GDALRasterCalcAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt)
942 : {
943 84 : CPLAssert(!m_outputDataset.GetDatasetRef());
944 :
945 84 : GDALCalcOptions options;
946 84 : options.checkExtent = !m_noCheckExtent;
947 84 : options.checkSRS = !m_noCheckSRS;
948 84 : if (!m_type.empty())
949 : {
950 4 : options.dstType = GDALGetDataTypeByName(m_type.c_str());
951 : }
952 :
953 168 : std::vector<std::string> inputFilenames;
954 84 : if (!ReadFileLists(m_inputDataset, inputFilenames))
955 : {
956 0 : return false;
957 : }
958 :
959 168 : std::vector<std::vector<std::string>> pixelFunctionArgs;
960 84 : if (m_dialect == "builtin")
961 : {
962 27 : for (std::string &expr : m_expr)
963 : {
964 : const CPLStringList aosTokens(
965 : CSLTokenizeString2(expr.c_str(), "()",
966 14 : CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
967 14 : const char *pszFunction = aosTokens[0];
968 : const auto *pair =
969 14 : VRTDerivedRasterBand::GetPixelFunction(pszFunction);
970 14 : if (!pair)
971 : {
972 0 : ReportError(CE_Failure, CPLE_NotSupported,
973 : "'%s' is a unknown builtin function", pszFunction);
974 0 : return false;
975 : }
976 14 : if (aosTokens.size() == 2)
977 : {
978 2 : std::vector<std::string> validArguments;
979 2 : AddOptionsSuggestions(pair->second.c_str(), 0, std::string(),
980 : validArguments);
981 6 : for (std::string &s : validArguments)
982 : {
983 4 : if (!s.empty() && s.back() == '=')
984 4 : s.pop_back();
985 : }
986 :
987 : const CPLStringList aosTokensArgs(CSLTokenizeString2(
988 : aosTokens[1], ",",
989 2 : CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
990 4 : for (const auto &[key, value] :
991 6 : cpl::IterateNameValue(aosTokensArgs))
992 : {
993 2 : if (std::find(validArguments.begin(), validArguments.end(),
994 2 : key) == validArguments.end())
995 : {
996 0 : if (validArguments.empty())
997 : {
998 0 : ReportError(
999 : CE_Failure, CPLE_IllegalArg,
1000 : "'%s' is a unrecognized argument for builtin "
1001 : "function '%s'. It does not accept any "
1002 : "argument",
1003 : key, pszFunction);
1004 : }
1005 : else
1006 : {
1007 0 : std::string validArgumentsStr;
1008 0 : for (const std::string &s : validArguments)
1009 : {
1010 0 : if (!validArgumentsStr.empty())
1011 0 : validArgumentsStr += ", ";
1012 0 : validArgumentsStr += '\'';
1013 0 : validArgumentsStr += s;
1014 0 : validArgumentsStr += '\'';
1015 : }
1016 0 : ReportError(
1017 : CE_Failure, CPLE_IllegalArg,
1018 : "'%s' is a unrecognized argument for builtin "
1019 : "function '%s'. Only %s %s supported",
1020 : key, pszFunction,
1021 0 : validArguments.size() == 1 ? "is" : "are",
1022 : validArgumentsStr.c_str());
1023 : }
1024 0 : return false;
1025 : }
1026 2 : CPL_IGNORE_RET_VAL(value);
1027 : }
1028 2 : pixelFunctionArgs.emplace_back(aosTokensArgs);
1029 : }
1030 : else
1031 : {
1032 12 : pixelFunctionArgs.push_back(std::vector<std::string>());
1033 : }
1034 14 : expr = pszFunction;
1035 : }
1036 : }
1037 : else
1038 : {
1039 71 : pixelFunctionArgs.resize(m_expr.size());
1040 : }
1041 :
1042 84 : if (m_propagateNoData)
1043 : {
1044 2 : if (m_nodata == "none")
1045 : {
1046 0 : ReportError(CE_Failure, CPLE_AppDefined,
1047 : "Output NoData value must be specified to use "
1048 : "--propagate-nodata");
1049 0 : return false;
1050 : }
1051 4 : for (auto &args : pixelFunctionArgs)
1052 : {
1053 2 : args.push_back("propagateNoData=1");
1054 : }
1055 : }
1056 :
1057 84 : int maxSourceBands = 0;
1058 84 : auto vrt = GDALCalcCreateVRTDerived(inputFilenames, m_expr, m_dialect,
1059 84 : m_flatten, m_nodata, pixelFunctionArgs,
1060 168 : options, maxSourceBands);
1061 84 : if (vrt == nullptr)
1062 : {
1063 12 : return false;
1064 : }
1065 :
1066 72 : if (!m_noCheckExpression)
1067 : {
1068 : const bool bIsVRT =
1069 144 : m_format == "VRT" ||
1070 58 : (m_format.empty() &&
1071 54 : EQUAL(
1072 : CPLGetExtensionSafe(m_outputDataset.GetName().c_str()).c_str(),
1073 59 : "VRT"));
1074 : const bool bIsGDALG =
1075 144 : m_format == "GDALG" ||
1076 58 : (m_format.empty() &&
1077 27 : cpl::ends_with(m_outputDataset.GetName(), ".gdalg.json"));
1078 59 : if (!m_standaloneStep || m_format == "stream" || bIsVRT || bIsGDALG)
1079 : {
1080 : // Try reading a single pixel to check formulas are valid.
1081 31 : std::vector<GByte> dummyData(vrt->GetRasterCount());
1082 :
1083 31 : auto poGTIFFDrv = GetGDALDriverManager()->GetDriverByName("GTiff");
1084 31 : std::string osTmpFilename;
1085 31 : if (poGTIFFDrv)
1086 : {
1087 : std::string osFilename =
1088 62 : VSIMemGenerateHiddenFilename("tmp.tif");
1089 : auto poDS = std::unique_ptr<GDALDataset>(
1090 : poGTIFFDrv->Create(osFilename.c_str(), 1, 1, maxSourceBands,
1091 62 : GDT_Byte, nullptr));
1092 31 : if (poDS)
1093 31 : osTmpFilename = std::move(osFilename);
1094 : }
1095 31 : if (!osTmpFilename.empty())
1096 : {
1097 : auto fakeVRT = GDALCalcCreateVRTDerived(
1098 31 : inputFilenames, m_expr, m_dialect, m_flatten, m_nodata,
1099 31 : pixelFunctionArgs, options, maxSourceBands, osTmpFilename);
1100 62 : if (fakeVRT &&
1101 31 : fakeVRT->RasterIO(GF_Read, 0, 0, 1, 1, dummyData.data(), 1,
1102 : 1, GDT_Byte, vrt->GetRasterCount(),
1103 31 : nullptr, 0, 0, 0, nullptr) != CE_None)
1104 : {
1105 5 : return false;
1106 : }
1107 : }
1108 26 : if (bIsGDALG)
1109 : {
1110 1 : return true;
1111 : }
1112 : }
1113 : }
1114 :
1115 66 : if (m_format == "stream" || !m_standaloneStep)
1116 : {
1117 24 : m_outputDataset.Set(std::move(vrt));
1118 24 : return true;
1119 : }
1120 :
1121 84 : CPLStringList translateArgs;
1122 42 : if (!m_format.empty())
1123 : {
1124 7 : translateArgs.AddString("-of");
1125 7 : translateArgs.AddString(m_format.c_str());
1126 : }
1127 43 : for (const auto &co : m_creationOptions)
1128 : {
1129 1 : translateArgs.AddString("-co");
1130 1 : translateArgs.AddString(co.c_str());
1131 : }
1132 :
1133 : GDALTranslateOptions *translateOptions =
1134 42 : GDALTranslateOptionsNew(translateArgs.List(), nullptr);
1135 42 : GDALTranslateOptionsSetProgress(translateOptions, ctxt.m_pfnProgress,
1136 : ctxt.m_pProgressData);
1137 :
1138 : auto poOutDS =
1139 : std::unique_ptr<GDALDataset>(GDALDataset::FromHandle(GDALTranslate(
1140 42 : m_outputDataset.GetName().c_str(), GDALDataset::ToHandle(vrt.get()),
1141 84 : translateOptions, nullptr)));
1142 42 : GDALTranslateOptionsFree(translateOptions);
1143 :
1144 42 : const bool bOK = poOutDS != nullptr;
1145 42 : m_outputDataset.Set(std::move(poOutDS));
1146 :
1147 42 : return bOK;
1148 : }
1149 :
1150 : GDALRasterCalcAlgorithmStandalone::~GDALRasterCalcAlgorithmStandalone() =
1151 : default;
1152 :
1153 : //! @endcond
|