Add navmap_tools: GIS-based world/NavMap generator - #24
Merged
Conversation
Signed-off-by: Francisco Martín Rico <fmrico@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new ROS 2 package (navmap_tools) that can generate a Gazebo world and/or a .navmap directly from real-world GIS elevation + imagery data, and extends navmap_ros with an organized-grid conversion path to avoid meshing gaps when the point cloud is already grid-structured.
Changes:
- Add
navmap_tools(Python GIS pipeline + scaffold/mesh/PCD utilities) and two C++ CLI tools:pointcloud_to_navmapandnavmap_publisher. - Extend
navmap_rosconversions withfrom_regular_grid()and adjustfrom_points()edge/candidate selection logic to reduce “hole” artifacts. - Refactor
navmap_rviz_pluginto centralize the “Color (vertex RGBA)” layer name and expose it conditionally.
Reviewed changes
Copilot reviewed 39 out of 40 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| navmap_tools/test/test_xmllint.py | Adds XML lint test harness for package files. |
| navmap_tools/test/test_terrain.py | Tests terrain grid sampling + texture rendering logic. |
| navmap_tools/test/test_scaffold.py | Tests generated world-package scaffold layout and contents. |
| navmap_tools/test/test_projection.py | Tests local AEQD projection and bbox computations. |
| navmap_tools/test/test_pnoa.py | Tests PNOA WMS chunking/mosaicking with monkeypatching. |
| navmap_tools/test/test_pep257.py | Adds PEP257 lint test harness. |
| navmap_tools/test/test_pcd_writer.py | Tests PCD + RGB sidecar writing (incl. organized clouds). |
| navmap_tools/test/test_net.py | Tests retrying GET-to-file helper without real network. |
| navmap_tools/test/test_mesh_export.py | Tests STL/DAE/texture export and geometry correctness. |
| navmap_tools/test/test_imagery.py | Tests Esri imagery tiling, placeholders, and fallback zoom. |
| navmap_tools/test/test_google.py | Tests Google Map Tiles API session/token/caching logic (mocked). |
| navmap_tools/test/test_flake8.py | Adds flake8 lint test harness. |
| navmap_tools/test/test_dem.py | Tests Copernicus DEM tile naming, sampling, and mosaicking (mocked). |
| navmap_tools/test/test_copyright.py | Adds copyright lint test harness. |
| navmap_tools/test/test_cli.py | Tests CLI argument parsing and validation behavior. |
| navmap_tools/test/test_cache.py | Tests DiskCache correctness and cleanup semantics. |
| navmap_tools/test/init.py | Marks test directory as a Python package. |
| navmap_tools/src/pointcloud_to_navmap.cpp | New C++ tool to build .navmap (and optional vertex colors) from .pcd. |
| navmap_tools/src/navmap_publisher.cpp | New C++ tool to periodically publish a .navmap to a topic. |
| navmap_tools/scripts/navmap_gis_tool | Console entrypoint wrapper for the Python CLI. |
| navmap_tools/package.xml | Declares dependencies for the new navmap_tools package. |
| navmap_tools/navmap_tools/terrain.py | Builds TerrainGrid samples and renders a baked texture. |
| navmap_tools/navmap_tools/scaffold.py | Generates colcon package structure and template files for Gazebo. |
| navmap_tools/navmap_tools/pcd_writer.py | Writes XYZ PCD (optionally organized) plus RGB CSV sidecar. |
| navmap_tools/navmap_tools/mesh_export.py | Exports regular-grid terrain as textured DAE + collision STL. |
| navmap_tools/navmap_tools/geo/projection.py | Implements local ENU projection and size→bbox perimeter sampling. |
| navmap_tools/navmap_tools/geo/pnoa.py | Fetches/mosaics PNOA imagery via WMS with caching and chunking. |
| navmap_tools/navmap_tools/geo/net.py | Implements retrying streaming download helper. |
| navmap_tools/navmap_tools/geo/imagery.py | Fetches/mosaics Esri XYZ imagery and detects placeholder tiles. |
| navmap_tools/navmap_tools/geo/google.py | Fetches/mosaics Google Map Tiles imagery with session tokens + caching. |
| navmap_tools/navmap_tools/geo/dem.py | Fetches/mosaics Copernicus DEM tiles and provides bilinear sampling. |
| navmap_tools/navmap_tools/geo/cache.py | Implements content-addressed disk cache for downloads. |
| navmap_tools/navmap_tools/geo/init.py | Package init for GIS submodule. |
| navmap_tools/navmap_tools/cli.py | Orchestrates the full GIS→TerrainGrid→Gazebo/NavMap pipeline. |
| navmap_tools/navmap_tools/init.py | Package init for navmap_tools. |
| navmap_tools/CMakeLists.txt | Builds/installs the Python package and the two C++ tools; enables tests. |
| navmap_rviz_plugin/src/navmap_rviz_plugin/NavMapDisplay.cpp | Refactors vertex-color layer name into a shared constant and conditionally exposes it. |
| navmap_ros/tests/test_conversions.cpp | Adds gtests for from_regular_grid() behavior and coverage. |
| navmap_ros/src/navmap_ros/conversions.cpp | Fixes/adjusts from_points() edge/candidate logic and implements from_regular_grid(). |
| navmap_ros/include/navmap_ros/conversions.hpp | Exposes from_regular_grid() API with documentation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+25
to
+29
| #include <fstream> | ||
| #include <iostream> | ||
| #include <sstream> | ||
| #include <string> | ||
| #include <vector> |
Comment on lines
+101
to
+111
| std::istringstream iss(line); | ||
| std::string r_s, g_s, b_s; | ||
| if (!std::getline(iss, r_s, ',') || !std::getline(iss, g_s, ',') || | ||
| !std::getline(iss, b_s, ',')) | ||
| { | ||
| throw std::runtime_error("malformed colors CSV line: " + line); | ||
| } | ||
| colors.push_back( | ||
| {static_cast<uint8_t>(std::stoi(r_s)), static_cast<uint8_t>(std::stoi(g_s)), | ||
| static_cast<uint8_t>(std::stoi(b_s))}); | ||
| } |
Comment on lines
+1429
to
+1441
| Eigen::Vector3f a(A.x, A.y, A.z), b(B.x, B.y, B.z), c(C.x, C.y, C.z); | ||
| Eigen::Vector3f n = (b - a).cross(c - a); | ||
| const float nn = n.norm(); | ||
| if (nn < 1e-9f) {return;} | ||
| n /= nn; | ||
| if (n.dot(Eigen::Vector3f::UnitZ()) < cos_max_slope) {return;} | ||
|
|
||
| // Canonical orientation (normal facing +Z), matching try_add_triangle. | ||
| if (n.dot(Eigen::Vector3f::UnitZ()) < 0.0f) { | ||
| triangles.emplace_back(i0, i2, i1); | ||
| } else { | ||
| triangles.emplace_back(i0, i1, i2); | ||
| } |
Comment on lines
+77
to
+80
| def _is_placeholder_tile(path) -> bool: | ||
| """Return True if the tile at `path` is Esri's known "no data" placeholder image.""" | ||
| digest = hashlib.md5(open(path, 'rb').read()).hexdigest() | ||
| return digest in _PLACEHOLDER_MD5_HASHES |
Comment on lines
+70
to
+76
| } else if (arg == "--resolution") { | ||
| const char * v = next("--resolution"); if (!v) {return false;} | ||
| args.resolution = std::stof(v); | ||
| } else if (arg == "--max-slope-deg") { | ||
| const char * v = next("--max-slope-deg"); if (!v) {return false;} | ||
| args.max_slope_deg = std::stof(v); | ||
| } else { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi,
This PR adds a new package,
navmap_tools, with a CLI tool (navmap_gis_tool) thatgenerates a Gazebo world and/or a NavMap directly from real elevation and
aerial-imagery data, given only a GPS center and a size in meters. Also adds
the
navmap_rosconversion this tool relies on, and a smallnavmap_rviz_pluginrefactor.navmap_tools(new package)navmap_gis_tool: fetches a DEM (Copernicus GLO-30) and aerial imagery(Esri World Imagery, PNOA for Spain, or Google Maps Platform) for a given
GPS center + square size, builds a regular elevation grid, and emits:
--gazebo: a full colcon package (worlds/,models/<name>/meshes/ {.dae,.stl},launch/,env-hooks/) with a textured, Z-up terrain meshand a matching
<spherical_coordinates>GPS anchor.--navmap: a colored, slope-aware.navmap(via the newpointcloud_to_navmapC++ tool), sharing the exact same local-ENU originas the Gazebo world so both line up.
navmap_publisher: publishes a.navmapfile to a topic fornavmap_rviz_plugin.geo/cache.py) so repeated runs over the same area don'tre-fetch DEM/imagery.
navmap_rosfrom_regular_grid(): builds a NavMap directly from an organized(grid-shaped) point cloud using its known (i, j) connectivity, instead of
neighbor search — no meshing-artifact gaps. Used by
pointcloud_to_navmapfor
navmap_gis_tool-generated grids.from_points()'smax_edge_lenrejection: it could be tighter thanthe geometrically-possible worst case (search radius + max |Δz|),
spuriously punching holes in otherwise-navigable, slightly noisy terrain.
navmap_rviz_pluginshared constant instead of a string literal duplicated in two places.
Usage
Useful flags:
--resolution1.0--gazebogeometry and--navmap)--max-slope-deg30.0--imagery-sourceesriesri(worldwide, keyless),pnoa(Spain only, keyless, sharper in rural areas), orgoogle(worldwide, paid — needsGOOGLE_MAPS_API_KEY)--output-dir./<package>--force-refreshGoogle Maps imagery requires a Google Maps Platform API key with "Map Tiles
API" enabled, set via
GOOGLE_MAPS_API_KEY.