Conversation
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
83e0223 to
cfe0636
Compare
There was a problem hiding this comment.
Pull request overview
Adds support for reading/writing LAStools (.lax / EVLR) quadtree spatial indexes and integrates it into copy/conversion workflows, while also refactoring parallelism utilities, printing helpers, and build configuration.
Changes:
- Introduce
QuadtreeSpatialIndexand add LASReader/LASWriter support for detecting, copying, and optionally generating LAStools spatial index EVLRs. - Replace
<execution>-based parallel loops with an internal thread-pool approach, including a new parallel reduction helper. - Add printing helpers (indentation + limited map printing), new tests, and adjust CMake/CI to improve cross-compiler compatibility.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vlr.hpp | Improves VLR/EVLR printing and adds helpers to identify LAStools spatial index records. |
| src/utilities/thread_pool.hpp | Adds parallel_for_reduction and related includes/utilities. |
| src/utilities/tests/test_printing.cpp | Adds tests for new printing helpers. |
| src/utilities/printing.hpp | Adds Indented/LimitedMap wrappers and std::map streaming support. |
| src/utilities/assert.hpp | Adjusts seek macro warnings and headers/includes. |
| src/tests/test_writer_copy_from_reader.cpp | Adds coverage for writer copy-from-reader and spatial index behavior. |
| src/tests/test_transform_accessors.cpp | Adds tests for new non-const Transform accessors. |
| src/tests/test_reader_constructors.cpp | Adds tests for reader constructors and .lax discovery/precedence behavior. |
| src/tests/test_reader.cpp | Adds tests for interval→chunk mapping and reading non-contiguous chunk lists. |
| src/tests/test_bound2d.cpp | Adds tests for new Bound2D. |
| src/spatial_index.hpp | New LAStools spatial index implementation (read/write + cell computations). |
| src/laz/tests/test_spatial_index.cpp | Adds LAZ-side tests for spatial index functionality. |
| src/laz/tests/test_laszip_interop.cpp | Updates Transform usage to new API. |
| src/laz/stream.hpp | Tightens bounds-checked reads and seek behavior for pointer-backed streams. |
| src/laz/laz_writer.hpp | Adds handling for (unsupported) LAZ item type Byte14 and removes <execution>. |
| src/laz/laz_reader.hpp | Adds handling for (unsupported) LAZ item type Byte14. |
| src/laz/layered_stream.hpp | Formatting/initialization tweak for the empty-layer buffer. |
| src/laz/chunktable.hpp | Fixes #pragma pack(pop) placement. |
| src/las_writer.hpp | Adds copy-from-reader and spatial index writing; refactors parallel copy/stats to thread-pool utilities. |
| src/las_reader.hpp | Adds spatial index detection (EVLR + .lax), stream-parallel LAZ chunk reads, and interval/chunk helpers. |
| src/las_point.hpp | Minor include cleanup. |
| src/las_header.hpp | Introduces Bound2D, adds Transform accessors, and exposes points-by-return helper. |
| src/example_custom_las_point.hpp | Adds missing standard includes for example point type. |
| cmake/SetupLazperf.cmake | Makes warning suppression flags compiler-specific (GCC vs Clang). |
| cmake/SetupLaszip.cmake | Makes warning suppression flags compiler-specific (GCC vs Clang). |
| apps/validate_spatial_index.cpp | New CLI tool to validate spatial index correctness vs points. |
| apps/las2las++.cpp | Updates CLI to use copy-from-reader and optionally add spatial index. |
| apps/inspect_vlrs.cpp | New CLI tool to inspect VLR/EVLRs and spatial index details. |
| apps/create_test_file.cpp | Updates Transform usage to new API. |
| apps/CMakeLists.txt | Builds/installs new CLI tools and adds MSVC /bigobj handling. |
| CMakeLists.txt | Scopes strict flags/aggressive opts to top-level project; MSVC /bigobj for tests. |
| .github/workflows/cmake-multi-platform.yml | Removes Clang OpenMP install step. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Up to standards ✅🟢 Issues
|
| Category | Results |
|---|---|
| BestPractice | 8 medium |
| ErrorProne | 2 medium |
| Complexity | 16 medium |
🟢 Metrics 245 complexity
Metric Results Complexity 245
TIP This summary will be updated as you push new changes. Give us feedback
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // LASPP_DEBUG_ASSERT compiles out entirely in release (zero overhead on hot paths). | ||
| // In debug builds it is identical to LASPP_ASSERT. | ||
| #ifdef LASPP_DEBUG_ASSERTS | ||
| #define LASPP_DEBUG_ASSERT(condition, ...) LASPP_ASSERT(condition, __VA_ARGS__) | ||
| #define LASPP_DEBUG_ASSERT_BIN_OP(a, b, op, nop, ...) \ | ||
| LASPP_ASSERT_BIN_OP(a, b, op, nop, __VA_ARGS__) | ||
| #else |
There was a problem hiding this comment.
LASPP_DEBUG_ASSERT / LASPP_DEBUG_ASSERT_BIN_OP (and the derived LASPP_DEBUG_ASSERT_* shorthands) forward __VA_ARGS__ by always inserting a comma. When these macros are invoked without an extra message (e.g. LASPP_DEBUG_ASSERT_LT(symbol, NSymbols);), this expands to an invalid call with a trailing comma and will fail to compile when LASPP_DEBUG_ASSERTS is enabled (tests enable it). Consider using __VA_OPT__ (C++20) to conditionally insert the comma, or otherwise make the forwarding macros accept empty varargs safely.
| template <typename T, typename Func> | ||
| void parallel_for_reduction(size_t begin, size_t end, T& result, Func func, | ||
| size_t chunk_size = 1) { | ||
| if (begin >= end) { | ||
| return; | ||
| } | ||
|
|
||
| size_t requested_threads = get_num_threads(); | ||
| ensure_threads(requested_threads); | ||
| size_t active_threads = std::min(requested_threads, m_max_threads); | ||
|
|
||
| const size_t total_work = end - begin; | ||
| const size_t num_chunks = (total_work + chunk_size - 1) / chunk_size; // Ceiling division | ||
|
|
There was a problem hiding this comment.
parallel_for_reduction computes num_chunks using (total_work + chunk_size - 1) / chunk_size without guarding chunk_size. Passing chunk_size == 0 will cause division-by-zero/UB. Please add a validation (assert or early return / treat 0 as 1) before using chunk_size.
| friend std::ostream& operator<<(std::ostream& os, const LimitedMap<K, V>& limited) { | ||
| size_t count = 0; | ||
| for (const auto& kv : limited.m_map) { | ||
| if (count++ >= limited.m_limit) { | ||
| break; | ||
| } | ||
| os << kv.second << '\n'; | ||
| } | ||
| if (limited.m_show_remaining && limited.m_map.size() > limited.m_limit) { | ||
| os << "... (" << (limited.m_map.size() - limited.m_limit) << " more)" << '\n'; | ||
| } | ||
| return os; |
There was a problem hiding this comment.
LimitedMap prints only kv.second and drops the key entirely. For many maps (e.g., std::map<int,std::string>) this loses essential information and makes output ambiguous. Consider printing key: value pairs (or at least providing an option to include the key) so the wrapper is generally useful.
| template <typename K, typename V> | ||
| inline std::ostream& operator<<(std::ostream& os, const std::map<K, V>& map) { | ||
| os << "{"; | ||
| for (const auto& [k, v] : map) { | ||
| os << k << ": " << v << ", "; | ||
| } | ||
| return os << "}"; | ||
| } |
There was a problem hiding this comment.
operator<< for std::map always emits a trailing ", " before the closing } (e.g. {1: one, }). This makes the output look malformed and harder to diff/parse. Consider writing delimiters only between elements (or trimming the last delimiter).
| // Determine which level a cell index belongs to | ||
| // Returns the 0-based level: 0 = root, 1 = first subdivision, 2 = second subdivision, etc. | ||
| uint32_t get_cell_level_from_index(int32_t cell_index) const { | ||
| if (m_quadtree_header.levels == 0 || cell_index == 0) return 0; | ||
|
|
||
| // Find the highest level offset that is <= cell_index | ||
| // Level offsets: offset[0]=0, offset[1]=1, offset[2]=5, offset[3]=21, etc. | ||
| // Cell index 1 is at level 1 (first subdivision), which is 0-indexed as level 0 | ||
| // But we want to return the actual level number (1-based converted to 0-based) | ||
| for (uint32_t level = m_quadtree_header.levels; level > 0; --level) { | ||
| uint32_t offset = calculate_level_offset(level); | ||
| if (static_cast<uint32_t>(cell_index) >= offset) { | ||
| // level is 1-based here, so level 1 -> return 0, level 2 -> return 1, etc. | ||
| return level - 1; | ||
| } |
There was a problem hiding this comment.
The documentation for get_cell_level_from_index says it returns “0 = root, 1 = first subdivision, 2 = second subdivision, …”, but the implementation (and tests) return 0 for level-1 cells and 1 for level-2 cells. This mismatch is confusing for callers. Please either adjust the returned value to match the documented semantics, or update the comment to reflect the current 0-based scheme used by the code/tests.
| // Write raw point data (for cases where you have raw bytes, e.g., with extra bytes) | ||
| // This updates the point count and offset automatically | ||
| void write_raw_point_data(std::span<const std::byte> raw_point_data, size_t num_points) { | ||
| LASPP_ASSERT_LE(m_stage, WritingStage::POINTS); | ||
| LASPP_ASSERT(!m_header.is_laz_compressed(), | ||
| "Raw point data writing not supported for compressed files"); | ||
| LASPP_ASSERT_EQ(raw_point_data.size(), num_points * m_header.point_data_record_length(), | ||
| "Raw point data size doesn't match expected size"); | ||
|
|
||
| // Set stage to POINTS if not already there | ||
| if (m_stage < WritingStage::POINTS) { | ||
| m_stage = WritingStage::POINTS; | ||
| // Update offset_to_point_data to current stream position | ||
| header().m_offset_to_point_data = static_cast<uint32_t>(m_output_stream.tellp()); | ||
| } | ||
|
|
||
| // Write raw point data | ||
| m_output_stream.write(reinterpret_cast<const char*>(raw_point_data.data()), | ||
| static_cast<int64_t>(raw_point_data.size())); | ||
|
|
||
| // Update point count (normally done automatically by write_points()) | ||
| header().m_number_of_point_records = num_points; | ||
| if (header().m_number_of_point_records < std::numeric_limits<uint32_t>::max() && | ||
| (header().point_format() < 6 || | ||
| (header().point_format() >= 128 && header().point_format() < 128 + 6))) { | ||
| header().m_legacy_number_of_point_records = static_cast<uint32_t>(num_points); | ||
| } else { | ||
| header().m_legacy_number_of_point_records = 0; | ||
| } | ||
| } |
There was a problem hiding this comment.
write_raw_point_data updates point counts/offsets but does not update derived header fields like bounds and per-return counts. As a public API this can easily produce a structurally valid file with incorrect metadata. Consider either (a) documenting that the caller must update bounds/return counts before destruction, (b) providing optional parameters to supply/compute these, or (c) making this helper private/internal if it’s only intended for narrow use cases.
No description provided.