From fec126440821630eee445a6b0222996748e778ee Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Tue, 24 Feb 2026 15:52:03 -0500 Subject: [PATCH 1/6] Added octree data structure --- lib/octree/Makefile | 21 ++ lib/octree/main.c | 66 ++++++ lib/octree/octree.c | 100 +++++++++ lib/octree/octree.h | 28 +++ lib/octree/octreelib.dox | 115 +++++++++++ lib/octree/test/Makefile | 12 ++ lib/octree/test/octree_impl.c | 2 + lib/octree/test/test_create_node.c | 209 +++++++++++++++++++ lib/octree/test/test_free_octree.c | 119 +++++++++++ lib/octree/test/test_insert_point.c | 302 ++++++++++++++++++++++++++++ lib/octree/test/test_main.c | 80 ++++++++ lib/octree/test/test_octree.h | 25 +++ 12 files changed, 1079 insertions(+) create mode 100644 lib/octree/Makefile create mode 100644 lib/octree/main.c create mode 100644 lib/octree/octree.c create mode 100644 lib/octree/octree.h create mode 100644 lib/octree/octreelib.dox create mode 100644 lib/octree/test/Makefile create mode 100644 lib/octree/test/octree_impl.c create mode 100644 lib/octree/test/test_create_node.c create mode 100644 lib/octree/test/test_free_octree.c create mode 100644 lib/octree/test/test_insert_point.c create mode 100644 lib/octree/test/test_main.c create mode 100644 lib/octree/test/test_octree.h diff --git a/lib/octree/Makefile b/lib/octree/Makefile new file mode 100644 index 00000000000..130b5f8deec --- /dev/null +++ b/lib/octree/Makefile @@ -0,0 +1,21 @@ +MODULE_TOPDIR = ../.. + +include $(MODULE_TOPDIR)/include/Make/Vars.make + +LIB = OCTREE + +MOD_OBJS := $(filter-out try.o,$(AUTO_OBJS) + +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/main.c b/lib/octree/main.c new file mode 100644 index 00000000000..3ee5cf73af2 --- /dev/null +++ b/lib/octree/main.c @@ -0,0 +1,66 @@ +#include "octree.h" +#include +#include +#include +#include + +// Main function for the GRASS module +int main(int argc, char *argv[]) +{ + struct GModule *module; + struct Option *input, *output; + struct Map_info In, Out; + + G_gisinit(argv[0]); + module = G_define_module(); + module->description = "Builds an octree from a vector point map."; + + input = G_define_standard_option(G_OPT_V_INPUT); + output = G_define_standard_option(G_OPT_V_OUTPUT); + + if (G_parser(argc, argv)) + exit(EXIT_FAILURE); + + // Open the input vector map + if (Vect_open_old(&In, input->answer, "") < 0) + G_fatal_error("Unable to open vector map <%s>", input->answer); + + // Determine the bounding box of the input points + BOUND_BOX box; + Vect_get_map_box(&In, &box); + + // Create the root octree node + OctreeNode *root = + create_octree_node(box.W, box.E, box.S, box.N, box.B, box.T); + + // Read points and insert them into the octree + struct line_pnts *points = Vect_new_line_struct(); + struct line_cats *cats = Vect_new_cats_struct(); + + int type; +#pragma omp parallel for private(points, cats, type) + for (int line = 1; line <= Vect_get_num_lines(&In); line++) { + type = Vect_read_line(&In, points, cats, line); + if (!(type & GV_POINT)) + continue; + + Point3D p = {points->x[0], points->y[0], points->z[0]}; +#pragma omp critical + insert_point(root, p); + } + + Vect_close(&In); + + // If an output is specified, write some kind of output (e.g., octree + // statistics or a visual representation) + if (output->answer) { + Vect_open_new(&Out, output->answer, 0); + // Here you would add code to write out the octree structure or stats + Vect_build(&Out); + Vect_close(&Out); + } + + // Clean up + free_octree(root); + return EXIT_SUCCESS; +} diff --git a/lib/octree/octree.c b/lib/octree/octree.c new file mode 100644 index 00000000000..543f0cb7dfa --- /dev/null +++ b/lib/octree/octree.c @@ -0,0 +1,100 @@ +#include "octree.h" +#include + +// Create a new octree node +OctreeNode *create_octree_node(double min_x, double max_x, double min_y, + double max_y, double min_z, double max_z) +{ + OctreeNode *node = (OctreeNode *)malloc(sizeof(OctreeNode)); + for (int i = 0; i < 8; i++) { + node->children[i] = NULL; + } + node->points = (Point3D *)malloc(sizeof(Point3D) * MAX_POINTS_PER_NODE); + node->point_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; + return node; +} + +// Determine the child index for a given point +static int get_child_index(OctreeNode *node, Point3D 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; +} + +// Subdivide a node into 8 children +static void subdivide_node(OctreeNode *node) +{ + double mid_x = (node->min_x + node->max_x) / 2; + double mid_y = (node->min_y + node->max_y) / 2; + double mid_z = (node->min_z + node->max_z) / 2; + + for (int i = 0; i < 8; i++) { + double new_min_x = (i & 1) ? mid_x : node->min_x; + double new_max_x = (i & 1) ? node->max_x : mid_x; + double new_min_y = (i & 2) ? mid_y : node->min_y; + double new_max_y = (i & 2) ? node->max_y : mid_y; + double new_min_z = (i & 4) ? mid_z : node->min_z; + double new_max_z = (i & 4) ? node->max_z : mid_z; + + node->children[i] = create_octree_node(new_min_x, new_max_x, new_min_y, + new_max_y, new_min_z, new_max_z); + } +} + +// Insert a point into the octree +void insert_point(OctreeNode *node, Point3D point) +{ + if (node->point_count < MAX_POINTS_PER_NODE) { + node->points[node->point_count++] = point; + } + else { + // If we exceed the point capacity, we need to subdivide and + // redistribute points + if (node->children[0] == NULL) { + subdivide_node(node); + } + + // Re-insert existing points into the appropriate children + for (size_t i = 0; i < node->point_count; i++) { + int child_index = get_child_index(node, node->points[i]); + insert_point(node->children[child_index], node->points[i]); + } + + // Now insert the new point + int new_child_index = get_child_index(node, point); + insert_point(node->children[new_child_index], point); + + // Clear the current node's points (they've moved to children) + free(node->points); + node->points = NULL; + node->point_count = 0; + } +} + +// Recursively free the octree +void free_octree(OctreeNode *node) +{ + if (node == NULL) + return; + + for (int i = 0; i < 8; i++) { + if (node->children[i] != NULL) { + free_octree(node->children[i]); + } + } + + free(node->points); + free(node); +} diff --git a/lib/octree/octree.h b/lib/octree/octree.h new file mode 100644 index 00000000000..96cc58d26e3 --- /dev/null +++ b/lib/octree/octree.h @@ -0,0 +1,28 @@ +#ifndef OCTREE_H +#define OCTREE_H + +#include + +// Define the maximum number of points per octree node before splitting +#define MAX_POINTS_PER_NODE 10 + +// Point structure +typedef struct { + double x, y, z; +} Point3D; + +// Octree node structure +typedef struct OctreeNode { + struct OctreeNode *children[8]; // 8 children for octants + Point3D *points; // Array of points in this node + size_t point_count; // Number of points in this node + double min_x, max_x, min_y, max_y, min_z, max_z; // Bounding box +} OctreeNode; + +// Function prototypes +OctreeNode *create_octree_node(double min_x, double max_x, double min_y, + double max_y, double min_z, double max_z); +void insert_point(OctreeNode *node, Point3D point); +void free_octree(OctreeNode *node); + +#endif // OCTREE_H diff --git a/lib/octree/octreelib.dox b/lib/octree/octreelib.dox new file mode 100644 index 00000000000..0124013b509 --- /dev/null +++ b/lib/octree/octreelib.dox @@ -0,0 +1,115 @@ +/** + * \file octree.c + * \brief Octree spatial indexing library + * + * \author GRASS Development Team + * + * \par Overview + * This library provides an octree spatial data structure for efficient 3D point + * indexing and spatial queries. An octree recursively subdivides 3D space into + * eight octants, enabling fast nearest neighbor searches and range queries. + * + * \par Purpose + * The octree library is designed to: + * - Efficiently store and index 3D point data + * - Perform fast spatial queries (nearest neighbors, radius searches) + * - Support dynamic insertion of points + * - Optimize memory usage through adaptive subdivision + * + * \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} + * struct octree *tree; + * double min[3] = {0.0, 0.0, 0.0}; + * double max[3] = {100.0, 100.0, 100.0}; + * int max_points_per_node = 10; + * + * tree = octree_new(min, max, max_points_per_node); + * \endcode + * + * \section octree_insert Inserting Points + * + * Add 3D points to the octree: + * \code{.c} + * double point[3] = {25.5, 30.2, 15.8}; + * int point_id = 42; + * + * octree_insert(tree, point, point_id); + * + * // Insert multiple points + * for (int i = 0; i < num_points; i++) { + * double coords[3] = {x[i], y[i], z[i]}; + * octree_insert(tree, coords, i); + * } + * \endcode + * + * \section octree_query Querying Points Within Radius + * + * Find all points within a specified radius from a query point: + * \code{.c} + * double query_point[3] = {50.0, 50.0, 50.0}; + * double search_radius = 10.0; + * struct octree_result *results; + * int num_results; + * + * // Perform radius search + * num_results = octree_search_radius(tree, query_point, search_radius, &results); + * + * // Process results + * for (int i = 0; i < num_results; i++) { + * int found_id = results[i].point_id; + * double distance = results[i].distance; + * printf("Point ID: %d, Distance: %f\n", found_id, distance); + * } + * + * // Free results + * octree_free_results(results); + * \endcode + * + * \section octree_nearest Nearest Neighbor Search + * + * Find the k nearest neighbors: + * \code{.c} + * double query_point[3] = {75.0, 25.0, 50.0}; + * int k = 5; + * struct octree_result *neighbors; + * int num_found; + * + * num_found = octree_knn(tree, query_point, k, &neighbors); + * + * for (int i = 0; i < num_found; i++) { + * printf("Neighbor %d: ID=%d, Distance=%f\n", + * i, neighbors[i].point_id, neighbors[i].distance); + * } + * + * octree_free_results(neighbors); + * \endcode + * + * \section octree_cleanup Cleanup + * + * Free octree memory when done: + * \code{.c} + * octree_free(tree); + * \endcode + * + * \par Performance Considerations + * - Choose 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 + * + * \par Thread Safety + * The octree structure is not thread-safe by default. For concurrent access, + * use external synchronization (e.g., mutexes) or consider using thread-local + * copies of the octree. The library does not provide built-in locking mechanisms. + */ diff --git a/lib/octree/test/Makefile b/lib/octree/test/Makefile new file mode 100644 index 00000000000..85877274770 --- /dev/null +++ b/lib/octree/test/Makefile @@ -0,0 +1,12 @@ +MODULE_TOPDIR = ../../.. + +PGM = test.octree.lib + +LIBES = $(GISLIB) +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..d6634d0735b --- /dev/null +++ b/lib/octree/test/test_create_node.c @@ -0,0 +1,209 @@ +/**************************************************************************** + * + * 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); + +/* ************************************************************************* */ +/* Perform the create_octree_node unit tests ****************************** */ +/* ************************************************************************* */ +int unit_test_create_node(void) +{ + int sum = 0; + + G_message(_("\n++ Running create_octree_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(); + + if (sum > 0) + G_warning(_("\n-- create_octree_node unit tests failure --")); + else + G_message(_("\n-- create_octree_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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + if (node == NULL) { + G_warning("create_octree_node returned NULL"); + return 1; + } + + if (node->point_count != 0) { + G_warning("Expected point_count 0, got %zu", node->point_count); + sum++; + } + + if (node->points == NULL) { + G_warning("Expected non-NULL points array"); + sum++; + } + + free_octree(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 = + create_octree_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++; + } + + free_octree(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 = create_octree_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++; + } + } + + free_octree(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 = create_octree_node(5.0, 5.0, 5.0, 5.0, 5.0, 5.0); + + if (node == NULL) { + G_warning("create_octree_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++; + } + + free_octree(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 = + create_octree_node(-100.0, -50.0, -200.0, -100.0, -50.0, -10.0); + + if (node == NULL) { + G_warning("create_octree_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++; + } + + free_octree(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..702cba1d2b4 --- /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 free_octree unit tests ************************************* */ +/* ************************************************************************* */ +int unit_test_free_octree(void) +{ + int sum = 0; + + G_message(_("\n++ Running free_octree 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-- free_octree unit tests failure --")); + else + G_message(_("\n-- free_octree unit tests finished successfully --")); + + return sum; +} + +/* ************************************************************************* */ +/* Test that free_octree handles NULL gracefully ************************** */ +/* ************************************************************************* */ +static int test_free_null_node(void) +{ + G_message("\t * testing free_octree with NULL\n"); + + /* Should not crash */ + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Should not crash; just exercises the deallocation path */ + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + for (int i = 0; i < 5; i++) { + Point3D p = {(double)i, (double)i, (double)i}; + insert_point(root, p); + } + + /* Should not crash; frees the points array and the node */ + free_octree(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 = create_octree_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 <= MAX_POINTS_PER_NODE * 3; i++) { + Point3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + insert_point(root, p); + } + + /* Should not crash; recursively frees all children and their points */ + free_octree(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..acfb19051b5 --- /dev/null +++ b/lib/octree/test/test_insert_point.c @@ -0,0 +1,302 @@ +/**************************************************************************** + * + * 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 "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); + +/* ************************************************************************* */ +/* Perform the insert_point unit tests ************************************ */ +/* ************************************************************************* */ +int unit_test_insert_point(void) +{ + int sum = 0; + + G_message(_("\n++ Running 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(); + + if (sum > 0) + G_warning(_("\n-- insert_point unit tests failure --")); + else + G_message(_("\n-- 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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + Point3D p = {5.0, 5.0, 5.0}; + + insert_point(root, p); + + 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; + } + } + + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert exactly MAX_POINTS_PER_NODE points */ + for (int i = 0; i < MAX_POINTS_PER_NODE; i++) { + Point3D p = {(double)i, (double)i, (double)i}; + insert_point(root, p); + } + + if (root->point_count != MAX_POINTS_PER_NODE) { + G_warning("Expected point_count %d, got %zu", 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; + } + } + + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert MAX_POINTS_PER_NODE + 1 points to trigger subdivision */ + for (int i = 0; i <= MAX_POINTS_PER_NODE; i++) { + Point3D p = {(double)i * 0.5, (double)i * 0.5, (double)i * 0.5}; + insert_point(root, p); + } + + /* After subdivision, root should have children */ + 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++; + } + + free_octree(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 = create_octree_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 < MAX_POINTS_PER_NODE; i++) { + Point3D p = {5.0, 5.0, 5.0}; + insert_point(root, p); + } + + /* These 8 points should land in each of the 8 octants after subdivision */ + Point3D 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++) { + insert_point(root, octant_points[i]); + } + + /* Verify all 8 children exist */ + for (int i = 0; i < 8; i++) { + if (root->children[i] == NULL) { + G_warning("Expected child[%d] to exist after octant insertion", i); + sum++; + } + } + + free_octree(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 = create_octree_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++) { + Point3D p = {3.0, 3.0, 3.0}; + 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++; + } + } + + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert point at the exact midpoint (boundary between octants) */ + Point3D mid = {5.0, 5.0, 5.0}; + 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 */ + Point3D corner = {0.0, 0.0, 0.0}; + 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 */ + Point3D max_corner = {10.0, 10.0, 10.0}; + 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++; + } + + free_octree(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..a736b6aa1af --- /dev/null +++ b/lib/octree/test/test_main.c @@ -0,0 +1,80 @@ +/**************************************************************************** + * + * 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" + +/*- Parameters and global variables -----------------------------------------*/ +typedef struct { + struct Flag *testunit, *full; +} paramType; + +paramType param; + +/*- prototypes --------------------------------------------------------------*/ +static void set_params(void); + +/* ************************************************************************* */ +/* Set up the arguments we are expecting ********************************** */ +/* ************************************************************************* */ +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(); + } + + 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..4eaad501c2b --- /dev/null +++ b/lib/octree/test/test_octree.h @@ -0,0 +1,25 @@ +/**************************************************************************** + * + * 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); + +#endif /* TEST_OCTREE_H */ From 9e8ca28ba217ff73c6ef4571376ffb85aa18a7c7 Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Wed, 8 Apr 2026 15:28:20 -0400 Subject: [PATCH 2/6] Added tests and fixed memory leak --- lib/octree/Makefile | 2 +- lib/octree/main.c | 47 ++++---- lib/octree/octree.c | 174 +++++++++++++++++++--------- lib/octree/octree.h | 27 +++-- lib/octree/octreelib.dox | 98 ++++++---------- lib/octree/test/test_create_node.c | 46 +++++++- lib/octree/test/test_insert_point.c | 131 ++++++++++++++++++++- 7 files changed, 369 insertions(+), 156 deletions(-) diff --git a/lib/octree/Makefile b/lib/octree/Makefile index 130b5f8deec..04328fcfe0e 100644 --- a/lib/octree/Makefile +++ b/lib/octree/Makefile @@ -4,7 +4,7 @@ include $(MODULE_TOPDIR)/include/Make/Vars.make LIB = OCTREE -MOD_OBJS := $(filter-out try.o,$(AUTO_OBJS) +MOD_OBJS := $(filter-out main.o,$(AUTO_OBJS)) include $(MODULE_TOPDIR)/include/Make/Lib.make diff --git a/lib/octree/main.c b/lib/octree/main.c index 3ee5cf73af2..f2351842d42 100644 --- a/lib/octree/main.c +++ b/lib/octree/main.c @@ -1,66 +1,63 @@ -#include "octree.h" +#include + #include +#include #include -#include -#include -// Main function for the GRASS module +#include "octree.h" + int main(int argc, char *argv[]) { struct GModule *module; - struct Option *input, *output; - struct Map_info In, Out; + struct Option *input; + struct Map_info In; G_gisinit(argv[0]); module = G_define_module(); - module->description = "Builds an octree from a vector point map."; + module->description = _("Builds an octree from a vector point map."); input = G_define_standard_option(G_OPT_V_INPUT); - output = G_define_standard_option(G_OPT_V_OUTPUT); if (G_parser(argc, argv)) exit(EXIT_FAILURE); - // Open the input vector map if (Vect_open_old(&In, input->answer, "") < 0) - G_fatal_error("Unable to open vector map <%s>", input->answer); + G_fatal_error(_("Unable to open vector map <%s>"), input->answer); - // Determine the bounding box of the input points BOUND_BOX box; Vect_get_map_box(&In, &box); - // Create the root octree node OctreeNode *root = create_octree_node(box.W, box.E, box.S, box.N, box.B, box.T); - // Read points and insert them into the octree struct line_pnts *points = Vect_new_line_struct(); struct line_cats *cats = Vect_new_cats_struct(); int type; -#pragma omp parallel for private(points, cats, type) + int inserted = 0; + int skipped = 0; + for (int line = 1; line <= Vect_get_num_lines(&In); line++) { type = Vect_read_line(&In, points, cats, line); if (!(type & GV_POINT)) continue; Point3D p = {points->x[0], points->y[0], points->z[0]}; -#pragma omp critical - insert_point(root, p); + + if (insert_point(root, p) == 0) + inserted++; + else + skipped++; } + Vect_destroy_line_struct(points); + Vect_destroy_cats_struct(cats); Vect_close(&In); - // If an output is specified, write some kind of output (e.g., octree - // statistics or a visual representation) - if (output->answer) { - Vect_open_new(&Out, output->answer, 0); - // Here you would add code to write out the octree structure or stats - Vect_build(&Out); - Vect_close(&Out); - } + G_message(_("Inserted %d points into octree (%d skipped)"), inserted, + skipped); - // Clean up free_octree(root); + return EXIT_SUCCESS; } diff --git a/lib/octree/octree.c b/lib/octree/octree.c index 543f0cb7dfa..a232be6d9ef 100644 --- a/lib/octree/octree.c +++ b/lib/octree/octree.c @@ -1,100 +1,170 @@ +#include + #include "octree.h" -#include -// Create a new octree node +/* 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 *create_octree_node(double min_x, double max_x, double min_y, double max_y, double min_z, double max_z) { - OctreeNode *node = (OctreeNode *)malloc(sizeof(OctreeNode)); - for (int i = 0; i < 8; i++) { + if (min_x > max_x || min_y > max_y || min_z > max_z) + G_fatal_error("create_octree_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 = (Point3D *)malloc(sizeof(Point3D) * MAX_POINTS_PER_NODE); + + node->points = NULL; node->point_count = 0; + node->point_capacity = 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 index for a given point -static int get_child_index(OctreeNode *node, Point3D point) +/* Determine the child octant index for a given point */ +static int get_child_index(const OctreeNode *node, Point3D 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; } -// Subdivide a node into 8 children -static void subdivide_node(OctreeNode *node) +/* Create a child node for the given octant index */ +static OctreeNode *create_child_node(OctreeNode *parent, int index) { - double mid_x = (node->min_x + node->max_x) / 2; - double mid_y = (node->min_y + node->max_y) / 2; - double mid_z = (node->min_z + node->max_z) / 2; + 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; - for (int i = 0; i < 8; i++) { - double new_min_x = (i & 1) ? mid_x : node->min_x; - double new_max_x = (i & 1) ? node->max_x : mid_x; - double new_min_y = (i & 2) ? mid_y : node->min_y; - double new_max_y = (i & 2) ? node->max_y : mid_y; - double new_min_z = (i & 4) ? mid_z : node->min_z; - double new_max_z = (i & 4) ? node->max_z : mid_z; - - node->children[i] = create_octree_node(new_min_x, new_max_x, new_min_y, - new_max_y, new_min_z, new_max_z); - } + 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 = create_octree_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; } -// Insert a point into the octree -void insert_point(OctreeNode *node, Point3D point) +/* Ensure the points array has room for at least the requested capacity */ +static void ensure_points_capacity(OctreeNode *node, size_t needed) { + if (node->point_capacity >= needed) + return; + + size_t new_cap = + needed > MAX_POINTS_PER_NODE ? needed : MAX_POINTS_PER_NODE; + node->points = + (Point3D *)G_realloc(node->points, sizeof(Point3D) * 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. */ +int insert_point(OctreeNode *node, Point3D point) +{ + /* 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); + + return insert_point(node->children[child_index], point); + } + + /* 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; + return 0; + } + + /* Leaf with room: store the point */ if (node->point_count < MAX_POINTS_PER_NODE) { + ensure_points_capacity(node, node->point_count + 1); node->points[node->point_count++] = point; + return 0; } - else { - // If we exceed the point capacity, we need to subdivide and - // redistribute points - if (node->children[0] == NULL) { - subdivide_node(node); - } - - // Re-insert existing points into the appropriate children - for (size_t i = 0; i < node->point_count; i++) { - int child_index = get_child_index(node, node->points[i]); - insert_point(node->children[child_index], node->points[i]); - } - - // Now insert the new point - int new_child_index = get_child_index(node, point); - insert_point(node->children[new_child_index], point); - - // Clear the current node's points (they've moved to children) - free(node->points); - node->points = NULL; - node->point_count = 0; + + /* Leaf at capacity: subdivide and redistribute existing points */ + Point3D *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); + + 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); + + return insert_point(node->children[child_index], point); } -// Recursively free the octree +/* Recursively free the octree */ void free_octree(OctreeNode *node) { if (node == NULL) return; for (int i = 0; i < 8; i++) { - if (node->children[i] != NULL) { + if (node->children[i] != NULL) free_octree(node->children[i]); - } } - free(node->points); - free(node); + if (node->points != NULL) + G_free(node->points); + G_free(node); } diff --git a/lib/octree/octree.h b/lib/octree/octree.h index 96cc58d26e3..fad4471664a 100644 --- a/lib/octree/octree.h +++ b/lib/octree/octree.h @@ -3,26 +3,33 @@ #include -// Define the maximum number of points per octree node before splitting +#include + +/* Maximum number of points per octree node before splitting */ #define MAX_POINTS_PER_NODE 10 -// Point structure +/* Maximum tree depth to prevent infinite recursion with coincident points */ +#define OCTREE_MAX_DEPTH 21 + +/* Point structure */ typedef struct { double x, y, z; } Point3D; -// Octree node structure +/* Octree node structure */ typedef struct OctreeNode { - struct OctreeNode *children[8]; // 8 children for octants - Point3D *points; // Array of points in this node - size_t point_count; // Number of points in this node - double min_x, max_x, min_y, max_y, min_z, max_z; // Bounding box + struct OctreeNode *children[8]; /* 8 children for octants */ + Point3D *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 */ + 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; -// Function prototypes +/* Function prototypes */ OctreeNode *create_octree_node(double min_x, double max_x, double min_y, double max_y, double min_z, double max_z); -void insert_point(OctreeNode *node, Point3D point); +int insert_point(OctreeNode *node, Point3D point); void free_octree(OctreeNode *node); -#endif // OCTREE_H +#endif /* OCTREE_H */ diff --git a/lib/octree/octreelib.dox b/lib/octree/octreelib.dox index 0124013b509..fd8a9c6c8a2 100644 --- a/lib/octree/octreelib.dox +++ b/lib/octree/octreelib.dox @@ -6,22 +6,26 @@ * * \par Overview * This library provides an octree spatial data structure for efficient 3D point - * indexing and spatial queries. An octree recursively subdivides 3D space into - * eight octants, enabling fast nearest neighbor searches and range queries. + * 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 - * - Perform fast spatial queries (nearest neighbors, radius searches) * - Support dynamic insertion of points - * - Optimize memory usage through adaptive subdivision + * - 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 + * - 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 + * - 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 * @@ -29,87 +33,55 @@ * * Initialize an octree with spatial bounds: * \code{.c} - * struct octree *tree; - * double min[3] = {0.0, 0.0, 0.0}; - * double max[3] = {100.0, 100.0, 100.0}; - * int max_points_per_node = 10; + * OctreeNode *tree; * - * tree = octree_new(min, max, max_points_per_node); + * tree = create_octree_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} - * double point[3] = {25.5, 30.2, 15.8}; - * int point_id = 42; + * Point3D p = {25.5, 30.2, 15.8}; * - * octree_insert(tree, point, point_id); + * if (insert_point(tree, p) != 0) + * G_warning("Point is outside octree bounds"); * * // Insert multiple points * for (int i = 0; i < num_points; i++) { - * double coords[3] = {x[i], y[i], z[i]}; - * octree_insert(tree, coords, i); + * Point3D pt = {x[i], y[i], z[i]}; + * insert_point(tree, pt); * } * \endcode * - * \section octree_query Querying Points Within Radius + * \section octree_cleanup Cleanup * - * Find all points within a specified radius from a query point: + * Free octree memory when done: * \code{.c} - * double query_point[3] = {50.0, 50.0, 50.0}; - * double search_radius = 10.0; - * struct octree_result *results; - * int num_results; - * - * // Perform radius search - * num_results = octree_search_radius(tree, query_point, search_radius, &results); - * - * // Process results - * for (int i = 0; i < num_results; i++) { - * int found_id = results[i].point_id; - * double distance = results[i].distance; - * printf("Point ID: %d, Distance: %f\n", found_id, distance); - * } - * - * // Free results - * octree_free_results(results); + * free_octree(tree); * \endcode * - * \section octree_nearest Nearest Neighbor Search + * \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 MAX_POINTS_PER_NODE as needed. * - * Find the k nearest neighbors: - * \code{.c} - * double query_point[3] = {75.0, 25.0, 50.0}; - * int k = 5; - * struct octree_result *neighbors; - * int num_found; - * - * num_found = octree_knn(tree, query_point, k, &neighbors); - * - * for (int i = 0; i < num_found; i++) { - * printf("Neighbor %d: ID=%d, Distance=%f\n", - * i, neighbors[i].point_id, neighbors[i].distance); - * } + * \par Bounds Validation + * insert_point() returns -1 if the point is outside the node's bounding box, + * and 0 on success. * - * octree_free_results(neighbors); - * \endcode - * - * \section octree_cleanup Cleanup - * - * Free octree memory when done: - * \code{.c} - * octree_free(tree); - * \endcode + * \par Memory Management + * Points arrays are allocated lazily on first insertion and grow as needed + * using G_realloc(). Child nodes are created on demand (only the octants + * that actually receive points are allocated). * * \par Performance Considerations - * - Choose max_points_per_node based on data density (typical values: 8-32) + * - Choose 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 * * \par Thread Safety - * The octree structure is not thread-safe by default. For concurrent access, - * use external synchronization (e.g., mutexes) or consider using thread-local - * copies of the octree. The library does not provide built-in locking mechanisms. + * The octree structure is not thread-safe. For concurrent access, use + * external synchronization or thread-local copies. */ diff --git a/lib/octree/test/test_create_node.c b/lib/octree/test/test_create_node.c index d6634d0735b..c51e17e367b 100644 --- a/lib/octree/test/test_create_node.c +++ b/lib/octree/test/test_create_node.c @@ -26,6 +26,7 @@ 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 create_octree_node unit tests ****************************** */ @@ -41,6 +42,7 @@ int unit_test_create_node(void) 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-- create_octree_node unit tests failure --")); @@ -72,8 +74,8 @@ static int test_create_node_basic(void) sum++; } - if (node->points == NULL) { - G_warning("Expected non-NULL points array"); + if (node->depth != 0) { + G_warning("Expected depth 0, got %d", node->depth); sum++; } @@ -207,3 +209,43 @@ static int test_create_node_negative_bounds(void) free_octree(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 = create_octree_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 */ + Point3D p = {5.0, 5.0, 5.0}; + 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++; + } + + free_octree(node); + return sum; +} diff --git a/lib/octree/test/test_insert_point.c b/lib/octree/test/test_insert_point.c index acfb19051b5..5176117691f 100644 --- a/lib/octree/test/test_insert_point.c +++ b/lib/octree/test/test_insert_point.c @@ -27,6 +27,9 @@ 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); /* ************************************************************************* */ /* Perform the insert_point unit tests ************************************ */ @@ -43,6 +46,9 @@ int unit_test_insert_point(void) 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(); if (sum > 0) G_warning(_("\n-- insert_point unit tests failure --")); @@ -64,7 +70,12 @@ static int test_insert_single_point(void) OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); Point3D p = {5.0, 5.0, 5.0}; - insert_point(root, p); + int ret = insert_point(root, p); + + if (ret != 0) { + G_warning("insert_point returned %d, expected 0", ret); + sum++; + } if (root->point_count != 1) { G_warning("Expected point_count 1, got %zu", root->point_count); @@ -143,7 +154,7 @@ static int test_insert_triggers_subdivision(void) insert_point(root, p); } - /* After subdivision, root should have children */ + /* After subdivision, root should have at least one child */ int has_children = 0; for (int i = 0; i < 8; i++) { @@ -208,7 +219,7 @@ static int test_insert_points_into_all_octants(void) insert_point(root, octant_points[i]); } - /* Verify all 8 children exist */ + /* 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); @@ -300,3 +311,117 @@ static int test_insert_boundary_point(void) free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Trigger subdivision */ + for (int i = 0; i <= MAX_POINTS_PER_NODE; i++) { + Point3D p = {(double)i * 0.9, (double)i * 0.9, (double)i * 0.9}; + 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++) { + Point3D p = {(double)(i % 10), (double)((i * 3) % 10), + (double)((i * 7) % 10)}; + int ret = insert_point(root, p); + + if (ret != 0) { + G_warning("insert_point returned %d for in-bounds point", ret); + sum++; + } + } + + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + /* Insert many identical points, well past MAX_POINTS_PER_NODE. + Without the depth limit, this would recurse infinitely. */ + int count = MAX_POINTS_PER_NODE * 5; + + for (int i = 0; i < count; i++) { + Point3D p = {3.0, 3.0, 3.0}; + int ret = insert_point(root, p); + + if (ret != 0) { + G_warning("insert_point returned %d for duplicate point %d", ret, + i); + sum++; + } + } + + free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + Point3D outside = {-1.0, 5.0, 5.0}; + + if (insert_point(root, outside) != -1) { + G_warning("Expected -1 for point with x below min_x"); + sum++; + } + + Point3D above = {5.0, 5.0, 15.0}; + + if (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++; + } + + free_octree(root); + return sum; +} From abded48079852acfa89df9b9ef08a38ca64ef04a Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Fri, 17 Apr 2026 10:12:25 -0400 Subject: [PATCH 3/6] Added additional tests, added to CMakeLists, removed unneeded file --- lib/CMakeLists.txt | 4 ++ lib/octree/Makefile | 2 - lib/octree/main.c | 63 ---------------- lib/octree/octree.c | 21 ++++-- lib/octree/octreelib.dox | 2 +- lib/octree/test/Makefile | 2 +- lib/octree/test/test_insert_point.c | 107 ++++++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 71 deletions(-) delete mode 100644 lib/octree/main.c 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/octree/Makefile b/lib/octree/Makefile index 04328fcfe0e..43104e13a49 100644 --- a/lib/octree/Makefile +++ b/lib/octree/Makefile @@ -4,8 +4,6 @@ include $(MODULE_TOPDIR)/include/Make/Vars.make LIB = OCTREE -MOD_OBJS := $(filter-out main.o,$(AUTO_OBJS)) - include $(MODULE_TOPDIR)/include/Make/Lib.make HEADERS := $(ARCH_INCDIR)/octree.h diff --git a/lib/octree/main.c b/lib/octree/main.c deleted file mode 100644 index f2351842d42..00000000000 --- a/lib/octree/main.c +++ /dev/null @@ -1,63 +0,0 @@ -#include - -#include -#include -#include - -#include "octree.h" - -int main(int argc, char *argv[]) -{ - struct GModule *module; - struct Option *input; - struct Map_info In; - - G_gisinit(argv[0]); - module = G_define_module(); - module->description = _("Builds an octree from a vector point map."); - - input = G_define_standard_option(G_OPT_V_INPUT); - - if (G_parser(argc, argv)) - exit(EXIT_FAILURE); - - if (Vect_open_old(&In, input->answer, "") < 0) - G_fatal_error(_("Unable to open vector map <%s>"), input->answer); - - BOUND_BOX box; - Vect_get_map_box(&In, &box); - - OctreeNode *root = - create_octree_node(box.W, box.E, box.S, box.N, box.B, box.T); - - struct line_pnts *points = Vect_new_line_struct(); - struct line_cats *cats = Vect_new_cats_struct(); - - int type; - int inserted = 0; - int skipped = 0; - - for (int line = 1; line <= Vect_get_num_lines(&In); line++) { - type = Vect_read_line(&In, points, cats, line); - if (!(type & GV_POINT)) - continue; - - Point3D p = {points->x[0], points->y[0], points->z[0]}; - - if (insert_point(root, p) == 0) - inserted++; - else - skipped++; - } - - Vect_destroy_line_struct(points); - Vect_destroy_cats_struct(cats); - Vect_close(&In); - - G_message(_("Inserted %d points into octree (%d skipped)"), inserted, - skipped); - - free_octree(root); - - return EXIT_SUCCESS; -} diff --git a/lib/octree/octree.c b/lib/octree/octree.c index a232be6d9ef..6255fbc14d1 100644 --- a/lib/octree/octree.c +++ b/lib/octree/octree.c @@ -1,3 +1,5 @@ +#include + #include #include "octree.h" @@ -78,23 +80,34 @@ static OctreeNode *create_child_node(OctreeNode *parent, int index) return child; } -/* Ensure the points array has room for at least the requested capacity */ +/* 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 = - needed > MAX_POINTS_PER_NODE ? needed : MAX_POINTS_PER_NODE; + size_t new_cap = node->point_capacity == 0 ? (size_t)MAX_POINTS_PER_NODE + : node->point_capacity * 2; + if (new_cap < needed) + new_cap = needed; + node->points = (Point3D *)G_realloc(node->points, sizeof(Point3D) * 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. */ + Returns 0 on success, -1 if the point is outside the node bounds or has + non-finite coordinates. */ int insert_point(OctreeNode *node, Point3D 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 || diff --git a/lib/octree/octreelib.dox b/lib/octree/octreelib.dox index fd8a9c6c8a2..47b8f33a0df 100644 --- a/lib/octree/octreelib.dox +++ b/lib/octree/octreelib.dox @@ -2,7 +2,7 @@ * \file octree.c * \brief Octree spatial indexing library * - * \author GRASS Development Team + * \author Corey White and GRASS Development Team * * \par Overview * This library provides an octree spatial data structure for efficient 3D point diff --git a/lib/octree/test/Makefile b/lib/octree/test/Makefile index 85877274770..1e55033a37e 100644 --- a/lib/octree/test/Makefile +++ b/lib/octree/test/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../../.. PGM = test.octree.lib -LIBES = $(GISLIB) +LIBES = $(GISLIB) $(MATHLIB) DEPENDENCIES = $(GISDEP) EXTRA_INC = -I.. EXTRA_CFLAGS = diff --git a/lib/octree/test/test_insert_point.c b/lib/octree/test/test_insert_point.c index 5176117691f..646f5d66de3 100644 --- a/lib/octree/test/test_insert_point.c +++ b/lib/octree/test/test_insert_point.c @@ -12,6 +12,7 @@ * *****************************************************************************/ +#include #include #include @@ -30,6 +31,8 @@ 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 insert_point unit tests ************************************ */ @@ -49,6 +52,8 @@ int unit_test_insert_point(void) 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-- insert_point unit tests failure --")); @@ -425,3 +430,105 @@ static int test_insert_out_of_bounds(void) free_octree(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 = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + + Point3D nan_point = {NAN, 5.0, 5.0}; + + if (insert_point(root, nan_point) != -1) { + G_warning("Expected -1 for NaN x coordinate"); + sum++; + } + + Point3D inf_point = {5.0, INFINITY, 5.0}; + + if (insert_point(root, inf_point) != -1) { + G_warning("Expected -1 for infinite y coordinate"); + sum++; + } + + Point3D neg_inf = {5.0, 5.0, -INFINITY}; + + if (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++; + } + + free_octree(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 = create_octree_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 + MAX_POINTS_PER_NODE and exercise multiple capacity doublings. */ + int count = MAX_POINTS_PER_NODE * 20; + + for (int i = 0; i < count; i++) { + Point3D p = {7.0, 7.0, 7.0}; + + if (insert_point(root, p) != 0) { + G_warning("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++; + } + + free_octree(root); + return sum; +} From fbbc18004c8521524c1ecf006845188a740e869e Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Fri, 17 Apr 2026 10:47:29 -0400 Subject: [PATCH 4/6] Fixed non ascii character in docs, added subtree_count and cache --- .pre-commit-config.yaml | 2 + lib/octree/octree.c | 191 ++++++++- lib/octree/octree.h | 53 ++- lib/octree/octreelib.dox | 77 +++- lib/octree/test/test_create_node.c | 42 +- lib/octree/test/test_free_octree.c | 38 +- lib/octree/test/test_insert_point.c | 165 +++---- lib/octree/test/test_main.c | 9 +- lib/octree/test/test_octree.h | 1 + lib/octree/test/test_query.c | 644 ++++++++++++++++++++++++++++ 10 files changed, 1052 insertions(+), 170 deletions(-) create mode 100644 lib/octree/test/test_query.c 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/lib/octree/octree.c b/lib/octree/octree.c index 6255fbc14d1..2cc80bab036 100644 --- a/lib/octree/octree.c +++ b/lib/octree/octree.c @@ -16,11 +16,11 @@ static int has_children(const OctreeNode *node) /* Create a new octree node with the given bounding box. Caller must ensure min <= max for each axis. */ -OctreeNode *create_octree_node(double min_x, double max_x, double min_y, +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("create_octree_node: invalid bounds " + 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); @@ -33,6 +33,7 @@ OctreeNode *create_octree_node(double min_x, double max_x, double min_y, 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; @@ -45,7 +46,7 @@ OctreeNode *create_octree_node(double min_x, double max_x, double min_y, } /* Determine the child octant index for a given point */ -static int get_child_index(const OctreeNode *node, Point3D point) +static int get_child_index(const OctreeNode *node, OctreePoint3D point) { int index = 0; @@ -73,7 +74,7 @@ static OctreeNode *create_child_node(OctreeNode *parent, int index) double new_min_z = (index & 4) ? mid_z : parent->min_z; double new_max_z = (index & 4) ? parent->max_z : mid_z; - OctreeNode *child = create_octree_node(new_min_x, new_max_x, new_min_y, + 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; @@ -88,20 +89,21 @@ 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)MAX_POINTS_PER_NODE - : node->point_capacity * 2; + 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 = - (Point3D *)G_realloc(node->points, sizeof(Point3D) * new_cap); + 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 insert_point(OctreeNode *node, Point3D point) +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. */ @@ -122,25 +124,33 @@ int insert_point(OctreeNode *node, Point3D point) if (node->children[child_index] == NULL) node->children[child_index] = create_child_node(node, child_index); - return insert_point(node->children[child_index], point); + 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 < MAX_POINTS_PER_NODE) { + 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 */ - Point3D *old_points = node->points; + /* 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; @@ -153,7 +163,7 @@ int insert_point(OctreeNode *node, Point3D point) if (node->children[child_index] == NULL) node->children[child_index] = create_child_node(node, child_index); - insert_point(node->children[child_index], old_points[i]); + octree_insert_point(node->children[child_index], old_points[i]); } G_free(old_points); @@ -163,21 +173,168 @@ int insert_point(OctreeNode *node, Point3D point) if (node->children[child_index] == NULL) node->children[child_index] = create_child_node(node, child_index); - return insert_point(node->children[child_index], point); + int rc = octree_insert_point(node->children[child_index], point); + + if (rc == 0) + node->subtree_count++; + return rc; } /* Recursively free the octree */ -void free_octree(OctreeNode *node) +void octree_free(OctreeNode *node) { if (node == NULL) return; for (int i = 0; i < 8; i++) { if (node->children[i] != NULL) - free_octree(node->children[i]); + 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 index fad4471664a..424f9c1484e 100644 --- a/lib/octree/octree.h +++ b/lib/octree/octree.h @@ -6,30 +6,69 @@ #include /* Maximum number of points per octree node before splitting */ -#define MAX_POINTS_PER_NODE 10 +#define OCTREE_MAX_POINTS_PER_NODE 10 /* Maximum tree depth to prevent infinite recursion with coincident points */ -#define OCTREE_MAX_DEPTH 21 +#define OCTREE_MAX_DEPTH 21 /* Point structure */ typedef struct { double x, y, z; -} Point3D; +} OctreePoint3D; /* Octree node structure */ typedef struct OctreeNode { struct OctreeNode *children[8]; /* 8 children for octants */ - Point3D *points; /* Array of points in this node */ + 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 *create_octree_node(double min_x, double max_x, double min_y, +OctreeNode *octree_create_node(double min_x, double max_x, double min_y, double max_y, double min_z, double max_z); -int insert_point(OctreeNode *node, Point3D point); -void free_octree(OctreeNode *node); +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 index 47b8f33a0df..6fe24f8cfb9 100644 --- a/lib/octree/octreelib.dox +++ b/lib/octree/octreelib.dox @@ -35,22 +35,22 @@ * \code{.c} * OctreeNode *tree; * - * tree = create_octree_node(0.0, 100.0, 0.0, 100.0, 0.0, 100.0); + * 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} - * Point3D p = {25.5, 30.2, 15.8}; + * OctreePoint3D p = {25.5, 30.2, 15.8}; * - * if (insert_point(tree, p) != 0) - * G_warning("Point is outside octree bounds"); + * 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++) { - * Point3D pt = {x[i], y[i], z[i]}; - * insert_point(tree, pt); + * OctreePoint3D pt = {x[i], y[i], z[i]}; + * octree_insert_point(tree, pt); * } * \endcode * @@ -58,28 +58,71 @@ * * Free octree memory when done: * \code{.c} - * free_octree(tree); + * 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 MAX_POINTS_PER_NODE as needed. + * depth, leaf nodes grow beyond OCTREE_MAX_POINTS_PER_NODE as needed. * * \par Bounds Validation - * insert_point() returns -1 if the point is outside the node's bounding box, - * and 0 on success. + * 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 as needed - * using G_realloc(). Child nodes are created on demand (only the octants - * that actually receive points are allocated). + * 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 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 + * - 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 diff --git a/lib/octree/test/test_create_node.c b/lib/octree/test/test_create_node.c index c51e17e367b..a4efe306f6f 100644 --- a/lib/octree/test/test_create_node.c +++ b/lib/octree/test/test_create_node.c @@ -29,13 +29,13 @@ static int test_create_node_negative_bounds(void); static int test_create_node_lazy_allocation(void); /* ************************************************************************* */ -/* Perform the create_octree_node unit tests ****************************** */ +/* Perform the octree_create_node unit tests ****************************** */ /* ************************************************************************* */ int unit_test_create_node(void) { int sum = 0; - G_message(_("\n++ Running create_octree_node unit tests ++")); + G_message(_("\n++ Running octree_create_node unit tests ++")); sum += test_create_node_basic(); sum += test_create_node_bounds(); @@ -45,9 +45,9 @@ int unit_test_create_node(void) sum += test_create_node_lazy_allocation(); if (sum > 0) - G_warning(_("\n-- create_octree_node unit tests failure --")); + G_warning(_("\n-- octree_create_node unit tests failure --")); else - G_message(_("\n-- create_octree_node unit tests finished " + G_message(_("\n-- octree_create_node unit tests finished " "successfully --")); return sum; @@ -62,10 +62,10 @@ static int test_create_node_basic(void) G_message("\t * testing basic node creation\n"); - OctreeNode *node = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *node = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); if (node == NULL) { - G_warning("create_octree_node returned NULL"); + G_warning("octree_create_node returned NULL"); return 1; } @@ -79,7 +79,7 @@ static int test_create_node_basic(void) sum++; } - free_octree(node); + octree_free(node); return sum; } @@ -97,7 +97,7 @@ static int test_create_node_bounds(void) double min_z = 0.0, max_z = 100.0; OctreeNode *node = - create_octree_node(min_x, max_x, min_y, max_y, min_z, max_z); + 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); @@ -124,7 +124,7 @@ static int test_create_node_bounds(void) sum++; } - free_octree(node); + octree_free(node); return sum; } @@ -137,7 +137,7 @@ static int test_create_node_children_null(void) G_message("\t * testing that children are initialized to NULL\n"); - OctreeNode *node = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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) { @@ -146,7 +146,7 @@ static int test_create_node_children_null(void) } } - free_octree(node); + octree_free(node); return sum; } @@ -159,10 +159,10 @@ static int test_create_node_zero_bounds(void) G_message("\t * testing node creation with zero-sized bounds\n"); - OctreeNode *node = create_octree_node(5.0, 5.0, 5.0, 5.0, 5.0, 5.0); + OctreeNode *node = octree_create_node(5.0, 5.0, 5.0, 5.0, 5.0, 5.0); if (node == NULL) { - G_warning("create_octree_node returned NULL for zero-sized bounds"); + G_warning("octree_create_node returned NULL for zero-sized bounds"); return 1; } @@ -172,7 +172,7 @@ static int test_create_node_zero_bounds(void) sum++; } - free_octree(node); + octree_free(node); return sum; } @@ -186,10 +186,10 @@ static int test_create_node_negative_bounds(void) G_message("\t * testing node creation with negative bounds\n"); OctreeNode *node = - create_octree_node(-100.0, -50.0, -200.0, -100.0, -50.0, -10.0); + octree_create_node(-100.0, -50.0, -200.0, -100.0, -50.0, -10.0); if (node == NULL) { - G_warning("create_octree_node returned NULL for negative bounds"); + G_warning("octree_create_node returned NULL for negative bounds"); return 1; } @@ -206,7 +206,7 @@ static int test_create_node_negative_bounds(void) sum++; } - free_octree(node); + octree_free(node); return sum; } @@ -219,7 +219,7 @@ static int test_create_node_lazy_allocation(void) G_message("\t * testing lazy allocation of points array\n"); - OctreeNode *node = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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"); @@ -232,8 +232,8 @@ static int test_create_node_lazy_allocation(void) } /* After one insert, points should be allocated */ - Point3D p = {5.0, 5.0, 5.0}; - insert_point(node, p); + 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"); @@ -246,6 +246,6 @@ static int test_create_node_lazy_allocation(void) sum++; } - free_octree(node); + octree_free(node); return sum; } diff --git a/lib/octree/test/test_free_octree.c b/lib/octree/test/test_free_octree.c index 702cba1d2b4..b9bf5a993e9 100644 --- a/lib/octree/test/test_free_octree.c +++ b/lib/octree/test/test_free_octree.c @@ -27,13 +27,13 @@ static int test_free_tree_with_points(void); static int test_free_subdivided_tree(void); /* ************************************************************************* */ -/* Perform the free_octree unit tests ************************************* */ +/* Perform the octree_free unit tests ************************************* */ /* ************************************************************************* */ int unit_test_free_octree(void) { int sum = 0; - G_message(_("\n++ Running free_octree unit tests ++")); + G_message(_("\n++ Running octree_free unit tests ++")); sum += test_free_null_node(); sum += test_free_empty_tree(); @@ -41,22 +41,22 @@ int unit_test_free_octree(void) sum += test_free_subdivided_tree(); if (sum > 0) - G_warning(_("\n-- free_octree unit tests failure --")); + G_warning(_("\n-- octree_free unit tests failure --")); else - G_message(_("\n-- free_octree unit tests finished successfully --")); + G_message(_("\n-- octree_free unit tests finished successfully --")); return sum; } /* ************************************************************************* */ -/* Test that free_octree handles NULL gracefully ************************** */ +/* Test that octree_free handles NULL gracefully ************************** */ /* ************************************************************************* */ static int test_free_null_node(void) { - G_message("\t * testing free_octree with NULL\n"); + G_message("\t * testing octree_free with NULL\n"); /* Should not crash */ - free_octree(NULL); + octree_free(NULL); return 0; } @@ -68,10 +68,10 @@ static int test_free_empty_tree(void) { G_message("\t * testing free of empty tree\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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 */ - free_octree(root); + octree_free(root); return 0; } @@ -83,15 +83,15 @@ static int test_free_tree_with_points(void) { G_message("\t * testing free of tree with points\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); for (int i = 0; i < 5; i++) { - Point3D p = {(double)i, (double)i, (double)i}; - insert_point(root, p); + OctreePoint3D p = {(double)i, (double)i, (double)i}; + octree_insert_point(root, p); } /* Should not crash; frees the points array and the node */ - free_octree(root); + octree_free(root); return 0; } @@ -103,17 +103,17 @@ static int test_free_subdivided_tree(void) { G_message("\t * testing free of subdivided tree\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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 <= MAX_POINTS_PER_NODE * 3; i++) { - Point3D p = {(double)(i % 10), (double)((i * 3) % 10), - (double)((i * 7) % 10)}; - insert_point(root, p); + 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 */ - free_octree(root); + octree_free(root); return 0; } diff --git a/lib/octree/test/test_insert_point.c b/lib/octree/test/test_insert_point.c index 646f5d66de3..e9c24da07d3 100644 --- a/lib/octree/test/test_insert_point.c +++ b/lib/octree/test/test_insert_point.c @@ -35,13 +35,14 @@ static int test_insert_non_finite(void); static int test_insert_capacity_growth(void); /* ************************************************************************* */ -/* Perform the insert_point unit tests ************************************ */ +/* Perform the octree_insert_point unit tests + * ************************************ */ /* ************************************************************************* */ int unit_test_insert_point(void) { int sum = 0; - G_message(_("\n++ Running insert_point unit tests ++")); + G_message(_("\n++ Running octree_insert_point unit tests ++")); sum += test_insert_single_point(); sum += test_insert_fill_node(); @@ -56,9 +57,10 @@ int unit_test_insert_point(void) sum += test_insert_capacity_growth(); if (sum > 0) - G_warning(_("\n-- insert_point unit tests failure --")); + G_warning(_("\n-- octree_insert_point unit tests failure --")); else - G_message(_("\n-- insert_point unit tests finished successfully --")); + G_message( + _("\n-- octree_insert_point unit tests finished successfully --")); return sum; } @@ -72,13 +74,13 @@ static int test_insert_single_point(void) G_message("\t * testing single point insertion\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); - Point3D p = {5.0, 5.0, 5.0}; + 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 = insert_point(root, p); + int ret = octree_insert_point(root, p); if (ret != 0) { - G_warning("insert_point returned %d, expected 0", ret); + G_warning("octree_insert_point returned %d, expected 0", ret); sum++; } @@ -102,7 +104,7 @@ static int test_insert_single_point(void) } } - free_octree(root); + octree_free(root); return sum; } @@ -115,17 +117,17 @@ static int test_insert_fill_node(void) G_message("\t * testing filling node to capacity\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); - /* Insert exactly MAX_POINTS_PER_NODE points */ - for (int i = 0; i < MAX_POINTS_PER_NODE; i++) { - Point3D p = {(double)i, (double)i, (double)i}; - insert_point(root, p); + /* 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 != MAX_POINTS_PER_NODE) { - G_warning("Expected point_count %d, got %zu", MAX_POINTS_PER_NODE, - root->point_count); + 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++; } @@ -138,7 +140,7 @@ static int test_insert_fill_node(void) } } - free_octree(root); + octree_free(root); return sum; } @@ -151,12 +153,12 @@ static int test_insert_triggers_subdivision(void) G_message("\t * testing subdivision on capacity overflow\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); - /* Insert MAX_POINTS_PER_NODE + 1 points to trigger subdivision */ - for (int i = 0; i <= MAX_POINTS_PER_NODE; i++) { - Point3D p = {(double)i * 0.5, (double)i * 0.5, (double)i * 0.5}; - insert_point(root, p); + /* 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 */ @@ -186,7 +188,7 @@ static int test_insert_triggers_subdivision(void) sum++; } - free_octree(root); + octree_free(root); return sum; } @@ -199,17 +201,17 @@ static int test_insert_points_into_all_octants(void) G_message("\t * testing insertion into all 8 octants\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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 < MAX_POINTS_PER_NODE; i++) { - Point3D p = {5.0, 5.0, 5.0}; - insert_point(root, p); + 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 */ - Point3D octant_points[8] = { + 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 */ @@ -221,7 +223,7 @@ static int test_insert_points_into_all_octants(void) }; for (int i = 0; i < 8; i++) { - insert_point(root, octant_points[i]); + octree_insert_point(root, octant_points[i]); } /* Verify children exist for all 8 octants */ @@ -232,7 +234,7 @@ static int test_insert_points_into_all_octants(void) } } - free_octree(root); + octree_free(root); return sum; } @@ -245,12 +247,12 @@ static int test_insert_duplicate_points(void) G_message("\t * testing insertion of duplicate points\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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++) { - Point3D p = {3.0, 3.0, 3.0}; - insert_point(root, p); + OctreePoint3D p = {3.0, 3.0, 3.0}; + octree_insert_point(root, p); } if (root->point_count != 5) { @@ -268,7 +270,7 @@ static int test_insert_duplicate_points(void) } } - free_octree(root); + octree_free(root); return sum; } @@ -281,11 +283,11 @@ static int test_insert_boundary_point(void) G_message("\t * testing boundary point insertion\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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) */ - Point3D mid = {5.0, 5.0, 5.0}; - insert_point(root, mid); + 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", @@ -294,8 +296,8 @@ static int test_insert_boundary_point(void) } /* Insert point at exact min corner */ - Point3D corner = {0.0, 0.0, 0.0}; - insert_point(root, 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", @@ -304,8 +306,8 @@ static int test_insert_boundary_point(void) } /* Insert point at exact max corner */ - Point3D max_corner = {10.0, 10.0, 10.0}; - insert_point(root, 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", @@ -313,7 +315,7 @@ static int test_insert_boundary_point(void) sum++; } - free_octree(root); + octree_free(root); return sum; } @@ -326,12 +328,12 @@ static int test_insert_after_subdivision(void) G_message("\t * testing insertion after subdivision\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); /* Trigger subdivision */ - for (int i = 0; i <= MAX_POINTS_PER_NODE; i++) { - Point3D p = {(double)i * 0.9, (double)i * 0.9, (double)i * 0.9}; - insert_point(root, p); + 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 */ @@ -351,17 +353,18 @@ static int test_insert_after_subdivision(void) /* Insert more points after subdivision; this must not crash */ for (int i = 0; i < 20; i++) { - Point3D p = {(double)(i % 10), (double)((i * 3) % 10), - (double)((i * 7) % 10)}; - int ret = insert_point(root, p); + 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("insert_point returned %d for in-bounds point", ret); + G_warning("octree_insert_point returned %d for in-bounds point", + ret); sum++; } } - free_octree(root); + octree_free(root); return sum; } @@ -374,24 +377,24 @@ static int test_insert_many_duplicates(void) G_message("\t * testing many coincident points (depth limit)\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); - /* Insert many identical points, well past MAX_POINTS_PER_NODE. + /* Insert many identical points, well past OCTREE_MAX_POINTS_PER_NODE. Without the depth limit, this would recurse infinitely. */ - int count = MAX_POINTS_PER_NODE * 5; + int count = OCTREE_MAX_POINTS_PER_NODE * 5; for (int i = 0; i < count; i++) { - Point3D p = {3.0, 3.0, 3.0}; - int ret = insert_point(root, p); + OctreePoint3D p = {3.0, 3.0, 3.0}; + int ret = octree_insert_point(root, p); if (ret != 0) { - G_warning("insert_point returned %d for duplicate point %d", ret, - i); + G_warning("octree_insert_point returned %d for duplicate point %d", + ret, i); sum++; } } - free_octree(root); + octree_free(root); return sum; } @@ -404,18 +407,18 @@ static int test_insert_out_of_bounds(void) G_message("\t * testing out-of-bounds point rejection\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); - Point3D outside = {-1.0, 5.0, 5.0}; + OctreePoint3D outside = {-1.0, 5.0, 5.0}; - if (insert_point(root, outside) != -1) { + if (octree_insert_point(root, outside) != -1) { G_warning("Expected -1 for point with x below min_x"); sum++; } - Point3D above = {5.0, 5.0, 15.0}; + OctreePoint3D above = {5.0, 5.0, 15.0}; - if (insert_point(root, above) != -1) { + if (octree_insert_point(root, above) != -1) { G_warning("Expected -1 for point with z above max_z"); sum++; } @@ -427,7 +430,7 @@ static int test_insert_out_of_bounds(void) sum++; } - free_octree(root); + octree_free(root); return sum; } @@ -440,25 +443,25 @@ static int test_insert_non_finite(void) G_message("\t * testing non-finite coordinate rejection\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + OctreeNode *root = octree_create_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); - Point3D nan_point = {NAN, 5.0, 5.0}; + OctreePoint3D nan_point = {NAN, 5.0, 5.0}; - if (insert_point(root, nan_point) != -1) { + if (octree_insert_point(root, nan_point) != -1) { G_warning("Expected -1 for NaN x coordinate"); sum++; } - Point3D inf_point = {5.0, INFINITY, 5.0}; + OctreePoint3D inf_point = {5.0, INFINITY, 5.0}; - if (insert_point(root, inf_point) != -1) { + if (octree_insert_point(root, inf_point) != -1) { G_warning("Expected -1 for infinite y coordinate"); sum++; } - Point3D neg_inf = {5.0, 5.0, -INFINITY}; + OctreePoint3D neg_inf = {5.0, 5.0, -INFINITY}; - if (insert_point(root, neg_inf) != -1) { + if (octree_insert_point(root, neg_inf) != -1) { G_warning("Expected -1 for -infinity z coordinate"); sum++; } @@ -469,7 +472,7 @@ static int test_insert_non_finite(void) sum++; } - free_octree(root); + octree_free(root); return sum; } @@ -482,17 +485,17 @@ static int test_insert_capacity_growth(void) G_message("\t * testing geometric capacity growth with many duplicates\n"); - OctreeNode *root = create_octree_node(0.0, 10.0, 0.0, 10.0, 0.0, 10.0); + 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 - MAX_POINTS_PER_NODE and exercise multiple capacity doublings. */ - int count = MAX_POINTS_PER_NODE * 20; + 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++) { - Point3D p = {7.0, 7.0, 7.0}; + OctreePoint3D p = {7.0, 7.0, 7.0}; - if (insert_point(root, p) != 0) { - G_warning("insert_point failed on duplicate %d", i); + if (octree_insert_point(root, p) != 0) { + G_warning("octree_insert_point failed on duplicate %d", i); sum++; } } @@ -529,6 +532,6 @@ static int test_insert_capacity_growth(void) sum++; } - free_octree(root); + octree_free(root); return sum; } diff --git a/lib/octree/test/test_main.c b/lib/octree/test/test_main.c index a736b6aa1af..b5ea1896872 100644 --- a/lib/octree/test/test_main.c +++ b/lib/octree/test/test_main.c @@ -20,19 +20,14 @@ #include "test_octree.h" -/*- Parameters and global variables -----------------------------------------*/ typedef struct { struct Flag *testunit, *full; } paramType; paramType param; -/*- prototypes --------------------------------------------------------------*/ static void set_params(void); -/* ************************************************************************* */ -/* Set up the arguments we are expecting ********************************** */ -/* ************************************************************************* */ void set_params(void) { param.testunit = G_define_flag(); @@ -44,9 +39,6 @@ void set_params(void) param.full->description = _("Run all unit and integration tests"); } -/* ************************************************************************* */ -/* ************************************************************************* */ -/* ************************************************************************* */ int main(int argc, char *argv[]) { struct GModule *module; @@ -69,6 +61,7 @@ int main(int argc, char *argv[]) returnstat += unit_test_create_node(); returnstat += unit_test_insert_point(); returnstat += unit_test_free_octree(); + returnstat += unit_test_query(); } if (returnstat != 0) diff --git a/lib/octree/test/test_octree.h b/lib/octree/test/test_octree.h index 4eaad501c2b..eafb4689b33 100644 --- a/lib/octree/test/test_octree.h +++ b/lib/octree/test/test_octree.h @@ -21,5 +21,6 @@ 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; +} From 1deaba76979b285af70e6fe3f36677f326ee0d41 Mon Sep 17 00:00:00 2001 From: cwhite911 Date: Fri, 17 Apr 2026 11:08:30 -0400 Subject: [PATCH 5/6] Added missing reference to the lib for full build --- GRASSConfig.cmake.in | 1 + grasslib.dox | 1 + include/Make/Grass.make | 2 ++ lib/Makefile | 1 + lib/octree/octreelib.dox | 3 +-- 5 files changed, 6 insertions(+), 2 deletions(-) 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/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/octreelib.dox b/lib/octree/octreelib.dox index 6fe24f8cfb9..6e2f68738ba 100644 --- a/lib/octree/octreelib.dox +++ b/lib/octree/octreelib.dox @@ -1,5 +1,4 @@ -/** - * \file octree.c +/*! \page octree GRASS Octree library * \brief Octree spatial indexing library * * \author Corey White and GRASS Development Team From a716213acb30cafea20cd0607bfaec643242ec03 Mon Sep 17 00:00:00 2001 From: Arth Parikshit Patel Date: Fri, 29 May 2026 11:29:01 +0530 Subject: [PATCH 6/6] lib/octree: Add user_data tracking pointer to OctreePoint3D struct --- lib/octree/octree.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/octree/octree.h b/lib/octree/octree.h index 424f9c1484e..4100f6ab1ec 100644 --- a/lib/octree/octree.h +++ b/lib/octree/octree.h @@ -14,6 +14,7 @@ /* Point structure */ typedef struct { double x, y, z; + void *user_data; /* Arbitrary user pointer to attach point IDs or metadata */ } OctreePoint3D; /* Octree node structure */