You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ma...@apache.org on 2022/03/26 12:22:01 UTC
[commons-geometry] branch master updated: GEOMETRY-142: adding PointMap and PointSet implementations
This is an automated email from the ASF dual-hosted git repository.
mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
The following commit(s) were added to refs/heads/master by this push:
new 6c6d046 GEOMETRY-142: adding PointMap and PointSet implementations
6c6d046 is described below
commit 6c6d046532ab6c0c5cd670aaa4dc9ad384190d97
Author: Matt <ma...@apache.org>
AuthorDate: Thu Mar 10 22:33:07 2022 -0500
GEOMETRY-142: adding PointMap and PointSet implementations
---
.../commons/geometry/core/collection/PointMap.java | 41 +
.../PointSet.java} | 33 +-
.../package-info.java} | 23 +-
.../core/internal/AbstractBucketPointMap.java | 1034 +++++++++++
.../geometry/core/internal/AbstractPointMap1D.java | 171 ++
.../core/internal/GeometryInternalUtils.java | 18 +
.../core/internal/PointMapAsSetAdapter.java | 96 ++
.../core/collection/PointCollectionTestBase.java | 112 ++
.../geometry/core/collection/PointMapTestBase.java | 1809 ++++++++++++++++++++
.../geometry/core/collection/PointSetTestBase.java | 801 +++++++++
.../core/internal/AbstractBucketPointMapTest.java | 121 ++
.../core/internal/AbstractPointMap1DTest.java | 152 ++
.../core/internal/PointMapAsSetAdapterTest.java | 90 +
.../core/internal/TestBucketPointMap1D.java | 98 ++
.../geometry/euclidean/EuclideanCollections.java | 102 ++
.../commons/geometry/euclidean/PointMap1DImpl.java | 95 +
.../commons/geometry/euclidean/PointMap2DImpl.java | 151 ++
.../commons/geometry/euclidean/PointMap3DImpl.java | 175 ++
.../euclidean/threed/mesh/SimpleTriangleMesh.java | 44 +-
.../euclidean/DocumentationExamplesTest.java | 20 +
.../geometry/euclidean/oned/PointMap1DTest.java | 120 ++
.../geometry/euclidean/oned/PointSet1DTest.java | 87 +
.../geometry/euclidean/threed/PointMap3DTest.java | 175 ++
.../geometry/euclidean/threed/PointSet3DTest.java | 118 ++
.../geometry/euclidean/twod/PointMap2DTest.java | 166 ++
.../geometry/euclidean/twod/PointSet2DTest.java | 109 ++
commons-geometry-examples/examples-jmh/pom.xml | 9 +
.../jmh/euclidean/PointMap3DPerformance.java | 339 ++++
.../jmh/euclidean/pointmap/BucketKDTree.java | 984 +++++++++++
.../examples/jmh/euclidean/pointmap/KDTree.java | 576 +++++++
.../pointmap/PointMapDataStructurePerformance.java | 326 ++++
.../jmh/euclidean/pointmap/RebuildingKDTree.java | 248 +++
.../euclidean/pointmap/VariableSplitOctree.java | 449 +++++
.../jmh/euclidean/pointmap/Vector3DEntry.java | 29 +-
.../jmh/euclidean/pointmap/package-info.java | 25 +-
.../pointmap/PointMapDataStructureTest.java | 235 +++
.../commons/geometry/spherical/PointMap1SImpl.java | 299 ++++
.../commons/geometry/spherical/PointMap2SImpl.java | 181 ++
.../geometry/spherical/SphericalCollections.java | 78 +
.../commons/geometry/spherical/package-info.java | 26 +-
.../spherical/DocumentationExamplesTest.java | 20 +
.../geometry/spherical/oned/PointMap1STest.java | 132 ++
.../geometry/spherical/oned/PointSet1STest.java | 84 +
.../geometry/spherical/twod/PointMap2STest.java | 189 ++
.../geometry/spherical/twod/PointSet2STest.java | 117 ++
src/site/xdoc/userguide/index.xml | 65 +-
46 files changed, 10242 insertions(+), 130 deletions(-)
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/PointMap.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/PointMap.java
new file mode 100644
index 0000000..d8b16ab
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/PointMap.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.collection;
+
+import java.util.Map;
+
+import org.apache.commons.geometry.core.Point;
+
+/** {@link Map} type that uses points as keys. This interface is intended for
+ * use in cases where effectively equivalent (but not necessarily equal) points must
+ * map to the same entry. As such, this interface breaks the strict contract for
+ * {@link Map} where key equality is consistent with {@link Object#equals(Object)}.
+ * @param <P> Point type
+ * @param <V> Value type
+ */
+public interface PointMap<P extends Point<P>, V> extends Map<P, V> {
+
+ /** Get the map entry with a key equivalent to {@code pt} or {@code null}
+ * if no such entry exists. The returned instance supports use of
+ * the {@link Map.Entry#setValue(Object)} method to modify the
+ * mapping.
+ * @param pt point to fetch the map entry for
+ * @return map entry for the given point or null if no such entry
+ * exists
+ */
+ Map.Entry<P, V> getEntry(P pt);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/PointSet.java
similarity index 50%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/PointSet.java
index 8ba46f3..539f635 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/PointSet.java
@@ -14,24 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.internal;
+package org.apache.commons.geometry.core.collection;
-/** Internal utility methods for <em>commons-geometry</em>.
- */
-public final class GeometryInternalUtils {
+import java.util.Set;
+
+import org.apache.commons.geometry.core.Point;
- /** Utility class; no instantiation. */
- private GeometryInternalUtils() {}
+/** {@link Set} containing {@link Point} values. This interface is intended for
+ * use in cases where effectively equivalent (but not necessarily equal) points must
+ * be considered as equal by the set. As such, this interface breaks the strict contract
+ * for {@link Set} where membership is consistent with {@link Object#equals(Object)}.
+ * @param <P> Point type
+ */
+public interface PointSet<P extends Point<P>> extends Set<P> {
- /** Return {@code true} if {@code a} is the same instance as {@code b}, as
- * determined by the {@code ==} operator. This method exists primarily to
- * document the fact that reference equality was intended and is not a
- * programming error.
- * @param a first instance
- * @param b second instance
- * @return {@code true} if the arguments are the exact same instance
+ /** Get the set entry equivalent to {@code pt} or null if no
+ * such entry exists.
+ * @param pt point to find an equivalent for
+ * @return set entry equivalent to {@code pt} or null if
+ * no such entry exists
*/
- public static boolean sameInstance(final Object a, final Object b) {
- return a == b;
- }
+ P get(P pt);
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/package-info.java
similarity index 51%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/package-info.java
index 8ba46f3..a789a42 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/collection/package-info.java
@@ -14,24 +14,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.internal;
-
-/** Internal utility methods for <em>commons-geometry</em>.
- */
-public final class GeometryInternalUtils {
-
- /** Utility class; no instantiation. */
- private GeometryInternalUtils() {}
-
- /** Return {@code true} if {@code a} is the same instance as {@code b}, as
- * determined by the {@code ==} operator. This method exists primarily to
- * document the fact that reference equality was intended and is not a
- * programming error.
- * @param a first instance
- * @param b second instance
- * @return {@code true} if the arguments are the exact same instance
- */
- public static boolean sameInstance(final Object a, final Object b) {
- return a == b;
- }
-}
+/** This package contains core geometric collection types. */
+package org.apache.commons.geometry.core.collection;
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/AbstractBucketPointMap.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/AbstractBucketPointMap.java
new file mode 100644
index 0000000..cbb19b4
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/AbstractBucketPointMap.java
@@ -0,0 +1,1034 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.numbers.core.Precision;
+
+/** Abstract tree-based {@link PointMap} implementation that stores entries in bucket nodes
+ * that are split once a certain entry count threshold is reached. The main goal of this class
+ * is to provide a generic, multidimensional implementation that maintains reasonable performance
+ * regardless of point count and insertion order. Like other tree data structures, performance
+ * is tied closely to tree depth, which can vary depending on insertion order for a given set of
+ * points. In order to help maintain performance in cases of non-optimal point insertion order,
+ * this class uses a strategy of "point folding", implemented as follows:
+ * <ul>
+ * <li>Two separate tree roots are maintained by the map: a primary root and a secondary root.</li>
+ * <li>Entries are added to the primary root until the it reaches its capacity and is split using
+ * an algorithm specific to the space and dimension. At this point, the populated primary root
+ * becomes the secondary root and a new, empty primary root is created.</li>
+ * <li>Points are inserted into the new primary root as before. However, for each new point inserted,
+ * an existing point is removed from the secondary root and inserted into the primary root.</li>
+ * <li>Points are moved from the secondary root and inserted into the primary root in this way until the
+ * secondary root is empty. At this point, the primary root becomes the secondary root and another
+ * primary root is created.</li>
+ * </ul>
+ * In this way, previously inserted points can apply a balancing influence on the low levels of the tree
+ * as new points are inserted.
+ *
+ * <p>This class is <em>not</em> thread-safe.</p>
+ * @param <P> Point type
+ * @param <V> Map value type
+ */
+public abstract class AbstractBucketPointMap<P extends Point<P>, V>
+ extends AbstractMap<P, V>
+ implements PointMap<P, V> {
+
+ /** Function used to construct new node instances. */
+ private final BiFunction<AbstractBucketPointMap<P, V>, BucketNode<P, V>, BucketNode<P, V>> nodeFactory;
+
+ /** Maximum number of entries stored per node before the node is split. */
+ private final int maxNodeEntryCount;
+
+ /** Number of child nodes for each non-leaf node. */
+ private final int nodeChildCount;
+
+ /** Precision context. */
+ private final Precision.DoubleEquivalence precision;
+
+ /** Primary tree root. */
+ private BucketNode<P, V> root;
+
+ /** Secondary tree root. */
+ private BucketNode<P, V> secondaryRoot;
+
+ /** Version counter, used to track tree modifications. */
+ private int version;
+
+ /** Construct a new instance.
+ * @param nodeFactory object used to construct new node instances
+ * @param maxNodeEntryCount maximum number of map entries per node before
+ * the node is split
+ * @param nodeChildCount number of child nodes per internal node
+ * @param precision precision object used for floating point comparisons
+ */
+ protected AbstractBucketPointMap(
+ final BiFunction<AbstractBucketPointMap<P, V>, BucketNode<P, V>, BucketNode<P, V>> nodeFactory,
+ final int maxNodeEntryCount,
+ final int nodeChildCount,
+ final Precision.DoubleEquivalence precision) {
+ this.nodeFactory = nodeFactory;
+ this.maxNodeEntryCount = maxNodeEntryCount;
+ this.nodeChildCount = nodeChildCount;
+ this.precision = precision;
+ this.root = nodeFactory.apply(this, null);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Map.Entry<P, V> getEntry(final P pt) {
+ return findEntryByPoint(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return root.getEntryCount() +
+ (secondaryRoot != null ? secondaryRoot.getEntryCount() : 0);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V put(final P key, final V value) {
+ GeometryInternalUtils.validatePointMapKey(key);
+
+ Entry<P, V> entry = findEntryByPoint(key);
+ if (entry != null) {
+ return entry.setValue(value);
+ }
+
+ root.insertEntry(new SimpleEntry<>(key, value));
+ entryAdded();
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ return getValue(findEntry(key));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ @SuppressWarnings("unchecked")
+ final Entry<P, V> entry = removeEntryByPoint((P) key);
+ if (entry != null) {
+ entryRemoved();
+ return entry.getValue();
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean containsKey(final Object key) {
+ return findEntry(key) != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean containsValue(final Object value) {
+ return root.findEntryByValue(value) != null ||
+ (secondaryRoot != null && secondaryRoot.findEntryByValue(value) != null);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<P, V>> entrySet() {
+ return new EntrySet<>(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clear() {
+ root = createNode(null);
+ secondaryRoot = null;
+ }
+
+ /** Return true if the given points are equivalent using the precision
+ * configured for the map.
+ * @param a first point
+ * @param b second point
+ * @return true if the given points are equivalent
+ */
+ protected abstract boolean pointsEq(P a, P b);
+
+ /** Get the configured precision for the instance.
+ * @return precision object
+ */
+ protected Precision.DoubleEquivalence getPrecision() {
+ return precision;
+ }
+
+ /** Construct a new node instance.
+ * @param parent parent node; will be null or the tree root
+ * @return the new node instance
+ */
+ private BucketNode<P, V> createNode(final BucketNode<P, V> parent) {
+ return nodeFactory.apply(this, parent);
+ }
+
+ /** Method called when a new entry is added to the tree.
+ */
+ private void entryAdded() {
+ ++version;
+
+ if (!root.isLeaf() && secondaryRoot == null) {
+ secondaryRoot = root;
+ root = createNode(null);
+ }
+
+ migrateSecondaryEntry();
+ checkSecondaryRoot();
+ }
+
+ /** Method called when an entry is removed from the tree.
+ */
+ private void entryRemoved() {
+ ++version;
+
+ checkSecondaryRoot();
+ }
+
+ /** Create a list for storing map entries.
+ * @return list for storing map entries
+ */
+ private List<Entry<P, V>> createEntryList() {
+ return new ArrayList<>(maxNodeEntryCount);
+ }
+
+ /** Create a list for storing node children. The returned list contains
+ * {@code nodeChildCount} number of {@code null} entries.
+ * @return list for storing node children
+ */
+ @SuppressWarnings("unchecked")
+ private List<BucketNode<P, V>> createNodeChildList() {
+ return Arrays.asList(new BucketNode[nodeChildCount]);
+ }
+
+ /** Get the entry for the given key or null if not found.
+ * @param key key to search for
+ * @return entry for the given key or null if not found
+ */
+ @SuppressWarnings("unchecked")
+ private Entry<P, V> findEntry(final Object key) {
+ return findEntryByPoint((P) key);
+ }
+
+ /** Find the entry for the given point or null if one does not
+ * exist.
+ * @param pt point to find the entry for
+ * @return entry for the given point or null if one does not exist
+ */
+ private Entry<P, V> findEntryByPoint(final P pt) {
+ Entry<P, V> entry = null;
+ if (pt.isFinite()) {
+ entry = root.findEntry(pt);
+ if (entry == null && secondaryRoot != null) {
+ entry = secondaryRoot.findEntry(pt);
+ }
+ }
+ return entry;
+ }
+
+ /** Remove and return the entry for the given point or null
+ * if no such entry exists.
+ * @param pt point to remove the entry for
+ * @return the removed entry or null if not found
+ */
+ private Entry<P, V> removeEntryByPoint(final P pt) {
+ Entry<P, V> entry = null;
+ if (pt.isFinite()) {
+ entry = root.removeEntry(pt);
+ if (entry == null && secondaryRoot != null) {
+ entry = secondaryRoot.removeEntry(pt);
+ }
+ }
+ return entry;
+ }
+
+ /** Move an entry from the secondary root (if present) to the primary root. This process
+ * reintroduces points from a previous insertion back into higher levels of the root tree,
+ * thereby giving the root tree more balanced split points.
+ */
+ private void migrateSecondaryEntry() {
+ if (secondaryRoot != null) {
+ final int offset = version % nodeChildCount;
+ final boolean even = (offset & 1) > 0;
+ final int idx = even ?
+ offset / 2 :
+ nodeChildCount - 1 - (offset / 2);
+
+ final Entry<P, V> entry = secondaryRoot.removeEntryAlongChildIndexPath(idx);
+ if (entry != null) {
+ root.insertEntry(entry);
+ }
+ }
+ }
+
+ /** Remove the secondary root if empty.
+ */
+ private void checkSecondaryRoot() {
+ if (secondaryRoot != null && secondaryRoot.isEmpty()) {
+ secondaryRoot.destroy();
+ secondaryRoot = null;
+ }
+ }
+
+ /** Return the value for the argument or {@code null} if {@code entry}
+ * is {@code null}.
+ * @param <V> Value type
+ * @param entry entry to return the value for; may be null
+ * @return value for the argument or null if the argument is null
+ */
+ private static <V> V getValue(final Entry<?, V> entry) {
+ return entry != null ?
+ entry.getValue() :
+ null;
+ }
+
+ /** Spatial partitioning node type that stores map entries in a list until
+ * a threshold is reached, at which point the node is split.
+ * @param <P> Point type
+ * @param <V> Value type
+ */
+ protected abstract static class BucketNode<P extends Point<P>, V>
+ implements Iterable<Entry<P, V>> {
+
+ /** Owning map. */
+ private AbstractBucketPointMap<P, V> map;
+
+ /** Parent node. */
+ private BucketNode<P, V> parent;
+
+ /** Child nodes. */
+ private List<BucketNode<P, V>> children;
+
+ /** Entries stored in the node; will be null for non-leaf nodes. */
+ private List<Entry<P, V>> entries;
+
+ /** Number of entries in this subtree. */
+ private int entryCount;
+
+ /** Construct a new instance.
+ * @param map owning map
+ * @param parent parent node or null if the tree root
+ */
+ protected BucketNode(
+ final AbstractBucketPointMap<P, V> map,
+ final BucketNode<P, V> parent) {
+ this.map = map;
+ this.parent = parent;
+
+ // pull an entry list from the parent map; this will make
+ // this node a leaf initially
+ this.entries = map.createEntryList();
+ }
+
+ /**
+ * Return true if this node is a leaf node.
+ * @return true if this node is a leaf node
+ */
+ public boolean isLeaf() {
+ return entries != null;
+ }
+
+ /**
+ * Return true if the subtree rooted at this node does not
+ * contain any map entries.
+ * @return true if the subtree root at this node is empty
+ */
+ public boolean isEmpty() {
+ return entryCount < 1;
+ }
+
+ /** Get the number of map entries in the subtree rooted at this node.
+ * @return number of map entries in the subtree rooted at this node
+ */
+ public int getEntryCount() {
+ return entryCount;
+ }
+
+ /** Find and return the map entry matching the given key.
+ * @param key point key
+ * @return entry matching the given key or null if not found
+ */
+ public Entry<P, V> findEntry(final P key) {
+ if (isLeaf()) {
+ // leaf node; check the list of entries for a match
+ for (final Entry<P, V> entry : entries) {
+ if (map.pointsEq(key, entry.getKey())) {
+ return entry;
+ }
+ }
+ } else {
+ // internal node; delegate to each child that could possibly contain
+ // the point or an equivalent point
+ final int loc = getSearchLocation(key);
+ for (int i = 0; i < children.size(); ++i) {
+ if (testChildLocation(i, loc)) {
+ final Entry<P, V> entry = getEntryInChild(i, key);
+ if (entry != null) {
+ return entry;
+ }
+ }
+ }
+ }
+
+ // not found
+ return null;
+ }
+
+ /** Find the first entry in the tree with the given value or null if not found.
+ * @param value value to search for
+ * @return the first entry in the tree with the given value or null if not found
+ */
+ public Entry<P, V> findEntryByValue(final Object value) {
+ if (isLeaf()) {
+ // leaf node; check the list of entries for a match
+ for (final Entry<P, V> entry : entries) {
+ if (Objects.equals(entry.getValue(), value)) {
+ return entry;
+ }
+ }
+ } else {
+ // internal node; delegate to each child
+ for (final BucketNode<P, V> child : children) {
+ if (child != null) {
+ final Entry<P, V> childResult = child.findEntryByValue(value);
+ if (childResult != null) {
+ return childResult;
+ }
+ }
+ }
+ }
+
+ // not found
+ return null;
+ }
+
+ /** Insert a new entry into the subtree, returning the new size of the
+ * subtree. No check is made as to whether or not the entry already exists.
+ * @param entry entry to insert
+ */
+ public void insertEntry(final Map.Entry<P, V> entry) {
+ if (isLeaf()) {
+ if (entries.size() < map.maxNodeEntryCount) {
+ // we have an open spot here so just add the entry
+ append(entry);
+ return;
+ }
+
+ // no available entries; split the node and add the new
+ // entry to a child
+ splitNode();
+ }
+
+ // insert into the first matching child
+ final int loc = getInsertLocation(entry.getKey());
+
+ for (int i = 0; i < children.size(); ++i) {
+ if (testChildLocation(i, loc)) {
+ getOrCreateChild(i).insertEntry(entry);
+
+ // update the subtree state
+ subtreeEntryAdded();
+ break;
+ }
+ }
+ }
+
+ /** Remove the given key, returning the previously mapped entry.
+ * @param key key to remove
+ * @return the value previously mapped to the key or null if no
+ * value was mapped
+ */
+ public Entry<P, V> removeEntry(final P key) {
+ if (isLeaf()) {
+ // leaf node; check the existing entries for a match
+ final Iterator<Entry<P, V>> it = entries.iterator();
+ while (it.hasNext()) {
+ final Entry<P, V> entry = it.next();
+ if (map.pointsEq(key, entry.getKey())) {
+ it.remove();
+
+ // update the subtree state
+ subtreeEntryRemoved();
+
+ return entry;
+ }
+ }
+ } else {
+ // internal node; look through children
+ final int loc = getSearchLocation(key);
+ for (int i = 0; i < children.size(); ++i) {
+ if (testChildLocation(i, loc)) {
+ final Entry<P, V> entry = removeFromChild(i, key);
+ if (entry != null) {
+ // update the subtree state
+ subtreeEntryRemoved();
+
+ return entry;
+ }
+ }
+ }
+ }
+
+ // not found
+ return null;
+ }
+
+ /** Append an entry to the entry list for this node. This method must
+ * only be called on leaf nodes.
+ * @param entry entry to append
+ */
+ public void append(final Entry<P, V> entry) {
+ entries.add(entry);
+ subtreeEntryAdded();
+ }
+
+ /** Remove an entry in a leaf node lying on the given child index path.
+ * @param childIdx target child index
+ * @return removed entry
+ */
+ public Entry<P, V> removeEntryAlongChildIndexPath(final int childIdx) {
+ if (isLeaf()) {
+ if (!entries.isEmpty()) {
+ // remove the last entry in the list
+ final Entry<P, V> entry = entries.remove(entries.size() - 1);
+ subtreeEntryRemoved();
+
+ return entry;
+ }
+ } else {
+ final int childCount = children.size();
+ final int delta = childIdx < (map.nodeChildCount / 2) ?
+ +1 :
+ -1;
+
+ for (int n = 0, i = childIdx;
+ n < childCount;
+ ++n, i += delta) {
+ final int adjustedIndex = (i + childCount) % childCount;
+
+ final BucketNode<P, V> child = children.get(adjustedIndex);
+ if (child != null) {
+ final Entry<P, V> entry = child.removeEntryAlongChildIndexPath(childIdx);
+ if (entry != null) {
+ // destroy and remove the child if empty
+ if (child.isEmpty()) {
+ child.destroy();
+ children.set(adjustedIndex, null);
+ }
+
+ subtreeEntryRemoved();
+
+ return entry;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Destroy this node. The node must not be used after this method is called.
+ */
+ public void destroy() {
+ this.map = null;
+ this.parent = null;
+ this.children = null;
+ this.entries = null;
+ this.entryCount = 0;
+ }
+
+ /** Return true if this node has been destroyed.
+ * @return true if this node has been destroyed
+ */
+ public boolean isDestroyed() {
+ return map == null;
+ }
+
+ /** Return an iterator for accessing the entries stored in this node. The {@code remove()}
+ * method of the returned iterator correctly updates the tree state. This method must only
+ * be called on leaf nodes.
+ * @return iterator for accessing the entries stored in this node
+ */
+ @Override
+ public Iterator<Map.Entry<P, V>> iterator() {
+ return iterator(0);
+ }
+
+ /** Return an iterator for accessing the entries stored in this node, starting at the given
+ * index. The {@code remove()} method of the returned iterator correctly updates the tree state.
+ * This method must only be called on leaf nodes.
+ * @param idx starting index for the iterator
+ * @return iterator for accessing the entries stored in this node, starting with the entry at
+ * the given index
+ */
+ private Iterator<Map.Entry<P, V>> iterator(final int idx) {
+ final List<Map.Entry<P, V>> iteratedList = idx == 0 ?
+ entries :
+ entries.subList(idx, entries.size());
+
+ final Iterator<Map.Entry<P, V>> it = iteratedList.iterator();
+
+ return new Iterator<Map.Entry<P, V>>() {
+
+ @Override
+ public boolean hasNext() {
+ return !isDestroyed() && it.hasNext();
+ }
+
+ @Override
+ public Entry<P, V> next() {
+ return it.next();
+ }
+
+ @Override
+ public void remove() {
+ it.remove();
+
+ // store the owning map since we may be destroyed as part of
+ // entry removal
+ final AbstractBucketPointMap<P, V> owningMap = map;
+
+ // navigate up the tree and perform updates
+ BucketNode<P, V> current = BucketNode.this;
+ while (current != null) {
+ current.subtreeEntryRemoved();
+
+ current = current.parent;
+ }
+
+ // notify the owning map
+ owningMap.entryRemoved();
+ }
+ };
+ }
+
+ /** Compute the split for this node from the current set
+ * of map entries. Subclasses are responsible for managing
+ * the storage of the split.
+ */
+ protected abstract void computeSplit();
+
+ /** Get an int encoding the search locations of {@code pt} relative to the
+ * node split. The return value must include all possible locations of
+ * {@code pt} and equivalent points.
+ * @param pt point to determine the relative location of
+ * @return encoded search location
+ */
+ protected abstract int getSearchLocation(P pt);
+
+ /** Get an int encoding the insert location of {@code pt} relative to the
+ * node split. The return value must be strict and only include the single
+ * location where {@code pt} should be inserted.
+ * @param pt point to determine the insert location of
+ * @return encoded insert location
+ */
+ protected abstract int getInsertLocation(P pt);
+
+ /** Return true if the child node at {@code childIdx} matches the given
+ * encoded point location.
+ * @param childIdx child index to test
+ * @param loc encoded relative point location
+ * @return true if the child node a {@code childIdx} matches the location
+ */
+ protected abstract boolean testChildLocation(int childIdx, int loc);
+
+ /** Make this node a leaf node, using the given list of entries.
+ * @param leafEntries list of map entries to use for the node
+ */
+ protected void makeLeaf(final List<Entry<P, V>> leafEntries) {
+ children = null;
+ entries = leafEntries;
+ }
+
+ /** Split the node and place all entries into the new child nodes.
+ * This node becomes an internal node.
+ */
+ protected void splitNode() {
+ computeSplit();
+
+ children = map.createNodeChildList();
+
+ for (final Entry<P, V> entry : entries) {
+ moveToChild(entry);
+ }
+
+ entries = null;
+ }
+
+ /** Get the precision context for the instance.
+ * @return precision context for the instance
+ */
+ protected Precision.DoubleEquivalence getPrecision() {
+ return map.precision;
+ }
+
+ /** Get the given entry in the child at {@code idx} or null if not found.
+ * @param idx child index
+ * @param key key to search for
+ * @return entry matching {@code key} in child or null if not found
+ */
+ private Entry<P, V> getEntryInChild(final int idx, final P key) {
+ final BucketNode<P, V> child = children.get(idx);
+ if (child != null) {
+ return child.findEntry(key);
+ }
+ return null;
+ }
+
+ /** Remove the given key from the child at {@code idx}.
+ * @param idx index of the child
+ * @param key key to remove
+ * @return entry removed from the child or null if not found
+ */
+ private Entry<P, V> removeFromChild(final int idx, final P key) {
+ final BucketNode<P, V> child = children.get(idx);
+ if (child != null) {
+ return child.removeEntry(key);
+ }
+ return null;
+ }
+
+ /** Move the previously created entry to a child node.
+ * @param entry entry to mode
+ */
+ private void moveToChild(final Entry<P, V> entry) {
+ final int loc = getInsertLocation(entry.getKey());
+
+ final int numChildren = children.size();
+ for (int i = 0; i < numChildren; ++i) {
+ // place the entry in the first child that contains it
+ if (testChildLocation(i, loc)) {
+ getOrCreateChild(i).append(entry);
+ break;
+ }
+ }
+ }
+
+ /** Get the child node at the given index, creating it if needed.
+ * @param idx index of the child node
+ * @return child node at the given index
+ */
+ private BucketNode<P, V> getOrCreateChild(final int idx) {
+ BucketNode<P, V> child = children.get(idx);
+ if (child == null) {
+ child = map.createNode(this);
+ children.set(idx, child);
+ }
+ return child;
+ }
+
+ /** Method called when an entry is added to the subtree represented
+ * by this node.
+ */
+ private void subtreeEntryAdded() {
+ ++entryCount;
+ }
+
+ /** Method called when an entry is removed from the subtree represented
+ * by this node. If the subtree is an empty internal node, it is converted
+ * to a leaf node.
+ */
+ private void subtreeEntryRemoved() {
+ --entryCount;
+
+ final int condenseThreshold = map.maxNodeEntryCount / 2;
+
+ if (!isLeaf() && entryCount <= condenseThreshold) {
+ final List<Entry<P, V>> subtreeEntries = map.createEntryList();
+
+ if (!isEmpty() &&
+ (parent == null || parent.getEntryCount() > condenseThreshold)) {
+ collectSubtreeEntriesRecursive(subtreeEntries, false);
+ }
+
+ makeLeaf(subtreeEntries);
+ }
+ }
+
+ /** Add all map entries in the subtree rooted at this node to {@code entryList}. If {@code destroyNode}
+ * is true, the node is destroyed after its entries are added to the list.
+ * @param entryList list to add entries to
+ * @param destroyNode if true, the node will be destroyed after its entries are added to the list
+ */
+ private void collectSubtreeEntriesRecursive(final List<Entry<P, V>> entryList, final boolean destroyNode) {
+ if (isLeaf()) {
+ entryList.addAll(entries);
+ } else {
+ for (final BucketNode<P, V> child : children) {
+ if (child != null) {
+ child.collectSubtreeEntriesRecursive(entryList, true);
+ }
+ }
+ }
+
+ if (destroyNode) {
+ destroy();
+ }
+ }
+
+ /** Get an encoded search location value for the given comparison result. If
+ * {@code cmp} is {@code 0}, then the bitwise OR of {@code neg} and {@code pos}
+ * is returned, indicating that both spaces are valid search locations. Otherwise,
+ * {@code neg} is returned for negative {@code cmp} values and {@code pos} for
+ * positive ones. This location value is to be used during entry searches,
+ * when comparisons must be loose and all possible locations included.
+ * @param cmp comparison result
+ * @param neg negative flag
+ * @param pos positive flag
+ * @return encoded search location value
+ */
+ public static int getSearchLocationValue(final int cmp, final int neg, final int pos) {
+ if (cmp < 0) {
+ return neg;
+ } else if (cmp > 0) {
+ return pos;
+ }
+ return neg | pos;
+ }
+
+ /** Get an insert location value for the given comparison result. If {@code cmp}
+ * is less than or equal to {@code 0}, then {@code neg} is returned. Otherwise,
+ * {@code pos} is returned. This location value is to be used during entry inserts,
+ * where comparisons must be strict.
+ * @param cmp comparison result
+ * @param neg negative flag
+ * @param pos positive flag
+ * @return encoded insert location value
+ */
+ public static int getInsertLocationValue(final int cmp, final int neg, final int pos) {
+ return cmp <= 0 ?
+ neg :
+ pos;
+ }
+ }
+
+ /** Set view of the map entries.
+ * @param <P> Point type
+ * @param <V> Value type
+ */
+ private static final class EntrySet<P extends Point<P>, V>
+ extends AbstractSet<Map.Entry<P, V>> {
+
+ /** Owning map. */
+ private final AbstractBucketPointMap<P, V> map;
+
+ /** Construct a new instance for the given map.
+ * @param map map instance
+ */
+ EntrySet(final AbstractBucketPointMap<P, V> map) {
+ this.map = map;
+ }
+
+ /** {@inheritDoc} */
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean contains(final Object obj) {
+ if (obj instanceof Map.Entry) {
+ final Map.Entry<?, ?> search = (Map.Entry<?, ?>) obj;
+ final Object key = search.getKey();
+
+ final Map.Entry<P, V> actual = map.findEntry(key);
+ if (actual != null) {
+ return map.pointsEq(actual.getKey(), (P) search.getKey()) &&
+ Objects.equals(actual.getValue(), search.getValue());
+ }
+ }
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<Entry<P, V>> iterator() {
+ return new EntryIterator<>(map);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return map.size();
+ }
+ }
+
+ /** Iterator for iterating through each entry in the map.
+ * @param <P> Point type
+ * @param <V> Value type
+ */
+ private static final class EntryIterator<P extends Point<P>, V>
+ implements Iterator<Map.Entry<P, V>> {
+
+ /** Owning map. */
+ private final AbstractBucketPointMap<P, V> map;
+
+ /** Size of the owning map. */
+ private int size;
+
+ /** Iterator that produces the next entry to be returned. */
+ private Iterator<Map.Entry<P, V>> nextEntryIterator;
+
+ /** Index of the entry that will be returned next from the iterator. */
+ private int nextIdx;
+
+ /** Expected map modification version. */
+ private int expectedVersion;
+
+ /** Construct a new instance for the given map.
+ * @param map map instance
+ */
+ EntryIterator(final AbstractBucketPointMap<P, V> map) {
+ this.map = map;
+ this.size = map.size();
+
+ updateExpectedVersion();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasNext() {
+ return nextIdx < size;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Entry<P, V> next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ checkVersion();
+
+ if (nextEntryIterator == null ||
+ !nextEntryIterator.hasNext()) {
+ nextEntryIterator = findIterator();
+ }
+
+ final Map.Entry<P, V> result = nextEntryIterator.next();
+ ++nextIdx;
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void remove() {
+ if (nextEntryIterator == null) {
+ throw new IllegalStateException("Cannot remove: no entry has yet been returned");
+ }
+
+ nextEntryIterator.remove();
+ --nextIdx;
+ --size;
+
+ updateExpectedVersion();
+ }
+
+ /** Find the next entry iterator in the map.
+ * @return next map entry iterator
+ */
+ private Iterator<Entry<P, V>> findIterator() {
+ int offset = 0;
+ if (map.secondaryRoot != null) {
+ final Iterator<Entry<P, V>> secondaryIt = findIteratorRecursive(map.secondaryRoot, offset);
+ if (secondaryIt != null) {
+ return secondaryIt;
+ }
+
+ offset += map.secondaryRoot.getEntryCount();
+ }
+
+ return findIteratorRecursive(map.root, offset);
+ }
+
+ /** Find the next map entry iterator recursively in the subtree rooted at {@code node}.
+ * @param node root of the subtree to obtain an iterator in
+ * @param offset index offset of the first entry in the subtree
+ * @return the next map entry iterator or null if no leaf nodes in the subtree contain the
+ * entry at {@code nextIdx}
+ */
+ private Iterator<Entry<P, V>> findIteratorRecursive(final BucketNode<P, V> node, final int offset) {
+ if (nextIdx >= offset && nextIdx < offset + node.getEntryCount()) {
+ if (node.isLeaf()) {
+ return node.iterator(nextIdx - offset);
+ } else {
+ return findIteratorInNodeChildren(node, offset);
+ }
+ }
+
+ return null;
+ }
+
+ /** Find the next map entry iterator in the children of {@code node}.
+ * @param node root of the subtree to obtain an iterator in
+ * @param offset index offset of the first entry in the subtree
+ * @return the next map entry iterator or null if no leaf nodes in the subtree contain the
+ * entry at {@code nextIdx}
+ */
+ private Iterator<Entry<P, V>> findIteratorInNodeChildren(final BucketNode<P, V> node, final int offset) {
+ final int childCount = node.children.size();
+
+ int currentOffset = offset;
+ for (int i = 0; i < childCount; ++i) {
+ final BucketNode<P, V> child = node.children.get(i);
+ if (child != null) {
+ Iterator<Entry<P, V>> childIt = findIteratorRecursive(child, currentOffset);
+ if (childIt != null) {
+ return childIt;
+ }
+
+ currentOffset += child.getEntryCount();
+ }
+ }
+ return null;
+ }
+
+ /** Throw a {@link ConcurrentModificationException} if the map version does
+ * not match the expected version.
+ */
+ private void checkVersion() {
+ if (map.version != expectedVersion) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /** Update the expected modification version of the map. This must be called
+ * whenever the map is changed through this instance.
+ */
+ private void updateExpectedVersion() {
+ expectedVersion = map.version;
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/AbstractPointMap1D.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/AbstractPointMap1D.java
new file mode 100644
index 0000000..8270c20
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/AbstractPointMap1D.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.collection.PointMap;
+
+/** Abstract base class for 1D {@link PointMap} implementations. This class delegates
+ * entry storage to an internal {@link TreeMap} instance. Simple methods, such as
+ * {@link Map#size()} are directly implemented here but subclasses must provide their
+ * own logic for manipulating the map entries.
+ * @param <P> Point type
+ * @param <V> Value type
+ */
+public abstract class AbstractPointMap1D<P extends Point<P>, V>
+ implements PointMap<P, V> {
+
+ /** Underlying map. */
+ private final NavigableMap<P, V> map;
+
+ /** Construct a new instance that uses the given comparator in the
+ * underlying {@link TreeMap}.
+ * @param cmp tree map comparator
+ */
+ protected AbstractPointMap1D(final Comparator<P> cmp) {
+ this.map = new TreeMap<>(cmp);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return map.size();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean containsValue(final Object value) {
+ return map.containsValue(value);
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public Map.Entry<P, V> getEntry(final P key) {
+ return exportEntry(getEntryInternal(key));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V put(final P key, final V value) {
+ GeometryInternalUtils.validatePointMapKey(key);
+ return putInternal(key, value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void putAll(final Map<? extends P, ? extends V> m) {
+ for (final Map.Entry<? extends P, ? extends V> entry : m.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Collection<V> values() {
+ return map.values();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return map.hashCode();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ return map.equals(obj);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return map.toString();
+ }
+
+ /** Get the raw {@link Map.Entry} for the given key from the underlying map.
+ * @param key map key
+ * @return entry for the given key or {@code null} if not found
+ */
+ protected abstract Map.Entry<P, V> getEntryInternal(P key);
+
+ /** Add or update the entry for the given key/value pair.
+ * @param key entry key
+ * @param value entry value
+ * @return the value of the previous entry for {@code key} or null
+ * if no such entry exists
+ */
+ protected abstract V putInternal(P key, V value);
+
+ /** Get the underlying map instance.
+ * @return map instance
+ */
+ protected NavigableMap<P, V> getMap() {
+ return map;
+ }
+
+ /** Return a {@link Map.Entry} instance containing the values from the
+ * argument and suitable for direct use by external users. The returned
+ * entry supports use of the {@link Map.Entry#setValue(Object)} method.
+ * @param entry entry to export
+ * @return entry instance suitable for direct user by callers
+ */
+ protected Entry<P, V> exportEntry(final Entry<P, V> entry) {
+ return entry != null ?
+ new MutableEntryWrapper(entry) :
+ null;
+ }
+
+ /** {@link Map.Entry} subclass that adds support for the {@link Map.Entry#setValue(Object)}.
+ */
+ private final class MutableEntryWrapper extends SimpleEntry<P, V> {
+
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20220317L;
+
+ /** Construct a new instance representing the same mapping as the argument.
+ * @param entry target entry
+ */
+ MutableEntryWrapper(final Entry<? extends P, ? extends V> entry) {
+ super(entry);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V setValue(final V value) {
+ // replace the value in the map
+ map.replace(getKey(), value);
+
+ // set the local value
+ return super.setValue(value);
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
index 8ba46f3..2856fb9 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
@@ -16,6 +16,10 @@
*/
package org.apache.commons.geometry.core.internal;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Point;
+
/** Internal utility methods for <em>commons-geometry</em>.
*/
public final class GeometryInternalUtils {
@@ -34,4 +38,18 @@ public final class GeometryInternalUtils {
public static boolean sameInstance(final Object a, final Object b) {
return a == b;
}
+
+ /** Throw an exception if the given point cannot be used as a key in a
+ * {@link org.apache.commons.geometry.core.collection.PointMap PointMap}.
+ * @param <P> Point type
+ * @param pt point to check
+ * @throws NullPointerException if the point is null
+ * @throws IllegalArgumentException if the point is not finite
+ */
+ public static <P extends Point<P>> void validatePointMapKey(final P pt) {
+ Objects.requireNonNull(pt);
+ if (!pt.isFinite()) {
+ throw new IllegalArgumentException("Non-finite map key: " + pt);
+ }
+ }
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/PointMapAsSetAdapter.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/PointMapAsSetAdapter.java
new file mode 100644
index 0000000..a841645
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/PointMapAsSetAdapter.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.collection.PointSet;
+
+/** Internal utility class that exposes a {@link PointMap} as a {@link PointSet}.
+ * This class is not intended for direct use by users of this library. Users should
+ * instead create {@link PointSet} instances using the factory methods available in
+ * each space.
+ * @param <P> Point type
+ * @param <M> Map type
+ */
+public class PointMapAsSetAdapter<P extends Point<P>, M extends PointMap<P, Object>>
+ extends AbstractSet<P>
+ implements PointSet<P> {
+
+ /** Dummy map value used to indicate presence in the set. */
+ private static final Object PRESENT = new Object();
+
+ /** Backing map. */
+ private final M map;
+
+ /** Construct a new instance that use the argument as its backing map.
+ * @param backingMap backing map
+ */
+ public PointMapAsSetAdapter(final M backingMap) {
+ this.map = backingMap;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public P get(final P pt) {
+ final Map.Entry<P, Object> entry = map.getEntry(pt);
+ return entry != null ?
+ entry.getKey() :
+ null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<P> iterator() {
+ return map.keySet().iterator();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return map.size();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Object obj) {
+ return map.containsKey(obj);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean add(final P pt) {
+ return map.put(pt, PRESENT) == null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean remove(final Object obj) {
+ final Object prev = map.remove(obj);
+ return GeometryInternalUtils.sameInstance(prev, PRESENT);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clear() {
+ map.clear();
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointCollectionTestBase.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointCollectionTestBase.java
new file mode 100644
index 0000000..c7071e4
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointCollectionTestBase.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.collection;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.numbers.core.Precision;
+
+/** Base test class for point collection types.
+ * @param <P> Point type
+ */
+public abstract class PointCollectionTestBase<P extends Point<P>> {
+
+ public static final double EPS = 1e-10;
+
+ public static final Precision.DoubleEquivalence PRECISION =
+ Precision.doubleEquivalenceOfEpsilon(EPS);
+
+ /** Create an empty array of the target point type.
+ * @return empty array of the target point type
+ */
+ protected abstract P[] createPointArray();
+
+ /** Get a list of points with {@code NaN} coordinates.
+ * @return list of points with {@code NaN} coordinates
+ */
+ protected abstract List<P> getNaNPoints();
+
+ /** Get a list of points with infinite coordinates.
+ * @return list of points with infinite coordinates
+ */
+ protected abstract List<P> getInfPoints();
+
+ /** Get {@code cnt} number of unique test points that differ from each other in
+ * each dimension by <em>at least</em> {@code eps}.
+ * @param cnt number of points to return
+ * @param eps minimum value that each point must differ from other points along
+ * each dimension
+ * @return list of test points
+ */
+ protected abstract List<P> getTestPoints(int cnt, double eps);
+
+ /** Get a list of points that lie {@code dist} distance from {@code pt}.
+ * @param pt input point
+ * @param dist distance from {@code pt}
+ * @return list of points that lie {@code dist} distance from {@code pt}
+ */
+ protected abstract List<P> getTestPointsAtDistance(P pt, double dist);
+
+ /** Get {@code cnt} number of unique test points that differ from each other in
+ * each dimension by <em>at least</em> {@code eps}. The returned list is shuffled
+ * using {@code rnd}.
+ * @param cnt number of points to return
+ * @param eps minimum value that each point must differ from other points along
+ * each dimension
+ * @param rnd random instance used to shuffle the order of the points
+ * @return randomly ordered list of test points
+ */
+ protected List<P> getTestPoints(final int cnt, final double eps, final Random rnd) {
+ final List<P> pts = new ArrayList<>(getTestPoints(cnt, eps));
+ Collections.shuffle(pts, rnd);
+
+ return pts;
+ }
+
+ /** Return true if the given points are equivalent to each other using the given precision.
+ * @param a first point
+ * @param b second point
+ * @param precision precision context
+ * @return true if the two points are equivalent when compared using the given precision
+ */
+ protected abstract boolean eq(P a, P b, Precision.DoubleEquivalence precision);
+
+ /** Assert that {@code a} and {@code b} are equivalent using the given precision context.
+ * @param a first point
+ * @param b second point
+ * @param precision precision context
+ */
+ protected void assertEq(final P a, final P b, final Precision.DoubleEquivalence precision) {
+ assertTrue(eq(a, b, precision), () -> "Expected " + a + " and " + b + " to be equivalent");
+ }
+
+ /** Assert that {@code a} and {@code b} are not equivalent using the given precision context.
+ * @param a first point
+ * @param b second point
+ * @param precision precision context
+ */
+ protected void assertNotEq(final P a, final P b, final Precision.DoubleEquivalence precision) {
+ assertFalse(eq(a, b, precision), () -> "Expected " + a + " and " + b + " to not be equivalent");
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointMapTestBase.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointMapTestBase.java
new file mode 100644
index 0000000..4cb7477
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointMapTestBase.java
@@ -0,0 +1,1809 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.collection;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Base test class for {@link PointMap} implementations.
+ * @param <P> Point type
+ */
+public abstract class PointMapTestBase<P extends Point<P>>
+ extends PointCollectionTestBase<P> {
+
+ /** Get a new point map instance for testing.
+ * @param <V> Value type
+ * @param precision precision context to determine floating point equality
+ * @return a new map instance for testing.
+ */
+ protected abstract <V> PointMap<P, V> getMap(Precision.DoubleEquivalence precision);
+
+ @Test
+ void testEmpty() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final P pt = getTestPoints(1, EPS).get(0);
+
+ // act/assert
+ Assertions.assertEquals(0, map.size());
+ Assertions.assertTrue(map.isEmpty());
+
+ Assertions.assertNull(map.get(pt));
+ Assertions.assertFalse(map.containsKey(pt));
+ }
+
+ @Test
+ void testSingleEntry() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ final P a = pts.get(0);
+
+ // act
+ map.put(a, 1);
+
+ // assert
+ checkerFor(map)
+ .expectEntry(a, 1)
+ .doesNotContainKeys(pts.subList(1, pts.size()))
+ .check();
+ }
+
+ @Test
+ void testMultipleEntries() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final PointMapChecker<P, Integer> checker = checkerFor(map);
+
+ final int putCnt = 1000;
+ final List<P> pts = getTestPoints(putCnt * 2, EPS, new Random(1L));
+
+ // act
+ for (int i = 0; i < putCnt; ++i) {
+ final P key = pts.get(i);
+
+ map.put(key, i);
+
+ checker.expectEntry(key, i);
+ }
+
+ // assert
+ checker
+ .doesNotContainKeys(pts.subList(putCnt, pts.size()))
+ .check();
+ }
+
+ @Test
+ void testGet() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ insertPoints(pts.subList(1, 3), map);
+ // act/assert
+ Assertions.assertNull(map.get(pts.get(0)));
+
+ Assertions.assertEquals(Integer.valueOf(0), map.get(pts.get(1)));
+ Assertions.assertEquals(Integer.valueOf(1), map.get(pts.get(2)));
+ }
+
+ @Test
+ void testGet_equivalentPoints_singleEntry() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(1, EPS);
+ insertPoints(pts, map);
+
+ final P pt = pts.get(0);
+
+ // act/assert
+ Assertions.assertEquals(0, map.get(pt));
+ Assertions.assertTrue(map.containsKey(pt));
+ Assertions.assertEquals(new SimpleEntry<>(pt, 0), map.getEntry(pt));
+
+ for (final P closePt : getTestPointsAtDistance(pt, EPS * 0.75)) {
+ Assertions.assertEquals(0, map.get(closePt));
+ Assertions.assertTrue(map.containsKey(closePt));
+ Assertions.assertEquals(new SimpleEntry<>(pt, 0), map.getEntry(closePt));
+
+ Assertions.assertTrue(map.entrySet().contains(new SimpleEntry<>(closePt, 0)));
+ Assertions.assertTrue(map.keySet().contains(closePt));
+
+ assertEq(closePt, pt, PRECISION);
+ }
+
+ for (final P farPt : getTestPointsAtDistance(pt, EPS * 1.25)) {
+ Assertions.assertNull(map.get(farPt));
+ Assertions.assertFalse(map.containsKey(farPt));
+ Assertions.assertNull(map.getEntry(farPt));
+
+ Assertions.assertFalse(map.entrySet().contains(new SimpleEntry<>(farPt, 0)));
+ Assertions.assertFalse(map.keySet().contains(farPt));
+
+ assertNotEq(farPt, pt, PRECISION);
+ }
+ }
+
+ @Test
+ void testGet_equivalentPoints_multipleEntries() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(1_000, 3 * EPS);
+ insertPoints(pts, map);
+
+ // act/assert
+ int i = -1;
+ for (final P pt : pts) {
+ final int value = ++i;
+
+ Assertions.assertEquals(value, map.get(pt));
+ Assertions.assertTrue(map.containsKey(pt));
+ Assertions.assertEquals(new SimpleEntry<>(pt, value), map.getEntry(pt));
+
+ for (final P closePt : getTestPointsAtDistance(pt, EPS * 0.75)) {
+ Assertions.assertEquals(value, map.get(closePt));
+ Assertions.assertTrue(map.containsKey(closePt));
+ Assertions.assertEquals(new SimpleEntry<>(pt, value), map.getEntry(closePt));
+
+ Assertions.assertTrue(map.entrySet().contains(new SimpleEntry<>(closePt, value)));
+ Assertions.assertTrue(map.keySet().contains(closePt));
+
+ assertEq(closePt, pt, PRECISION);
+ }
+
+ for (final P farPt : getTestPointsAtDistance(pt, EPS * 1.25)) {
+ Assertions.assertNull(map.get(farPt));
+ Assertions.assertFalse(map.containsKey(farPt));
+ Assertions.assertNull(map.getEntry(farPt));
+
+ Assertions.assertFalse(map.entrySet().contains(new SimpleEntry<>(farPt, 0)));
+ Assertions.assertFalse(map.keySet().contains(farPt));
+
+ assertNotEq(farPt, pt, PRECISION);
+ }
+ }
+ }
+
+ @Test
+ void testGet_invalidArgs() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ insertPoints(getTestPoints(3, EPS), map);
+
+ // act/assert
+ Assertions.assertThrows(NullPointerException.class, () -> map.get(null));
+ Assertions.assertThrows(ClassCastException.class, () -> map.get(new Object()));
+ }
+
+ @Test
+ void testGet_nanAndInf() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ insertPoints(getTestPoints(100, EPS), map);
+
+ // act/assert
+ for (final P pt : getNaNPoints()) {
+ Assertions.assertNull(map.get(pt));
+ }
+
+ for (final P pt : getInfPoints()) {
+ Assertions.assertNull(map.get(pt));
+ }
+ }
+
+ @Test
+ void testGetEntry_canSetValue() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(100, EPS);
+ insertPoints(pts, map);
+
+ final Map.Entry<P, Integer> entry = map.getEntry(pts.get(1));
+
+ // act
+ Assertions.assertEquals(1, entry.setValue(Integer.valueOf(-1)));
+ Assertions.assertEquals(-1, entry.setValue(Integer.valueOf(-2)));
+
+ // assert
+ Assertions.assertEquals(-2, map.get(pts.get(1)));
+ }
+
+ @Test
+ void testGetEntry_setCalledAfterEntryRemoved_updatesLocalEntryOnly() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(100, EPS);
+ insertPoints(pts, map);
+
+ final Map.Entry<P, Integer> entry = map.getEntry(pts.get(49));
+ final Integer oldValue = entry.getValue();
+
+ // act/assert
+ map.remove(entry.getKey());
+
+ Assertions.assertEquals(oldValue, entry.setValue(-1));
+ Assertions.assertEquals(-1, entry.setValue(-2));
+
+ Assertions.assertNull(map.get(entry.getKey()));
+ }
+
+ @Test
+ void testPut_replaceValue() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ final P a = pts.get(0);
+
+ // act
+ Assertions.assertNull(map.put(a, 1));
+ Assertions.assertEquals(1, map.put(a, 2));
+ Assertions.assertEquals(2, map.put(a, 3));
+
+ // assert
+ checkerFor(map)
+ .expectEntry(a, 3)
+ .doesNotContainKeys(pts.subList(1, pts.size()))
+ .check();
+ }
+
+ @Test
+ void testPut_equivalentValues_multipleEntries() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(1_000, EPS, new Random(10L));
+ insertPoints(pts, map);
+
+ final double delta = 0.75 * EPS;
+
+ // act/assert
+ int i = 0;
+ for (final P pt : pts) {
+ final List<P> closePts = getTestPointsAtDistance(pt, delta);
+ checkPut(map, closePts, i++);
+ }
+ }
+
+ private void checkPut(final PointMap<P, Integer> map, final List<? extends P> pts, final int startValue) {
+ int currentValue = startValue;
+ for (final P pt : pts) {
+ int nextValue = currentValue + 1;
+
+ Assertions.assertEquals(currentValue, map.put(pt, nextValue));
+ Assertions.assertEquals(nextValue, map.get(pt));
+
+ currentValue = nextValue;
+ }
+ }
+
+ @Test
+ void testPut_nullKey() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ // act/assert
+ Assertions.assertThrows(NullPointerException.class, () -> map.put(null, 0));
+ }
+
+ @Test
+ void testPut_nanAndInf() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ // act/assert
+ for (final P nanPt : getNaNPoints()) {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> map.put(nanPt, 0));
+ }
+
+ for (final P infPt : getInfPoints()) {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> map.put(infPt, 0));
+ }
+ }
+
+ @Test
+ void testPutAll_nonPointMap() {
+ // arrange
+ final Map<P, Integer> a = new HashMap<>();
+ final Map<P, Integer> b = new HashMap<>();
+
+ final PointMap<P, Integer> c = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(5, EPS);
+
+ a.put(pts.get(0), 0);
+ a.put(pts.get(1), 1);
+ a.put(pts.get(2), 2);
+
+ b.put(pts.get(2), 0);
+ b.put(pts.get(3), 1);
+ b.put(pts.get(4), 2);
+
+ // act
+ c.putAll(a);
+ c.putAll(b);
+
+ // assert
+ checkerFor(c)
+ .expectEntry(pts.get(0), 0)
+ .expectEntry(pts.get(1), 1)
+ .expectEntry(pts.get(2), 0)
+ .expectEntry(pts.get(3), 1)
+ .expectEntry(pts.get(4), 2)
+ .check();
+ }
+
+ @Test
+ void testPutAll_otherPointMap() {
+ // arrange
+ final PointMap<P, Integer> a = getMap(PRECISION);
+ final PointMap<P, Integer> b = getMap(PRECISION);
+ final PointMap<P, Integer> c = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(5, EPS);
+
+ insertPoints(pts.subList(0, 3), a);
+ insertPoints(pts.subList(2, 5), b);
+
+ // act
+ c.putAll(a);
+ c.putAll(b);
+
+ // assert
+ checkerFor(c)
+ .expectEntry(pts.get(0), 0)
+ .expectEntry(pts.get(1), 1)
+ .expectEntry(pts.get(2), 0)
+ .expectEntry(pts.get(3), 1)
+ .expectEntry(pts.get(4), 2)
+ .check();
+ }
+
+ @Test
+ void testPutAll_nanAndInf() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ // act/assert
+ for (final P nanPt : getNaNPoints()) {
+ final Map<P, Integer> nanMap = new HashMap<>();
+ nanMap.put(nanPt, 0);
+
+ Assertions.assertThrows(IllegalArgumentException.class, () -> map.putAll(nanMap));
+ }
+
+ for (final P infPt : getInfPoints()) {
+ final Map<P, Integer> infMap = new HashMap<>();
+ infMap.put(infPt, 0);
+
+ Assertions.assertThrows(IllegalArgumentException.class, () -> map.putAll(infMap));
+ }
+ }
+
+ @Test
+ void testRemove() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+ final P c = pts.get(2);
+ final P d = pts.get(3);
+
+ map.put(a, 1);
+ map.put(b, 2);
+ map.put(c, 3);
+
+ // act/assert
+ Assertions.assertNull(map.remove(d));
+ Assertions.assertEquals(1, map.remove(a));
+ Assertions.assertEquals(2, map.remove(b));
+ Assertions.assertEquals(3, map.remove(c));
+
+ Assertions.assertNull(map.remove(a));
+ Assertions.assertNull(map.remove(b));
+ Assertions.assertNull(map.remove(c));
+ Assertions.assertNull(map.remove(d));
+
+ checkerFor(map)
+ .doesNotContainKeys(pts)
+ .check();
+ }
+
+ @Test
+ void testRemove_equivalentPoints() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(100, EPS, new Random(9L));
+ insertPoints(pts, map);
+
+ // act
+ for (final P pt : pts) {
+ final Integer val = map.get(pt);
+
+ for (final P closePt : getTestPointsAtDistance(pt, 0.9 * EPS)) {
+ map.put(pt, val);
+
+ Assertions.assertEquals(val, map.remove(closePt));
+ }
+ }
+
+ // assert
+ assertEmpty(map);
+ }
+
+ @Test
+ void testRemove_largeEntryCount() {
+ // -- arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final Random rnd = new Random(2L);
+
+ final int cnt = 10_000;
+ final List<P> pts = getTestPoints(cnt * 2, EPS, rnd);
+
+ final List<P> testPts = new ArrayList<>(pts.subList(0, cnt));
+ final List<P> otherPts = new ArrayList<>(pts.subList(cnt, pts.size()));
+
+ // -- act/assert
+ // insert the test points
+ final PointMapChecker<P, Integer> allChecker = checkerFor(map);
+ final PointMapChecker<P, Integer> oddChecker = checkerFor(map);
+
+ final List<P> evenKeys = new ArrayList<>();
+ final List<P> oddKeys = new ArrayList<>();
+
+ for (int i = 0; i < cnt; ++i) {
+ final P key = testPts.get(i);
+
+ Assertions.assertNull(map.put(key, i));
+
+ allChecker.expectEntry(key, i);
+
+ if (i % 2 == 0) {
+ evenKeys.add(key);
+ } else {
+ oddKeys.add(key);
+ oddChecker.expectEntry(key, i);
+ }
+ }
+
+ // check map state after insertion of all test points
+ allChecker
+ .doesNotContainKeys(otherPts)
+ .check();
+
+ // remove points inserted on even indices; remove the keys in
+ // a different order than insertion
+ Collections.shuffle(evenKeys);
+ for (final P key : evenKeys) {
+ Assertions.assertNotNull(map.remove(key));
+ }
+
+ // check map state after partial removal
+ oddChecker
+ .doesNotContainKeys(otherPts)
+ .doesNotContainKeys(evenKeys)
+ .check();
+
+ // remove remaining points
+ Collections.shuffle(oddKeys);
+ for (final P key : oddKeys) {
+ Assertions.assertNotNull(map.remove(key));
+ }
+
+ // ensure that nothing is left
+ checkerFor(map)
+ .doesNotContainKeys(pts)
+ .check();
+ }
+
+ @Test
+ void testClear_empty() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ // act
+ map.clear();
+
+ // assert
+ assertEmpty(map);
+ }
+
+ @Test
+ void testClear_populated() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ insertPoints(getTestPoints(1_000, EPS, new Random(6L)), map);
+
+ // act
+ map.clear();
+
+ // assert
+ assertEmpty(map);
+ }
+
+ @Test
+ void testRepeatedUse() {
+ // -- arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final Random rnd = new Random(3L);
+
+ final int cnt = 10_000;
+ final List<P> pts = getTestPoints(cnt, EPS, rnd);
+
+ // -- act
+ final int iterations = 10;
+ final int subListSize = cnt / iterations;
+ for (int i = 0; i < iterations; ++i) {
+ final int subListStart = i * subListSize;
+ final List<P> subList = pts.subList(subListStart, subListStart + subListSize);
+
+ // add sublist
+ insertPoints(subList, map);
+
+ // remove sublist in different order
+ final List<P> shuffledSubList = new ArrayList<>(subList);
+ Collections.shuffle(shuffledSubList, rnd);
+
+ removePoints(shuffledSubList, map);
+
+ // add sublist again
+ insertPoints(subList, map);
+ }
+
+ // -- assert
+ PointMapChecker<P, Integer> checker = checkerFor(map);
+
+ for (int i = 0; i < iterations * subListSize; ++i) {
+ checker.expectEntry(pts.get(i), i % subListSize);
+ }
+
+ checker.check();
+ }
+
+ @Test
+ void testHashCode() {
+ // arrange
+ final PointMap<P, Integer> a = getMap(PRECISION);
+ final PointMap<P, Integer> b = getMap(PRECISION);
+ final PointMap<P, Integer> c = getMap(PRECISION);
+ final PointMap<P, Integer> d = getMap(PRECISION);
+ final PointMap<P, Integer> e = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ insertPoints(pts, a);
+ insertPoints(pts.subList(0, 2), b);
+
+ insertPoints(pts, d);
+ d.put(pts.get(0), -1);
+
+ insertPoints(pts, e);
+
+ // act
+ final int hash = a.hashCode();
+
+ // act/assert
+ Assertions.assertEquals(hash, a.hashCode());
+
+ Assertions.assertNotEquals(hash, b.hashCode());
+ Assertions.assertNotEquals(hash, c.hashCode());
+ Assertions.assertNotEquals(hash, d.hashCode());
+
+ Assertions.assertEquals(hash, e.hashCode());
+ }
+
+ @Test
+ void testEquals() {
+ // arrange
+ final PointMap<P, Integer> a = getMap(PRECISION);
+ final PointMap<P, Integer> b = getMap(PRECISION);
+ final PointMap<P, Integer> c = getMap(PRECISION);
+ final PointMap<P, Integer> d = getMap(PRECISION);
+ final PointMap<P, Integer> e = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ insertPoints(pts, a);
+ insertPoints(pts.subList(0, 2), b);
+
+ insertPoints(pts, d);
+ d.put(pts.get(0), -1);
+
+ insertPoints(pts, e);
+
+ // act/assert
+ Assertions.assertFalse(a.equals(null));
+ Assertions.assertFalse(a.equals(new Object()));
+
+ Assertions.assertTrue(a.equals(a));
+
+ Assertions.assertFalse(a.equals(b));
+ Assertions.assertFalse(a.equals(c));
+ Assertions.assertFalse(a.equals(d));
+
+ Assertions.assertTrue(a.equals(e));
+ }
+
+ @Test
+ void testToString() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ final List<P> pts = getTestPoints(1, EPS);
+ insertPoints(pts, map);
+
+ // act
+ final String str = map.toString();
+
+ // assert
+ GeometryTestUtils.assertContains(pts.get(0).toString(), str);
+ }
+
+ // EntrySet -----------------------------------
+
+ @Test
+ void testEntrySet_add_unsupported() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final P pt = getTestPoints(1, EPS).get(0);
+ final Map.Entry<P, Integer> entry = new SimpleEntry<>(pt, 100);
+
+ // act/assert
+ assertCollectionAddUnsupported(map.entrySet(), entry);
+ }
+
+ @Test
+ void testEntrySet_setValue() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(100, EPS);
+ insertPoints(pts, map);
+
+ final int offset = 100;
+
+ // act
+ for (final Map.Entry<P, Integer> entry : map.entrySet()) {
+ final Integer val = entry.getValue();
+ final Integer newVal = val + offset;
+
+ Assertions.assertEquals(val, entry.setValue(newVal));
+ Assertions.assertEquals(newVal, entry.getValue());
+ }
+
+ // assert
+ final PointMapChecker<P, Integer> checker = checkerFor(map);
+ int i = offset;
+ for (final P pt : pts) {
+ checker.expectEntry(pt, i++);
+ }
+
+ checker.check();
+ }
+
+ @Test
+ void testEntrySet_clear() {
+ // act/assert
+ assertCollectionClear(PointMap::entrySet);
+ }
+
+ @Test
+ void testEntrySet_contains() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ insertPoints(pts.subList(0, 1), map);
+
+ final Set<Map.Entry<P, Integer>> entrySet = map.entrySet();
+
+ // act/assert
+ Assertions.assertFalse(entrySet.contains(null));
+ Assertions.assertFalse(entrySet.contains(new Object()));
+
+ Assertions.assertTrue(entrySet.contains(new SimpleEntry<>(pts.get(0), 0)));
+
+ Assertions.assertFalse(entrySet.contains(new SimpleEntry<>(pts.get(0), 1)));
+ Assertions.assertFalse(entrySet.contains(new SimpleEntry<>(pts.get(1), 0)));
+ }
+
+ @Test
+ void testEntrySet_containsAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 2), map);
+
+ final Set<Map.Entry<P, Integer>> entrySet = map.entrySet();
+
+ final Map.Entry<P, Integer> a = new SimpleEntry<>(pts.get(0), 0);
+ final Map.Entry<P, Integer> b = new SimpleEntry<>(pts.get(1), 1);
+
+ final Map.Entry<P, Integer> c = new SimpleEntry<>(pts.get(2), 2);
+ final Map.Entry<P, Integer> d = new SimpleEntry<>(pts.get(3), 3);
+
+ // act/assert
+ Assertions.assertFalse(entrySet.containsAll(Arrays.asList(new Object(), new Object())));
+
+ Assertions.assertTrue(entrySet.containsAll(new ArrayList<>()));
+
+ Assertions.assertTrue(entrySet.containsAll(Arrays.asList(a)));
+ Assertions.assertTrue(entrySet.containsAll(Arrays.asList(b, a)));
+
+ Assertions.assertFalse(entrySet.containsAll(Arrays.asList(a, b, c)));
+ Assertions.assertFalse(entrySet.containsAll(Arrays.asList(c, d)));
+ }
+
+ @Test
+ void testEntrySet_remove() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ insertPoints(pts, map);
+
+ final Set<Map.Entry<P, Integer>> entrySet = map.entrySet();
+
+ final Map.Entry<P, Integer> a = new SimpleEntry<>(pts.get(0), 0);
+ final Map.Entry<P, Integer> b = new SimpleEntry<>(pts.get(1), 1);
+ final Map.Entry<P, Integer> c = new SimpleEntry<>(pts.get(2), 2);
+
+ // act/assert
+ Assertions.assertFalse(entrySet.remove(null));
+ Assertions.assertFalse(entrySet.remove(new Object()));
+
+ Assertions.assertFalse(entrySet.remove(new SimpleEntry<>(pts.get(0), 1)));
+ Assertions.assertFalse(entrySet.remove(new SimpleEntry<>(pts.get(1), 0)));
+
+ checkerFor(map)
+ .expectEntry(a)
+ .expectEntry(b)
+ .expectEntry(c)
+ .check();
+
+ Assertions.assertTrue(entrySet.remove(a));
+
+ checkerFor(map)
+ .expectEntry(b)
+ .expectEntry(c)
+ .check();
+
+ Assertions.assertTrue(entrySet.remove(b));
+
+ checkerFor(map)
+ .expectEntry(c)
+ .check();
+
+ Assertions.assertTrue(entrySet.remove(c));
+
+ Assertions.assertEquals(0, entrySet.size());
+ assertEmpty(map);
+
+ Assertions.assertFalse(entrySet.remove(a));
+ Assertions.assertFalse(entrySet.remove(b));
+ Assertions.assertFalse(entrySet.remove(c));
+ }
+
+ @Test
+ void testEntrySet_removeAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Set<Map.Entry<P, Integer>> entrySet = map.entrySet();
+
+ final Map.Entry<P, Integer> a = new SimpleEntry<>(pts.get(0), 0);
+ final Map.Entry<P, Integer> b = new SimpleEntry<>(pts.get(1), 1);
+ final Map.Entry<P, Integer> c = new SimpleEntry<>(pts.get(2), 2);
+ final Map.Entry<P, Integer> d = new SimpleEntry<>(pts.get(3), 3);
+
+ // act/assert
+ Assertions.assertFalse(entrySet.removeAll(Arrays.asList(new Object(), new Object())));
+
+ Assertions.assertFalse(entrySet.removeAll(new ArrayList<>()));
+ Assertions.assertFalse(entrySet.removeAll(Arrays.asList(d)));
+
+ checkerFor(map)
+ .expectEntry(a)
+ .expectEntry(b)
+ .expectEntry(c)
+ .check();
+
+ Assertions.assertTrue(entrySet.removeAll(Arrays.asList(a, b)));
+
+ checkerFor(map)
+ .expectEntry(c)
+ .check();
+
+ Assertions.assertTrue(entrySet.removeAll(Arrays.asList(c, d)));
+
+ Assertions.assertEquals(0, entrySet.size());
+ assertEmpty(map);
+
+ Assertions.assertFalse(entrySet.removeAll(Arrays.asList(a, b)));
+ Assertions.assertFalse(entrySet.removeAll(Arrays.asList(c, d)));
+ }
+
+ @Test
+ void testEntrySet_retainAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Set<Map.Entry<P, Integer>> entrySet = map.entrySet();
+
+ final Map.Entry<P, Integer> a = new SimpleEntry<>(pts.get(0), 0);
+ final Map.Entry<P, Integer> b = new SimpleEntry<>(pts.get(1), 1);
+ final Map.Entry<P, Integer> c = new SimpleEntry<>(pts.get(2), 2);
+ final Map.Entry<P, Integer> d = new SimpleEntry<>(pts.get(3), 3);
+
+ // act/assert
+ Assertions.assertFalse(entrySet.retainAll(Arrays.asList(a, b, c)));
+
+ checkerFor(map)
+ .expectEntry(a)
+ .expectEntry(b)
+ .expectEntry(c)
+ .check();
+
+ Assertions.assertTrue(entrySet.retainAll(Arrays.asList(a, b, d)));
+
+ checkerFor(map)
+ .expectEntry(a)
+ .expectEntry(b)
+ .check();
+
+ Assertions.assertTrue(entrySet.retainAll(Arrays.asList(new Object(), new Object())));
+
+ Assertions.assertEquals(0, entrySet.size());
+ assertEmpty(map);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void testEntrySet_toArray() {
+ // act/assert
+ assertCollectionToArray(PointMap::entrySet, new Map.Entry[0]);
+ }
+
+ @Test
+ void testEntrySet_equalsAndHashCode() {
+ // act/assert
+ assertCollectionEquals(PointMap::entrySet);
+ assertCollectionHashCode(PointMap::entrySet);
+ }
+
+ @Test
+ void testEntrySet_toString() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ final List<P> pts = getTestPoints(20, EPS);
+ insertPoints(pts, map);
+
+ // act
+ final String str = map.entrySet().toString();
+
+ // assert
+ GeometryTestUtils.assertContains(pts.get(17).toString(), str);
+ GeometryTestUtils.assertContains(Integer.toString(17), str);
+ }
+
+ @Test
+ void testEntrySetIterator() {
+ // act/assert
+ assertCollectionIterator(PointMap::entrySet);
+ assertCollectionIteratorRemove(PointMap::entrySet);
+ assertCollectionIteratorRemoveWithMultiplePasses(PointMap::entrySet);
+ assertCollectionIteratorRemoveWithoutNext(PointMap::entrySet);
+ assertCollectionIteratorRemoveMultipleCalls(PointMap::entrySet);
+ assertCollectionIteratorConcurrentModification(PointMap::entrySet);
+ }
+
+ // KeySet -----------------------------------
+
+ @Test
+ void testKeySet_add_unsupported() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final P pt = getTestPoints(1, EPS).get(0);
+
+ // act/assert
+ assertCollectionAddUnsupported(map.keySet(), pt);
+ }
+
+ @Test
+ void testKeySet_clear() {
+ // act/assert
+ assertCollectionClear(PointMap::keySet);
+ }
+
+ @Test
+ void testKeySet_contains() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ insertPoints(pts.subList(0, 1), map);
+
+ final Set<P> keySet = map.keySet();
+
+ // act/assert
+ Assertions.assertTrue(keySet.contains(pts.get(0)));
+ Assertions.assertFalse(keySet.contains(pts.get(1)));
+ }
+
+ @Test
+ void testKeySet_containsAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 2), map);
+
+ final Set<P> keySet = map.keySet();
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ final P c = pts.get(2);
+ final P d = pts.get(3);
+
+ // act/assert
+ Assertions.assertTrue(keySet.containsAll(new ArrayList<>()));
+
+ Assertions.assertTrue(keySet.containsAll(Arrays.asList(a)));
+ Assertions.assertTrue(keySet.containsAll(Arrays.asList(b, a)));
+
+ Assertions.assertFalse(keySet.containsAll(Arrays.asList(a, b, c)));
+ Assertions.assertFalse(keySet.containsAll(Arrays.asList(c, d)));
+ }
+
+ @Test
+ void testKeySet_remove() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Set<P> keySet = map.keySet();
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+ final P c = pts.get(2);
+ final P d = pts.get(3);
+
+ // act/assert
+ Assertions.assertTrue(keySet.remove(a));
+ Assertions.assertTrue(keySet.remove(b));
+ Assertions.assertFalse(keySet.remove(d));
+
+ checkerFor(map)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(keySet.remove(c));
+
+ Assertions.assertEquals(0, keySet.size());
+ assertEmpty(map);
+
+ Assertions.assertFalse(keySet.remove(a));
+ Assertions.assertFalse(keySet.remove(b));
+ Assertions.assertFalse(keySet.remove(c));
+ Assertions.assertFalse(keySet.remove(d));
+ }
+
+ @Test
+ void testKeySet_removeAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Set<P> keySet = map.keySet();
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ final P c = pts.get(2);
+ final P d = pts.get(3);
+
+ // act/assert
+ Assertions.assertFalse(keySet.removeAll(new ArrayList<>()));
+ Assertions.assertFalse(keySet.removeAll(Arrays.asList(d)));
+
+ checkerFor(map)
+ .expectEntry(a, 0)
+ .expectEntry(b, 1)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(keySet.removeAll(Arrays.asList(a, b)));
+
+ checkerFor(map)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(keySet.removeAll(Arrays.asList(c, d)));
+
+ Assertions.assertEquals(0, keySet.size());
+ assertEmpty(map);
+
+ Assertions.assertFalse(keySet.removeAll(Arrays.asList(a, b)));
+ Assertions.assertFalse(keySet.removeAll(Arrays.asList(c, d)));
+ }
+
+ @Test
+ void testKeySet_retainAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Set<P> keySet = map.keySet();
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ final P c = pts.get(2);
+ final P d = pts.get(3);
+
+ // act/assert
+ Assertions.assertFalse(keySet.retainAll(Arrays.asList(a, b, c)));
+
+ checkerFor(map)
+ .expectEntry(a, 0)
+ .expectEntry(b, 1)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(keySet.retainAll(Arrays.asList(a, b, d)));
+
+ checkerFor(map)
+ .expectEntry(a, 0)
+ .expectEntry(b, 1)
+ .check();
+
+ Assertions.assertTrue(keySet.retainAll(Arrays.asList(new Object(), new Object())));
+
+ Assertions.assertEquals(0, keySet.size());
+ assertEmpty(map);
+ }
+
+ @Test
+ void testKeySet_toArray() {
+ // act/assert
+ assertCollectionToArray(PointMap::keySet, createPointArray());
+ }
+
+ @Test
+ void testKeySet_equalsAndHashCode() {
+ // act/assert
+ assertCollectionEquals(PointMap::keySet);
+ assertCollectionHashCode(PointMap::keySet);
+ }
+
+ @Test
+ void testKeySet_toString() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ final List<P> pts = getTestPoints(1, EPS);
+ insertPoints(pts, map);
+
+ // act
+ final String str = map.keySet().toString();
+
+ // assert
+ GeometryTestUtils.assertContains(pts.get(0).toString(), str);
+ }
+
+ @Test
+ void testKeySetIterator() {
+ // act/assert
+ assertCollectionIterator(PointMap::keySet);
+ assertCollectionIteratorRemove(PointMap::keySet);
+ assertCollectionIteratorRemoveWithMultiplePasses(PointMap::keySet);
+ assertCollectionIteratorRemoveWithoutNext(PointMap::keySet);
+ assertCollectionIteratorRemoveMultipleCalls(PointMap::keySet);
+ assertCollectionIteratorConcurrentModification(PointMap::keySet);
+ }
+
+ // Values -----------------------------------
+
+ @Test
+ void testValues_add_unsupported() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ // act/assert
+ assertCollectionAddUnsupported(map.values(), 2);
+ }
+
+ @Test
+ void testValues_clear() {
+ // act/assert
+ assertCollectionClear(PointMap::values);
+ }
+
+ @Test
+ void testValues_contains() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ insertPoints(pts.subList(0, 1), map);
+
+ final Collection<Integer> values = map.values();
+
+ // act/assert
+ Assertions.assertTrue(values.contains(0));
+ Assertions.assertFalse(values.contains(1));
+ }
+
+ @Test
+ void testValues_containsAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 2), map);
+
+ final Collection<Integer> values = map.values();
+
+ // act/assert
+ Assertions.assertFalse(values.containsAll(Arrays.asList(new Object())));
+
+ Assertions.assertTrue(values.containsAll(new ArrayList<>()));
+
+ Assertions.assertTrue(values.containsAll(Arrays.asList(0)));
+ Assertions.assertTrue(values.containsAll(Arrays.asList(1, 0)));
+
+ Assertions.assertFalse(values.containsAll(Arrays.asList(0, 1, 2)));
+ Assertions.assertFalse(values.containsAll(Arrays.asList(2, 3)));
+ }
+
+ @Test
+ void testValues_remove() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(5, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ map.put(pts.get(4), 0);
+
+ final Collection<Integer> values = map.values();
+
+ // act/assert
+ Assertions.assertTrue(values.remove(0));
+ Assertions.assertTrue(values.remove(0));
+ Assertions.assertTrue(values.remove(1));
+ Assertions.assertFalse(values.remove(3));
+
+ checkerFor(map)
+ .expectEntry(pts.get(2), 2)
+ .check();
+
+ Assertions.assertTrue(values.remove(2));
+
+ Assertions.assertEquals(0, values.size());
+ assertEmpty(map);
+
+ Assertions.assertFalse(values.remove(0));
+ Assertions.assertFalse(values.remove(1));
+ Assertions.assertFalse(values.remove(2));
+ Assertions.assertFalse(values.remove(3));
+ }
+
+ @Test
+ void testValues_removeAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Collection<Integer> values = map.values();
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ final P c = pts.get(2);
+
+ // act/assert
+ Assertions.assertFalse(values.removeAll(new ArrayList<>()));
+ Assertions.assertFalse(values.removeAll(Arrays.asList(4)));
+
+ checkerFor(map)
+ .expectEntry(a, 0)
+ .expectEntry(b, 1)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(values.removeAll(Arrays.asList(0, 1)));
+
+ checkerFor(map)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(values.removeAll(Arrays.asList(2, 3)));
+
+ Assertions.assertEquals(0, values.size());
+ assertEmpty(map);
+
+ Assertions.assertFalse(values.removeAll(Arrays.asList(0, 1)));
+ Assertions.assertFalse(values.removeAll(Arrays.asList(2, 3)));
+ }
+
+ @Test
+ void testValues_retainAll() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ insertPoints(pts.subList(0, 3), map);
+
+ final Collection<Integer> values = map.values();
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ final P c = pts.get(2);
+
+ // act/assert
+ Assertions.assertFalse(values.retainAll(Arrays.asList(0, 1, 2)));
+
+ checkerFor(map)
+ .expectEntry(a, 0)
+ .expectEntry(b, 1)
+ .expectEntry(c, 2)
+ .check();
+
+ Assertions.assertTrue(values.retainAll(Arrays.asList(0, 1, 3)));
+
+ checkerFor(map)
+ .expectEntry(a, 0)
+ .expectEntry(b, 1)
+ .check();
+
+ Assertions.assertTrue(values.retainAll(Arrays.asList(new Object(), new Object())));
+
+ Assertions.assertEquals(0, values.size());
+ assertEmpty(map);
+ }
+
+ @Test
+ void testValues_toArray() {
+ // act/assert
+ assertCollectionToArray(PointMap::values, new Integer[0]);
+ }
+
+ @Test
+ void testValues_toString() {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ final List<P> pts = getTestPoints(20, EPS);
+ insertPoints(pts, map);
+
+ // act
+ final String str = map.values().toString();
+
+ // assert
+ GeometryTestUtils.assertContains(Integer.toString(17), str);
+ }
+
+ @Test
+ void testValuesIterator() {
+ // act/assert
+ assertCollectionIterator(PointMap::values);
+ assertCollectionIteratorRemove(PointMap::values);
+ assertCollectionIteratorRemoveWithMultiplePasses(PointMap::values);
+ assertCollectionIteratorRemoveWithoutNext(PointMap::values);
+ assertCollectionIteratorRemoveMultipleCalls(PointMap::values);
+ assertCollectionIteratorConcurrentModification(PointMap::values);
+ }
+
+ // Helpers -----------------------------------
+
+ private <V> void assertCollectionAddUnsupported(final Collection<V> coll, final V value) {
+ Assertions.assertThrows(UnsupportedOperationException.class, () -> coll.add(value));
+
+ final List<V> valueList = Arrays.asList(value);
+ Assertions.assertThrows(UnsupportedOperationException.class, () -> coll.addAll(valueList));
+ }
+
+ private void assertCollectionClear(final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ insertPoints(pts, map);
+
+ final Collection<?> coll = collectionFactory.apply(map);
+
+ // act
+ coll.clear();
+
+ // assert
+ Assertions.assertEquals(0, coll.size());
+ Assertions.assertTrue(coll.isEmpty());
+
+ assertEmpty(map);
+ }
+
+ private <T> void assertCollectionToArray(final Function<PointMap<P, Integer>, Collection<T>> collectionFactory,
+ final T[] typedArray) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ insertPoints(pts, map);
+
+ final Collection<T> coll = collectionFactory.apply(map);
+
+ // act
+ final Object[] objArr = coll.toArray();
+ final T[] tArr = coll.toArray(typedArray);
+
+ // assert
+ Assertions.assertEquals(map.size(), objArr.length);
+ Assertions.assertEquals(map.size(), tArr.length);
+
+ int i = 0;
+ for (final T element : coll) {
+ Assertions.assertEquals(element, objArr[i]);
+ Assertions.assertEquals(element, tArr[i]);
+
+ ++i;
+ }
+ }
+
+ private void assertCollectionEquals(final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> mapA = getMap(PRECISION);
+ final PointMap<P, Integer> mapB = getMap(PRECISION);
+ final PointMap<P, Integer> mapC = getMap(PRECISION);
+ final PointMap<P, Integer> mapD = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ insertPoints(pts, mapA);
+ insertPoints(pts.subList(0, 2), mapB);
+
+ insertPoints(pts, mapD);
+
+ final Collection<?> a = collectionFactory.apply(mapA);
+ final Collection<?> b = collectionFactory.apply(mapB);
+ final Collection<?> c = collectionFactory.apply(mapC);
+ final Collection<?> d = collectionFactory.apply(mapD);
+
+ // act/assert
+ Assertions.assertFalse(a.equals(null));
+ Assertions.assertFalse(a.equals(new Object()));
+
+ Assertions.assertTrue(a.equals(a));
+
+ Assertions.assertFalse(a.equals(b));
+ Assertions.assertFalse(a.equals(c));
+
+ Assertions.assertTrue(a.equals(d));
+ }
+
+ private void assertCollectionHashCode(final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> mapA = getMap(PRECISION);
+ final PointMap<P, Integer> mapB = getMap(PRECISION);
+ final PointMap<P, Integer> mapC = getMap(PRECISION);
+ final PointMap<P, Integer> mapD = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ insertPoints(pts, mapA);
+ insertPoints(pts.subList(0, 2), mapB);
+
+ insertPoints(pts, mapD);
+
+ final Collection<?> a = collectionFactory.apply(mapA);
+ final Collection<?> b = collectionFactory.apply(mapB);
+ final Collection<?> c = collectionFactory.apply(mapC);
+ final Collection<?> d = collectionFactory.apply(mapD);
+
+ // act
+ final int hash = a.hashCode();
+
+ // assert
+ Assertions.assertEquals(hash, a.hashCode());
+
+ Assertions.assertNotEquals(hash, b.hashCode());
+ Assertions.assertNotEquals(hash, c.hashCode());
+
+ Assertions.assertEquals(hash, d.hashCode());
+ }
+
+ private void assertCollectionIterator(final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ insertPoints(getTestPoints(1, EPS), map);
+
+ // act/assert
+ final Collection<?> coll = collectionFactory.apply(map);
+
+ final Iterator<?> it = coll.iterator();
+
+ Assertions.assertTrue(it.hasNext());
+ Assertions.assertNotNull(it.next());
+
+ Assertions.assertFalse(it.hasNext());
+ Assertions.assertThrows(NoSuchElementException.class, () -> it.next());
+ }
+
+ private void assertCollectionIteratorRemove(final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(1_000, EPS, new Random(10L));
+
+ insertPoints(pts, map);
+
+ // act
+ final Collection<?> coll = collectionFactory.apply(map);
+
+ int removed = 0;
+
+ final Iterator<?> it = coll.iterator();
+ while (it.hasNext()) {
+ Assertions.assertNotNull(it.next());
+ it.remove();
+
+ ++removed;
+ }
+
+ // assert
+ Assertions.assertEquals(pts.size(), removed);
+
+ Assertions.assertEquals(0, coll.size());
+ Assertions.assertTrue(coll.isEmpty());
+
+ assertEmpty(map);
+ }
+
+ private void assertCollectionIteratorRemoveWithMultiplePasses(
+ final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final List<P> pts = getTestPoints(1_000, EPS, new Random(10L));
+
+ insertPoints(pts, map);
+
+ // act
+ // remove the entries in two passes: one to remove the entries with even
+ // values and the second to remove the remaining
+ final Collection<?> coll = collectionFactory.apply(map);
+
+ final Iterator<?> firstPass = coll.iterator();
+ int removed = 0;
+ int i = 0;
+ while (firstPass.hasNext()) {
+ Assertions.assertNotNull(firstPass.next());
+ if ((++i) % 2 == 0) {
+ ++removed;
+ firstPass.remove();
+ }
+ }
+
+ final Iterator<?> secondPass = coll.iterator();
+ while (secondPass.hasNext()) {
+ Assertions.assertNotNull(secondPass.next());
+ secondPass.remove();
+ ++removed;
+ }
+
+ // assert
+ Assertions.assertEquals(pts.size(), removed);
+
+ Assertions.assertEquals(0, coll.size());
+ Assertions.assertTrue(coll.isEmpty());
+
+ assertEmpty(map);
+ }
+
+ private void assertCollectionIteratorRemoveWithoutNext(
+ final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+
+ final Collection<?> coll = collectionFactory.apply(map);
+
+ // act/assert
+ final Iterator<?> it = coll.iterator();
+
+ Assertions.assertThrows(IllegalStateException.class, () -> it.remove());
+ }
+
+ private void assertCollectionIteratorRemoveMultipleCalls(
+ final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ insertPoints(getTestPoints(1, EPS), map);
+
+ final Collection<?> coll = collectionFactory.apply(map);
+ final Iterator<?> it = coll.iterator();
+
+ // act/assert
+ Assertions.assertNotNull(it.next());
+ it.remove();
+
+ Assertions.assertThrows(IllegalStateException.class, () -> it.remove());
+
+ Assertions.assertEquals(0, map.size());
+ }
+
+ private void assertCollectionIteratorConcurrentModification(
+ final Function<PointMap<P, ?>, Collection<?>> collectionFactory) {
+ // arrange
+ final PointMap<P, Integer> map = getMap(PRECISION);
+ final List<P> pts = getTestPoints(3, EPS);
+
+ insertPoints(pts.subList(0, 2), map);
+
+ final Collection<?> coll = collectionFactory.apply(map);
+ final Iterator<?> it = coll.iterator();
+
+ // act
+ it.next();
+ map.put(pts.get(2), 3);
+
+ // assert
+ Assertions.assertTrue(it.hasNext());
+ Assertions.assertThrows(ConcurrentModificationException.class, () -> it.next());
+ }
+
+ /** Insert the given list of points into {@code map}. The value of each key is the index of the key
+ * in {@code pts}.
+ * @param <P> Point type
+ * @param pts list of points
+ * @param map map to insert into
+ */
+ public static <P extends Point<P>> void insertPoints(final List<P> pts, final PointMap<P, Integer> map) {
+ int i = -1;
+ for (final P pt : pts) {
+ map.put(pt, ++i);
+ }
+ }
+
+ /** Remove each point in {@code pts} from {@code map}.
+ * @param <P> Point type
+ * @param pts points to remove
+ * @param map map to remove from
+ */
+ public static <P extends Point<P>> void removePoints(final List<P> pts, final PointMap<P, Integer> map) {
+ for (final P pt : pts) {
+ map.remove(pt);
+ }
+ }
+
+ /** Return a new {@link PointMapChecker} for asserting the contents
+ * of the given map.
+ * @return a new checker instance
+ */
+ public static <P extends Point<P>, V> PointMapChecker<P, V> checkerFor(final PointMap<P, V> map) {
+ return new PointMapChecker<>(map);
+ }
+
+ /** Assert that the given map is empty.
+ * @param map map to assert empty
+ */
+ public static void assertEmpty(final PointMap<?, ?> map) {
+ checkerFor(map)
+ .check();
+ }
+
+ /** Class designed to assist with performing assertions on the state
+ * of a point map.
+ */
+ public static class PointMapChecker<P extends Point<P>, V> {
+
+ private final PointMap<P, V> map;
+
+ private final Map<P, V> expectedMap = new HashMap<>();
+
+ private final List<P> unexpectedKeys = new ArrayList<>();
+
+ public PointMapChecker(final PointMap<P, V> map) {
+ this.map = map;
+ }
+
+ public PointMapChecker<P, V> expectEntry(final P key, final V value) {
+ expectedMap.put(key, value);
+
+ return this;
+ }
+
+ public PointMapChecker<P, V> expectEntry(final Map.Entry<P, V> entry) {
+ return expectEntry(entry.getKey(), entry.getValue());
+ }
+
+ public PointMapChecker<P, V> doesNotContainKey(final P key) {
+ unexpectedKeys.add(key);
+
+ return this;
+ }
+
+ public PointMapChecker<P, V> doesNotContainKeys(final Iterable<? extends P> keys) {
+ for (final P key : keys) {
+ doesNotContainKey(key);
+ }
+
+ return this;
+ }
+
+ public void check() {
+ checkSize();
+
+ checkEntries();
+
+ checkEntrySet();
+ checkKeySet();
+ checkValues();
+
+ checkUnexpectedKeys();
+ }
+
+ private void checkSize() {
+ Assertions.assertEquals(expectedMap.size(), map.size(), "Unexpected map size");
+ Assertions.assertEquals(expectedMap.isEmpty(), map.isEmpty(), "Unexpected isEmpty() result");
+ }
+
+ private void checkEntries() {
+ for (final Map.Entry<P, V> expectedEntry : expectedMap.entrySet()) {
+ final P expectedKey = expectedEntry.getKey();
+ final V expectedValue = expectedEntry.getValue();
+
+ Assertions.assertEquals(new SimpleEntry<>(expectedKey, expectedValue), map.getEntry(expectedKey),
+ () -> "Failed to get entry for key " + expectedKey);
+ Assertions.assertEquals(expectedValue, map.get(expectedKey),
+ () -> "Unexpected value for key " + expectedKey);
+
+ Assertions.assertTrue(map.containsKey(expectedKey),
+ () -> "Expected map to contain key " + expectedKey);
+ Assertions.assertTrue(map.containsValue(expectedValue),
+ () -> "Expected map to contain value " + expectedValue);
+ }
+ }
+
+ private void checkKeySet() {
+ Set<P> expectedKeySet = expectedMap.keySet();
+
+ Set<P> keySet = map.keySet();
+ Assertions.assertEquals(expectedKeySet.size(), keySet.size(), "Unexpected key set size");
+ Assertions.assertEquals(expectedKeySet.isEmpty(), keySet.isEmpty(),
+ "Unexpected key set \"isEmpty\" value");
+
+ for (final P key : keySet) {
+ Assertions.assertTrue(expectedKeySet.contains(key),
+ () -> "Unexpected key in key set: " + key);
+ }
+
+ for (final P expectedKey : expectedKeySet) {
+ Assertions.assertTrue(keySet.contains(expectedKey),
+ () -> "Key set is missing expected key: " + expectedKey);
+ }
+ }
+
+ private void checkEntrySet() {
+ final Set<Map.Entry<P, V>> entrySet = map.entrySet();
+ Assertions.assertEquals(expectedMap.size(), entrySet.size(), "Unexpected entry set size");
+ Assertions.assertEquals(expectedMap.isEmpty(), entrySet.isEmpty(),
+ "Unexpected entry set \"isEmpty\" value");
+
+ final Map<P, V> remainingEntryMap = new HashMap<>(expectedMap);
+ for (final Map.Entry<P, V> actualEntry : entrySet) {
+ Assertions.assertTrue(remainingEntryMap.containsKey(actualEntry.getKey()),
+ "Unexpected key in entry set: " + actualEntry.getKey());
+
+ final V expectedValue = remainingEntryMap.remove(actualEntry.getKey());
+ Assertions.assertEquals(expectedValue, actualEntry.getValue(),
+ () -> "Unexpected value in entry set for key " + actualEntry.getKey());
+ }
+
+ Assertions.assertTrue(remainingEntryMap.isEmpty(),
+ () -> "Entry set is missing expected entries: " + remainingEntryMap);
+ }
+
+ private void checkValues() {
+ Collection<V> actualValues = map.values();
+
+ Assertions.assertEquals(expectedMap.size(), actualValues.size(),
+ "Unexpected values collection size");
+
+ // check that each value in the list occurs the value number of times
+ // as expect)ed
+ final Map<V, Integer> expectedCounts = new HashMap<>();
+ for (final Map.Entry<P, V> entry : expectedMap.entrySet()) {
+ expectedCounts.merge(entry.getValue(), 1, (a, b) -> a + b);
+ }
+
+ final Map<V, Integer> actualCounts = new HashMap<>();
+ for (final V value : actualValues) {
+ actualCounts.merge(value, 1, (a, b) -> a + b);
+ }
+
+ for (final Map.Entry<V, Integer> expected : expectedCounts.entrySet()) {
+ Assertions.assertEquals(expected.getValue(), actualCounts.get(expected.getKey()),
+ () -> "Unexpected count for value " + expected.getKey());
+ }
+ }
+
+ private void checkUnexpectedKeys() {
+ for (final P key : unexpectedKeys) {
+ Assertions.assertFalse(map.containsKey(key), () -> "Expected map to not contain key " + key);
+ Assertions.assertNull(map.get(key), () -> "Expected map to not contain value for key " + key);
+
+ Assertions.assertNull(map.getEntry(key), () -> "Expected map to not contain key " + key);
+
+ Assertions.assertFalse(map.keySet().contains(key),
+ () -> "Expected map key set to not contain " + key);
+
+ final boolean inEntrySet = map.entrySet().stream()
+ .anyMatch(e -> e.getKey().equals(key));
+ Assertions.assertFalse(inEntrySet, () -> "Expected map entry set to not contain key " + key);
+ }
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointSetTestBase.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointSetTestBase.java
new file mode 100644
index 0000000..54102fb
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/collection/PointSetTestBase.java
@@ -0,0 +1,801 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.collection;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.Set;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Abstract base class for {@link PointSet} implementations.
+ * @param <P> Point type
+ */
+public abstract class PointSetTestBase<P extends Point<P>>
+ extends PointCollectionTestBase<P> {
+
+ /** Get a new point set instance for testing.
+ * @param <V> Value type
+ * @param precision precision context to determine floating point equality
+ * @return a new set instance for testing.
+ */
+ protected abstract PointSet<P> getSet(Precision.DoubleEquivalence precision);
+
+ @Test
+ void testEmpty() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final P pt = getTestPoints(1, EPS).get(0);
+
+ // act/assert
+ Assertions.assertEquals(0, set.size());
+ Assertions.assertTrue(set.isEmpty());
+
+ Assertions.assertFalse(set.contains(pt));
+ Assertions.assertNull(set.get(pt));
+ }
+
+ @Test
+ void testSingleEntry() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ final P a = pts.get(0);
+
+ // act
+ set.add(a);
+
+ // assert
+ checkerFor(set)
+ .expect(a)
+ .doesNotContain(pts.subList(1, pts.size()))
+ .check();
+ }
+
+ @Test
+ void testMultipleEntries() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final PointSetChecker<P> checker = checkerFor(set);
+
+ final int putCnt = 1000;
+ final List<P> pts = getTestPoints(putCnt * 2, EPS, new Random(1L));
+
+ // act
+ for (int i = 0; i < putCnt; ++i) {
+ final P pt = pts.get(i);
+
+ set.add(pt);
+
+ checker.expect(pt);
+ }
+
+ // assert
+ checker
+ .doesNotContain(pts.subList(putCnt, pts.size()))
+ .check();
+ }
+
+ @Test
+ void testAdd() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(500, EPS);
+
+ // act/assert
+ for (final P pt : pts) {
+ Assertions.assertTrue(set.add(pt));
+ Assertions.assertFalse(set.add(pt));
+
+ for (final P closePt : getTestPointsAtDistance(pt, 0.5 * EPS)) {
+ Assertions.assertFalse(set.add(closePt));
+ }
+ }
+
+ checkerFor(set)
+ .expect(pts)
+ .check();
+ }
+
+ @Test
+ void testContainsGet_equivalentPoints_singleEntry() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(1, EPS);
+ set.addAll(pts);
+
+ final P pt = pts.get(0);
+
+ // act/assert
+ Assertions.assertTrue(set.contains(pt));
+ Assertions.assertEquals(pt, set.get(pt));
+
+ for (final P closePt : getTestPointsAtDistance(pt, EPS * 0.75)) {
+ Assertions.assertTrue(set.contains(closePt));
+ Assertions.assertEquals(pt, set.get(closePt));
+
+ assertEq(closePt, pt, PRECISION);
+ }
+
+ for (final P farPt : getTestPointsAtDistance(pt, EPS * 1.25)) {
+ Assertions.assertFalse(set.contains(farPt));
+ Assertions.assertNull(set.get(farPt));
+
+ assertNotEq(farPt, pt, PRECISION);
+ }
+ }
+
+ @Test
+ void testContainsGet_equivalentPoints_multipleEntries() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(1_000, 3 * EPS);
+ set.addAll(pts);
+
+ // act/assert
+ for (final P pt : pts) {
+ Assertions.assertTrue(set.contains(pt));
+ Assertions.assertEquals(pt, set.get(pt));
+
+ for (final P closePt : getTestPointsAtDistance(pt, EPS * 0.75)) {
+ Assertions.assertTrue(set.contains(closePt));
+ Assertions.assertEquals(pt, set.get(closePt));
+
+ assertEq(closePt, pt, PRECISION);
+ }
+
+ for (final P farPt : getTestPointsAtDistance(pt, EPS * 1.25)) {
+ Assertions.assertFalse(set.contains(farPt));
+ Assertions.assertNull(set.get(farPt));
+
+ assertNotEq(farPt, pt, PRECISION);
+ }
+ }
+ }
+
+ @Test
+ void testContainsGet_invalidArgs() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ set.addAll(getTestPoints(3, EPS));
+
+ // act/assert
+ Assertions.assertThrows(NullPointerException.class, () -> set.get(null));
+ Assertions.assertThrows(ClassCastException.class, () -> set.contains(new Object()));
+ }
+
+ @Test
+ void testContainsGet_nanAndInf() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+ set.addAll(getTestPoints(100, EPS));
+
+ // act/assert
+ for (final P pt : getNaNPoints()) {
+ Assertions.assertFalse(set.contains(pt));
+ Assertions.assertNull(set.get(pt));
+ }
+
+ for (final P pt : getInfPoints()) {
+ Assertions.assertFalse(set.contains(pt));
+ Assertions.assertNull(set.get(pt));
+ }
+ }
+
+ @Test
+ void testContainsAll() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(1_000, 3 * EPS);
+ final List<P> addedPts = pts.subList(0, pts.size() / 2);
+
+ set.addAll(addedPts);
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+ final P c = pts.get(pts.size() - 1);
+ final P d = pts.get(pts.size() - 2);
+
+ // act/assert
+ Assertions.assertTrue(set.containsAll(Arrays.asList()));
+ Assertions.assertTrue(set.containsAll(Arrays.asList(a)));
+ Assertions.assertTrue(set.containsAll(Arrays.asList(b)));
+ Assertions.assertTrue(set.containsAll(addedPts));
+
+ Assertions.assertFalse(set.containsAll(Arrays.asList(c)));
+ Assertions.assertFalse(set.containsAll(Arrays.asList(c, d)));
+ Assertions.assertFalse(set.containsAll(Arrays.asList(a, b, c, d)));
+ }
+
+ @Test
+ void testContainsAll_empty() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(10, 3 * EPS);
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ // act/assert
+ Assertions.assertTrue(set.containsAll(Arrays.asList()));
+
+ Assertions.assertFalse(set.containsAll(Arrays.asList(a, b)));
+ Assertions.assertFalse(set.containsAll(pts));
+ }
+
+ @Test
+ void testRemove() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(4, EPS);
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+ final P c = pts.get(2);
+ final P d = pts.get(3);
+
+ set.add(a);
+ set.add(b);
+ set.add(c);
+
+ // act/assert
+ Assertions.assertFalse(set.remove(d));
+ Assertions.assertTrue(set.remove(a));
+ Assertions.assertTrue(set.remove(b));
+ Assertions.assertTrue(set.remove(c));
+
+ Assertions.assertFalse(set.remove(a));
+ Assertions.assertFalse(set.remove(b));
+ Assertions.assertFalse(set.remove(c));
+ Assertions.assertFalse(set.remove(d));
+
+ checkerFor(set)
+ .doesNotContain(pts)
+ .check();
+ }
+
+ @Test
+ void testRemove_largeEntryCount() {
+ // -- arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final Random rnd = new Random(2L);
+
+ final int cnt = 10_000;
+ final List<P> pts = getTestPoints(cnt * 2, EPS, rnd);
+
+ final List<P> testPts = new ArrayList<>(pts.subList(0, cnt));
+ final List<P> otherPts = new ArrayList<>(pts.subList(cnt, pts.size()));
+
+ // -- act/assert
+ // insert the test points
+ final PointSetChecker<P> allChecker = checkerFor(set);
+ final PointSetChecker<P> oddChecker = checkerFor(set);
+
+ final List<P> evenPts = new ArrayList<>();
+ final List<P> oddPts = new ArrayList<>();
+
+ for (int i = 0; i < cnt; ++i) {
+ final P pt = testPts.get(i);
+
+ Assertions.assertTrue(set.add(pt));
+
+ allChecker.expect(pt);
+
+ if (i % 2 == 0) {
+ evenPts.add(pt);
+ } else {
+ oddPts.add(pt);
+ oddChecker.expect(pt);
+ }
+ }
+
+ // check state after insertion of all test points
+ allChecker
+ .doesNotContain(otherPts)
+ .check();
+
+ // remove points inserted on even indices; remove the values in
+ // a different order than insertion
+ Collections.shuffle(evenPts);
+ for (final P pt : evenPts) {
+ Assertions.assertTrue(set.remove(pt));
+ }
+
+ // check state after partial removal
+ oddChecker
+ .doesNotContain(otherPts)
+ .doesNotContain(evenPts)
+ .check();
+
+ // remove remaining points
+ Collections.shuffle(oddPts);
+ for (final P pt : oddPts) {
+ Assertions.assertTrue(set.remove(pt));
+ }
+
+ // ensure that nothing is left
+ assertEmpty(set);
+ }
+
+ @Test
+ void testRemoveAll() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final int size = 500;
+
+ final List<P> pts = getTestPoints(size * 2, 3 * EPS);
+ final List<P> addedPts = pts.subList(0, size);
+
+ set.addAll(addedPts);
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+ final P c = pts.get(3);
+ final P d = pts.get(pts.size() - 1);
+ final P e = pts.get(pts.size() - 2);
+
+ // act/assert
+ Assertions.assertFalse(set.removeAll(Arrays.asList()));
+ Assertions.assertEquals(size, set.size());
+
+ Assertions.assertTrue(set.removeAll(Arrays.asList(a)));
+ Assertions.assertEquals(size - 1, set.size());
+
+ Assertions.assertTrue(set.removeAll(Arrays.asList(a, b, c)));
+ Assertions.assertEquals(size - 3, set.size());
+
+ Assertions.assertFalse(set.removeAll(Arrays.asList(a, b, c)));
+ Assertions.assertEquals(size - 3, set.size());
+
+ Assertions.assertFalse(set.removeAll(Arrays.asList(d, e)));
+ Assertions.assertEquals(size - 3, set.size());
+
+ Assertions.assertTrue(set.removeAll(addedPts));
+ assertEmpty(set);
+ }
+
+ @Test
+ void testRemoveAll_empty() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(10, 3 * EPS);
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ // act/assert
+ Assertions.assertFalse(set.removeAll(Arrays.asList()));
+
+ Assertions.assertFalse(set.removeAll(Arrays.asList(a, b)));
+ Assertions.assertFalse(set.removeAll(pts));
+ }
+
+ @Test
+ void testRetainAll() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final int size = 500;
+
+ final List<P> pts = getTestPoints(size * 2, 3 * EPS);
+ final List<P> addedPts = pts.subList(0, size);
+
+ set.addAll(addedPts);
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+ final P c = pts.get(3);
+ final P d = pts.get(pts.size() - 1);
+ final P e = pts.get(pts.size() - 2);
+
+ // act/assert
+ Assertions.assertFalse(set.retainAll(addedPts));
+ Assertions.assertEquals(size, set.size());
+
+ Assertions.assertTrue(set.retainAll(Arrays.asList(a, b, c)));
+ Assertions.assertEquals(3, set.size());
+
+ Assertions.assertTrue(set.retainAll(Arrays.asList(d, e)));
+ assertEmpty(set);
+
+ Assertions.assertFalse(set.retainAll(Arrays.asList()));
+ assertEmpty(set);
+ }
+
+ @Test
+ void testRetainAll_empty() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(10, 3 * EPS);
+
+ final P a = pts.get(0);
+ final P b = pts.get(1);
+
+ // act/assert
+ Assertions.assertFalse(set.retainAll(Arrays.asList()));
+
+ Assertions.assertFalse(set.retainAll(Arrays.asList(a, b)));
+ Assertions.assertFalse(set.retainAll(pts));
+ }
+
+ @Test
+ void testClear_empty() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ // act
+ set.clear();
+
+ // assert
+ assertEmpty(set);
+ }
+
+ @Test
+ void testClear_populated() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+ set.addAll(getTestPoints(1_000, EPS, new Random(6L)));
+
+ // act
+ set.clear();
+
+ // assert
+ assertEmpty(set);
+ }
+
+ @Test
+ void testRepeatedUse() {
+ // -- arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final Random rnd = new Random(3L);
+
+ final int cnt = 10_000;
+ final List<P> pts = getTestPoints(cnt, EPS, rnd);
+
+ // -- act
+ final int iterations = 10;
+ final int subListSize = cnt / iterations;
+ for (int i = 0; i < iterations; ++i) {
+ final int subListStart = i * subListSize;
+ final List<P> subList = pts.subList(subListStart, subListStart + subListSize);
+
+ // add sublist
+ set.addAll(subList);
+
+ // remove sublist in different order
+ final List<P> shuffledSubList = new ArrayList<>(subList);
+ Collections.shuffle(shuffledSubList, rnd);
+
+ for (final P pt : shuffledSubList) {
+ Assertions.assertTrue(set.remove(pt));
+ }
+
+ // add sublist again
+ set.addAll(subList);
+ }
+
+ // -- assert
+ checkerFor(set)
+ .expect(pts)
+ .check();
+ }
+
+ @Test
+ void testIterator() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final Set<P> testPts = new HashSet<>(getTestPoints(1_000, 2 * EPS));
+ set.addAll(testPts);
+
+ // act/assert
+ final Iterator<P> it = set.iterator();
+ while (it.hasNext()) {
+ final P pt = it.next();
+
+ Assertions.assertTrue(testPts.remove(pt), () -> "Unexpected iterator point " + pt);
+ }
+
+ Assertions.assertEquals(0, testPts.size(), "Expected iterator to visit all points");
+
+ Assertions.assertFalse(it.hasNext());
+ Assertions.assertThrows(NoSuchElementException.class, () -> it.next());
+ }
+
+ @Test
+ void testIterator_empty() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ // act
+ final Iterator<P> it = set.iterator();
+
+ // assert
+ Assertions.assertFalse(it.hasNext());
+ Assertions.assertThrows(NoSuchElementException.class, () -> it.next());
+ }
+
+ @Test
+ void testIterator_remove() {
+ // --- arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final int size = 1_000;
+ final Set<P> testPts = new HashSet<>(getTestPoints(size, 2 * EPS));
+
+ set.addAll(testPts);
+
+ // --- act/assert
+ final Iterator<P> it = set.iterator();
+ while (it.hasNext()) {
+ final P pt = it.next();
+
+ it.remove();
+ Assertions.assertTrue(testPts.remove(pt), () -> "Unexpected iterator point " + pt);
+ }
+
+ Assertions.assertEquals(0, testPts.size(), "Expected iterator to visit all points");
+
+ assertEmpty(set);
+ }
+
+ @Test
+ void testIterator_remove_multiplePasses() {
+ // --- arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final int size = 1_000;
+ final Set<P> testPts = new HashSet<>(getTestPoints(size, 2 * EPS));
+
+ set.addAll(testPts);
+
+ // --- act/assert
+ // remove the points in two passes
+ final Iterator<P> firstIt = set.iterator();
+
+ int i = -1;
+ while (firstIt.hasNext()) {
+ final P pt = firstIt.next();
+
+ if ((++i) % 2 == 0) {
+ firstIt.remove();
+ Assertions.assertTrue(testPts.remove(pt), () -> "Unexpected iterator point " + pt);
+ }
+ }
+
+ Assertions.assertEquals(size / 2, set.size());
+
+ final Iterator<P> secondIt = set.iterator();
+ while (secondIt.hasNext()) {
+ final P pt = secondIt.next();
+
+ secondIt.remove();
+
+ Assertions.assertTrue(testPts.remove(pt), () -> "Unexpected iterator point " + pt);
+ }
+
+ Assertions.assertEquals(0, testPts.size(), "Expected iterator to visit all points");
+
+ assertEmpty(set);
+ }
+
+ @Test
+ void testToArray() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+ set.addAll(pts);
+
+ // act
+ final Object[] objArr = set.toArray();
+ final P[] typedArr = set.toArray(createPointArray());
+
+ // assert
+ Assertions.assertEquals(set.size(), objArr.length);
+ Assertions.assertEquals(set.size(), typedArr.length);
+
+ int i = 0;
+ for (final P element : set) {
+ Assertions.assertEquals(element, objArr[i]);
+ Assertions.assertEquals(element, typedArr[i]);
+
+ ++i;
+ }
+ }
+
+ @Test
+ void testHashCode() {
+ // arrange
+ final PointSet<P> a = getSet(PRECISION);
+ final PointSet<P> b = getSet(PRECISION);
+ final PointSet<P> c = getSet(PRECISION);
+ final PointSet<P> d = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ a.addAll(pts);
+ b.addAll(pts.subList(0, 2));
+
+ d.addAll(pts);
+
+ // act
+ final int hash = a.hashCode();
+
+ // act/assert
+ Assertions.assertEquals(hash, a.hashCode());
+
+ Assertions.assertNotEquals(hash, b.hashCode());
+ Assertions.assertNotEquals(hash, c.hashCode());
+
+ Assertions.assertEquals(hash, d.hashCode());
+ }
+
+ @Test
+ void testEquals() {
+ // arrange
+ final PointSet<P> a = getSet(PRECISION);
+ final PointSet<P> b = getSet(PRECISION);
+ final PointSet<P> c = getSet(PRECISION);
+ final PointSet<P> d = getSet(PRECISION);
+
+ final List<P> pts = getTestPoints(3, EPS);
+
+ a.addAll(pts);
+ b.addAll(pts.subList(0, 2));
+
+ d.addAll(pts);
+
+ // act/assert
+ Assertions.assertFalse(a.equals(null));
+ Assertions.assertFalse(a.equals(new Object()));
+
+ Assertions.assertTrue(a.equals(a));
+
+ Assertions.assertFalse(a.equals(b));
+ Assertions.assertFalse(a.equals(c));
+
+ Assertions.assertTrue(a.equals(d));
+ }
+
+ @Test
+ void testToString() {
+ // arrange
+ final PointSet<P> set = getSet(PRECISION);
+ final P pt = getTestPoints(1, EPS).get(0);
+
+ set.add(pt);
+
+ // act
+ final String str = set.toString();
+
+ // assert
+ GeometryTestUtils.assertContains(pt.toString(), str);
+ }
+
+ /** Return a new {@link PointSetChecker} for asserting the contents
+ * of the given set.
+ * @param set
+ * @return a new checker instance
+ */
+ public static <P extends Point<P>> PointSetChecker<P> checkerFor(final PointSet<P> set) {
+ return new PointSetChecker<>(set);
+ }
+
+ /** Assert that the given set is empty.
+ * @param set setto assert empty
+ */
+ public static void assertEmpty(final PointSet<?> set) {
+ checkerFor(set)
+ .check();
+ }
+
+ /** Class designed to assist with performing assertions on the state
+ * of a point set.
+ */
+ public static class PointSetChecker<P extends Point<P>> {
+
+ private final PointSet<P> set;
+
+ private final List<P> expected = new ArrayList<>();
+
+ private final List<P> unexpected = new ArrayList<>();
+
+ public PointSetChecker(final PointSet<P> set) {
+ this.set = set;
+ }
+
+ public PointSetChecker<P> expect(final P value) {
+ expected.add(value);
+
+ return this;
+ }
+
+ public PointSetChecker<P> expect(final Iterable<? extends P> values) {
+ for (final P value : values) {
+ expect(value);
+ }
+
+ return this;
+ }
+
+ public PointSetChecker<P> doesNotContain(final P value) {
+ unexpected.add(value);
+
+ return this;
+ }
+
+ public PointSetChecker<P> doesNotContain(final Iterable<? extends P> values) {
+ for (final P value : values) {
+ doesNotContain(value);
+ }
+
+ return this;
+ }
+
+ public void check() {
+ checkSize();
+ checkValues();
+ checkUnexpectedValues();
+ }
+
+ private void checkSize() {
+ Assertions.assertEquals(expected.size(), set.size(), "Unexpected set size");
+ Assertions.assertEquals(expected.isEmpty(), set.isEmpty(), "Unexpected isEmpty() result");
+ }
+
+ private void checkValues() {
+ Assertions.assertEquals(expected.size(), set.size(), "Unexpected size");
+
+ for (final P value : expected) {
+ Assertions.assertTrue(set.contains(value), () -> "Expected set to contain value " + value);
+ Assertions.assertEquals(value, set.get(value), () -> "Expected set to contain value " + value);
+ }
+ }
+
+ private void checkUnexpectedValues() {
+ for (final P value : unexpected) {
+ Assertions.assertFalse(set.contains(value), () -> "Expected set to not contain value " + value);
+ Assertions.assertNull(set.get(value), () -> "Expected set to not contain value " + value);
+ }
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/AbstractBucketPointMapTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/AbstractBucketPointMapTest.java
new file mode 100644
index 0000000..c61f0d6
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/AbstractBucketPointMapTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMapTestBase;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class AbstractBucketPointMapTest extends PointMapTestBase<TestPoint1D> {
+
+ @Test
+ void testPut_pointsCloseToSplit() {
+ // arrange
+ TestBucketPointMap1D<Integer> map = getMap(PRECISION);
+
+ final List<TestPoint1D> pts = createPointList(0, 1, TestBucketPointMap1D.MAX_ENTRY_COUNT);
+ insertPoints(pts, map);
+
+ final TestPoint1D split = centroid(pts);
+
+ final TestPoint1D pt = new TestPoint1D(split.getX() + (1.25 * EPS));
+
+ map.put(pt, 100);
+
+ // act/assert
+ final TestPoint1D close = new TestPoint1D(split.getX() + (0.75 * EPS));
+
+ Assertions.assertEquals(100, map.put(close, 101));
+ Assertions.assertEquals(101, map.get(close));
+ Assertions.assertEquals(101, map.get(pt));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected <V> TestBucketPointMap1D<V> getMap(final Precision.DoubleEquivalence precision) {
+ return new TestBucketPointMap1D<>(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected TestPoint1D[] createPointArray() {
+ return new TestPoint1D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getNaNPoints() {
+ return Arrays.asList(new TestPoint1D(Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getInfPoints() {
+ return Arrays.asList(
+ new TestPoint1D(Double.NEGATIVE_INFINITY),
+ new TestPoint1D(Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getTestPoints(final int cnt, final double eps) {
+ final double delta = 10 * eps;
+ return createPointList(-1.0, delta, cnt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getTestPointsAtDistance(final TestPoint1D pt, final double dist) {
+ return Arrays.asList(
+ new TestPoint1D(pt.getX() - dist),
+ new TestPoint1D(pt.getX() + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final TestPoint1D a, final TestPoint1D b, final Precision.DoubleEquivalence precision) {
+ return precision.eq(a.getX(), b.getX());
+ }
+
+ private static List<TestPoint1D> createPointList(final double start, final double delta, final int cnt) {
+ final List<TestPoint1D> pts = new ArrayList<>(cnt);
+
+ double x = start;
+ for (int i = 0; i < cnt; ++i) {
+ pts.add(new TestPoint1D(x));
+
+ x += delta;
+ }
+
+ return pts;
+ }
+
+ private static TestPoint1D centroid(final List<TestPoint1D> pts) {
+ double sum = 0;
+ for (final TestPoint1D pt : pts) {
+ sum += pt.getX();
+ }
+
+ return new TestPoint1D(sum / pts.size());
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/AbstractPointMap1DTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/AbstractPointMap1DTest.java
new file mode 100644
index 0000000..60166cd
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/AbstractPointMap1DTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Set;
+
+import org.apache.commons.geometry.core.collection.PointMapTestBase;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.numbers.core.Precision;
+
+class AbstractPointMap1DTest extends PointMapTestBase<TestPoint1D> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected <V> TestPointMap1D<V> getMap(final Precision.DoubleEquivalence precision) {
+ return new TestPointMap1D<>(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected TestPoint1D[] createPointArray() {
+ return new TestPoint1D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getNaNPoints() {
+ return Arrays.asList(new TestPoint1D(Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getInfPoints() {
+ return Arrays.asList(
+ new TestPoint1D(Double.NEGATIVE_INFINITY),
+ new TestPoint1D(Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getTestPoints(final int cnt, final double eps) {
+ final double delta = 10 * eps;
+ return createPointList(-1.0, delta, cnt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getTestPointsAtDistance(final TestPoint1D pt, final double dist) {
+ return Arrays.asList(
+ new TestPoint1D(pt.getX() - dist),
+ new TestPoint1D(pt.getX() + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final TestPoint1D a, final TestPoint1D b, final Precision.DoubleEquivalence precision) {
+ return precision.eq(a.getX(), b.getX());
+ }
+
+ private static List<TestPoint1D> createPointList(final double start, final double delta, final int cnt) {
+ final List<TestPoint1D> pts = new ArrayList<>(cnt);
+
+ double x = start;
+ for (int i = 0; i < cnt; ++i) {
+ pts.add(new TestPoint1D(x));
+
+ x += delta;
+ }
+
+ return pts;
+ }
+
+ private static final class TestPointMap1D<V> extends AbstractPointMap1D<TestPoint1D, V> {
+
+ TestPointMap1D(final Precision.DoubleEquivalence precision) {
+ super((a, b) -> precision.compare(a.getX(), b.getX()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean containsKey(final Object key) {
+ return getMap().containsKey(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ return getMap().get(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ return getMap().remove(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clear() {
+ getMap().clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<TestPoint1D> keySet() {
+ return getMap().keySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<TestPoint1D, V>> entrySet() {
+ return getMap().entrySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Entry<TestPoint1D, V> getEntryInternal(final TestPoint1D key) {
+ final NavigableMap<TestPoint1D, V> map = getMap();
+ final Map.Entry<TestPoint1D, V> floor = map.floorEntry(key);
+ if (floor != null &&
+ map.comparator().compare(floor.getKey(), key) == 0) {
+ return floor;
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected V putInternal(final TestPoint1D key, final V value) {
+ return getMap().put(key, value);
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/PointMapAsSetAdapterTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/PointMapAsSetAdapterTest.java
new file mode 100644
index 0000000..2fdbae1
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/PointMapAsSetAdapterTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointSet;
+import org.apache.commons.geometry.core.collection.PointSetTestBase;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.numbers.core.Precision.DoubleEquivalence;
+
+class PointMapAsSetAdapterTest extends PointSetTestBase<TestPoint1D> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected PointSet<TestPoint1D> getSet(final Precision.DoubleEquivalence precision) {
+ return new PointMapAsSetAdapter<>(new TestBucketPointMap1D<>(precision));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected TestPoint1D[] createPointArray() {
+ return new TestPoint1D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getNaNPoints() {
+ return Arrays.asList(new TestPoint1D(Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getInfPoints() {
+ return Arrays.asList(
+ new TestPoint1D(Double.NEGATIVE_INFINITY),
+ new TestPoint1D(Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getTestPoints(final int cnt, final double eps) {
+ final double delta = 10 * eps;
+ return createPointList(-1.0, delta, cnt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<TestPoint1D> getTestPointsAtDistance(final TestPoint1D pt, final double dist) {
+ return Arrays.asList(
+ new TestPoint1D(pt.getX() - dist),
+ new TestPoint1D(pt.getX() + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final TestPoint1D a, final TestPoint1D b, final DoubleEquivalence precision) {
+ return precision.eq(a.getX(), b.getX());
+ }
+
+ private static List<TestPoint1D> createPointList(final double start, final double delta, final int cnt) {
+ final List<TestPoint1D> pts = new ArrayList<>(cnt);
+
+ double x = start;
+ for (int i = 0; i < cnt; ++i) {
+ pts.add(new TestPoint1D(x));
+
+ x += delta;
+ }
+
+ return pts;
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/TestBucketPointMap1D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/TestBucketPointMap1D.java
new file mode 100644
index 0000000..49d9004
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/TestBucketPointMap1D.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.internal;
+
+import java.util.Map;
+
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.numbers.core.Precision;
+
+/** {@link AbstractBucketPointMap} implementation for use in tests.
+ * @param <V> Value type
+ */
+public class TestBucketPointMap1D<V> extends AbstractBucketPointMap<TestPoint1D, V> {
+
+ static final int MAX_ENTRY_COUNT = 16;
+
+ static final int NODE_CHILD_COUNT = 2;
+
+ TestBucketPointMap1D(final Precision.DoubleEquivalence precision) {
+ super(TestNode1D::new,
+ MAX_ENTRY_COUNT,
+ NODE_CHILD_COUNT,
+ precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean pointsEq(final TestPoint1D a, final TestPoint1D b) {
+ return getPrecision().eq(a.getX(), b.getX());
+ }
+
+ private static final class TestNode1D<V> extends AbstractBucketPointMap.BucketNode<TestPoint1D, V> {
+
+ /** Negative half-space flag. */
+ private static final int NEG = 1 << 1;
+
+ /** Positve half-space flag. */
+ private static final int POS = 1;
+
+ /** Location flags for child nodes. */
+ private static final int[] CHILD_LOCATIONS = {
+ NEG,
+ POS
+ };
+
+ private double split;
+
+ TestNode1D(
+ final AbstractBucketPointMap<TestPoint1D, V> map,
+ final BucketNode<TestPoint1D, V> parent) {
+ super(map, parent);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void computeSplit() {
+ double sum = 0;
+ for (Map.Entry<TestPoint1D, V> entry : this) {
+ sum += entry.getKey().getX();
+ }
+
+ split = sum / TestBucketPointMap1D.MAX_ENTRY_COUNT;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getSearchLocation(final TestPoint1D pt) {
+ return getSearchLocationValue(getPrecision().compare(pt.getX(), split), NEG, POS);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getInsertLocation(final TestPoint1D pt) {
+ return getInsertLocationValue(Double.compare(pt.getX(), split), NEG, POS);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean testChildLocation(final int childIdx, final int loc) {
+ final int childLoc = CHILD_LOCATIONS[childIdx];
+ return (childLoc & loc) == childLoc;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanCollections.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanCollections.java
new file mode 100644
index 0000000..77e66b7
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanCollections.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.collection.PointSet;
+import org.apache.commons.geometry.core.internal.PointMapAsSetAdapter;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.numbers.core.Precision;
+
+/** Class containing utility methods for Euclidean collection types.
+ */
+public final class EuclideanCollections {
+
+ /** No instantiation. */
+ private EuclideanCollections() {}
+
+ /** Construct a new 1D {@link PointSet} instance using the given precision context to determine
+ * equality between points.
+ *
+ * <p>NOTE: The returned instance is <em>not</em> thread-safe.</p>
+ * @param precision precision context used to determine point equality
+ * @return new 1D point set instance
+ */
+ public static PointSet<Vector1D> pointSet1D(final Precision.DoubleEquivalence precision) {
+ return new PointMapAsSetAdapter<>(pointMap1D(precision));
+ }
+
+ /** Construct a new 1D {@link PointMap} instance using the given precision context to determine
+ * equality between points.
+ *
+ * <p>NOTE: The returned instance is <em>not</em> thread-safe.</p>
+ * @param <V> Map value type
+ * @param precision precision context used to determine point equality
+ * @return new 1D point map instance
+ */
+ public static <V> PointMap<Vector1D, V> pointMap1D(final Precision.DoubleEquivalence precision) {
+ return new PointMap1DImpl<>(precision);
+ }
+
+ /** Construct a new 2D {@link PointSet} instance using the given precision context to determine
+ * equality between points.
+ *
+ * <p>NOTE: The returned instance is <em>not</em> thread-safe.</p>
+ * @param precision precision context used to determine point equality
+ * @return new 2D point set instance
+ */
+ public static PointSet<Vector2D> pointSet2D(final Precision.DoubleEquivalence precision) {
+ return new PointMapAsSetAdapter<>(pointMap2D(precision));
+ }
+
+ /** Construct a new 2D {@link PointMap} instance using the given precision context to determine
+ * equality between points.
+ *
+ * <p>NOTE: The returned instance is <em>not</em> thread-safe.</p>
+ * @param <V> Map value type
+ * @param precision precision context used to determine point equality
+ * @return new 2D point map instance
+ */
+ public static <V> PointMap<Vector2D, V> pointMap2D(final Precision.DoubleEquivalence precision) {
+ return new PointMap2DImpl<>(precision);
+ }
+
+ /** Construct a new 3D {@link PointSet} instance using the given precision context to determine
+ * equality between points.
+ *
+ * <p>NOTE: The returned instance is <em>not</em> thread-safe.</p>
+ * @param precision precision context used to determine point equality
+ * @return new 3D point set instance
+ */
+ public static PointSet<Vector3D> pointSet3D(final Precision.DoubleEquivalence precision) {
+ return new PointMapAsSetAdapter<>(pointMap3D(precision));
+ }
+
+ /** Construct a new 3D {@link PointMap} instance using the given precision context to determine
+ * equality between points.
+ *
+ * <p>NOTE: The returned instance is <em>not</em> thread-safe.</p>
+ * @param <V> Map value type
+ * @param precision precision context used to determine point equality
+ * @return new 3D point map instance
+ */
+ public static <V> PointMap<Vector3D, V> pointMap3D(final Precision.DoubleEquivalence precision) {
+ return new PointMap3DImpl<>(precision);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap1DImpl.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap1DImpl.java
new file mode 100644
index 0000000..1525d1a
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap1DImpl.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean;
+
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Set;
+
+import org.apache.commons.geometry.core.internal.AbstractPointMap1D;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.numbers.core.Precision;
+
+/** Internal {@link org.apache.commons.geometry.core.collection.PointMap PointMap}
+ * implementation for Euclidean 1D space.
+ * @param <V> Map value type
+ */
+final class PointMap1DImpl<V>
+ extends AbstractPointMap1D<Vector1D, V> {
+
+ /** Construct a new instance using the given precision context to determine
+ * floating point equality.
+ * @param precision precision context
+ */
+ PointMap1DImpl(final Precision.DoubleEquivalence precision) {
+ super((a, b) -> precision.compare(a.getX(), b.getX()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean containsKey(final Object key) {
+ return getMap().containsKey(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ return getMap().get(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ return getMap().remove(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clear() {
+ getMap().clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Vector1D> keySet() {
+ return getMap().keySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<Vector1D, V>> entrySet() {
+ return getMap().entrySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map.Entry<Vector1D, V> getEntryInternal(final Vector1D key) {
+ final NavigableMap<Vector1D, V> map = getMap();
+ final Map.Entry<Vector1D, V> floor = map.floorEntry(key);
+ if (floor != null &&
+ map.comparator().compare(floor.getKey(), key) == 0) {
+ return floor;
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected V putInternal(final Vector1D key, final V value) {
+ return getMap().put(key, value);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap2DImpl.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap2DImpl.java
new file mode 100644
index 0000000..472d6ca
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap2DImpl.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.internal.AbstractBucketPointMap;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.numbers.core.Precision;
+
+/** Internal {@link PointMap} implementation for Euclidean 2D space.
+ * @param <V> Map value type
+ */
+final class PointMap2DImpl<V>
+ extends AbstractBucketPointMap<Vector2D, V>
+ implements PointMap<Vector2D, V> {
+
+ /** Number of children per node. */
+ private static final int NODE_CHILD_COUNT = 4;
+
+ /** Max entries per node. */
+ private static final int MAX_ENTRIES_PER_NODE = 16;
+
+ /** X negative quadrant flag. */
+ private static final int XNEG = 1 << 3;
+
+ /** X postive quadrant flag. */
+ private static final int XPOS = 1 << 2;
+
+ /** Y negative quadrant flag. */
+ private static final int YNEG = 1 << 1;
+
+ /** Y positive quadrant flag. */
+ private static final int YPOS = 1;
+
+ /** Quadtree location flags for child nodes. */
+ private static final int[] CHILD_LOCATIONS = {
+ XNEG | YNEG,
+ XNEG | YPOS,
+ XPOS | YNEG,
+ XPOS | YPOS
+ };
+
+ /** Construct a new instance using the given precision context to determine
+ * floating point equality.
+ * @param precision precision context
+ */
+ PointMap2DImpl(final Precision.DoubleEquivalence precision) {
+ super(MapNode2D::new,
+ MAX_ENTRIES_PER_NODE,
+ NODE_CHILD_COUNT,
+ precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean pointsEq(final Vector2D a, final Vector2D b) {
+ return a.eq(b, getPrecision());
+ }
+
+ /** Tree node class for {@link PointMap2DImpl}.
+ * @param <V> Map value type
+ */
+ private static final class MapNode2D<V> extends BucketNode<Vector2D, V> {
+
+ /** Point to split child spaces; will be null for leaf nodes. */
+ private Vector2D split;
+
+ /** Construct a new instance.
+ * @param map owning map
+ * @param parent parent node; set to null for the root node
+ */
+ MapNode2D(final AbstractBucketPointMap<Vector2D, V> map,
+ final BucketNode<Vector2D, V> parent) {
+ super(map, parent);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getSearchLocation(final Vector2D pt) {
+ final Precision.DoubleEquivalence precision = getPrecision();
+
+ int loc = getSearchLocationValue(
+ precision.compare(pt.getX(), split.getX()),
+ XNEG,
+ XPOS);
+ loc |= getSearchLocationValue(
+ precision.compare(pt.getY(), split.getY()),
+ YNEG,
+ YPOS);
+
+ return loc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getInsertLocation(final Vector2D pt) {
+ int loc = getInsertLocationValue(
+ Double.compare(pt.getX(), split.getX()),
+ XNEG,
+ XPOS);
+ loc |= getInsertLocationValue(
+ Double.compare(pt.getY(), split.getY()),
+ YNEG,
+ YPOS);
+
+ return loc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void computeSplit() {
+ final Vector2D.Sum sum = Vector2D.Sum.create();
+ for (final Entry<Vector2D, V> entry : this) {
+ sum.add(entry.getKey());
+ }
+
+ split = sum.get().multiply(1.0 / MAX_ENTRIES_PER_NODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean testChildLocation(final int childIdx, final int loc) {
+ final int childLoc = CHILD_LOCATIONS[childIdx];
+ return (childLoc & loc) == childLoc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void makeLeaf(final List<Entry<Vector2D, V>> leafEntries) {
+ super.makeLeaf(leafEntries);
+
+ split = null;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap3DImpl.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap3DImpl.java
new file mode 100644
index 0000000..eae192a
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/PointMap3DImpl.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.internal.AbstractBucketPointMap;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision;
+
+/** Internal {@link PointMap} implementation for Euclidean 3D space.
+ * @param <V> Map value type
+ */
+final class PointMap3DImpl<V>
+ extends AbstractBucketPointMap<Vector3D, V>
+ implements PointMap<Vector3D, V> {
+
+ /** Number of children per node. */
+ private static final int NODE_CHILD_COUNT = 8;
+
+ /** Max entries per node. This value was determined empirically and was chosen to
+ * provide a balance between having a small number of entries in each node when
+ * searching and having a large number of samples to provide a good split point
+ * during insertion. See the {@code org.apache.commons.geometry.examples.jmh.euclidean.PointMap3DPerformance}
+ * class in the {@code examples-jmh} module for details on the performance tests used.
+ */
+ private static final int MAX_ENTRIES_PER_NODE = 32;
+
+ /** X negative octant flag. */
+ private static final int XNEG = 1 << 5;
+
+ /** X postive octant flag. */
+ private static final int XPOS = 1 << 4;
+
+ /** Y negative octant flag. */
+ private static final int YNEG = 1 << 3;
+
+ /** Y positive octant flag. */
+ private static final int YPOS = 1 << 2;
+
+ /** Z negative octant flag. */
+ private static final int ZNEG = 1 << 1;
+
+ /** Z positive octant flag. */
+ private static final int ZPOS = 1;
+
+ /** Octant location flags for child nodes. */
+ private static final int[] CHILD_LOCATIONS = {
+ XNEG | YNEG | ZNEG,
+ XNEG | YNEG | ZPOS,
+ XNEG | YPOS | ZNEG,
+ XNEG | YPOS | ZPOS,
+
+ XPOS | YNEG | ZNEG,
+ XPOS | YNEG | ZPOS,
+ XPOS | YPOS | ZNEG,
+ XPOS | YPOS | ZPOS
+ };
+
+ /** Construct a new instance using the given precision context to determine
+ * floating point equality.
+ * @param precision precision context
+ */
+ PointMap3DImpl(final Precision.DoubleEquivalence precision) {
+ super(MapNode3D::new,
+ MAX_ENTRIES_PER_NODE,
+ NODE_CHILD_COUNT,
+ precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean pointsEq(final Vector3D a, final Vector3D b) {
+ return a.eq(b, getPrecision());
+ }
+
+ /** Tree node class for {@link PointMap3DImpl}.
+ * @param <V> Map value type
+ */
+ private static final class MapNode3D<V> extends BucketNode<Vector3D, V> {
+
+ /** Point to split child spaces; will be null for leaf nodes. */
+ private Vector3D split;
+
+ /** Construct a new instance.
+ * @param map owning map
+ * @param parent parent node; set to null for the root node
+ */
+ MapNode3D(final AbstractBucketPointMap<Vector3D, V> map,
+ final BucketNode<Vector3D, V> parent) {
+ super(map, parent);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void computeSplit() {
+ final Vector3D.Sum sum = Vector3D.Sum.create();
+ for (final Entry<Vector3D, V> entry : this) {
+ sum.add(entry.getKey());
+ }
+
+ split = sum.get().multiply(1.0 / MAX_ENTRIES_PER_NODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getSearchLocation(final Vector3D pt) {
+ final Precision.DoubleEquivalence precision = getPrecision();
+
+ int loc = getSearchLocationValue(
+ precision.compare(pt.getX(), split.getX()),
+ XNEG,
+ XPOS);
+ loc |= getSearchLocationValue(
+ precision.compare(pt.getY(), split.getY()),
+ YNEG,
+ YPOS);
+ loc |= getSearchLocationValue(
+ precision.compare(pt.getZ(), split.getZ()),
+ ZNEG,
+ ZPOS);
+
+ return loc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getInsertLocation(final Vector3D pt) {
+ int loc = getInsertLocationValue(
+ Double.compare(pt.getX(), split.getX()),
+ XNEG,
+ XPOS);
+ loc |= getInsertLocationValue(
+ Double.compare(pt.getY(), split.getY()),
+ YNEG,
+ YPOS);
+ loc |= getInsertLocationValue(
+ Double.compare(pt.getZ(), split.getZ()),
+ ZNEG,
+ ZPOS);
+
+ return loc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean testChildLocation(final int childIdx, final int loc) {
+ final int childLoc = CHILD_LOCATIONS[childIdx];
+ return (childLoc & loc) == childLoc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void makeLeaf(final List<Entry<Vector3D, V>> leafEntries) {
+ super.makeLeaf(leafEntries);
+
+ split = null;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/mesh/SimpleTriangleMesh.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/mesh/SimpleTriangleMesh.java
index 409e8ad..d85476d 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/mesh/SimpleTriangleMesh.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/mesh/SimpleTriangleMesh.java
@@ -20,18 +20,17 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
-import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
import org.apache.commons.geometry.euclidean.threed.Bounds3D;
@@ -417,7 +416,7 @@ public final class SimpleTriangleMesh implements TriangleMesh {
private final ArrayList<Vector3D> vertices = new ArrayList<>();
/** Map of vertices to their first occurrence in the vertex list. */
- private Map<Vector3D, Integer> vertexIndexMap;
+ private PointMap<Vector3D, Integer> vertexIndexMap;
/** List of face vertex indices. */
private final ArrayList<int[]> faces = new ArrayList<>();
@@ -674,9 +673,9 @@ public final class SimpleTriangleMesh implements TriangleMesh {
/** Get the vertex index map, creating and initializing it if needed.
* @return the vertex index map
*/
- private Map<Vector3D, Integer> getVertexIndexMap() {
+ private PointMap<Vector3D, Integer> getVertexIndexMap() {
if (vertexIndexMap == null) {
- vertexIndexMap = new TreeMap<>(new FuzzyVectorComparator(precision));
+ vertexIndexMap = EuclideanCollections.pointMap3D(precision);
// populate the index map
final int size = vertices.size();
@@ -697,7 +696,7 @@ public final class SimpleTriangleMesh implements TriangleMesh {
* @return the index now associated with the given vertex or its equivalent
*/
private int addToVertexIndexMap(final Vector3D vertex, final int targetIdx,
- final Map<? super Vector3D, Integer> map) {
+ final PointMap<Vector3D, Integer> map) {
validateCanModify();
final Integer actualIdx = map.putIfAbsent(vertex, targetIdx);
@@ -745,35 +744,4 @@ public final class SimpleTriangleMesh implements TriangleMesh {
}
}
}
-
- /** Comparator used to sort vectors using non-strict ("fuzzy") comparisons.
- * Vectors are considered equal if their values in all coordinate dimensions
- * are equivalent as evaluated by the precision context.
- */
- private static final class FuzzyVectorComparator implements Comparator<Vector3D> {
- /** Precision context to determine floating-point equality. */
- private final Precision.DoubleEquivalence precision;
-
- /** Construct a new instance that uses the given precision context for
- * floating point comparisons.
- * @param precision precision context used for floating point comparisons
- */
- FuzzyVectorComparator(final Precision.DoubleEquivalence precision) {
- this.precision = precision;
- }
-
- /** {@inheritDoc} */
- @Override
- public int compare(final Vector3D a, final Vector3D b) {
- int result = precision.compare(a.getX(), b.getX());
- if (result == 0) {
- result = precision.compare(a.getY(), b.getY());
- if (result == 0) {
- result = precision.compare(a.getZ(), b.getZ());
- }
- }
-
- return result;
- }
- }
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
index e87848d..5fe389c 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.collection.PointMap;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
import org.apache.commons.geometry.euclidean.oned.Interval;
@@ -441,4 +442,23 @@ class DocumentationExamplesTest {
EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), intersection, TEST_EPS);
EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1), normal, TEST_EPS);
}
+
+ @Test
+ void testPointMap3DExample() {
+ final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-6);
+
+ final PointMap<Vector3D, String> map = EuclideanCollections.pointMap3D(precision);
+ map.put(Vector3D.ZERO, "a");
+ map.put(Vector3D.Unit.PLUS_X, "b");
+
+ final String originValue = map.get(Vector3D.of(1e-8, 1e-8, -1e-8)); // originValue = "a"
+ final String plusXValue = map.get(Vector3D.of(1, 0, 1e-8)); // plusXValue = "b"
+
+ final String missingValue = map.get(Vector3D.of(1e-5, 0, 0)); // missingValue = null
+
+ // ---------------------
+ Assertions.assertEquals("a", originValue);
+ Assertions.assertEquals("b", plusXValue);
+ Assertions.assertNull(missingValue);
+ }
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/PointMap1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/PointMap1DTest.java
new file mode 100644
index 0000000..683b7ce
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/PointMap1DTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.oned;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.collection.PointMapTestBase;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class PointMap1DTest extends PointMapTestBase<Vector1D> {
+
+ @Test
+ void testDenseLine() {
+ // arrange
+ final PointMap<Vector1D, Integer> map = getMap(PRECISION);
+
+ final double step = 1.1 * EPS;
+ final double start = -1.0;
+ final int cnt = 1_000_000;
+
+ // act
+ double x = start;
+ for (int i = 0; i < cnt; ++i) {
+ map.put(Vector1D.of(x), 0);
+
+ x += step;
+ }
+
+ // act
+ assertEquals(cnt, map.size());
+
+ final double offset = 0.9 * EPS;
+ x = start;
+ for (int i = 0; i < cnt; ++i) {
+ Assertions.assertEquals(0, map.get(Vector1D.of(x + offset)));
+
+ x += step;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected <V> PointMap<Vector1D, V> getMap(final Precision.DoubleEquivalence precision) {
+ return EuclideanCollections.pointMap1D(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector1D[] createPointArray() {
+ return new Vector1D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getNaNPoints() {
+ return Collections.singletonList(Vector1D.NaN);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getInfPoints() {
+ return Arrays.asList(
+ Vector1D.NEGATIVE_INFINITY,
+ Vector1D.POSITIVE_INFINITY);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getTestPoints(final int cnt, final double eps) {
+ final List<Vector1D> pts = new ArrayList<>(cnt);
+
+ final double delta = 10 * eps;
+
+ double x = 0.0;
+ for (int i = 0; i < cnt; ++i) {
+ pts.add(Vector1D.of(x));
+
+ x += delta;
+ }
+
+ return pts;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getTestPointsAtDistance(final Vector1D pt, final double dist) {
+ return Arrays.asList(
+ Vector1D.of(pt.getX() - dist),
+ Vector1D.of(pt.getX() + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final Vector1D a, final Vector1D b, final Precision.DoubleEquivalence precision) {
+ return a.eq(b, precision);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/PointSet1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/PointSet1DTest.java
new file mode 100644
index 0000000..b57e0e8
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/PointSet1DTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.oned;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointSet;
+import org.apache.commons.geometry.core.collection.PointSetTestBase;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.numbers.core.Precision;
+
+class PointSet1DTest extends PointSetTestBase<Vector1D> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected PointSet<Vector1D> getSet(final Precision.DoubleEquivalence precision) {
+ return EuclideanCollections.pointSet1D(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector1D[] createPointArray() {
+ return new Vector1D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getNaNPoints() {
+ return Collections.singletonList(Vector1D.NaN);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getInfPoints() {
+ return Arrays.asList(
+ Vector1D.NEGATIVE_INFINITY,
+ Vector1D.POSITIVE_INFINITY);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getTestPoints(final int cnt, final double eps) {
+ final List<Vector1D> pts = new ArrayList<>(cnt);
+
+ final double delta = 10 * eps;
+
+ double x = 0.0;
+ for (int i = 0; i < cnt; ++i) {
+ pts.add(Vector1D.of(x));
+
+ x += delta;
+ }
+
+ return pts;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector1D> getTestPointsAtDistance(final Vector1D pt, final double dist) {
+ return Arrays.asList(
+ Vector1D.of(pt.getX() - dist),
+ Vector1D.of(pt.getX() + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final Vector1D a, final Vector1D b, final Precision.DoubleEquivalence precision) {
+ return a.eq(b, precision);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PointMap3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PointMap3DTest.java
new file mode 100644
index 0000000..37e3b1d
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PointMap3DTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.collection.PointMapTestBase;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class PointMap3DTest extends PointMapTestBase<Vector3D> {
+
+ @Test
+ void testDenseGrid() {
+ // arrange
+ final PointMap<Vector3D, Integer> map = getMap(PRECISION);
+
+ final double step = 3 * EPS;
+ final int stepsPerHalfSide = 50;
+ final double max = step * stepsPerHalfSide;
+ final int sideLength = (2 * stepsPerHalfSide) + 1;
+
+ // act
+ EuclideanTestUtils.permute(-max, max, step,
+ (x, y, z) -> map.put(Vector3D.of(x, y, z), 0));
+
+ // act
+ assertEquals(sideLength * sideLength * sideLength, map.size());
+
+ final double offset = 0.9 * EPS;
+ EuclideanTestUtils.permute(-max, max, step, (x, y, z) -> {
+ Assertions.assertEquals(0, map.get(Vector3D.of(x + offset, y + offset, z + offset)));
+ });
+ }
+
+ @Test
+ void testDenseLine() {
+ // arrange
+ final PointMap<Vector3D, Integer> map = getMap(PRECISION);
+
+ final double step = 1.1 * EPS;
+ final double start = -1.0;
+ final int cnt = 10_000;
+
+ // act
+ double x = start;
+ for (int i = 0; i < cnt; ++i) {
+ map.put(Vector3D.of(x, 0, 0), 0);
+
+ x += step;
+ }
+
+ // act
+ assertEquals(cnt, map.size());
+
+ final double offset = 0.9 * EPS;
+ x = start;
+ for (int i = 0; i < cnt; ++i) {
+ Assertions.assertEquals(0, map.get(Vector3D.of(x + offset, 0, 0)));
+
+ x += step;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected <V> PointMap<Vector3D, V> getMap(final Precision.DoubleEquivalence precision) {
+ return EuclideanCollections.pointMap3D(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector3D[] createPointArray() {
+ return new Vector3D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getNaNPoints() {
+ return Arrays.asList(
+ Vector3D.NaN,
+ Vector3D.of(Double.NaN, 0, 0),
+ Vector3D.of(0, Double.NaN, 0),
+ Vector3D.of(0, 0, Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getInfPoints() {
+ return Arrays.asList(
+ Vector3D.NEGATIVE_INFINITY,
+ Vector3D.POSITIVE_INFINITY,
+
+ Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0),
+ Vector3D.of(0, Double.NEGATIVE_INFINITY, 0),
+ Vector3D.of(0, 0, Double.NEGATIVE_INFINITY),
+
+ Vector3D.of(Double.POSITIVE_INFINITY, 0, 0),
+ Vector3D.of(0, Double.POSITIVE_INFINITY, 0),
+ Vector3D.of(0, 0, Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getTestPoints(final int cnt, final double eps) {
+ final List<Vector3D> pts = new ArrayList<>(cnt);
+
+ final double delta = 10 * eps;
+
+ double x = 0.0;
+ double y = 0.0;
+ double z = 0.0;
+ for (int i = 0; i < cnt; ++i) {
+
+ pts.add(Vector3D.of(x, y, z));
+
+ final int m = i % 3;
+ if (m == 0) {
+ x += delta;
+ } else if (m == 1) {
+ y += delta;
+ } else {
+ z += delta;
+ }
+ }
+
+ return pts;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getTestPointsAtDistance(final Vector3D pt, final double dist) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+ final double z = pt.getZ();
+
+ return Arrays.asList(
+ Vector3D.of(x - dist, y, z),
+ Vector3D.of(x + dist, y, z),
+
+ Vector3D.of(x, y - dist, z),
+ Vector3D.of(x, y + dist, z),
+
+ Vector3D.of(x, y, z - dist),
+ Vector3D.of(x, y, z + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final Vector3D a, final Vector3D b, final Precision.DoubleEquivalence precision) {
+ return a.eq(b, precision);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PointSet3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PointSet3DTest.java
new file mode 100644
index 0000000..e6bf890
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PointSet3DTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointSet;
+import org.apache.commons.geometry.core.collection.PointSetTestBase;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.numbers.core.Precision;
+
+class PointSet3DTest extends PointSetTestBase<Vector3D> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected PointSet<Vector3D> getSet(final Precision.DoubleEquivalence precision) {
+ return EuclideanCollections.pointSet3D(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector3D[] createPointArray() {
+ return new Vector3D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getNaNPoints() {
+ return Arrays.asList(
+ Vector3D.NaN,
+ Vector3D.of(Double.NaN, 0, 0),
+ Vector3D.of(0, Double.NaN, 0),
+ Vector3D.of(0, 0, Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getInfPoints() {
+ return Arrays.asList(
+ Vector3D.NEGATIVE_INFINITY,
+ Vector3D.POSITIVE_INFINITY,
+
+ Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0),
+ Vector3D.of(0, Double.NEGATIVE_INFINITY, 0),
+ Vector3D.of(0, 0, Double.NEGATIVE_INFINITY),
+
+ Vector3D.of(Double.POSITIVE_INFINITY, 0, 0),
+ Vector3D.of(0, Double.POSITIVE_INFINITY, 0),
+ Vector3D.of(0, 0, Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getTestPoints(final int cnt, final double eps) {
+ final List<Vector3D> pts = new ArrayList<>(cnt);
+
+ final double delta = 10 * eps;
+
+ double x = 0.0;
+ double y = 0.0;
+ double z = 0.0;
+ for (int i = 0; i < cnt; ++i) {
+
+ pts.add(Vector3D.of(x, y, z));
+
+ final int m = i % 3;
+ if (m == 0) {
+ x += delta;
+ } else if (m == 1) {
+ y += delta;
+ } else {
+ z += delta;
+ }
+ }
+
+ return pts;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector3D> getTestPointsAtDistance(final Vector3D pt, final double dist) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+ final double z = pt.getZ();
+
+ return Arrays.asList(
+ Vector3D.of(x - dist, y, z),
+ Vector3D.of(x + dist, y, z),
+
+ Vector3D.of(x, y - dist, z),
+ Vector3D.of(x, y + dist, z),
+
+ Vector3D.of(x, y, z - dist),
+ Vector3D.of(x, y, z + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final Vector3D a, final Vector3D b, final Precision.DoubleEquivalence precision) {
+ return a.eq(b, precision);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PointMap2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PointMap2DTest.java
new file mode 100644
index 0000000..0a06826
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PointMap2DTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.collection.PointMapTestBase;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.numbers.core.Precision.DoubleEquivalence;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class PointMap2DTest extends PointMapTestBase<Vector2D> {
+
+ @Test
+ void testDenseGrid() {
+ // arrange
+ final PointMap<Vector2D, Integer> map = getMap(PRECISION);
+
+ final double step = 3 * EPS;
+ final int stepsPerHalfSide = 100;
+ final double max = step * stepsPerHalfSide;
+ final int sideLength = (2 * stepsPerHalfSide) + 1;
+
+ // act
+ EuclideanTestUtils.permute(-max, max, step,
+ (x, y, z) -> map.put(Vector2D.of(x, y), 0));
+
+ // act
+ assertEquals(sideLength * sideLength, map.size());
+
+ final double offset = 0.9 * EPS;
+ EuclideanTestUtils.permute(-max, max, step, (x, y) -> {
+ Assertions.assertEquals(0, map.get(Vector2D.of(x + offset, y + offset)));
+ });
+ }
+
+ @Test
+ void testDenseLine() {
+ // arrange
+ final PointMap<Vector2D, Integer> map = getMap(PRECISION);
+
+ final double step = 1.1 * EPS;
+ final double start = -1.0;
+ final int cnt = 10_000;
+
+ // act
+ double x = start;
+ for (int i = 0; i < cnt; ++i) {
+ map.put(Vector2D.of(x, 0), 0);
+
+ x += step;
+ }
+
+ // act
+ assertEquals(cnt, map.size());
+
+ final double offset = 0.9 * EPS;
+ x = start;
+ for (int i = 0; i < cnt; ++i) {
+ Assertions.assertEquals(0, map.get(Vector2D.of(x + offset, 0)));
+
+ x += step;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected <V> PointMap<Vector2D, V> getMap(final DoubleEquivalence precision) {
+ return EuclideanCollections.pointMap2D(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector2D[] createPointArray() {
+ return new Vector2D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getNaNPoints() {
+ return Arrays.asList(
+ Vector2D.NaN,
+ Vector2D.of(Double.NaN, 0),
+ Vector2D.of(0, Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getInfPoints() {
+ return Arrays.asList(
+ Vector2D.NEGATIVE_INFINITY,
+ Vector2D.POSITIVE_INFINITY,
+
+ Vector2D.of(Double.NEGATIVE_INFINITY, 0),
+ Vector2D.of(0, Double.NEGATIVE_INFINITY),
+
+ Vector2D.of(Double.POSITIVE_INFINITY, 0),
+ Vector2D.of(0, Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getTestPoints(final int cnt, final double eps) {
+ final List<Vector2D> pts = new ArrayList<>(cnt);
+
+ final double delta = 10 * eps;
+
+ double x = 0.0;
+ double y = 0.0;
+ for (int i = 0; i < cnt; ++i) {
+
+ pts.add(Vector2D.of(x, y));
+
+ final int m = i % 2;
+ if (m == 0) {
+ x += delta;
+ } else {
+ y += delta;
+ }
+ }
+
+ return pts;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getTestPointsAtDistance(final Vector2D pt, final double dist) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+
+ return Arrays.asList(
+ Vector2D.of(x - dist, y),
+ Vector2D.of(x + dist, y),
+
+ Vector2D.of(x, y - dist),
+ Vector2D.of(x, y + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final Vector2D a, final Vector2D b, final Precision.DoubleEquivalence precision) {
+ return a.eq(b, precision);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PointSet2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PointSet2DTest.java
new file mode 100644
index 0000000..de79314
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PointSet2DTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointSet;
+import org.apache.commons.geometry.core.collection.PointSetTestBase;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.numbers.core.Precision.DoubleEquivalence;
+
+class PointSet2DTest extends PointSetTestBase<Vector2D> {
+
+ /** {@inheritDoc} */
+ @Override
+ protected PointSet<Vector2D> getSet(final DoubleEquivalence precision) {
+ return EuclideanCollections.pointSet2D(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Vector2D[] createPointArray() {
+ return new Vector2D[0];
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getNaNPoints() {
+ return Arrays.asList(
+ Vector2D.NaN,
+ Vector2D.of(Double.NaN, 0),
+ Vector2D.of(0, Double.NaN));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getInfPoints() {
+ return Arrays.asList(
+ Vector2D.NEGATIVE_INFINITY,
+ Vector2D.POSITIVE_INFINITY,
+
+ Vector2D.of(Double.NEGATIVE_INFINITY, 0),
+ Vector2D.of(0, Double.NEGATIVE_INFINITY),
+
+ Vector2D.of(Double.POSITIVE_INFINITY, 0),
+ Vector2D.of(0, Double.POSITIVE_INFINITY));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getTestPoints(final int cnt, final double eps) {
+ final List<Vector2D> pts = new ArrayList<>(cnt);
+
+ final double delta = 10 * eps;
+
+ double x = 0.0;
+ double y = 0.0;
+ for (int i = 0; i < cnt; ++i) {
+
+ pts.add(Vector2D.of(x, y));
+
+ final int m = i % 2;
+ if (m == 0) {
+ x += delta;
+ } else {
+ y += delta;
+ }
+ }
+
+ return pts;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected List<Vector2D> getTestPointsAtDistance(final Vector2D pt, final double dist) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+
+ return Arrays.asList(
+ Vector2D.of(x - dist, y),
+ Vector2D.of(x + dist, y),
+
+ Vector2D.of(x, y - dist),
+ Vector2D.of(x, y + dist));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean eq(final Vector2D a, final Vector2D b, final Precision.DoubleEquivalence precision) {
+ return a.eq(b, precision);
+ }
+}
diff --git a/commons-geometry-examples/examples-jmh/pom.xml b/commons-geometry-examples/examples-jmh/pom.xml
index 2e02769..c50ea4e 100644
--- a/commons-geometry-examples/examples-jmh/pom.xml
+++ b/commons-geometry-examples/examples-jmh/pom.xml
@@ -92,6 +92,15 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-geometry-euclidean</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<profiles>
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/PointMap3DPerformance.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/PointMap3DPerformance.java
new file mode 100644
index 0000000..01d3802
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/PointMap3DPerformance.java
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
+import org.apache.commons.geometry.euclidean.threed.SphericalCoordinates;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.angle.Angle;
+import org.apache.commons.numbers.core.Precision;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/** Benchmarks for the 3D Euclidean
+ * {@link org.apache.commons.geometry.core.collection.PointMap PointMap} implementation.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"})
+public class PointMap3DPerformance {
+
+ /** Precision context. */
+ private static final Precision.DoubleEquivalence PRECISION =
+ Precision.doubleEquivalenceOfEpsilon(1e-6);
+
+ /** Value inserted into maps during runs. */
+ private static final Integer VAL = Integer.valueOf(1);
+
+ /** Base input class for point map benchmarks. */
+ @State(Scope.Thread)
+ public static class PointMapInput {
+
+ /** Data structure implementation. */
+ @Param({"treemap", "pointmap3d"})
+ private String impl;
+
+ /** Point list shape. */
+ @Param({"block", "line", "sphere"})
+ private String shape;
+
+ /** Point distribution. */
+ @Param({"none", "random", "ordered"})
+ private String dist;
+
+ /** Seed value for randomization. */
+ @Param({"1"})
+ private int randomSeed;
+
+ /** Map instance for the run. */
+ private Map<Vector3D, Integer> map;
+
+ /** List of points for the run. */
+ private List<Vector3D> points;
+
+ /** Random instance. */
+ private Random random;
+
+ /** Set up the instance for the benchmark. */
+ @Setup(Level.Iteration)
+ public void setup() {
+ random = new Random(randomSeed);
+
+ map = createMap();
+ points = createPoints();
+
+ switch (dist) {
+ case "none":
+ break;
+ case "random":
+ Collections.shuffle(points, random);
+ break;
+ case "ordered":
+ Collections.sort(points, Vector3D.COORDINATE_ASCENDING_ORDER);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown distribution: " + dist);
+ }
+ }
+
+ /** Get the map instance under test.
+ * @return map instance
+ */
+ public Map<Vector3D, Integer> getMap() {
+ return map;
+ }
+
+ /** Get the points for the run.
+ * @return list of points
+ */
+ public List<Vector3D> getPoints() {
+ return points;
+ }
+
+ /** Get the random number generator for the instance.
+ * @return random number generate
+ */
+ public Random getRandom() {
+ return random;
+ }
+
+ /** Create the map implementation for the run.
+ * @return map instance
+ */
+ private Map<Vector3D, Integer> createMap() {
+ switch (impl.toLowerCase()) {
+ case "treemap":
+ return new TreeMap<>((a, b) -> {
+ int cmp = PRECISION.compare(a.getX(), b.getX());
+ if (cmp == 0) {
+ cmp = PRECISION.compare(a.getY(), b.getY());
+ if (cmp == 0) {
+ cmp = PRECISION.compare(a.getZ(), b.getZ());
+ }
+ }
+ return cmp;
+ });
+ case "pointmap3d":
+ return EuclideanCollections.pointMap3D(PRECISION);
+ default:
+ throw new IllegalArgumentException("Unknown map implementation: " + impl);
+ }
+ }
+
+ /** Create the list of points for the run.
+ * @return list of points
+ */
+ private List<Vector3D> createPoints() {
+ switch (shape.toLowerCase()) {
+ case "block":
+ return createPointBlock(20, 1);
+ case "line":
+ return createLine(8_000, 1);
+ case "sphere":
+ return createPointSphere(5, 5, 10);
+ default:
+ throw new IllegalArgumentException("Unknown point distribution " + impl);
+ }
+ }
+ }
+
+ /** Input class containing pre-inserted points. */
+ @State(Scope.Thread)
+ public static class PreInsertedPointMapInput extends PointMapInput {
+
+ /** List of test points. */
+ private List<Vector3D> testPoints;
+
+ /** {@inheritDoc} */
+ @Override
+ @Setup(Level.Iteration)
+ public void setup() {
+ super.setup();
+
+ final List<Vector3D> pts = getPoints();
+
+ // add the points to the map
+ final Map<Vector3D, Integer> map = getMap();
+ for (final Vector3D pt : pts) {
+ map.put(pt, VAL);
+ }
+
+ // compute test points
+ testPoints = new ArrayList<>(pts.size() * 2);
+ testPoints.addAll(pts);
+
+ final Random rnd = getRandom();
+ final Bounds3D bounds = Bounds3D.from(pts);
+ final Vector3D diag = bounds.getDiagonal();
+ for (int i = 0; i < pts.size(); ++i) {
+ testPoints.add(Vector3D.of(
+ bounds.getMin().getX() + (rnd.nextDouble() * diag.getX()),
+ bounds.getMin().getY() + (rnd.nextDouble() * diag.getY()),
+ bounds.getMin().getZ() + (rnd.nextDouble() * diag.getZ())));
+ }
+
+ Collections.shuffle(testPoints, rnd);
+ }
+
+ /** Get a list of test points to look for in the map. The
+ * returned list contains 2x the number of points in the map,
+ * with half equal to map entries and half random.
+ * @return list of test points
+ */
+ public List<Vector3D> getTestPoints() {
+ return testPoints;
+ }
+ }
+
+ /** Create a solid block of points.
+ * @param pointsPerSide number of points along each side
+ * @param spacing spacing between each point
+ * @return list of points in a block
+ */
+ private static List<Vector3D> createPointBlock(final int pointsPerSide, final double spacing) {
+ final List<Vector3D> points = new ArrayList<>(pointsPerSide * pointsPerSide * pointsPerSide);
+
+ for (int x = 0; x < pointsPerSide; ++x) {
+ for (int y = 0; y < pointsPerSide; ++y) {
+ for (int z = 0; z < pointsPerSide; ++z) {
+ points.add(Vector3D.of(x, y, z).multiply(spacing));
+ }
+ }
+ }
+
+ return points;
+ }
+
+ /** Create a line of points.
+ * @param count number of points
+ * @param spacing spacing between each point
+ * @return list of points in a lin
+ */
+ private static List<Vector3D> createLine(final int count, final double spacing) {
+ final List<Vector3D> points = new ArrayList<>(count);
+
+ final Vector3D base = Vector3D.of(2.0, 1.0, 0.5);
+ for (int i = 0; i < count; ++i) {
+ points.add(base.multiply(i));
+ }
+
+ return points;
+ }
+
+ /** Create a hollow sphere of points.
+ * @param slices number of sections in the x-y plane, not counting the poles
+ * @param segments number of section perpendicular to the x-y plane for each slice
+ * @param radius sphere radius
+ * @return list of points in a hollow sphere
+ */
+ private static List<Vector3D> createPointSphere(final int slices, final int segments, final double radius) {
+ final List<Vector3D> points = new ArrayList<>();
+
+ final double polarDelta = Math.PI / (slices + 1);
+ final double azDelta = Angle.TWO_PI / segments;
+
+ // add the top pole
+ points.add(Vector3D.of(0, 0, radius));
+
+ // add the lines of latitude
+ for (int i = 1; i <= slices; ++i) {
+ for (int j = 0; j < segments; ++j) {
+ final SphericalCoordinates coords = SphericalCoordinates.of(
+ radius,
+ j * azDelta,
+ i * polarDelta);
+
+ points.add(coords.toVector());
+ }
+ }
+
+ // add the bottom pole
+ points.add(Vector3D.of(0, 0, -radius));
+
+ return points;
+ }
+
+ /** Benchmark that inserts each point in the input into the target map.
+ * @param input input for the run
+ * @param bh blackhole instance
+ * @return input instance
+ */
+ @Benchmark
+ public PointMapInput put(final PointMapInput input, final Blackhole bh) {
+ final Map<Vector3D, Integer> map = input.getMap();
+
+ for (final Vector3D p : input.getPoints()) {
+ bh.consume(map.put(p, VAL));
+ }
+
+ return input;
+ }
+
+ /** Benchmark that retrieves each point in the input from the target map.
+ * @param input input for the run
+ * @param bh blackhole instance
+ * @return input instance
+ */
+ @Benchmark
+ public PointMapInput get(final PreInsertedPointMapInput input, final Blackhole bh) {
+ final Map<Vector3D, Integer> map = input.getMap();
+
+ for (final Vector3D p : input.getTestPoints()) {
+ bh.consume(map.get(p));
+ }
+
+ return input;
+ }
+
+ /** Benchmark that remove each point in the input from the target map.
+ * @param input input for the run
+ * @param bh blackhole instance
+ * @return input instance
+ */
+ @Benchmark
+ public PointMapInput remove(final PreInsertedPointMapInput input, final Blackhole bh) {
+ final Map<Vector3D, Integer> map = input.getMap();
+
+ for (final Vector3D p : input.getPoints()) {
+ bh.consume(map.remove(p));
+ }
+
+ return input;
+ }
+}
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/BucketKDTree.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/BucketKDTree.java
new file mode 100644
index 0000000..7d29e9e
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/BucketKDTree.java
@@ -0,0 +1,984 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.ToDoubleFunction;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.internal.GeometryInternalUtils;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision;
+
+/**
+ * KD-tree implementation that buffers vector entries at leaf nodes
+ * and then splits on the median of the buffer contents when enough
+ * nodes are collected. This guarantees that the tree has at least some
+ * branching.
+ * @param <V> Value type
+ */
+public class BucketKDTree<V> extends AbstractMap<Vector3D, V> {
+
+ /** Token used in debug tree strings. */
+ private static final String TREE_STRING_EQ_TOKEN = " => ";
+
+ /** Number of map entries stored in leaf nodes before splitting. */
+ private static final int ENTRY_BUFFER_SIZE = 10;
+
+ /** Precision context. */
+ private final Precision.DoubleEquivalence precision;
+
+ /** Root node; not null. */
+ private Node<V> root;
+
+ /** Tree node count. */
+ private int nodeCount;
+
+ /** Construct a new instance with the given precision.
+ * @param precision object used to determine floating point equality between dimension
+ * coordinates
+ */
+ public BucketKDTree(final Precision.DoubleEquivalence precision) {
+ this.precision = precision;
+ this.root = new BucketNode<>(this, null);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return nodeCount;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V put(final Vector3D key, final V value) {
+ validateKey(key);
+
+ final FindOrInsertResult<V> result = root.findOrInsert(key);
+ if (result.isNewEntry()) {
+ ++nodeCount;
+ }
+
+ final Vector3DEntry<V> entry = result.getEntry();
+ final V prevValue = entry.getValue();
+ entry.setValue(value);
+
+ return prevValue;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ final Vector3DEntry<V> entry = root.find((Vector3D) key);
+ return entry != null ?
+ entry.getValue() :
+ null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ final Vector3DEntry<V> entry = root.remove((Vector3D) key);
+ if (entry != null) {
+ --nodeCount;
+
+ root.condense();
+
+ return entry.getValue();
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<Vector3D, V>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** Return a string representation of this tree for debugging purposes.
+ * @return a string representation of this tree
+ */
+ public String treeString() {
+ final StringBuilder sb = new StringBuilder();
+
+ root.treeString(0, sb);
+
+ return sb.toString();
+ }
+
+ /** Throw an exception if {@code key} is invalid.
+ * @param key map key
+ */
+ private void validateKey(final Vector3D key) {
+ Objects.requireNonNull(key);
+ if (!key.isFinite()) {
+ throw new IllegalArgumentException("Map key must be finite; was " + key);
+ }
+ }
+
+ /** Enum containing possible node cut dimensions. */
+ private enum CutDimension {
+
+ /** X dimension. */
+ X(Vector3D::getX),
+
+ /** Y dimension. */
+ Y(Vector3D::getY),
+
+ /** Z dimension. */
+ Z(Vector3D::getZ);
+
+ /** Coordinate extraction function. */
+ private final ToDoubleFunction<Vector3D> coordinateFn;
+
+ CutDimension(final ToDoubleFunction<Vector3D> coordinateFn) {
+ this.coordinateFn = coordinateFn;
+ }
+
+ /** Get the coordinate for this dimension.
+ * @param pt point
+ * @return dimension coordinate
+ */
+ public double getCoordinate(final Vector3D pt) {
+ return coordinateFn.applyAsDouble(pt);
+ }
+ }
+
+ /** Interface representing a reference to single entry in the tree.
+ * @param <V> Value type
+ */
+ private interface EntryReference<V> {
+
+ /** Get the entry.
+ * @return the entry
+ */
+ Vector3DEntry<V> getEntry();
+
+ /** Remove the referenced entry from the tree.
+ * @return the removed entry
+ */
+ Vector3DEntry<V> remove();
+ }
+
+ /** Class encapsulating the result of a find-or-insert operation.
+ * @param <V> Value type
+ */
+ private static final class FindOrInsertResult<V> {
+ /** Map entry. */
+ private final Vector3DEntry<V> entry;
+
+ /** Flag indicating if the entry is new. */
+ private final boolean newEntry;
+
+ FindOrInsertResult(final Vector3DEntry<V> entry, final boolean newEntry) {
+ this.entry = entry;
+ this.newEntry = newEntry;
+ }
+
+ /** Return the map entry.
+ * @return map entry
+ */
+ public Vector3DEntry<V> getEntry() {
+ return entry;
+ }
+
+ /** Return true if the map entry is new.
+ * @return true if the map entry is new
+ */
+ public boolean isNewEntry() {
+ return newEntry;
+ }
+
+ /** Construct a new result instance representing an existing map entry.
+ * @param <V> Value type
+ * @param entry existing entry
+ * @return instance representing an existing map entry
+ */
+ public static <V> FindOrInsertResult<V> existingEntry(final Vector3DEntry<V> entry) {
+ return new FindOrInsertResult<>(entry, false);
+ }
+
+ /** Construct a new result instance representing a new map entry.
+ * @param <V> Value type
+ * @param entry new map entry
+ * @return instance representing a new map entry
+ */
+ public static <V> FindOrInsertResult<V> newEntry(final Vector3DEntry<V> entry) {
+ return new FindOrInsertResult<>(entry, true);
+ }
+ }
+
+ /** Abstract base class for bucket kd-tree nodes.
+ * @param <V> Value type
+ */
+ private abstract static class Node<V> {
+
+ /** Owning tree. */
+ protected final BucketKDTree<V> tree;
+
+ /** Parent node; null for root node. */
+ protected CutNode<V> parent;
+
+ /** True if the node needs to be condensed. */
+ protected boolean requiresCondense;
+
+ Node(final BucketKDTree<V> tree, final CutNode<V> parent) {
+ this.tree = tree;
+ this.parent = parent;
+ }
+
+ /** Return the map entry equivalent to the given key or null if not
+ * found.
+ * @param key key to search for
+ * @return entry equivalent to {@code key} or null if not found
+ */
+ public abstract Vector3DEntry<V> find(Vector3D key);
+
+ /** Return the map entry equivalent to the given key. An entry is created
+ * if one does not exist.
+ * @param key map key
+ * @return result of the operation
+ */
+ public abstract FindOrInsertResult<V> findOrInsert(Vector3D key);
+
+ /** Find the entry in the subtree rooted at this node containing the minimum value measured
+ * along the cut dimension.
+ * @param dim dimension to measure along
+ * @return the entry containing the minimum value measured along the cut dimension
+ */
+ public abstract EntryReference<V> findMin(CutDimension dim);
+
+ /** Insert an entry from another portion of the tree into the subtree
+ * rooted at this node.
+ * @param entry entry to insert
+ */
+ public abstract void insertExisting(Vector3DEntry<V> entry);
+
+ /** Remove the map entry equivalent to the given key or null if one
+ * was not found.
+ * @param key map key
+ * @return the removed entry or null if not found
+ */
+ public abstract Vector3DEntry<V> remove(Vector3D key);
+
+ /** Mark this node and its ancestors as requiring condensing.
+ */
+ public void markRequiresCondense() {
+ Node<V> node = this;
+ while (node != null && !node.requiresCondense) {
+ node.requiresCondense = true;
+ node = node.parent;
+ }
+ }
+
+ /** Condense the subtree rooted at this node if possible.
+ */
+ public abstract void condense();
+
+ /** Return a string representation of the subtree rooted at this node.
+ * @param depth depth of this node
+ * @param sb string builder to append content to
+ */
+ public void treeString(final int depth, final StringBuilder sb) {
+ for (int i = 0; i < depth; ++i) {
+ sb.append(" ");
+ }
+
+ String label = parent == null ?
+ "*" :
+ isLeftChild() ? "L" : "R";
+
+ sb.append("[")
+ .append(label);
+ }
+
+ /** Validate the state of the tree.
+ */
+ public abstract void validate();
+
+ /** Return true if the entry and key are equivalent according to the map
+ * precision context.
+ * @param entry map entry
+ * @param key key value
+ * @return true if the entry and key are equivalent
+ */
+ protected boolean eq(final Vector3DEntry<V> entry, final Vector3D key) {
+ return entry.getKey().eq(key, tree.precision);
+ }
+
+ /** Compare the given values using the precision context of the map.
+ * @param a first value
+ * @param b second value
+ * @return comparison result
+ */
+ protected int eqCompare(final double a, final double b) {
+ return tree.precision.compare(a, b);
+ }
+
+ /** Replace this node with the given node.
+ * @param node replacement node; may be null
+ */
+ protected void replaceSelf(final Node<V> node) {
+ if (parent == null) {
+ // this is the root
+ if (node != null) {
+ node.parent = null;
+ }
+
+ tree.root = node;
+ } else {
+ if (node != null) {
+ node.parent = parent;
+ }
+
+ if (isLeftChild()) {
+ parent.left = node;
+ } else {
+ parent.right = node;
+ }
+ }
+ }
+
+ /** Return true if this node is the left child of its parent.
+ * @return true if this node is the left child of its parent
+ */
+ protected boolean isLeftChild() {
+ return parent != null && GeometryInternalUtils.sameInstance(parent.left, this);
+ }
+ }
+
+ /** Internal tree node containing a single entry and a cut dimension.
+ * @param <V> Value type
+ */
+ private static final class CutNode<V> extends Node<V> {
+
+ /** Map entry. */
+ private Vector3DEntry<V> entry;
+
+ /** Left child node; may be null. */
+ private Node<V> left;
+
+ /** Right child node; may be null. */
+ private Node<V> right;
+
+ /** Cut dimension; not null. */
+ private CutDimension cutDimension;
+
+ CutNode(final BucketKDTree<V> tree, final CutNode<V> parent, final Vector3DEntry<V> entry,
+ final CutDimension cutDimension) {
+ super(tree, parent);
+
+ this.entry = entry;
+ this.cutDimension = cutDimension;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> find(final Vector3D key) {
+ // pull the coordinates for the node cut dimension
+ final double nodeCoord = cutDimension.getCoordinate(entry.getKey());
+ final double keyCoord = cutDimension.getCoordinate(key);
+
+ // perform an equivalence comparison
+ final int eqcmp = eqCompare(keyCoord, nodeCoord);
+
+ if (eqcmp < 0) {
+ return left != null ?
+ left.find(key) :
+ null;
+ } else if (eqcmp > 0) {
+ return right != null ?
+ right.find(key) :
+ null;
+ } else if (eq(entry, key)) {
+ // our entry key is equivalent to the search key
+ return entry;
+ } else {
+ // Not equivalent; the matching node (if any) could be on either
+ // side of the cut so we'll need to search both subtrees. Since points with
+ // cut dimension coordinates are always inserted into the right subtree,
+ // search that subtree first.
+ final Vector3DEntry<V> rightSearchResult = right != null ?
+ right.find(key) :
+ null;
+ if (rightSearchResult != null) {
+ return rightSearchResult;
+ }
+
+ return left != null ?
+ left.find(key) :
+ null;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public FindOrInsertResult<V> findOrInsert(final Vector3D key) {
+ // pull the coordinates for the node cut dimension
+ final double nodeCoord = cutDimension.getCoordinate(entry.getKey());
+ final double keyCoord = cutDimension.getCoordinate(key);
+
+ // perform an equivalence comparison
+ final int eqCmp = eqCompare(keyCoord, nodeCoord);
+
+ if (eqCmp < 0) {
+ // we definitely belong in the left subtree
+ return getOrCreateLeft().findOrInsert(key);
+ } else if (eqCmp > 0) {
+ // we definitely belong in the right subtree
+ return getOrCreateRight().findOrInsert(key);
+ } else if (eq(entry, key)) {
+ // our entry key is equivalent to the search key
+ return FindOrInsertResult.existingEntry(entry);
+ } else {
+ // We are not equivalent and we straddle the cut line for this node,
+ // meaning that an existing node equivalent to the key could be on either
+ // side of the cut. Perform a strict comparison to determine where the
+ // node would be inserted if we were inserting. Then check the opposite
+ // subtree for an existing node and if not found, go ahead and try inserting
+ // into the target subtree.
+ final int strictCmp = Double.compare(keyCoord, nodeCoord);
+ if (strictCmp < 0) {
+ // insertion, if needed, would be performed in the left subtree, so
+ // check the right subtree first
+ final Vector3DEntry<V> rightExisting = right != null ?
+ right.find(key) :
+ null;
+ if (rightExisting != null) {
+ return FindOrInsertResult.existingEntry(rightExisting);
+ }
+
+ return getOrCreateLeft().findOrInsert(key);
+ } else {
+ // insertion, if needed, would be performed in the right subtree, so
+ // check the left subtree first
+ final Vector3DEntry<V> leftExisting = left != null ?
+ left.find(key) :
+ null;
+ if (leftExisting != null) {
+ return FindOrInsertResult.existingEntry(leftExisting);
+ }
+
+ return getOrCreateRight().findOrInsert(key);
+ }
+ }
+ }
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> remove(final Vector3D key) {
+ // pull the coordinates for the node cut dimension
+ final double nodeCoord = cutDimension.getCoordinate(entry.getKey());
+ final double keyCoord = cutDimension.getCoordinate(key);
+
+ // perform an equivalence comparison
+ final int eqcmp = eqCompare(keyCoord, nodeCoord);
+
+ if (eqcmp < 0) {
+ return left != null ?
+ left.remove(key) :
+ null;
+ } else if (eqcmp > 0) {
+ return right != null ?
+ right.remove(key) :
+ null;
+ } else if (eq(entry, key)) {
+ return removeOwnEntry();
+ } else {
+ // Not equivalent; the matching node (if any) could be on either
+ // side of the cut so we'll need to search both subtrees. Use the side
+ // closest to the key as the first to search.
+ final Node<V> first;
+ final Node<V> second;
+ if (Double.compare(keyCoord, nodeCoord) < 0) {
+ first = left;
+ second = right;
+ } else {
+ first = right;
+ second = left;
+ }
+
+ final Vector3DEntry<V> firstRemoveResult = first != null ?
+ first.remove(key) :
+ null;
+ if (firstRemoveResult != null) {
+ return firstRemoveResult;
+ }
+
+ return second != null ?
+ second.remove(key) :
+ null;
+ }
+ }
+
+ /** Remove the entry for this cut node and replace it with an appropriate
+ * value from a child node.
+ * @return the entry removed from this node.
+ */
+ private Vector3DEntry<V> removeOwnEntry() {
+ final Vector3DEntry<V> result = entry;
+ entry = null;
+
+ if (right != null) {
+ entry = right.findMin(cutDimension).remove();
+ } else if (left != null) {
+ // swap left and right subtrees; the replacement entry will
+ // contain the minimum of the entire subtree
+ entry = left.findMin(cutDimension).remove();
+
+ right = left;
+ left = null;
+ }
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EntryReference<V> findMin(final CutDimension dim) {
+ if (cutDimension.equals(dim)) {
+ // this node splits on the dimensions we're searching for, so we
+ // only need to search the left subtree
+ if (left == null) {
+ return new CutNodeEntryReference<>(this);
+ }
+ return left.findMin(cutDimension);
+ } else {
+ // search both subtrees for the minimum value
+ final EntryReference<V> leftMin = left != null ?
+ left.findMin(dim) :
+ null;
+ final EntryReference<V> rightMin = right != null ?
+ right.findMin(dim) :
+ null;
+
+ return minResult(
+ leftMin,
+ new CutNodeEntryReference<>(this),
+ rightMin,
+ dim);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void condense() {
+ if (requiresCondense) {
+ // condense children
+ if (left != null) {
+ left.condense();
+ }
+ if (right != null) {
+ right.condense();
+ }
+
+ if (entry == null) {
+ // no entries in this subtree; remove completely
+ replaceSelf(null);
+ } else if (right == null && left == null) {
+ // no more children; convert to a bucket node
+ final BucketNode<V> bucket = new BucketNode<>(tree, null);
+ bucket.entries.add(entry);
+
+ replaceSelf(bucket);
+ }
+ }
+
+ requiresCondense = false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void insertExisting(final Vector3DEntry<V> newEntry) {
+ // pull the coordinates for the node cut dimension
+ final double nodeCoord = cutDimension.getCoordinate(entry.getKey());
+ final double newCoord = cutDimension.getCoordinate(newEntry.getKey());
+
+ // perform an equivalence comparison
+ final int eqCmp = eqCompare(newCoord, nodeCoord);
+
+ if (eqCmp < 0) {
+ // we definitely belong in the left subtree
+ getOrCreateLeft().insertExisting(newEntry);
+ } else if (eqCmp > 0) {
+ // we definitely belong in the right subtree
+ getOrCreateRight().insertExisting(newEntry);
+ } else {
+ // We are not equivalent and we straddle the cut line for this node,
+ // meaning that an existing node equivalent to the key could be on either
+ // side of the cut. Perform a strict comparison to determine where the
+ // node would be inserted if we were inserting. Then check, search the
+ // opposite subtree for an existing node and if not found, go ahead and
+ // try inserting into the target subtree.
+ final int strictCmp = Double.compare(newCoord, nodeCoord);
+ if (strictCmp < 0) {
+ getOrCreateLeft().insertExisting(newEntry);
+ } else {
+ getOrCreateRight().insertExisting(newEntry);
+ }
+ }
+ }
+
+ /** Get or create the left child node.
+ * @return left child node
+ */
+ private Node<V> getOrCreateLeft() {
+ if (left == null) {
+ left = new BucketNode<>(tree, this);
+ }
+ return left;
+ }
+
+ /** Get or create the right child node.
+ * @return right child node
+ */
+ private Node<V> getOrCreateRight() {
+ if (right == null) {
+ right = new BucketNode<>(tree, this);
+ }
+ return right;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void treeString(final int depth, final StringBuilder sb) {
+ super.treeString(depth, sb);
+
+ sb.append(" | ")
+ .append(cutDimension)
+ .append("] ")
+ .append(entry.getKey())
+ .append(TREE_STRING_EQ_TOKEN)
+ .append(entry.getValue())
+ .append("\n");
+
+ if (left != null) {
+ left.treeString(depth + 1, sb);
+ }
+ if (right != null) {
+ right.treeString(depth + 1, sb);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void validate() {
+ if (entry == null) {
+ throw new IllegalArgumentException("Cut node entry cannot be null");
+ }
+ if (left == null && right == null) {
+ throw new IllegalArgumentException("Cut node cannot be leaf: " + entry.getKey());
+ }
+
+ if (left != null) {
+ left.validate();
+ }
+
+ if (right != null) {
+ right.validate();
+ }
+ }
+ }
+
+ /** {@link EntryReference} implementation representing the entry from a {@link CutNode}.
+ * @param <V> Value type
+ */
+ private static final class CutNodeEntryReference<V> implements EntryReference<V> {
+
+ /** Cut node instance. */
+ private final CutNode<V> node;
+
+ CutNodeEntryReference(final CutNode<V> node) {
+ this.node = node;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> getEntry() {
+ return node.entry;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> remove() {
+ final Vector3DEntry<V> result = node.entry;
+ node.removeOwnEntry();
+
+ return result;
+ }
+ }
+
+ /** Leaf node class containing multiple entries.
+ * @param <V> Value type
+ */
+ private static final class BucketNode<V> extends Node<V> {
+
+ /** List of entries. */
+ private List<Vector3DEntry<V>> entries = new ArrayList<>(ENTRY_BUFFER_SIZE);
+
+ BucketNode(final BucketKDTree<V> tree, final CutNode<V> parent) {
+ super(tree, parent);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> find(final Vector3D key) {
+ for (Vector3DEntry<V> entry : entries) {
+ if (eq(entry, key)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public FindOrInsertResult<V> findOrInsert(final Vector3D key) {
+ Vector3DEntry<V> entry = find(key);
+ if (entry != null) {
+ return FindOrInsertResult.existingEntry(entry);
+ } else {
+ // we need to create the entry
+ entry = new Vector3DEntry<>(key, null);
+
+ if (entries.size() < ENTRY_BUFFER_SIZE) {
+ entries.add(entry);
+ } else {
+ // we need to split
+ final CutNode<V> splitNode = split();
+
+ replaceSelf(splitNode);
+
+ splitNode.insertExisting(entry);
+ }
+
+ return FindOrInsertResult.newEntry(entry);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> remove(final Vector3D key) {
+ final Iterator<Vector3DEntry<V>> it = entries.iterator();
+ while (it.hasNext()) {
+ final Vector3DEntry<V> entry = it.next();
+ if (eq(entry, key)) {
+ it.remove();
+
+ checkCondense();
+
+ return entry;
+ }
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EntryReference<V> findMin(final CutDimension dim) {
+ int idx = 0;
+ int minIdx = -1;
+ double minCoord = Double.POSITIVE_INFINITY;
+
+ for (final Vector3DEntry<V> entry : entries) {
+ final double coord = dim.getCoordinate(entry.getKey());
+ if (minIdx < 0 || coord < minCoord) {
+ minIdx = idx;
+ minCoord = coord;
+ }
+
+ ++idx;
+ }
+
+ return new BucketNodeEntryReference<>(this, minIdx);
+ }
+
+ /** Check if this node requires condensing and mark it if so.
+ */
+ private void checkCondense() {
+ if (entries.isEmpty() && parent != null) {
+ markRequiresCondense();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void condense() {
+ if (requiresCondense) {
+ replaceSelf(null);
+ }
+
+ requiresCondense = false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void insertExisting(final Vector3DEntry<V> entry) {
+ entries.add(entry);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void treeString(final int depth, final StringBuilder sb) {
+ super.treeString(depth, sb);
+
+ String entryStr = entries.stream()
+ .map(e -> e.getKey() + TREE_STRING_EQ_TOKEN + e.getValue())
+ .collect(Collectors.joining(", "));
+
+ sb.append("] [")
+ .append(entryStr)
+ .append("]\n");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void validate() {
+ if (entries.isEmpty() && parent != null) {
+ throw new IllegalArgumentException("Non-root bucket node entry list is empty");
+ }
+ }
+
+ /** Split this instance into a {@link CutNode} and child nodes.
+ * @return the new cut node that should replace this instance
+ */
+ private CutNode<V> split() {
+ final CutDimension dim = determineCutDimension();
+
+ // find the median
+ entries.sort((a, b) ->
+ Double.compare(dim.getCoordinate(a.getKey()), dim.getCoordinate(b.getKey())));
+ final int medianIdx = entries.size() / 2;
+ final Vector3DEntry<V> median = entries.get(medianIdx);
+
+ // create the subtree root node on the median
+ final CutNode<V> subtree = new CutNode<>(tree, null, median, dim);
+
+ // insert the other entries
+ for (int i = 0; i < entries.size(); ++i) {
+ if (i != medianIdx) {
+ subtree.insertExisting(entries.get(i));
+ }
+ }
+
+ // clean up
+ entries.clear();
+
+ return subtree;
+ }
+
+ /** Determine the best cutting dimension to use for the current set of entries.
+ * @return split cutting dimension
+ */
+ private CutDimension determineCutDimension() {
+ final Bounds3D.Builder boundsBuilder = Bounds3D.builder();
+ for (Vector3DEntry<V> entry : entries) {
+ boundsBuilder.add(entry.getKey());
+ }
+
+ final Bounds3D bounds = boundsBuilder.build();
+ final Vector3D diff = bounds.getDiagonal();
+
+ if (diff.getX() > diff.getY()) {
+ if (diff.getX() > diff.getZ()) {
+ return CutDimension.X;
+ } else {
+ return CutDimension.Z;
+ }
+ } else if (diff.getY() > diff.getZ()) {
+ return CutDimension.Y;
+ } else {
+ return CutDimension.Z;
+ }
+ }
+ }
+
+ /** {@link EntryReference} implementation representing the entry from a {@link BucketNode}.
+ * @param <V> Value type
+ */
+ private static final class BucketNodeEntryReference<V> implements EntryReference<V> {
+
+ /** Bucket node reference. */
+ private final BucketNode<V> node;
+
+ /** Entry index. */
+ private final int idx;
+
+ BucketNodeEntryReference(final BucketNode<V> node, final int idx) {
+ this.node = node;
+ this.idx = idx;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> getEntry() {
+ return node.entries.get(idx);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3DEntry<V> remove() {
+ final Vector3DEntry<V> result = node.entries.remove(idx);
+ node.checkCondense();
+
+ return result;
+ }
+ }
+
+ /** Return the node containing the minimum value along the given cut dimension.
+ * @param <V> Value type
+ * @param a first node
+ * @param b second node
+ * @param c third node
+ * @param cutDimension search dimension
+ * @return minimum node
+ */
+ private static <V> EntryReference<V> minResult(final EntryReference<V> a, final EntryReference<V> b,
+ final EntryReference<V> c,
+ final CutDimension cutDimension) {
+ final EntryReference<V> tempMin = minResult(a, b, cutDimension);
+ return minResult(tempMin, c, cutDimension);
+ }
+
+ /** Return the node containing the minimum value along the given cut dimension. If one
+ * argument is null, the other argument is returned.
+ * @param <V> Value type
+ * @param a first node
+ * @param b second node
+ * @param cutDimension search dimension
+ * @return minimum node
+ */
+ private static <V> EntryReference<V> minResult(final EntryReference<V> a, final EntryReference<V> b,
+ final CutDimension cutDimension) {
+ if (a == null) {
+ return b;
+ } else if (b == null) {
+ return a;
+ }
+
+ final double aCoord = cutDimension.getCoordinate(a.getEntry().getKey());
+ final double bCoord = cutDimension.getCoordinate(b.getEntry().getKey());
+
+ return aCoord < bCoord ? a : b;
+ }
+}
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/KDTree.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/KDTree.java
new file mode 100644
index 0000000..c096c63
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/KDTree.java
@@ -0,0 +1,576 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.ToDoubleFunction;
+
+import org.apache.commons.geometry.core.internal.GeometryInternalUtils;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision;
+
+/**
+ * Standard kd-tree implementation with no self-balancing or rebuilding features.
+ * @param <V> Value type
+ */
+public class KDTree<V> extends AbstractMap<Vector3D, V> {
+
+ /** Enum containing possible node cut dimensions. */
+ enum CutDimension {
+
+ /** X dimension. */
+ X(Vector3D::getX),
+
+ /** Y dimension. */
+ Y(Vector3D::getY),
+
+ /** Z dimension. */
+ Z(Vector3D::getZ);
+
+ /** Coordinate extraction function. */
+ private final ToDoubleFunction<Vector3D> coordinateFn;
+
+ CutDimension(final ToDoubleFunction<Vector3D> coordinateFn) {
+ this.coordinateFn = coordinateFn;
+ }
+
+ /** Get the coordinate for this dimension.
+ * @param pt point
+ * @return dimension coordinate
+ */
+ public double getCoordinate(final Vector3D pt) {
+ return coordinateFn.applyAsDouble(pt);
+ }
+ }
+
+ /** Array of cut dimensions; pull these eagerly to avoid having to call values() constantly. */
+ private static final CutDimension[] CUT_DIMENSIONS = CutDimension.values();
+
+ /** Precision context. */
+ private final Precision.DoubleEquivalence precision;
+
+ /** Root node; may be null. */
+ private KDTreeNode<V> root;
+
+ /** Tree node count. */
+ private int nodeCount;
+
+ /** Construct a new instance with the given precision.
+ * @param precision object used to determine floating point equality between dimension
+ * coordinates
+ */
+ public KDTree(final Precision.DoubleEquivalence precision) {
+ this.precision = precision;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return nodeCount;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V put(final Vector3D key, final V value) {
+ validateKey(key);
+
+ final KDTreeNode<V> node;
+
+ if (root == null) {
+ // first node; enter as the root
+ root = createNode(null, key, 0);
+ node = root;
+ } else {
+ // not the first node; enter into the tree
+ node = findOrInsertNodeRecursive(root, key, 0);
+ }
+
+ return node.setValue(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ final KDTreeNode<V> node = findNodeRecursive(root, (Vector3D) key);
+ return node != null ?
+ node.value :
+ null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ final KDTreeNode<V> node = findNodeRecursive(root, (Vector3D) key);
+ if (node != null) {
+ final V prevValue = node.value;
+
+ removeKey(node);
+ --nodeCount;
+
+ return prevValue;
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<Vector3D, V>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** Return a string representation of this tree for debugging purposes.
+ * @return a string representation of this tree
+ */
+ public String treeString() {
+ final StringBuilder sb = new StringBuilder();
+
+ treeStringRecursive(root, 0, sb);
+
+ return sb.toString();
+ }
+
+ /** Recursively build a string representation of a subtree.
+ * @param node subtree root
+ * @param depth node depth
+ * @param sb string output
+ */
+ private void treeStringRecursive(final KDTreeNode<V> node, final int depth, final StringBuilder sb) {
+ if (node != null) {
+ for (int i = 0; i < depth; ++i) {
+ sb.append(" ");
+ }
+
+ String label = node.parent == null ?
+ "*" :
+ node.isLeftChild() ? "L" : "R";
+
+ sb.append("[")
+ .append(label)
+ .append(" | ")
+ .append(node.cutDimension)
+ .append("] ")
+ .append(node.key)
+ .append(" => ")
+ .append(node.value)
+ .append("\n");
+
+ treeStringRecursive(node.left, depth + 1, sb);
+ treeStringRecursive(node.right, depth + 1, sb);
+ }
+ }
+
+ /** Get the tree root node.
+ * @return tree root node
+ */
+ protected KDTreeNode<V> getRoot() {
+ return root;
+ }
+
+ /** Set the tree root.
+ * @param root tree root
+ */
+ protected void setRoot(final KDTreeNode<V> root) {
+ this.root = root;
+ }
+
+ /** Throw an exception if {@code key} is invalid.
+ * @param key map key
+ */
+ private void validateKey(final Vector3D key) {
+ Objects.requireNonNull(key);
+ if (!key.isFinite()) {
+ throw new IllegalArgumentException("Map key must be finite; was " + key);
+ }
+ }
+
+ /** Create a new {@link KDTreeNode} for entry into the tree and increment the internal
+ * node count.
+ * @param parent parent node
+ * @param key map key
+ * @param depth node depth
+ * @return the newly created node
+ */
+ private KDTreeNode<V> createNode(final KDTreeNode<V> parent, final Vector3D key, final int depth) {
+ final KDTreeNode<V> node = new KDTreeNode<>(
+ parent,
+ key,
+ getCutDimensionForDepth(depth));
+
+ ++nodeCount;
+
+ return node;
+ }
+
+ /** Find the node in the subtree rooted at {@code node} matching {@code key} or create it if not found.
+ * @param node subtree root
+ * @param key map key
+ * @param depth current node depth
+ * @return the existing or newly created node
+ */
+ private KDTreeNode<V> findOrInsertNodeRecursive(final KDTreeNode<V> node, final Vector3D key, final int depth) {
+ // pull the coordinates for the node cut dimension
+ final double nodeCoord = node.cutDimension.getCoordinate(node.key);
+ final double keyCoord = node.cutDimension.getCoordinate(key);
+
+ // perform an equivalence comparison
+ final int eqCmp = precision.compare(keyCoord, nodeCoord);
+
+ final int childDepth = depth + 1;
+
+ if (eqCmp < 0) {
+ // we definitely belong in the left subtree
+ if (node.left == null) {
+ node.left = createNode(node, key, childDepth);
+ return node.left;
+ } else {
+ return findOrInsertNodeRecursive(node.left, key, childDepth);
+ }
+ } else if (eqCmp > 0) {
+ // we definitely belong in the right subtree
+ if (node.right == null) {
+ node.right = createNode(node, key, childDepth);
+ return node.right;
+ } else {
+ return findOrInsertNodeRecursive(node.right, key, childDepth);
+ }
+ } else {
+ // check if we are equivalent to the point for this node
+ if (key.eq(node.key, precision)) {
+ return node;
+ }
+
+ // We are not equivalent and we straddle the cut line for this node,
+ // meaning that an existing node equivalent to the key could be on either
+ // side of the cut. Perform a strict comparison to determine where the
+ // node would be inserted if we were inserting. Then check, search the
+ // opposite subtree for an existing node and if not found, go ahead and
+ // try inserting into the target subtree.
+ final int strictCmp = Double.compare(keyCoord, nodeCoord);
+ if (strictCmp < 0) {
+ // insertion, if needed, would be performed in the left subtree, so
+ // check the right subtree first
+ final KDTreeNode<V> rightExistingNode = findNodeRecursive(node.right, key);
+ if (rightExistingNode != null) {
+ return rightExistingNode;
+ }
+
+ if (node.left == null) {
+ node.left = createNode(node, key, childDepth);
+ return node.left;
+ }
+
+ return findOrInsertNodeRecursive(node.left, key, childDepth);
+ } else {
+ // insertion, if needed, would be performed in the right subtree, so
+ // check the left subtree first
+ final KDTreeNode<V> leftExistingNode = findNodeRecursive(node.left, key);
+ if (leftExistingNode != null) {
+ return leftExistingNode;
+ }
+
+ if (node.right == null) {
+ node.right = createNode(node, key, childDepth);
+ return node.right;
+ }
+
+ return findOrInsertNodeRecursive(node.right, key, childDepth);
+ }
+ }
+ }
+
+ /** Find the node with the given {@code key} or null if not found.
+ * @param node subtree root
+ * @param key map key
+ * @return the node matching the given {@code key} or null if not found
+ */
+ private KDTreeNode<V> findNodeRecursive(final KDTreeNode<V> node, final Vector3D key) {
+ if (node != null) {
+ // pull the coordinates for the node cut dimension
+ final double nodeCoord = node.cutDimension.getCoordinate(node.key);
+ final double keyCoord = node.cutDimension.getCoordinate(key);
+
+ // perform an equivalence comparison
+ final int eqcmp = precision.compare(keyCoord, nodeCoord);
+
+ if (eqcmp < 0) {
+ return findNodeRecursive(node.left, key);
+ } else if (eqcmp > 0) {
+ return findNodeRecursive(node.right, key);
+ } else {
+ // check if we are equivalent to the point for this node
+ if (key.eq(node.key, precision)) {
+ return node;
+ }
+
+ // Not equivalent; the matching node (if any) could be on either
+ // side of the cut so we'll need to search both subtrees. Since points with
+ // cut dimension coordinates are always inserted into the right subtree,
+ // search that subtree first.
+ final KDTreeNode<V> righttSearchResult = findNodeRecursive(node.right, key);
+ if (righttSearchResult != null) {
+ return righttSearchResult;
+ }
+
+ return findNodeRecursive(node.left, key);
+ }
+ }
+ return null;
+ }
+
+ /** Remove the given node from the tree.
+ * @param node node to remove
+ */
+ private void removeKey(final KDTreeNode<V> node) {
+ // find a child node to replace this one
+ KDTreeNode<V> replacement = null;
+ if (node.right != null) {
+ replacement = findMin(node.right, node.cutDimension);
+ } else if (node.left != null) {
+ replacement = findMin(node.left, node.cutDimension);
+
+ // swap left and right subtrees; the replacement will
+ // contain the minimum of the entire subtree
+ node.right = node.left;
+ node.left = null;
+ }
+
+ // perform the replacement
+ if (replacement != null) {
+ node.key = replacement.key;
+ node.value = replacement.value;
+
+ removeKey(replacement);
+ } else {
+ // leaf node; disconnect from the subtree
+ if (GeometryInternalUtils.sameInstance(root, node)) {
+ this.root = null;
+ }
+
+ if (node.parent != null) {
+ if (node.isLeftChild()) {
+ node.parent.left = null;
+ } else {
+ node.parent.right = null;
+ }
+ }
+ }
+ }
+
+ /** Find the node with the minimum value along the given cut dimension.
+ * @param node subtree root
+ * @param cutDimension cut dimension to search along
+ * @return node with the minimum value along the cut dimension or null if {@code node}
+ * is null
+ */
+ private KDTreeNode<V> findMin(final KDTreeNode<V> node, final CutDimension cutDimension) {
+ if (node != null) {
+ if (node.isLeaf()) {
+ // leaf node; automatically the min
+ return node;
+ } else if (node.cutDimension.equals(cutDimension)) {
+ // this node splits on the dimensions we're searching for, so we
+ // only need to search the left subtree
+ if (node.left == null) {
+ return node;
+ }
+ return findMin(node.left, cutDimension);
+ } else {
+ // this node doesn't split on our target dimension, so search both subtrees
+ // and the current node
+ return cutDimensionMin(
+ findMin(node.left, cutDimension),
+ node,
+ findMin(node.right, cutDimension),
+ cutDimension);
+ }
+ }
+ return null;
+ }
+
+ /** Return the node containing the minimum value along the given cut dimension. Null
+ * nodes are ignored.
+ * @param a first node
+ * @param b second node
+ * @param c third node
+ * @param cutDimension search dimension
+ * @return minimum node
+ */
+ private KDTreeNode<V> cutDimensionMin(final KDTreeNode<V> a, final KDTreeNode<V> b, final KDTreeNode<V> c,
+ final CutDimension cutDimension) {
+ final KDTreeNode<V> tempMin = cutDimensionMin(a, b, cutDimension);
+ return cutDimensionMin(tempMin, c, cutDimension);
+ }
+
+ /** Return the node containing the minimum value along the given cut dimension. If one
+ * argument is null, the other argument is returned.
+ * @param a first node
+ * @param b second node
+ * @param cutDimension search dimension
+ * @return minimum node
+ */
+ private KDTreeNode<V> cutDimensionMin(final KDTreeNode<V> a, final KDTreeNode<V> b,
+ final CutDimension cutDimension) {
+ if (a == null) {
+ return b;
+ } else if (b == null) {
+ return a;
+ }
+
+ final double aCoord = cutDimension.getCoordinate(a.key);
+ final double bCoord = cutDimension.getCoordinate(b.key);
+
+ return aCoord < bCoord ? a : b;
+ }
+
+ /** Get the cut dimension for the given node depth.
+ * @param depth node depth
+ * @return cut dimension instance
+ */
+ protected CutDimension getCutDimensionForDepth(final int depth) {
+ return CUT_DIMENSIONS[depth % CUT_DIMENSIONS.length];
+ }
+
+ /** Class representing a node in a KD tree.
+ * @param <V> Value type
+ */
+ static final class KDTreeNode<V> implements Map.Entry<Vector3D, V> {
+
+ /** Parent node; may be null. */
+ private KDTreeNode<V> parent;
+
+ /** Map key value. */
+ private Vector3D key;
+
+ /** Map entry value. */
+ private V value;
+
+ /** Left child; may be null. */
+ private KDTreeNode<V> left;
+
+ /** Right child; may be null. */
+ private KDTreeNode<V> right;
+
+ /** Node cut dimension. */
+ private CutDimension cutDimension;
+
+ /** Construct a new instance.
+ * @param parent parent node; may be null
+ * @param key map key
+ * @param cutDimension cut dimension
+ */
+ private KDTreeNode(final KDTreeNode<V> parent, final Vector3D key,
+ final CutDimension cutDimension) {
+ this.parent = parent;
+ this.cutDimension = cutDimension;
+ this.key = key;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getKey() {
+ return key;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V getValue() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V setValue(final V newValue) {
+ final V oldValue = this.value;
+ this.value = newValue;
+ return oldValue;
+ }
+
+ /** Get the cut dimension for this node.
+ * @return cut dimension
+ */
+ public CutDimension getCutDimension() {
+ return cutDimension;
+ }
+
+ /** Set the cut dimension for this node.
+ * @param cutDimension cut dimension
+ */
+ public void setCutDimension(final CutDimension cutDimension) {
+ this.cutDimension = cutDimension;
+ }
+
+ /** Get the parent node.
+ * @return parent node; may be null
+ */
+ public KDTreeNode<V> getParent() {
+ return parent;
+ }
+
+ /** Set the parent node.
+ * @param parent parent node
+ */
+ public void setParent(final KDTreeNode<V> parent) {
+ this.parent = parent;
+ }
+
+ /** Get the left child node.
+ * @return left child node; may be null
+ */
+ public KDTreeNode<V> getLeft() {
+ return left;
+ }
+
+ /** Set the left child node.
+ * @param left left child node
+ */
+ public void setLeft(final KDTreeNode<V> left) {
+ this.left = left;
+ }
+
+ /** Get the right child node.
+ * @return right child node; may be null
+ */
+ public KDTreeNode<V> getRight() {
+ return right;
+ }
+
+ /** Set the right child node.
+ * @param right right child node
+ */
+ public void setRight(final KDTreeNode<V> right) {
+ this.right = right;
+ }
+
+ /** Return true if this node is a leaf node.
+ * @return true if this node is a leaf node
+ */
+ public boolean isLeaf() {
+ return left == null && right == null;
+ }
+
+ /** Return true if this node is the left child of its parent.
+ * @return true if this node is the left child of its
+ * parent
+ */
+ public boolean isLeftChild() {
+ return parent != null && GeometryInternalUtils.sameInstance(parent.left, this);
+ }
+ }
+}
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/PointMapDataStructurePerformance.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/PointMapDataStructurePerformance.java
new file mode 100644
index 0000000..aa2e718
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/PointMapDataStructurePerformance.java
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
+import org.apache.commons.geometry.euclidean.threed.SphericalCoordinates;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.angle.Angle;
+import org.apache.commons.numbers.core.Precision;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/** Benchmarks for the testing candidate implementations of point map
+ * data structures.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"})
+public class PointMapDataStructurePerformance {
+
+ /** Precision context. */
+ private static final Precision.DoubleEquivalence PRECISION =
+ Precision.doubleEquivalenceOfEpsilon(1e-6);
+
+ /** Value inserted into maps during runs. */
+ private static final Integer VAL = Integer.valueOf(1);
+
+ /** Base input class for point map benchmarks. */
+ @State(Scope.Thread)
+ public static class PointMapInput {
+
+ /** Data structure implementation. */
+ @Param({"treemap", "varoctree", "kdtree", "rebuilding-kdtree", "bucket-kdtree"})
+ private String impl;
+
+ /** Point list shape. */
+ @Param({"block", "sphere"})
+ private String shape;
+
+ /** Point distribution. */
+ @Param({"none", "random", "ordered"})
+ private String dist;
+
+ /** Seed value for randomization. */
+ @Param({"1"})
+ private int randomSeed;
+
+ /** Map instance for the run. */
+ private Map<Vector3D, Integer> map;
+
+ /** List of points for the run. */
+ private List<Vector3D> points;
+
+ /** Random instance. */
+ private Random random;
+
+ /** Set up the instance for the benchmark. */
+ @Setup(Level.Iteration)
+ public void setup() {
+ random = new Random(randomSeed);
+
+ map = createMap();
+ points = createPoints();
+
+ switch (dist) {
+ case "none":
+ break;
+ case "random":
+ Collections.shuffle(points, random);
+ break;
+ case "ordered":
+ Collections.sort(points, Vector3D.COORDINATE_ASCENDING_ORDER);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown distribution: " + dist);
+ }
+ }
+
+ /** Get the map instance under test.
+ * @return map instance
+ */
+ public Map<Vector3D, Integer> getMap() {
+ return map;
+ }
+
+ /** Get the points for the run.
+ * @return list of points
+ */
+ public List<Vector3D> getPoints() {
+ return points;
+ }
+
+ /** Get the random number generator for the instance.
+ * @return random number generate
+ */
+ public Random getRandom() {
+ return random;
+ }
+
+ /** Create the map implementation for the run.
+ * @return map instance
+ */
+ private Map<Vector3D, Integer> createMap() {
+ switch (impl.toLowerCase()) {
+ case "treemap":
+ return new TreeMap<>((a, b) -> {
+ int cmp = PRECISION.compare(a.getX(), b.getX());
+ if (cmp == 0) {
+ cmp = PRECISION.compare(a.getY(), b.getY());
+ if (cmp == 0) {
+ cmp = PRECISION.compare(a.getZ(), b.getZ());
+ }
+ }
+ return cmp;
+ });
+ case "varoctree":
+ return new VariableSplitOctree<>(PRECISION);
+ case "kdtree":
+ return new KDTree<>(PRECISION);
+ case "rebuilding-kdtree":
+ return new RebuildingKDTree<>(PRECISION);
+ case "bucket-kdtree":
+ return new BucketKDTree<>(PRECISION);
+ default:
+ throw new IllegalArgumentException("Unknown map implementation: " + impl);
+ }
+ }
+
+ /** Create the list of points for the run.
+ * @return list of points
+ */
+ private List<Vector3D> createPoints() {
+ switch (shape.toLowerCase()) {
+ case "block":
+ return createPointBlock(20, 1);
+ case "sphere":
+ return createPointSphere(5, 5, 10);
+ default:
+ throw new IllegalArgumentException("Unknown point distribution " + impl);
+ }
+ }
+ }
+
+ /** Input class containing pre-inserted points. */
+ @State(Scope.Thread)
+ public static class PreInsertedPointMapInput extends PointMapInput {
+
+ /** List of test points. */
+ private List<Vector3D> testPoints;
+
+ /** {@inheritDoc} */
+ @Override
+ @Setup(Level.Iteration)
+ public void setup() {
+ super.setup();
+
+ final List<Vector3D> pts = getPoints();
+
+ // add the points to the map
+ final Map<Vector3D, Integer> map = getMap();
+ for (final Vector3D pt : pts) {
+ map.put(pt, VAL);
+ }
+
+ // compute test points
+ testPoints = new ArrayList<>(pts.size() * 2);
+ testPoints.addAll(pts);
+
+ final Random rnd = getRandom();
+ final Bounds3D bounds = Bounds3D.from(pts);
+ final Vector3D diag = bounds.getDiagonal();
+ for (int i = 0; i < pts.size(); ++i) {
+ testPoints.add(Vector3D.of(
+ bounds.getMin().getX() + (rnd.nextDouble() * diag.getX()),
+ bounds.getMin().getY() + (rnd.nextDouble() * diag.getY()),
+ bounds.getMin().getZ() + (rnd.nextDouble() * diag.getZ())));
+ }
+
+ Collections.shuffle(testPoints, rnd);
+ }
+
+ /** Get a list of test points to look for in the map. The
+ * returned list contains 2x the number of points in the map,
+ * with half equal to map entries and half random.
+ * @return list of test points
+ */
+ public List<Vector3D> getTestPoints() {
+ return testPoints;
+ }
+ }
+
+ /** Create a solid block of points.
+ * @param pointsPerSide number of points along each side
+ * @param spacing spacing between each point
+ * @return list of points in a block
+ */
+ private static List<Vector3D> createPointBlock(final int pointsPerSide, final double spacing) {
+ final List<Vector3D> points = new ArrayList<>(pointsPerSide * pointsPerSide * pointsPerSide);
+
+ for (int x = 0; x < pointsPerSide; ++x) {
+ for (int y = 0; y < pointsPerSide; ++y) {
+ for (int z = 0; z < pointsPerSide; ++z) {
+ points.add(Vector3D.of(x, y, z).multiply(spacing));
+ }
+ }
+ }
+
+ return points;
+ }
+
+ /** Create a hollow sphere of points.
+ * @param slices number of sections in the x-y plane, not counting the poles
+ * @param segments number of section perpendicular to the x-y plane for each slice
+ * @param radius sphere radius
+ * @return list of points in a hollow sphere
+ */
+ private static List<Vector3D> createPointSphere(final int slices, final int segments, final double radius) {
+ final List<Vector3D> points = new ArrayList<>();
+
+ final double polarDelta = Math.PI / (slices + 1);
+ final double azDelta = Angle.TWO_PI / segments;
+
+ // add the top pole
+ points.add(Vector3D.of(0, 0, radius));
+
+ // add the lines of latitude
+ for (int i = 1; i <= slices; ++i) {
+ for (int j = 0; j < segments; ++j) {
+ final SphericalCoordinates coords = SphericalCoordinates.of(
+ radius,
+ j * azDelta,
+ i * polarDelta);
+
+ points.add(coords.toVector());
+ }
+ }
+
+ // add the bottom pole
+ points.add(Vector3D.of(0, 0, -radius));
+
+ return points;
+ }
+
+ /** Benchmark that inserts each point in the input into the target map.
+ * @param input input for the run
+ * @param bh blackhole instance
+ * @return input instance
+ */
+ @Benchmark
+ public PointMapInput put(final PointMapInput input, final Blackhole bh) {
+ final Map<Vector3D, Integer> map = input.getMap();
+
+ for (final Vector3D p : input.getPoints()) {
+ bh.consume(map.put(p, VAL));
+ }
+
+ return input;
+ }
+
+ /** Benchmark that retrieves each point in the input from the target map.
+ * @param input input for the run
+ * @param bh blackhole instance
+ * @return input instance
+ */
+ @Benchmark
+ public PointMapInput get(final PreInsertedPointMapInput input, final Blackhole bh) {
+ final Map<Vector3D, Integer> map = input.getMap();
+
+ for (final Vector3D p : input.getTestPoints()) {
+ bh.consume(map.get(p));
+ }
+
+ return input;
+ }
+
+ /** Benchmark that remove each point in the input from the target map.
+ * @param input input for the run
+ * @param bh blackhole instance
+ * @return input instance
+ */
+ @Benchmark
+ public PointMapInput remove(final PreInsertedPointMapInput input, final Blackhole bh) {
+ final Map<Vector3D, Integer> map = input.getMap();
+
+ for (final Vector3D p : input.getPoints()) {
+ bh.consume(map.remove(p));
+ }
+
+ return input;
+ }
+}
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/RebuildingKDTree.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/RebuildingKDTree.java
new file mode 100644
index 0000000..c5925cc
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/RebuildingKDTree.java
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision.DoubleEquivalence;
+
+/** {@link KDTree} subclass that rebuilds the tree periodically in an
+ * attempt to restrict the overall height of the tree.
+ * @param <V> Value type
+ */
+public class RebuildingKDTree<V> extends KDTree<V> {
+
+ /** Default rebuild maximum. */
+ private static final int DEFAULT_REBUILD_MAX = 16;
+
+ /** Maximum size of the tree before it is rebuilt. */
+ private int rebuildMax = DEFAULT_REBUILD_MAX;
+
+ /** Minimum size of the tree before it is rebuilt. */
+ private int rebuildMin;
+
+ /** Construct a new instance.
+ * @param precision precision context
+ */
+ public RebuildingKDTree(final DoubleEquivalence precision) {
+ super(precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V put(final Vector3D key, final V value) {
+ final V result = super.put(key, value);
+
+ if (size() >= rebuildMax) {
+ rebuild();
+ }
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ final V result = super.remove(key);
+
+ if (size() <= rebuildMin) {
+ rebuild();
+ }
+
+ return result;
+ }
+
+ /**
+ * Rebuild the tree, attempting to reduce the tree depth.
+ */
+ public void rebuild() {
+ int n = size();
+ if (n > 0) {
+ // construct an array list containing all of the tree nodes
+ final List<KDTreeNode<V>> nodes = collectNodes();
+
+ // rebuild recursively and set the new root
+ final KDTreeNode<V> newRoot = rebuildRecursive(nodes, 0, n, 0);
+
+ setRoot(newRoot);
+ }
+
+ rebuildMax = Math.max(DEFAULT_REBUILD_MAX, 2 * n);
+ rebuildMin = n / 2;
+ }
+
+ /** Get a list containing all nodes in the tree. The node connections are all cleared.
+ * @return list containing all nodes in the tree
+ */
+ protected List<KDTreeNode<V>> collectNodes() {
+ final List<KDTreeNode<V>> nodes = new ArrayList<>(size());
+ collectNodesRecursive(getRoot(), nodes);
+
+ return nodes;
+ }
+
+ /** Add nodes in the subtree rooted at {@code curr} to {@code nodes}. The node connection
+ * references are all cleared.
+ * @param curr subtree root node
+ * @param nodes node list
+ */
+ protected void collectNodesRecursive(final KDTreeNode<V> curr, final List<KDTreeNode<V>> nodes) {
+ if (curr != null) {
+ collectNodesRecursive(curr.getLeft(), nodes);
+ nodes.add(curr);
+ collectNodesRecursive(curr.getRight(), nodes);
+
+ curr.setParent(null);
+ curr.setLeft(null);
+ curr.setRight(null);
+ }
+ }
+
+ /** Recursively rebuild the tree using the specified node sublist.
+ * @param nodes node list
+ * @param startIdx sub list start index (inclusive)
+ * @param endIdx sub list end index (exclusive)
+ * @param depth node depth
+ * @return the root of the subtree containing the nodes between {@code startIdx} and {@code endIdx}
+ */
+ protected KDTreeNode<V> rebuildRecursive(final List<KDTreeNode<V>> nodes, final int startIdx, final int endIdx,
+ final int depth) {
+ final CutDimension cutDimension = getCutDimensionForDepth(depth);
+
+ final KDTreeNode<V> node;
+ if ((endIdx - startIdx) < 2) {
+ // only a single node here
+ node = nodes.get(startIdx);
+ } else {
+ final int splitIdx = partition(nodes, startIdx, endIdx, cutDimension);
+
+ node = nodes.get(splitIdx);
+
+ if (startIdx < splitIdx) {
+ node.setLeft(rebuildRecursive(nodes, startIdx, splitIdx, depth + 1));
+ node.getLeft().setParent(node);
+ }
+
+ if (splitIdx < endIdx - 1) {
+ node.setRight(rebuildRecursive(nodes, splitIdx + 1, endIdx, depth + 1));
+ node.getRight().setParent(node);
+ }
+ }
+
+ node.setCutDimension(cutDimension);
+
+ return node;
+ }
+
+ /** Partition the given sublist into values below the median and values above. The
+ * index of the median is returned.
+ * @param nodes node list
+ * @param startIdx start index (inclusive) of the sublist to partition
+ * @param endIdx end index (exclusive) of the sublist to partition
+ * @param cutDimension cut dimension
+ * @return index of the sublist median
+ */
+ protected int partition(final List<KDTreeNode<V>> nodes, final int startIdx, final int endIdx,
+ final CutDimension cutDimension) {
+ int n = endIdx - startIdx;
+ if (n < 2) {
+ return startIdx;
+ } else if (n == 2) {
+ final int bIdx = endIdx - 1;
+
+ final double a = cutDimension.getCoordinate(nodes.get(startIdx).getKey());
+ final double b = cutDimension.getCoordinate(nodes.get(bIdx).getKey());
+ if (a <= b) {
+ return startIdx;
+ } else {
+ Collections.swap(nodes, startIdx, bIdx);
+ return bIdx;
+ }
+ } else {
+ return findMedianStart(nodes, startIdx, endIdx, cutDimension);
+ }
+ }
+
+ /** Find the starting index of the node median value in the given list. The list is
+ * partially sorted and value less than the median come before the returned index while
+ * values greater than or equal come after.
+ * @param nodes list of node
+ * @param startIdx sublist start index (inclusive)
+ * @param endIdx sublist end index (exclusive)
+ * @param cutDimension cut dimension
+ * @return index of the median in the specific sublist of {@code nodes} along the cut dimension
+ */
+ protected int findMedianStart(final List<KDTreeNode<V>> nodes, final int startIdx, final int endIdx,
+ final CutDimension cutDimension) {
+ int k = startIdx + ((endIdx - startIdx) / 2);
+ int low = startIdx;
+ int high = endIdx - 1;
+ int lowTemp;
+ int highTemp;
+ double x;
+ while (low < high) {
+ x = cutDimension.getCoordinate(nodes.get(k).getKey());
+ lowTemp = low;
+ highTemp = high;
+ do {
+ while (cutDimension.getCoordinate(nodes.get(lowTemp).getKey()) < x) {
+ ++lowTemp;
+ }
+ while (cutDimension.getCoordinate(nodes.get(highTemp).getKey()) > x) {
+ --highTemp;
+ }
+
+ if (lowTemp <= highTemp) {
+ Collections.swap(nodes, lowTemp, highTemp);
+
+ ++lowTemp;
+ --highTemp;
+ }
+ } while (lowTemp <= highTemp);
+
+ if (k < lowTemp) {
+ // search low part
+ high = highTemp;
+ }
+ if (k > highTemp) {
+ // search high part
+ low = lowTemp;
+ }
+ }
+
+ // back up to the start of the median value
+ x = cutDimension.getCoordinate(nodes.get(k).getKey());
+ while (k > startIdx &&
+ cutDimension.getCoordinate(nodes.get(k - 1).getKey()) == x) {
+ --k;
+ }
+
+ return k;
+ }
+
+ /** Construct a comparator the sorts by the given cut dimension.
+ * @param cutDimension cut dimension to sort by
+ * @return comparator along the cut dimension
+ */
+ protected Comparator<KDTreeNode<V>> comparator(final CutDimension cutDimension) {
+ return (a, b) -> Double.compare(cutDimension.getCoordinate(a.getKey()), cutDimension.getCoordinate(b.getKey()));
+ }
+}
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/VariableSplitOctree.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/VariableSplitOctree.java
new file mode 100644
index 0000000..e82b824
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/VariableSplitOctree.java
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision;
+
+/** Candidate point map that stores entries in a modified octree data structure.
+ * The split point for the octree child nodes is variable and is placed at the
+ * centroid of the points stored in the node at the time it is split.
+* @param <V> map value type
+*/
+public class VariableSplitOctree<V> extends AbstractMap<Vector3D, V> {
+
+ /** Precision context. */
+ private final Precision.DoubleEquivalence precision;
+
+ /** Root of the tree. */
+ private VariableSplitOctreeNode<V> root;
+
+ /** Size of the tree. */
+ private int entryCount;
+
+ public VariableSplitOctree(final Precision.DoubleEquivalence precision) {
+ this.precision = precision;
+ this.root = new VariableSplitOctreeNode<>(this);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return entryCount;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V put(final Vector3D key, final V value) {
+ Objects.requireNonNull(key);
+ if (!key.isFinite()) {
+ throw new IllegalArgumentException("Keys must be finite");
+ }
+
+ final Vector3DEntry<V> entry = root.getEntry(key);
+ if (entry == null) {
+ root.insertEntry(key, value);
+ entryAdded();
+
+ return null;
+ }
+
+ final V prev = entry.getValue();
+ entry.setValue(value);
+
+ return prev;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ final Vector3DEntry<V> entry = root.getEntry((Vector3D) key);
+ return entry != null ?
+ entry.getValue() :
+ null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ final Vector3DEntry<V> entry = root.removeEntry((Vector3D) key);
+ if (entry != null) {
+ entryRemoved();
+ return entry.getValue();
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<Vector3D, V>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** Method called when a new entry is added to the tree.
+ */
+ private void entryAdded() {
+ ++entryCount;
+ }
+
+ /** Method called when an entry is removed from the tree.
+ */
+ private void entryRemoved() {
+ --entryCount;
+ }
+
+ /** Octree node class.
+ * @param <V> Value type
+ */
+ private static final class VariableSplitOctreeNode<V> {
+
+ /** X negative octant flag. */
+ private static final int XNEG = 1 << 5;
+
+ /** X postive octant flag. */
+ private static final int XPOS = 1 << 4;
+
+ /** Y negative octant flag. */
+ private static final int YNEG = 1 << 3;
+
+ /** Y positive octant flag. */
+ private static final int YPOS = 1 << 2;
+
+ /** Z negative octant flag. */
+ private static final int ZNEG = 1 << 1;
+
+ /** Z positive octant flag. */
+ private static final int ZPOS = 1;
+
+ /** Octant location flags for child nodes. */
+ private static final int[] CHILD_LOCATIONS = {
+ XNEG | YNEG | ZNEG,
+ XNEG | YNEG | ZPOS,
+ XNEG | YPOS | ZNEG,
+ XNEG | YPOS | ZPOS,
+
+ XPOS | YNEG | ZNEG,
+ XPOS | YNEG | ZPOS,
+ XPOS | YPOS | ZNEG,
+ XPOS | YPOS | ZPOS
+ };
+
+ /** Max entries per node. */
+ private static final int MAX_ENTRIES = 16;
+
+ /** Number of children for internal nodes. */
+ private static final int NUM_CHILDREN = 8;
+
+ /** Owning map. */
+ private final VariableSplitOctree<V> map;
+
+ /** Child nodes. */
+ private List<VariableSplitOctreeNode<V>> children;
+
+ /** Points stored in the node; this will only be populated for leaf nodes. */
+ private List<Vector3DEntry<V>> entries = new ArrayList<>(MAX_ENTRIES);
+
+ /** The split point of the node; will be null for leaf nodes. */
+ private Vector3D splitPoint;
+
+ VariableSplitOctreeNode(final VariableSplitOctree<V> map) {
+ this.map = map;
+ }
+
+ /** Return true if the node is a leaf.
+ * @return true if the node is a leaf
+ */
+ public boolean isLeaf() {
+ return splitPoint == null;
+ }
+
+ /** Return true if this node is a leaf node and contains no entries.
+ * @return true if this node is a leaf node and contains no entries
+ */
+ public boolean isEmpty() {
+ return isLeaf() && entries.isEmpty();
+ }
+
+ /** Insert a new entry containing the given key and value. No check
+ * is made as to whether or not an entry already exists for the key.
+ * @param key key to insert
+ * @param value value to insert
+ */
+ public void insertEntry(final Vector3D key, final V value) {
+ if (isLeaf()) {
+ if (entries.size() < MAX_ENTRIES) {
+ // we have an open spot here so just add the entry
+ entries.add(new Vector3DEntry<>(key, value));
+ return;
+ }
+
+ // no available entries; split the node and add to a child
+ splitNode();
+ }
+
+ // non-leaf node
+ // determine the relative location of the key
+ final int loc = getLocation(key);
+
+ // insert into the first child that can contain the key
+ for (int i = 0; i < NUM_CHILDREN; ++i) {
+ if (testChildLocation(i, loc)) {
+ getOrCreateChild(i).insertEntry(key, value);
+ break;
+ }
+ }
+ }
+
+ /** Get the entry matching the given key or null if not found.
+ * @param key key to search for
+ * @return the entry matching the given key or null if not found
+ */
+ public Vector3DEntry<V> getEntry(final Vector3D key) {
+ if (isLeaf()) {
+ // check the list of entries for a match
+ for (final Vector3DEntry<V> entry : entries) {
+ if (key.eq(entry.getKey(), map.precision)) {
+ return entry;
+ }
+ }
+ // not found
+ return null;
+ }
+
+ // delegate to each child that could possibly contain the
+ // point or an equivalent point
+ final int loc = getLocation(key);
+ for (int i = 0; i < NUM_CHILDREN; ++i) {
+ if (testChildLocation(i, loc)) {
+ final Vector3DEntry<V> entry = getEntryInChild(i, key);
+ if (entry != null) {
+ return entry;
+ }
+ }
+ }
+
+ // not found
+ return null;
+ }
+
+ /** Remove the given key, returning the previously mapped entry.
+ * @param key key to remove
+ * @return the value previously mapped to the key or null if no
+ * value was mapped
+ */
+ public Vector3DEntry<V> removeEntry(final Vector3D key) {
+ if (isLeaf()) {
+ // check the existing entries for a match
+ final Iterator<Vector3DEntry<V>> it = entries.iterator();
+ while (it.hasNext()) {
+ final Vector3DEntry<V> entry = it.next();
+ if (key.eq(entry.getKey(), map.precision)) {
+ it.remove();
+ return entry;
+ }
+ }
+
+ // not found
+ return null;
+ }
+
+ // look through children
+ final int loc = getLocation(key);
+ for (int i = 0; i < NUM_CHILDREN; ++i) {
+ if (testChildLocation(i, loc)) {
+ final Vector3DEntry<V> entry = removeFromChild(i, key);
+ if (entry != null) {
+
+ checkMakeLeaf();
+
+ return entry;
+ }
+ }
+ }
+
+ // not found
+ return null;
+ }
+
+ /** Get the given entry in the child at {@code idx} or null if not found.
+ * @param idx child index
+ * @param key key to search for
+ * @return entry matching {@code key} in child or null if not found
+ */
+ private Vector3DEntry<V> getEntryInChild(final int idx, final Vector3D key) {
+ final VariableSplitOctreeNode<V> child = children.get(idx);
+ if (child != null) {
+ return child.getEntry(key);
+ }
+ return null;
+ }
+
+ /** Remove the given key from the child at {@code idx}.
+ * @param idx index of the child
+ * @param key key to remove
+ * @return entry removed from the child or null if not found
+ */
+ private Vector3DEntry<V> removeFromChild(final int idx, final Vector3D key) {
+ final VariableSplitOctreeNode<V> child = children.get(idx);
+ if (child != null) {
+ return child.removeEntry(key);
+ }
+ return null;
+ }
+
+ /** Split the node and place all entries into the new child nodes.
+ * This node becomes an internal node.
+ */
+ private void splitNode() {
+ splitPoint = computeCentroid();
+
+ children = new ArrayList<>(NUM_CHILDREN);
+ // add null placeholders entries for children these will be replaced
+ // with actual nodes
+ for (int i = 0; i < NUM_CHILDREN; ++i) {
+ children.add(null);
+ }
+
+ for (final Vector3DEntry<V> entry : entries) {
+ moveToChild(entry);
+ }
+
+ entries.clear();
+ }
+
+ /** Attempt to condense the subtree rooted at this internal node by converting
+ * it to a leaf if no children contain entries.
+ */
+ private void checkMakeLeaf() {
+ // go through all children and remove empty ones
+ boolean empty = true;
+ for (int i = 0; i < NUM_CHILDREN; ++i) {
+ final VariableSplitOctreeNode<V> child = children.get(i);
+ if (child != null && !child.isEmpty()) {
+ empty = false;
+ break;
+ }
+ }
+
+ if (empty) {
+ makeLeaf();
+ }
+ }
+
+ /** Make this node a leaf node.
+ */
+ private void makeLeaf() {
+ splitPoint = null;
+ children = null;
+ }
+
+ /** Move the previously created entry to a child node.
+ * @param entry entry to mode
+ */
+ private void moveToChild(final Vector3DEntry<V> entry) {
+ final int loc = getLocation(entry.getKey());
+
+ for (int i = 0; i < NUM_CHILDREN; ++i) {
+ // place the entry in the first child that contains it
+ if (testChildLocation(i, loc)) {
+ getOrCreateChild(i).entries.add(entry);
+ break;
+ }
+ }
+ }
+
+ /** Get the child node at the given index, creating it if needed.
+ * @param idx index of the child node
+ * @return child node at the given index
+ */
+ private VariableSplitOctreeNode<V> getOrCreateChild(final int idx) {
+ VariableSplitOctreeNode<V> child = children.get(idx);
+ if (child == null) {
+ child = new VariableSplitOctreeNode<>(map);
+ children.set(idx, child);
+ }
+ return child;
+ }
+
+ /** Get an int encoding the location of {@code pt} relative to the
+ * node split point.
+ * @param pt point to determine the relative location of
+ * @return encoded point location
+ */
+ private int getLocation(final Vector3D pt) {
+ int loc = getLocationValue(
+ map.precision.compare(pt.getX(), splitPoint.getX()),
+ XNEG,
+ XPOS);
+ loc |= getLocationValue(
+ map.precision.compare(pt.getY(), splitPoint.getY()),
+ YNEG,
+ YPOS);
+ loc |= getLocationValue(
+ map.precision.compare(pt.getZ(), splitPoint.getZ()),
+ ZNEG,
+ ZPOS);
+
+ return loc;
+ }
+
+ /** Get the encoded location value for the given comparison value.
+ * @param cmp comparison result
+ * @param neg negative flag
+ * @param pos positive flag
+ * @return encoded location value
+ */
+ private int getLocationValue(final int cmp, final int neg, final int pos) {
+ if (cmp < 0) {
+ return neg;
+ } else if (cmp > 0) {
+ return pos;
+ }
+ return neg | pos;
+ }
+
+ /** Return true if the child node at {@code childIdx} matches the given
+ * encoded point location.
+ * @param childIdx child index to test
+ * @param loc encoded relative point location
+ * @return true if the child node a {@code childIdx} matches the location
+ */
+ private boolean testChildLocation(final int childIdx, final int loc) {
+ final int childLoc = CHILD_LOCATIONS[childIdx];
+ return (childLoc & loc) == childLoc;
+ }
+
+ /** Compute the centroid of all points currently in the node.
+ * @return centroid of the node points
+ */
+ private Vector3D computeCentroid() {
+ Vector3D.Sum sum = Vector3D.Sum.create();
+ for (Vector3DEntry<V> entry : entries) {
+ sum.add(entry.getKey());
+ }
+
+ return sum.get().multiply(1.0 / entries.size());
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/Vector3DEntry.java
similarity index 53%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
copy to commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/Vector3DEntry.java
index 8ba46f3..cd03581 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/Vector3DEntry.java
@@ -14,24 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.internal;
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
-/** Internal utility methods for <em>commons-geometry</em>.
+import java.util.AbstractMap;
+
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+
+/** Map entry class with a {@link Vector3D} key.
+ * @param <V> Map entry value type
*/
-public final class GeometryInternalUtils {
+public class Vector3DEntry<V> extends AbstractMap.SimpleEntry<Vector3D, V> {
- /** Utility class; no instantiation. */
- private GeometryInternalUtils() {}
+ /** Serializable UID. */
+ private static final long serialVersionUID = 20211121L;
- /** Return {@code true} if {@code a} is the same instance as {@code b}, as
- * determined by the {@code ==} operator. This method exists primarily to
- * document the fact that reference equality was intended and is not a
- * programming error.
- * @param a first instance
- * @param b second instance
- * @return {@code true} if the arguments are the exact same instance
+ /** Construct a new map entry.
+ * @param key entry key
+ * @param value entry value
*/
- public static boolean sameInstance(final Object a, final Object b) {
- return a == b;
+ public Vector3DEntry(final Vector3D key, final V value) {
+ super(key, value);
}
}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/package-info.java
similarity index 51%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
copy to commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/package-info.java
index 8ba46f3..0a0aeea 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalUtils.java
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/package-info.java
@@ -14,24 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.internal;
-
-/** Internal utility methods for <em>commons-geometry</em>.
+/**
+ * Benchmarks for candidate point map data structures and algorithms. The
+ * classes in this package are not complete implementations. Rather, they
+ * include just enough code to evaluate the algorithms in question.
*/
-public final class GeometryInternalUtils {
-
- /** Utility class; no instantiation. */
- private GeometryInternalUtils() {}
-
- /** Return {@code true} if {@code a} is the same instance as {@code b}, as
- * determined by the {@code ==} operator. This method exists primarily to
- * document the fact that reference equality was intended and is not a
- * programming error.
- * @param a first instance
- * @param b second instance
- * @return {@code true} if the arguments are the exact same instance
- */
- public static boolean sameInstance(final Object a, final Object b) {
- return a == b;
- }
-}
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
diff --git a/commons-geometry-examples/examples-jmh/src/test/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/PointMapDataStructureTest.java b/commons-geometry-examples/examples-jmh/src/test/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/PointMapDataStructureTest.java
new file mode 100644
index 0000000..fc3a37b
--- /dev/null
+++ b/commons-geometry-examples/examples-jmh/src/test/java/org/apache/commons/geometry/examples/jmh/euclidean/pointmap/PointMapDataStructureTest.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean.pointmap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.numbers.core.Precision.DoubleEquivalence;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Base class for potential point map data structures. These tests are not
+ * meant to be complete. The are only intended to perform enough assertions
+ * to ensure that a potential algorithm is not missing any critical functionality.
+ */
+abstract class PointMapDataStructureTest {
+
+ private static final double EPS = 1e-1;
+
+ private static final Precision.DoubleEquivalence PRECISION =
+ Precision.doubleEquivalenceOfEpsilon(EPS);
+
+ /** Get a new map instance for testing.
+ * @param precision precision context to determine floating point equality
+ * @return a new map instance for testing.
+ */
+ abstract Map<Vector3D, Integer> getMap(Precision.DoubleEquivalence precision);
+
+ @Test
+ void testMapOperations_simple() {
+ // -- arrange
+ final Map<Vector3D, Integer> map = getMap(PRECISION);
+ final Vector3D a = Vector3D.of(1, 2, 3);
+ final Vector3D b = Vector3D.of(3, 4, 5);
+ final Vector3D c = Vector3D.of(6, 7, 8);
+
+ final Vector3D aAlt = Vector3D.of(1.09, 2.09, 3.09);
+ final Vector3D bAlt = Vector3D.of(2.91, 3.91, 4.91);
+ final Vector3D cAlt = Vector3D.of(6.09, 6.91, 8.09);
+
+ // -- act/assert
+ Assertions.assertNull(map.put(a, 1));
+ Assertions.assertNull(map.put(b, 2));
+ Assertions.assertNull(map.put(c, 3));
+
+ Assertions.assertEquals(3, map.size());
+
+ Assertions.assertEquals(1, map.get(a));
+ Assertions.assertEquals(1, map.get(aAlt));
+ Assertions.assertEquals(2, map.get(b));
+ Assertions.assertEquals(2, map.get(bAlt));
+ Assertions.assertEquals(3, map.get(c));
+ Assertions.assertEquals(3, map.get(cAlt));
+
+ Assertions.assertEquals(1, map.put(aAlt, -1));
+ Assertions.assertEquals(2, map.put(bAlt, -2));
+ Assertions.assertEquals(3, map.put(cAlt, -3));
+
+ Assertions.assertEquals(3, map.size());
+
+ Assertions.assertEquals(-1, map.get(a));
+ Assertions.assertEquals(-1, map.get(aAlt));
+ Assertions.assertEquals(-2, map.get(b));
+ Assertions.assertEquals(-2, map.get(bAlt));
+ Assertions.assertEquals(-3, map.get(c));
+ Assertions.assertEquals(-3, map.get(cAlt));
+
+ Assertions.assertEquals(-1, map.remove(aAlt));
+ Assertions.assertEquals(-2, map.remove(bAlt));
+ Assertions.assertEquals(-3, map.remove(cAlt));
+
+ Assertions.assertEquals(0, map.size());
+
+ Assertions.assertNull(map.get(a));
+ Assertions.assertNull(map.get(aAlt));
+ Assertions.assertNull(map.get(b));
+ Assertions.assertNull(map.get(bAlt));
+ Assertions.assertNull(map.get(c));
+ Assertions.assertNull(map.get(cAlt));
+ }
+
+ @Test
+ void testGetResolution_simple() {
+ // -- arrange
+ final Map<Vector3D, Integer> map = getMap(PRECISION);
+ final Vector3D v = Vector3D.ZERO;
+ final double smallDelta = 0.05;
+ final double largeDelta = 0.15;
+
+ map.put(v, 1);
+
+ // -- act/assert
+ EuclideanTestUtils.permute(-1, 1, 1, (x, y, z) -> {
+ final Vector3D pt = Vector3D.of(x, y, z).multiply(smallDelta);
+ Assertions.assertEquals(1, map.get(pt), () -> "Point " + pt + " not found in map");
+ });
+
+ EuclideanTestUtils.permuteSkipZero(-1, 1, 1, (x, y, z) -> {
+ final Vector3D pt = Vector3D.of(x, y, z).multiply(largeDelta);
+ Assertions.assertNull(map.get(pt), () -> "Point " + pt + " found in map");
+ });
+ }
+
+ @Test
+ void testGetResolution_populatedMap() {
+ // -- arrange
+ final Map<Vector3D, Integer> map = getMap(PRECISION);
+ final Vector3D v = Vector3D.ZERO;
+ final double smallDelta = 0.05;
+ final double largeDelta = 0.15;
+
+ // add a number of points about the origin to make sure the map is populated
+ // and we're not just dealing with trivial cases
+ final double insertDelta = 0.3;
+ EuclideanTestUtils.permuteSkipZero(-4, 4, 1, (x, y, z) ->
+ map.put(Vector3D.of(x, y, z).multiply(insertDelta), 0));
+
+ // add a point exactly at the origin
+ map.put(v, 1);
+
+ // -- act/assert
+ EuclideanTestUtils.permute(-1, 1, 1, (x, y, z) -> {
+ final Vector3D pt = Vector3D.of(x, y, z).multiply(smallDelta);
+ Assertions.assertEquals(1, map.get(pt), () -> "Point " + pt + " not found in map");
+ });
+
+ EuclideanTestUtils.permuteSkipZero(-1, 1, 1, (x, y, z) -> {
+ final Vector3D pt = Vector3D.of(x, y, z).multiply(largeDelta);
+ Assertions.assertNull(map.get(pt), () -> "Point " + pt + " found in map");
+ });
+ }
+
+ @Test
+ void testMapOperations_randomOrder() {
+ // -- arrange
+ final Map<Vector3D, Integer> map = getMap(PRECISION);
+ final Vector3D v = Vector3D.of(1, 2, -1);
+
+ final double start = -3.0;
+ final double stop = 3.0;
+ final double step = 0.3;
+
+ // -- act/assert
+
+ // populate the map with entries in a random order
+ final List<Vector3D> points = new ArrayList<>();
+ EuclideanTestUtils.permute(start, stop, step, (x, y, z) -> points.add(Vector3D.of(x, y, z)));
+ Collections.shuffle(points, new Random(1L));
+
+ points.forEach(p -> map.put(p, -1));
+ map.put(v, 1);
+
+ Assertions.assertEquals(1, map.get(v));
+
+ // assert that each entry has a value
+ EuclideanTestUtils.permute(start, stop, step, (x, y, z) -> {
+ final Vector3D k = Vector3D.of(x, y, z);
+ final int val = k.eq(v, PRECISION) ?
+ 1 :
+ -1;
+
+ Assertions.assertEquals(val, map.get(k));
+ });
+
+ // remove entries
+ EuclideanTestUtils.permute(start, stop, step, (x, y, z) -> {
+ map.remove(Vector3D.of(x, y, z));
+ });
+ map.remove(v);
+
+ // check that we don't have anything left
+ Assertions.assertEquals(0, map.size());
+ Assertions.assertNull(map.get(v));
+ }
+
+ /** Unit test for the {@link VariableSplitOctree} data structure.
+ */
+ static class VariableSplitOctreeTest extends PointMapDataStructureTest {
+ /** {@inheritDoc} */
+ @Override
+ Map<Vector3D, Integer> getMap(final DoubleEquivalence precision) {
+ return new VariableSplitOctree<>(PRECISION);
+ }
+ }
+
+ /** Unit test for the {@link KDTreeTest} data structure.
+ */
+ static class KDTreeTest extends PointMapDataStructureTest {
+ /** {@inheritDoc} */
+ @Override
+ Map<Vector3D, Integer> getMap(final DoubleEquivalence precision) {
+ return new KDTree<>(PRECISION);
+ }
+ }
+
+ /** Unit test for the {@link RebuildingKDTreeTest} data structure.
+ */
+ static class RebuildingKDTreeTest extends PointMapDataStructureTest {
+ /** {@inheritDoc} */
+ @Override
+ Map<Vector3D, Integer> getMap(final DoubleEquivalence precision) {
+ return new RebuildingKDTree<>(PRECISION);
+ }
+ }
+
+ /** Unit test for the {@link BucketKDTree} data structure.
+ */
+ static class BucketKDTreeTest extends PointMapDataStructureTest {
+ /** {@inheritDoc} */
+ @Override
+ Map<Vector3D, Integer> getMap(final DoubleEquivalence precision) {
+ return new BucketKDTree<>(PRECISION);
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/PointMap1SImpl.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/PointMap1SImpl.java
new file mode 100644
index 0000000..879a217
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/PointMap1SImpl.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.spherical;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.commons.geometry.core.internal.AbstractPointMap1D;
+import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.numbers.angle.Angle;
+import org.apache.commons.numbers.core.Precision;
+
+/** Internal {@link org.apache.commons.geometry.core.collection.PointMap PointMap}
+ * implementation for 1D spherical space. This class uses a {@link NavigableMap}
+ * internally with special logic to handle wrap around.
+ * @param <V> Map value type
+ */
+final class PointMap1SImpl<V>
+ extends AbstractPointMap1D<Point1S, V> {
+
+ /** Precision context used to determine floating point equality. */
+ private final Precision.DoubleEquivalence precision;
+
+ /** Minimum key in the map, or null if not known. */
+ private Point1S minKey;
+
+ /** Maximum key in the map, or null if not known. */
+ private Point1S maxKey;
+
+ /** Construct a new instance using the given precision object to determine
+ * floating point equality.
+ * @param precision object used to determine floating point equality
+ */
+ PointMap1SImpl(final Precision.DoubleEquivalence precision) {
+ super((a, b) -> precision.compare(a.getNormalizedAzimuth(), b.getNormalizedAzimuth()));
+ this.precision = precision;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean containsKey(final Object key) {
+ return getEntryInternal((Point1S) key) != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V get(final Object key) {
+ return getValue(getEntryInternal((Point1S) key));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public V remove(final Object key) {
+ final Map.Entry<Point1S, V> entry = getEntryInternal((Point1S) key);
+ if (entry != null) {
+ final V result = getMap().remove(entry.getKey());
+
+ mapUpdated();
+
+ return result;
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clear() {
+ getMap().clear();
+ mapUpdated();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Point1S> keySet() {
+ return new KeySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<Entry<Point1S, V>> entrySet() {
+ return new EntrySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map.Entry<Point1S, V> getEntryInternal(final Point1S pt) {
+ final NavigableMap<Point1S, V> map = getMap();
+
+ final Map.Entry<Point1S, V> floor = map.floorEntry(pt);
+ if (floor != null && keyEq(pt, floor)) {
+ return floor;
+ } else {
+ if (pt.getNormalizedAzimuth() < Math.PI) {
+ if (wrapsLowToHigh(pt)) {
+ return map.lastEntry();
+ }
+ } else if (wrapsHighToLow(pt)) {
+ return map.firstEntry();
+ }
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected V putInternal(final Point1S key, final V value) {
+ final NavigableMap<Point1S, V> map = getMap();
+
+ final Map.Entry<Point1S, V> entry = getEntryInternal(key);
+ if (entry != null) {
+ return map.put(entry.getKey(), value);
+ }
+
+ final V result = map.put(key, value);
+ mapUpdated();
+
+ return result;
+ }
+
+ /** Method called when the map is updated.
+ */
+ private void mapUpdated() {
+ minKey = null;
+ maxKey = null;
+ }
+
+ /** Return true if {@code pt} is directly equivalent to the key for {@code entry},
+ * without considering wrap around.
+ * @param pt point
+ * @param entry map entry
+ * @return true if {@code pt} is directly equivalent to the key for {@code entry},
+ * without considering wrap around
+ */
+ private boolean keyEq(final Point1S pt, final Map.Entry<Point1S, V> entry) {
+ return precision.eq(pt.getNormalizedAzimuth(), entry.getKey().getNormalizedAzimuth());
+ }
+
+ /** Return true if the given point wraps around the zero point from high to low
+ * and is equivalent to the first point in the map.
+ * @param pt point to check
+ * @return true if the normalized azimuth of {@code pt} plus 2pi is equivalent
+ * to the first key in the map
+ */
+ private boolean wrapsHighToLow(final Point1S pt) {
+ if (size() > 0) {
+ if (minKey == null) {
+ minKey = getMap().firstKey();
+ }
+
+ final double adjustedAz = pt.getNormalizedAzimuth() - Angle.TWO_PI;
+ return precision.eq(adjustedAz, minKey.getNormalizedAzimuth());
+ }
+
+ return false;
+ }
+
+ /** Return true if the given point wraps around the zero point from low to high
+ * and is equivalent to the last point in the map.
+ * @param pt point to check
+ * @return true if the normalized azimuth of {@code pt} minus 2pi is equivalent
+ * to the first key in the map
+ */
+ private boolean wrapsLowToHigh(final Point1S pt) {
+ if (size() > 0) {
+ if (maxKey == null) {
+ maxKey = getMap().lastKey();
+ }
+
+ final double adjustedAz = pt.getNormalizedAzimuth() + Angle.TWO_PI;
+ return precision.eq(adjustedAz, maxKey.getNormalizedAzimuth());
+ }
+
+ return false;
+ }
+
+ /** Null-safe method to get the value from a map entry.
+ * @param <V> Value type
+ * @param entry map entry
+ * @return map value or null if {@code entry} is null
+ */
+ private static <V> V getValue(final Map.Entry<?, V> entry) {
+ return entry != null ?
+ entry.getValue() :
+ null;
+ }
+
+ /** Key set view of the map.
+ */
+ private final class KeySet
+ extends AbstractSet<Point1S> {
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Object obj) {
+ return PointMap1SImpl.this.containsKey(obj);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return PointMap1SImpl.this.size();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<Point1S> iterator() {
+ return new MapIterator<>(getMap().keySet().iterator());
+ }
+ }
+
+ /** Entry set view of the map.
+ */
+ private final class EntrySet
+ extends AbstractSet<Map.Entry<Point1S, V>> {
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Object obj) {
+ if (obj instanceof Map.Entry) {
+ final Map.Entry<?, ?> search = (Map.Entry<?, ?>) obj;
+ final Object key = search.getKey();
+
+ final Map.Entry<Point1S, V> actual = getEntry((Point1S) key);
+ if (actual != null) {
+ return actual.getKey().eq((Point1S) search.getKey(), precision) &&
+ Objects.equals(actual.getValue(), search.getValue());
+ }
+ }
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int size() {
+ return PointMap1SImpl.this.size();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterator<Entry<Point1S, V>> iterator() {
+ return new MapIterator<>(getMap().entrySet().iterator());
+ }
+ }
+
+ /** Iterator for iterating through elements in the map.
+ * @param <E> Element type
+ */
+ private final class MapIterator<E>
+ implements Iterator<E> {
+
+ /** Underlying iterator. */
+ private final Iterator<E> it;
+
+ /** Construct a new instance that wraps the given iterator.
+ * @param it underlying iterator
+ */
+ MapIterator(final Iterator<E> it) {
+ this.it = it;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public E next() {
+ return it.next();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void remove() {
+ it.remove();
+ mapUpdated();
+ }
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/PointMap2SImpl.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/PointMap2SImpl.java
new file mode 100644
index 0000000..a3c3484
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/PointMap2SImpl.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.spherical;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.collection.PointMap;
+import org.apache.commons.geometry.core.internal.AbstractBucketPointMap;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.spherical.twod.GreatCircle;
+import org.apache.commons.geometry.spherical.twod.GreatCircles;
+import org.apache.commons.geometry.spherical.twod.Point2S;
+import org.apache.commons.numbers.core.Precision;
+
+/** Internal {@link PointMap} implementation for 2D spherical space.
+ * @param <V> Map value type
+ */
+final class PointMap2SImpl<V>
+ extends AbstractBucketPointMap<Point2S, V>
+ implements PointMap<Point2S, V> {
+
+ /** Number of children per node. */
+ private static final int NODE_CHILD_COUNT = 4;
+
+ /** Max entries per node. */
+ private static final int MAX_ENTRIES_PER_NODE = 16;
+
+ /** First negative quadrant flag. */
+ private static final int NEG1 = 1 << 3;
+
+ /** First positive quadrant flag. */
+ private static final int POS1 = 1 << 2;
+
+ /** Second negative quadrant flag. */
+ private static final int NEG2 = 1 << 1;
+
+ /** Second positive quadrant flag. */
+ private static final int POS2 = 1;
+
+ /** Location flags for child nodes. */
+ private static final int[] CHILD_LOCATIONS = {
+ NEG1 | NEG2,
+ NEG1 | POS2,
+ POS1 | NEG2,
+ POS1 | POS2
+ };
+
+ PointMap2SImpl(final Precision.DoubleEquivalence precision) {
+ super(MapNode2S::new,
+ MAX_ENTRIES_PER_NODE,
+ NODE_CHILD_COUNT,
+ precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean pointsEq(final Point2S a, final Point2S b) {
+ return a.eq(b, getPrecision());
+ }
+
+ /** Tree node class for {@link PointMap2SImpl}.
+ * @param <V> Map value type
+ */
+ private static final class MapNode2S<V>
+ extends BucketNode<Point2S, V> {
+
+ /** First hyperplane split. */
+ private GreatCircle firstSplit;
+
+ /** Second hyperplane split. */
+ private GreatCircle secondSplit;
+
+ MapNode2S(
+ final AbstractBucketPointMap<Point2S, V> map,
+ final BucketNode<Point2S, V> parent) {
+ super(map, parent);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void computeSplit() {
+ final Vector3D.Sum sum = Vector3D.Sum.create();
+
+ for (Entry<Point2S, V> entry : this) {
+ sum.add(entry.getKey().getVector());
+ }
+
+ // construct an orthonormal basis
+ Vector3D.Unit u = sum.get().multiply(1.0 / MAX_ENTRIES_PER_NODE)
+ .normalizeOrNull();
+ if (u == null) {
+ u = Vector3D.Unit.PLUS_X;
+ }
+
+ Vector3D.Unit v = Vector3D.Unit.PLUS_Z.cross(u)
+ .normalizeOrNull();
+ if (v == null) {
+ v = Vector3D.Unit.PLUS_Y.cross(u)
+ .normalizeOrNull();
+ }
+ final Vector3D.Unit w = u.cross(v).normalize();
+
+ // construct the two great circles
+ firstSplit = GreatCircles.fromPole(v.add(w), getPrecision());
+ secondSplit = GreatCircles.fromPole(v.negate().add(w), getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getSearchLocation(final Point2S pt) {
+ int loc = getSearchLocationValue(firstSplit.classify(pt), NEG1, POS1);
+
+ loc |= getSearchLocationValue(secondSplit.classify(pt), NEG2, POS2);
+
+ return loc;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected int getInsertLocation(final Point2S pt) {
... 840 lines suppressed ...