Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL
4 : * Purpose: gdal "raster pipeline" subcommand
5 : * Author: Even Rouault <even dot rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2024, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "gdalalg_raster_pipeline.h"
14 : #include "gdalalg_materialize.h"
15 : #include "gdalalg_raster_read.h"
16 : #include "gdalalg_raster_calc.h"
17 : #include "gdalalg_raster_aspect.h"
18 : #include "gdalalg_raster_clip.h"
19 : #include "gdalalg_raster_color_map.h"
20 : #include "gdalalg_raster_color_merge.h"
21 : #include "gdalalg_raster_compare.h"
22 : #include "gdalalg_raster_edit.h"
23 : #include "gdalalg_raster_fill_nodata.h"
24 : #include "gdalalg_raster_hillshade.h"
25 : #include "gdalalg_raster_info.h"
26 : #include "gdalalg_raster_mosaic.h"
27 : #include "gdalalg_raster_nodata_to_alpha.h"
28 : #include "gdalalg_raster_overview.h"
29 : #include "gdalalg_raster_pansharpen.h"
30 : #include "gdalalg_raster_proximity.h"
31 : #include "gdalalg_raster_reclassify.h"
32 : #include "gdalalg_raster_reproject.h"
33 : #include "gdalalg_raster_resize.h"
34 : #include "gdalalg_raster_rgb_to_palette.h"
35 : #include "gdalalg_raster_roughness.h"
36 : #include "gdalalg_raster_scale.h"
37 : #include "gdalalg_raster_select.h"
38 : #include "gdalalg_raster_set_type.h"
39 : #include "gdalalg_raster_sieve.h"
40 : #include "gdalalg_raster_slope.h"
41 : #include "gdalalg_raster_stack.h"
42 : #include "gdalalg_raster_tile.h"
43 : #include "gdalalg_raster_write.h"
44 : #include "gdalalg_raster_tpi.h"
45 : #include "gdalalg_raster_tri.h"
46 : #include "gdalalg_raster_unscale.h"
47 : #include "gdalalg_raster_viewshed.h"
48 : #include "gdalalg_tee.h"
49 :
50 : #include "cpl_conv.h"
51 : #include "cpl_progress.h"
52 : #include "cpl_string.h"
53 : #include "cpl_vsi.h"
54 : #include "gdal_priv.h"
55 : #include "gdal_utils.h"
56 :
57 : #include <algorithm>
58 : #include <array>
59 : #include <cassert>
60 :
61 : //! @cond Doxygen_Suppress
62 :
63 : #ifndef _
64 : #define _(x) (x)
65 : #endif
66 :
67 : GDALRasterAlgorithmStepRegistry::~GDALRasterAlgorithmStepRegistry() = default;
68 :
69 : /************************************************************************/
70 : /* GDALRasterPipelineStepAlgorithm::GDALRasterPipelineStepAlgorithm() */
71 : /************************************************************************/
72 :
73 1876 : GDALRasterPipelineStepAlgorithm::GDALRasterPipelineStepAlgorithm(
74 : const std::string &name, const std::string &description,
75 1876 : const std::string &helpURL, bool standaloneStep)
76 : : GDALRasterPipelineStepAlgorithm(
77 : name, description, helpURL,
78 1876 : ConstructorOptions().SetStandaloneStep(standaloneStep))
79 : {
80 1876 : }
81 :
82 : /************************************************************************/
83 : /* GDALRasterPipelineStepAlgorithm::GDALRasterPipelineStepAlgorithm() */
84 : /************************************************************************/
85 :
86 4240 : GDALRasterPipelineStepAlgorithm::GDALRasterPipelineStepAlgorithm(
87 : const std::string &name, const std::string &description,
88 4240 : const std::string &helpURL, const ConstructorOptions &options)
89 4240 : : GDALPipelineStepAlgorithm(name, description, helpURL, options)
90 : {
91 4240 : if (m_standaloneStep)
92 : {
93 1062 : m_supportsStreamedOutput = true;
94 :
95 1062 : if (m_constructorOptions.addDefaultArguments)
96 : {
97 336 : AddRasterInputArgs(false, false);
98 336 : AddProgressArg();
99 336 : AddRasterOutputArgs(false);
100 : }
101 : }
102 3178 : else if (m_constructorOptions.addDefaultArguments)
103 : {
104 1540 : AddRasterHiddenInputDatasetArg();
105 : }
106 4240 : }
107 :
108 : GDALRasterPipelineStepAlgorithm::~GDALRasterPipelineStepAlgorithm() = default;
109 :
110 : /************************************************************************/
111 : /* GDALRasterPipelineStepAlgorithm::SetOutputVRTCompatible() */
112 : /************************************************************************/
113 :
114 374 : void GDALRasterPipelineStepAlgorithm::SetOutputVRTCompatible(bool b)
115 : {
116 374 : m_outputVRTCompatible = b;
117 374 : if (m_outputFormatArg)
118 : {
119 92 : m_outputFormatArg->AddMetadataItem(GAAMDI_VRT_COMPATIBLE,
120 184 : {b ? "true" : "false"});
121 : }
122 374 : }
123 :
124 : /************************************************************************/
125 : /* GDALRasterPipelineAlgorithm::GDALRasterPipelineAlgorithm() */
126 : /************************************************************************/
127 :
128 128 : GDALRasterPipelineAlgorithm::GDALRasterPipelineAlgorithm(
129 128 : bool openForMixedRasterVector)
130 : : GDALAbstractPipelineAlgorithm(NAME, DESCRIPTION, HELP_URL,
131 0 : ConstructorOptions()
132 128 : .SetAddDefaultArguments(false)
133 256 : .SetInputDatasetMaxCount(INT_MAX))
134 : {
135 128 : m_supportsStreamedOutput = true;
136 :
137 128 : AddRasterInputArgs(openForMixedRasterVector, /* hiddenForCLI = */ true);
138 128 : AddProgressArg();
139 256 : AddArg("pipeline", 0, _("Pipeline string"), &m_pipeline)
140 128 : .SetHiddenForCLI()
141 128 : .SetPositional();
142 128 : AddRasterOutputArgs(/* hiddenForCLI = */ true);
143 :
144 128 : AddOutputStringArg(&m_output).SetHiddenForCLI();
145 128 : AddStdoutArg(&m_stdout);
146 :
147 128 : RegisterAlgorithms(m_stepRegistry, false);
148 128 : }
149 :
150 : /************************************************************************/
151 : /* GDALRasterPipelineAlgorithm::RegisterAlgorithms() */
152 : /************************************************************************/
153 :
154 : /* static */
155 308 : void GDALRasterPipelineAlgorithm::RegisterAlgorithms(
156 : GDALRasterAlgorithmStepRegistry ®istry, bool forMixedPipeline)
157 : {
158 308 : GDALAlgorithmRegistry::AlgInfo algInfo;
159 :
160 : const auto addSuffixIfNeeded =
161 2772 : [forMixedPipeline](const char *name) -> std::string
162 : {
163 4392 : return forMixedPipeline ? std::string(name).append(RASTER_SUFFIX)
164 7164 : : std::string(name);
165 308 : };
166 :
167 308 : registry.Register<GDALRasterReadAlgorithm>(
168 616 : addSuffixIfNeeded(GDALRasterReadAlgorithm::NAME));
169 :
170 308 : registry.Register<GDALRasterCalcAlgorithm>();
171 :
172 308 : registry.Register<GDALRasterWriteAlgorithm>(
173 616 : addSuffixIfNeeded(GDALRasterWriteAlgorithm::NAME));
174 :
175 308 : registry.Register<GDALRasterInfoAlgorithm>(
176 616 : addSuffixIfNeeded(GDALRasterInfoAlgorithm::NAME));
177 :
178 308 : registry.Register<GDALRasterAspectAlgorithm>();
179 :
180 308 : registry.Register<GDALRasterClipAlgorithm>(
181 616 : addSuffixIfNeeded(GDALRasterClipAlgorithm::NAME));
182 :
183 308 : registry.Register<GDALRasterColorMapAlgorithm>();
184 308 : registry.Register<GDALRasterColorMergeAlgorithm>();
185 308 : registry.Register<GDALRasterCompareAlgorithm>();
186 :
187 308 : registry.Register<GDALRasterEditAlgorithm>(
188 616 : addSuffixIfNeeded(GDALRasterEditAlgorithm::NAME));
189 :
190 308 : registry.Register<GDALRasterNoDataToAlphaAlgorithm>();
191 308 : registry.Register<GDALRasterFillNodataAlgorithm>();
192 308 : registry.Register<GDALRasterHillshadeAlgorithm>();
193 :
194 308 : registry.Register<GDALMaterializeRasterAlgorithm>(
195 616 : addSuffixIfNeeded(GDALMaterializeRasterAlgorithm::NAME));
196 :
197 308 : registry.Register<GDALRasterMosaicAlgorithm>();
198 308 : registry.Register<GDALRasterOverviewAlgorithm>();
199 308 : registry.Register<GDALRasterPansharpenAlgorithm>();
200 308 : registry.Register<GDALRasterProximityAlgorithm>();
201 308 : registry.Register<GDALRasterReclassifyAlgorithm>();
202 :
203 308 : registry.Register<GDALRasterReprojectAlgorithm>(
204 616 : addSuffixIfNeeded(GDALRasterReprojectAlgorithm::NAME));
205 :
206 308 : registry.Register<GDALRasterResizeAlgorithm>();
207 308 : registry.Register<GDALRasterRGBToPaletteAlgorithm>();
208 308 : registry.Register<GDALRasterRoughnessAlgorithm>();
209 308 : registry.Register<GDALRasterScaleAlgorithm>();
210 :
211 308 : registry.Register<GDALRasterSelectAlgorithm>(
212 616 : addSuffixIfNeeded(GDALRasterSelectAlgorithm::NAME));
213 :
214 308 : registry.Register<GDALRasterSetTypeAlgorithm>();
215 308 : registry.Register<GDALRasterSieveAlgorithm>();
216 308 : registry.Register<GDALRasterSlopeAlgorithm>();
217 308 : registry.Register<GDALRasterStackAlgorithm>();
218 308 : registry.Register<GDALRasterTileAlgorithm>();
219 308 : registry.Register<GDALRasterTPIAlgorithm>();
220 308 : registry.Register<GDALRasterTRIAlgorithm>();
221 308 : registry.Register<GDALRasterUnscaleAlgorithm>();
222 308 : registry.Register<GDALRasterViewshedAlgorithm>();
223 308 : registry.Register<GDALTeeRasterAlgorithm>(
224 616 : addSuffixIfNeeded(GDALTeeRasterAlgorithm::NAME));
225 308 : }
226 :
227 : /************************************************************************/
228 : /* GDALRasterPipelineAlgorithm::GetUsageForCLI() */
229 : /************************************************************************/
230 :
231 8 : std::string GDALRasterPipelineAlgorithm::GetUsageForCLI(
232 : bool shortUsage, const UsageOptions &usageOptions) const
233 : {
234 8 : UsageOptions stepUsageOptions;
235 8 : stepUsageOptions.isPipelineStep = true;
236 :
237 8 : if (!m_helpDocCategory.empty() && m_helpDocCategory != "main")
238 : {
239 4 : auto alg = GetStepAlg(m_helpDocCategory);
240 2 : if (alg)
241 : {
242 2 : alg->SetCallPath({m_helpDocCategory});
243 1 : alg->GetArg("help-doc")->Set(true);
244 1 : return alg->GetUsageForCLI(shortUsage, stepUsageOptions);
245 : }
246 : else
247 : {
248 1 : fprintf(stderr, "ERROR: unknown pipeline step '%s'\n",
249 : m_helpDocCategory.c_str());
250 : return CPLSPrintf("ERROR: unknown pipeline step '%s'\n",
251 1 : m_helpDocCategory.c_str());
252 : }
253 : }
254 :
255 6 : UsageOptions usageOptionsMain(usageOptions);
256 6 : usageOptionsMain.isPipelineMain = true;
257 : std::string ret =
258 12 : GDALAlgorithm::GetUsageForCLI(shortUsage, usageOptionsMain);
259 6 : if (shortUsage)
260 2 : return ret;
261 :
262 : ret += "\n<PIPELINE> is of the form: read|mosaic|stack [READ-OPTIONS] "
263 : "( ! <STEP-NAME> [STEP-OPTIONS] )* ! info|compare|tile|write "
264 4 : "[WRITE-OPTIONS]\n";
265 :
266 4 : if (m_helpDocCategory == "main")
267 : {
268 1 : return ret;
269 : }
270 :
271 3 : ret += '\n';
272 3 : ret += "Example: 'gdal raster pipeline --progress ! read in.tif ! \\\n";
273 3 : ret += " reproject --dst-crs=EPSG:32632 ! ";
274 3 : ret += "write out.tif --overwrite'\n";
275 3 : ret += '\n';
276 3 : ret += "Potential steps are:\n";
277 :
278 108 : for (const std::string &name : m_stepRegistry.GetNames())
279 : {
280 210 : auto alg = GetStepAlg(name);
281 105 : auto [options, maxOptLen] = alg->GetArgNamesForCLI();
282 105 : stepUsageOptions.maxOptLen =
283 105 : std::max(stepUsageOptions.maxOptLen, maxOptLen);
284 : }
285 :
286 : {
287 3 : const auto name = GDALRasterReadAlgorithm::NAME;
288 3 : ret += '\n';
289 6 : auto alg = GetStepAlg(name);
290 6 : alg->SetCallPath({name});
291 3 : ret += alg->GetUsageForCLI(shortUsage, stepUsageOptions);
292 : }
293 108 : for (const std::string &name : m_stepRegistry.GetNames())
294 : {
295 210 : auto alg = GetStepAlg(name);
296 105 : assert(alg);
297 117 : if (alg->CanBeFirstStep() && !alg->CanBeMiddleStep() &&
298 117 : !alg->IsHidden() && name != GDALRasterReadAlgorithm::NAME)
299 : {
300 9 : ret += '\n';
301 18 : alg->SetCallPath({name});
302 9 : ret += alg->GetUsageForCLI(shortUsage, stepUsageOptions);
303 : }
304 : }
305 108 : for (const std::string &name : m_stepRegistry.GetNames())
306 : {
307 210 : auto alg = GetStepAlg(name);
308 105 : assert(alg);
309 105 : if (alg->CanBeMiddleStep() && !alg->IsHidden())
310 : {
311 81 : ret += '\n';
312 162 : alg->SetCallPath({name});
313 81 : ret += alg->GetUsageForCLI(shortUsage, stepUsageOptions);
314 : }
315 : }
316 108 : for (const std::string &name : m_stepRegistry.GetNames())
317 : {
318 210 : auto alg = GetStepAlg(name);
319 105 : assert(alg);
320 120 : if (alg->CanBeLastStep() && !alg->CanBeMiddleStep() &&
321 120 : !alg->IsHidden() && name != GDALRasterWriteAlgorithm::NAME)
322 : {
323 9 : ret += '\n';
324 18 : alg->SetCallPath({name});
325 9 : ret += alg->GetUsageForCLI(shortUsage, stepUsageOptions);
326 : }
327 : }
328 : {
329 3 : const auto name = GDALRasterWriteAlgorithm::NAME;
330 3 : ret += '\n';
331 6 : auto alg = GetStepAlg(name);
332 6 : alg->SetCallPath({name});
333 3 : ret += alg->GetUsageForCLI(shortUsage, stepUsageOptions);
334 : }
335 3 : ret += GetUsageForCLIEnd();
336 :
337 3 : return ret;
338 : }
339 :
340 : /************************************************************************/
341 : /* GDALRasterPipelineNonNativelyStreamingAlgorithm() */
342 : /************************************************************************/
343 :
344 313 : GDALRasterPipelineNonNativelyStreamingAlgorithm::
345 : GDALRasterPipelineNonNativelyStreamingAlgorithm(
346 : const std::string &name, const std::string &description,
347 313 : const std::string &helpURL, bool standaloneStep)
348 : : GDALRasterPipelineStepAlgorithm(name, description, helpURL,
349 313 : standaloneStep)
350 : {
351 313 : }
352 :
353 : /************************************************************************/
354 : /* IsNativelyStreamingCompatible() */
355 : /************************************************************************/
356 :
357 46 : bool GDALRasterPipelineNonNativelyStreamingAlgorithm::
358 : IsNativelyStreamingCompatible() const
359 : {
360 46 : return false;
361 : }
362 :
363 : /************************************************************************/
364 : /* MustCreateOnDiskTempDataset() */
365 : /************************************************************************/
366 :
367 55 : static bool MustCreateOnDiskTempDataset(int nWidth, int nHeight, int nBands,
368 : GDALDataType eDT)
369 : {
370 : // Config option mostly for autotest purposes
371 55 : if (CPLTestBool(CPLGetConfigOption(
372 : "GDAL_RASTER_PIPELINE_USE_GTIFF_FOR_TEMP_DATASET", "NO")))
373 5 : return true;
374 :
375 : // Allow up to 10% of RAM usage for temporary dataset
376 50 : const auto nRAM = CPLGetUsablePhysicalRAM() / 10;
377 50 : const int nDTSize = GDALGetDataTypeSizeBytes(eDT);
378 50 : const bool bOnDisk =
379 100 : nBands > 0 && nDTSize > 0 && nRAM > 0 &&
380 50 : static_cast<int64_t>(nWidth) * nHeight > nRAM / (nBands * nDTSize);
381 50 : return bOnDisk;
382 : }
383 :
384 : /************************************************************************/
385 : /* CreateTemporaryDataset() */
386 : /************************************************************************/
387 :
388 : std::unique_ptr<GDALDataset>
389 30 : GDALRasterPipelineNonNativelyStreamingAlgorithm::CreateTemporaryDataset(
390 : int nWidth, int nHeight, int nBands, GDALDataType eDT,
391 : bool bTiledIfPossible, GDALDataset *poSrcDSForMetadata, bool bCopyMetadata)
392 : {
393 : const bool bOnDisk =
394 30 : MustCreateOnDiskTempDataset(nWidth, nHeight, nBands, eDT);
395 30 : const char *pszDriverName = bOnDisk ? "GTIFF" : "MEM";
396 : GDALDriver *poDriver =
397 30 : GetGDALDriverManager()->GetDriverByName(pszDriverName);
398 60 : CPLStringList aosOptions;
399 60 : std::string osTmpFilename;
400 30 : if (bOnDisk)
401 : {
402 : osTmpFilename =
403 4 : CPLGenerateTempFilenameSafe(
404 : poSrcDSForMetadata
405 4 : ? CPLGetBasenameSafe(poSrcDSForMetadata->GetDescription())
406 2 : .c_str()
407 4 : : "") +
408 2 : ".tif";
409 2 : if (bTiledIfPossible)
410 2 : aosOptions.SetNameValue("TILED", "YES");
411 : const char *pszCOList =
412 2 : poDriver->GetMetadataItem(GDAL_DMD_CREATIONOPTIONLIST);
413 : aosOptions.SetNameValue("COMPRESS",
414 2 : pszCOList && strstr(pszCOList, "ZSTD") ? "ZSTD"
415 4 : : "LZW");
416 2 : aosOptions.SetNameValue("SPARSE_OK", "YES");
417 : }
418 : std::unique_ptr<GDALDataset> poOutDS(
419 30 : poDriver ? poDriver->Create(osTmpFilename.c_str(), nWidth, nHeight,
420 30 : nBands, eDT, aosOptions.List())
421 60 : : nullptr);
422 30 : if (poOutDS && bOnDisk)
423 : {
424 : // In file systems that allow it (all but Windows...), we want to
425 : // delete the temporary file as soon as soon as possible after
426 : // having open it, so that if someone kills the process there are
427 : // no temp files left over. If that unlink() doesn't succeed
428 : // (on Windows), then the file will eventually be deleted when
429 : // poTmpDS is cleaned due to MarkSuppressOnClose().
430 0 : VSIUnlink(osTmpFilename.c_str());
431 0 : poOutDS->MarkSuppressOnClose();
432 : }
433 :
434 30 : if (poOutDS && poSrcDSForMetadata)
435 : {
436 28 : poOutDS->SetSpatialRef(poSrcDSForMetadata->GetSpatialRef());
437 28 : GDALGeoTransform gt;
438 28 : if (poSrcDSForMetadata->GetGeoTransform(gt) == CE_None)
439 27 : poOutDS->SetGeoTransform(gt);
440 28 : if (const int nGCPCount = poSrcDSForMetadata->GetGCPCount())
441 : {
442 0 : const auto apsGCPs = poSrcDSForMetadata->GetGCPs();
443 0 : if (apsGCPs)
444 : {
445 0 : poOutDS->SetGCPs(nGCPCount, apsGCPs,
446 0 : poSrcDSForMetadata->GetGCPSpatialRef());
447 : }
448 : }
449 28 : if (bCopyMetadata)
450 : {
451 14 : poOutDS->SetMetadata(poSrcDSForMetadata->GetMetadata());
452 : }
453 : }
454 :
455 60 : return poOutDS;
456 : }
457 :
458 : /************************************************************************/
459 : /* CreateTemporaryCopy() */
460 : /************************************************************************/
461 :
462 : std::unique_ptr<GDALDataset>
463 25 : GDALRasterPipelineNonNativelyStreamingAlgorithm::CreateTemporaryCopy(
464 : GDALAlgorithm *poAlg, GDALDataset *poSrcDS, int nSingleBand,
465 : bool bTiledIfPossible, GDALProgressFunc pfnProgress, void *pProgressData)
466 : {
467 25 : const int nBands = nSingleBand > 0 ? 1 : poSrcDS->GetRasterCount();
468 : const auto eDT =
469 25 : nBands ? poSrcDS->GetRasterBand(1)->GetRasterDataType() : GDT_Unknown;
470 25 : const bool bOnDisk = MustCreateOnDiskTempDataset(
471 : poSrcDS->GetRasterXSize(), poSrcDS->GetRasterYSize(), nBands, eDT);
472 25 : const char *pszDriverName = bOnDisk ? "GTIFF" : "MEM";
473 :
474 50 : CPLStringList options;
475 25 : if (nSingleBand > 0)
476 : {
477 25 : options.AddString("-b");
478 25 : options.AddString(CPLSPrintf("%d", nSingleBand));
479 : }
480 :
481 25 : options.AddString("-of");
482 25 : options.AddString(pszDriverName);
483 :
484 50 : std::string osTmpFilename;
485 25 : if (bOnDisk)
486 : {
487 : osTmpFilename =
488 3 : CPLGenerateTempFilenameSafe(
489 9 : CPLGetBasenameSafe(poSrcDS->GetDescription()).c_str()) +
490 3 : ".tif";
491 3 : if (bTiledIfPossible)
492 : {
493 3 : options.AddString("-co");
494 3 : options.AddString("TILED=YES");
495 : }
496 :
497 : GDALDriver *poDriver =
498 3 : GetGDALDriverManager()->GetDriverByName(pszDriverName);
499 : const char *pszCOList =
500 3 : poDriver ? poDriver->GetMetadataItem(GDAL_DMD_CREATIONOPTIONLIST)
501 3 : : nullptr;
502 3 : options.AddString("-co");
503 3 : options.AddString(pszCOList && strstr(pszCOList, "ZSTD")
504 : ? "COMPRESS=ZSTD"
505 6 : : "COMPRESS=LZW");
506 : }
507 :
508 : GDALTranslateOptions *translateOptions =
509 25 : GDALTranslateOptionsNew(options.List(), nullptr);
510 :
511 25 : if (pfnProgress)
512 12 : GDALTranslateOptionsSetProgress(translateOptions, pfnProgress,
513 : pProgressData);
514 :
515 : std::unique_ptr<GDALDataset> poOutDS(GDALDataset::FromHandle(
516 : GDALTranslate(osTmpFilename.c_str(), GDALDataset::ToHandle(poSrcDS),
517 25 : translateOptions, nullptr)));
518 25 : GDALTranslateOptionsFree(translateOptions);
519 :
520 25 : if (!poOutDS)
521 : {
522 2 : poAlg->ReportError(CE_Failure, CPLE_AppDefined,
523 : "Failed to create temporary dataset");
524 : }
525 23 : else if (bOnDisk)
526 : {
527 : // In file systems that allow it (all but Windows...), we want to
528 : // delete the temporary file as soon as soon as possible after
529 : // having open it, so that if someone kills the process there are
530 : // no temp files left over. If that unlink() doesn't succeed
531 : // (on Windows), then the file will eventually be deleted when
532 : // poTmpDS is cleaned due to MarkSuppressOnClose().
533 1 : VSIUnlink(osTmpFilename.c_str());
534 1 : poOutDS->MarkSuppressOnClose();
535 : }
536 50 : return poOutDS;
537 : }
538 :
539 : //! @endcond
|