Skip to content

mm2las: export flag '--frame geodetic' for georeferenced clouds#49

Merged
jlblancoc merged 5 commits intodevelopfrom
mm2las/georef
Mar 18, 2026
Merged

mm2las: export flag '--frame geodetic' for georeferenced clouds#49
jlblancoc merged 5 commits intodevelopfrom
mm2las/georef

Conversation

@jlblancoc
Copy link
Copy Markdown
Member

@jlblancoc jlblancoc commented Mar 17, 2026

Summary by CodeRabbit

  • New Features

    • Added geodetic coordinate export (EPSG:4979/WGS‑84) for LAS output with CRS embedded as a WKT VLR.
    • New "geodetic" frame mode alongside "map" and "enu"; CLI validates presence of georeferencing data for geodetic exports.
    • Uses per-point lat/lon/alt when present; otherwise converts map/ENU coordinates on the fly.
    • LAS 1.4 adjustments: finer scales for geodetic coords and added VLRs/extra-dimension support.
  • Documentation

    • Updated mm2las docs and examples covering geodetic workflow, CRS embedding, precision, and limitations.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a "geodetic" export mode to mm2las (EPSG:4979). CLI accepts "geodetic"; saveToLas gains an optional Georeferencing parameter; per‑point lat/lon/alt fields are used if present or ENU/map→geodetic conversion is performed via mrpt‑topography; LAS header/scale/VLRs updated and mrpt‑topography is linked.

Changes

Cohort / File(s) Summary
Build Configuration
apps/mm2las/CMakeLists.txt
Added find_package(mrpt-topography REQUIRED) and linked mrpt::topography into the mm2las target.
Export Implementation
apps/mm2las/main.cpp
Added geodetic frame option; introduced kWKT_EPSG4979, includes for mrpt/topography, extended saveToLas signature to accept an optional Georeferencing payload, detect/use per-point lat/lon/alt or convert ENU/map→geodetic on the fly, adjusted LAS header/scale/offsets, added/wrote WKT CRS & extra-dimension VLRs, and propagated CLI changes.
Documentation
docs/source/app_mm2las.rst
Documented --frame geodetic behavior, CRS VLR embedding (EPSG:4979), per-point vs on-the-fly conversion rules, precision notes, examples, and limitations.

Sequence Diagram

sequenceDiagram
    participant CLI as User/CLI
    participant Parser as Option Parser
    participant Map as Input Map/Points
    participant Topo as Topography Converter (mrpt-topography)
    participant LAS as LAS Writer

    CLI->>Parser: --frame geodetic + files
    Parser->>Map: Load points & optional georeferencing
    alt Per-point lat/lon/alt present
        Map->>LAS: Emit per-point geodetic coords
    else
        Map->>Topo: Request ENU/map→geodetic conversion (uses georef)
        Topo->>Map: Return lat/lon/alt per point
        Map->>LAS: Emit converted geodetic coords
    end
    LAS->>LAS: Add EPSG:4979 WKT VLR and extra-dim VLRs
    LAS->>CLI: Write LAS file with geodetic coords and VLRs
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

Poem

