Coverage Report

Created: 2026-02-14 06:52

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