/src/gdal/apps/gdalalg_vector_compare.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: GDAL |
4 | | * Purpose: gdal "vector compare" subcommand |
5 | | * Author: Even Rouault <even dot rouault at spatialys.com> |
6 | | * |
7 | | ****************************************************************************** |
8 | | * Copyright (c) 2026, Even Rouault <even dot rouault at spatialys.com> |
9 | | * |
10 | | * SPDX-License-Identifier: MIT |
11 | | ****************************************************************************/ |
12 | | |
13 | | #include "gdalalg_vector_compare.h" |
14 | | |
15 | | #include "cpl_conv.h" |
16 | | #include "cpl_enumerate.h" |
17 | | #include "gdal_dataset.h" |
18 | | #include "ogrsf_frmts.h" |
19 | | |
20 | | //! @cond Doxygen_Suppress |
21 | | |
22 | | #ifndef _ |
23 | 0 | #define _(x) (x) |
24 | | #endif |
25 | | |
26 | | /************************************************************************/ |
27 | | /* GDALVectorCompareAlgorithm::GDALVectorCompareAlgorithm() */ |
28 | | /************************************************************************/ |
29 | | |
30 | | GDALVectorCompareAlgorithm::GDALVectorCompareAlgorithm(bool standaloneStep) |
31 | 0 | : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL, |
32 | 0 | ConstructorOptions() |
33 | 0 | .SetStandaloneStep(standaloneStep) |
34 | 0 | .SetInputDatasetMaxCount(1) |
35 | 0 | .SetAddDefaultArguments(false)) |
36 | 0 | { |
37 | 0 | if (standaloneStep) |
38 | 0 | { |
39 | 0 | AddProgressArg(); |
40 | 0 | } |
41 | 0 | else |
42 | 0 | { |
43 | 0 | AddVectorHiddenInputDatasetArg(); |
44 | 0 | } |
45 | |
|
46 | 0 | auto &referenceDatasetArg = AddArg("reference", 0, _("Reference dataset"), |
47 | 0 | &m_referenceDataset, GDAL_OF_VECTOR) |
48 | 0 | .SetPositional() |
49 | 0 | .SetRequired(); |
50 | |
|
51 | 0 | SetAutoCompleteFunctionForFilename(referenceDatasetArg, GDAL_OF_VECTOR); |
52 | |
|
53 | 0 | if (standaloneStep) |
54 | 0 | { |
55 | 0 | AddVectorInputArgs(/* hiddenForCLI = */ false); |
56 | 0 | } |
57 | |
|
58 | 0 | AddArg("lax-geometry", 0, _("Lax geometry comparison"), |
59 | 0 | &m_laxGeometryComparison); |
60 | |
|
61 | 0 | AddArg("skip-all-optional", 0, _("Skip all optional comparisons"), |
62 | 0 | &m_skipAllOptional); |
63 | 0 | AddArg("skip-binary", 0, _("Skip binary file comparison"), &m_skipBinary); |
64 | 0 | AddArg("skip-crs", 0, _("Skip CRS comparison"), &m_skipCRS); |
65 | 0 | AddArg("skip-metadata", 0, _("Skip metadata comparison"), &m_skipMetadata); |
66 | 0 | AddArg("skip-fid", 0, _("Skip FID comparison"), &m_skipFID); |
67 | |
|
68 | 0 | AddOutputStringArg(&m_output); |
69 | |
|
70 | 0 | AddArg("return-code", 0, _("Return code"), &m_retCode) |
71 | 0 | .SetHiddenForCLI() |
72 | 0 | .SetIsInput(false) |
73 | 0 | .SetIsOutput(true); |
74 | 0 | } |
75 | | |
76 | | /************************************************************************/ |
77 | | /* GDALVectorCompareAlgorithm::RunStep() */ |
78 | | /************************************************************************/ |
79 | | |
80 | | bool GDALVectorCompareAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt) |
81 | 0 | { |
82 | 0 | auto poRefDS = m_referenceDataset.GetDatasetRef(); |
83 | 0 | CPLAssert(poRefDS); |
84 | | |
85 | 0 | CPLAssert(m_inputDataset.size() == 1); |
86 | 0 | auto poInputDS = m_inputDataset[0].GetDatasetRef(); |
87 | 0 | CPLAssert(poInputDS); |
88 | | |
89 | 0 | if (m_skipAllOptional) |
90 | 0 | { |
91 | 0 | m_skipBinary = true; |
92 | 0 | m_skipCRS = true; |
93 | 0 | m_skipMetadata = true; |
94 | 0 | m_skipFID = true; |
95 | 0 | } |
96 | |
|
97 | 0 | if (poRefDS == poInputDS) |
98 | 0 | { |
99 | 0 | return true; |
100 | 0 | } |
101 | | |
102 | 0 | std::vector<std::string> aosReport; |
103 | |
|
104 | 0 | if (!m_skipBinary && poRefDS->GetDriver() == poInputDS->GetDriver() && |
105 | 0 | CPLStringList(poRefDS->GetFileList()).size() == 1 && |
106 | 0 | CPLStringList(poInputDS->GetFileList()).size() == 1) |
107 | 0 | { |
108 | 0 | if (BinaryComparison(this, aosReport, poRefDS, poInputDS)) |
109 | 0 | { |
110 | 0 | return true; |
111 | 0 | } |
112 | 0 | } |
113 | | |
114 | 0 | if (!m_skipMetadata) |
115 | 0 | { |
116 | 0 | MetadataComparison(aosReport, "(dataset default metadata domain)", |
117 | 0 | poRefDS->GetMetadata(), poInputDS->GetMetadata()); |
118 | 0 | } |
119 | |
|
120 | 0 | if (m_inputLayerNames.empty()) |
121 | 0 | { |
122 | 0 | const int nRefCount = poRefDS->GetLayerCount(); |
123 | 0 | const int nInputCount = poInputDS->GetLayerCount(); |
124 | 0 | if (nRefCount != nInputCount) |
125 | 0 | { |
126 | 0 | aosReport.push_back(CPLSPrintf("Reference dataset has %d layer(s), " |
127 | 0 | "whereas input dataset has %d", |
128 | 0 | nRefCount, nInputCount)); |
129 | 0 | } |
130 | |
|
131 | 0 | if (nRefCount == 1 && nInputCount == 1) |
132 | 0 | { |
133 | | // Special case to compare for example 2 shapefiles whose layer name |
134 | | // is related to the filename |
135 | 0 | if (!CompareLayer(aosReport, poRefDS->GetLayer(0), |
136 | 0 | poInputDS->GetLayer(0), ctxt.m_pfnProgress, |
137 | 0 | ctxt.m_pProgressData)) |
138 | 0 | { |
139 | 0 | return false; |
140 | 0 | } |
141 | 0 | } |
142 | 0 | else |
143 | 0 | { |
144 | 0 | int iCurLayer = 0; |
145 | 0 | for (auto *poRefLayer : poRefDS->GetLayers()) |
146 | 0 | { |
147 | 0 | auto *poInputLayer = |
148 | 0 | poInputDS->GetLayerByName(poRefLayer->GetName()); |
149 | 0 | if (poInputLayer) |
150 | 0 | { |
151 | 0 | std::unique_ptr<void, decltype(&GDALDestroyScaledProgress)> |
152 | 0 | pScaledProgress( |
153 | 0 | GDALCreateScaledProgress( |
154 | 0 | static_cast<double>(iCurLayer) / nRefCount, |
155 | 0 | static_cast<double>(iCurLayer + 1) / nRefCount, |
156 | 0 | ctxt.m_pfnProgress, ctxt.m_pProgressData), |
157 | 0 | GDALDestroyScaledProgress); |
158 | 0 | ++iCurLayer; |
159 | 0 | if (!CompareLayer(aosReport, poRefLayer, poInputLayer, |
160 | 0 | pScaledProgress ? GDALScaledProgress |
161 | 0 | : nullptr, |
162 | 0 | pScaledProgress.get())) |
163 | 0 | { |
164 | 0 | return false; |
165 | 0 | } |
166 | 0 | } |
167 | 0 | else |
168 | 0 | { |
169 | 0 | aosReport.push_back( |
170 | 0 | CPLSPrintf("Layer %s present in reference dataset is " |
171 | 0 | "absent from input dataset", |
172 | 0 | poRefLayer->GetName())); |
173 | 0 | } |
174 | 0 | } |
175 | | |
176 | 0 | for (auto *poInputLayer : poInputDS->GetLayers()) |
177 | 0 | { |
178 | 0 | if (!poRefDS->GetLayerByName(poInputLayer->GetName())) |
179 | 0 | { |
180 | 0 | aosReport.push_back( |
181 | 0 | CPLSPrintf("Layer %s present in input dataset is " |
182 | 0 | "absent from reference dataset", |
183 | 0 | poInputLayer->GetName())); |
184 | 0 | } |
185 | 0 | } |
186 | 0 | } |
187 | 0 | } |
188 | 0 | else |
189 | 0 | { |
190 | 0 | for (const auto &[i, osLayerName] : cpl::enumerate(m_inputLayerNames)) |
191 | 0 | { |
192 | 0 | auto *poRefLayer = poRefDS->GetLayerByName(osLayerName.c_str()); |
193 | 0 | auto *poInputLayer = poInputDS->GetLayerByName(osLayerName.c_str()); |
194 | 0 | CPLAssert(poInputLayer); // guaranteed by GDALAlgorithm |
195 | 0 | if (poRefLayer) |
196 | 0 | { |
197 | 0 | std::unique_ptr<void, decltype(&GDALDestroyScaledProgress)> |
198 | 0 | pScaledProgress( |
199 | 0 | GDALCreateScaledProgress( |
200 | 0 | static_cast<double>(i) / |
201 | 0 | static_cast<double>(m_inputLayerNames.size()), |
202 | 0 | static_cast<double>(i + 1) / |
203 | 0 | static_cast<double>(m_inputLayerNames.size()), |
204 | 0 | ctxt.m_pfnProgress, ctxt.m_pProgressData), |
205 | 0 | GDALDestroyScaledProgress); |
206 | 0 | if (!CompareLayer(aosReport, poRefLayer, poInputLayer, |
207 | 0 | pScaledProgress ? GDALScaledProgress |
208 | 0 | : nullptr, |
209 | 0 | pScaledProgress.get())) |
210 | 0 | { |
211 | 0 | return false; |
212 | 0 | } |
213 | 0 | } |
214 | 0 | else |
215 | 0 | { |
216 | 0 | ReportError(CE_Failure, CPLE_AppDefined, |
217 | 0 | "Layer %s present in input dataset is absent from " |
218 | 0 | "reference dataset", |
219 | 0 | osLayerName.c_str()); |
220 | 0 | return false; |
221 | 0 | } |
222 | 0 | } |
223 | 0 | } |
224 | | |
225 | | // Ignore difference related to DBF_DATE_LAST_UPDATE if no other difference |
226 | 0 | if (aosReport.size() == 1 && |
227 | 0 | aosReport[0].find("DBF_DATE_LAST_UPDATE") != std::string::npos) |
228 | 0 | aosReport.clear(); |
229 | |
|
230 | 0 | for (const auto &s : aosReport) |
231 | 0 | { |
232 | 0 | m_output += s; |
233 | 0 | m_output += '\n'; |
234 | 0 | } |
235 | |
|
236 | 0 | m_retCode = static_cast<int>(aosReport.size()); |
237 | |
|
238 | 0 | return true; |
239 | 0 | } |
240 | | |
241 | | /************************************************************************/ |
242 | | /* GDALVectorCompareAlgorithm::CompareLayer() */ |
243 | | /************************************************************************/ |
244 | | |
245 | | bool GDALVectorCompareAlgorithm::CompareLayer( |
246 | | std::vector<std::string> &aosReport, OGRLayer *poRefLayer, |
247 | | OGRLayer *poInputLayer, GDALProgressFunc pfnProgressFunc, |
248 | | void *pProgressData) |
249 | 0 | { |
250 | 0 | const bool bSameLayerName = |
251 | 0 | EQUAL(poRefLayer->GetName(), poInputLayer->GetName()); |
252 | 0 | const std::string osLayerCtxt = |
253 | 0 | bSameLayerName ? CPLSPrintf("Layer %s: ", poRefLayer->GetName()) : ""; |
254 | |
|
255 | 0 | if (!m_skipMetadata) |
256 | 0 | { |
257 | 0 | MetadataComparison( |
258 | 0 | aosReport, |
259 | 0 | CPLSPrintf( |
260 | 0 | "(layer%s default metadata domain)", |
261 | 0 | bSameLayerName |
262 | 0 | ? std::string(" ").append(poRefLayer->GetName()).c_str() |
263 | 0 | : ""), |
264 | 0 | poRefLayer->GetMetadata(), poInputLayer->GetMetadata()); |
265 | 0 | } |
266 | |
|
267 | 0 | const OGRFeatureDefn *poRefDefn = poRefLayer->GetLayerDefn(); |
268 | 0 | const OGRFeatureDefn *poInputDefn = poInputLayer->GetLayerDefn(); |
269 | |
|
270 | 0 | auto poRefDS = m_referenceDataset.GetDatasetRef(); |
271 | 0 | CPLAssert(poRefDS); |
272 | | |
273 | 0 | CPLAssert(m_inputDataset.size() == 1); |
274 | 0 | auto poInputDS = m_inputDataset[0].GetDatasetRef(); |
275 | 0 | CPLAssert(poInputDS); |
276 | | |
277 | 0 | auto poRefDriver = poRefDS->GetDriver(); |
278 | 0 | auto poInputDriver = poInputDS->GetDriver(); |
279 | |
|
280 | 0 | const bool bRefWidthIncludesSign = |
281 | 0 | poRefDriver && poRefDriver->GetMetadataItem( |
282 | 0 | GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN); |
283 | 0 | const bool bInputWidthIncludesSign = |
284 | 0 | poInputDriver && poInputDriver->GetMetadataItem( |
285 | 0 | GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN); |
286 | 0 | const bool bRefWidthIncludesDecimalSeparator = |
287 | 0 | poRefDriver && |
288 | 0 | poRefDriver->GetMetadataItem( |
289 | 0 | GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR); |
290 | 0 | const bool bInputWidthIncludesDecimalSeparator = |
291 | 0 | poInputDriver && |
292 | 0 | poInputDriver->GetMetadataItem( |
293 | 0 | GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR); |
294 | | |
295 | | // Compare attribute field definitions |
296 | 0 | const int nRefFieldCount = poRefDefn->GetFieldCount(); |
297 | 0 | const int nInputFieldCount = poInputDefn->GetFieldCount(); |
298 | 0 | if (nRefFieldCount != nInputFieldCount) |
299 | 0 | { |
300 | 0 | aosReport.push_back(CPLSPrintf("%sReference layer has %d attribute " |
301 | 0 | "field(s), whereas input layer has %d", |
302 | 0 | osLayerCtxt.c_str(), nRefFieldCount, |
303 | 0 | nInputFieldCount)); |
304 | 0 | } |
305 | |
|
306 | 0 | std::vector<int> anMapRefToInputFields; |
307 | 0 | for (const auto *poRefFieldDefn : poRefDefn->GetFields()) |
308 | 0 | { |
309 | 0 | const int nInputFieldDefnIdx = |
310 | 0 | poInputDefn->GetFieldIndex(poRefFieldDefn->GetNameRef()); |
311 | 0 | anMapRefToInputFields.push_back(nInputFieldDefnIdx); |
312 | 0 | if (nInputFieldDefnIdx >= 0) |
313 | 0 | { |
314 | 0 | const auto *poInputFieldDefn = |
315 | 0 | poInputDefn->GetFieldDefn(nInputFieldDefnIdx); |
316 | |
|
317 | 0 | if (poRefFieldDefn->GetType() != poInputFieldDefn->GetType()) |
318 | 0 | { |
319 | 0 | aosReport.push_back(CPLSPrintf( |
320 | 0 | "%sField '%s' has type %s in reference layer, but %s in " |
321 | 0 | "input layer", |
322 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
323 | 0 | OGR_GetFieldTypeName(poRefFieldDefn->GetType()), |
324 | 0 | OGR_GetFieldTypeName(poInputFieldDefn->GetType()))); |
325 | 0 | } |
326 | |
|
327 | 0 | if (poRefFieldDefn->GetSubType() != poInputFieldDefn->GetSubType()) |
328 | 0 | { |
329 | 0 | aosReport.push_back(CPLSPrintf( |
330 | 0 | "%sField '%s' has subtype %s in reference layer, but %s in " |
331 | 0 | "input layer", |
332 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
333 | 0 | OGR_GetFieldSubTypeName(poRefFieldDefn->GetSubType()), |
334 | 0 | OGR_GetFieldSubTypeName(poInputFieldDefn->GetSubType()))); |
335 | 0 | } |
336 | |
|
337 | 0 | if (poRefFieldDefn->GetType() == OFTReal && |
338 | 0 | poInputFieldDefn->GetType() == OFTReal && |
339 | 0 | (poRefFieldDefn->GetWidth() != 0 || |
340 | 0 | poInputFieldDefn->GetWidth() != 0) && |
341 | 0 | (bRefWidthIncludesSign != bInputWidthIncludesSign)) |
342 | 0 | { |
343 | 0 | aosReport.push_back(CPLSPrintf( |
344 | 0 | "%sField '%s' has width %d in reference layer and %s sign " |
345 | 0 | "but width %d in input layer and %s sign", |
346 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
347 | 0 | poRefFieldDefn->GetWidth(), |
348 | 0 | bRefWidthIncludesSign ? "includes" : "does not include", |
349 | 0 | poInputFieldDefn->GetWidth(), |
350 | 0 | bInputWidthIncludesSign ? "includes" : "does not include")); |
351 | 0 | } |
352 | 0 | else if (poRefFieldDefn->GetWidth() != poInputFieldDefn->GetWidth()) |
353 | 0 | { |
354 | 0 | aosReport.push_back(CPLSPrintf( |
355 | 0 | "%sField '%s' has width %d in reference layer, but %d in " |
356 | 0 | "input layer", |
357 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
358 | 0 | poRefFieldDefn->GetWidth(), poInputFieldDefn->GetWidth())); |
359 | 0 | } |
360 | |
|
361 | 0 | if (poRefFieldDefn->GetType() == OFTReal && |
362 | 0 | poInputFieldDefn->GetType() == OFTReal && |
363 | 0 | (poRefFieldDefn->GetPrecision() != 0 || |
364 | 0 | poInputFieldDefn->GetPrecision() != 0) && |
365 | 0 | (bRefWidthIncludesDecimalSeparator != |
366 | 0 | bInputWidthIncludesDecimalSeparator)) |
367 | 0 | { |
368 | 0 | aosReport.push_back(CPLSPrintf( |
369 | 0 | "%sField '%s' has precision %d in reference layer and %s " |
370 | 0 | "decimal separator but precision %d in input layer and %s " |
371 | 0 | "decimal separator", |
372 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
373 | 0 | poRefFieldDefn->GetPrecision(), |
374 | 0 | bRefWidthIncludesDecimalSeparator ? "includes" |
375 | 0 | : "does not include", |
376 | 0 | poInputFieldDefn->GetPrecision(), |
377 | 0 | bInputWidthIncludesDecimalSeparator ? "includes" |
378 | 0 | : "does not include")); |
379 | 0 | } |
380 | 0 | else if (poRefFieldDefn->GetPrecision() != |
381 | 0 | poInputFieldDefn->GetPrecision()) |
382 | 0 | { |
383 | 0 | aosReport.push_back(CPLSPrintf( |
384 | 0 | "%sField '%s' has precision %d in reference layer, but %d " |
385 | 0 | "in " |
386 | 0 | "input layer", |
387 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
388 | 0 | poRefFieldDefn->GetPrecision(), |
389 | 0 | poInputFieldDefn->GetPrecision())); |
390 | 0 | } |
391 | |
|
392 | 0 | if (!EQUAL(poRefFieldDefn->GetAlternativeNameRef(), |
393 | 0 | poInputFieldDefn->GetAlternativeNameRef())) |
394 | 0 | { |
395 | 0 | aosReport.push_back(CPLSPrintf( |
396 | 0 | "%sField '%s' has '%s' as alternative name in reference " |
397 | 0 | "layer, but '%s' in input layer", |
398 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
399 | 0 | poRefFieldDefn->GetAlternativeNameRef(), |
400 | 0 | poInputFieldDefn->GetAlternativeNameRef())); |
401 | 0 | } |
402 | |
|
403 | 0 | if (poRefFieldDefn->IsNullable() != poInputFieldDefn->IsNullable()) |
404 | 0 | { |
405 | 0 | aosReport.push_back(CPLSPrintf( |
406 | 0 | "%sField '%s' has nullable=%d in reference layer, but " |
407 | 0 | "nullable=%d in input layer", |
408 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
409 | 0 | poRefFieldDefn->IsNullable(), |
410 | 0 | poInputFieldDefn->IsNullable())); |
411 | 0 | } |
412 | |
|
413 | 0 | if (poRefFieldDefn->IsUnique() != poInputFieldDefn->IsUnique()) |
414 | 0 | { |
415 | 0 | aosReport.push_back(CPLSPrintf( |
416 | 0 | "%sField '%s' has unique constraint=%d in reference layer, " |
417 | 0 | "but unique constraint=%d in input layer", |
418 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
419 | 0 | poRefFieldDefn->IsUnique(), poInputFieldDefn->IsUnique())); |
420 | 0 | } |
421 | |
|
422 | 0 | if (poRefFieldDefn->IsGenerated() != |
423 | 0 | poInputFieldDefn->IsGenerated()) |
424 | 0 | { |
425 | 0 | aosReport.push_back(CPLSPrintf( |
426 | 0 | "%sField '%s' has generated status=%d in reference layer, " |
427 | 0 | "but generated status=%d in input layer", |
428 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
429 | 0 | poRefFieldDefn->IsGenerated(), |
430 | 0 | poInputFieldDefn->IsGenerated())); |
431 | 0 | } |
432 | |
|
433 | 0 | if (poRefFieldDefn->GetComment() != poInputFieldDefn->GetComment()) |
434 | 0 | { |
435 | 0 | aosReport.push_back(CPLSPrintf( |
436 | 0 | "%sField '%s' has '%s' as comment in reference layer, but " |
437 | 0 | "'%s' in input layer", |
438 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
439 | 0 | poRefFieldDefn->GetComment().c_str(), |
440 | 0 | poInputFieldDefn->GetComment().c_str())); |
441 | 0 | } |
442 | |
|
443 | 0 | if (poRefFieldDefn->GetDomainName() != |
444 | 0 | poInputFieldDefn->GetDomainName()) |
445 | 0 | { |
446 | 0 | aosReport.push_back(CPLSPrintf( |
447 | 0 | "%sField '%s' has '%s' as domain name in reference layer, " |
448 | 0 | "but '%s' in input layer", |
449 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
450 | 0 | poRefFieldDefn->GetDomainName().c_str(), |
451 | 0 | poInputFieldDefn->GetDomainName().c_str())); |
452 | 0 | } |
453 | |
|
454 | 0 | const char *pszRefDefault = poRefFieldDefn->GetDefault(); |
455 | 0 | const char *pszInputDefault = poInputFieldDefn->GetDefault(); |
456 | 0 | if (((pszRefDefault != nullptr) != (pszInputDefault != nullptr)) || |
457 | 0 | (pszRefDefault && pszInputDefault && |
458 | 0 | !EQUAL(pszRefDefault, pszInputDefault))) |
459 | 0 | { |
460 | 0 | aosReport.push_back(CPLSPrintf( |
461 | 0 | "%sField '%s' has '%s' as default in reference layer, but " |
462 | 0 | "'%s' in input layer", |
463 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
464 | 0 | pszRefDefault ? pszRefDefault : "(null)", |
465 | 0 | pszInputDefault ? pszInputDefault : "(null)")); |
466 | 0 | } |
467 | 0 | } |
468 | 0 | else |
469 | 0 | { |
470 | 0 | aosReport.push_back(CPLSPrintf("%sReference layer has field %s, " |
471 | 0 | "which is absent in input layer", |
472 | 0 | osLayerCtxt.c_str(), |
473 | 0 | poRefFieldDefn->GetNameRef())); |
474 | 0 | } |
475 | 0 | } |
476 | |
|
477 | 0 | for (const auto *poInputFieldDefn : poInputDefn->GetFields()) |
478 | 0 | { |
479 | 0 | if (poRefDefn->GetFieldIndex(poInputFieldDefn->GetNameRef()) < 0) |
480 | 0 | { |
481 | 0 | aosReport.push_back(CPLSPrintf("%sInput layer has field %s, which " |
482 | 0 | "is absent in reference layer", |
483 | 0 | osLayerCtxt.c_str(), |
484 | 0 | poInputFieldDefn->GetNameRef())); |
485 | 0 | } |
486 | 0 | } |
487 | | |
488 | | // Compare geometry field definitions |
489 | 0 | const int nRefGeomFieldCount = poRefDefn->GetGeomFieldCount(); |
490 | 0 | const int nInputGeomFieldCount = poInputDefn->GetGeomFieldCount(); |
491 | 0 | if (nRefGeomFieldCount != nInputGeomFieldCount) |
492 | 0 | { |
493 | 0 | aosReport.push_back(CPLSPrintf("%sReference layer has %d geometry " |
494 | 0 | "field(s), whereas input layer has %d", |
495 | 0 | osLayerCtxt.c_str(), nRefGeomFieldCount, |
496 | 0 | nInputGeomFieldCount)); |
497 | 0 | } |
498 | |
|
499 | 0 | const bool bSingleGeomField = |
500 | 0 | nRefGeomFieldCount == 1 && nInputGeomFieldCount == 1; |
501 | |
|
502 | 0 | std::vector<int> anMapRefToInputGeomFields; |
503 | 0 | for (const auto *poRefFieldDefn : poRefDefn->GetGeomFields()) |
504 | 0 | { |
505 | 0 | const int nInputFieldDefnIdx = |
506 | 0 | bSingleGeomField |
507 | 0 | ? 0 |
508 | 0 | : poInputDefn->GetGeomFieldIndex(poRefFieldDefn->GetNameRef()); |
509 | 0 | anMapRefToInputGeomFields.push_back(nInputFieldDefnIdx); |
510 | 0 | if (nInputFieldDefnIdx >= 0) |
511 | 0 | { |
512 | 0 | const auto *poInputFieldDefn = |
513 | 0 | poInputDefn->GetGeomFieldDefn(nInputFieldDefnIdx); |
514 | |
|
515 | 0 | if (poRefFieldDefn->GetType() != poInputFieldDefn->GetType()) |
516 | 0 | { |
517 | 0 | aosReport.push_back(CPLSPrintf( |
518 | 0 | "%sGeometry field '%s' has geometry type %s in reference " |
519 | 0 | "layer, but %s in input layer", |
520 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
521 | 0 | OGRGeometryTypeToName(poRefFieldDefn->GetType()), |
522 | 0 | OGRGeometryTypeToName(poInputFieldDefn->GetType()))); |
523 | 0 | } |
524 | |
|
525 | 0 | if (poRefFieldDefn->IsNullable() != poInputFieldDefn->IsNullable()) |
526 | 0 | { |
527 | 0 | aosReport.push_back(CPLSPrintf( |
528 | 0 | "%sGeometry field '%s' has nullable=%d in reference layer, " |
529 | 0 | "but nullable=%d in input layer", |
530 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
531 | 0 | poRefFieldDefn->IsNullable(), |
532 | 0 | poInputFieldDefn->IsNullable())); |
533 | 0 | } |
534 | |
|
535 | 0 | if (!m_skipCRS) |
536 | 0 | { |
537 | 0 | const auto poRefSRS = poRefFieldDefn->GetSpatialRef(); |
538 | 0 | const auto poInputSRS = poInputFieldDefn->GetSpatialRef(); |
539 | 0 | if (((poRefSRS != nullptr) != (poInputSRS != nullptr)) || |
540 | 0 | (poRefSRS && poInputSRS && !poRefSRS->IsSame(poInputSRS))) |
541 | 0 | { |
542 | 0 | const char *apszOptions[] = {"FORMAT=WKT2_2019", nullptr}; |
543 | 0 | aosReport.push_back(CPLSPrintf( |
544 | 0 | "%sGeometry field '%s' has different CRS in reference " |
545 | 0 | "and input layers. Value in reference layer is %s, " |
546 | 0 | "whereas it is %s in input layer", |
547 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef(), |
548 | 0 | poRefSRS ? poRefSRS->exportToWkt(apszOptions).c_str() |
549 | 0 | : "null", |
550 | 0 | poInputSRS |
551 | 0 | ? poInputSRS->exportToWkt(apszOptions).c_str() |
552 | 0 | : "null")); |
553 | 0 | } |
554 | 0 | } |
555 | 0 | } |
556 | 0 | else |
557 | 0 | { |
558 | 0 | aosReport.push_back( |
559 | 0 | CPLSPrintf("%sReference layer has geometry field '%s', which " |
560 | 0 | "is absent in input layer", |
561 | 0 | osLayerCtxt.c_str(), poRefFieldDefn->GetNameRef())); |
562 | 0 | } |
563 | 0 | } |
564 | |
|
565 | 0 | if (!bSingleGeomField) |
566 | 0 | { |
567 | 0 | for (const auto *poInputFieldDefn : poInputDefn->GetGeomFields()) |
568 | 0 | { |
569 | 0 | if (poRefDefn->GetGeomFieldIndex(poInputFieldDefn->GetNameRef()) < |
570 | 0 | 0) |
571 | 0 | { |
572 | 0 | aosReport.push_back(CPLSPrintf( |
573 | 0 | "%sInput layer has geometry field '%s', which is absent in " |
574 | 0 | "reference layer", |
575 | 0 | osLayerCtxt.c_str(), poInputFieldDefn->GetNameRef())); |
576 | 0 | } |
577 | 0 | } |
578 | 0 | } |
579 | |
|
580 | 0 | const GIntBig nRefFeatureCount = |
581 | 0 | pfnProgressFunc ? poRefLayer->GetFeatureCount() : -1; |
582 | 0 | const GIntBig nInputFeatureCount = |
583 | 0 | pfnProgressFunc ? poInputLayer->GetFeatureCount() : -1; |
584 | 0 | if (nRefFeatureCount != nInputFeatureCount && nRefFeatureCount != -1 && |
585 | 0 | nInputFeatureCount != -1) |
586 | 0 | { |
587 | 0 | aosReport.push_back(CPLSPrintf( |
588 | 0 | "%sReference layer has " CPL_FRMT_GIB |
589 | 0 | " feature(s), whereas input layer has " CPL_FRMT_GIB, |
590 | 0 | osLayerCtxt.c_str(), nRefFeatureCount, nInputFeatureCount)); |
591 | 0 | } |
592 | | |
593 | | // Compare features |
594 | 0 | poRefLayer->ResetReading(); |
595 | 0 | poInputLayer->ResetReading(); |
596 | |
|
597 | 0 | GIntBig nCount = 0; |
598 | 0 | while (true) |
599 | 0 | { |
600 | 0 | auto poRefFeature = |
601 | 0 | std::unique_ptr<OGRFeature>(poRefLayer->GetNextFeature()); |
602 | 0 | auto poInputFeature = |
603 | 0 | std::unique_ptr<OGRFeature>(poInputLayer->GetNextFeature()); |
604 | 0 | if (!poRefFeature) |
605 | 0 | { |
606 | 0 | if (poInputFeature && |
607 | 0 | (nRefFeatureCount < 0 || nInputFeatureCount < 0)) |
608 | 0 | { |
609 | 0 | GIntBig nExtraFeatures = 1; |
610 | 0 | while ( |
611 | 0 | std::unique_ptr<OGRFeature>(poInputLayer->GetNextFeature())) |
612 | 0 | { |
613 | 0 | nExtraFeatures++; |
614 | 0 | } |
615 | 0 | aosReport.push_back(CPLSPrintf( |
616 | 0 | "%sInput layer has " CPL_FRMT_GIB |
617 | 0 | " feature(s), whereas reference layer has " CPL_FRMT_GIB, |
618 | 0 | osLayerCtxt.c_str(), nCount + nExtraFeatures, nCount)); |
619 | 0 | } |
620 | 0 | break; |
621 | 0 | } |
622 | | |
623 | 0 | if (!poInputFeature) |
624 | 0 | { |
625 | 0 | if (nRefFeatureCount < 0 || nInputFeatureCount < 0) |
626 | 0 | { |
627 | 0 | GIntBig nExtraFeatures = 1; |
628 | 0 | while ( |
629 | 0 | std::unique_ptr<OGRFeature>(poRefLayer->GetNextFeature())) |
630 | 0 | { |
631 | 0 | nExtraFeatures++; |
632 | 0 | } |
633 | 0 | aosReport.push_back(CPLSPrintf( |
634 | 0 | "%sReference layer has " CPL_FRMT_GIB |
635 | 0 | " feature(s), whereas input layer has " CPL_FRMT_GIB, |
636 | 0 | osLayerCtxt.c_str(), nCount + nExtraFeatures, nCount)); |
637 | 0 | } |
638 | 0 | break; |
639 | 0 | } |
640 | | |
641 | 0 | if (!m_skipFID && poRefFeature->GetFID() != poInputFeature->GetFID()) |
642 | 0 | { |
643 | 0 | aosReport.push_back( |
644 | 0 | CPLSPrintf("%sFeature at index " CPL_FRMT_GIB |
645 | 0 | " has feature id " CPL_FRMT_GIB |
646 | 0 | " in reference layer, whereas it is " CPL_FRMT_GIB |
647 | 0 | " in input layer", |
648 | 0 | osLayerCtxt.c_str(), nCount, poRefFeature->GetFID(), |
649 | 0 | poInputFeature->GetFID())); |
650 | 0 | } |
651 | | |
652 | | // Compare attribute field values |
653 | 0 | for (int i = 0; i < nRefFieldCount; ++i) |
654 | 0 | { |
655 | 0 | const int j = anMapRefToInputFields[i]; |
656 | 0 | if (j >= 0) |
657 | 0 | { |
658 | 0 | if (!OGRFeature::IsSameFieldValue(poRefFeature.get(), i, |
659 | 0 | poInputFeature.get(), j)) |
660 | 0 | { |
661 | 0 | aosReport.push_back( |
662 | 0 | CPLSPrintf("%sFeature at index " CPL_FRMT_GIB |
663 | 0 | " has value '%s' for field %s in reference " |
664 | 0 | "layer, whereas it is '%s' in input layer", |
665 | 0 | osLayerCtxt.c_str(), nCount, |
666 | 0 | poRefFeature->GetFieldAsString(i), |
667 | 0 | poRefDefn->GetFieldDefn(i)->GetNameRef(), |
668 | 0 | poInputFeature->GetFieldAsString(j))); |
669 | 0 | } |
670 | 0 | } |
671 | 0 | } |
672 | | |
673 | | // Compare geometry field values |
674 | 0 | for (int i = 0; i < nRefGeomFieldCount; ++i) |
675 | 0 | { |
676 | 0 | const int j = anMapRefToInputGeomFields[i]; |
677 | 0 | if (j >= 0) |
678 | 0 | { |
679 | 0 | const auto poRefGeom = poRefFeature->GetGeomFieldRef(i); |
680 | 0 | const auto poInputGeom = poInputFeature->GetGeomFieldRef(j); |
681 | 0 | if ((poRefGeom != nullptr) != (poInputGeom != nullptr) || |
682 | 0 | (poRefGeom && poInputGeom && |
683 | 0 | !poRefGeom->Equals(poInputGeom))) |
684 | 0 | { |
685 | 0 | bool bSame = false; |
686 | 0 | if (poRefGeom && poInputGeom && m_laxGeometryComparison) |
687 | 0 | { |
688 | 0 | if (wkbFlatten(OGR_GT_GetCollection( |
689 | 0 | poRefGeom->getGeometryType())) == |
690 | 0 | poInputGeom->getGeometryType()) |
691 | 0 | { |
692 | 0 | auto poNewGeom = OGRGeometryFactory::forceTo( |
693 | 0 | std::unique_ptr<OGRGeometry>( |
694 | 0 | poRefGeom->clone()), |
695 | 0 | poInputGeom->getGeometryType()); |
696 | 0 | bSame = poNewGeom && poNewGeom->Equals(poInputGeom); |
697 | 0 | } |
698 | 0 | else if (wkbFlatten(OGR_GT_GetCollection( |
699 | 0 | poInputGeom->getGeometryType())) == |
700 | 0 | poRefGeom->getGeometryType()) |
701 | 0 | { |
702 | 0 | auto poNewGeom = OGRGeometryFactory::forceTo( |
703 | 0 | std::unique_ptr<OGRGeometry>( |
704 | 0 | poInputGeom->clone()), |
705 | 0 | poRefGeom->getGeometryType()); |
706 | 0 | bSame = poNewGeom && poNewGeom->Equals(poRefGeom); |
707 | 0 | } |
708 | 0 | } |
709 | 0 | if (!bSame) |
710 | 0 | { |
711 | 0 | aosReport.push_back(CPLSPrintf( |
712 | 0 | "%sFeature at index " CPL_FRMT_GIB |
713 | 0 | " has value %s for geometry field '%s' in " |
714 | 0 | "reference layer, whereas it is %s in input layer", |
715 | 0 | osLayerCtxt.c_str(), nCount, |
716 | 0 | poRefGeom ? poRefGeom->exportToWkt().c_str() |
717 | 0 | : "null", |
718 | 0 | poRefDefn->GetGeomFieldDefn(i)->GetNameRef(), |
719 | 0 | poInputGeom ? poInputGeom->exportToWkt().c_str() |
720 | 0 | : "null")); |
721 | 0 | } |
722 | 0 | } |
723 | 0 | } |
724 | 0 | } |
725 | |
|
726 | 0 | if (pfnProgressFunc && nRefFeatureCount > 0 && |
727 | 0 | (nCount < 1000 || (nCount % 128) == 0) && |
728 | 0 | !pfnProgressFunc(static_cast<double>(nCount) / |
729 | 0 | static_cast<double>(nRefFeatureCount), |
730 | 0 | "", pProgressData)) |
731 | 0 | { |
732 | 0 | ReportError(CE_Failure, CPLE_UserInterrupt, "Interrupted by user"); |
733 | 0 | return false; |
734 | 0 | } |
735 | | |
736 | 0 | ++nCount; |
737 | 0 | } |
738 | | |
739 | 0 | if (pfnProgressFunc && !pfnProgressFunc(1.0, "", pProgressData)) |
740 | 0 | { |
741 | 0 | ReportError(CE_Failure, CPLE_UserInterrupt, "Interrupted by user"); |
742 | 0 | return false; |
743 | 0 | } |
744 | 0 | return true; |
745 | 0 | } |
746 | | |
747 | | /************************************************************************/ |
748 | | /* ~GDALVectorCompareAlgorithmStandalone() */ |
749 | | /************************************************************************/ |
750 | | |
751 | 0 | GDALVectorCompareAlgorithmStandalone::~GDALVectorCompareAlgorithmStandalone() = |
752 | | default; |
753 | | |
754 | | //! @endcond |