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