Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL
4 : * Purpose: gdal "vector concat" subcommand
5 : * Author: Even Rouault <even dot rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "gdalalg_vector_concat.h"
14 : #include "gdalalg_vector_write.h"
15 :
16 : #include "cpl_conv.h"
17 : #include "cpl_enumerate.h"
18 : #include "gdal_priv.h"
19 : #include "gdal_utils.h"
20 : #include "ogrsf_frmts.h"
21 :
22 : #include "ogrlayerdecorator.h"
23 : #include "ogrlayerpool.h"
24 : #include "ogrunionlayer.h"
25 : #include "ogrwarpedlayer.h"
26 :
27 : #include <algorithm>
28 : #include <set>
29 :
30 : //! @cond Doxygen_Suppress
31 :
32 : #ifndef _
33 : #define _(x) (x)
34 : #endif
35 :
36 : /************************************************************************/
37 : /* GDALVectorConcatAlgorithm::GDALVectorConcatAlgorithm() */
38 : /************************************************************************/
39 :
40 112 : GDALVectorConcatAlgorithm::GDALVectorConcatAlgorithm(bool bStandalone)
41 : : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
42 0 : ConstructorOptions()
43 112 : .SetStandaloneStep(bStandalone)
44 112 : .SetAddDefaultArguments(bStandalone)
45 224 : .SetInputDatasetMetaVar("INPUTS")
46 112 : .SetInputDatasetMaxCount(INT_MAX)
47 112 : .SetAddOutputLayerNameArgument(false)
48 336 : .SetAutoOpenInputDatasets(false))
49 : {
50 112 : if (!bStandalone)
51 : {
52 40 : AddVectorInputArgs(/* hiddenForCLI = */ false);
53 : }
54 :
55 : AddArg(
56 : "mode", 0,
57 : _("Determine the strategy to create output layers from source layers "),
58 224 : &m_mode)
59 112 : .SetChoices("merge-per-layer-name", "stack", "single")
60 112 : .SetDefault(m_mode);
61 : AddArg(GDAL_ARG_NAME_OUTPUT_LAYER, 0,
62 : _("Name of the output vector layer (single mode), or template to "
63 : "name the output vector layers (stack mode)"),
64 112 : &m_layerNameTemplate);
65 : AddArg("source-layer-field-name", 0,
66 : _("Name of the new field to add to contain identification of the "
67 : "source layer, with value determined from "
68 : "'source-layer-field-content'"),
69 112 : &m_sourceLayerFieldName);
70 : AddArg("source-layer-field-content", 0,
71 : _("A string, possibly using {AUTO_NAME}, {DS_NAME}, {DS_BASENAME}, "
72 : "{DS_INDEX}, {LAYER_NAME}, {LAYER_INDEX}"),
73 112 : &m_sourceLayerFieldContent);
74 : AddArg("field-strategy", 0,
75 : _("How to determine target fields from source fields"),
76 224 : &m_fieldStrategy)
77 112 : .SetChoices("union", "intersection")
78 112 : .SetDefault(m_fieldStrategy);
79 224 : AddArg("input-crs", 's', _("Input CRS"), &m_srsCrs)
80 224 : .SetIsCRSArg()
81 224 : .AddHiddenAlias("s_srs")
82 112 : .AddHiddenAlias("src-crs");
83 224 : AddArg("output-crs", 'd', _("Output CRS"), &m_dstCrs)
84 224 : .SetIsCRSArg()
85 224 : .AddHiddenAlias("t_srs")
86 112 : .AddHiddenAlias("dst-crs");
87 112 : }
88 :
89 : GDALVectorConcatAlgorithm::~GDALVectorConcatAlgorithm() = default;
90 :
91 : /************************************************************************/
92 : /* GDALVectorConcatOutputDataset */
93 : /************************************************************************/
94 :
95 : class GDALVectorConcatOutputDataset final : public GDALDataset
96 : {
97 : // The layers read lazily from the objects declared before them, and
98 : // members are destroyed in reverse declaration order. Do not reorder.
99 : std::vector<std::unique_ptr<GDALDataset, GDALDatasetUniquePtrReleaser>>
100 : m_srcDatasets{};
101 : std::unique_ptr<OGRLayerPool> m_poLayerPool{};
102 : std::vector<std::unique_ptr<OGRLayer>> m_tempLayers{};
103 : std::vector<std::unique_ptr<OGRLayer>> m_layers{};
104 :
105 : public:
106 30 : GDALVectorConcatOutputDataset() = default;
107 :
108 : /** Add a dataset the layers read from, taking over a reference on it. */
109 45 : void AddSrcDatasetRef(GDALDataset *poSrcDS)
110 : {
111 45 : m_srcDatasets.emplace_back(poSrcDS);
112 45 : }
113 :
114 : /** Create the pool used by proxied layers, and return a borrowed pointer. */
115 2 : OGRLayerPool *CreateLayerPool(int nMaxSimultaneouslyOpened)
116 : {
117 : m_poLayerPool =
118 2 : std::make_unique<OGRLayerPool>(nMaxSimultaneouslyOpened);
119 2 : return m_poLayerPool.get();
120 : }
121 :
122 : /** Add an intermediate layer, and return a borrowed pointer to it. */
123 14 : OGRLayer *AddTempLayer(std::unique_ptr<OGRLayer> layer)
124 : {
125 14 : m_tempLayers.push_back(std::move(layer));
126 14 : return m_tempLayers.back().get();
127 : }
128 :
129 38 : void AddLayer(std::unique_ptr<OGRLayer> layer)
130 : {
131 38 : m_layers.push_back(std::move(layer));
132 38 : }
133 :
134 : int GetLayerCount() const override;
135 :
136 65 : OGRLayer *GetLayer(int idx) const override
137 : {
138 65 : return idx >= 0 && idx < GetLayerCount() ? m_layers[idx].get()
139 65 : : nullptr;
140 : }
141 :
142 31 : bool TestCapability(const char *pszCap) const override
143 : {
144 31 : if (EQUAL(pszCap, ODsCCurveGeometries) ||
145 29 : EQUAL(pszCap, ODsCMeasuredGeometries) ||
146 28 : EQUAL(pszCap, ODsCZGeometries))
147 : {
148 4 : return true;
149 : }
150 27 : return false;
151 : }
152 : };
153 :
154 175 : int GDALVectorConcatOutputDataset::GetLayerCount() const
155 : {
156 175 : return static_cast<int>(m_layers.size());
157 : }
158 :
159 : /************************************************************************/
160 : /* GDALVectorConcatRenamedLayer */
161 : /************************************************************************/
162 :
163 : class GDALVectorConcatRenamedLayer final : public OGRLayerDecorator
164 : {
165 : public:
166 7 : GDALVectorConcatRenamedLayer(OGRLayer *poSrcLayer,
167 : const std::string &newName)
168 7 : : OGRLayerDecorator(poSrcLayer, false), m_newName(newName)
169 : {
170 7 : }
171 :
172 : const char *GetName() const override;
173 :
174 : private:
175 : const std::string m_newName;
176 : };
177 :
178 7 : const char *GDALVectorConcatRenamedLayer::GetName() const
179 : {
180 7 : return m_newName.c_str();
181 : }
182 :
183 : /************************************************************************/
184 : /* BuildLayerName() */
185 : /************************************************************************/
186 :
187 18 : static std::string BuildLayerName(const std::string &layerNameTemplate,
188 : int dsIdx, const std::string &dsName,
189 : int lyrIdx, const std::string &lyrName)
190 : {
191 36 : CPLString ret = layerNameTemplate;
192 36 : std::string baseName;
193 : VSIStatBufL sStat;
194 18 : if (VSIStatL(dsName.c_str(), &sStat) == 0)
195 3 : baseName = CPLGetBasenameSafe(dsName.c_str());
196 :
197 18 : if (baseName == lyrName)
198 : {
199 3 : ret = ret.replaceAll("{AUTO_NAME}", baseName);
200 : }
201 : else
202 : {
203 : ret = ret.replaceAll("{AUTO_NAME}",
204 30 : std::string(baseName.empty() ? dsName : baseName)
205 15 : .append("_")
206 15 : .append(lyrName));
207 : }
208 :
209 : ret =
210 18 : ret.replaceAll("{DS_BASENAME}", !baseName.empty() ? baseName : dsName);
211 18 : ret = ret.replaceAll("{DS_NAME}", dsName);
212 18 : ret = ret.replaceAll("{DS_INDEX}", std::to_string(dsIdx).c_str());
213 18 : ret = ret.replaceAll("{LAYER_NAME}", lyrName);
214 18 : ret = ret.replaceAll("{LAYER_INDEX}", std::to_string(lyrIdx).c_str());
215 :
216 36 : return std::string(std::move(ret));
217 : }
218 :
219 : namespace
220 : {
221 :
222 : /************************************************************************/
223 : /* OpenProxiedLayer() */
224 : /************************************************************************/
225 :
226 : struct PooledInitData
227 : {
228 : std::unique_ptr<GDALDataset> poDS{};
229 : std::string osDatasetName{};
230 : // Copies, and not pointers to the algorithm members, as a layer may be
231 : // (re)opened after the algorithm has been destroyed.
232 : CPLStringList aosInputFormats{};
233 : CPLStringList aosOpenOptions{};
234 : int iLayer = 0;
235 : };
236 :
237 14 : static OGRLayer *OpenProxiedLayer(void *pUserData)
238 : {
239 14 : PooledInitData *pData = static_cast<PooledInitData *>(pUserData);
240 14 : pData->poDS.reset(GDALDataset::Open(
241 : pData->osDatasetName.c_str(), GDAL_OF_VECTOR | GDAL_OF_VERBOSE_ERROR,
242 14 : pData->aosInputFormats.List(), pData->aosOpenOptions.List(), nullptr));
243 14 : if (!pData->poDS)
244 0 : return nullptr;
245 14 : return pData->poDS->GetLayer(pData->iLayer);
246 : }
247 :
248 : /************************************************************************/
249 : /* ReleaseProxiedLayer() */
250 : /************************************************************************/
251 :
252 14 : static void ReleaseProxiedLayer(OGRLayer *, void *pUserData)
253 : {
254 14 : PooledInitData *pData = static_cast<PooledInitData *>(pUserData);
255 14 : pData->poDS.reset();
256 14 : }
257 :
258 : /************************************************************************/
259 : /* FreeProxiedLayerUserData() */
260 : /************************************************************************/
261 :
262 4 : static void FreeProxiedLayerUserData(void *pUserData)
263 : {
264 4 : delete static_cast<PooledInitData *>(pUserData);
265 4 : }
266 :
267 : } // namespace
268 :
269 : /************************************************************************/
270 : /* GDALVectorConcatAlgorithm::RunStep() */
271 : /************************************************************************/
272 :
273 32 : bool GDALVectorConcatAlgorithm::RunStep(GDALPipelineStepRunContext &)
274 : {
275 32 : std::unique_ptr<OGRSpatialReference> poSrcCRS;
276 32 : if (!m_srsCrs.empty())
277 : {
278 1 : poSrcCRS = std::make_unique<OGRSpatialReference>();
279 1 : poSrcCRS->SetFromUserInput(m_srsCrs.c_str());
280 1 : poSrcCRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
281 : }
282 :
283 64 : OGRSpatialReference oDstCRS;
284 32 : if (!m_dstCrs.empty())
285 : {
286 4 : oDstCRS.SetFromUserInput(m_dstCrs.c_str());
287 4 : oDstCRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
288 : }
289 :
290 : struct LayerDesc
291 : {
292 : int iDS = 0;
293 : int iLayer = 0;
294 : std::string osDatasetName{};
295 : };
296 :
297 32 : if (m_layerNameTemplate.empty())
298 : {
299 29 : if (m_mode == "single")
300 1 : m_layerNameTemplate = "merged";
301 28 : else if (m_mode == "stack")
302 3 : m_layerNameTemplate = "{AUTO_NAME}";
303 : }
304 3 : else if (m_mode == "merge-per-layer-name")
305 : {
306 1 : ReportError(CE_Failure, CPLE_IllegalArg,
307 : "'layer-name' name argument cannot be specified in "
308 : "mode=merge-per-layer-name");
309 1 : return false;
310 : }
311 :
312 31 : if (m_sourceLayerFieldContent.empty())
313 25 : m_sourceLayerFieldContent = "{AUTO_NAME}";
314 6 : else if (m_sourceLayerFieldName.empty())
315 1 : m_sourceLayerFieldName = "source_ds_lyr";
316 :
317 : const int nMaxSimultaneouslyOpened =
318 93 : std::max(atoi(CPLGetConfigOption(
319 : "GDAL_VECTOR_CONCAT_MAX_OPENED_DATASETS", "100")),
320 31 : 1);
321 :
322 : // First pass on input layers
323 62 : std::map<std::string, std::vector<LayerDesc>> allLayerNames;
324 31 : int iDS = 0;
325 31 : int nonOpenedDSCount = 0;
326 80 : for (auto &srcDS : m_inputDataset)
327 : {
328 50 : GDALDataset *poSrcDS = srcDS.GetDatasetRef();
329 0 : std::unique_ptr<GDALDataset> poTmpDS;
330 50 : if (!poSrcDS)
331 : {
332 17 : poTmpDS.reset(GDALDataset::Open(
333 17 : srcDS.GetName().c_str(), GDAL_OF_VECTOR | GDAL_OF_VERBOSE_ERROR,
334 34 : CPLStringList(m_inputFormats).List(),
335 34 : CPLStringList(m_openOptions).List(), nullptr));
336 17 : poSrcDS = poTmpDS.get();
337 17 : if (!poSrcDS)
338 0 : return false;
339 17 : if (static_cast<int>(m_inputDataset.size()) <=
340 : nMaxSimultaneouslyOpened)
341 : {
342 13 : srcDS.Set(std::move(poTmpDS));
343 13 : poSrcDS = srcDS.GetDatasetRef();
344 : }
345 : else
346 : {
347 4 : ++nonOpenedDSCount;
348 : }
349 : }
350 :
351 50 : int iLayer = 0;
352 107 : for (const auto &poLayer : poSrcDS->GetLayers())
353 : {
354 60 : if (m_inputLayerNames.empty() ||
355 0 : std::find(m_inputLayerNames.begin(), m_inputLayerNames.end(),
356 60 : poLayer->GetName()) != m_inputLayerNames.end())
357 : {
358 62 : if (!m_dstCrs.empty() && m_srsCrs.empty() &&
359 5 : poLayer->GetSpatialRef() == nullptr)
360 : {
361 1 : ReportError(
362 : CE_Failure, CPLE_AppDefined,
363 : "Layer '%s' of '%s' has no spatial reference system",
364 1 : poLayer->GetName(), poSrcDS->GetDescription());
365 1 : return false;
366 : }
367 112 : LayerDesc layerDesc;
368 56 : layerDesc.iDS = iDS;
369 56 : layerDesc.iLayer = iLayer;
370 56 : layerDesc.osDatasetName = poSrcDS->GetDescription();
371 : const std::string outLayerName =
372 56 : m_mode == "single" ? m_layerNameTemplate
373 50 : : m_mode == "merge-per-layer-name"
374 39 : ? std::string(poLayer->GetName())
375 11 : : BuildLayerName(m_layerNameTemplate, iDS,
376 11 : poSrcDS->GetDescription(), iLayer,
377 206 : poLayer->GetName());
378 56 : CPLDebugOnly("gdal_vector_concat", "%s,%s->%s",
379 : poSrcDS->GetDescription(), poLayer->GetName(),
380 : outLayerName.c_str());
381 56 : allLayerNames[outLayerName].push_back(std::move(layerDesc));
382 : }
383 57 : ++iLayer;
384 : }
385 49 : ++iDS;
386 : }
387 :
388 60 : auto poUnionDS = std::make_unique<GDALVectorConcatOutputDataset>();
389 :
390 30 : OGRLayerPool *poLayerPool = nullptr;
391 30 : if (nonOpenedDSCount > nMaxSimultaneouslyOpened)
392 2 : poLayerPool = poUnionDS->CreateLayerPool(nMaxSimultaneouslyOpened);
393 :
394 30 : bool ret = true;
395 68 : for (const auto &[outLayerName, listOfLayers] : allLayerNames)
396 : {
397 38 : const int nLayerCount = static_cast<int>(listOfLayers.size());
398 : std::unique_ptr<OGRLayer *, VSIFreeReleaser> papoSrcLayers(
399 : static_cast<OGRLayer **>(
400 38 : CPLCalloc(nLayerCount, sizeof(OGRLayer *))));
401 94 : for (const auto [i, layer] : cpl::enumerate(listOfLayers))
402 : {
403 56 : auto &srcDS = m_inputDataset[layer.iDS];
404 56 : GDALDataset *poSrcDS = srcDS.GetDatasetRef();
405 0 : std::unique_ptr<GDALDataset> poTmpDS;
406 56 : if (!poSrcDS)
407 : {
408 4 : poTmpDS.reset(GDALDataset::Open(
409 : layer.osDatasetName.c_str(),
410 : GDAL_OF_VECTOR | GDAL_OF_VERBOSE_ERROR,
411 8 : CPLStringList(m_inputFormats).List(),
412 8 : CPLStringList(m_openOptions).List(), nullptr));
413 4 : poSrcDS = poTmpDS.get();
414 4 : if (!poSrcDS)
415 0 : return false;
416 : }
417 56 : OGRLayer *poSrcLayer = poSrcDS->GetLayer(layer.iLayer);
418 :
419 56 : if (poLayerPool)
420 : {
421 8 : auto pData = std::make_unique<PooledInitData>();
422 4 : pData->osDatasetName = layer.osDatasetName;
423 4 : pData->aosInputFormats = CPLStringList(m_inputFormats);
424 4 : pData->aosOpenOptions = CPLStringList(m_openOptions);
425 4 : pData->iLayer = layer.iLayer;
426 : auto proxiedLayer = std::make_unique<OGRProxiedLayer>(
427 : poLayerPool, OpenProxiedLayer, ReleaseProxiedLayer,
428 4 : FreeProxiedLayerUserData, pData.release());
429 4 : proxiedLayer->SetDescription(poSrcLayer->GetDescription());
430 4 : poSrcLayer = poUnionDS->AddTempLayer(std::move(proxiedLayer));
431 : }
432 52 : else if (poTmpDS)
433 : {
434 0 : srcDS.Set(std::move(poTmpDS));
435 : }
436 :
437 56 : if (m_sourceLayerFieldName.empty())
438 : {
439 49 : papoSrcLayers.get()[i] = poSrcLayer;
440 : }
441 : else
442 : {
443 : const std::string newSrcLayerName = BuildLayerName(
444 7 : m_sourceLayerFieldContent, listOfLayers[i].iDS,
445 7 : listOfLayers[i].osDatasetName.c_str(),
446 28 : listOfLayers[i].iLayer, poSrcLayer->GetName());
447 7 : ret = !newSrcLayerName.empty() && ret;
448 14 : papoSrcLayers.get()[i] = poUnionDS->AddTempLayer(
449 14 : std::make_unique<GDALVectorConcatRenamedLayer>(
450 : poSrcLayer, newSrcLayerName));
451 : }
452 : }
453 :
454 : // Auto-wrap source layers if needed
455 38 : if (!m_dstCrs.empty())
456 : {
457 8 : for (int i = 0; ret && i < nLayerCount; ++i)
458 : {
459 : const OGRSpatialReference *poSrcLayerCRS;
460 5 : if (poSrcCRS)
461 1 : poSrcLayerCRS = poSrcCRS.get();
462 : else
463 4 : poSrcLayerCRS = papoSrcLayers.get()[i]->GetSpatialRef();
464 5 : if (poSrcLayerCRS && !poSrcLayerCRS->IsSame(&oDstCRS))
465 : {
466 : auto poCT = std::unique_ptr<OGRCoordinateTransformation>(
467 : OGRCreateCoordinateTransformation(poSrcLayerCRS,
468 6 : &oDstCRS));
469 : auto poReversedCT =
470 : std::unique_ptr<OGRCoordinateTransformation>(
471 : OGRCreateCoordinateTransformation(&oDstCRS,
472 6 : poSrcLayerCRS));
473 3 : ret = (poCT != nullptr) && (poReversedCT != nullptr);
474 3 : if (ret)
475 : {
476 6 : papoSrcLayers.get()[i] = poUnionDS->AddTempLayer(
477 3 : std::make_unique<OGRWarpedLayer>(
478 3 : papoSrcLayers.get()[i], /* iGeomField = */ 0,
479 6 : /*bTakeOwnership = */ false, std::move(poCT),
480 3 : std::move(poReversedCT)));
481 : }
482 : }
483 : }
484 : }
485 :
486 : auto poUnionLayer = std::make_unique<OGRUnionLayer>(
487 38 : outLayerName.c_str(), nLayerCount, papoSrcLayers.release(),
488 76 : /* bTakeLayerOwnership = */ false);
489 :
490 38 : if (!m_sourceLayerFieldName.empty())
491 : {
492 7 : poUnionLayer->SetSourceLayerFieldName(
493 : m_sourceLayerFieldName.c_str());
494 : }
495 :
496 : const FieldUnionStrategy eStrategy =
497 38 : m_fieldStrategy == "union" ? FIELD_UNION_ALL_LAYERS
498 38 : : FIELD_INTERSECTION_ALL_LAYERS;
499 38 : poUnionLayer->SetFields(eStrategy, 0, nullptr, 0, nullptr);
500 :
501 38 : poUnionDS->AddLayer(std::move(poUnionLayer));
502 : }
503 :
504 30 : if (ret)
505 : {
506 79 : for (auto &srcDS : m_inputDataset)
507 : {
508 49 : if (GDALDataset *poSrcDS = srcDS.GetDatasetIncreaseRefCount())
509 45 : poUnionDS->AddSrcDatasetRef(poSrcDS);
510 : }
511 :
512 30 : m_outputDataset.Set(std::move(poUnionDS));
513 : }
514 30 : return ret;
515 : }
516 :
517 : /************************************************************************/
518 : /* GDALVectorConcatAlgorithm::RunImpl() */
519 : /************************************************************************/
520 :
521 58 : bool GDALVectorConcatAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
522 : void *pProgressData)
523 : {
524 58 : if (m_standaloneStep)
525 : {
526 29 : GDALVectorWriteAlgorithm writeAlg;
527 580 : for (auto &arg : writeAlg.GetArgs())
528 : {
529 1044 : if (!arg->IsHidden() &&
530 493 : arg->GetName() != GDAL_ARG_NAME_OUTPUT_LAYER)
531 : {
532 464 : auto stepArg = GetArg(arg->GetName());
533 464 : if (stepArg && stepArg->IsExplicitlySet())
534 : {
535 58 : arg->SetSkipIfAlreadySet(true);
536 58 : arg->SetFrom(*stepArg);
537 : }
538 : }
539 : }
540 :
541 : // Already checked by GDALAlgorithm::Run()
542 29 : CPLAssert(!m_executionForStreamOutput ||
543 : EQUAL(m_format.c_str(), "stream"));
544 :
545 29 : m_standaloneStep = false;
546 29 : m_alreadyRun = false;
547 29 : bool ret = Run(pfnProgress, pProgressData);
548 29 : m_standaloneStep = true;
549 29 : if (ret)
550 : {
551 27 : if (m_format == "stream")
552 : {
553 4 : ret = true;
554 : }
555 : else
556 : {
557 23 : writeAlg.m_inputDataset.clear();
558 23 : writeAlg.m_inputDataset.resize(1);
559 23 : writeAlg.m_inputDataset[0].Set(m_outputDataset.GetDatasetRef());
560 23 : if (writeAlg.Run(pfnProgress, pProgressData))
561 : {
562 23 : m_outputDataset.Set(
563 : writeAlg.m_outputDataset.GetDatasetRef());
564 23 : ret = true;
565 : }
566 : }
567 : }
568 :
569 29 : return ret;
570 : }
571 : else
572 : {
573 29 : GDALPipelineStepRunContext stepCtxt;
574 29 : stepCtxt.m_pfnProgress = pfnProgress;
575 29 : stepCtxt.m_pProgressData = pProgressData;
576 29 : return RunStep(stepCtxt);
577 : }
578 : }
579 :
580 : GDALVectorConcatAlgorithmStandalone::~GDALVectorConcatAlgorithmStandalone() =
581 : default;
582 :
583 : //! @endcond
|