/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 | | bool 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 | | bool 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 | | auto eType = wkbFlatten(poGeom->getGeometryType()); |
150 | | GEOSGeometry *poGeosGeom = poGeom->exportToGEOS(m_geosContext); |
151 | | |
152 | | if (!poGeosGeom) |
153 | | { |
154 | | // Try to find a useful message / coordinate from |
155 | | // GEOS exception message. |
156 | | poErrorFeature = CreateFeatureFromLastError(); |
157 | | |
158 | | if (eType == wkbPolygon) |
159 | | { |
160 | | const OGRLinearRing *poRing = |
161 | | poGeom->toPolygon()->getExteriorRing(); |
162 | | if (poRing != nullptr && !poRing->IsEmpty()) |
163 | | { |
164 | | auto poPoint = std::make_unique<OGRPoint>(); |
165 | | poRing->StartPoint(poPoint.get()); |
166 | | auto poMultiPoint = std::make_unique<OGRMultiPoint>(); |
167 | | poMultiPoint->addGeometry(std::move(poPoint)); |
168 | | poErrorFeature->SetGeometry(std::move(poMultiPoint)); |
169 | | } |
170 | | else |
171 | | { |
172 | | // TODO get a point from somewhere else? |
173 | | } |
174 | | } |
175 | | } |
176 | | else |
177 | | { |
178 | | char *pszReason = nullptr; |
179 | | GEOSGeometry *location = nullptr; |
180 | | char ret = 1; |
181 | | bool warnAboutGeosVersion = false; |
182 | | bool checkedSimple = false; |
183 | | |
184 | | // check all geometry types for validity; IsSimple does not |
185 | | // detect NaN/Inf coordinates |
186 | | ret = GEOSisValidDetail_r(m_geosContext, poGeosGeom, 0, |
187 | | &pszReason, &location); |
188 | | |
189 | | if (ret == 1 && |
190 | | (eType == wkbLineString || eType == wkbMultiLineString || |
191 | | eType == wkbCircularString || eType == wkbCompoundCurve || |
192 | | eType == wkbGeometryCollection)) |
193 | | { |
194 | | checkedSimple = true; |
195 | | #if GEOS_VERSION_MAJOR > 3 || \ |
196 | | (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 14) |
197 | | ret = GEOSisSimpleDetail_r(m_geosContext, poGeosGeom, 1, |
198 | | &location); |
199 | | #else |
200 | | ret = GEOSisSimple_r(m_geosContext, poGeosGeom); |
201 | | warnAboutGeosVersion = true; |
202 | | #endif |
203 | | } |
204 | | |
205 | | GEOSGeom_destroy_r(m_geosContext, poGeosGeom); |
206 | | if (ret == 0) |
207 | | { |
208 | | if (warnAboutGeosVersion) |
209 | | { |
210 | | CPLErrorOnce( |
211 | | CE_Warning, CPLE_AppDefined, |
212 | | "Detected a non-simple linear geometry, but " |
213 | | "cannot output self-intersection points " |
214 | | "because GEOS library version is < 3.14."); |
215 | | } |
216 | | |
217 | | poErrorFeature = std::make_unique<OGRFeature>(m_defn.get()); |
218 | | if (pszReason == nullptr) |
219 | | { |
220 | | if (checkedSimple) |
221 | | { |
222 | | poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD, |
223 | | "self-intersection"); |
224 | | } |
225 | | } |
226 | | else |
227 | | { |
228 | | poErrorFeature->SetField(ERROR_DESCRIPTION_FIELD, |
229 | | pszReason); |
230 | | GEOSFree_r(m_geosContext, pszReason); |
231 | | } |
232 | | |
233 | | if (location != nullptr) |
234 | | { |
235 | | std::unique_ptr<OGRGeometry> poErrorGeom( |
236 | | OGRGeometryFactory::createFromGEOS(m_geosContext, |
237 | | location)); |
238 | | GEOSGeom_destroy_r(m_geosContext, location); |
239 | | |
240 | | if (poErrorGeom->getGeometryType() == wkbPoint) |
241 | | { |
242 | | auto poMultiPoint = |
243 | | std::make_unique<OGRMultiPoint>(); |
244 | | poMultiPoint->addGeometry(std::move(poErrorGeom)); |
245 | | poErrorGeom = std::move(poMultiPoint); |
246 | | } |
247 | | |
248 | | poErrorGeom->assignSpatialReference( |
249 | | m_srcLayer.GetLayerDefn() |
250 | | ->GetGeomFieldDefn(m_srcGeomField) |
251 | | ->GetSpatialRef()); |
252 | | |
253 | | poErrorFeature->SetGeometry(std::move(poErrorGeom)); |
254 | | } |
255 | | } |
256 | | else if (ret == 2) |
257 | | { |
258 | | poErrorFeature = CreateFeatureFromLastError(); |
259 | | } |
260 | | } |
261 | | } |
262 | | |
263 | | if (!poErrorFeature && !m_skipValid) |
264 | | { |
265 | | poErrorFeature = std::make_unique<OGRFeature>(m_defn.get()); |
266 | | // TODO Set geometry to POINT EMPTY ? |
267 | | } |
268 | | |
269 | | if (poErrorFeature) |
270 | | { |
271 | | if (!m_srcFieldMap.empty()) |
272 | | { |
273 | | poErrorFeature->SetFieldsFrom( |
274 | | poSrcFeature.get(), m_srcFieldMap.data(), false, false); |
275 | | } |
276 | | poErrorFeature->SetFID(poSrcFeature->GetFID()); |
277 | | if (PassesFilters(poErrorFeature.get())) |
278 | | apoOutputFeatures.push_back(std::move(poErrorFeature)); |
279 | | } |
280 | | |
281 | | return true; |
282 | | } |
283 | | |
284 | | CPL_DISALLOW_COPY_ASSIGN(GDALInvalidLocationLayer) |
285 | | |
286 | | private: |
287 | | std::vector<int> m_srcFieldMap{}; |
288 | | const OGRFeatureDefnRefCountedPtr m_defn; |
289 | | const GEOSContextHandle_t m_geosContext; |
290 | | const int m_srcGeomField; |
291 | | const bool m_skipValid; |
292 | | }; |
293 | | |
294 | | GDALInvalidLocationLayer::~GDALInvalidLocationLayer() |
295 | | { |
296 | | finishGEOS_r(m_geosContext); |
297 | | } |
298 | | |
299 | | #endif |
300 | | |
301 | | bool GDALVectorCheckGeometryAlgorithm::RunStep(GDALPipelineStepRunContext &) |
302 | 0 | { |
303 | | #ifdef HAVE_GEOS |
304 | | auto poSrcDS = m_inputDataset[0].GetDatasetRef(); |
305 | | CPLAssert(poSrcDS); |
306 | | CPLAssert(m_outputDataset.GetName().empty()); |
307 | | CPLAssert(!m_outputDataset.GetDatasetRef()); |
308 | | |
309 | | const bool bSingleLayerOutput = m_inputLayerNames.empty() |
310 | | ? poSrcDS->GetLayerCount() == 1 |
311 | | : m_inputLayerNames.size() == 1; |
312 | | |
313 | | auto outDS = std::make_unique<GDALVectorPipelineOutputDataset>(*poSrcDS); |
314 | | for (auto &&poSrcLayer : poSrcDS->GetLayers()) |
315 | | { |
316 | | if (m_inputLayerNames.empty() || |
317 | | std::find(m_inputLayerNames.begin(), m_inputLayerNames.end(), |
318 | | poSrcLayer->GetDescription()) != m_inputLayerNames.end()) |
319 | | { |
320 | | const auto poSrcLayerDefn = poSrcLayer->GetLayerDefn(); |
321 | | if (poSrcLayerDefn->GetGeomFieldCount() == 0) |
322 | | { |
323 | | if (m_inputLayerNames.empty()) |
324 | | continue; |
325 | | ReportError(CE_Failure, CPLE_AppDefined, |
326 | | "Specified layer '%s' has no geometry field", |
327 | | poSrcLayer->GetDescription()); |
328 | | return false; |
329 | | } |
330 | | |
331 | | const int geomFieldIndex = |
332 | | m_geomField.empty() |
333 | | ? 0 |
334 | | : poSrcLayerDefn->GetGeomFieldIndex(m_geomField.c_str()); |
335 | | |
336 | | if (geomFieldIndex == -1) |
337 | | { |
338 | | ReportError(CE_Failure, CPLE_AppDefined, |
339 | | "Specified geometry field '%s' does not exist in " |
340 | | "layer '%s'", |
341 | | m_geomField.c_str(), poSrcLayer->GetDescription()); |
342 | | return false; |
343 | | } |
344 | | |
345 | | std::vector<int> includeFieldIndices; |
346 | | if (!GetFieldIndices(m_includeFields, |
347 | | OGRLayer::ToHandle(poSrcLayer), |
348 | | includeFieldIndices)) |
349 | | { |
350 | | return false; |
351 | | } |
352 | | |
353 | | outDS->AddLayer(*poSrcLayer, |
354 | | std::make_unique<GDALInvalidLocationLayer>( |
355 | | *poSrcLayer, includeFieldIndices, |
356 | | bSingleLayerOutput, geomFieldIndex, |
357 | | !m_includeValid)); |
358 | | } |
359 | | } |
360 | | |
361 | | m_outputDataset.Set(std::move(outDS)); |
362 | | |
363 | | return true; |
364 | | #else |
365 | 0 | ReportError(CE_Failure, CPLE_AppDefined, |
366 | 0 | "%s requires GDAL to be built against the GEOS library.", NAME); |
367 | 0 | return false; |
368 | 0 | #endif |
369 | 0 | } |
370 | | |
371 | | GDALVectorCheckGeometryAlgorithmStandalone:: |
372 | 0 | ~GDALVectorCheckGeometryAlgorithmStandalone() = default; |
373 | | |
374 | | //! @endcond |