Coverage Report

Created: 2026-04-10 07:04

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