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_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(OGRFeatureDefnRefCountedPtr::makeInstance(
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->SetGeomType(wkbMultiPoint);
73
74
        if (!srcFieldIndices.empty())
75
        {
76
            const OGRFeatureDefn &srcDefn = *layer.GetLayerDefn();
77
            m_srcFieldMap.resize(srcDefn.GetFieldCount(), -1);
78
            int iDstField = 0;
79
            for (int iSrcField : srcFieldIndices)
80
            {
81
                m_defn->AddFieldDefn(srcDefn.GetFieldDefn(iSrcField));
82
                m_srcFieldMap[iSrcField] = iDstField++;
83
            }
84
        }
85
86
        auto poDescriptionFieldDefn =
87
            std::make_unique<OGRFieldDefn>(ERROR_DESCRIPTION_FIELD, OFTString);
88
        m_defn->AddFieldDefn(std::move(poDescriptionFieldDefn));
89
90
        m_defn->GetGeomFieldDefn(0)->SetSpatialRef(
91
            m_srcLayer.GetLayerDefn()
92
                ->GetGeomFieldDefn(m_srcGeomField)
93
                ->GetSpatialRef());
94
    }
95
96
    ~GDALInvalidLocationLayer() override;
97
98
    int TestCapability(const char *) const override
99
    {
100
        return false;
101
    }
102
103
    const char *GetDescription() const override
104
    {
105
        return GetName();
106
    }
107
108
    const OGRFeatureDefn *GetLayerDefn() const override
109
    {
110
        return m_defn.get();
111
    }
112
113
    std::unique_ptr<OGRFeature> CreateFeatureFromLastError() const
114
    {
115
        auto poErrorFeature = std::make_unique<OGRFeature>(m_defn.get());
116
117
        std::string msg = CPLGetLastErrorMsg();
118
119
        // Trim GEOS exception name
120
        const auto subMsgPos = msg.find(": ");
121
        if (subMsgPos != std::string::npos)
122
        {
123
            msg = msg.substr(subMsgPos + strlen(": "));
124
        }
125
126
        // Trim newline from end of GEOS exception message
127
        if (!msg.empty() && msg.back() == '\n')
128
        {
129
            msg.pop_back();
130
        }
131
132
        poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD, msg.c_str());
133
134
        CPLErrorReset();
135
136
        return poErrorFeature;
137
    }
138
139
    void TranslateFeature(
140
        std::unique_ptr<OGRFeature> poSrcFeature,
141
        std::vector<std::unique_ptr<OGRFeature>> &apoOutputFeatures) override
