/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 |