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 113 : GDALVectorConcatAlgorithm::GDALVectorConcatAlgorithm(bool bStandalone)
41 : : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
42 0 : ConstructorOptions()
43 113 : .SetStandaloneStep(bStandalone)
44 113 : .SetAddDefaultArguments(bStandalone)
45 226 : .SetInputDatasetMetaVar("INPUTS")
46 113 : .SetInputDatasetMaxCount(INT_MAX)
47 113 : .SetAddOutputLayerNameArgument(false)
48 339 : .SetAutoOpenInputDatasets(false))
49 : {
50 113 : 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 226 : &m_mode)
59 113 : .SetChoices("merge-per-layer-name", "stack", "single")
60 113 : .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 113 : &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 113 : &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 113 : &m_sourceLayerFieldContent);
74 : AddArg("field-strategy", 0,
75 : _("How to determine target fields from source fields"),
76 226 : &m_fieldStrategy)
77 113 : .SetChoices("union", "intersection")
78 113 : .SetDefault(m_fieldStrategy);
79 226 : AddArg("input-crs", 's', _("Input CRS"), &m_srsCrs)
80 226 : .SetIsCRSArg()
81 226 : .AddHiddenAlias("s_srs")
82 113 : .AddHiddenAlias("src-crs");
83 226 : AddArg("output-crs", 'd', _("Output CRS"), &m_dstCrs)
84 226 : .SetIsCRSArg()
85 226 : .AddHiddenAlias("t_srs")
86 113 : .AddHiddenAlias("dst-crs");
87 113 : }
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 31 : GDALVectorConcatOutputDataset() = default;
107 :
108 : /** Add a dataset the layers read from, taking over a reference on it. */
109 47 : void AddSrcDatasetRef(GDALDataset *poSrcDS)
110 : {
111 47 : m_srcDatasets.emplace_back(poSrcDS);
112 47 : }
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 39 : void AddLayer(std::unique_ptr<OGRLayer> layer)
130 : {
131 39 : m_layers.push_back(std::move(layer));
132 39 : }
133 :
134 : int GetLayerCount() const override;
135 :
136 66 : OGRLayer *GetLayer(int idx) const override
137 : {
138 66 : return idx >= 0 && idx < GetLayerCount() ? m_layers[idx].get()
139 66 : : nullptr;
140 : }
141 :
142 32 : bool TestCapability(const char *pszCap) const override
143 : {
144 32 : if (EQUAL(pszCap, ODsCCurveGeometries) ||
145 30 : EQUAL(pszCap, ODsCMeasuredGeometries) ||
146 29 : EQUAL(pszCap, ODsCZGeometries))
147 : {
148 4 : return true;
149 : }
150 28 : return false;
151 : }
152 : };
153 :
154 178 : int GDALVectorConcatOutputDataset::GetLayerCount() const
155 : {
156 178 : 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 33 : bool GDALVectorConcatAlgorithm::RunStep(GDALPipelineStepRunContext &)
274 : {
275 33 : std::unique_ptr<OGRSpatialReference> poSrcCRS;
276 33 : 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 66 : OGRSpatialReference oDstCRS;
284 33 : 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 33 : if (m_layerNameTemplate.empty())
298 : {
299 30 : if (m_mode == "single")
300 1 : m_layerNameTemplate = "merged";
301 29 : 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 32 : if (m_sourceLayerFieldContent.empty())
313 26 : m_sourceLayerFieldContent = "{AUTO_NAME}";
314 6 : else if (m_sourceLayerFieldName.empty())
315 1 : m_sourceLayerFieldName = "source_ds_lyr";
316 :
317 : const int nMaxSimultaneouslyOpened =
318 96 : std::max(atoi(CPLGetConfigOption(
319 : "GDAL_VECTOR_CONCAT_MAX_OPENED_DATASETS", "100")),
320 32 : 1);
321 :
322 : // First pass on input layers
323 64 : std::map<std::string, std::vector<LayerDesc>> allLayerNames;
324 32 : int iDS = 0;
325 32 : int nonOpenedDSCount = 0;
326 83 : for (auto &srcDS : m_inputDataset)
327 : {
328 52 : GDALDataset *poSrcDS = srcDS.GetDatasetRef();
329 0 : std::unique_ptr<GDALDataset> poTmpDS;
330 52 : if (!poSrcDS)
331 : {
332 19 : poTmpDS.reset(GDALDataset::Open(
333 19 : srcDS.GetName().c_str(), GDAL_OF_VECTOR | GDAL_OF_VERBOSE_ERROR,
334 38 : CPLStringList(m_inputFormats).List(),
335 38 : CPLStringList(m_openOptions).List(), nullptr));
336 19 : poSrcDS = poTmpDS.get();
337 19 : if (!poSrcDS)
338 0 : return false;
339 19 : if (static_cast<int>(m_inputDataset.size()) <=
340 : nMaxSimultaneouslyOpened)
341 : {
342 15 : srcDS.Set(std::move(poTmpDS));
343 15 : poSrcDS = srcDS.GetDatasetRef();
344 : }
345 : else
346 : {
347 4 : ++nonOpenedDSCount;
348 : }
349 : }
350 :
351 52 : int iLayer = 0;
352 111 : for (const auto &poLayer : poSrcDS->GetLayers())
353 : {
354 62 : if (m_inputLayerNames.empty() ||
355 0 : std::find(m_inputLayerNames.begin(), m_inputLayerNames.end(),
356 62 : poLayer->GetName()) != m_inputLayerNames.end())
357 : {
358 64 : 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 116 : LayerDesc layerDesc;
368 58 : layerDesc.iDS = iDS;
369 58 : layerDesc.iLayer = iLayer;
370 58 : layerDesc.osDatasetName = poSrcDS->GetDescription();
371 : const std::string outLayerName =
372 58 : m_mode == "single" ? m_layerNameTemplate
373 52 : : m_mode == "merge-per-layer-name"
374 41 : ? std::string(poLayer->GetName())
375 11 : : BuildLayerName(m_layerNameTemplate, iDS,
376 11 : poSrcDS->GetDescription(), iLayer,
377 212 : poLayer->GetName());
378 58 : CPLDebugOnly("gdal_vector_concat", "%s,%s->%s",
379 : poSrcDS->GetDescription(), poLayer->GetName(),
380 : outLayerName.c_str());
381 58 : allLayerNames[outLayerName].push_back(std::move(layerDesc));
382 : }
383 59 : ++iLayer;
384 : }
385 51 : ++iDS;
386 : }
387 :
388 62 : auto poUnionDS = std::make_unique<GDALVectorConcatOutputDataset>();
389 :
390 31 : OGRLayerPool *poLayerPool = nullptr;
391 31 : if (nonOpenedDSCount > nMaxSimultaneouslyOpened)
392 2 : poLayerPool = poUnionDS->CreateLayerPool(nMaxSimultaneouslyOpened);
393 :
394 31 : bool ret = true;
395 70 : for (const auto &[outLayerName, listOfLayers] : allLayerNames)
396 : {
397 39 : const int nLayerCount = static_cast<int>(listOfLayers.size());
398 : std::unique_ptr<OGRLayer *, VSIFreeReleaser> papoSrcLayers(
399 : static_cast<OGRLayer **>(
400 39 : CPLCalloc(nLayerCount, sizeof(OGRLayer *))));
401 97 : for (const auto [i, layer] : cpl::enumerate(listOfLayers))
402 : {
403 58 : auto &srcDS = m_inputDataset[layer.iDS];
404 58 : GDALDataset *poSrcDS = srcDS.GetDatasetRef();
405 0 : std::unique_ptr<GDALDataset> poTmpDS;
406 58 : 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 58 : OGRLayer *poSrcLayer = poSrcDS->GetLayer(layer.iLayer);
418 :
419 58 : 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 54 : else if (poTmpDS)
433 : {
434 0 : srcDS.Set(std::move(poTmpDS));
435 : }
436 :
437 58 : if (m_sourceLayerFieldName.empty())
438 : {
439 51 : 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 39 : 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 39 : outLayerName.c_str(), nLayerCount, papoSrcLayers.release(),
488 78 : /* bTakeLayerOwnership = */ false);
489 :
490 39 : if (!m_sourceLayerFieldName.empty())
491 : {
492 7 : poUnionLayer->SetSourceLayerFieldName(
493 : m_sourceLayerFieldName.c_str());
494 : }
495 :
496 : const FieldUnionStrategy eStrategy =
497 39 : m_fieldStrategy == "union" ? FIELD_UNION_ALL_LAYERS
498 39 : : FIELD_INTERSECTION_ALL_LAYERS;
499 39 : poUnionLayer->SetFields(eStrategy, 0, nullptr, 0, nullptr);
500 :
501 39 : poUnionDS->AddLayer(std::move(poUnionLayer));
502 : }
503 :
504 31 : if (ret)
505 : {
506 82 : for (auto &srcDS : m_inputDataset)
507 : {
508 51 : if (GDALDataset *poSrcDS = srcDS.GetDatasetIncreaseRefCount())
509 47 : poUnionDS->AddSrcDatasetRef(poSrcDS);
510 : }
511 :
512 31 : m_outputDataset.Set(std::move(poUnionDS));
513 : }
514 31 : return ret;
515 : }
516 :
517 : /************************************************************************/
518 : /* GDALVectorConcatAlgorithm::RunImpl() */
519 : /************************************************************************/
520 :
521 60 : bool GDALVectorConcatAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
522 : void *pProgressData)
523 : {
524 60 : if (m_standaloneStep)
525 : {
526 60 : GDALVectorWriteAlgorithm writeAlg;
527 600 : for (auto &arg : writeAlg.GetArgs())
528 : {
529 1080 : if (!arg->IsHidden() &&
530 510 : arg->GetName() != GDAL_ARG_NAME_OUTPUT_LAYER)
531 : {
532 480 : auto stepArg = GetArg(arg->GetName());
533 480 : if (stepArg && stepArg->IsExplicitlySet())
534 : {
535 59 : arg->SetSkipIfAlreadySet(true);
536 59 : arg->SetFrom(*stepArg);
537 : }
538 : }
539 : }
540 :
541 : // Already checked by GDALAlgorithm::Run()
542 30 : CPLAssert(!m_executionForStreamOutput ||
543 : EQUAL(m_format.c_str(), "stream"));
544 :
545 30 : m_standaloneStep = false;
546 30 : m_alreadyRun = false;
547 30 : const bool ok = Run(pfnProgress, pProgressData);
548 30 : m_standaloneStep = true;
549 :
550 30 : if (!ok)
551 : {
552 2 : return false;
553 : }
554 :
555 28 : if (m_format == "stream")
556 : {
557 4 : return true;
558 : }
559 :
560 24 : writeAlg.m_inputDataset.clear();
561 24 : writeAlg.m_inputDataset.resize(1);
562 24 : writeAlg.m_inputDataset[0].Set(m_outputDataset.GetDatasetRef());
563 24 : if (!writeAlg.Run(pfnProgress, pProgressData))
564 : {
565 1 : return false;
566 : }
567 :
568 23 : m_outputDataset.Set(writeAlg.m_outputDataset.GetDatasetRef());
569 23 : return true;
570 : }
571 : else
572 : {
573 30 : GDALPipelineStepRunContext stepCtxt;
574 30 : stepCtxt.m_pfnProgress = pfnProgress;
575 30 : stepCtxt.m_pProgressData = pProgressData;
576 30 : return RunStep(stepCtxt);
577 : }
578 : }
579 :
580 : GDALVectorConcatAlgorithmStandalone::~GDALVectorConcatAlgorithmStandalone() =
581 : default;
582 :
583 : //! @endcond
|