🐰
I hopped through points with ears held high,
From ENU fields to a WGS‑84 sky.
Lon and lat tucked neatly into LAS,
A WKT VLR winked — the geodetic pass.
Tiny paws, big maps, carrot-coded jazz. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a '--frame geodetic' export flag to mm2las for handling georeferenced point clouds.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mm2las/georef
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/mm2las/CMakeLists.txt`:
- Line 4: The package is missing the corresponding ROS2 package dependency for
mrpt-topography; update package.xml to add a runtime/build dependency by
inserting a <depend>mrpt_libtopography</depend> entry (matching how mrpt_libmaps
and mrpt_libobs are declared) so colcon/ROS2 builds recognize the
mrpt-topography linkage referenced by find_package(mrpt-topography REQUIRED) in
the CMakeLists.

In `@apps/mm2las/main.cpp`:
- Around line 263-265: The variable lonBuf is incorrectly declared as a
pointer-to-pointer; change its type from const
mrpt::aligned_std_vector<double>** to const mrpt::aligned_std_vector<double>* so
it matches latBuf and altBuf and can be dereferenced safely (update the
declaration of lonBuf and any related initializations/uses where lonBuf is
dereferenced, e.g., places referenced near the current declarations and where
it's used later around the dereferences at lines that correspond to the code
using lonBuf).
- Around line 309-314: The precomputed transform T_map_to_enu_geodetic is
inverted incorrectly; when geodetic.has_value() and !hasPerPointGeodetic you
should assign the map→ENU transform directly from geodetic->T_enu_to_map.mean
(not its negation) so subsequent calls that expect
T_enu_to_map.composePoint(p_map) to produce ENU coordinates behave the same as
the ENU export path; update the initialization of T_map_to_enu_geodetic
accordingly in the block that references geodetic and hasPerPointGeodetic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3644753-844f-4378-9255-fd288bee573f

📥 Commits

Reviewing files that changed from the base of the PR and between 148597d and 9f42b6d.

📒 Files selected for processing (3)
  • apps/mm2las/CMakeLists.txt
  • apps/mm2las/main.cpp
  • docs/source/app_mm2las.rst

Comment thread apps/mm2las/CMakeLists.txt
Comment thread apps/mm2las/main.cpp Outdated
Comment thread apps/mm2las/main.cpp Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/mm2las/main.cpp (2)

263-265: ⚠️ Potential issue | 🔴 Critical

Critical type error: lonBuf declared with double pointer.

lonBuf is declared as const mrpt::aligned_std_vector<double>** but should be const mrpt::aligned_std_vector<double>* to match latBuf and altBuf. This will cause a compilation error at line 290 where getPointsBufferRef_double_field returns a single pointer.

🐛 Proposed fix
     [[maybe_unused]] const mrpt::aligned_std_vector<double>*  latBuf = nullptr;
-    [[maybe_unused]] const mrpt::aligned_std_vector<double>** lonBuf = nullptr;
+    [[maybe_unused]] const mrpt::aligned_std_vector<double>*  lonBuf = nullptr;
     [[maybe_unused]] const mrpt::aligned_std_vector<double>*  altBuf = nullptr;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 263 - 265, The variable lonBuf is
incorrectly declared as a double pointer; change its type to match latBuf and
altBuf by declaring lonBuf as const mrpt::aligned_std_vector<double>* (not **),
so it can hold the single-pointer returned by getPointsBufferRef_double_field;
update the declaration near other buffers (latBuf, altBuf) and ensure subsequent
uses with getPointsBufferRef_double_field compile without pointer-level
mismatch.

309-314: ⚠️ Potential issue | 🔴 Critical

Transformation direction inconsistency: geodetic path inverts the transform incorrectly.

Per the codebase convention, T_enu_to_map.composePoint(p_map) yields ENU-frame coordinates. The ENU export path at line 728 correctly uses T_enu_to_map directly. However, the geodetic path uses the negation (inverse) at line 313, which reverses the transformation direction.

🐛 Proposed fix
     std::optional<mrpt::poses::CPose3D> T_map_to_enu_geodetic;
     if (geodetic.has_value() && !hasPerPointGeodetic)
     {
-        T_map_to_enu_geodetic = -geodetic->T_enu_to_map.mean;
+        T_map_to_enu_geodetic = geodetic->T_enu_to_map.mean;
     }

Based on learnings: "T_enu_to_map.composePoint(p_map) as mapping from map-frame to ENU-frame".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 309 - 314, The code incorrectly negates
the geodetic transform when computing T_map_to_enu_geodetic, reversing the
intended map->ENU direction; in the geodetic block (check geodetic and
!hasPerPointGeodetic) set T_map_to_enu_geodetic to geodetic->T_enu_to_map.mean
(use the transform as-is) instead of assigning its negation so that
T_enu_to_map.composePoint(p_map) continues to produce ENU-frame coordinates.
🧹 Nitpick comments (1)
apps/mm2las/main.cpp (1)

686-708: Consider caching geodetic coordinates to avoid duplicate computation.

The pointToGeodetic lambda is called twice per point: once during bounds calculation (lines 710-737) and once during point data writing (lines 930-952). For large point clouds, this doubles the cost of the expensive geodetic conversion (involving ellipsoid math).

Consider pre-computing all geodetic coordinates into temporary buffers before the bounds loop, then reusing them for both bounds and point writing. This trades memory for CPU time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 686 - 708, The pointToGeodetic lambda is
being invoked twice per point causing redundant expensive conversions; instead,
before the bounds calculation loop, allocate and fill cache vectors (e.g.,
std::vector<double> cached_lon, cached_lat, cached_alt or
std::vector<std::array<double,3>> cached_geod) by iterating over indices and
using the same logic as pointToGeodetic (respecting hasPerPointGeodetic by
copying from lonBuf/latBuf/altBuf when true, otherwise performing the
ENU->geocentric->geodetic conversion using T_map_to_enu_geodetic,
ENUToGeocentric and geocentricToGeodetic), then replace calls to pointToGeodetic
in the bounds calculation and the point-writing section with reads from these
cached vectors (referencing xs/ys/zs only for the conversion step when filling
the cache).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/mm2las/main.cpp`:
- Around line 181-203: The WKT string kWKT_EPSG4979 is WKT2 and declares
latitude as ORDER[1], which will cause CRS-aware readers to swap X/Y; replace it
with a WKT1 (OGC 2001) representation that explicitly uses GEOGCS-style syntax
and declares longitude as the first axis (lon, lat, ellipsoidal height) to match
how coordinates are stored (X=lon, Y=lat, Z=height). Generate or copy a correct
WKT1 for EPSG:4979 (3D WGS84) — e.g. using proj/gdal utilities — and update the
kWKT_EPSG4979 string so axis order and keywords conform to WKT1 (ensure AXIS
entries put longitude first) to avoid misinterpretation by strict LAS readers.

