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