142
    {
143
        const OGRGeometry *poGeom =
144
            poSrcFeature->GetGeomFieldRef(m_srcGeomField);
145
        std::unique_ptr<OGRFeature> poErrorFeature;
146
147
        if (poGeom)
148
        {
149
            if (poGeom->getDimension() < 1)
150
            {
151
                CPLErrorOnce(CE_Warning, CPLE_AppDefined,
152
                             "Point geometry passed to 'gdal vector "
153
                             "check-geometry'. Point geometries are "
154
                             "always valid/simple.");
155
            }
156
            else
157
            {
158
                auto eType = wkbFlatten(poGeom->getGeometryType());
159
                GEOSGeometry *poGeosGeom = poGeom->exportToGEOS(m_geosContext);
160
161
                if (!poGeosGeom)
162
                {
163
                    // Try to find a useful message / coordinate from
164
                    // GEOS exception message.
165
                    poErrorFeature = CreateFeatureFromLastError();
166
167
                    if (eType == wkbPolygon)
168
                    {
169
                        const OGRLinearRing *poRing =
170
                            poGeom->toPolygon()->getExteriorRing();
171
                        if (poRing != nullptr && !poRing->IsEmpty())
172
                        {
173
                            auto poPoint = std::make_unique<OGRPoint>();
174
                            poRing->StartPoint(poPoint.get());
175
                            auto poMultiPoint =
176
                                std::make_unique<OGRMultiPoint>();
177
                            poMultiPoint->addGeometry(std::move(poPoint));
178
                            poErrorFeature->SetGeometry(
179
                                std::move(poMultiPoint));
180
                        }
181
                        else
182
                        {
183
                            // TODO get a point from somewhere else?
184
                        }
185
                    }
186
                }
187
                else
188
                {
189
                    char *pszReason = nullptr;
190
                    GEOSGeometry *location = nullptr;
191
                    char ret = 1;
192
                    bool warnAboutGeosVersion = false;
193
                    bool checkedSimple = false;
194
195
                    if (eType == wkbPolygon || eType == wkbMultiPolygon ||
196
                        eType == wkbCurvePolygon || eType == wkbMultiSurface ||
197
                        eType == wkbGeometryCollection)
198
                    {
199
                        ret = GEOSisValidDetail_r(m_geosContext, poGeosGeom, 0,
200
                                                  &pszReason, &location);
201
                    }
202
203
                    if (eType == wkbLineString || eType == wkbMultiLineString ||
204
                        eType == wkbCircularString ||
205
                        eType == wkbCompoundCurve ||
206
                        (ret == 1 && eType == wkbGeometryCollection))
207
                    {
208
                        checkedSimple = true;
209
#if GEOS_VERSION_MAJOR > 3 ||                                                  \
210
    (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 14)
211
                        ret = GEOSisSimpleDetail_r(m_geosContext, poGeosGeom, 1,
212
                                                   &location);
213
#else
214
                        ret = GEOSisSimple_r(m_geosContext, poGeosGeom);
215
                        warnAboutGeosVersion = true;
216
#endif
217
                    }
218
219
                    GEOSGeom_destroy_r(m_geosContext, poGeosGeom);
220
                    if (ret == 0)
221
                    {
222
                        if (warnAboutGeosVersion)
223
                        {
224
                            CPLErrorOnce(
225
                                CE_Warning, CPLE_AppDefined,
226
                                "Detected a non-simple linear geometry, but "
227
                                "cannot output self-intersection points "
228
                                "because GEOS library version is < 3.14.");
229
                        }
230
231
                        poErrorFeature =
232
                            std::make_unique<OGRFeature>(m_defn.get());
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.get());
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
    const OGRFeatureDefnRefCountedPtr 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
    finishGEOS_r(m_geosContext);
312
}
313
314
#endif
315
316
bool GDALVectorCheckGeometryAlgorithm::RunStep(GDALPipelineStepRunContext &)
317
0
{
318
#ifdef HAVE_GEOS
319
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
320
    CPLAssert(poSrcDS);
321
    CPLAssert(m_outputDataset.GetName().empty());
322
    CPLAssert(!m_outputDataset.GetDatasetRef());
323
324
    const bool bSingleLayerOutput = m_inputLayerNames.empty()
325
                                        ? poSrcDS->GetLayerCount() == 1
326
                                        : m_inputLayerNames.size() == 1;
327
328
    auto outDS = std::make_unique<GDALVectorPipelineOutputDataset>(*poSrcDS);
329
    for (auto &&poSrcLayer : poSrcDS->GetLayers())
330
    {
331
        if (m_inputLayerNames.empty() ||
332
            std::find(m_inputLayerNames.begin(), m_inputLayerNames.end(),
333
                      poSrcLayer->GetDescription()) != m_inputLayerNames.end())
334
        {
335
            const auto poSrcLayerDefn = poSrcLayer->GetLayerDefn();
336
            if (poSrcLayerDefn->GetGeomFieldCount() == 0)
337
            {
338
                if (m_inputLayerNames.empty())
339
                    continue;
340
                ReportError(CE_Failure, CPLE_AppDefined,
341
                            "Specified layer '%s' has no geometry field",
342
                            poSrcLayer->GetDescription());
343
                return false;
344
            }
345
346
            const int geomFieldIndex =
347
                m_geomField.empty()
348
                    ? 0
349
                    : poSrcLayerDefn->GetGeomFieldIndex(m_geomField.c_str());
350
351
            if (geomFieldIndex == -1)
352
            {
353
                ReportError(CE_Failure, CPLE_AppDefined,
354
                            "Specified geometry field '%s' does not exist in "
355
                            "layer '%s'",
356
                            m_geomField.c_str(), poSrcLayer->GetDescription());
357
                return false;
358
            }
359
360
            std::vector<int> includeFieldIndices;
361
            if (!GetFieldIndices(m_includeFields,
362
                                 OGRLayer::ToHandle(poSrcLayer),
363
                                 includeFieldIndices))
364
            {
365
                return false;
366
            }
367
368
            outDS->AddLayer(*poSrcLayer,
369
                            std::make_unique<GDALInvalidLocationLayer>(
370
                                *poSrcLayer, includeFieldIndices,
371
                                bSingleLayerOutput, geomFieldIndex,
372
                                !m_includeValid));
373
        }
374
    }
375
376
    m_outputDataset.Set(std::move(outDS));
377
378
    return true;
379
#else
380
0
    ReportError(CE_Failure, CPLE_AppDefined,
381
0
                "%s requires GDAL to be built against the GEOS library.", NAME);
382
0
    return false;
383
0
#endif
384
0
}
385
386
GDALVectorCheckGeometryAlgorithmStandalone::
387
0
    ~GDALVectorCheckGeometryAlgorithmStandalone() = default;
388
389
//! @endcond