---

Duplicate comments:
In `@apps/mm2las/main.cpp`:
- Around line 263-265: The variable lonBuf is incorrectly declared as a double
pointer; change its type to match latBuf and altBuf by declaring lonBuf as const
mrpt::aligned_std_vector<double>* (not **), so it can hold the single-pointer
returned by getPointsBufferRef_double_field; update the declaration near other
buffers (latBuf, altBuf) and ensure subsequent uses with
getPointsBufferRef_double_field compile without pointer-level mismatch.
- Around line 309-314: The code incorrectly negates the geodetic transform when
computing T_map_to_enu_geodetic, reversing the intended map->ENU direction; in
the geodetic block (check geodetic and !hasPerPointGeodetic) set
T_map_to_enu_geodetic to geodetic->T_enu_to_map.mean (use the transform as-is)
instead of assigning its negation so that T_enu_to_map.composePoint(p_map)
continues to produce ENU-frame coordinates.

---

Nitpick comments:
In `@apps/mm2las/main.cpp`:
- Around line 686-708: The pointToGeodetic lambda is being invoked twice per
point causing redundant expensive conversions; instead, before the bounds
calculation loop, allocate and fill cache vectors (e.g., std::vector<double>
cached_lon, cached_lat, cached_alt or std::vector<std::array<double,3>>
cached_geod) by iterating over indices and using the same logic as
pointToGeodetic (respecting hasPerPointGeodetic by copying from
lonBuf/latBuf/altBuf when true, otherwise performing the
ENU->geocentric->geodetic conversion using T_map_to_enu_geodetic,
ENUToGeocentric and geocentricToGeodetic), then replace calls to pointToGeodetic
in the bounds calculation and the point-writing section with reads from these
cached vectors (referencing xs/ys/zs only for the conversion step when filling
the cache).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d25eb613-0c90-45f4-b89b-bcac5e3a83a3

📥 Commits

Reviewing files that changed from the base of the PR and between 9f42b6d and 8d6e28f.

📒 Files selected for processing (3)
  • apps/mm2las/CMakeLists.txt
  • apps/mm2las/main.cpp
  • docs/source/app_mm2las.rst
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/mm2las/CMakeLists.txt
  • docs/source/app_mm2las.rst

