Coverage Report

Created: 2026-07-25 06:50

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