diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c111c0821c..332ab4bbfdf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -108,6 +108,8 @@ repos: rev: v2.4.2 hooks: - id: codespell + args: + - --ignore-words-list=LOD # lib/octree/octreelib.dox:85 # TODO: Remove these exclusions, they are just not fixed completely yet, nor ignored exclude_types: - c diff --git a/GRASSConfig.cmake.in b/GRASSConfig.cmake.in index 153f0c05b7d..89e19a0a9ca 100644 --- a/GRASSConfig.cmake.in +++ b/GRASSConfig.cmake.in @@ -36,6 +36,7 @@ set(_GRASS_supported_components manage neta nviz + octree ogsf parson pngdriver diff --git a/grasslib.dox b/grasslib.dox index 003f6a02745..5c05a4683a9 100644 --- a/grasslib.dox +++ b/grasslib.dox @@ -100,6 +100,7 @@ href="https://grass.osgeo.org">https://grass.osgeo.org - btree: \ref btree.h - btree2: \ref btree2 + - octree: \ref octree (3D point octree library) - rtree: \ref rtree.h (R search tree library) \subsection dblibs Database Management Libraries diff --git a/include/Make/Grass.make b/include/Make/Grass.make index 5a1e8cba7d3..967b0e7c535 100644 --- a/include/Make/Grass.make +++ b/include/Make/Grass.make @@ -149,6 +149,7 @@ libs = \ LRS:lrs \ MANAGE:manage \ NVIZ:nviz \ + OCTREE:octree \ OGSF:ogsf \ OPTRI:optri \ PARSON:parson \ @@ -208,6 +209,7 @@ LIDARDEPS = $(VECTORLIB) $(DBMILIB) $(GMATHLIB) $(RASTERLIB) $(SEGMENTLIB LRSDEPS = $(DBMILIB) $(GISLIB) MANAGEDEPS = $(VECTORLIB) $(GISLIB) NVIZDEPS = $(OGSFLIB) $(GISLIB) $(OPENGLLIB) +OCTREEDEPS = $(GISLIB) $(MATHLIB) OGSFDEPS = $(BITMAPLIB) $(RASTER3DLIB) $(VECTORLIB) $(DBMILIB) $(RASTERLIB) $(GISLIB) $(TIFFLIBPATH) $(TIFFLIB) $(OPENGLLIB) $(OPENGLULIB) $(MATHLIB) PNGDRIVERDEPS = $(DRIVERLIB) $(GISLIB) $(PNGLIB) $(MATHLIB) PSDRIVERDEPS = $(DRIVERLIB) $(GISLIB) $(MATHLIB) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 3478b1d4534..36d1c1d9bee 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -134,6 +134,10 @@ build_library_in_subdir(btree) build_library_in_subdir(btree2 HEADERS "kdtree.h" DEPENDS grass_gis) +build_library_in_subdir(octree HEADERS "octree.h" DEPENDS grass_gis ${LIBM}) + +build_program_in_subdir(octree/test NAME test.octree.lib DEPENDS grass_gis grass_octree ${LIBM}) + build_library_in_subdir(display DEFS ${_grass_display_DEFS} DEPENDS ${_grass_display_DEPENDS}) diff --git a/lib/Makefile b/lib/Makefile index 96a23af2069..973f1f6c100 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -19,6 +19,7 @@ SUBDIRS = \ bitmap \ btree \ btree2 \ + octree \ display \ db \ fonts \ diff --git a/lib/octree/Makefile b/lib/octree/Makefile new file mode 100644 index 00000000000..43104e13a49 --- /dev/null +++ b/lib/octree/Makefile @@ -0,0 +1,19 @@ +MODULE_TOPDIR = ../.. + +include $(MODULE_TOPDIR)/include/Make/Vars.make + +LIB = OCTREE + +include $(MODULE_TOPDIR)/include/Make/Lib.make + +HEADERS := $(ARCH_INCDIR)/octree.h + +default: headers + $(MAKE) lib + +headers: $(HEADERS) + +$(ARCH_INCDIR)/octree.h: octree.h + $(INSTALL_DATA) $< $@ + +DOXNAME = octree diff --git a/lib/octree/octree.c b/lib/octree/octree.c new file mode 100644 index 00000000000..2cc80bab036 --- /dev/null +++ b/lib/octree/octree.c @@ -0,0 +1,340 @@ +#include + +#include + +#include "octree.h" + +/* Check whether a node is an internal node (has any children) */ +static int has_children(const OctreeNode *node) +{ + for (int i = 0; i < 8; i++) { + if (node->children[i] != NULL) + return 1; + } + return 0; +} + +/* Create a new octree node with the given bounding box. + Caller must ensure min <= max for each axis. */ +OctreeNode *octree_create_node(double min_x, double max_x, double min_y, + double max_y, double min_z, double max_z) +{ + if (min_x > max_x || min_y > max_y || min_z > max_z) + G_fatal_error("octree_create_node: invalid bounds " + "(min_x=%f max_x=%f min_y=%f max_y=%f " + "min_z=%f max_z=%f)", + min_x, max_x, min_y, max_y, min_z, max_z); + + OctreeNode *node = (OctreeNode *)G_malloc(sizeof(OctreeNode)); + + for (int i = 0; i < 8; i++) + node->children[i] = NULL; + + node->points = NULL; + node->point_count = 0; + node->point_capacity = 0; + node->subtree_count = 0; + node->min_x = min_x; + node->max_x = max_x; + node->min_y = min_y; + node->max_y = max_y; + node->min_z = min_z; + node->max_z = max_z; + node->depth = 0; + + return node; +} + +/* Determine the child octant index for a given point */ +static int get_child_index(const OctreeNode *node, OctreePoint3D point) +{ + int index = 0; + + if (point.x > (node->min_x + node->max_x) / 2) + index |= 1; + if (point.y > (node->min_y + node->max_y) / 2) + index |= 2; + if (point.z > (node->min_z + node->max_z) / 2) + index |= 4; + + return index; +} + +/* Create a child node for the given octant index */ +static OctreeNode *create_child_node(OctreeNode *parent, int index) +{ + double mid_x = (parent->min_x + parent->max_x) / 2; + double mid_y = (parent->min_y + parent->max_y) / 2; + double mid_z = (parent->min_z + parent->max_z) / 2; + + double new_min_x = (index & 1) ? mid_x : parent->min_x; + double new_max_x = (index & 1) ? parent->max_x : mid_x; + double new_min_y = (index & 2) ? mid_y : parent->min_y; + double new_max_y = (index & 2) ? parent->max_y : mid_y; + double new_min_z = (index & 4) ? mid_z : parent->min_z; + double new_max_z = (index & 4) ? parent->max_z : mid_z; + + OctreeNode *child = octree_create_node(new_min_x, new_max_x, new_min_y, + new_max_y, new_min_z, new_max_z); + child->depth = parent->depth + 1; + + return child; +} + +/* Ensure the points array has room for at least the requested capacity. + Grows geometrically so repeated inserts at a max-depth leaf (e.g., many + coincident points) stay amortized O(1) instead of O(n) per insert. */ +static void ensure_points_capacity(OctreeNode *node, size_t needed) +{ + if (node->point_capacity >= needed) + return; + + size_t new_cap = node->point_capacity == 0 + ? (size_t)OCTREE_MAX_POINTS_PER_NODE + : node->point_capacity * 2; + if (new_cap < needed) + new_cap = needed; + + node->points = (OctreePoint3D *)G_realloc(node->points, + sizeof(OctreePoint3D) * new_cap); + node->point_capacity = new_cap; +} + +/* Insert a point into the octree. + Returns 0 on success, -1 if the point is outside the node bounds or has + non-finite coordinates. */ +int octree_insert_point(OctreeNode *node, OctreePoint3D point) +{ + /* Reject non-finite coordinates: NaN compares false against any bound, + which would otherwise silently bypass the bounds check below. */ + if (!isfinite(point.x) || !isfinite(point.y) || !isfinite(point.z)) + return -1; + + /* Validate that the point is within bounds */ + if (point.x < node->min_x || point.x > node->max_x || + point.y < node->min_y || point.y > node->max_y || + point.z < node->min_z || point.z > node->max_z) { + return -1; + } + + /* Internal node: delegate to the appropriate child */ + if (has_children(node)) { + int child_index = get_child_index(node, point); + + if (node->children[child_index] == NULL) + node->children[child_index] = create_child_node(node, child_index); + + int rc = octree_insert_point(node->children[child_index], point); + + if (rc == 0) + node->subtree_count++; + return rc; + } + + /* At max depth, store here regardless of capacity */ + if (node->depth >= OCTREE_MAX_DEPTH) { + ensure_points_capacity(node, node->point_count + 1); + node->points[node->point_count++] = point; + node->subtree_count++; + return 0; + } + + /* Leaf with room: store the point */ + if (node->point_count < OCTREE_MAX_POINTS_PER_NODE) { + ensure_points_capacity(node, node->point_count + 1); + node->points[node->point_count++] = point; + node->subtree_count++; + return 0; + } + + /* Leaf at capacity: subdivide and redistribute existing points. + subtree_count already accounts for these points and stays unchanged; + children's subtree_counts grow as each old point is reinserted. */ + OctreePoint3D *old_points = node->points; + size_t old_count = node->point_count; + + node->points = NULL; + node->point_count = 0; + node->point_capacity = 0; + + for (size_t i = 0; i < old_count; i++) { + int child_index = get_child_index(node, old_points[i]); + + if (node->children[child_index] == NULL) + node->children[child_index] = create_child_node(node, child_index); + + octree_insert_point(node->children[child_index], old_points[i]); + } + G_free(old_points); + + /* Insert the new point into the appropriate child */ + int child_index = get_child_index(node, point); + + if (node->children[child_index] == NULL) + node->children[child_index] = create_child_node(node, child_index); + + int rc = octree_insert_point(node->children[child_index], point); + + if (rc == 0) + node->subtree_count++; + return rc; +} + +/* Recursively free the octree */ +void octree_free(OctreeNode *node) +{ + if (node == NULL) + return; + + for (int i = 0; i < 8; i++) { + if (node->children[i] != NULL) + octree_free(node->children[i]); + } + + if (node->points != NULL) + G_free(node->points); + G_free(node); +} + +/* Whether a node's bounding box intersects the query box (inclusive). */ +static int box_intersects(const OctreeNode *node, double min_x, double max_x, + double min_y, double max_y, double min_z, + double max_z) +{ + return !(node->max_x < min_x || node->min_x > max_x || + node->max_y < min_y || node->min_y > max_y || + node->max_z < min_z || node->min_z > max_z); +} + +/* Whether a point lies inside the inclusive query box. */ +static int point_in_box(OctreePoint3D p, double min_x, double max_x, + double min_y, double max_y, double min_z, double max_z) +{ + return p.x >= min_x && p.x <= max_x && p.y >= min_y && p.y <= max_y && + p.z >= min_z && p.z <= max_z; +} + +/* Recursive core of octree_query_box. Returns 1 to propagate early-stop. */ +static int query_box_recurse(const OctreeNode *node, double min_x, double max_x, + double min_y, double max_y, double min_z, + double max_z, OctreePointVisitor visitor, + void *user_data, size_t *count) +{ + if (!box_intersects(node, min_x, max_x, min_y, max_y, min_z, max_z)) + return 0; + + for (size_t i = 0; i < node->point_count; i++) { + if (!point_in_box(node->points[i], min_x, max_x, min_y, max_y, min_z, + max_z)) + continue; + + (*count)++; + if (visitor != NULL && visitor(&node->points[i], user_data) != 0) + return 1; + } + + for (int i = 0; i < 8; i++) { + if (node->children[i] == NULL) + continue; + + if (query_box_recurse(node->children[i], min_x, max_x, min_y, max_y, + min_z, max_z, visitor, user_data, count)) + return 1; + } + return 0; +} + +size_t octree_query_box(const OctreeNode *node, double min_x, double max_x, + double min_y, double max_y, double min_z, double max_z, + OctreePointVisitor visitor, void *user_data) +{ + if (node == NULL) + return 0; + + size_t count = 0; + + query_box_recurse(node, min_x, max_x, min_y, max_y, min_z, max_z, visitor, + user_data, &count); + return count; +} + +/* Recursive core of octree_visit_to_depth. Returns 1 to propagate early-stop. + */ +static int visit_to_depth_recurse(const OctreeNode *node, int max_depth, + OctreeNodeVisitor visitor, void *user_data) +{ + int is_leaf = !has_children(node); + + if (is_leaf || node->depth >= max_depth) + return visitor(node, user_data) != 0; + + for (int i = 0; i < 8; i++) { + if (node->children[i] == NULL) + continue; + + if (visit_to_depth_recurse(node->children[i], max_depth, visitor, + user_data)) + return 1; + } + return 0; +} + +void octree_visit_to_depth(const OctreeNode *node, int max_depth, + OctreeNodeVisitor visitor, void *user_data) +{ + if (node == NULL || visitor == NULL) + return; + + visit_to_depth_recurse(node, max_depth, visitor, user_data); +} + +size_t octree_subtree_count(const OctreeNode *node) +{ + return node == NULL ? 0 : node->subtree_count; +} + +/* Sum coordinates over an entire subtree for centroid computation. */ +static void accumulate_subtree(const OctreeNode *node, double *sx, double *sy, + double *sz) +{ + for (size_t i = 0; i < node->point_count; i++) { + *sx += node->points[i].x; + *sy += node->points[i].y; + *sz += node->points[i].z; + } + + for (int i = 0; i < 8; i++) { + if (node->children[i] != NULL) + accumulate_subtree(node->children[i], sx, sy, sz); + } +} + +int octree_subtree_representative(const OctreeNode *node, + OctreePoint3D *out_centroid, + size_t *out_count) +{ + if (out_count != NULL) + *out_count = 0; + + if (node == NULL) + return -1; + + size_t count = node->subtree_count; + + if (out_count != NULL) + *out_count = count; + + if (count == 0) + return -1; + + if (out_centroid != NULL) { + double sx = 0.0, sy = 0.0, sz = 0.0; + + accumulate_subtree(node, &sx, &sy, &sz); + + out_centroid->x = sx / (double)count; + out_centroid->y = sy / (double)count; + out_centroid->z = sz / (double)count; + } + return 0; +} diff --git a/lib/octree/octree.h b/lib/octree/octree.h new file mode 100644 index 00000000000..424f9c1484e --- /dev/null +++ b/lib/octree/octree.h @@ -0,0 +1,74 @@ +#ifndef OCTREE_H +#define OCTREE_H + +#include + +#include + +/* Maximum number of points per octree node before splitting */ +#define OCTREE_MAX_POINTS_PER_NODE 10 + +/* Maximum tree depth to prevent infinite recursion with coincident points */ +#define OCTREE_MAX_DEPTH 21 + +/* Point structure */ +typedef struct { + double x, y, z; +} OctreePoint3D; + +/* Octree node structure */ +typedef struct OctreeNode { + struct OctreeNode *children[8]; /* 8 children for octants */ + OctreePoint3D *points; /* Array of points in this node */ + size_t point_count; /* Number of points in this node */ + size_t point_capacity; /* Allocated capacity for points */ + size_t subtree_count; /* Total points in this node's subtree */ + double min_x, max_x, min_y, max_y, min_z, max_z; /* Bounding box */ + int depth; /* Depth of this node in the tree */ +} OctreeNode; + +/* Visitor invoked per matching point in a range query. + Return 0 to continue iteration, non-zero to stop early. + The point pointer aliases internal storage and must not be retained past + the call or across tree modifications. */ +typedef int (*OctreePointVisitor)(const OctreePoint3D *point, void *user_data); + +/* Visitor invoked per terminal node in a depth-limited traversal. + A terminal node is either a true leaf (no children) or a node at the + requested max_depth. Return 0 to continue, non-zero to stop early. */ +typedef int (*OctreeNodeVisitor)(const OctreeNode *node, void *user_data); + +/* Function prototypes */ +OctreeNode *octree_create_node(double min_x, double max_x, double min_y, + double max_y, double min_z, double max_z); +int octree_insert_point(OctreeNode *node, OctreePoint3D point); +void octree_free(OctreeNode *node); + +/* Visit every point inside the inclusive axis-aligned query box. + Subtrees whose bounding box does not intersect the query are pruned. + visitor may be NULL to count matches without a callback. + Returns the number of matching points visited. */ +size_t octree_query_box(const OctreeNode *node, double min_x, double max_x, + double min_y, double max_y, double min_z, double max_z, + OctreePointVisitor visitor, void *user_data); + +/* Traverse the tree, invoking visitor once per terminal node. A node is + treated as terminal when it has no children or when its depth reaches + max_depth. Intended for level-of-detail rendering: pick max_depth based + on zoom and emit one primitive per visited node. */ +void octree_visit_to_depth(const OctreeNode *node, int max_depth, + OctreeNodeVisitor visitor, void *user_data); + +/* Return the cached total number of points in this node's subtree. + Returns 0 for a NULL node. O(1). */ +size_t octree_subtree_count(const OctreeNode *node); + +/* Compute the centroid and total point count of a subtree. + out_centroid and out_count may each be NULL if not needed. + Returns 0 on success, -1 if the subtree contains no points. + The count is O(1) (cached); computing the centroid is O(subtree size). */ +int octree_subtree_representative(const OctreeNode *node, + OctreePoint3D *out_centroid, + size_t *out_count); + +#endif /* OCTREE_H */ diff --git a/lib/octree/octreelib.dox b/lib/octree/octreelib.dox new file mode 100644 index 00000000000..6e2f68738ba --- /dev/null +++ b/lib/octree/octreelib.dox @@ -0,0 +1,129 @@ +/*! \page octree GRASS Octree library + * \brief Octree spatial indexing library + * + * \author Corey White and GRASS Development Team + * + * \par Overview + * This library provides an octree spatial data structure for efficient 3D point + * indexing. An octree recursively subdivides 3D space into eight octants, + * storing points in leaf nodes and subdividing when a node exceeds its + * capacity. + * + * \par Purpose + * The octree library is designed to: + * - Efficiently store and index 3D point data + * - Support dynamic insertion of points + * - Optimize memory usage through adaptive subdivision and lazy allocation + * - Handle degenerate cases (coincident points) via a depth limit + * + * \par Octree vs KD-Tree + * - Octree: Subdivides space into octants, better for uniformly distributed + * data + * - KD-Tree: Splits space along data dimensions, better for clustered data + * - Octree can handle dynamic insertion more efficiently than KD-Tree + * - KD-Tree may require rebalancing after insertions, while octree adapts + * naturally + * - KD-Tree is often faster for low-dimensional data, while octree excels in + * 3D spatial indexing + * + * \par Usage + * + * \section octree_create Creating an Octree + * + * Initialize an octree with spatial bounds: + * \code{.c} + * OctreeNode *tree; + * + * tree = octree_create_node(0.0, 100.0, 0.0, 100.0, 0.0, 100.0); + * \endcode + * + * \section octree_insert Inserting Points + * + * Add 3D points to the octree: + * \code{.c} + * OctreePoint3D p = {25.5, 30.2, 15.8}; + * + * if (octree_insert_point(tree, p) != 0) + * G_warning("Point is outside octree bounds or non-finite"); + * + * // Insert multiple points + * for (int i = 0; i < num_points; i++) { + * OctreePoint3D pt = {x[i], y[i], z[i]}; + * octree_insert_point(tree, pt); + * } + * \endcode + * + * \section octree_cleanup Cleanup + * + * Free octree memory when done: + * \code{.c} + * octree_free(tree); + * \endcode + * + * \section octree_query Querying + * + * Four read-only accessors are provided for traversal and level-of-detail + * rendering: + * + * - octree_query_box() visits every point inside an axis-aligned box, + * pruning subtrees whose bounding box does not intersect the query. Pass + * a NULL visitor to count matches without a callback. + * - octree_visit_to_depth() invokes a visitor once per terminal node: a + * true leaf or a node whose depth reaches the requested cap. Intended + * for LOD rendering -- pick the depth from the current zoom level and + * emit one primitive per visited node. + * - octree_subtree_count() returns the cached total number of points in a + * node's subtree. O(1). + * - octree_subtree_representative() returns the cached count together with + * the centroid of all points in the subtree; useful inside an + * octree_visit_to_depth() callback to produce a single drawable + * representative per LOD cell. + * + * All accept NULL for the root and return a sentinel rather than crashing. + * Visitor callbacks may return non-zero to abort iteration early. Point + * pointers passed to visitors alias internal storage and must not be + * retained across tree modifications. + * + * \par Future: frustum-pruned Level of Detail (LOD) traversal + * octree_visit_to_depth() does not prune by a query box; callers that + * filter inside the visitor still pay traversal cost for every subtree. + * For view-frustum rendering this matters: the per-frame cost should scale + * with what is visible, not with total tree size. A combined primitive + * (depth-limited traversal with box pruning at the internal-node level) is + * intentionally not yet provided. The preferred shape -- hard-coded bbox + * filter vs. caller-supplied "should I descend?" predicate -- depends on + * the first real consumer (e.g., the NVIZ point-cloud LOD path in + * lib/ogsf). Add it when that consumer lands, shaped to its actual needs. + * + * \par Depth Limit + * The tree enforces a maximum depth of OCTREE_MAX_DEPTH (21) to prevent + * infinite recursion when many points share the same coordinates. At max + * depth, leaf nodes grow beyond OCTREE_MAX_POINTS_PER_NODE as needed. + * + * \par Bounds Validation + * octree_insert_point() returns -1 if the point is outside the node's + * bounding box or has non-finite (NaN/infinity) coordinates, and 0 on + * success. + * + * \par Memory Management + * Points arrays are allocated lazily on first insertion and grow + * geometrically (doubling) using G_realloc(). Child nodes are created on + * demand (only the octants that actually receive points are allocated). + * Each node also caches the total point count of its subtree, updated + * during insertion, so octree_subtree_count() is O(1). + * + * \par Performance Considerations + * - Choose OCTREE_MAX_POINTS_PER_NODE based on data density (typical + * values: 8-32). Larger values reduce tree depth but increase search + * time per node; smaller values increase memory overhead but improve + * query performance. + * - Ensure octree bounds encompass all data points for optimal performance. + * - octree_subtree_count() is O(1) thanks to the per-node cache. + * - octree_subtree_representative() returns count in O(1) but the centroid + * is O(subtree size); do not call it per-frame on a large subtree + * without caching the result in the caller. + * + * \par Thread Safety + * The octree structure is not thread-safe. For concurrent access, use + * external synchronization or thread-local copies. + */ diff --git a/lib/octree/test/Makefile b/lib/octree/test/Makefile new file mode 100644 index 00000000000..1e55033a37e --- /dev/null +++ b/lib/octree/test/Makefile @@ -0,0 +1,12 @@ +MODULE_TOPDIR = ../../.. + +PGM = test.octree.lib + +LIBES = $(GISLIB) $(MATHLIB) +DEPENDENCIES = $(GISDEP) +EXTRA_INC = -I.. +EXTRA_CFLAGS = + +include $(MODULE_TOPDIR)/include/Make/Module.make + +default: cmd diff --git a/lib/octree/test/octree_impl.c b/lib/octree/test/octree_impl.c new file mode 100644 index 00000000000..22cec56201b --- /dev/null +++ b/lib/octree/test/octree_impl.c @@ -0,0 +1,2 @@ +/* Include the octree library source for compilation in the test binary */ +#include "../octree.c" diff --git a/lib/octree/test/test_create_node.c b/lib/octree/test/test_create_node.c new file mode 100644 index 00000000000..a4efe306f6f --- /dev/null +++ b/lib/octree/test/test_create_node.c @@ -0,0 +1,251 @@ +/**************************************************************************** + * + * MODULE: test.octree.lib + * + * AUTHOR(S): Corey White + * + * PURPOSE: Unit tests for octree node creation + * + * COPYRIGHT: (C) 2026 by the GRASS Development Team + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + *****************************************************************************/ + +#include +#include + +#include +#include + +#include "test_octree.h" + +/* prototypes */ +static int test_create_node_basic(void); +static int test_create_node_bounds(void); +static int test_create_node_children_null(void); +static int test_create_node_zero_bounds(void); +static int test_create_node_negative_bounds(void); +static int test_create_node_lazy_allocation(void); + +/* ************************************************************************* */ +/* Perform the octree_create_node unit tests ****************************** */ +/* ************************************************************************* */ +int unit_test_create_node(void) +{ + int sum = 0; + + G_message(_("\n++ Running octree_create_node unit tests ++")); + + sum += test_create_node_basic(); + sum += test_create_node_bounds(); + sum += test_create_node_children_null(); + sum += test_create_node_zero_bounds(); + sum += test_create_node_negative_bounds(); + sum += test_create_node_lazy_allocation(); + + if (sum > 0) + G_warning(_("\n-- octree_create_node unit tests failure --")); + else + G_message(_("\n-- octree_create_node unit tests finished " + "successfully --")); + + return sum; +} + +/* ************************************************************************* */ +/* Test basic node creation *********************************************** */ +/* ************************************************************************* */ +static int test_create_node_basic(void) +{ + int sum = 0; + + G_message("\t * testing basic node creation\n"); + + OctreeNode *node = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + if (node == NULL) { + G_warning("octree_create_node returned NULL"); + return 1; + } + + if (node->point_count != 0) { + G_warning("Expected point_count 0, got %zu", node->point_count); + sum++; + } + + if (node->depth != 0) { + G_warning("Expected depth 0, got %d", node->depth); + sum++; + } + + octree_free(node); + return sum; +} + +/* ************************************************************************* */ +/* Test that bounding box values are set correctly ************************ */ +/* ************************************************************************* */ +static int test_create_node_bounds(void) +{ + int sum = 0; + + G_message("\t * testing node bounding box values\n"); + + double min_x = -5.0, max_x = 15.0; + double min_y = -10.0, max_y = 20.0; + double min_z = 0.0, max_z = 100.0; + + OctreeNode *node = + octree_create_node(min_x, max_x, min_y, max_y, min_z, max_z); + + if (node->min_x != min_x) { + G_warning("Expected min_x %f, got %f", min_x, node->min_x); + sum++; + } + if (node->max_x != max_x) { + G_warning("Expected max_x %f, got %f", max_x, node->max_x); + sum++; + } + if (node->min_y != min_y) { + G_warning("Expected min_y %f, got %f", min_y, node->min_y); + sum++; + } + if (node->max_y != max_y) { + G_warning("Expected max_y %f, got %f", max_y, node->max_y); + sum++; + } + if (node->min_z != min_z) { + G_warning("Expected min_z %f, got %f", min_z, node->min_z); + sum++; + } + if (node->max_z != max_z) { + G_warning("Expected max_z %f, got %f", max_z, node->max_z); + sum++; + } + + octree_free(node); + return sum; +} + +/* ************************************************************************* */ +/* Test that all children are initialized to NULL ************************* */ +/* ************************************************************************* */ +static int test_create_node_children_null(void) +{ + int sum = 0; + + G_message("\t * testing that children are initialized to NULL\n"); + + OctreeNode *node = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + for (int i = 0; i < 8; i++) { + if (node->children[i] != NULL) { + G_warning("Expected child[%d] to be NULL", i); + sum++; + } + } + + octree_free(node); + return sum; +} + +/* ************************************************************************* */ +/* Test node creation with zero-sized bounds ****************************** */ +/* ************************************************************************* */ +static int test_create_node_zero_bounds(void) +{ + int sum = 0; + + G_message("\t * testing node creation with zero-sized bounds\n"); + + OctreeNode *node = octree_create_node(5.0, 5.0, 5.0, 5.0, 5.0, 5.0); + + if (node == NULL) { + G_warning("octree_create_node returned NULL for zero-sized bounds"); + return 1; + } + + if (node->min_x != node->max_x || node->min_y != node->max_y || + node->min_z != node->max_z) { + G_warning("Zero-sized bounds not preserved"); + sum++; + } + + octree_free(node); + return sum; +} + +/* ************************************************************************* */ +/* Test node creation with negative coordinate bounds ********************* */ +/* ************************************************************************* */ +static int test_create_node_negative_bounds(void) +{ + int sum = 0; + + G_message("\t * testing node creation with negative bounds\n"); + + OctreeNode *node = + octree_create_node(-100.0, -50.0, -200.0, -100.0, -50.0, -10.0); + + if (node == NULL) { + G_warning("octree_create_node returned NULL for negative bounds"); + return 1; + } + + if (node->min_x != -100.0 || node->max_x != -50.0) { + G_warning("Negative x bounds not preserved"); + sum++; + } + if (node->min_y != -200.0 || node->max_y != -100.0) { + G_warning("Negative y bounds not preserved"); + sum++; + } + if (node->min_z != -50.0 || node->max_z != -10.0) { + G_warning("Negative z bounds not preserved"); + sum++; + } + + octree_free(node); + return sum; +} + +/* ************************************************************************* */ +/* Test that points array is lazily allocated (NULL until first insert) *** */ +/* ************************************************************************* */ +static int test_create_node_lazy_allocation(void) +{ + int sum = 0; + + G_message("\t * testing lazy allocation of points array\n"); + + OctreeNode *node = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + if (node->points != NULL) { + G_warning("Expected NULL points array before first insert"); + sum++; + } + + if (node->point_capacity != 0) { + G_warning("Expected point_capacity 0, got %zu", node->point_capacity); + sum++; + } + + /* After one insert, points should be allocated */ + OctreePoint3D p = {5.0, 5.0, 5.0}; + octree_insert_point(node, p); + + if (node->points == NULL) { + G_warning("Expected non-NULL points array after insert"); + sum++; + } + + if (node->point_capacity < 1) { + G_warning("Expected point_capacity >= 1, got %zu", + node->point_capacity); + sum++; + } + + octree_free(node); + return sum; +} diff --git a/lib/octree/test/test_free_octree.c b/lib/octree/test/test_free_octree.c new file mode 100644 index 00000000000..b9bf5a993e9 --- /dev/null +++ b/lib/octree/test/test_free_octree.c @@ -0,0 +1,119 @@ +/**************************************************************************** + * + * MODULE: test.octree.lib + * + * AUTHOR(S): Corey White + * + * PURPOSE: Unit tests for octree memory deallocation + * + * COPYRIGHT: (C) 2026 by the GRASS Development Team + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + *****************************************************************************/ + +#include +#include + +#include +#include + +#include "test_octree.h" + +/* prototypes */ +static int test_free_null_node(void); +static int test_free_empty_tree(void); +static int test_free_tree_with_points(void); +static int test_free_subdivided_tree(void); + +/* ************************************************************************* */ +/* Perform the octree_free unit tests ************************************* */ +/* ************************************************************************* */ +int unit_test_free_octree(void) +{ + int sum = 0; + + G_message(_("\n++ Running octree_free unit tests ++")); + + sum += test_free_null_node(); + sum += test_free_empty_tree(); + sum += test_free_tree_with_points(); + sum += test_free_subdivided_tree(); + + if (sum > 0) + G_warning(_("\n-- octree_free unit tests failure --")); + else + G_message(_("\n-- octree_free unit tests finished successfully --")); + + return sum; +} + +/* ************************************************************************* */ +/* Test that octree_free handles NULL gracefully ************************** */ +/* ************************************************************************* */ +static int test_free_null_node(void) +{ + G_message("\t * testing octree_free with NULL\n"); + + /* Should not crash */ + octree_free(NULL); + + return 0; +} + +/* ************************************************************************* */ +/* Test freeing an empty tree (no points, no children) ******************** */ +/* ************************************************************************* */ +static int test_free_empty_tree(void) +{ + G_message("\t * testing free of empty tree\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Should not crash; just exercises the deallocation path */ + octree_free(root); + + return 0; +} + +/* ************************************************************************* */ +/* Test freeing a tree that contains points (leaf node) ******************* */ +/* ************************************************************************* */ +static int test_free_tree_with_points(void) +{ + G_message("\t * testing free of tree with points\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + for (int i = 0; i < 5; i++) { + OctreePoint3D p = {(double)i, (double)i, (double)i}; + octree_insert_point(root, p); + } + + /* Should not crash; frees the points array and the node */ + octree_free(root); + + return 0; +} + +/* ************************************************************************* */ +/* Test freeing a subdivided tree (multiple levels) *********************** */ +/* ************************************************************************* */ +static int test_free_subdivided_tree(void) +{ + G_message("\t * testing free of subdivided tree\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert enough diverse points to trigger subdivision and deeper levels */ + for (int i = 0; i <= OCTREE_MAX_POINTS_PER_NODE * 3; i++) { + OctreePoint3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + octree_insert_point(root, p); + } + + /* Should not crash; recursively frees all children and their points */ + octree_free(root); + + return 0; +} diff --git a/lib/octree/test/test_insert_point.c b/lib/octree/test/test_insert_point.c new file mode 100644 index 00000000000..e9c24da07d3 --- /dev/null +++ b/lib/octree/test/test_insert_point.c @@ -0,0 +1,537 @@ +/**************************************************************************** + * + * MODULE: test.octree.lib + * + * AUTHOR(S): Corey White + * + * PURPOSE: Unit tests for octree point insertion + * + * COPYRIGHT: (C) 2026 by the GRASS Development Team + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + *****************************************************************************/ + +#include +#include +#include + +#include +#include + +#include "test_octree.h" + +/* prototypes */ +static int test_insert_single_point(void); +static int test_insert_fill_node(void); +static int test_insert_triggers_subdivision(void); +static int test_insert_points_into_all_octants(void); +static int test_insert_duplicate_points(void); +static int test_insert_boundary_point(void); +static int test_insert_after_subdivision(void); +static int test_insert_many_duplicates(void); +static int test_insert_out_of_bounds(void); +static int test_insert_non_finite(void); +static int test_insert_capacity_growth(void); + +/* ************************************************************************* */ +/* Perform the octree_insert_point unit tests + * ************************************ */ +/* ************************************************************************* */ +int unit_test_insert_point(void) +{ + int sum = 0; + + G_message(_("\n++ Running octree_insert_point unit tests ++")); + + sum += test_insert_single_point(); + sum += test_insert_fill_node(); + sum += test_insert_triggers_subdivision(); + sum += test_insert_points_into_all_octants(); + sum += test_insert_duplicate_points(); + sum += test_insert_boundary_point(); + sum += test_insert_after_subdivision(); + sum += test_insert_many_duplicates(); + sum += test_insert_out_of_bounds(); + sum += test_insert_non_finite(); + sum += test_insert_capacity_growth(); + + if (sum > 0) + G_warning(_("\n-- octree_insert_point unit tests failure --")); + else + G_message( + _("\n-- octree_insert_point unit tests finished successfully --")); + + return sum; +} + +/* ************************************************************************* */ +/* Test inserting a single point ****************************************** */ +/* ************************************************************************* */ +static int test_insert_single_point(void) +{ + int sum = 0; + + G_message("\t * testing single point insertion\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreePoint3D p = {5.0, 5.0, 5.0}; + + int ret = octree_insert_point(root, p); + + if (ret != 0) { + G_warning("octree_insert_point returned %d, expected 0", ret); + sum++; + } + + if (root->point_count != 1) { + G_warning("Expected point_count 1, got %zu", root->point_count); + sum++; + } + + if (root->points[0].x != 5.0 || root->points[0].y != 5.0 || + root->points[0].z != 5.0) { + G_warning("Inserted point coordinates do not match"); + sum++; + } + + /* Verify no children were created */ + for (int i = 0; i < 8; i++) { + if (root->children[i] != NULL) { + G_warning("Expected no children after single insert"); + sum++; + break; + } + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test filling a node to capacity without subdivision ******************** */ +/* ************************************************************************* */ +static int test_insert_fill_node(void) +{ + int sum = 0; + + G_message("\t * testing filling node to capacity\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert exactly OCTREE_MAX_POINTS_PER_NODE points */ + for (int i = 0; i < OCTREE_MAX_POINTS_PER_NODE; i++) { + OctreePoint3D p = {(double)i, (double)i, (double)i}; + octree_insert_point(root, p); + } + + if (root->point_count != OCTREE_MAX_POINTS_PER_NODE) { + G_warning("Expected point_count %d, got %zu", + OCTREE_MAX_POINTS_PER_NODE, root->point_count); + sum++; + } + + /* Verify no children were created (node is at capacity, not over) */ + for (int i = 0; i < 8; i++) { + if (root->children[i] != NULL) { + G_warning("Expected no children when node is at capacity"); + sum++; + break; + } + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test that exceeding capacity triggers subdivision ********************** */ +/* ************************************************************************* */ +static int test_insert_triggers_subdivision(void) +{ + int sum = 0; + + G_message("\t * testing subdivision on capacity overflow\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert OCTREE_MAX_POINTS_PER_NODE + 1 points to trigger subdivision */ + for (int i = 0; i <= OCTREE_MAX_POINTS_PER_NODE; i++) { + OctreePoint3D p = {(double)i * 0.5, (double)i * 0.5, (double)i * 0.5}; + octree_insert_point(root, p); + } + + /* After subdivision, root should have at least one child */ + int has_children = 0; + + for (int i = 0; i < 8; i++) { + if (root->children[i] != NULL) { + has_children = 1; + break; + } + } + + if (!has_children) { + G_warning("Expected children after exceeding capacity"); + sum++; + } + + /* After subdivision, root's points should be cleared */ + if (root->point_count != 0) { + G_warning("Expected root point_count 0 after subdivision, got %zu", + root->point_count); + sum++; + } + + if (root->points != NULL) { + G_warning("Expected root points to be NULL after subdivision"); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test inserting points into all 8 octants ******************************* */ +/* ************************************************************************* */ +static int test_insert_points_into_all_octants(void) +{ + int sum = 0; + + G_message("\t * testing insertion into all 8 octants\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Fill to capacity first, then add points in distinct octants to + force subdivision */ + for (int i = 0; i < OCTREE_MAX_POINTS_PER_NODE; i++) { + OctreePoint3D p = {5.0, 5.0, 5.0}; + octree_insert_point(root, p); + } + + /* These 8 points should land in each of the 8 octants after subdivision */ + OctreePoint3D octant_points[8] = { + {1.0, 1.0, 1.0}, /* octant 0: low x, low y, low z */ + {9.0, 1.0, 1.0}, /* octant 1: high x, low y, low z */ + {1.0, 9.0, 1.0}, /* octant 2: low x, high y, low z */ + {9.0, 9.0, 1.0}, /* octant 3: high x, high y, low z */ + {1.0, 1.0, 9.0}, /* octant 4: low x, low y, high z */ + {9.0, 1.0, 9.0}, /* octant 5: high x, low y, high z */ + {1.0, 9.0, 9.0}, /* octant 6: low x, high y, high z */ + {9.0, 9.0, 9.0}, /* octant 7: high x, high y, high z */ + }; + + for (int i = 0; i < 8; i++) { + octree_insert_point(root, octant_points[i]); + } + + /* Verify children exist for all 8 octants */ + for (int i = 0; i < 8; i++) { + if (root->children[i] == NULL) { + G_warning("Expected child[%d] to exist after octant insertion", i); + sum++; + } + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test inserting duplicate points at the same location ******************* */ +/* ************************************************************************* */ +static int test_insert_duplicate_points(void) +{ + int sum = 0; + + G_message("\t * testing insertion of duplicate points\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert a few identical points (within capacity) */ + for (int i = 0; i < 5; i++) { + OctreePoint3D p = {3.0, 3.0, 3.0}; + octree_insert_point(root, p); + } + + if (root->point_count != 5) { + G_warning("Expected point_count 5 for duplicates, got %zu", + root->point_count); + sum++; + } + + /* Verify all stored points have the same coordinates */ + for (int i = 0; i < 5; i++) { + if (root->points[i].x != 3.0 || root->points[i].y != 3.0 || + root->points[i].z != 3.0) { + G_warning("Duplicate point %d has wrong coordinates", i); + sum++; + } + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test inserting a point on the boundary of the bounding box ************* */ +/* ************************************************************************* */ +static int test_insert_boundary_point(void) +{ + int sum = 0; + + G_message("\t * testing boundary point insertion\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert point at the exact midpoint (boundary between octants) */ + OctreePoint3D mid = {5.0, 5.0, 5.0}; + octree_insert_point(root, mid); + + if (root->point_count != 1) { + G_warning("Expected point_count 1 for boundary point, got %zu", + root->point_count); + sum++; + } + + /* Insert point at exact min corner */ + OctreePoint3D corner = {0.0, 0.0, 0.0}; + octree_insert_point(root, corner); + + if (root->point_count != 2) { + G_warning("Expected point_count 2 after corner insert, got %zu", + root->point_count); + sum++; + } + + /* Insert point at exact max corner */ + OctreePoint3D max_corner = {10.0, 10.0, 10.0}; + octree_insert_point(root, max_corner); + + if (root->point_count != 3) { + G_warning("Expected point_count 3 after max corner insert, got %zu", + root->point_count); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test continued insertion after subdivision (was a NULL deref bug) ****** */ +/* ************************************************************************* */ +static int test_insert_after_subdivision(void) +{ + int sum = 0; + + G_message("\t * testing insertion after subdivision\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Trigger subdivision */ + for (int i = 0; i <= OCTREE_MAX_POINTS_PER_NODE; i++) { + OctreePoint3D p = {(double)i * 0.9, (double)i * 0.9, (double)i * 0.9}; + octree_insert_point(root, p); + } + + /* Verify subdivision happened */ + int has_children = 0; + + for (int i = 0; i < 8; i++) { + if (root->children[i] != NULL) { + has_children = 1; + break; + } + } + + if (!has_children) { + G_warning("Subdivision did not happen"); + return 1; + } + + /* Insert more points after subdivision; this must not crash */ + for (int i = 0; i < 20; i++) { + OctreePoint3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + int ret = octree_insert_point(root, p); + + if (ret != 0) { + G_warning("octree_insert_point returned %d for in-bounds point", + ret); + sum++; + } + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test that many coincident points do not cause infinite recursion ******* */ +/* ************************************************************************* */ +static int test_insert_many_duplicates(void) +{ + int sum = 0; + + G_message("\t * testing many coincident points (depth limit)\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert many identical points, well past OCTREE_MAX_POINTS_PER_NODE. + Without the depth limit, this would recurse infinitely. */ + int count = OCTREE_MAX_POINTS_PER_NODE * 5; + + for (int i = 0; i < count; i++) { + OctreePoint3D p = {3.0, 3.0, 3.0}; + int ret = octree_insert_point(root, p); + + if (ret != 0) { + G_warning("octree_insert_point returned %d for duplicate point %d", + ret, i); + sum++; + } + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test that out-of-bounds points are rejected ***************************** */ +/* ************************************************************************* */ +static int test_insert_out_of_bounds(void) +{ + int sum = 0; + + G_message("\t * testing out-of-bounds point rejection\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + OctreePoint3D outside = {-1.0, 5.0, 5.0}; + + if (octree_insert_point(root, outside) != -1) { + G_warning("Expected -1 for point with x below min_x"); + sum++; + } + + OctreePoint3D above = {5.0, 5.0, 15.0}; + + if (octree_insert_point(root, above) != -1) { + G_warning("Expected -1 for point with z above max_z"); + sum++; + } + + /* Verify no points were stored */ + if (root->point_count != 0) { + G_warning("Expected point_count 0 after rejected inserts, got %zu", + root->point_count); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test that non-finite coordinates (NaN, infinity) are rejected *********** */ +/* ************************************************************************* */ +static int test_insert_non_finite(void) +{ + int sum = 0; + + G_message("\t * testing non-finite coordinate rejection\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + OctreePoint3D nan_point = {NAN, 5.0, 5.0}; + + if (octree_insert_point(root, nan_point) != -1) { + G_warning("Expected -1 for NaN x coordinate"); + sum++; + } + + OctreePoint3D inf_point = {5.0, INFINITY, 5.0}; + + if (octree_insert_point(root, inf_point) != -1) { + G_warning("Expected -1 for infinite y coordinate"); + sum++; + } + + OctreePoint3D neg_inf = {5.0, 5.0, -INFINITY}; + + if (octree_insert_point(root, neg_inf) != -1) { + G_warning("Expected -1 for -infinity z coordinate"); + sum++; + } + + if (root->point_count != 0) { + G_warning("Expected point_count 0 after rejecting non-finite, got %zu", + root->point_count); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +/* Test geometric capacity growth at max depth (no O(n^2) realloc) ******** */ +/* ************************************************************************* */ +static int test_insert_capacity_growth(void) +{ + int sum = 0; + + G_message("\t * testing geometric capacity growth with many duplicates\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert enough coincident points to push a max-depth leaf well past + OCTREE_MAX_POINTS_PER_NODE and exercise multiple capacity doublings. */ + int count = OCTREE_MAX_POINTS_PER_NODE * 20; + + for (int i = 0; i < count; i++) { + OctreePoint3D p = {7.0, 7.0, 7.0}; + + if (octree_insert_point(root, p) != 0) { + G_warning("octree_insert_point failed on duplicate %d", i); + sum++; + } + } + + /* Descend to the deepest leaf holding these points and verify the + capacity grew geometrically (>= count, and at least double the + previous power-of-two threshold). */ + OctreeNode *n = root; + + while (n != NULL && n->points == NULL) { + OctreeNode *next = NULL; + + for (int i = 0; i < 8; i++) { + if (n->children[i] != NULL) { + next = n->children[i]; + break; + } + } + n = next; + } + + if (n == NULL) { + G_warning("Could not locate leaf holding duplicates"); + sum++; + } + else if (n->point_count != (size_t)count) { + G_warning("Expected leaf point_count %d, got %zu", count, + n->point_count); + sum++; + } + else if (n->point_capacity < n->point_count) { + G_warning("Leaf capacity %zu < count %zu", n->point_capacity, + n->point_count); + sum++; + } + + octree_free(root); + return sum; +} diff --git a/lib/octree/test/test_main.c b/lib/octree/test/test_main.c new file mode 100644 index 00000000000..b5ea1896872 --- /dev/null +++ b/lib/octree/test/test_main.c @@ -0,0 +1,73 @@ +/**************************************************************************** + * + * MODULE: test.octree.lib + * + * AUTHOR(S): Corey White + * + * PURPOSE: Unit tests for the octree library + * + * COPYRIGHT: (C) 2026 by the GRASS Development Team + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + *****************************************************************************/ + +#include +#include + +#include +#include + +#include "test_octree.h" + +typedef struct { + struct Flag *testunit, *full; +} paramType; + +paramType param; + +static void set_params(void); + +void set_params(void) +{ + param.testunit = G_define_flag(); + param.testunit->key = 'u'; + param.testunit->description = _("Run all unit tests"); + + param.full = G_define_flag(); + param.full->key = 'a'; + param.full->description = _("Run all unit and integration tests"); +} + +int main(int argc, char *argv[]) +{ + struct GModule *module; + int returnstat = 0; + + /* Initialize GRASS */ + G_gisinit(argv[0]); + + module = G_define_module(); + module->description = _("Performs unit tests for the octree library"); + + /* Get parameters from user */ + set_params(); + + if (G_parser(argc, argv)) + exit(EXIT_FAILURE); + + /* Run all unit tests */ + if (param.testunit->answer || param.full->answer) { + returnstat += unit_test_create_node(); + returnstat += unit_test_insert_point(); + returnstat += unit_test_free_octree(); + returnstat += unit_test_query(); + } + + if (returnstat != 0) + G_warning(_("Errors detected while testing the octree lib")); + else + G_message(_("\n-- octree lib tests finished successfully --")); + + return returnstat; +} diff --git a/lib/octree/test/test_octree.h b/lib/octree/test/test_octree.h new file mode 100644 index 00000000000..eafb4689b33 --- /dev/null +++ b/lib/octree/test/test_octree.h @@ -0,0 +1,26 @@ +/**************************************************************************** + * + * MODULE: test.octree.lib + * + * AUTHOR(S): Corey White + * + * PURPOSE: Unit tests for the octree library + * + * COPYRIGHT: (C) 2026 by the GRASS Development Team + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + *****************************************************************************/ + +#ifndef TEST_OCTREE_H +#define TEST_OCTREE_H + +#include "octree.h" + +/* Unit test function prototypes */ +int unit_test_create_node(void); +int unit_test_insert_point(void); +int unit_test_free_octree(void); +int unit_test_query(void); + +#endif /* TEST_OCTREE_H */ diff --git a/lib/octree/test/test_query.c b/lib/octree/test/test_query.c new file mode 100644 index 00000000000..72dcb506896 --- /dev/null +++ b/lib/octree/test/test_query.c @@ -0,0 +1,644 @@ +/**************************************************************************** + * + * MODULE: test.octree.lib + * + * AUTHOR(S): Corey White + * + * PURPOSE: Unit tests for octree range query, depth-limited traversal, + * and subtree representative accessor + * + * COPYRIGHT: (C) 2026 by the GRASS Development Team + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + *****************************************************************************/ + +#include +#include +#include + +#include +#include + +#include "test_octree.h" + +/* prototypes */ +static int test_query_empty_tree(void); +static int test_query_null_visitor_counts(void); +static int test_query_disjoint_box(void); +static int test_query_partial_overlap(void); +static int test_query_early_stop(void); +static int test_query_collects_points(void); +static int test_visit_to_depth_root_only(void); +static int test_visit_to_depth_full(void); +static int test_visit_to_depth_cap(void); +static int test_representative_null(void); +static int test_representative_empty(void); +static int test_representative_single(void); +static int test_representative_centroid(void); +static int test_subtree_count_null_and_empty(void); +static int test_subtree_count_matches_inserts(void); +static int test_subtree_count_after_subdivision(void); +static int test_subtree_count_ignores_rejected(void); + +/* Visitor state used by several range-query tests. */ +typedef struct { + size_t calls; + size_t stop_after; + OctreePoint3D last; +} VisitorState; + +static int count_visitor(const OctreePoint3D *p, void *user_data) +{ + VisitorState *s = (VisitorState *)user_data; + + (void)p; + s->calls++; + return 0; +} + +static int stopping_visitor(const OctreePoint3D *p, void *user_data) +{ + VisitorState *s = (VisitorState *)user_data; + + s->last = *p; + s->calls++; + return s->calls >= s->stop_after ? 1 : 0; +} + +static int terminal_counter(const OctreeNode *node, void *user_data) +{ + size_t *n = (size_t *)user_data; + + (void)node; + (*n)++; + return 0; +} + +/* ************************************************************************* */ +/* Driver *********************************************************** */ +/* ************************************************************************* */ +int unit_test_query(void) +{ + int sum = 0; + + G_message(_("\n++ Running octree query unit tests ++")); + + sum += test_query_empty_tree(); + sum += test_query_null_visitor_counts(); + sum += test_query_disjoint_box(); + sum += test_query_partial_overlap(); + sum += test_query_early_stop(); + sum += test_query_collects_points(); + sum += test_visit_to_depth_root_only(); + sum += test_visit_to_depth_full(); + sum += test_visit_to_depth_cap(); + sum += test_representative_null(); + sum += test_representative_empty(); + sum += test_representative_single(); + sum += test_representative_centroid(); + sum += test_subtree_count_null_and_empty(); + sum += test_subtree_count_matches_inserts(); + sum += test_subtree_count_after_subdivision(); + sum += test_subtree_count_ignores_rejected(); + + if (sum > 0) + G_warning(_("\n-- octree query unit tests failure --")); + else + G_message(_("\n-- octree query unit tests finished successfully --")); + + return sum; +} + +/* ************************************************************************* */ +static int test_query_empty_tree(void) +{ + int sum = 0; + + G_message("\t * testing query on empty tree\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + VisitorState s = {0, 0, {0, 0, 0}}; + size_t n = octree_query_box(root, 0, 10, 0, 10, 0, 10, count_visitor, &s); + + if (n != 0 || s.calls != 0) { + G_warning("Expected 0 matches on empty tree, got n=%zu calls=%zu", n, + s.calls); + sum++; + } + + if (octree_query_box(NULL, 0, 10, 0, 10, 0, 10, count_visitor, &s) != 0) { + G_warning("Expected 0 matches for NULL tree"); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_query_null_visitor_counts(void) +{ + int sum = 0; + + G_message("\t * testing query counts with NULL visitor\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + for (int i = 0; i < 7; i++) { + OctreePoint3D p = {(double)i, (double)i, (double)i}; + octree_insert_point(root, p); + } + + size_t n = octree_query_box(root, 0, 10, 0, 10, 0, 10, NULL, NULL); + + if (n != 7) { + G_warning("Expected 7 matches with NULL visitor, got %zu", n); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_query_disjoint_box(void) +{ + int sum = 0; + + G_message("\t * testing query with disjoint box prunes entire tree\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + for (int i = 0; i < 20; i++) { + OctreePoint3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + octree_insert_point(root, p); + } + + VisitorState s = {0, 0, {0, 0, 0}}; + size_t n = + octree_query_box(root, 100, 200, 100, 200, 100, 200, count_visitor, &s); + + if (n != 0 || s.calls != 0) { + G_warning("Expected 0 matches for disjoint box, got n=%zu calls=%zu", n, + s.calls); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_query_partial_overlap(void) +{ + int sum = 0; + + G_message("\t * testing query with partial overlap\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreePoint3D inside_pts[] = {{1, 1, 1}, {2, 2, 2}, {3, 3, 3}}; + OctreePoint3D outside_pts[] = {{8, 8, 8}, {9, 9, 9}}; + + for (size_t i = 0; i < sizeof(inside_pts) / sizeof(inside_pts[0]); i++) + octree_insert_point(root, inside_pts[i]); + for (size_t i = 0; i < sizeof(outside_pts) / sizeof(outside_pts[0]); i++) + octree_insert_point(root, outside_pts[i]); + + VisitorState s = {0, 0, {0, 0, 0}}; + size_t n = octree_query_box(root, 0, 4, 0, 4, 0, 4, count_visitor, &s); + + if (n != 3 || s.calls != 3) { + G_warning("Expected 3 matches, got n=%zu calls=%zu", n, s.calls); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_query_early_stop(void) +{ + int sum = 0; + + G_message("\t * testing query early-stop on visitor return\n"); + + OctreeNode *root = octree_create_node(0.0, 100.0, 0.0, 100.0, 0.0, 100.0); + + /* Insert enough to force subdivision and guarantee multiple leaves. */ + for (int i = 0; i < OCTREE_MAX_POINTS_PER_NODE * 3; i++) { + OctreePoint3D p = {(double)i, (double)i, (double)i}; + octree_insert_point(root, p); + } + + VisitorState s = {0, 3, {0, 0, 0}}; + size_t n = + octree_query_box(root, 0, 100, 0, 100, 0, 100, stopping_visitor, &s); + + if (s.calls != 3) { + G_warning("Expected visitor stop after 3 calls, got %zu", s.calls); + sum++; + } + + /* octree_query_box returns the count of matched points; early-stop + leaves the count at the stopping call. */ + if (n != 3) { + G_warning("Expected returned count 3 after stop, got %zu", n); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_query_collects_points(void) +{ + int sum = 0; + + G_message("\t * testing query returns correct points after subdivision\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Force subdivision by exceeding OCTREE_MAX_POINTS_PER_NODE in one octant + and spreading the rest across others. */ + for (int i = 0; i <= OCTREE_MAX_POINTS_PER_NODE; i++) { + OctreePoint3D p = {0.5 + i * 0.01, 0.5, 0.5}; + octree_insert_point(root, p); + } + OctreePoint3D far_pts[] = {{9, 9, 9}, {8, 1, 1}, {1, 8, 1}}; + + for (size_t i = 0; i < sizeof(far_pts) / sizeof(far_pts[0]); i++) + octree_insert_point(root, far_pts[i]); + + VisitorState s = {0, 0, {0, 0, 0}}; + size_t n = octree_query_box(root, 0, 1, 0, 1, 0, 1, count_visitor, &s); + + if (n != (size_t)(OCTREE_MAX_POINTS_PER_NODE + 1)) { + G_warning("Expected %d matches in {0..1}^3, got %zu", + OCTREE_MAX_POINTS_PER_NODE + 1, n); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_visit_to_depth_root_only(void) +{ + int sum = 0; + + G_message("\t * testing visit_to_depth with max_depth=0\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + for (int i = 0; i < OCTREE_MAX_POINTS_PER_NODE * 2; i++) { + OctreePoint3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + octree_insert_point(root, p); + } + + size_t visits = 0; + + octree_visit_to_depth(root, 0, terminal_counter, &visits); + + if (visits != 1) { + G_warning("Expected 1 visit at max_depth=0, got %zu", visits); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_visit_to_depth_full(void) +{ + int sum = 0; + + G_message("\t * testing visit_to_depth covers every leaf\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Unsubdivided leaf: single terminal node. */ + OctreePoint3D p = {1, 1, 1}; + + octree_insert_point(root, p); + + size_t visits = 0; + + octree_visit_to_depth(root, 100, terminal_counter, &visits); + + if (visits != 1) { + G_warning("Expected 1 leaf visit on unsubdivided tree, got %zu", + visits); + sum++; + } + + /* Force subdivision into two distinct octants. */ + octree_free(root); + root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + for (int i = 0; i < OCTREE_MAX_POINTS_PER_NODE + 1; i++) { + OctreePoint3D q = {1.0, 1.0, 1.0}; + octree_insert_point(root, q); + } + OctreePoint3D far = {9, 9, 9}; + + octree_insert_point(root, far); + + visits = 0; + octree_visit_to_depth(root, 100, terminal_counter, &visits); + + /* The coincident points collapse into one deep leaf; far lands in its + own child. At minimum we expect 2 terminal nodes. */ + if (visits < 2) { + G_warning("Expected >= 2 terminal visits after subdivision, got %zu", + visits); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_visit_to_depth_cap(void) +{ + int sum = 0; + + G_message("\t * testing visit_to_depth honors depth cap\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Many coincident points force deep subdivision (up to max depth). */ + for (int i = 0; i < OCTREE_MAX_POINTS_PER_NODE * 4; i++) { + OctreePoint3D p = {5.0, 5.0, 5.0}; + octree_insert_point(root, p); + } + + size_t visits_shallow = 0; + size_t visits_deep = 0; + + octree_visit_to_depth(root, 1, terminal_counter, &visits_shallow); + octree_visit_to_depth(root, 50, terminal_counter, &visits_deep); + + /* Shallow cap should visit no more than deep traversal. */ + if (visits_shallow > visits_deep) { + G_warning("Shallow visits (%zu) exceed deep visits (%zu)", + visits_shallow, visits_deep); + sum++; + } + if (visits_shallow == 0) { + G_warning("Expected at least one terminal at depth cap 1"); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_representative_null(void) +{ + int sum = 0; + + G_message("\t * testing representative on NULL node\n"); + + OctreePoint3D c; + size_t n = 42; + + if (octree_subtree_representative(NULL, &c, &n) != -1) { + G_warning("Expected -1 for NULL node"); + sum++; + } + if (n != 0) { + G_warning("Expected out_count=0 for NULL node, got %zu", n); + sum++; + } + + return sum; +} + +/* ************************************************************************* */ +static int test_representative_empty(void) +{ + int sum = 0; + + G_message("\t * testing representative on empty subtree\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreePoint3D c = {1, 2, 3}; + size_t n = 99; + + if (octree_subtree_representative(root, &c, &n) != -1) { + G_warning("Expected -1 for empty subtree"); + sum++; + } + if (n != 0) { + G_warning("Expected count 0 for empty subtree, got %zu", n); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_representative_single(void) +{ + int sum = 0; + + G_message("\t * testing representative of a single point\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreePoint3D p = {2.5, 3.5, 4.5}; + + octree_insert_point(root, p); + + OctreePoint3D c; + size_t n = 0; + + if (octree_subtree_representative(root, &c, &n) != 0) { + G_warning("Expected success for single-point subtree"); + sum++; + } + if (n != 1 || c.x != p.x || c.y != p.y || c.z != p.z) { + G_warning("Unexpected representative: n=%zu c=(%f,%f,%f)", n, c.x, c.y, + c.z); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_representative_centroid(void) +{ + int sum = 0; + + G_message("\t * testing representative centroid across subdivision\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Spread points so subdivision is triggered; average is (4.5, 4.5, 4.5) */ + OctreePoint3D pts[] = {{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}, + {5, 5, 5}, {6, 6, 6}, {7, 7, 7}, {8, 8, 8}, + {9, 9, 9}, {1, 9, 1}, {9, 1, 9}, {1, 1, 9}}; + size_t total = sizeof(pts) / sizeof(pts[0]); + double sx = 0, sy = 0, sz = 0; + + for (size_t i = 0; i < total; i++) { + octree_insert_point(root, pts[i]); + sx += pts[i].x; + sy += pts[i].y; + sz += pts[i].z; + } + + OctreePoint3D c; + size_t n = 0; + + octree_subtree_representative(root, &c, &n); + + if (n != total) { + G_warning("Expected count %zu, got %zu", total, n); + sum++; + } + + double tol = 1e-9; + + if (fabs(c.x - sx / total) > tol || fabs(c.y - sy / total) > tol || + fabs(c.z - sz / total) > tol) { + G_warning("Centroid mismatch: got (%f,%f,%f) expected (%f,%f,%f)", c.x, + c.y, c.z, sx / total, sy / total, sz / total); + sum++; + } + + /* Passing NULL for both out parameters should still succeed (return + only reports 0/-1); just verify no crash. */ + if (octree_subtree_representative(root, NULL, NULL) != 0) { + G_warning("Expected success when out parameters are NULL"); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_subtree_count_null_and_empty(void) +{ + int sum = 0; + + G_message("\t * testing subtree_count on NULL and empty trees\n"); + + if (octree_subtree_count(NULL) != 0) { + G_warning("Expected 0 count for NULL node"); + sum++; + } + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + if (octree_subtree_count(root) != 0) { + G_warning("Expected 0 count for empty tree"); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_subtree_count_matches_inserts(void) +{ + int sum = 0; + + G_message("\t * testing subtree_count matches insert count\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + size_t n = 7; + + for (size_t i = 0; i < n; i++) { + OctreePoint3D p = {(double)i, (double)i, (double)i}; + octree_insert_point(root, p); + } + + if (octree_subtree_count(root) != n) { + G_warning("Expected root subtree_count %zu, got %zu", n, + octree_subtree_count(root)); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_subtree_count_after_subdivision(void) +{ + int sum = 0; + + G_message("\t * testing subtree_count after subdivision sums children\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + size_t n = OCTREE_MAX_POINTS_PER_NODE * 4 + 3; + + for (size_t i = 0; i < n; i++) { + OctreePoint3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + octree_insert_point(root, p); + } + + if (octree_subtree_count(root) != n) { + G_warning("Root subtree_count %zu != inserted %zu", + octree_subtree_count(root), n); + sum++; + } + + /* Root must be internal after this many inserts; verify each child's + count sums to the root's. */ + size_t child_total = 0; + + for (int i = 0; i < 8; i++) { + if (root->children[i] != NULL) + child_total += octree_subtree_count(root->children[i]); + } + + if (child_total != n) { + G_warning("Sum of child counts %zu != root count %zu", child_total, n); + sum++; + } + + octree_free(root); + return sum; +} + +/* ************************************************************************* */ +static int test_subtree_count_ignores_rejected(void) +{ + int sum = 0; + + G_message("\t * testing subtree_count ignores rejected inserts\n"); + + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + OctreePoint3D good = {1, 1, 1}; + OctreePoint3D out_of_bounds = {100, 100, 100}; + OctreePoint3D nan_point = {NAN, 1, 1}; + + octree_insert_point(root, good); + octree_insert_point(root, out_of_bounds); + octree_insert_point(root, nan_point); + + if (octree_subtree_count(root) != 1) { + G_warning("Expected count 1 after 2 rejected inserts, got %zu", + octree_subtree_count(root)); + sum++; + } + + octree_free(root); + return sum; +}