Comment thread apps/mm2las/main.cpp Outdated
Comment on lines +181 to +203
// WKT string for EPSG:4979 (WGS 84 geographic 3D)
// X=Longitude, Y=Latitude, Z=Ellipsoidal height
static const char* const kWKT_EPSG4979 =
"GEOGCRS[\"WGS 84\","
"ENSEMBLE[\"World Geodetic System 1984 ensemble\","
"MEMBER[\"World Geodetic System 1984 (Transit)\"],"
"MEMBER[\"World Geodetic System 1984 (G730)\"],"
"MEMBER[\"World Geodetic System 1984 (G873)\"],"
"MEMBER[\"World Geodetic System 1984 (G1150)\"],"
"MEMBER[\"World Geodetic System 1984 (G1674)\"],"
"MEMBER[\"World Geodetic System 1984 (G1762)\"],"
"MEMBER[\"World Geodetic System 1984 (G2139)\"],"
"ELLIPSOID[\"WGS 84\",6378137,298.257223563,"
"LENGTHUNIT[\"metre\",1]],"
"ENSEMBLEACCURACY[2.0]],"
"PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],"
"CS[ellipsoidal,3],"
"AXIS[\"geodetic latitude (Lat)\",north,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],"
"AXIS[\"geodetic longitude (Lon)\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],"
"AXIS[\"ellipsoidal height (h)\",up,ORDER[3],LENGTHUNIT[\"metre\",1]],"
"USAGE[SCOPE[\"Geodesy.\"],AREA[\"World.\"],"
"BBOX[-90,-180,90,180]],"
"ID[\"EPSG\",4979]]";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

EPSG:4979 axis order WKT LAS file convention

💡 Result:

  • EPSG:4979 (WGS 84 3D) axis order in EPSG / WKT2: the CRS is defined with axes (latitude, longitude, ellipsoidal height) = (north, east, up). (epsg.io)
  • But LAS point storage is always X/Y/Z: LAS point records store coordinates as X, Y, Z, and most tooling treats that as (longitude/easting, latitude/northing, height) (i.e., x = east/longitude, y = north/latitude). (gdal.org)
  • LAS WKT convention constraint: LAS 1.4 WKT CRS metadata (in VLR/EVLR) is specified to use OGC 2001 “WKT1”, and (per USGS Lidar Base Spec guidance quoting LAS 1.4-R13) OGC 2015 / WKT2 is forbidden in LAS WKT records. (pubs.usgs.gov)
  • Practical “LAS convention” takeaway for EPSG:4979: even though EPSG:4979’s formal axis order is lat, lon, h, in a LAS file you should expect/emit coordinate tuples in X,Y,Z = lon,lat,h order, and ensure your WKT/metadata doesn’t accidentally cause axis swapping in software that honors EPSG axis rules. (epsg.io)

Citations:


🏁 Script executed:

# First, check the actual coordinate storage around lines 717-719 and 939-941
cat -n apps/mm2las/main.cpp | sed -n '710,730p'

Repository: MOLAorg/mp2p_icp

Length of output: 771


🏁 Script executed:

cat -n apps/mm2las/main.cpp | sed -n '930,950p'

Repository: MOLAorg/mp2p_icp

Length of output: 809


🏁 Script executed:

# Check context around the WKT definition and how it's used
cat -n apps/mm2las/main.cpp | sed -n '175,220p'

Repository: MOLAorg/mp2p_icp

Length of output: 2488


🏁 Script executed:

# Check if there are any comments or documentation about WKT format choice
grep -n "WKT1\|WKT2\|OGC 2001\|OGC 2015\|LAS 1.4" apps/mm2las/main.cpp

Repository: MOLAorg/mp2p_icp

Length of output: 531


🏁 Script executed:

# Check if there are tests or other references to this WKT constant
grep -r "kWKT_EPSG4979" . --include="*.cpp" --include="*.h"

Repository: MOLAorg/mp2p_icp

Length of output: 338


🏁 Script executed:

# Check nearby comments to understand intent of geodetic mode
sed -n '200,230p' apps/mm2las/main.cpp

Repository: MOLAorg/mp2p_icp

Length of output: 1273


WKT format violates LAS 1.4 spec and risks axis-swapping in readers.

The code correctly stores coordinates as X=longitude, Y=latitude, Z=height (LAS convention), but the WKT string uses WKT2 format (GEOGCRS, ENSEMBLE keywords). LAS 1.4 spec requires WKT1 (OGC 2001), not WKT2.

More critically: the WKT declares axis ORDER[1]=latitude, ORDER[2]=longitude (lines 198–199). Strict CRS-aware readers honoring these EPSG axis rules will swap X/Y, interpreting stored (lon,lat) as (lat,lon), breaking coordinates.

