Coverage Report

Created: 2026-04-01 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_vector_check_geometry.cpp
Line
Count
Source
1
/******************************************************************************
2
*
3
 * Project:  GDAL
4
 * Purpose:  "gdal vector check-geometry" subcommand
5
 * Author:   Daniel Baston
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, ISciences LLC
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_vector_check_geometry.h"
14
15
#include "cpl_error.h"
16
#include "gdal_priv.h"
17
#include "gdalalg_vector_geom.h"
18
#include "ogr_geometry.h"
19
#include "ogr_geos.h"
20
21
#include <cinttypes>
22
23
#ifndef _
24
0
#define _(x) (x)
25
#endif
26
27
//! @cond Doxygen_Suppress
28
29
GDALVectorCheckGeometryAlgorithm::GDALVectorCheckGeometryAlgorithm(
30
    bool standaloneStep)
31
0
    : GDALVectorPipelineStepAlgorithm(
32
0
          NAME, DESCRIPTION, HELP_URL,
33
0
          ConstructorOptions()
34
0
              .SetStandaloneStep(standaloneStep)
35
0
              .SetNoCreateEmptyLayersArgument(standaloneStep))
36
0
{
37
0
    AddArg("include-field", 0,
38
0
           _("Fields from input layer to include in output (special values: "
39
0
             "ALL and NONE)"),
40
0
           &m_includeFields)
41
0
        .SetDefault("NONE");
42
43
0
    AddArg("include-valid", 0,
44
0
           _("Include valid inputs in output, with empty geometry"),
45
0
           &m_includeValid);
46
47
0
    AddArg("geometry-field", 0, _("Name of geometry field to check"),
48
0
           &m_geomField);
49
0
}
50
51
#ifdef HAVE_GEOS
52
53
class GDALInvalidLocationLayer final : public GDALVectorPipelineOutputLayer
54
{
55
  private:
56
    static constexpr const char *ERROR_DESCRIPTION_FIELD = "error";
57
58
  public:
59
    GDALInvalidLocationLayer(OGRLayer &layer,
60
                             const std::vector<int> &srcFieldIndices,
61
                             bool bSingleLayerOutput, int srcGeomField,
62
                             bool skipValid)
63
        : GDALVectorPipelineOutputLayer(layer),
64
          m_defn(OGRFeatureDefn::CreateFeatureDefn(
65
              bSingleLayerOutput ? "error_location"
66
                                 : std::string("error_location_")
67
                                       .append(layer.GetDescription())
68
                                       .c_str())),
69
          m_geosContext(OGRGeometry::createGEOSContext()),
70
          m_srcGeomField(srcGeomField), m_skipValid(skipValid)
71
    {
72
        m_defn->Reference();
73
        m_defn->SetGeomType(wkbMultiPoint);
74
75
        if (!srcFieldIndices.empty())
76
        {
77
            const OGRFeatureDefn &srcDefn = *layer.GetLayerDefn();
78
            m_srcFieldMap.resize(srcDefn.GetFieldCount(), -1);
79
            int iDstField = 0;
80
            for (int iSrcField : srcFieldIndices)
81
            {
82
                m_defn->AddFieldDefn(srcDefn.GetFieldDefn(iSrcField));
83
                m_srcFieldMap[iSrcField] = iDstField++;
84
            }
85
        }
86
87
        auto poDescriptionFieldDefn =
88
            std::make_unique<OGRFieldDefn>(ERROR_DESCRIPTION_FIELD, OFTString);
89
        m_defn->AddFieldDefn(std::move(poDescriptionFieldDefn));
90
91
        m_defn->GetGeomFieldDefn(0)->SetSpatialRef(
92
            m_srcLayer.GetLayerDefn()
93
                ->GetGeomFieldDefn(m_srcGeomField)
94
                ->GetSpatialRef());
95
    }
96
97
    ~GDALInvalidLocationLayer() override;
98
99
    int TestCapability(const char *) const override
100
    {
101
        return false;
102
    }
103
104
    const char *GetDescription() const override
105
    {
106
        return GetName();
107
    }
108
109
    const OGRFeatureDefn *GetLayerDefn() const override
110
    {
111
        return m_defn;
112
    }
113
114
    std::unique_ptr<OGRFeature> CreateFeatureFromLastError() const
115
    {
116
        auto poErrorFeature = std::make_unique<OGRFeature>(m_defn);
117
118
        std::string msg = CPLGetLastErrorMsg();
119
120
        // Trim GEOS exception name
121
        const auto subMsgPos = msg.find(": ");
122
        if (subMsgPos != std::string::npos)
123
        {
124
            msg = msg.substr(subMsgPos + strlen(": "));
125
        }
126
127
        // Trim newline from end of GEOS exception message
128
        if (!msg.empty() && msg.back() == '\n')
129
        {
130
            msg.pop_back();
131
        }
132
133
        poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD, msg.c_str());
134
135
        CPLErrorReset();
136
137
        return poErrorFeature;
138
    }
139
140
    void TranslateFeature(
141
        std::unique_ptr<OGRFeature> poSrcFeature,
142
        std::vector<std::unique_ptr<OGRFeature>> &apoOutputFeatures) override
143
    {
144
        const OGRGeometry *poGeom =
145
            poSrcFeature->GetGeomFieldRef(m_srcGeomField);
146
        std::unique_ptr<OGRFeature> poErrorFeature;
147
148
        if (poGeom)
149
        {
150
            if (poGeom->getDimension() < 1)
151
            {
152
                CPLErrorOnce(CE_Warning, CPLE_AppDefined,
153
                             "Point geometry passed to 'gdal vector "
154
                             "check-geometry'. Point geometries are "
155
                             "always valid/simple.");
156
            }
157
            else
158
            {
159
                auto eType = wkbFlatten(poGeom->getGeometryType());
160
                GEOSGeometry *poGeosGeom = poGeom->exportToGEOS(m_geosContext);
161
162
                if (!poGeosGeom)
163
                {
164
                    // Try to find a useful message / coordinate from
165
                    // GEOS exception message.
166
                    poErrorFeature = CreateFeatureFromLastError();
167
168
                    if (eType == wkbPolygon)
169
                    {
170
                        const OGRLinearRing *poRing =
171
                            poGeom->toPolygon()->getExteriorRing();
172
                        if (poRing != nullptr && !poRing->IsEmpty())
173
                        {
174
                            auto poPoint = std::make_unique<OGRPoint>();
175
                            poRing->StartPoint(poPoint.get());
176
                            auto poMultiPoint =
177
                                std::make_unique<OGRMultiPoint>();
178
                            poMultiPoint->addGeometry(std::move(poPoint));
179
                            poErrorFeature->SetGeometry(
180
                                std::move(poMultiPoint));
181
                        }
182
                        else
183
                        {
184
                            // TODO get a point from somewhere else?
185
                        }
186
                    }
187
                }
188
                else
189
                {
190
                    char *pszReason = nullptr;
191
                    GEOSGeometry *location = nullptr;
192
                    char ret = 1;
193
                    bool warnAboutGeosVersion = false;
194
                    bool checkedSimple = false;
195
196
                    if (eType == wkbPolygon || eType == wkbMultiPolygon ||
197
                        eType == wkbCurvePolygon || eType == wkbMultiSurface ||
198
                        eType == wkbGeometryCollection)
199
                    {
200
                        ret = GEOSisValidDetail_r(m_geosContext, poGeosGeom, 0,
201
                                                  &pszReason, &location);
202
                    }
203
204
                    if (eType == wkbLineString || eType == wkbMultiLineString ||
205
                        eType == wkbCircularString ||
206
                        eType == wkbCompoundCurve ||
207
                        (ret == 1 && eType == wkbGeometryCollection))
208
                    {
209
                        checkedSimple = true;
210
#if GEOS_VERSION_MAJOR > 3 ||                                                  \
211
    (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 14)
212
                        ret = GEOSisSimpleDetail_r(m_geosContext, poGeosGeom, 1,
213
                                                   &location);
214
#else
215
                        ret = GEOSisSimple_r(m_geosContext, poGeosGeom);
216
                        warnAboutGeosVersion = true;
217
#endif
218
                    }
219
220
                    GEOSGeom_destroy_r(m_geosContext, poGeosGeom);
221
                    if (ret == 0)
222
                    {
223
                        if (warnAboutGeosVersion)
224
                        {
225
                            CPLErrorOnce(
226
                                CE_Warning, CPLE_AppDefined,
227
                                "Detected a non-simple linear geometry, but "
228
                                "cannot output self-intersection points "
229
                                "because GEOS library version is < 3.14.");
230
                        }
231
232
                        poErrorFeature = std::make_unique<OGRFeature>(m_defn);
233
                        if (pszReason == nullptr)
234
                        {
235
                            if (checkedSimple)
236
                            {
237
                                poErrorFeature->SetField(
238
                                    ERROR_DESCRIPTION_FIELD,
239
                                    "self-intersection");
240
                            }
241
                        }
242
                        else
243
                        {
244
                            poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD,
245
                                                     pszReason);
246
                            GEOSFree_r(m_geosContext, pszReason);
247
                        }
248
249
                        if (location != nullptr)
250
                        {
251
                            std::unique_ptr<OGRGeometry> poErrorGeom(
252
                                OGRGeometryFactory::createFromGEOS(
253
                                    m_geosContext, location));
254
                            GEOSGeom_destroy_r(m_geosContext, location);
255
256
                            if (poErrorGeom->getGeometryType() == wkbPoint)
257
                            {
258
                                auto poMultiPoint =
259
                                    std::make_unique<OGRMultiPoint>();
260
                                poMultiPoint->addGeometry(
261
                                    std::move(poErrorGeom));
262
                                poErrorGeom = std::move(poMultiPoint);
263
                            }
264
265
                            poErrorGeom->assignSpatialReference(
266
                                m_srcLayer.GetLayerDefn()
267
                                    ->GetGeomFieldDefn(m_srcGeomField)
268
                                    ->GetSpatialRef());
269
270
                            poErrorFeature->SetGeometry(std::move(poErrorGeom));
271
                        }
272
                    }
273
                    else if (ret == 2)
274
                    {
275
                        poErrorFeature = CreateFeatureFromLastError();
276
                    }
277
                }
278
            }
279
        }
280
281
        if (!poErrorFeature && !m_skipValid)
282
        {
283
            poErrorFeature = std::make_unique<OGRFeature>(m_defn);
284
            // TODO Set geometry to POINT EMPTY ?
285
        }
286
287
        if (poErrorFeature)
288
        {
289
            if (!m_srcFieldMap.empty())
290
            {
291
                poErrorFeature->SetFieldsFrom(
292
                    poSrcFeature.get(), m_srcFieldMap.data(), false, false);
293
            }
294
            poErrorFeature->SetFID(poSrcFeature->GetFID());
295
            apoOutputFeatures.push_back(std::move(poErrorFeature));
296
        }
297
    }
298
299
    CPL_DISALLOW_COPY_ASSIGN(GDALInvalidLocationLayer)
300
301
  private:
302
    std::vector<int> m_srcFieldMap{};
303
    OGRFeatureDefn *const m_defn;
304
    const GEOSContextHandle_t m_geosContext;
305
    const int m_srcGeomField;
306
    const bool m_skipValid;
307
};
308
309
GDALInvalidLocationLayer::~GDALInvalidLocationLayer()
310
{
311
    m_defn->Release();
312
    finishGEOS_r(m_geosContext);
313
}
314
315
#endif
316
317
bool GDALVectorCheckGeometryAlgorithm::RunStep(GDALPipelineStepRunContext &)
318
0
{
319
#ifdef HAVE_GEOS
320
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
321
    CPLAssert(poSrcDS);
322
    CPLAssert(m_outputDataset.GetName().empty());
323
    CPLAssert(!m_outputDataset.GetDatasetRef());
324
325
    const bool bSingleLayerOutput = m_inputLayerNames.empty()
326
                                        ? poSrcDS->GetLayerCount() == 1
327
                                        : m_inputLayerNames.size() == 1;
328
329
    auto outDS = std::make_unique<GDALVectorPipelineOutputDataset>(*poSrcDS);
330
    for (auto &&poSrcLayer : poSrcDS->GetLayers())
331
    {
332
        if (m_inputLayerNames.empty() ||
333
            std::find(m_inputLayerNames.begin(), m_inputLayerNames.end(),
334
                      poSrcLayer->GetDescription()) != m_inputLayerNames.end())
335
        {
336
            const auto poSrcLayerDefn = poSrcLayer->GetLayerDefn();
337
            if (poSrcLayerDefn->GetGeomFieldCount() == 0)
338
            {
339
                if (m_inputLayerNames.empty())
340
                    continue;
341
                ReportError(CE_Failure, CPLE_AppDefined,
342
                            "Specified layer '%s' has no geometry field",
343
                            poSrcLayer->GetDescription());
344
                return false;
345
            }
346
347
            const int geomFieldIndex =
348
                m_geomField.empty()
349
                    ? 0
350
                    : poSrcLayerDefn->GetGeomFieldIndex(m_geomField.c_str());
351
352
            if (geomFieldIndex == -1)
353
            {
354
                ReportError(CE_Failure, CPLE_AppDefined,
355
                            "Specified geometry field '%s' does not exist in "
356
                            "layer '%s'",
357
                            m_geomField.c_str(), poSrcLayer->GetDescription());
358
                return false;
359
            }
360
361
            std::vector<int> includeFieldIndices;
362
            if (!GetFieldIndices(m_includeFields,
363
                                 OGRLayer::ToHandle(poSrcLayer),
364
                                 includeFieldIndices))
365
            {
366
                return false;
367
            }
368
369
            outDS->AddLayer(*poSrcLayer,
370
                            std::make_unique<GDALInvalidLocationLayer>(
371
                                *poSrcLayer, includeFieldIndices,
372
                                bSingleLayerOutput, geomFieldIndex,
373
                                !m_includeValid));
374
        }
375
    }
376
377
    m_outputDataset.Set(std::move(outDS));
378
379
    return true;
380
#else
381
0
    ReportError(CE_Failure, CPLE_AppDefined,
382
0
                "%s requires GDAL to be built against the GEOS library.", NAME);
383
0
    return false;
384
0
#endif
385
0
}
386
387
GDALVectorCheckGeometryAlgorithmStandalone::
388
0
    ~GDALVectorCheckGeometryAlgorithmStandalone() = default;
389
390
//! @endcond