1919import logging
2020import sys
2121from pathlib import Path
22- from typing import Any , Optional
22+ from typing import Any , List , Optional
2323
2424
2525# Run $env:PYTHONPATH="src" if it fails to be executed from the project root.
@@ -76,15 +76,23 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
7676 """
7777 Walk from a representation object to its CRS document.
7878
79- Tries the common path ``local_crs`` (or nested in ``grid2d_patch.geometry``
80- for v2.0.1 ``Grid2DRepresentation``). Returns the resolved CRS object or
81- ``None``.
79+ The CRS DOR is always present in ``LocalCrs`` — the difference between
80+ RESQML versions is the depth of the path:
81+
82+ * **v2.2** : ``Geometry.LocalCrs`` (``PointGeometry`` sits directly on the object)
83+ * **v2.0.1**: ``Grid2dPatch.Geometry.LocalCrs`` (geometry is inside a patch sub-object)
84+
85+ ``get_object_attribute_rgx`` resolves dot-delimited paths at exactly the
86+ depth specified, so we try the shallower v2.2 path first, then fall back
87+ to the deeper v2.0.1 path.
88+
89+ Returns the resolved CRS object or ``None``.
8290 """
83- # Direct attribute ( v2.0.1 and many v2.2 envelopes )
84- dor = get_object_attribute_rgx (grid_obj , "[Ll]ocal[_]?[Cc]rs" )
91+ # v2.2: Geometry.LocalCrs (PointGeometry directly on the object )
92+ dor = get_object_attribute_rgx (grid_obj , "[Gg]eometry.[ Ll]ocal[_]?[Cc]rs" )
8593
8694 if dor is None :
87- # Nested path used by Grid2DRepresentation v2.0.1
95+ # v2.0.1: Grid2dPatch.Geometry.LocalCrs (geometry wrapped in a patch)
8896 dor = get_object_attribute_rgx (
8997 grid_obj ,
9098 "[Gg]rid2[Dd][Pp]atch.[Gg]eometry.[Ll]ocal[_]?[Cc]rs" ,
@@ -101,6 +109,31 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
101109 return candidates [0 ] if candidates else None
102110
103111
112+ def resolve_crs_from_triangulated_set (triangulated_obj : Any , epc : Epc ) -> List [Optional [Any ]]:
113+ """
114+ Walk from a TriangulatedSetRepresentation to its CRS document.
115+
116+ Each patch of a TriangulatedSetRepresentation may reference a CRS via its
117+ ``local_crs`` attribute. This function tries to resolve the first patch's CRS.
118+ """
119+ dor = get_object_attribute_rgx (triangulated_obj , "triangle_patch.\d+.geometry.local_crs" )
120+ # print(f" Found DOR for TriangulatedSetRepresentation patch CRS: {dor}")
121+ if dor is None :
122+ return []
123+
124+ if isinstance (dor , list ):
125+ candidates = []
126+ for d in dor :
127+ uuid = get_obj_uuid (d )
128+ if uuid :
129+ obj_candidates = epc .get_object_by_uuid (uuid )
130+ print (f" Found DOR for TriangulatedSetRepresentation patch CRS: { d } → candidates: { len (obj_candidates )} " )
131+ candidates .append (obj_candidates [0 ] if obj_candidates else None )
132+ return candidates
133+
134+ return None
135+
136+
104137# ===========================================================================
105138# RESQML v2.0.1 — testingPackageCpp.epc
106139# ===========================================================================
@@ -124,7 +157,9 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
124157check ("z_offset" , 15.0 , info .z_offset , approx = True )
125158check ("projected_uom (raw from xsdata enum)" , "M" , info .projected_uom )
126159check ("vertical_uom (raw from xsdata enum)" , "M" , info .vertical_uom )
127- check ("z_increasing_downward" , False , info .z_increasing_downward )
160+ # ZIncreasingDownward=true in the file; VerticalUnknownCrs sub-object carries
161+ # no direction field, so the sentinel correctly preserves the top-level value.
162+ check ("z_increasing_downward" , True , info .z_increasing_downward )
128163check ("areal_rotation_value" , 0.0 , info .areal_rotation_value , approx = True )
129164check ("projected_epsg_code" , None , info .projected_epsg_code )
130165check ("vertical_epsg_code" , None , info .vertical_epsg_code )
@@ -141,9 +176,9 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
141176check ("projected_epsg_code" , 23031 , info .projected_epsg_code )
142177check ("projected_uom" , "M" , info .projected_uom )
143178check ("vertical_uom" , "M" , info .vertical_uom )
144- # This particular depth CRS has z_increasing_downward=False in the file
145- # (the VerticalCrs it references has no direction set or direction=up)
146- check ("z_increasing_downward" , False , info .z_increasing_downward )
179+ # ZIncreasingDownward=true in the raw file; the linked VerticalUnknownCrs
180+ # carries no direction field, so the sentinel correctly preserves the value.
181+ check ("z_increasing_downward" , True , info .z_increasing_downward )
147182check ("x_offset" , 0.0 , info .x_offset , approx = True )
148183check ("y_offset" , 0.0 , info .y_offset , approx = True )
149184check ("z_offset" , 0.0 , info .z_offset , approx = True )
@@ -215,14 +250,17 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
215250 info = extract_crs_info (resolved_crs , workspace = epc20 )
216251 check (" projected_epsg_code" , 23031 , info .projected_epsg_code )
217252 check (" projected_uom" , "M" , info .projected_uom )
218- check (" z_increasing_downward" , False , info .z_increasing_downward )
253+ # Same LocalDepth3DCrs — ZIncreasingDownward=true in the raw file.
254+ check (" z_increasing_downward" , True , info .z_increasing_downward )
219255
220256# Grid 4e56b0e4 → also LocalDepth3DCrs (same uuid)
221257grid_depth2 = epc20 .get_object_by_uuid ("4e56b0e4-2cd1-4efa-97dd-95f72bcf9f80" )[0 ]
222258resolved_crs = _resolve_crs_from_grid (grid_depth2 , epc20 )
223259check ("Grid 4e56b0e4 resolved CRS uuid" , "0ae56ef3-fc79-405b-8deb-6942e0f2e77c" ,
224260 getattr (resolved_crs , "uuid" , None ))
225261
262+
263+
226264# ===========================================================================
227265# RESQML v2.2 / EML v2.3 — testingPackageCpp22.epc
228266# ===========================================================================
@@ -321,21 +359,19 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
321359check ("z_increasing_downward" , True , info .z_increasing_downward )
322360
323361# ── Grid2D v2.2 — CRS note ────────────────────────────────────────────────
324- section ("v2.2 · Grid2DRepresentation — CRS resolution note " )
362+ section ("v2.2 · Grid2DRepresentation — CRS resolution" )
325363print ("""
326- In RESQML v2.2, Grid2DRepresentation does not embed a local_crs DOR
327- in its geometry patch the same way v2.0.1 does. Instead the CRS is
328- referenced via the containing LocalEngineeringCompoundCrs or through
329- the representation's own schema-level CRS association.
330-
331- The canonical way to obtain CRS info from a v2.2 representation is to:
332- 1. Retrieve all LocalEngineeringCompoundCrs objects from the EPC.
333- 2. Match the one that logically covers your representation (by
334- consulting interpretation / framework associations).
335- or use the helper ``get_crs_obj(repr_obj, workspace=epc)`` which walks
336- the object hierarchy.
337-
338- Direct example — iterate all compound CRS objects and extract info:
364+ In RESQML v2.2, Grid2DRepresentation DOES embed a LocalCrs DOR, but at
365+ a shallower path than v2.0.1:
366+
367+ v2.2 : Geometry.LocalCrs (PointGeometry sits directly on the object)
368+ v2.0.1: Grid2dPatch.Geometry.LocalCrs (geometry is wrapped in a patch sub-object)
369+
370+ Both paths are resolved by trying the shallower v2.2 path first with
371+ ``get_object_attribute_rgx``, then falling back to the deeper v2.0.1 path.
372+ No indirect lookup through framework associations is needed.
373+
374+ All LocalEngineeringCompoundCrs objects in this EPC:
339375""" )
340376
341377for obj in epc22 .energyml_objects :
@@ -346,6 +382,51 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
346382 print (f" vertical_uom={ info .vertical_uom } z_down={ info .z_increasing_downward } " )
347383 print (f" offsets: x={ info .x_offset } y={ info .y_offset } z={ info .z_offset } " )
348384
385+ # ── Grid2DRepresentation v2.2 → CRS via Geometry.LocalCrs ─────────────────
386+
387+ section ("v2.2 · Grid2DRepresentation (uuid 4e56b0e4) → CRS via Geometry.LocalCrs" )
388+
389+ grid22 = epc22 .get_object_by_uuid ("4e56b0e4-2cd1-4efa-97dd-95f72bcf9f80" )
390+ if grid22 :
391+ grid22 = grid22 [0 ]
392+ resolved_crs22 = _resolve_crs_from_grid (grid22 , epc22 )
393+ check ("resolved CRS type" , "LocalEngineeringCompoundCrs" ,
394+ type (resolved_crs22 ).__name__ if resolved_crs22 else None )
395+ check ("resolved CRS uuid" , "6a18c177-93be-41ac-9084-f84bbb31f46d" ,
396+ getattr (resolved_crs22 , "uuid" , None ))
397+ if resolved_crs22 :
398+ info = extract_crs_info (resolved_crs22 , workspace = epc22 )
399+ check (" projected_epsg_code" , 23031 , info .projected_epsg_code )
400+ check (" projected_uom" , "M" , info .projected_uom )
401+ check (" vertical_uom" , "M" , info .vertical_uom )
402+ check (" z_increasing_downward" , True , info .z_increasing_downward )
403+ check (" x_offset" , 0.0 , info .x_offset , approx = True )
404+ check (" y_offset" , 0.0 , info .y_offset , approx = True )
405+ check (" z_offset" , 0.0 , info .z_offset , approx = True )
406+ else :
407+ print (" [SKIP] Grid 4e56b0e4 not found in testingPackageCpp22.epc" )
408+
409+
410+ # TriangulatedSetRepresentation 1a4112fa → LocalEngineeringCompoundCrs (6a18c177)
411+ triangulated_set = epc22 .get_object_by_uuid ("1a4112fa-c4ef-4c8d-aed0-47d9273bebc5" )[0 ]
412+ resolved_crs_list = resolve_crs_from_triangulated_set (triangulated_set , epc22 )
413+ check ("TriangulatedSetRepresentation resolved CRS uuid" , 5 ,
414+ len (resolved_crs_list ))
415+
416+ for i , resolved_crs in enumerate (resolved_crs_list ):
417+ check (f"{ i } ) patch { i } resolved CRS type" , "LocalEngineeringCompoundCrs" ,
418+ type (resolved_crs ).__name__ if resolved_crs else None )
419+ if resolved_crs :
420+ info = extract_crs_info (resolved_crs , workspace = epc22 )
421+ check (" projected_epsg_code (resolved)" , 23031 , info .projected_epsg_code )
422+ check (" projected_uom" , "M" , info .projected_uom )
423+ check (" vertical_uom" , "M" , info .vertical_uom )
424+ check (" z_increasing_downward" , True , info .z_increasing_downward )
425+ check (" x_offset" , 0.0 , info .x_offset , approx = True )
426+ check (" y_offset" , 0.0 , info .y_offset , approx = True )
427+ check (" z_offset" , 0.0 , info .z_offset , approx = True )
428+ check (" azimuth_reference" , "grid north" , info .azimuth_reference )
429+
349430# ===========================================================================
350431# Convenience helpers (delegates in helper.py)
351432# ===========================================================================
@@ -361,7 +442,8 @@ def _resolve_crs_from_grid(grid_obj: Any, epc: Epc) -> Optional[Any]:
361442)
362443
363444depth_crs = epc20 .get_object_by_uuid ("0ae56ef3-fc79-405b-8deb-6942e0f2e77c" )[0 ]
364- check ("is_z_reversed(LocalDepth3DCrs)" , False , is_z_reversed (depth_crs ))
445+ # ZIncreasingDownward=true in the raw file → is_z_reversed returns True.
446+ check ("is_z_reversed(LocalDepth3DCrs)" , True , is_z_reversed (depth_crs ))
365447check ("get_projected_epsg_code" , 23031 , get_projected_epsg_code (depth_crs ))
366448check ("get_projected_uom" , "M" , get_projected_uom (depth_crs ))
367449
0 commit comments