Convert the WKT string to WKT1 format with lon/lat axis order to match the actual coordinate storage, or accept that non-standard CRS readers may misinterpret the data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 181 - 203, The WKT string kWKT_EPSG4979 is
WKT2 and declares latitude as ORDER[1], which will cause CRS-aware readers to
swap X/Y; replace it with a WKT1 (OGC 2001) representation that explicitly uses
GEOGCS-style syntax and declares longitude as the first axis (lon, lat,
ellipsoidal height) to match how coordinates are stored (X=lon, Y=lat,
Z=height). Generate or copy a correct WKT1 for EPSG:4979 (3D WGS84) — e.g. using
proj/gdal utilities — and update the kWKT_EPSG4979 string so axis order and
keywords conform to WKT1 (ensure AXIS entries put longitude first) to avoid
misinterpretation by strict LAS readers.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/mm2las/main.cpp (1)

181-203: ⚠️ Potential issue | 🟠 Major

CRS axis metadata conflicts with written LAS coordinate order.

Line 198 and Line 199 declare axis order as latitude-first, but Line 932 and Line 933 write X=longitude and Y=latitude. CRS-aware readers can swap axes and misplace points.

🛠️ Proposed fix
-    "AXIS[\"geodetic latitude (Lat)\",north,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],"
-    "AXIS[\"geodetic longitude (Lon)\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],"
+    "AXIS[\"geodetic longitude (Lon)\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],"
+    "AXIS[\"geodetic latitude (Lat)\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],"
For LAS 1.4 WKT CRS VLR (record_id 2112), what WKT dialect and axis-order convention should be used for EPSG:4979 when point tuples are stored as X=longitude, Y=latitude, Z=ellipsoidal height?

Also applies to: 932-934

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 181 - 203, The WKT in kWKT_EPSG4979
declares axes latitude-first but the file stores points as X=longitude,
Y=latitude; update the WKT to a WKT2 flavor that explicitly defines longitude as
ORDER[1] and latitude as ORDER[2] (or use the WKT2_2019 convention) so axis
metadata matches stored tuples; specifically edit the AXIS entries in
kWKT_EPSG4979 to list "geodetic longitude (Lon)" as ORDER[1] and "geodetic
latitude (Lat)" as ORDER[2], and ensure the WKT dialect used for the LAS VLR
(record_id 2112) is WKT2 with explicit axis order to avoid CRS-aware readers
swapping X/Y.
🧹 Nitpick comments (1)
apps/mm2las/main.cpp (1)

705-723: Refactor duplicated coordinate-branch logic into one helper.

The map/ENU/geodetic coordinate-selection branch is repeated in two loops. Extracting one helper for px,py,pz avoids future divergence bugs.

