summaryrefslogtreecommitdiff
path: root/src/ix2.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ix2.c')
-rw-r--r--src/ix2.c632
1 files changed, 632 insertions, 0 deletions
diff --git a/src/ix2.c b/src/ix2.c
new file mode 100644
index 0000000..fcdeb79
--- /dev/null
+++ b/src/ix2.c
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2018 Vito Caputo - <vcaputo@pengaru.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* 2d spatial index - implemented as a quadtree */
+
+/* There are various outstanding TODO items littered throughout this code,
+ * it's usable as-is but by no means finished or optimized.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include "ix2.h"
+#include "list.h"
+
+typedef struct v2f_t {
+ float x, y;
+} v2f_t;
+
+typedef struct aabb_t {
+ v2f_t min, max;
+} aabb_t;
+
+typedef struct ix2_node_t ix2_node_t;
+
+typedef struct ix2_object_t {
+ list_head_t references; /* list of all ix2_node references to this object */
+ list_head_t hits; /* node on hit list for non-point searches, to prevent potential double hits like when an object and search both span nodes */
+ /* XXX: note this makes non-point searches non-reentrant! i.e. you can't perform another non-point search on the same ix2
+ * from a search callback operating on the same ix2. TODO investigate efficient solutions. An obvious one is to simply put
+ * multiple of these nodes in every object and support a small number of simultaneous non-point searches, which may suffice.
+ */
+ aabb_t aabb; /* AABB of this object */
+ float aabb_len_sq; /* length(aabb.max - aabb.min)^2 TODO: this isn't utilized yet, optimization potential */
+ void *object; /* user handle linking this object to the index */
+} ix2_object_t;
+
+typedef struct ix2_object_ref_t {
+ list_head_t objects; /* node on ix2_node objects list */
+ list_head_t references; /* node on ix2_object references list */
+ ix2_object_t *object; /* object being referenced */
+ ix2_node_t *node; /* node referencing object */
+} ix2_object_ref_t;
+
+struct ix2_node_t {
+ v2f_t center; /* spatial center point of ix2_node */
+ ix2_node_t *children; /* array of 4 child quadrants when not a leaf, NULL when a leaf */
+ unsigned n_objects; /* number of objects in leaf when !children */
+ list_head_t objects; /* list of all objects in leaf when !children */
+};
+
+typedef struct ix2_t {
+ aabb_t aabb; /* spatial bounds of ix2 root */
+ unsigned max_per_node; /* maximum number of objects to put in a node until max_depth reached */
+ unsigned max_depth; /* maximum depth of ix2 tree where nodes cease being divided further */
+ ix2_node_t root; /* root node of ix2 quadtree */
+} ix2_t;
+
+static ix2_object_t * add_object(ix2_t *ix2, unsigned *depth, ix2_node_t *node, aabb_t *node_aabb, ix2_object_t *object, ix2_object_ref_t **reference_cache);
+
+
+/* Check if point resides within aabb */
+static inline int aabb_contains_point(aabb_t *aabb, v2f_t *point)
+{
+ assert(aabb);
+ assert(point);
+
+ if (point->x < aabb->min.x)
+ return 0;
+
+ if (point->x > aabb->max.x)
+ return 0;
+
+ if (point->y < aabb->min.y)
+ return 0;
+
+ if (point->y > aabb->max.y)
+ return 0;
+
+ return 1;
+}
+
+
+/* check if a and b overlap */
+static inline int aabb_overlap(aabb_t *a, aabb_t *b)
+{
+ assert(a);
+ assert(b);
+
+ return (a->min.x <= b->max.x &&
+ a->max.x >= b->min.x &&
+ a->min.y <= b->max.y &&
+ a->max.y >= b->min.y);
+}
+
+
+/* initialize a node centered in aabb */
+static void init_node(ix2_node_t *node, aabb_t *aabb)
+{
+ assert(node);
+ assert(aabb);
+
+ INIT_LIST_HEAD(&node->objects);
+
+ node->center.x = aabb->min.x;
+ node->center.y = aabb->min.y;
+ node->center.x += .5f * (aabb->max.x - aabb->min.x);
+ node->center.y += .5f * (aabb->max.y - aabb->min.y);
+}
+
+
+/* initialize a quadrants aabb_t array from a parent node aabb and center point */
+static void init_quadrants(aabb_t *quadrants, aabb_t *parent_aabb, v2f_t *quadrants_center)
+{
+ assert(quadrants);
+ assert(parent_aabb);
+ assert(quadrants_center);
+/*
+ +-+-+
+ |0|1|
+ +-+-+
+ |2|3|
+ +-+-+
+*/
+ quadrants[0].min.x = parent_aabb->min.x;
+ quadrants[0].min.y = quadrants_center->y;
+ quadrants[0].max.x = quadrants_center->x;
+ quadrants[0].max.y = parent_aabb->max.y;
+
+ quadrants[1].min.x = quadrants_center->x;
+ quadrants[1].min.y = quadrants_center->y;
+ quadrants[1].max.x = parent_aabb->max.x;
+ quadrants[1].max.y = parent_aabb->max.y;
+
+ quadrants[2].min.x = parent_aabb->min.x;
+ quadrants[2].min.y = parent_aabb->min.y;
+ quadrants[2].max.x = quadrants_center->x;
+ quadrants[2].max.y = quadrants_center->y;
+
+ quadrants[3].min.x = quadrants_center->x;
+ quadrants[3].min.y = parent_aabb->min.y;
+ quadrants[3].max.x = parent_aabb->max.x;
+ quadrants[3].max.y = quadrants_center->y;
+}
+
+
+/* Unlink a single object reference, disconnecting it from the object and node */
+static void unlink_object_ref(ix2_object_ref_t *ref)
+{
+ assert(ref);
+
+ /* TODO: is there an actual need to maintain and detect the unlinked state w/NULL ptrs? */
+ if (!ref->node)
+ return;
+
+ list_del(&ref->objects);
+ list_del(&ref->references);
+ ref->node->n_objects--;
+ ref->object = NULL;
+ ref->node = NULL;
+}
+
+
+/* Unlink an object from the index, unlinking all node references.
+ * If references_cache is NULL, all unlinked references are freed.
+ * Otherwise, the unlinked references are added to the references_cache list.
+ */
+static void unlink_object(ix2_object_t *object, list_head_t *references_cache)
+{
+ ix2_object_ref_t *ref, *_ref;
+
+ assert(object);
+
+ list_for_each_entry_safe(ref, _ref, &object->references, references) {
+ unlink_object_ref(ref);
+
+ if (references_cache) {
+ list_add_tail(&ref->references, references_cache);
+ continue;
+ }
+
+ free(ref);
+ }
+}
+
+
+/* Link an object into a node.
+ * If ref is not NULL, it's an existing reference to move to a new node rather than allocating a new one.
+ * Returns the reference on success, NULL on failure
+ * On falure the supplied object is left intact.
+ */
+static ix2_object_ref_t * link_object(ix2_object_t *object, ix2_node_t *node, ix2_object_ref_t **cached_ref)
+{
+ ix2_object_ref_t *ref = NULL;
+
+ assert(object);
+ assert(node);
+
+ if (cached_ref && *cached_ref) {
+ ref = *cached_ref;
+ *cached_ref = NULL;
+ unlink_object_ref(ref);
+ }
+
+ if (!ref)
+ ref = calloc(1, sizeof(ix2_object_ref_t));
+
+ if (!ref)
+ return NULL;
+
+ list_add_tail(&ref->references, &object->references);
+ list_add_tail(&ref->objects, &node->objects);
+ ref->object = object;
+ ref->node = node;
+ node->n_objects++;
+
+ return ref;
+}
+
+
+/* Compute the diagonal length of the aabb, leaving it squared */
+static inline float aabb_len_sq(aabb_t *aabb)
+{
+ float len_sq;
+
+ assert(aabb);
+
+ /* TODO: import the full v2f.h and do this with v2f_sub() && v2f_dot()? */
+ len_sq = (aabb->max.x - aabb->min.x) * (aabb->max.x - aabb->min.x);
+ len_sq += (aabb->max.y - aabb->min.y) * (aabb->max.y - aabb->min.y);
+
+ return len_sq;
+}
+
+
+/* remove an object */
+static void remove_object(ix2_object_t *object)
+{
+ assert(object);
+
+ unlink_object(object, NULL);
+ free(object);
+}
+
+
+/* Split node and distribute objects referenced from node->objects into a
+ * newly created node->children.
+ */
+ix2_node_t * split_node(ix2_t *ix2, unsigned *depth, ix2_node_t *node, aabb_t *node_aabb)
+{
+ aabb_t quadrants[4];
+ ix2_object_ref_t *r, *_r;
+ int i;
+
+ assert(ix2);
+ assert(depth);
+ assert(node);
+ assert(!node->children);
+ assert(node_aabb);
+
+ /* allocate and setup the children nodes */
+ node->children = calloc(4, sizeof(ix2_node_t));
+ if (!node->children)
+ goto _fail;
+
+ init_quadrants(quadrants, node_aabb, &node->center);
+ for (i = 0; i < 4; i++)
+ init_node(&node->children[i], &quadrants[i]);
+
+ /* distribute objects across node->children, removing node->objects
+ * references along the way. The references are supplied
+ */
+ list_for_each_entry_safe(r, _r, &node->objects, objects) {
+ ix2_object_t *o = r->object;
+
+ /* XXX: this can probably be done more efficiently */
+ for (i = 0; i < 4; i++) {
+ if (!aabb_overlap(&quadrants[i], &o->aabb))
+ continue;
+
+ if (!add_object(ix2, depth, &node->children[i], &quadrants[i], o, &r))
+ goto _fail;
+ }
+ }
+
+ return node->children;
+
+_fail:
+ /* TODO: some kind of cleanup? */
+ return NULL;
+}
+
+
+/* Add a member object to the space described by node && node_aabb
+ *
+ * This function may need to divide nodes and/or recur.
+ *
+ * Since it may need to allocate memory for object references via
+ * link_object(), and may need to allocate space for children
+ * nodes, it may fail.
+ *
+ * As an optimization, a list of object refs may be supplied in object_refs for
+ * use when linking the object.
+ *
+ * On failure NULL is returned and object is freed (including all its references).
+ * On success the supplied object is simply returned, with potentialy new references added.
+ */
+static ix2_object_t * add_object(ix2_t *ix2, unsigned *depth, ix2_node_t *node, aabb_t *node_aabb, ix2_object_t *object, ix2_object_ref_t **reference_cache)
+{
+ assert(ix2);
+ assert(depth);
+ assert(node);
+ assert(node_aabb);
+ assert(object);
+
+ /* If not a leaf, simply recur on overlapping children */
+ if (node->children) {
+ aabb_t quadrants[4];
+ int i;
+
+ *depth++;
+ init_quadrants(quadrants, node_aabb, &node->center);
+ for (i = 0; i < 4; i++) {
+ if (!aabb_overlap(&quadrants[i], &object->aabb))
+ continue;
+
+ if (!add_object(ix2, depth, &node->children[i], &quadrants[i], object, reference_cache))
+ goto _fail;
+ }
+ *depth--;
+
+ return object;
+ }
+
+
+ /* Node is a leaf, optimistically link the object to the node */
+ if (!link_object(object, node, reference_cache))
+ goto _fail;
+
+
+ /* If the node is overflowing, split it */
+ if (node->n_objects > ix2->max_per_node && *depth < ix2->max_depth) {
+ *depth++;
+ if (!split_node(ix2, depth, node, node_aabb))
+ goto _fail;
+ *depth--;
+ }
+
+ return object;
+
+_fail:
+ remove_object(object);
+
+ return NULL;
+}
+
+
+/* Create a new ix2 index with bounds aabb, splitting nodes > max_per_node until max_depth */
+ix2_t * ix2_new(aabb_t *aabb, unsigned max_per_node, unsigned max_depth)
+{
+ ix2_t *ix2;
+
+ assert(aabb);
+
+ ix2 = calloc(1, sizeof(ix2_t));
+ if (!ix2)
+ return NULL;
+
+ init_node(&ix2->root, aabb);
+
+ ix2->aabb = *aabb;
+ ix2->max_per_node = max_per_node;
+ ix2->max_depth = max_depth;
+
+ return ix2;
+}
+
+
+/* Free the index ix2 and everything associated with it */
+/* Note the external objects which have been indexed are not freed */
+void ix2_free(ix2_t *ix2)
+{
+ /* TODO: free everything */
+ free(ix2);
+}
+
+
+/* Insert object spatially described by object_aabb into the index ix2 */
+ix2_object_t * ix2_insert_object(ix2_t *ix2, aabb_t *object_aabb, void *object)
+{
+ unsigned depth = 0;
+ ix2_object_t *o;
+
+ assert(ix2);
+ assert(object_aabb);
+ assert(object_aabb->min.x <= object_aabb->max.x);
+ assert(object_aabb->min.y <= object_aabb->max.y);
+
+ o = calloc(1, sizeof(ix2_object_t));
+ if (!o)
+ return NULL;
+
+ INIT_LIST_HEAD(&o->references);
+ INIT_LIST_HEAD(&o->hits);
+
+ o->aabb = *object_aabb;
+ o->object = object;
+ o->aabb_len_sq = aabb_len_sq(object_aabb);
+
+ if (!aabb_overlap(&ix2->aabb, object_aabb))
+ return object;
+
+ return add_object(ix2, &depth, &ix2->root, &ix2->aabb, o, NULL);
+}
+
+
+/* Remove object from the index ix2 and free it. */
+/* This may be either the object handle returned by ix2_node_insert(), or
+ * the handle supplied to ix2_search_cb().
+ */
+void ix2_remove_object(ix2_t *ix2, ix2_object_t *object)
+{
+ assert(ix2);
+ assert(object);
+
+ remove_object(object);
+}
+
+
+/* Move already inserted object to new aabb. */
+/* Returns object on success, NULL on failure. */
+/* On failure object is removed and freed. XXX: change to leave where it was? */
+ix2_object_t * ix2_move_object(ix2_t *ix2, ix2_object_t *object, aabb_t *object_aabb)
+{
+ unsigned depth = 0;
+
+ assert(ix2);
+ assert(object);
+ assert(object_aabb);
+ assert(object_aabb->min.x <= object_aabb->max.x);
+ assert(object_aabb->min.y <= object_aabb->max.y);
+
+ /* TODO FIXME: cache and reuse the references, requires changing
+ * add_object to receive a list of cached references rather than the
+ * singleton it currently expects.
+ * unlink_object() already knows how to leave those references on a
+ * supplied list head which is currently NULL.
+ */
+ unlink_object(object, NULL);
+
+ object->aabb = *object_aabb;
+ object->aabb_len_sq = aabb_len_sq(object_aabb);
+
+ if (!aabb_overlap(&ix2->aabb, object_aabb))
+ return object;
+
+ return add_object(ix2, &depth, &ix2->root, &ix2->aabb, object, NULL /* TODO */);
+}
+
+
+/* Search for objects overlapping a point in index ix2. */
+/* Returns number of cb calls performed on hits, if cb is NULL calls will be skipped but hits still counted for return. */
+unsigned ix2_search_by_point(ix2_t *ix2, v2f_t *point, ix2_search_cb cb, void *cb_context)
+{
+ unsigned n_hits = 0;
+ ix2_object_ref_t *ref, *_ref;
+ ix2_node_t *node;
+ aabb_t node_aabb;
+
+ assert(ix2);
+ assert(point);
+
+ if (!aabb_contains_point(&ix2->aabb, point))
+ return 0;
+
+ node = &ix2->root;
+ node_aabb = ix2->aabb;
+
+ /* find the leaf node containing point */
+ while (node->children) {
+ aabb_t quadrants[4];
+ int i;
+
+ /* TODO: this can be done more efficiently -
+ * by simply comparing point to the center and
+ * deriving the index from that
+ */
+ init_quadrants(quadrants, &node_aabb, &node->center);
+ for (i = 0; i < 4; i++) {
+ if (aabb_contains_point(&quadrants[i], point))
+ break;
+ }
+
+ node = &node->children[i];
+ node_aabb = quadrants[i];
+ }
+
+ /* search the leaf for matches */
+ list_for_each_entry_safe(ref, _ref, &node->objects, objects) {
+ if (!aabb_contains_point(&ref->object->aabb, point))
+ continue;
+
+ if (cb) {
+ ix2_search_status_t r;
+
+ r = cb(cb_context, ref->object, &ref->object->aabb, ref->object->object);
+ if (r == IX2_SEARCH_STOP)
+ break;
+
+ if (r == IX2_SEARCH_IGNORE)
+ continue;
+ }
+
+ n_hits++;
+ }
+
+
+ return n_hits;
+}
+
+
+/* recursively search the specified node for objects overlapping with search_aabb,
+ * add hits to the list @ hits.
+ */
+static void search_node_by_aabb(ix2_node_t *node, aabb_t *node_aabb, aabb_t *search_aabb, list_head_t *hits)
+{
+ ix2_object_ref_t *ref, *_ref;
+
+ assert(node);
+ assert(node_aabb);
+ assert(search_aabb);
+ assert(hits);
+
+ if (node->children) {
+ aabb_t quadrants[4];
+ int i;
+
+ init_quadrants(quadrants, node_aabb, &node->center);
+ for (i = 0; i < 4; i++) {
+ if (aabb_overlap(&quadrants[i], search_aabb))
+ search_node_by_aabb(&node->children[i], &quadrants[i], search_aabb, hits);
+ }
+
+ return;
+ }
+
+
+ list_for_each_entry_safe(ref, _ref, &node->objects, objects) {
+ if (!aabb_overlap(&ref->object->aabb, search_aabb))
+ continue;
+
+ list_move_tail(&ref->object->hits, hits);
+ }
+}
+
+
+/* Search for objects overlapping an aabb in index ix2. */
+/* Returns number of cb calls performed on hits, if cb is NULL calls will be skipped but hits still counted for return. */
+unsigned ix2_search_by_aabb(ix2_t *ix2, aabb_t *search_aabb, ix2_search_cb cb, void *cb_context)
+{
+ unsigned n_hits = 0;
+ LIST_HEAD (hits);
+ ix2_object_t *o, *_o;
+
+ assert(ix2);
+ assert(search_aabb);
+
+ if (!aabb_overlap(&ix2->aabb, search_aabb))
+ return 0;
+
+ /* XXX: for the aabb-based search a list of hits is first constructed,
+ * this prevents multiple callbacks on the same object by first
+ * allowing those multiple hits to simply move the object within the
+ * hits list. In a separate pass the hits are processed with the
+ * callbacks.
+ *
+ * There are two unfortunate consquences to this approach:
+ * 1. No nested/reentrant aabb searches on the same ix2, as the hit list
+ * node is embedded in the ix2_object_t.
+ * 2. No way to terminate the search from the callback.
+ * This may be mitigated by adding a limit count to the search
+ * function, but that can't address scenarios where you want to
+ * terminate the search on something arbitrary like after the first
+ * hit of a specific object type.
+ *
+ * TODO: investigate alternative solutions. Obvious options:
+ * - Putting more than one hit list node in every object for limited
+ * nesting.
+ * - Maintain a per-search hash-table which gets accessed before
+ * every callback to see if the object has been seen already, adding
+ * it if not before calling the callback. This strikes me as very
+ * heavyweight.
+ */
+ search_node_by_aabb(&ix2->root, &ix2->aabb, search_aabb, &hits);
+
+ list_for_each_entry_safe(o, _o, &hits, hits) {
+ list_del_init(&o->hits);
+
+ if (cb) {
+ ix2_search_status_t r;
+
+ r = cb(cb_context, o, &o->aabb, o->object);
+ if (r == IX2_SEARCH_STOP)
+ break;
+
+ if (r == IX2_SEARCH_IGNORE)
+ continue;
+ }
+
+ n_hits++;
+ }
+
+ /* just in case the search was aborted, finish deleting these nodes */
+ /* FIXME: this whole hit list thing is pretty janky */
+ list_for_each_entry_safe(o, _o, &hits, hits)
+ list_del_init(&o->hits);
+
+ return n_hits;
+}
© All Rights Reserved