Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: S-101 driver
4 : * Purpose: Implements OGRS101Reader
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 "ogr_s101.h"
14 : #include "ogrs101readerconstants.h"
15 :
16 : #include <memory>
17 :
18 : /************************************************************************/
19 : /* GetMultiPointLayerName() */
20 : /************************************************************************/
21 :
22 : /* static */ std::string
23 306 : OGRS101Reader::GetMultiPointLayerName(const OGRSpatialReference &oSRS)
24 : {
25 306 : return oSRS.GetAxesCount() == 2
26 : ? "MultiPoint2D"
27 918 : : CPLSPrintf("MultiPoint3D_%s", LaunderCRSName(oSRS).c_str());
28 : }
29 :
30 : /************************************************************************/
31 : /* CreateMultiPointFeatureDefns() */
32 : /************************************************************************/
33 :
34 : /** Create the feature definition(s) for the MultiPoint layer(s)
35 : *
36 : * There is a layer per CRS used by multipoints.
37 : */
38 347 : bool OGRS101Reader::CreateMultiPointFeatureDefns()
39 : {
40 347 : bool bError = false;
41 :
42 : m_oMapCRSIdToMultiPointRecordIdx =
43 347 : CreateMapCRSIdToRecordIdxForMultiPoints(bError);
44 347 : if (bError)
45 2 : return false;
46 426 : for (const auto &[nCRSId, anRecordIdx] : m_oMapCRSIdToMultiPointRecordIdx)
47 : {
48 81 : const auto &oSRS = m_oMapSRS[nCRSId];
49 81 : const bool bIs2D = nCRSId == HORIZONTAL_CRS_ID;
50 : auto poFDefn = OGRFeatureDefnRefCountedPtr::makeInstance(
51 81 : GetMultiPointLayerName(oSRS).c_str());
52 81 : poFDefn->SetGeomType(bIs2D ? wkbMultiPoint : wkbMultiPoint25D);
53 162 : poFDefn->GetGeomFieldDefn(0)->SetSpatialRef(
54 162 : OGRSpatialReferenceRefCountedPtr::makeClone(&oSRS).get());
55 81 : poFDefn->GetGeomFieldDefn(0)->SetCoordinatePrecision(
56 81 : m_coordinatePrecision);
57 : {
58 162 : OGRFieldDefn oFieldDefn(OGR_FIELD_NAME_RECORD_ID, OFTInteger);
59 81 : poFDefn->AddFieldDefn(&oFieldDefn);
60 : }
61 : {
62 162 : OGRFieldDefn oFieldDefn(OGR_FIELD_NAME_RECORD_VERSION, OFTInteger);
63 81 : poFDefn->AddFieldDefn(&oFieldDefn);
64 : }
65 81 : if (!InferFeatureDefn(m_oMultiPointRecordIndex, MRID_FIELD, INAS_FIELD,
66 81 : anRecordIdx, *poFDefn, m_oMapFieldDomains))
67 : {
68 0 : return false;
69 : }
70 81 : m_oMapMultiPointFeatureDefn[nCRSId] = std::move(poFDefn);
71 : }
72 :
73 345 : return true;
74 : }
75 :
76 : /************************************************************************/
77 : /* GetCRSIdForMultiPointRecord() */
78 : /************************************************************************/
79 :
80 : /** Return the CRS id for a given MultiPoint record, or INVALID_CRS_ID on error */
81 : OGRS101Reader::CRSId
82 162 : OGRS101Reader::GetCRSIdForMultiPointRecord(const DDFRecord *poRecord,
83 : int iRecord, int nRecordID) const
84 : {
85 162 : if (nRecordID < 0)
86 96 : nRecordID = poRecord->GetIntSubfield(MRID_FIELD, 0, RCID_SUBFIELD, 0);
87 :
88 10 : const auto GetErrorContext = [iRecord, nRecordID]()
89 : {
90 5 : if (iRecord >= 0)
91 5 : return CPLSPrintf("Record index=%d of MRID", iRecord);
92 : else
93 0 : return CPLSPrintf("Record ID=%d of MRID", nRecordID);
94 162 : };
95 :
96 162 : if (poRecord->FindField(C3IL_FIELD))
97 : {
98 : const CRSId nVCID =
99 23 : poRecord->GetIntSubfield(C3IL_FIELD, 0, VCID_SUBFIELD, 0);
100 23 : if (nVCID == HORIZONTAL_CRS_ID)
101 : {
102 2 : CPL_IGNORE_RET_VAL(EMIT_ERROR_OR_WARNING(
103 : CPLSPrintf("%s: VCID subfield = %d of C3IL "
104 : "field points to a non-3D CRS.",
105 : GetErrorContext(), static_cast<int>(nVCID))));
106 2 : return INVALID_CRS_ID;
107 : }
108 21 : else if (!cpl::contains(m_oMapSRS, nVCID))
109 : {
110 2 : CPL_IGNORE_RET_VAL(EMIT_ERROR_OR_WARNING(
111 : CPLSPrintf("%s: Unknown value %d for VCID subfield of C3IL "
112 : "field.",
113 : GetErrorContext(), static_cast<int>(nVCID))));
114 2 : return INVALID_CRS_ID;
115 : }
116 : else
117 : {
118 19 : return nVCID;
119 : }
120 : }
121 139 : else if (poRecord->FindField(C2IL_FIELD))
122 : {
123 138 : return HORIZONTAL_CRS_ID;
124 : }
125 : else
126 : {
127 1 : CPL_IGNORE_RET_VAL(EMIT_ERROR_OR_WARNING(
128 : CPLSPrintf("%s: No C2IL or C3IL field found.", GetErrorContext())));
129 1 : return INVALID_CRS_ID;
130 : }
131 : }
132 :
133 : /************************************************************************/
134 : /* CreateMapCRSIdToRecordIdxForMultiPoints() */
135 : /************************************************************************/
136 :
137 : /** Browse through m_oMultiPointRecordIndex to identify which record belongs to
138 : * each CRS and create a map from each CRS id to the record indices that use
139 : * it.
140 : */
141 : std::map<OGRS101Reader::CRSId, std::vector<int>>
142 347 : OGRS101Reader::CreateMapCRSIdToRecordIdxForMultiPoints(bool &bError) const
143 : {
144 694 : std::map<OGRS101Reader::CRSId, std::vector<int>> map;
145 :
146 347 : const int nRecords = m_oMultiPointRecordIndex.GetCount();
147 441 : for (int iRecord = 0; iRecord < nRecords; ++iRecord)
148 : {
149 96 : const auto poRecord = m_oMultiPointRecordIndex.GetByIndex(iRecord);
150 : const CRSId nCRSId = GetCRSIdForMultiPointRecord(poRecord, iRecord,
151 96 : /* nRecordID = */ -1);
152 96 : if (nCRSId == INVALID_CRS_ID)
153 : {
154 5 : if (m_bStrict)
155 : {
156 2 : bError = true;
157 2 : return {};
158 : }
159 : }
160 : else
161 : {
162 91 : map[nCRSId].push_back(iRecord);
163 : }
164 : }
165 :
166 345 : return map;
167 : }
168 :
169 : /************************************************************************/
170 : /* ReadMultiPointGeometry() */
171 : /************************************************************************/
172 :
173 : std::unique_ptr<OGRMultiPoint>
174 512 : OGRS101Reader::ReadMultiPointGeometry(const DDFRecord *poRecord, int iRecord,
175 : int nRecordID,
176 : const OGRSpatialReference *poSRS) const
177 : {
178 512 : const bool bIs3D = poRecord->FindField(C3IL_FIELD) != nullptr;
179 512 : const char *pszCoordFieldName = bIs3D ? C3IL_FIELD : C2IL_FIELD;
180 1024 : const auto apoCoordFields = poRecord->GetFields(pszCoordFieldName);
181 512 : if (apoCoordFields.empty())
182 0 : return nullptr;
183 :
184 1024 : auto poMP = std::make_unique<OGRMultiPoint>();
185 512 : poMP->assignSpatialReference(poSRS);
186 1022 : for (const auto *poCoordField : apoCoordFields)
187 : {
188 : const int nCoordCount =
189 128 : bIs3D && poCoordField->GetParts().size() == 2
190 640 : ? poCoordField->GetParts()[1]->GetRepeatCount()
191 384 : : poCoordField->GetRepeatCount();
192 :
193 1543 : for (int iPnt = 0; iPnt < nCoordCount; ++iPnt)
194 : {
195 : auto poPoint = ReadPointGeometryInternal(
196 : poRecord, iRecord, nRecordID, iPnt, poSRS, bIs3D, poCoordField,
197 1033 : MRID_FIELD);
198 1033 : if (!poPoint)
199 2 : return nullptr;
200 1031 : poMP->addGeometry(std::move(poPoint));
201 : }
202 : }
203 510 : return poMP;
204 : }
205 :
206 : /************************************************************************/
207 : /* FillFeatureMultiPoint() */
208 : /************************************************************************/
209 :
210 : /** Fill the content of the provided feature from the identified record
211 : * (of m_oMultiPointRecordIndex).
212 : */
213 348 : bool OGRS101Reader::FillFeatureMultiPoint(const DDFRecordIndex &oIndex,
214 : int iRecord,
215 : OGRFeature &oFeature) const
216 : {
217 348 : const auto poRecord = oIndex.GetByIndex(iRecord);
218 348 : CPLAssert(poRecord);
219 :
220 : const OGRSpatialReference *poSRS =
221 348 : oFeature.GetDefnRef()->GetGeomFieldDefn(0)->GetSpatialRef();
222 : auto poMP =
223 696 : ReadMultiPointGeometry(poRecord, iRecord, /* nRecordID = */ -1, poSRS);
224 348 : if (poMP)
225 : {
226 346 : oFeature.SetGeometry(std::move(poMP));
227 : }
228 2 : else if (m_bStrict)
229 2 : return false;
230 :
231 692 : return FillFeatureAttributes(oIndex, iRecord, INAS_FIELD, oFeature) &&
232 346 : FillFeatureWithNonAttrAssocSubfields(poRecord, iRecord, INAS_FIELD,
233 346 : oFeature);
234 : }
235 :
236 : /************************************************************************/
237 : /* ProcessUpdateRecordMultiPoint() */
238 : /************************************************************************/
239 :
240 : /** Updates the geometry part of poTargetRecord with poUpdateRecord */
241 33 : bool OGRS101Reader::ProcessUpdateRecordMultiPoint(
242 : const DDFRecord *poUpdateRecord, DDFRecord *poTargetRecord) const
243 : {
244 33 : return ProcessUpdatePointList(poUpdateRecord, poTargetRecord,
245 33 : /* bIs3DAllowed = */ true);
246 : }
247 :
248 : /************************************************************************/
249 : /* ProcessUpdatePointList() */
250 : /************************************************************************/
251 :
252 : /** Updates the point list of poTargetRecord with poUpdateRecord */
253 36 : bool OGRS101Reader::ProcessUpdatePointList(const DDFRecord *poUpdateRecord,
254 : DDFRecord *poTargetRecord,
255 : bool bIs3DAllowed) const
256 : {
257 36 : const auto poIDField = poUpdateRecord->GetField(0);
258 36 : CPLAssert(poIDField);
259 :
260 : // Record name
261 : const RecordName nRCNM =
262 36 : poUpdateRecord->GetIntSubfield(poIDField, RCNM_SUBFIELD, 0);
263 :
264 : // Record identifier
265 : const int nRCID =
266 36 : poUpdateRecord->GetIntSubfield(poIDField, RCID_SUBFIELD, 0);
267 :
268 : // Coordinate Control field
269 36 : const auto poControlField = poUpdateRecord->FindField(COCC_FIELD);
270 36 : if (!poControlField)
271 0 : return true;
272 :
273 : // Number of Coordinates
274 36 : constexpr const char *NCOR_SUBFIELD = "NCOR";
275 : const int nNCOR =
276 36 : poUpdateRecord->GetIntSubfield(poControlField, NCOR_SUBFIELD, 0);
277 :
278 : // Coordinate Update Instruction
279 36 : constexpr const char *COUI_SUBFIELD = "COUI";
280 : const int nCOUI =
281 36 : poUpdateRecord->GetIntSubfield(poControlField, COUI_SUBFIELD, 0);
282 :
283 36 : bool bIs3D = false;
284 36 : const DDFField *poUpdateField = nullptr;
285 36 : DDFField *poTargetField = nullptr;
286 36 : if (nCOUI == INSTRUCTION_DELETE)
287 : {
288 11 : if ((poTargetField = poTargetRecord->FindField(C2IL_FIELD)) != nullptr)
289 : {
290 11 : poUpdateField = poUpdateRecord->FindField(C2IL_FIELD);
291 : }
292 0 : else if (bIs3DAllowed && (poTargetField = poTargetRecord->FindField(
293 : C3IL_FIELD)) != nullptr)
294 : {
295 0 : poUpdateField = poUpdateRecord->FindField(C3IL_FIELD);
296 0 : bIs3D = true;
297 : }
298 : else
299 : {
300 0 : return EMIT_ERROR_OR_WARNING(
301 : CPLSPrintf("%s, RCNM=%d, RCID=%d: missing %s field in "
302 : "target record",
303 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID,
304 : bIs3DAllowed ? "C2IL / C3IL" : "C2IL"));
305 : }
306 : }
307 25 : else if ((poUpdateField = poUpdateRecord->FindField(C2IL_FIELD)) != nullptr)
308 : {
309 23 : if ((poTargetField = poTargetRecord->FindField(C2IL_FIELD)) == nullptr)
310 : {
311 0 : return EMIT_ERROR_OR_WARNING(CPLSPrintf(
312 : "%s, RCNM=%d, RCID=%d: cannot find C2IL field in target "
313 : "record",
314 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID));
315 : }
316 : }
317 4 : else if (bIs3DAllowed &&
318 2 : (poUpdateField = poUpdateRecord->FindField(C3IL_FIELD)) != nullptr)
319 : {
320 2 : if ((poTargetField = poTargetRecord->FindField(C3IL_FIELD)) == nullptr)
321 : {
322 0 : return EMIT_ERROR_OR_WARNING(CPLSPrintf(
323 : "%s, RCNM=%d, RCID=%d: cannot find C3IL field in target "
324 : "record",
325 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID));
326 : }
327 2 : bIs3D = true;
328 : }
329 : else
330 : {
331 0 : return EMIT_ERROR_OR_WARNING(
332 : CPLSPrintf("%s, RCNM=%d, RCID=%d: missing %s field in update "
333 : "record",
334 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID,
335 : bIs3DAllowed ? "C2IL / C3IL" : "C2IL"));
336 : }
337 :
338 36 : const char *pszCoordFieldName = bIs3D ? C3IL_FIELD : C2IL_FIELD;
339 108 : if ((poUpdateField && poUpdateRecord->GetFields(C2IL_FIELD).size() > 1) ||
340 72 : poTargetRecord->GetFields(C2IL_FIELD).size() > 1)
341 : {
342 0 : return EMIT_ERROR_OR_WARNING(
343 : CPLSPrintf("%s: only one instance of %s supported for update",
344 : m_osFilename.c_str(), pszCoordFieldName));
345 : }
346 :
347 : // Coordinate Index (1-based)
348 36 : constexpr const char *COIX_SUBFIELD = "COIX";
349 : const int nCOIX =
350 36 : poUpdateRecord->GetIntSubfield(poControlField, COIX_SUBFIELD, 0);
351 :
352 : const int nTargetRepeatCount =
353 2 : (bIs3D && poTargetField->GetParts().size() == 2)
354 38 : ? poTargetField->GetParts()[1]->GetRepeatCount()
355 34 : : poTargetField->GetRepeatCount();
356 :
357 : // Check that start index and count is consistent with update and
358 : // target list of points
359 36 : const int nMaxCOIXAllowed =
360 36 : nTargetRepeatCount + (nCOUI == INSTRUCTION_INSERT ? 1 : 0);
361 36 : if (nCOIX <= 0 || nCOIX > nMaxCOIXAllowed)
362 : {
363 4 : return EMIT_ERROR_OR_WARNING(CPLSPrintf(
364 : "%s, RCNM=%d, RCID=%d: invalid COIX = %d. Must be in [1,%d].",
365 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID, nCOIX,
366 : nMaxCOIXAllowed));
367 : }
368 :
369 : const int nUpdateRepeatCount =
370 55 : !poUpdateField ? 0
371 2 : : (bIs3D && poUpdateField->GetParts().size() == 2)
372 25 : ? poUpdateField->GetParts()[1]->GetRepeatCount()
373 21 : : poUpdateField->GetRepeatCount();
374 :
375 32 : if (poUpdateField && nNCOR != nUpdateRepeatCount)
376 : {
377 2 : return EMIT_ERROR_OR_WARNING(
378 : CPLSPrintf("%s, RCNM=%d, RCID=%d: invalid NCOR = %d. Expected %d",
379 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID,
380 : nNCOR, nUpdateRepeatCount));
381 : }
382 32 : else if (poUpdateField && nCOUI == INSTRUCTION_DELETE &&
383 2 : !EMIT_ERROR_OR_WARNING(
384 : CPLSPrintf("%s, RCNM=%d, RCID=%d: unexpected %s field in "
385 : "update record in COUI = %d (delete) mode",
386 : m_osFilename.c_str(), static_cast<int>(nRCNM),
387 : nRCID, pszCoordFieldName, nCOUI)))
388 : {
389 1 : return false;
390 : }
391 :
392 29 : if (nCOUI == INSTRUCTION_UPDATE || nCOUI == INSTRUCTION_DELETE)
393 : {
394 23 : if (nCOIX + nNCOR > nTargetRepeatCount + 1)
395 : {
396 3 : return EMIT_ERROR_OR_WARNING(CPLSPrintf(
397 : "%s, RCNM=%d, RCID=%d: invalid COIX = %d and/or NCOR=%d",
398 : m_osFilename.c_str(), static_cast<int>(nRCNM), nRCID, nCOIX,
399 : nNCOR));
400 : }
401 : }
402 6 : else if (nCOUI != INSTRUCTION_INSERT)
403 : {
404 2 : return EMIT_ERROR_OR_WARNING(CPLSPrintf(
405 : "%s, RCNM=%d, RCID=%d: invalid COUI = %d", m_osFilename.c_str(),
406 : static_cast<int>(nRCNM), nRCID, nCOUI));
407 : }
408 :
409 : struct Coord
410 : {
411 : int Y = 0;
412 : int X = 0;
413 : int Z = 0;
414 :
415 90 : void Read(const DDFRecord *poRecord, const DDFField *poField, int i,
416 : bool bIs3D)
417 : {
418 90 : Y = poRecord->GetIntSubfield(poField, YCOO_SUBFIELD, i);
419 90 : X = poRecord->GetIntSubfield(poField, XCOO_SUBFIELD, i);
420 90 : if (bIs3D)
421 6 : Z = poRecord->GetIntSubfield(poField, ZCOO_SUBFIELD, i);
422 90 : }
423 : };
424 :
425 48 : std::vector<Coord> asTarget;
426 : // Ingest the existing/target record
427 83 : for (int i = 0; i < nTargetRepeatCount; ++i)
428 : {
429 59 : Coord c;
430 59 : c.Read(poTargetRecord, poTargetField, i, bIs3D);
431 59 : asTarget.push_back(c);
432 : }
433 :
434 : // Apply the update record
435 48 : std::vector<Coord> asUpdate;
436 24 : if (poUpdateField)
437 : {
438 48 : for (int i = 0; i < nUpdateRepeatCount; ++i)
439 : {
440 31 : Coord c;
441 31 : c.Read(poUpdateRecord, poUpdateField, i, bIs3D);
442 31 : asUpdate.push_back(c);
443 : }
444 : }
445 :
446 24 : const auto oTargetBeginIter = asTarget.begin() + (nCOIX - 1);
447 24 : if (nCOUI == INSTRUCTION_INSERT)
448 : {
449 4 : asTarget.insert(oTargetBeginIter, asUpdate.begin(), asUpdate.end());
450 : }
451 20 : else if (nCOUI == INSTRUCTION_UPDATE)
452 : {
453 13 : std::copy(asUpdate.begin(), asUpdate.end(), oTargetBeginIter);
454 : }
455 : else
456 : {
457 7 : CPLAssert(nCOUI == INSTRUCTION_DELETE);
458 7 : asTarget.erase(oTargetBeginIter, oTargetBeginIter + nNCOR);
459 : }
460 :
461 : // Compose raw target field
462 24 : std::string s;
463 24 : if (bIs3D)
464 : {
465 2 : const DDFRecord *poRecord =
466 2 : poUpdateRecord ? poUpdateRecord : poTargetRecord;
467 2 : const DDFField *poField = poUpdateField ? poUpdateField : poTargetField;
468 2 : AppendUInt8(s, static_cast<uint8_t>(poRecord->GetIntSubfield(
469 : poField, VCID_SUBFIELD, 0)));
470 : }
471 :
472 88 : for (const auto &c : asTarget)
473 : {
474 64 : AppendInt32(s, c.Y);
475 64 : AppendInt32(s, c.X);
476 64 : if (bIs3D)
477 4 : AppendInt32(s, c.Z);
478 : }
479 24 : AppendUInt8(s, DDF_FIELD_TERMINATOR);
480 :
481 24 : poTargetRecord->SetFieldRaw(poTargetField, s.data(),
482 24 : static_cast<int>(s.size()));
483 :
484 24 : return true;
485 : }
|