Also applies to: 927-945

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 705 - 723, Refactor the duplicated
coordinate-selection logic into a single helper that returns or fills px, py, pz
given an index: create a function (e.g., getPointCoords(int i, double &px,
double &py, double &pz) or std::tuple<double,double,double> getPointCoords(int
i)) that contains the current branch using geodetic, pointToGeodetic, xs/ys/zs
and T_enu_to_map->composePoint; replace both duplicated blocks (the one using
geodetic and the one at the other loop) to call this helper so all callers use
the same logic (referencing variables/identifiers geodetic, pointToGeodetic, xs,
ys, zs, T_enu_to_map, composePoint).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/mm2las/main.cpp`:
- Around line 181-203: The WKT in kWKT_EPSG4979 declares axes latitude-first but
the file stores points as X=longitude, Y=latitude; update the WKT to a WKT2
flavor that explicitly defines longitude as ORDER[1] and latitude as ORDER[2]
(or use the WKT2_2019 convention) so axis metadata matches stored tuples;
specifically edit the AXIS entries in kWKT_EPSG4979 to list "geodetic longitude
(Lon)" as ORDER[1] and "geodetic latitude (Lat)" as ORDER[2], and ensure the WKT
dialect used for the LAS VLR (record_id 2112) is WKT2 with explicit axis order
to avoid CRS-aware readers swapping X/Y.

---

Nitpick comments:
In `@apps/mm2las/main.cpp`:
- Around line 705-723: Refactor the duplicated coordinate-selection logic into a
single helper that returns or fills px, py, pz given an index: create a function
(e.g., getPointCoords(int i, double &px, double &py, double &pz) or
std::tuple<double,double,double> getPointCoords(int i)) that contains the
current branch using geodetic, pointToGeodetic, xs/ys/zs and
T_enu_to_map->composePoint; replace both duplicated blocks (the one using
geodetic and the one at the other loop) to call this helper so all callers use
the same logic (referencing variables/identifiers geodetic, pointToGeodetic, xs,
ys, zs, T_enu_to_map, composePoint).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4cc381c8-4554-41f4-aefa-0f5d5a47f578

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6e28f and 963b01a.

📒 Files selected for processing (3)
  • apps/mm2las/CMakeLists.txt
  • apps/mm2las/main.cpp
  • docs/source/app_mm2las.rst
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/mm2las/CMakeLists.txt

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.59%. Comparing base (148597d) to head (ce0585b).
⚠️ Report is 6 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop      #49   +/-   ##
========================================
  Coverage    78.59%   78.59%           
========================================
  Files          190      190           
  Lines        10569    10569           
  Branches       980      980           
========================================
  Hits          8307     8307           
  Misses        2262     2262           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/mm2las/main.cpp (1)

706-726: Deduplicate export-coordinate branching used in both loops.

The same geodetic/map/ENU branching appears twice. A single helper (e.g., computeExportPoint(i)) will prevent bounds-vs-write divergence in future edits.

Also applies to: 930-948

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 706 - 726, The coordinate-export branching
for geodetic vs. cartesian/ENU is duplicated; extract it into a helper like
computeExportPoint(size_t i) that returns or fills (px,py,pz) and use it from
both loops to avoid future divergence. In computeExportPoint, call
pointToGeodetic(i) when geodetic.has_value() to set px/py/pz, otherwise read
xs[i], ys[i], zs[i] and, if T_enu_to_map.has_value(), call
T_enu_to_map->composePoint(xs[i], ys[i], zs[i], px, py, pz). Replace the
duplicated branches at both locations (the loop using pointToGeodetic and the
later loop at lines ~930-948) with calls to computeExportPoint to centralize
bounds and write logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/mm2las/main.cpp`:
- Around line 295-297: The fast-path flag hasPerPointGeodetic is being set based
only on latBuf->empty(), which can lead to out-of-bounds reads when latBuf,
lonBuf, or altBuf are shorter than N; update the check that sets
hasPerPointGeodetic to ensure latBuf, lonBuf, and altBuf are non-null and each
has size() >= N (or the points count used for indexing), e.g. verify
latBuf->size() >= N && lonBuf->size() >= N && altBuf->size() >= N before
enabling the fast-path; if any buffer is too short, leave hasPerPointGeodetic
false and fall back to the safe per-point lookup.
- Around line 768-787: Add an explicit int32 quantization range check before
writing points: for each axis (use
header.x_scale_factor/header.y_scale_factor/header.z_scale_factor and
header.x_offset/header.y_offset/header.z_offset) compute quantized =
round((coord - offset) / scale) for the point set extrema (min/max coords) and
verify they fit within int32 bounds (INT32_MIN..INT32_MAX); if any axis
overflows, fail early (return error) or adjust/export with a coarser
scale/offset according to policy. Insert this guard immediately before the point
quantization/writing block (the code that currently uses header.*_scale_factor
and header.*_offset to turn coordinates into int32 values) so no silent
truncation occurs.

---

Nitpick comments:
In `@apps/mm2las/main.cpp`:
- Around line 706-726: The coordinate-export branching for geodetic vs.
cartesian/ENU is duplicated; extract it into a helper like
computeExportPoint(size_t i) that returns or fills (px,py,pz) and use it from
both loops to avoid future divergence. In computeExportPoint, call
pointToGeodetic(i) when geodetic.has_value() to set px/py/pz, otherwise read
xs[i], ys[i], zs[i] and, if T_enu_to_map.has_value(), call
T_enu_to_map->composePoint(xs[i], ys[i], zs[i], px, py, pz). Replace the
duplicated branches at both locations (the loop using pointToGeodetic and the
later loop at lines ~930-948) with calls to computeExportPoint to centralize
bounds and write logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ee58865-481f-494b-8344-684baaac1141

📥 Commits

Reviewing files that changed from the base of the PR and between 963b01a and f66e2a4.

📒 Files selected for processing (1)
  • apps/mm2las/main.cpp

Comment thread apps/mm2las/main.cpp Outdated
Comment thread apps/mm2las/main.cpp
@jlblancoc jlblancoc enabled auto-merge March 17, 2026 22:02
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/mm2las/main.cpp (1)

684-706: Avoid doing the geodetic conversion twice per point.

When !hasPerPointGeodetic, pointToGeodetic(i) runs once in the bounds pass and again in the write pass. On large clouds that doubles the expensive ENU→geocentric→geodetic path; caching the converted triplets for geodetic mode would avoid paying that cost twice.

Also applies to: 708-718, 956-964

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mm2las/main.cpp` around lines 684 - 706, The pointToGeodetic lambda
performs an expensive ENU→geocentric→geodetic conversion twice per point when
hasPerPointGeodetic is false (once in the bounds pass and again in the write
pass); modify the code to compute and cache geodetic coordinates for each point
when hasPerPointGeodetic == false (e.g., allocate a vector<std::array<double,3>>
geodeticCache sized to number of points and fill it using pointToGeodetic logic
once using xs/ys/zs and geodetic->T_enu_to_map, then make pointToGeodetic (or
call sites that use it) return values from geodeticCache instead of recomputing;
keep existing behavior that reads directly from lonBuf/latBuf/altBuf when
hasPerPointGeodetic is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/mm2las/main.cpp`:
- Around line 212-217: The current saveToLas signature and preflight use
geodetic.has_value() both to indicate "export in geodetic mode" and to provide
ENU→geodetic context, which prevents the per-point latitude/longitude/altitude
fast path from being used when map-level mm.georeferencing is empty; change
saveToLas(const mrpt::maps::CPointsMap& pc, ..., const
std::optional<mp2p_icp::metric_map_t::Georeferencing>& geodetic = std::nullopt)
to accept an explicit export-frame flag/enum (e.g., requestedFrame or
exportInGeodetic bool/enum) separate from the optional Georeferencing context,
update callers to pass the desired frame, and alter the preflight checks so
Georeferencing is required only when requestedFrame indicates geodetic AND
per-point geodetic buffers are not present; update code paths in saveToLas, and
any related functions (references at lines ~272-311, 1161-1190, 1264-1266) to
perform on-the-fly conversion only when geodetic context is actually needed.
- Around line 819-825: The LASHeader_1_4 global_encoding bit for WKT is being
left set unconditionally while the WKT VLR (OGC WKT record 2112) is only written
when geodetic.has_value() is true; update the header write to set bit 4 (0x10)
only when geodetic.has_value() and clear it otherwise (i.e., modify
LASHeader_1_4.global_encoding around the VLR count/size logic where numVLRs and
vlrTotalBytes are updated) so the WKT flag matches whether you actually write
the WKT VLR record 2112.

---

Nitpick comments:
In `@apps/mm2las/main.cpp`:
- Around line 684-706: The pointToGeodetic lambda performs an expensive
ENU→geocentric→geodetic conversion twice per point when hasPerPointGeodetic is
false (once in the bounds pass and again in the write pass); modify the code to
compute and cache geodetic coordinates for each point when hasPerPointGeodetic
== false (e.g., allocate a vector<std::array<double,3>> geodeticCache sized to
number of points and fill it using pointToGeodetic logic once using xs/ys/zs and
geodetic->T_enu_to_map, then make pointToGeodetic (or call sites that use it)
return values from geodeticCache instead of recomputing; keep existing behavior
that reads directly from lonBuf/latBuf/altBuf when hasPerPointGeodetic is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3536380e-63fa-4660-9be5-3176ea4cd276

📥 Commits

Reviewing files that changed from the base of the PR and between f66e2a4 and ec94437.

📒 Files selected for processing (1)
  • apps/mm2las/main.cpp

Comment thread apps/mm2las/main.cpp Outdated
Comment thread apps/mm2las/main.cpp
@jlblancoc jlblancoc disabled auto-merge March 17, 2026 22:16
@jlblancoc jlblancoc enabled auto-merge March 18, 2026 09:38
@jlblancoc jlblancoc merged commit 620661c into develop Mar 18, 2026
13 checks passed
@jlblancoc jlblancoc deleted the mm2las/georef branch March 18, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant