diff --git a/.gitignore b/.gitignore index a4ccad2a..e56582dc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ nbactions.xml /store/graphstore/target/ .idea *.iml +.vscode/** \ No newline at end of file diff --git a/src/main/java/org/gephi/graph/api/Rect2D.java b/src/main/java/org/gephi/graph/api/Rect2D.java index 98ab3876..3970a908 100644 --- a/src/main/java/org/gephi/graph/api/Rect2D.java +++ b/src/main/java/org/gephi/graph/api/Rect2D.java @@ -114,8 +114,8 @@ public String toString() { } private String toString(NumberFormat formatter) { - return "(" + formatter.format(minX) + " " + formatter.format(minY) + ") < " + "(" + formatter - .format(maxX) + " " + formatter.format(maxY) + ")"; + return "min(x:" + formatter.format(minX) + " y:" + formatter.format(minY) + ") < " + "max(x:" + formatter + .format(maxX) + " y:" + formatter.format(maxY) + ")"; } /** @@ -174,6 +174,44 @@ public boolean intersects(float minX, float minY, float maxX, float maxY) { return this.minX <= maxX && minX <= this.maxX && this.maxY >= minY && maxY >= this.minY; } + /** + * Returns true if this rectangle contains or intersects with the given + * rectangle. This is equivalent to checking + * {@code this.contains(rect) || this.intersects(rect)} but more efficient as it + * performs the check in a single operation. + * + * @param rect the rectangle to check + * @return true if this rectangle contains or intersects with the given + * rectangle, false otherwise + */ + public boolean containsOrIntersects(Rect2D rect) { + if (rect == this) { + return true; + } + + return containsOrIntersects(rect.minX, rect.minY, rect.maxX, rect.maxY); + } + + /** + * Returns true if this rectangle contains or intersects with the given + * rectangle. This is equivalent to checking + * {@code this.contains(minX, minY, maxX, maxY) || this.intersects(minX, minY, maxX, maxY)} + * but more efficient as it performs the check in a single operation. + * + * @param minX the x coordinate of the minimum corner + * @param minY the y coordinate of the minimum corner + * @param maxX the x coordinate of the maximum corner + * @param maxY the y coordinate of the maximum corner + * + * @return true if this rectangle contains or intersects with the given + * rectangle, false otherwise + */ + public boolean containsOrIntersects(float minX, float minY, float maxX, float maxY) { + // Two rectangles have overlap if they intersect - containment is a subset of + // intersection + return this.minX <= maxX && minX <= this.maxX && this.maxY >= minY && maxY >= this.minY; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/src/main/java/org/gephi/graph/api/SpatialIndex.java b/src/main/java/org/gephi/graph/api/SpatialIndex.java index 997c9040..3502b669 100644 --- a/src/main/java/org/gephi/graph/api/SpatialIndex.java +++ b/src/main/java/org/gephi/graph/api/SpatialIndex.java @@ -16,7 +16,19 @@ package org.gephi.graph.api; /** - * Object to query the nodes and edges of the graph in a spatial context. + * Query the (quadtree-based) index based on the given rectangle area. + *

+ * The spatial index is not enabled by default. To enable it, set the + * appropriate configuration: + * @{@link Configuration.Builder#enableSpatialIndex(boolean)}. + *

+ * When nodes are moved, added or removed, the spatial index is automatically + * updated. Edges are not indexed, but they are queried based on whether their + * source or target nodes are in the given area. + *

+ * The Z position is not taken into account when querying the spatial index, + * only X/Y are supported. + *

* * @author Eduardo Ramos */ @@ -31,13 +43,35 @@ public interface SpatialIndex { NodeIterable getNodesInArea(Rect2D rect); /** - * Returns the edges in the given area. + * Returns the nodes in the given area using a faster, but approximate method. + *

+ * All nodes in the provided area are guaranteed to be returned, but some nodes + * outside the area may also be returned. + * + * @param rect area to query + * @return nodes in the area + */ + NodeIterable getApproximateNodesInArea(Rect2D rect); + + /** + * Returns the edges in the given area. Edges may be returned twice. * * @param rect area to query * @return edges in the area */ EdgeIterable getEdgesInArea(Rect2D rect); + /** + * Returns the edges in the given area using a faster, but approximate method. + *

+ * All edges in the provided area are guaranteed to be returned, but some edges + * outside the area may also be returned. Edges may also be returned twice. + * + * @param rect area to query + * @return edges in the area + */ + EdgeIterable getApproximateEdgesInArea(Rect2D rect); + /** * Returns the bounding rectangle that contains all nodes in the graph. The * boundaries are calculated based on each node's position and size. @@ -45,4 +79,20 @@ public interface SpatialIndex { * @return the bounding rectangle, or null if there are no nodes */ Rect2D getBoundaries(); + + /** + * Acquires a read lock on the spatial index. This is recommended when using the + * query functions in a stream context, to avoid the spatial index being + * modified while being queried. + *

+ * Every call to this method must be matched with a call to + * {@link #spatialIndexReadUnlock()}. + */ + void spatialIndexReadLock(); + + /** + * Releases a read lock on the spatial index. This must be called after a call + * to {@link #spatialIndexReadLock()}. + */ + void spatialIndexReadUnlock(); } diff --git a/src/main/java/org/gephi/graph/impl/EdgeStore.java b/src/main/java/org/gephi/graph/impl/EdgeStore.java index 497c7a00..b03277ba 100644 --- a/src/main/java/org/gephi/graph/impl/EdgeStore.java +++ b/src/main/java/org/gephi/graph/impl/EdgeStore.java @@ -419,14 +419,18 @@ public EdgeInIterator edgeInIterator(final Node node) { return new EdgeInIterator((NodeImpl) node); } - public EdgeInOutIterator edgeIterator(final Node node) { + public EdgeInOutIterator edgeIterator(final Node node, boolean locking) { checkValidNodeObject(node); - return new EdgeInOutIterator((NodeImpl) node); + return new EdgeInOutIterator((NodeImpl) node, locking); } - public Iterator edgeUndirectedIterator(final Node node) { + public EdgeInOutMultiIterator edgeIterator(final Iterator nodeIterator, boolean locking) { + return new EdgeInOutMultiIterator(nodeIterator, locking); + } + + public Iterator edgeUndirectedIterator(final Node node, boolean locking) { checkValidNodeObject(node); - return undirectedIterator(new EdgeInOutIterator((NodeImpl) node)); + return undirectedIterator(new EdgeInOutIterator((NodeImpl) node, locking)); } public EdgeTypeOutIterator edgeOutIterator(final Node node, int type) { @@ -471,7 +475,7 @@ public NeighborsIterator neighborInIterator(final Node node, int type) { public NeighborsIterator neighborIterator(Node node) { checkValidNodeObject(node); - return new NeighborsUndirectedIterator((NodeImpl) node, new EdgeInOutIterator((NodeImpl) node)); + return new NeighborsUndirectedIterator((NodeImpl) node, new EdgeInOutIterator((NodeImpl) node, true)); } public NeighborsIterator neighborIterator(final Node node, int type) { @@ -1285,6 +1289,9 @@ private void incrementVersion() { if (version != null) { version.incrementAndGetEdgeVersion(); } + if (spatialIndex != null) { + spatialIndex.incrementVersion(); + } } boolean isUndirectedToIgnore(EdgeImpl edge) { @@ -1491,10 +1498,15 @@ public boolean hasNext() { } } - protected final class EdgeInOutIterator implements Iterator { + /** + * Abstract base class for iterating over edges connected to nodes. Provides + * common logic for handling both incoming and outgoing edges. + */ + protected abstract class AbstractEdgeInOutIterator implements Iterator { - protected final int outTypeLength; - protected final int inTypeLength; + protected final boolean locking; + protected int outTypeLength; + protected int inTypeLength; protected EdgeImpl[] outArray; protected EdgeImpl[] inArray; protected int typeIndex = 0; @@ -1502,17 +1514,36 @@ protected final class EdgeInOutIterator implements Iterator { protected EdgeImpl lastEdge; protected boolean out = true; - public EdgeInOutIterator(NodeImpl node) { - readLock(); + protected AbstractEdgeInOutIterator(boolean locking) { + this.locking = locking; + if (locking) { + readLock(); + } + } + + /** + * Initialize arrays for the current node. Called when starting iteration for a + * new node. + */ + protected void initializeForNode(NodeImpl node) { outArray = node.headOut; outTypeLength = outArray.length; inArray = node.headIn; inTypeLength = inArray.length; + typeIndex = 0; + pointer = null; + out = true; } + /** + * Called when the current node has no more edges. Should return true if there + * are more nodes to process, false otherwise. + */ + protected abstract boolean moveToNextNode(); + @Override public boolean hasNext() { - if (pointer == null) { + while (pointer == null) { if (out) { while (pointer == null && typeIndex < outTypeLength) { pointer = outArray[typeIndex++]; @@ -1537,8 +1568,13 @@ public boolean hasNext() { } if (pointer == null) { - readUnlock(); - return false; + // No more edges for current node, try next node + if (!moveToNextNode()) { + if (locking) { + readUnlock(); + } + return false; + } } } return true; @@ -1579,6 +1615,53 @@ public void remove() { } } + /** + * Iterator for edges connected to a single node (both incoming and outgoing). + */ + protected final class EdgeInOutIterator extends AbstractEdgeInOutIterator { + + public EdgeInOutIterator(NodeImpl node, boolean locking) { + super(locking); + initializeForNode(node); + } + + @Override + protected boolean moveToNextNode() { + // Single node iterator - no more nodes to process + return false; + } + } + + /** + * Iterator for edges connected to multiple nodes (both incoming and outgoing). + * Iterates through all edges of all provided nodes without creating separate + * iterators. + */ + protected final class EdgeInOutMultiIterator extends AbstractEdgeInOutIterator { + + private final Iterator nodeIterator; + + public EdgeInOutMultiIterator(Iterator nodeIterator, boolean locking) { + super(locking); + this.nodeIterator = nodeIterator; + // Initialize with first node if available + if (nodeIterator.hasNext()) { + NodeImpl node = nodeIterator.next(); + checkValidNodeObject(node); + initializeForNode(node); + } + } + + @Override + protected boolean moveToNextNode() { + if (nodeIterator.hasNext()) { + initializeForNode(nodeIterator.next()); + return true; + } + return false; + } + } + protected final class EdgeOutIterator implements Iterator { protected final int typeLength; diff --git a/src/main/java/org/gephi/graph/impl/GraphStore.java b/src/main/java/org/gephi/graph/impl/GraphStore.java index 0bb033a1..d93aff11 100644 --- a/src/main/java/org/gephi/graph/impl/GraphStore.java +++ b/src/main/java/org/gephi/graph/impl/GraphStore.java @@ -277,7 +277,8 @@ public boolean removeNode(final Node node) { autoWriteLock(); try { nodeStore.checkNonNullNodeObject(node); - for (EdgeStore.EdgeInOutIterator edgeIterator = edgeStore.edgeIterator(node); edgeIterator.hasNext();) { + for (EdgeStore.EdgeInOutIterator edgeIterator = edgeStore.edgeIterator(node, false); edgeIterator + .hasNext();) { edgeIterator.next(); edgeIterator.remove(); } @@ -303,7 +304,8 @@ public boolean removeAllNodes(Collection nodes) { try { for (Node node : nodes) { nodeStore.checkNonNullNodeObject(node); - for (EdgeStore.EdgeInOutIterator edgeIterator = edgeStore.edgeIterator(node); edgeIterator.hasNext();) { + for (EdgeStore.EdgeInOutIterator edgeIterator = edgeStore.edgeIterator(node, false); edgeIterator + .hasNext();) { edgeIterator.next(); edgeIterator.remove(); } @@ -434,7 +436,7 @@ public NodeIterable getSuccessors(final Node node, final int type) { @Override public EdgeIterable getEdges(final Node node) { - return new EdgeIterableWrapper(() -> edgeStore.edgeIterator(node), getAutoLock()); + return new EdgeIterableWrapper(() -> edgeStore.edgeIterator(node, true), getAutoLock()); } @Override @@ -575,7 +577,7 @@ public boolean isIncident(final Node node, final Edge edge) { public void clearEdges(final Node node) { autoWriteLock(); try { - EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(node); + EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(node, false); for (; itr.hasNext();) { itr.next(); itr.remove(); diff --git a/src/main/java/org/gephi/graph/impl/GraphStoreConfiguration.java b/src/main/java/org/gephi/graph/impl/GraphStoreConfiguration.java index 02a4e0f4..3436ad9c 100644 --- a/src/main/java/org/gephi/graph/impl/GraphStoreConfiguration.java +++ b/src/main/java/org/gephi/graph/impl/GraphStoreConfiguration.java @@ -40,10 +40,10 @@ public final class GraphStoreConfiguration { public static final int NODESTORE_DEFAULT_DICTIONARY_SIZE = 8192; public final static float NODESTORE_DICTIONARY_LOAD_FACTOR = .7f; // EdgeStore - public static final int EDGESTORE_BLOCK_SIZE = 8192; - public static final int EDGESTORE_DEFAULT_BLOCKS = 10; + public static final int EDGESTORE_BLOCK_SIZE = 32768; + public static final int EDGESTORE_DEFAULT_BLOCKS = 5; public static final int EDGESTORE_DEFAULT_TYPE_COUNT = 1; - public static final int EDGESTORE_DEFAULT_DICTIONARY_SIZE = 1000; + public static final int EDGESTORE_DEFAULT_DICTIONARY_SIZE = 32768; public static final float EDGESTORE_DICTIONARY_LOAD_FACTOR = .7f; // GraphView public static final int VIEW_DEFAULT_TYPE_COUNT = 1; @@ -83,8 +83,10 @@ public final class GraphStoreConfiguration { public static final TimeRepresentation DEFAULT_TIME_REPRESENTATION = TimeRepresentation.TIMESTAMP; // Spatial index public static final int SPATIAL_INDEX_MAX_LEVELS = 16; - public static final int SPATIAL_INDEX_MAX_OBJECTS_PER_NODE = 5000; + public static final int SPATIAL_INDEX_MAX_OBJECTS_PER_NODE = 8192; public static final float SPATIAL_INDEX_DIMENSION_BOUNDARY = 1e6f; + public static final boolean SPATIAL_INDEX_APPROXIMATE_AREA_SEARCH = false; + public static final float SPATIAL_INDEX_LOCAL_ITERATOR_THRESHOLD = 0.3f; // Miscellaneous public static final double TIMESTAMP_STORE_GROWING_FACTOR = 1.1; public static final double INTERVAL_STORE_GROWING_FACTOR = 1.1; diff --git a/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java b/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java index 741016c5..1a05db41 100644 --- a/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java +++ b/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java @@ -299,7 +299,7 @@ public boolean contains(Node node) { checkValidNodeObject(node); graphStore.autoReadLock(); try { - return view.containsNode((NodeImpl) node); + return view.containsNode(node); } finally { graphStore.autoReadUnlock(); } @@ -433,7 +433,7 @@ public NodeIterable getNeighbors(Node node) { checkValidInViewNodeObject(node); return new NodeIterableWrapper( () -> new NeighborsIterator((NodeImpl) node, - new UndirectedEdgeViewIterator(graphStore.edgeStore.edgeIterator(node))), + new UndirectedEdgeViewIterator(graphStore.edgeStore.edgeIterator(node, true))), graphStore.getAutoLock()); } @@ -451,10 +451,10 @@ public EdgeIterable getEdges(Node node) { checkValidInViewNodeObject(node); if (undirected) { return new EdgeIterableWrapper( - () -> new UndirectedEdgeViewIterator(graphStore.edgeStore.edgeIterator(node)), + () -> new UndirectedEdgeViewIterator(graphStore.edgeStore.edgeIterator(node, true)), graphStore.getAutoLock()); } else { - return new EdgeIterableWrapper(() -> new EdgeViewIterator(graphStore.edgeStore.edgeIterator(node)), + return new EdgeIterableWrapper(() -> new EdgeViewIterator(graphStore.edgeStore.edgeIterator(node, true)), graphStore.getAutoLock()); } } @@ -507,7 +507,7 @@ public Node getOpposite(Node node, Edge edge) { public int getDegree(Node node) { if (undirected) { int count = 0; - EdgeStore.EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node); + EdgeStore.EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, true); while (itr.hasNext()) { EdgeImpl edge = itr.next(); if (view.containsEdge(edge) && !isUndirectedToIgnore(edge)) { @@ -520,7 +520,7 @@ public int getDegree(Node node) { return count; } else { int count = 0; - EdgeStore.EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node); + EdgeStore.EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, true); while (itr.hasNext()) { EdgeImpl edge = itr.next(); if (view.containsEdge(edge)) { @@ -598,7 +598,7 @@ public boolean isIncident(final Node node, final Edge edge) { public void clearEdges(Node node) { graphStore.autoWriteLock(); try { - EdgeStore.EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node); + EdgeStore.EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, false); while (itr.hasNext()) { EdgeImpl edge = itr.next(); view.removeEdge(edge); @@ -812,7 +812,7 @@ void checkValidNodeObject(final Node n) { if (!(n instanceof NodeImpl)) { throw new ClassCastException("Object must be a NodeImpl object"); } - if (((NodeImpl) n).storeId == NodeStore.NULL_ID) { + if (n.getStoreId() == NodeStore.NULL_ID) { throw new IllegalArgumentException("Node should belong to a store"); } } @@ -820,7 +820,7 @@ void checkValidNodeObject(final Node n) { void checkValidInViewNodeObject(final Node n) { checkValidNodeObject(n); - if (!view.containsNode((NodeImpl) n)) { + if (!view.containsNode(n)) { throw new RuntimeException("Node doesn't belong to this view"); } } @@ -832,7 +832,7 @@ void checkValidEdgeObject(final Edge n) { if (!(n instanceof EdgeImpl)) { throw new ClassCastException("Object must be a EdgeImpl object"); } - if (((EdgeImpl) n).storeId == EdgeStore.NULL_ID) { + if (n.getStoreId() == EdgeStore.NULL_ID) { throw new IllegalArgumentException("Edge should belong to a store"); } } @@ -840,7 +840,7 @@ void checkValidEdgeObject(final Edge n) { void checkValidInViewEdgeObject(final Edge e) { checkValidEdgeObject(e); - if (!view.containsEdge((EdgeImpl) e)) { + if (!view.containsEdge(e)) { throw new RuntimeException("Edge doesn't belong to this view"); } } @@ -871,9 +871,15 @@ public NodeIterable getNodesInArea(Rect2D rect) { if (graphStore.spatialIndex == null) { throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); } - return new NodeIterableWrapper( - () -> new NodeViewIterator(graphStore.spatialIndex.getNodesInArea(rect).iterator()), - graphStore.spatialIndex.nodesTree.lock); + return graphStore.spatialIndex.getNodesInArea(rect, view::containsNode); + } + + @Override + public NodeIterable getApproximateNodesInArea(Rect2D rect) { + if (graphStore.spatialIndex == null) { + throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); + } + return graphStore.spatialIndex.getApproximateNodesInArea(rect, view::containsNode); } @Override @@ -881,49 +887,39 @@ public EdgeIterable getEdgesInArea(Rect2D rect) { if (graphStore.spatialIndex == null) { throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); } - return new EdgeIterableWrapper( - () -> new EdgeViewIterator(graphStore.spatialIndex.getEdgesInArea(rect).iterator()), - graphStore.spatialIndex.nodesTree.lock); + return graphStore.spatialIndex.getEdgesInArea(rect, view::containsEdge); + } + + @Override + public EdgeIterable getApproximateEdgesInArea(Rect2D rect) { + if (graphStore.spatialIndex == null) { + throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); + } + return graphStore.spatialIndex.getApproximateEdgesInArea(rect, view::containsEdge); } @Override public Rect2D getBoundaries() { - graphStore.autoReadLock(); - try { - float minX = Float.POSITIVE_INFINITY; - float minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY; - float maxY = Float.NEGATIVE_INFINITY; - - boolean hasNodes = false; - - // Iterate only through nodes visible in this view - for (Node node : getNodes()) { - hasNodes = true; - final float x = node.x(); - final float y = node.y(); - final float size = node.size(); - - final float nodeMinX = x - size; - final float nodeMinY = y - size; - final float nodeMaxX = x + size; - final float nodeMaxY = y + size; - - if (nodeMinX < minX) - minX = nodeMinX; - if (nodeMinY < minY) - minY = nodeMinY; - if (nodeMaxX > maxX) - maxX = nodeMaxX; - if (nodeMaxY > maxY) - maxY = nodeMaxY; - } + if (graphStore.spatialIndex == null) { + throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); + } + return graphStore.spatialIndex.getBoundaries(view::containsNode); + } - return hasNodes ? new Rect2D(minX, minY, maxX, maxY) : new Rect2D(Float.NEGATIVE_INFINITY, - Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); - } finally { - graphStore.autoReadUnlock(); + @Override + public void spatialIndexReadLock() { + if (graphStore.spatialIndex == null) { + throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); + } + graphStore.spatialIndex.spatialIndexReadLock(); + } + + @Override + public void spatialIndexReadUnlock() { + if (graphStore.spatialIndex == null) { + throw new UnsupportedOperationException("Spatial index is disabled (from Configuration)"); } + graphStore.spatialIndex.spatialIndexReadUnlock(); } private final class NodeViewSpliterator implements Spliterator { diff --git a/src/main/java/org/gephi/graph/impl/GraphViewImpl.java b/src/main/java/org/gephi/graph/impl/GraphViewImpl.java index a797f2a2..6842d67a 100644 --- a/src/main/java/org/gephi/graph/impl/GraphViewImpl.java +++ b/src/main/java/org/gephi/graph/impl/GraphViewImpl.java @@ -135,7 +135,7 @@ public boolean addNode(final Node node) { if (nodeView && !edgeView) { // Add edges - EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node); + EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, false); while (itr.hasNext()) { EdgeImpl edge = itr.next(); NodeImpl opposite = edge.source == nodeImpl ? edge.target : edge.source; @@ -234,7 +234,7 @@ public boolean removeNode(final Node node) { } // Remove edges - EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node); + EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, false); while (itr.hasNext()) { EdgeImpl edgeImpl = itr.next(); @@ -462,15 +462,15 @@ public void fill() { } } - public boolean containsNode(final NodeImpl node) { + public boolean containsNode(final Node node) { if (!nodeView) { return true; } - return nodeBitVector.get(node.storeId); + return nodeBitVector.get(node.getStoreId()); } - public boolean containsEdge(final EdgeImpl edge) { - return edgeBitVector.get(edge.storeId); + public boolean containsEdge(final Edge edge) { + return edgeBitVector.get(edge.getStoreId()); } public void intersection(final GraphViewImpl otherView) { @@ -915,7 +915,7 @@ private void checkValidNodeObject(final Node n) { if (!(n instanceof NodeImpl)) { throw new ClassCastException("Object must be a NodeImpl object"); } - if (((NodeImpl) n).storeId == NodeStore.NULL_ID) { + if (n.getStoreId() == NodeStore.NULL_ID) { throw new IllegalArgumentException("Node should belong to a store"); } } diff --git a/src/main/java/org/gephi/graph/impl/NodeStore.java b/src/main/java/org/gephi/graph/impl/NodeStore.java index 0ec778c9..14e43744 100644 --- a/src/main/java/org/gephi/graph/impl/NodeStore.java +++ b/src/main/java/org/gephi/graph/impl/NodeStore.java @@ -687,7 +687,7 @@ public NodeImpl next() { public void remove() { checkWriteLock(); if (edgeStore != null) { - for (EdgeStore.EdgeInOutIterator edgeIterator = edgeStore.edgeIterator(pointer); edgeIterator + for (EdgeStore.EdgeInOutIterator edgeIterator = edgeStore.edgeIterator(pointer, false); edgeIterator .hasNext();) { edgeIterator.next(); edgeIterator.remove(); diff --git a/src/main/java/org/gephi/graph/impl/NodesQuadTree.java b/src/main/java/org/gephi/graph/impl/NodesQuadTree.java index 172c3490..a0996d64 100644 --- a/src/main/java/org/gephi/graph/impl/NodesQuadTree.java +++ b/src/main/java/org/gephi/graph/impl/NodesQuadTree.java @@ -1,14 +1,35 @@ +/* + * Copyright 2012-2013 Gephi Consortium + * + * Licensed 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.gephi.graph.impl; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.ConcurrentModificationException; +import org.gephi.graph.api.Edge; +import org.gephi.graph.api.EdgeIterable; import org.gephi.graph.api.Node; import org.gephi.graph.api.NodeIterable; import org.gephi.graph.api.Rect2D; @@ -25,16 +46,23 @@ public class NodesQuadTree { private final QuadTreeNode quadTreeRoot; private final int maxLevels; private final int maxObjectsPerNode; + private final GraphStore graphStore; + private int version = 0; public NodesQuadTree(Rect2D rect) { - this(rect, GraphStoreConfiguration.SPATIAL_INDEX_MAX_LEVELS, + this(null, rect); + } + + public NodesQuadTree(GraphStore store, Rect2D rect) { + this(store, rect, GraphStoreConfiguration.SPATIAL_INDEX_MAX_LEVELS, GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE); } - public NodesQuadTree(Rect2D rect, int maxLevels, int maxObjectsPerNode) { + public NodesQuadTree(GraphStore store, Rect2D rect, int maxLevels, int maxObjectsPerNode) { this.quadTreeRoot = new QuadTreeNode(rect); this.maxLevels = maxLevels; this.maxObjectsPerNode = maxObjectsPerNode; + this.graphStore = store; } public Rect2D quadRect() { @@ -45,14 +73,42 @@ public NodeIterable getNodes(Rect2D searchRect) { return quadTreeRoot.getNodes(searchRect); } - public NodeIterable getNodes(float minX, float minY, float maxX, float maxY) { - return quadTreeRoot.getNodes(new Rect2D(minX, minY, maxX, maxY)); + public NodeIterable getNodes(Rect2D searchRect, boolean approximate) { + return quadTreeRoot.getNodes(searchRect, approximate); + } + + public NodeIterable getNodes(Rect2D searchRect, boolean approximate, Predicate predicate) { + return quadTreeRoot.getNodes(searchRect, approximate, predicate); } public NodeIterable getAllNodes() { return quadTreeRoot.getAllNodes(); } + public NodeIterable getAllNodes(Predicate predicate) { + return quadTreeRoot.getAllNodes(predicate); + } + + public EdgeIterable getEdges() { + return quadTreeRoot.getAllEdges(); + } + + public EdgeIterable getEdges(Rect2D searchRect) { + return quadTreeRoot.getEdges(searchRect); + } + + public EdgeIterable getEdges(Rect2D searchRect, boolean approximate) { + return quadTreeRoot.getEdges(searchRect, approximate); + } + + public EdgeIterable getEdges(Rect2D searchRect, boolean approximate, Predicate predicate) { + return quadTreeRoot.getEdges(searchRect, approximate, predicate); + } + + public void incrementVersion() { + version++; + } + public boolean updateNode(NodeImpl item, float minX, float minY, float maxX, float maxY) { writeLock(); try { @@ -60,6 +116,7 @@ public boolean updateNode(NodeImpl item, float minX, float minY, float maxX, flo if (obj != null) { obj.updateBoundaries(minX, minY, maxX, maxY); quadTreeRoot.update(item); + version++; return true; } else { return false; @@ -86,6 +143,7 @@ public boolean addNode(NodeImpl item) { spatialData = new SpatialNodeDataImpl(minX, minY, maxX, maxY); item.setSpatialData(spatialData); quadTreeRoot.insert(item); + version++; return true; } else { return false; @@ -100,9 +158,10 @@ public void clear() { try { for (Node node : getAllNodes()) { SpatialNodeDataImpl spatialData = ((NodeImpl) node).getSpatialData(); - spatialData.setQuadTreeNode(null); + spatialData.clear(); } quadTreeRoot.clear(); + version++; } finally { writeUnlock(); } @@ -114,6 +173,7 @@ public boolean removeNode(NodeImpl item) { final SpatialNodeDataImpl spatialData = item.getSpatialData(); if (spatialData != null && spatialData.quadTreeNode != null) { quadTreeRoot.delete(item, true); + version++; return true; } return false; @@ -169,10 +229,21 @@ public int getDepth() { return depth; } + public int getNodeCount(boolean keepOnlyWithObjects) { + readLock(); + int count = quadTreeRoot.getNodeCount(keepOnlyWithObjects); + readUnlock(); + return count; + } + public Rect2D getBoundaries() { + return getBoundaries(null); + } + + public Rect2D getBoundaries(Predicate predicate) { readLock(); try { - NodeIterable allNodes = getAllNodes(); + NodeIterable allNodes = predicate == null ? getAllNodes() : getAllNodes(predicate); float minX = Float.POSITIVE_INFINITY; float minY = Float.POSITIVE_INFINITY; @@ -182,6 +253,9 @@ public Rect2D getBoundaries() { boolean hasNodes = false; for (Node node : allNodes) { + if (node == null) { + continue; + } SpatialNodeDataImpl spatialData = ((NodeImpl) node).getSpatialData(); if (spatialData != null) { hasNodes = true; @@ -207,13 +281,37 @@ public Rect2D getBoundaries() { } } + private int collectOverlapping(QuadTreeNode node, Rect2D searchRect, Set resultSet) { + if (searchRect != null && !node.rect.intersects(searchRect)) { + return 0; + } + + // If this node has objects and intersects with search rect, add it + int nodeCount = 0; + if (node.objectCount > 0) { + resultSet.add(node); + nodeCount += node.objectCount; + } + + // Recursively check children + if (node.childTL != null) { + nodeCount += collectOverlapping(node.childTL, searchRect, resultSet); + nodeCount += collectOverlapping(node.childTR, searchRect, resultSet); + nodeCount += collectOverlapping(node.childBL, searchRect, resultSet); + nodeCount += collectOverlapping(node.childBR, searchRect, resultSet); + } + return nodeCount; + } + protected class QuadTreeNode { - private Set objects = null; + private NodeImpl[] objects = null; // Fixed-size array for objects + private int objectCount = 0; // Number of objects currently in this node private final Rect2D rect; // The area this QuadTree represents private final QuadTreeNode parent; // The parent of this quad private final int level; + private int size = 0; // Total number of objects in this node and its children private QuadTreeNode childTL = null; // Top Left Child private QuadTreeNode childTR = null; // Top Right Child @@ -245,11 +343,11 @@ public QuadTreeNode parent() { } public int count() { - return objectCount(); + return size; } public boolean isEmptyLeaf() { - return count() == 0 && childTL == null; + return size == 0 && childTL == null; } public QuadTreeNode(Rect2D rect) { @@ -264,36 +362,70 @@ private QuadTreeNode(QuadTreeNode parent, int level, Rect2D rect) { private void add(NodeImpl item) { if (objects == null) { - objects = new LinkedHashSet<>(); + // Allocate initial array + objects = new NodeImpl[maxObjectsPerNode / 16]; } - item.getSpatialData().setQuadTreeNode(this); - objects.add(item); - } + // Check if item is already in this node (avoid duplicates) + SpatialNodeDataImpl spatialData = item.getSpatialData(); + if (spatialData.quadTreeNode == this && spatialData.arrayIndex >= 0) { + return; // Already in this node + } - private void remove(NodeImpl item) { - if (objects != null) { - objects.remove(item); + // Resize array if needed (can happen when at max depth or when objects don't + // fit in children) + if (objectCount >= objects.length) { + NodeImpl[] newArray = new NodeImpl[objects.length * 2]; + System.arraycopy(objects, 0, newArray, 0, objects.length); + objects = newArray; + } + + // Add to array + objects[objectCount] = item; + spatialData.setQuadTreeNode(this); + spatialData.setArrayIndex(objectCount); + objectCount++; + + // Update size and edge size for this node and all parents + QuadTreeNode node = this; + while (node != null) { + node.size++; + node = node.parent; } } - private int objectCount() { - int count = 0; + private void remove(NodeImpl item) { + if (objects != null && objectCount > 0) { + SpatialNodeDataImpl spatialData = item.getSpatialData(); + int index = spatialData.arrayIndex; + + if (index >= 0 && index < objectCount && objects[index] == item) { + // Swap with last element for O(1) removal + objectCount--; + NodeImpl lastItem = objects[objectCount]; + objects[index] = lastItem; + objects[objectCount] = null; + + // Update the moved item's index + if (lastItem != null && index < objectCount) { + lastItem.getSpatialData().setArrayIndex(index); + } - // add the objects at this level - if (objects != null) { - count += objects.size(); - } + // Clear removed item's data + spatialData.clear(); - // add the objects that are contained in the children - if (childTL != null) { - count += childTL.objectCount(); - count += childTR.objectCount(); - count += childBL.objectCount(); - count += childBR.objectCount(); + // Update size + QuadTreeNode node = this; + while (node != null) { + node.size--; + node = node.parent; + } + } } + } - return count; + private int objectCount() { + return size; } private void subdivide() { @@ -311,19 +443,40 @@ private void subdivide() { childBL = new QuadTreeNode(this, level + 1, new Rect2D(minX, halfY, halfX, maxY)); childBR = new QuadTreeNode(this, level + 1, new Rect2D(halfX, halfY, maxX, maxY)); + // Keep track of objects that couldn't be moved + NodeImpl[] remainingObjects = new NodeImpl[objectCount]; + int remainingCount = 0; + // If they're completely contained by the quad, bump objects down - final Iterator iterator = objects.iterator(); - while (iterator.hasNext()) { - NodeImpl obj = iterator.next(); + for (int i = 0; i < objectCount; i++) { + NodeImpl obj = objects[i]; QuadTreeNode destTree = getDestinationTree(obj); if (destTree != this) { - // Insert to the appropriate tree, remove the object, and - // back up one in the loop + // Insert to the appropriate tree destTree.insert(obj); - iterator.remove(); + // Update size + QuadTreeNode node = this; + while (node != null) { + node.size--; + node = node.parent; + } + } else { + // Keep this object in the current node + remainingObjects[remainingCount] = obj; + obj.getSpatialData().setArrayIndex(remainingCount); + remainingCount++; } } + + // Update this node's object array + for (int i = 0; i < remainingCount; i++) { + objects[i] = remainingObjects[i]; + } + for (int i = remainingCount; i < objectCount; i++) { + objects[i] = null; + } + objectCount = remainingCount; } private QuadTreeNode getDestinationTree(NodeImpl item) { @@ -412,10 +565,21 @@ private void clear() { // clear any objects at this level if (objects != null) { - objects.clear(); + // Clear spatial data references for all objects + for (int i = 0; i < objectCount; i++) { + if (objects[i] != null) { + SpatialNodeDataImpl spatialData = objects[i].getSpatialData(); + spatialData.clear(); + objects[i] = null; + } + } objects = null; + objectCount = 0; } + // Reset size and edge size + size = 0; + // Set the children to null childTL = null; childTR = null; @@ -452,8 +616,7 @@ private void insert(NodeImpl item) { } } - if (objects == null || (childTL == null && (level >= maxLevels || objects - .size() + 1 <= maxObjectsPerNode))) { + if (objects == null || (childTL == null && (level >= maxLevels || objectCount + 1 <= maxObjectsPerNode))) { // If there's room to add the object, just add it add(item); } else { @@ -476,10 +639,38 @@ private NodeIterable getNodes(Rect2D searchRect) { return new QuadTreeNodesIterable(searchRect); } + private NodeIterable getNodes(Rect2D searchRect, boolean approximate) { + return new QuadTreeNodesIterable(searchRect, approximate); + } + + private NodeIterable getNodes(Rect2D searchRect, boolean approximate, Predicate predicate) { + return new FilteredQuadTreeNodeIterable(searchRect, approximate, predicate); + } + private NodeIterable getAllNodes() { return new QuadTreeNodesIterable(null); } + private NodeIterable getAllNodes(Predicate predicate) { + return new FilteredQuadTreeNodeIterable(null, false, predicate); + } + + private EdgeIterable getEdges(Rect2D searchRect) { + return new QuadTreeEdgesIterable(searchRect); + } + + private EdgeIterable getEdges(Rect2D searchRect, boolean approximate) { + return new QuadTreeEdgesIterable(searchRect, approximate); + } + + private EdgeIterable getEdges(Rect2D searchRect, boolean approximate, Predicate predicate) { + return new FilteredQuadTreeEdgeIterable(searchRect, approximate, predicate); + } + + private EdgeIterable getAllEdges() { + return new QuadTreeEdgesIterable(null); + } + private void update(NodeImpl item) { SpatialNodeDataImpl spatialData = item.getSpatialData(); if (spatialData.quadTreeNode != null) { @@ -500,6 +691,25 @@ private int getDepth() { return maxLevel; } + private int getNodeCount(boolean withObjects) { + int count = 1; // Count this node + + // If withObjects is true, only count nodes that have objects + if (withObjects && (objects == null || objectCount == 0)) { + count = 0; + } + + // Recursively count children + if (childTL != null) { + count += childTL.getNodeCount(withObjects); + count += childTR.getNodeCount(withObjects); + count += childBL.getNodeCount(withObjects); + count += childBR.getNodeCount(withObjects); + } + + return count; + } + public void toString(StringBuilder sb) { for (int i = 0; i < level; i++) { sb.append(" "); @@ -507,13 +717,11 @@ public void toString(StringBuilder sb) { sb.append(rect.toString()).append('\n'); if (objects != null) { - for (NodeImpl object : objects) { - for (int i = 0; i <= level; i++) { - sb.append(" "); - } - - sb.append(object.getId()).append('\n'); + for (int j = 0; j <= level; j++) { + sb.append(" "); } + + sb.append(objectCount).append(" objects \n"); } if (childTL != null) { @@ -525,23 +733,53 @@ public void toString(StringBuilder sb) { } } + private class FilteredQuadTreeNodeIterable extends QuadTreeNodesIterable { + + private final Predicate predicate; + + public FilteredQuadTreeNodeIterable(Rect2D searchRect, boolean approximate, Predicate predicate) { + super(searchRect, approximate); + this.predicate = predicate; + } + + @Override + public Iterator iterator() { + return new QuadTreeNodesIterator(quadTreeRoot, searchRect, approximate, predicate); + } + + @Override + public Spliterator spliterator() { + return new FilteredQuadTreeNodesSpliterator(quadTreeRoot, searchRect, approximate, predicate); + } + } + private class QuadTreeNodesIterable implements NodeIterable { - private final Rect2D searchRect; + protected final Rect2D searchRect; + protected final boolean approximate; public QuadTreeNodesIterable(Rect2D searchRect) { + this(searchRect, GraphStoreConfiguration.SPATIAL_INDEX_APPROXIMATE_AREA_SEARCH); + } + + public QuadTreeNodesIterable(Rect2D searchRect, boolean approximate) { this.searchRect = searchRect; + this.approximate = approximate; } @Override public Iterator iterator() { - return new QuadTreeNodesIterator(quadTreeRoot, searchRect); + return new QuadTreeNodesIterator(quadTreeRoot, searchRect, approximate); + } + + @Override + public Spliterator spliterator() { + return new QuadTreeNodesSpliterator(quadTreeRoot, searchRect, approximate); } @Override public Node[] toArray() { - final Collection collection = toCollection(); - return collection.toArray(new Node[0]); + return toCollection().toArray(new Node[0]); } @Override @@ -570,12 +808,183 @@ public Set toSet() { public void doBreak() { readUnlock(); } + } + + private class FilteredQuadTreeEdgeIterable extends QuadTreeEdgesIterable { + + private final Predicate predicate; + + public FilteredQuadTreeEdgeIterable(Rect2D searchRect, boolean approximate, Predicate predicate) { + super(searchRect, approximate); + this.predicate = predicate; + } + + @Override + public Iterator iterator() { + return new QuadTreeEdgesIterator(quadTreeRoot, searchRect, approximate, predicate); + } + + @Override + public Spliterator spliterator() { + HashSet overlappingNodes = new HashSet<>(); + int nodeCount = collectOverlapping(quadTreeRoot, searchRect, overlappingNodes); + if (useDirectIterator(nodeCount)) { + return new QuadTreeGlobalEdgesSpliterator(searchRect, approximate, overlappingNodes, predicate); + } + // Use local iterator + return new FilteredQuadTreeEdgesSpliterator(quadTreeRoot, searchRect, approximate, predicate); + } + } + + private class QuadTreeEdgesIterable implements EdgeIterable { + + protected final Rect2D searchRect; + protected final boolean approximate; + + public QuadTreeEdgesIterable(Rect2D searchRect) { + this(searchRect, GraphStoreConfiguration.SPATIAL_INDEX_APPROXIMATE_AREA_SEARCH); + } + + public QuadTreeEdgesIterable(Rect2D searchRect, boolean approximate) { + this.searchRect = searchRect; + this.approximate = approximate; + } + + @Override + public Iterator iterator() { + return new QuadTreeEdgesIterator(quadTreeRoot, searchRect, approximate); + } + + protected boolean useDirectIterator(int nodeCount) { + return (float) nodeCount / quadTreeRoot.size > GraphStoreConfiguration.SPATIAL_INDEX_LOCAL_ITERATOR_THRESHOLD; + } + + @Override + public Spliterator spliterator() { + if (searchRect == null) { + // Special case: all edges + return new QuadTreeGlobalEdgesSpliterator(null, approximate, null, null); + } + HashSet overlappingNodes = new HashSet<>(); + int nodeCount = collectOverlapping(quadTreeRoot, searchRect, overlappingNodes); + if (approximate && nodeCount == quadTreeRoot.size) { + // Optimisation: approximate search and all nodes overlapping, so just return + // all edges + return new QuadTreeGlobalEdgesSpliterator(null, true, null, null); + } else if (useDirectIterator(nodeCount)) { + return new QuadTreeGlobalEdgesSpliterator(searchRect, approximate, overlappingNodes, null); + } + // Use local iterator + return new QuadTreeEdgesSpliterator(quadTreeRoot, searchRect, approximate); + } + + @Override + public Edge[] toArray() { + return toCollection().toArray(new Edge[0]); + } + + @Override + public Collection toCollection() { + final List list = new ArrayList<>(); + + for (Edge edge : this) { + list.add(edge); + } + + return list; + } + + @Override + public Set toSet() { + final Set set = new HashSet<>(); + + for (Edge edge : this) { + set.add(edge); + } + + return set; + } + + @Override + public void doBreak() { + readUnlock(); + } + + } + + private class QuadTreeEdgesIterator implements Iterator { + + private final EdgeStore.EdgeInOutMultiIterator edgeIterator; + private final Predicate predicate; + private boolean finished = false; + private Edge next; + + public QuadTreeEdgesIterator(QuadTreeNode root, Rect2D searchRect, boolean approximate) { + this(root, searchRect, approximate, null); + } + + public QuadTreeEdgesIterator(QuadTreeNode root, Rect2D searchRect, boolean approximate, Predicate predicate) { + this.predicate = predicate; + readLock(); + + // Create a node iterator for the quad tree + final QuadTreeNodesIterator nodeIterator = new QuadTreeNodesIterator(root, searchRect, approximate); + + // Create the edge iterator using the EdgeStore method + this.edgeIterator = graphStore.edgeStore.edgeIterator(new Iterator<>() { + @Override + public boolean hasNext() { + return nodeIterator.hasNext(); + } + + @Override + public NodeImpl next() { + return nodeIterator.next(); + } + }, true); + } + + @Override + public boolean hasNext() { + if (finished) { + return false; + } + + if (next != null) { + return true; + } + + // Look for next edge that passes predicate + while (edgeIterator != null && edgeIterator.hasNext()) { + Edge edge = edgeIterator.next(); + if (predicate == null || predicate.test(edge)) { + next = edge; + return true; + } + } + + readUnlock(); + finished = true; + return false; + } + + @Override + public Edge next() { + if (next == null && !hasNext()) { + throw new IllegalStateException("No next available!"); + } + Edge result = next; + next = null; + return result; + } } private class QuadTreeNodesIterator implements Iterator { private final Rect2D searchRect; + private final boolean approximate; + private final Predicate predicate; private final Deque nodesStack = new ArrayDeque<>(); private final Deque fullyContainedStack = new ArrayDeque<>(); @@ -586,8 +995,14 @@ private class QuadTreeNodesIterator implements Iterator { private NodeImpl next; - public QuadTreeNodesIterator(QuadTreeNode root, Rect2D searchRect) { + public QuadTreeNodesIterator(QuadTreeNode root, Rect2D searchRect, boolean approximate) { + this(root, searchRect, approximate, null); + } + + public QuadTreeNodesIterator(QuadTreeNode root, Rect2D searchRect, boolean approximate, Predicate predicate) { this.searchRect = searchRect; + this.approximate = approximate; + this.predicate = predicate; readLock(); @@ -598,7 +1013,7 @@ public QuadTreeNodesIterator(QuadTreeNode root, Rect2D searchRect) { // contained, to correctly handle the case of nodes out of the quad // tree bounds addChildrenToVisit(root, currentFullyContained); - currentIterator = root.objects != null ? root.objects.iterator() : null; + currentIterator = root.objects != null ? new ArrayIterator(root.objects, root.objectCount) : null; } private void addChildrenToVisit(QuadTreeNode quadTreeNode, boolean fullyContained) { @@ -629,10 +1044,21 @@ public boolean hasNext() { if (currentIterator != null) { while (currentIterator.hasNext()) { final NodeImpl elem = currentIterator.next(); - final SpatialNodeDataImpl spatialData = elem.getSpatialData(); - if (currentFullyContained || searchRect - .intersects(spatialData.minX, spatialData.minY, spatialData.maxX, spatialData.maxY)) { + // First check spatial conditions + boolean spatialMatch; + if (approximate || currentFullyContained) { + // In approximate mode or when fully contained, include all objects + spatialMatch = true; + } else { + // In exact mode, check intersection + final SpatialNodeDataImpl spatialData = elem.getSpatialData(); + spatialMatch = searchRect + .intersects(spatialData.minX, spatialData.minY, spatialData.maxX, spatialData.maxY); + } + + // If spatial conditions are met, check predicate + if (spatialMatch && (predicate == null || predicate.test(elem))) { next = elem; return true; } @@ -646,7 +1072,8 @@ public boolean hasNext() { if (currentFullyContained || pointer.rect.intersects(searchRect)) { addChildrenToVisit(pointer, currentFullyContained); - currentIterator = pointer.objects != null ? pointer.objects.iterator() : null; + currentIterator = pointer.objects != null + ? new ArrayIterator(pointer.objects, pointer.objectCount) : null; } else { currentIterator = null; } @@ -667,9 +1094,633 @@ public NodeImpl next() { final NodeImpl node = next; next = null; - return node; } } + + // Helper class to iterate over array elements + private static class ArrayIterator implements Iterator { + private final NodeImpl[] array; + private final int size; + private final Predicate predicate; + private int index = 0; + private NodeImpl next; + + public ArrayIterator(NodeImpl[] array, int size) { + this(array, size, null); + } + + public ArrayIterator(NodeImpl[] array, int size, Predicate predicate) { + this.array = array; + this.size = size; + this.predicate = predicate; + } + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + + // Find next element that passes predicate + while (index < size) { + NodeImpl candidate = array[index++]; + if (predicate == null || predicate.test(candidate)) { + next = candidate; + return true; + } + } + return false; + } + + @Override + public NodeImpl next() { + if (next == null && !hasNext()) { + throw new IllegalStateException("No more elements"); + } + NodeImpl result = next; + next = null; + return result; + } + } + + private abstract class AbstractQuadTreeSpliterator implements Spliterator { + protected final Rect2D searchRect; + protected final boolean approximate; + protected final Deque nodesStack = new ArrayDeque<>(); + protected final Deque fullyContainedStack = new ArrayDeque<>(); + + protected final int expectedVersion; + protected Iterator currentIterator; + protected boolean currentFullyContained; + protected T next; + protected int remainingSize; + + protected AbstractQuadTreeSpliterator(QuadTreeNode root, Rect2D searchRect, boolean approximate) { + this.searchRect = searchRect; + this.approximate = approximate; + this.expectedVersion = version; + + // Null rect means get all + currentFullyContained = searchRect == null; + + // Initialize with root + addNode(root, currentFullyContained); + currentIterator = createIteratorForNode(root); + + // For SIZED characteristic, we need exact count + if (searchRect == null) { + // Getting all elements, so we can use the maintained size + remainingSize = root.size; + } else if (approximate) { + // In approximate mode, count all elements in intersecting quadrants + remainingSize = countNodesInRectApproximate(root, searchRect); + } else { + // Need to count elements in the search rect + remainingSize = countNodesInRect(root, searchRect); + } + } + + protected AbstractQuadTreeSpliterator(QuadTreeNode node, Rect2D searchRect, boolean approximate, int expectedVersion, boolean fullyContained, int size) { + this.searchRect = searchRect; + this.approximate = approximate; + this.expectedVersion = expectedVersion; + this.remainingSize = size; + this.currentFullyContained = fullyContained; + + if (node != null) { + addNode(node, fullyContained); + currentIterator = createIteratorForNode(node); + } else { + currentIterator = null; + } + } + + protected void checkForComodification() { + if (version != expectedVersion) { + throw new ConcurrentModificationException(); + } + } + + protected void addNode(QuadTreeNode node, boolean fullyContained) { + if (node.childTL != null) { + nodesStack.push(node.childBR); + nodesStack.push(node.childBL); + nodesStack.push(node.childTR); + nodesStack.push(node.childTL); + + fullyContainedStack.push(fullyContained); + fullyContainedStack.push(fullyContained); + fullyContainedStack.push(fullyContained); + fullyContainedStack.push(fullyContained); + } + } + + protected int countNodesInRect(QuadTreeNode node, Rect2D rect) { + int count = 0; + + // Count objects at this level + if (node.objects != null) { + for (int i = 0; i < node.objectCount; i++) { + NodeImpl obj = node.objects[i]; + SpatialNodeDataImpl spatialData = obj.getSpatialData(); + if (rect.intersects(spatialData.minX, spatialData.minY, spatialData.maxX, spatialData.maxY)) { + count++; + } + } + } + + // Count in children if they intersect + if (node.childTL != null) { + if (rect.contains(node.childTL.rect)) { + count += node.childTL.size; + } else if (node.childTL.rect.intersects(rect)) { + count += countNodesInRect(node.childTL, rect); + } + + if (rect.contains(node.childTR.rect)) { + count += node.childTR.size; + } else if (node.childTR.rect.intersects(rect)) { + count += countNodesInRect(node.childTR, rect); + } + + if (rect.contains(node.childBL.rect)) { + count += node.childBL.size; + } else if (node.childBL.rect.intersects(rect)) { + count += countNodesInRect(node.childBL, rect); + } + + if (rect.contains(node.childBR.rect)) { + count += node.childBR.size; + } else if (node.childBR.rect.intersects(rect)) { + count += countNodesInRect(node.childBR, rect); + } + } + + return count; + } + + protected int countNodesInRectApproximate(QuadTreeNode node, Rect2D rect) { + int count = 0; + + // Count objects at this level if the node intersects + if (node.objects != null) { + count += node.objectCount; + } + + // Count in children if they intersect + if (node.childTL != null) { + if (rect.containsOrIntersects(node.childTL.rect)) { + count += node.childTL.size; + } + if (rect.containsOrIntersects(node.childTR.rect)) { + count += node.childTR.size; + } + if (rect.containsOrIntersects(node.childBL.rect)) { + count += node.childBL.size; + } + if (rect.containsOrIntersects(node.childBR.rect)) { + count += node.childBR.size; + } + } + + return count; + } + + @Override + public boolean tryAdvance(Consumer action) { + checkForComodification(); + + if (next != null || findNext()) { + action.accept(next); + next = null; + remainingSize--; + return true; + } + return false; + } + + protected abstract boolean findNext(); + + protected abstract Iterator createIteratorForNode(QuadTreeNode node); + + protected abstract boolean checkElementSpatialMatch(Object element); + + protected abstract AbstractQuadTreeSpliterator createSplitInstance(QuadTreeNode node, Rect2D searchRect, boolean approximate, int expectedVersion, boolean fullyContained, int size); + + @Override + public Spliterator trySplit() { + checkForComodification(); + + // Can only split if we have nodes on the stack + if (!nodesStack.isEmpty() && remainingSize > 1) { + // Take half of the remaining nodes from the stack + int nodesToSplit = Math.min(nodesStack.size() / 2, remainingSize / 2); + if (nodesToSplit > 0) { + Deque splitNodes = new ArrayDeque<>(); + Deque splitContained = new ArrayDeque<>(); + + // Calculate size for the split + int splitSize = 0; + + // Move nodes to split queues and calculate their size + for (int i = 0; i < nodesToSplit; i++) { + QuadTreeNode node = nodesStack.removeLast(); + boolean contained = fullyContainedStack.removeLast(); + splitNodes.addFirst(node); + splitContained.addFirst(contained); + + if (searchRect == null || contained) { + splitSize += node.size; + } else if (approximate) { + splitSize += countNodesInRectApproximate(node, searchRect); + } else { + splitSize += countNodesInRect(node, searchRect); + } + } + + // Update our remaining size + remainingSize -= splitSize; + + // Create new spliterator for the split portion with empty initial state + AbstractQuadTreeSpliterator split = createSplitInstance(null, searchRect, approximate, expectedVersion, false, splitSize); + + // Add all split nodes to the new spliterator's stack + while (!splitNodes.isEmpty()) { + split.nodesStack.push(splitNodes.removeLast()); + split.fullyContainedStack.push(splitContained.removeLast()); + } + + return split; + } + } + return null; + } + + @Override + public long estimateSize() { + return remainingSize; + } + } + + private class QuadTreeNodesSpliterator extends AbstractQuadTreeSpliterator { + + public QuadTreeNodesSpliterator(QuadTreeNode root, Rect2D searchRect, boolean approximate) { + super(root, searchRect, approximate); + } + + private QuadTreeNodesSpliterator(QuadTreeNode node, Rect2D searchRect, boolean approximate, int expectedVersion, boolean fullyContained, int size) { + super(node, searchRect, approximate, expectedVersion, fullyContained, size); + } + + @Override + protected Iterator createIteratorForNode(QuadTreeNode node) { + return node.objects != null ? new ArrayIterator(node.objects, node.objectCount) : null; + } + + @Override + protected boolean checkElementSpatialMatch(Object element) { + NodeImpl elem = (NodeImpl) element; + if (approximate || currentFullyContained || searchRect == null) { + return true; + } else { + final SpatialNodeDataImpl spatialData = elem.getSpatialData(); + return searchRect.intersects(spatialData.minX, spatialData.minY, spatialData.maxX, spatialData.maxY); + } + } + + @Override + protected AbstractQuadTreeSpliterator createSplitInstance(QuadTreeNode node, Rect2D searchRect, boolean approximate, int expectedVersion, boolean fullyContained, int size) { + return new QuadTreeNodesSpliterator(node, searchRect, approximate, expectedVersion, fullyContained, size); + } + + @Override + protected boolean findNext() { + while (currentIterator != null || !nodesStack.isEmpty()) { + if (currentIterator != null) { + while (currentIterator.hasNext()) { + final NodeImpl elem = (NodeImpl) currentIterator.next(); + + if (checkElementSpatialMatch(elem)) { + next = elem; + return true; + } + } + currentIterator = null; + } else { + final QuadTreeNode pointer = nodesStack.pop(); + currentFullyContained = fullyContainedStack + .pop() || (searchRect != null && searchRect.contains(pointer.rect)); + + if (currentFullyContained || searchRect == null || pointer.rect.intersects(searchRect)) { + addNode(pointer, currentFullyContained); + currentIterator = createIteratorForNode(pointer); + } + } + } + return false; + } + + @Override + public int characteristics() { + return DISTINCT | SIZED | SUBSIZED | NONNULL; + } + } + + private abstract static class FilteredSpliterator, P extends Spliterator> implements Spliterator { + protected final S parentSpliterator; + protected final Predicate predicate; + protected final Object[] holder = new Object[1]; + + protected FilteredSpliterator(S parentSpliterator, Predicate predicate) { + this.parentSpliterator = parentSpliterator; + this.predicate = predicate; + } + + protected abstract P createSplitInstance(S splitParent, Predicate predicate); + + protected abstract boolean testPredicate(T element); + + @Override + public boolean tryAdvance(Consumer action) { + while (true) { + boolean advanced = parentSpliterator.tryAdvance(e -> holder[0] = e); + if (!advanced) + return false; + @SuppressWarnings("unchecked") + T t = (T) holder[0]; + if (testPredicate(t)) { + action.accept(t); + return true; + } + } + } + + @Override + @SuppressWarnings("unchecked") + public Spliterator trySplit() { + S splitParent = (S) parentSpliterator.trySplit(); + if (splitParent != null) { + return createSplitInstance(splitParent, predicate); + } + return null; + } + + @Override + public long estimateSize() { + return parentSpliterator.estimateSize(); + } + + @Override + public int characteristics() { + return parentSpliterator.characteristics() & ~(Spliterator.SIZED | Spliterator.SUBSIZED); + } + } + + private class FilteredQuadTreeNodesSpliterator extends FilteredSpliterator { + + public FilteredQuadTreeNodesSpliterator(QuadTreeNode root, Rect2D searchRect, boolean approximate, Predicate predicate) { + super(new QuadTreeNodesSpliterator(root, searchRect, approximate), predicate); + } + + private FilteredQuadTreeNodesSpliterator(QuadTreeNodesSpliterator parentSpliterator, Predicate predicate) { + super(parentSpliterator, predicate); + } + + @Override + protected FilteredQuadTreeNodesSpliterator createSplitInstance(QuadTreeNodesSpliterator splitParent, Predicate predicate) { + return new FilteredQuadTreeNodesSpliterator(splitParent, predicate); + } + + @Override + protected boolean testPredicate(Node element) { + return predicate == null || predicate.test(element); + } + } + + private class QuadTreeEdgesSpliterator extends AbstractQuadTreeSpliterator { + + public QuadTreeEdgesSpliterator(QuadTreeNode root, Rect2D searchRect) { + this(root, searchRect, false); + } + + public QuadTreeEdgesSpliterator(QuadTreeNode root, Rect2D searchRect, boolean approximate) { + super(root, searchRect, approximate); + } + + private QuadTreeEdgesSpliterator(QuadTreeNode node, Rect2D searchRect, boolean approximate, int expectedVersion, boolean fullyContained, int size) { + super(node, searchRect, approximate, expectedVersion, fullyContained, size); + } + + @Override + protected Iterator createIteratorForNode(QuadTreeNode node) { + if (node.objects == null) { + return Collections.emptyIterator(); + } + return graphStore.edgeStore.edgeIterator(new ArrayIterator(node.objects, node.objectCount), false); + } + + @Override + protected boolean checkElementSpatialMatch(Object element) { + Edge edge = (Edge) element; + if (approximate || currentFullyContained || searchRect == null) { + return true; + } else { + // In exact mode, check if edge endpoints intersect with search rect + Node source = edge.getSource(); + Node target = edge.getTarget(); + SpatialNodeDataImpl sourceSpatialData = ((NodeImpl) source).getSpatialData(); + SpatialNodeDataImpl targetSpatialData = ((NodeImpl) target).getSpatialData(); + + return (sourceSpatialData != null && searchRect + .intersects(sourceSpatialData.minX, sourceSpatialData.minY, sourceSpatialData.maxX, sourceSpatialData.maxY)) || (targetSpatialData != null && searchRect + .intersects(targetSpatialData.minX, targetSpatialData.minY, targetSpatialData.maxX, targetSpatialData.maxY)); + } + } + + @Override + protected AbstractQuadTreeSpliterator createSplitInstance(QuadTreeNode node, Rect2D searchRect, boolean approximate, int expectedVersion, boolean fullyContained, int size) { + return new QuadTreeEdgesSpliterator(node, searchRect, approximate, expectedVersion, fullyContained, size); + } + + @Override + protected boolean findNext() { + while (currentIterator != null || !nodesStack.isEmpty()) { + if (currentIterator != null) { + if (currentIterator.hasNext()) { + Edge edge = (Edge) currentIterator.next(); + + if (checkElementSpatialMatch(edge)) { + next = edge; + return true; + } + } else { + currentIterator = null; + } + } else { + final QuadTreeNode pointer = nodesStack.pop(); + currentFullyContained = fullyContainedStack + .pop() || (searchRect != null && searchRect.contains(pointer.rect)); + + if (currentFullyContained || searchRect == null || pointer.rect.intersects(searchRect)) { + addNode(pointer, currentFullyContained); + currentIterator = createIteratorForNode(pointer); + } + } + } + return false; + } + + @Override + public int characteristics() { + return NONNULL; + } + } + + private class FilteredQuadTreeEdgesSpliterator extends FilteredSpliterator { + + public FilteredQuadTreeEdgesSpliterator(QuadTreeNode root, Rect2D searchRect, boolean approximate, Predicate predicate) { + super(new QuadTreeEdgesSpliterator(root, searchRect, approximate), predicate); + } + + private FilteredQuadTreeEdgesSpliterator(QuadTreeEdgesSpliterator parentSpliterator, Predicate predicate) { + super(parentSpliterator, predicate); + } + + @Override + protected FilteredQuadTreeEdgesSpliterator createSplitInstance(QuadTreeEdgesSpliterator splitParent, Predicate predicate) { + return new FilteredQuadTreeEdgesSpliterator(splitParent, predicate); + } + + @Override + protected boolean testPredicate(Edge element) { + return predicate == null || predicate.test(element); + } + } + + /** + * A spliterator that iterates through all edges in the EdgeStore and filters + * them based on whether their nodes belong to quad tree nodes that overlap with + * a search rectangle. This approach iterates edges directly rather than + * iterating nodes first. + */ + protected class QuadTreeGlobalEdgesSpliterator implements Spliterator { + + private final Rect2D searchRect; + private final boolean approximate; + private final Set overlappingQuadNodes; + private final Spliterator baseSpliterator; + private final int expectedVersion; + private final Predicate additionalPredicate; + + public QuadTreeGlobalEdgesSpliterator(Rect2D searchRect, boolean approximate, Set overlappingQuadNodes, Predicate additionalPredicate) { + this.searchRect = searchRect; + this.approximate = approximate; + this.additionalPredicate = additionalPredicate; + this.expectedVersion = version; + this.overlappingQuadNodes = overlappingQuadNodes; + + // Create the base spliterator from EdgeStore with our predicate + if (additionalPredicate == null) { + if (searchRect == null) { + // No filtering needed, use the full spliterator + this.baseSpliterator = graphStore.edgeStore.spliterator(); + } else { + // Only spatial filtering + this.baseSpliterator = graphStore.edgeStore.newFilteredSpliterator(this::shouldIncludeEdge); + } + } else { + if (searchRect == null) { + this.baseSpliterator = graphStore.edgeStore + .newFilteredSpliterator(this::shouldIncludeEdgeAllWithPredicate); + } else { + this.baseSpliterator = graphStore.edgeStore.newFilteredSpliterator(this::shouldIncludeEdge); + } + } + } + + private QuadTreeGlobalEdgesSpliterator(Rect2D searchRect, boolean approximate, Set overlappingQuadNodes, Spliterator baseSpliterator, int expectedVersion, Predicate additionalPredicate) { + this.searchRect = searchRect; + this.approximate = approximate; + this.overlappingQuadNodes = overlappingQuadNodes; + this.baseSpliterator = baseSpliterator; + this.expectedVersion = expectedVersion; + this.additionalPredicate = additionalPredicate; + } + + private boolean shouldIncludeEdgeAllWithPredicate(EdgeImpl edge) { + checkForComodification(); + + return additionalPredicate.test(edge); + } + + /** + * Determines if an edge should be included based on spatial filtering criteria + */ + private boolean shouldIncludeEdge(EdgeImpl edge) { + checkForComodification(); + + boolean spatialMatch = false; + + SpatialNodeDataImpl sourceSpatialData = edge.source.getSpatialData(); + SpatialNodeDataImpl targetSpatialData = edge.target.getSpatialData(); + + if (sourceSpatialData != null && sourceSpatialData.quadTreeNode != null) { + spatialMatch = overlappingQuadNodes.contains(sourceSpatialData.quadTreeNode); + } + + if (!spatialMatch && targetSpatialData != null && targetSpatialData.quadTreeNode != null) { + // Only check target if source wasn't already overlapping + spatialMatch = overlappingQuadNodes.contains(targetSpatialData.quadTreeNode); + } + + // Apply additional predicate if provided + if (spatialMatch && (additionalPredicate == null || additionalPredicate.test(edge))) { + if (approximate) { + return true; + } else { + // In exact mode, check if edge endpoints intersect with search rect + boolean sourceIntersects = sourceSpatialData != null && searchRect + .intersects(sourceSpatialData.minX, sourceSpatialData.minY, sourceSpatialData.maxX, sourceSpatialData.maxY); + boolean targetIntersects = targetSpatialData != null && searchRect + .intersects(targetSpatialData.minX, targetSpatialData.minY, targetSpatialData.maxX, targetSpatialData.maxY); + return sourceIntersects || targetIntersects; + } + } + return false; + } + + private void checkForComodification() { + if (expectedVersion != version) { + throw new ConcurrentModificationException(); + } + } + + @Override + public boolean tryAdvance(Consumer action) { + return baseSpliterator.tryAdvance(action); + } + + @Override + public Spliterator trySplit() { + Spliterator splitBase = baseSpliterator.trySplit(); + if (splitBase == null) { + return null; + } + + return new QuadTreeGlobalEdgesSpliterator(searchRect, approximate, overlappingQuadNodes, splitBase, + expectedVersion, additionalPredicate); + } + + @Override + public long estimateSize() { + return baseSpliterator.estimateSize(); + } + + @Override + public int characteristics() { + return baseSpliterator.characteristics(); + } + } } diff --git a/src/main/java/org/gephi/graph/impl/SpatialIndexImpl.java b/src/main/java/org/gephi/graph/impl/SpatialIndexImpl.java index 1a4152b4..7eebbff4 100644 --- a/src/main/java/org/gephi/graph/impl/SpatialIndexImpl.java +++ b/src/main/java/org/gephi/graph/impl/SpatialIndexImpl.java @@ -1,6 +1,6 @@ package org.gephi.graph.impl; -import java.util.Iterator; +import java.util.function.Predicate; import org.gephi.graph.api.Edge; import org.gephi.graph.api.EdgeIterable; import org.gephi.graph.api.Node; @@ -15,30 +15,68 @@ */ public class SpatialIndexImpl implements SpatialIndex { - private final GraphStore store; protected final NodesQuadTree nodesTree; public SpatialIndexImpl(GraphStore store) { - this.store = store; float boundaries = GraphStoreConfiguration.SPATIAL_INDEX_DIMENSION_BOUNDARY; - this.nodesTree = new NodesQuadTree(new Rect2D(-boundaries, -boundaries, boundaries, boundaries)); + this.nodesTree = new NodesQuadTree(store, + new Rect2D(-boundaries / 2, -boundaries / 2, boundaries / 2, boundaries / 2)); } @Override public NodeIterable getNodesInArea(Rect2D rect) { - return nodesTree.getNodes(rect); + return nodesTree.getNodes(rect, false); + } + + @Override + public NodeIterable getApproximateNodesInArea(Rect2D rect) { + return nodesTree.getNodes(rect, true); } @Override public EdgeIterable getEdgesInArea(Rect2D rect) { - return new EdgeIterableWrapper(() -> new EdgeIterator(rect, nodesTree.getNodes(rect).iterator()), - nodesTree.lock); + return nodesTree.getEdges(rect, false); + } + + @Override + public EdgeIterable getApproximateEdgesInArea(Rect2D rect) { + return nodesTree.getEdges(rect, true); + } + + @Override + public void spatialIndexReadLock() { + nodesTree.readLock(); + } + + @Override + public void spatialIndexReadUnlock() { + nodesTree.readUnlock(); + } + + public NodeIterable getNodesInArea(Rect2D rect, Predicate predicate) { + return nodesTree.getNodes(rect, false, predicate); + } + + public NodeIterable getApproximateNodesInArea(Rect2D rect, Predicate predicate) { + return nodesTree.getNodes(rect, true, predicate); + } + + public EdgeIterable getEdgesInArea(Rect2D rect, Predicate predicate) { + return nodesTree.getEdges(rect, false, predicate); + } + + public EdgeIterable getApproximateEdgesInArea(Rect2D rect, Predicate predicate) { + return nodesTree.getEdges(rect, true, predicate); } protected void clearNodes() { nodesTree.clear(); } + protected void incrementVersion() { + nodesTree.incrementVersion(); + } + protected void addNode(final NodeImpl node) { nodesTree.addNode(node); } @@ -65,62 +103,11 @@ public Rect2D getBoundaries() { return nodesTree.getBoundaries(); } - protected class EdgeIterator implements Iterator { - - private final Iterator nodeItr; - private final Rect2D rect2D; - private Iterator edgeItr; - private Edge pointer; - private Node node; - - public EdgeIterator(Rect2D rect2D, Iterator nodeIterator) { - this.nodeItr = nodeIterator; - this.rect2D = rect2D; - - nodesTree.readLock(); - } - - @Override - public boolean hasNext() { - while (pointer == null) { - while (pointer == null && edgeItr != null && edgeItr.hasNext()) { - pointer = edgeItr.next(); - if (!pointer.isSelfLoop()) { - Node oppositeNode = store.getOpposite(node, pointer); - // Skip edge - do not return same edges twice when both - // source and target nodes are visible - SpatialNodeDataImpl spatialData = ((NodeImpl) oppositeNode).getSpatialData(); - if (oppositeNode.getStoreId() < node.getStoreId() && rect2D - .intersects(spatialData.minX, spatialData.minY, spatialData.maxX, spatialData.maxY)) { - pointer = null; - } - } - } - if (pointer == null) { - edgeItr = null; - if (nodeItr != null && nodeItr.hasNext()) { - node = nodeItr.next(); - edgeItr = store.edgeStore.edgeIterator(node); - } else { - nodesTree.readUnlock(); - return false; - } - } - } - - return true; - } - - @Override - public Edge next() { - Edge res = pointer; - pointer = null; - return res; - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Not supported."); - } + public Rect2D getBoundaries(Predicate predicate) { + return nodesTree.getBoundaries(predicate); + } + + public int getObjectCount() { + return nodesTree.getObjectCount(); } } diff --git a/src/main/java/org/gephi/graph/impl/SpatialNodeDataImpl.java b/src/main/java/org/gephi/graph/impl/SpatialNodeDataImpl.java index cb4a04f9..9e62a5cc 100644 --- a/src/main/java/org/gephi/graph/impl/SpatialNodeDataImpl.java +++ b/src/main/java/org/gephi/graph/impl/SpatialNodeDataImpl.java @@ -5,6 +5,7 @@ public class SpatialNodeDataImpl { public float minX, minY, maxX, maxY; protected NodesQuadTree.QuadTreeNode quadTreeNode; + protected int arrayIndex = -1; // Index in the quad tree node's array, -1 if not in a node public SpatialNodeDataImpl(float minX, float minY, float maxX, float maxY) { this.minX = minX; @@ -23,4 +24,17 @@ public void updateBoundaries(float minX, float minY, float maxX, float maxY) { public void setQuadTreeNode(NodesQuadTree.QuadTreeNode quadTreeNode) { this.quadTreeNode = quadTreeNode; } + + public int getArrayIndex() { + return arrayIndex; + } + + public void setArrayIndex(int arrayIndex) { + this.arrayIndex = arrayIndex; + } + + public void clear() { + this.quadTreeNode = null; + this.arrayIndex = -1; + } } diff --git a/src/main/java/org/gephi/graph/impl/UndirectedDecorator.java b/src/main/java/org/gephi/graph/impl/UndirectedDecorator.java index 055a4d5e..013d15ad 100644 --- a/src/main/java/org/gephi/graph/impl/UndirectedDecorator.java +++ b/src/main/java/org/gephi/graph/impl/UndirectedDecorator.java @@ -203,7 +203,7 @@ public NodeIterable getNeighbors(Node node, int type) { @Override public EdgeIterable getEdges(Node node) { - return new EdgeIterableWrapper(() -> store.edgeStore.edgeUndirectedIterator(node), store.getAutoLock()); + return new EdgeIterableWrapper(() -> store.edgeStore.edgeUndirectedIterator(node, true), store.getAutoLock()); } @Override diff --git a/src/test/java/org/gephi/graph/impl/EdgeStoreTest.java b/src/test/java/org/gephi/graph/impl/EdgeStoreTest.java index 0498dfd3..205a5c33 100644 --- a/src/test/java/org/gephi/graph/impl/EdgeStoreTest.java +++ b/src/test/java/org/gephi/graph/impl/EdgeStoreTest.java @@ -859,7 +859,7 @@ public void testInOutIterator() { Object2ObjectMap outEdgeMap = getObjectMap(edges); for (NodeImpl n : getNodes(edges)) { - EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(n); + EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(n, true); for (; itr.hasNext();) { EdgeImpl e = itr.next(); if (e.isSelfLoop()) { @@ -881,13 +881,13 @@ public void testInOutIterator() { @Test(expectedExceptions = NullPointerException.class) public void testInOutIteratorNull() { EdgeStore edgeStore = new EdgeStore(); - edgeStore.edgeIterator(null); + edgeStore.edgeIterator((Node) null, true); } @Test(expectedExceptions = IllegalArgumentException.class) public void testInOutIteratorInvalid() { EdgeStore edgeStore = new EdgeStore(); - edgeStore.edgeIterator(new NodeImpl("0")); + edgeStore.edgeIterator(new NodeImpl("0"), true); } @Test @@ -903,7 +903,7 @@ public void testInOutIteratorAfterRemove() { Object2ObjectMap outEdgeMap = getObjectMap(edgeList.toArray(new EdgeImpl[0])); for (NodeImpl n : getNodes(edges)) { - EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(n); + EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(n, true); for (; itr.hasNext();) { EdgeImpl e = itr.next(); if (e.isSelfLoop()) { @@ -930,7 +930,7 @@ public void testInOutIteratorRemove() { int index = 0; for (NodeImpl n : getNodes(edges)) { - EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(n); + EdgeStore.EdgeInOutIterator itr = edgeStore.edgeIterator(n, true); for (; itr.hasNext();) { EdgeImpl e = itr.next(); itr.remove(); @@ -942,6 +942,80 @@ public void testInOutIteratorRemove() { testContainsNone(edgeStore, Arrays.asList(edges)); } + @Test + public void testEdgeInOutMultiIterator() { + EdgeStore edgeStore = new EdgeStore(); + EdgeImpl[] edges = GraphGenerator.generateSmallEdgeList(); + edgeStore.addAll(Arrays.asList(edges)); + + // Get all nodes from the edges + List nodeList = Arrays.asList(getNodes(edges)); + + // Collect edges using multi-iterator + EdgeStore.EdgeInOutMultiIterator multiIterator = edgeStore.edgeIterator(nodeList.iterator(), true); + Set multiIteratorEdges = new ObjectOpenHashSet<>(); + while (multiIterator.hasNext()) { + EdgeImpl edge = multiIterator.next(); + multiIteratorEdges.add(edge); + } + + // Collect edges using individual iterators (old approach) + Set individualIteratorEdges = new ObjectOpenHashSet<>(); + for (NodeImpl node : nodeList) { + EdgeStore.EdgeInOutIterator singleIterator = edgeStore.edgeIterator(node, true); + while (singleIterator.hasNext()) { + EdgeImpl edge = singleIterator.next(); + individualIteratorEdges.add(edge); + } + } + + // Both approaches should yield the same set of edges + Assert.assertEquals(multiIteratorEdges, individualIteratorEdges); + + // Verify all edges are accounted for + Set allEdges = new ObjectOpenHashSet<>(Arrays.asList(edges)); + Assert.assertEquals(multiIteratorEdges, allEdges); + } + + @Test + public void testEdgeInOutMultiIteratorEmpty() { + EdgeStore edgeStore = new EdgeStore(); + List emptyNodeList = new ArrayList<>(); + + EdgeStore.EdgeInOutMultiIterator multiIterator = edgeStore.edgeIterator(emptyNodeList.iterator(), true); + Assert.assertFalse(multiIterator.hasNext()); + } + + @Test + public void testEdgeInOutMultiIteratorRemove() { + EdgeStore edgeStore = new EdgeStore(); + EdgeImpl[] edges = GraphGenerator.generateSmallEdgeList(); + edgeStore.addAll(Arrays.asList(edges)); + + List nodeList = Arrays.asList(getNodes(edges)); + EdgeStore.EdgeInOutMultiIterator multiIterator = edgeStore.edgeIterator(nodeList.iterator(), true); + + int initialSize = edgeStore.size(); + int removedCount = 0; + + while (multiIterator.hasNext()) { + EdgeImpl edge = multiIterator.next(); + multiIterator.remove(); + removedCount++; + Assert.assertFalse(edgeStore.contains(edge)); + Assert.assertEquals(edgeStore.size(), initialSize - removedCount); + } + + Assert.assertEquals(removedCount, initialSize); + Assert.assertTrue(edgeStore.isEmpty()); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testEdgeInOutMultiIteratorNullIterator() { + EdgeStore edgeStore = new EdgeStore(); + edgeStore.edgeIterator((Iterator) null, true); + } + @Test public void testOutTypeIterator() { EdgeImpl[] edges = GraphGenerator.generateSmallMultiTypeEdgeList(); @@ -1966,7 +2040,7 @@ public void testInOutUndirectedIteratorRemove() { edgeStore.addAll(Arrays.asList(edges)); for (NodeImpl n : getNodes(edges)) { - Iterator itr = edgeStore.edgeUndirectedIterator(n); + Iterator itr = edgeStore.edgeUndirectedIterator(n, true); for (; itr.hasNext();) { itr.next(); itr.remove(); @@ -1983,7 +2057,7 @@ public void testInOutUndirectedIteratorRemoveDecorator() { edgeStore.addAll(Arrays.asList(edges)); for (NodeImpl n : getNodes(edges)) { - Iterator itr = edgeStore.edgeUndirectedIterator(n); + Iterator itr = edgeStore.edgeUndirectedIterator(n, true); for (; itr.hasNext();) { itr.next(); itr.remove(); @@ -2000,7 +2074,7 @@ public void testInOutUndirectedIterator() { Object2ObjectMap> neighbours = getNeighboursMap(edges, 0, true); for (NodeImpl n : getNodes(edges)) { - Iterator itr = edgeStore.edgeUndirectedIterator(n); + Iterator itr = edgeStore.edgeUndirectedIterator(n, true); Set incidentEdges = neighbours.get(n); diff --git a/src/test/java/org/gephi/graph/impl/GraphGenerator.java b/src/test/java/org/gephi/graph/impl/GraphGenerator.java index d9a351b0..31ddf79e 100644 --- a/src/test/java/org/gephi/graph/impl/GraphGenerator.java +++ b/src/test/java/org/gephi/graph/impl/GraphGenerator.java @@ -29,6 +29,7 @@ import org.gephi.graph.api.Configuration; import org.gephi.graph.api.Edge; import org.gephi.graph.api.Node; +import org.gephi.graph.api.Rect2D; import org.gephi.graph.api.TimeRepresentation; import org.gephi.graph.impl.BasicGraphStore.BasicEdgeStore; @@ -387,9 +388,20 @@ public static NodeImpl[] generateNodeList(int nodeCount) { } public static NodeImpl[] generateNodeList(int nodeCount, GraphStore graphStore) { + return generateNodeList(nodeCount, graphStore, null); + } + + public static NodeImpl[] generateNodeList(int nodeCount, GraphStore graphStore, Rect2D area) { NodeImpl[] nodes = new NodeImpl[nodeCount]; + Random random = new Random(); for (int i = 0; i < nodeCount; i++) { NodeImpl node = new NodeImpl(String.valueOf(i), graphStore); + if (area != null) { + float x = area.minX + random.nextFloat() * (area.maxX - area.minX); + float y = area.minY + random.nextFloat() * (area.maxY - area.minY); + node.setPosition(x, y); + node.setSize(1.0f); + } nodes[i] = node; } return nodes; diff --git a/src/test/java/org/gephi/graph/impl/NodesQuadTreeTest.java b/src/test/java/org/gephi/graph/impl/NodesQuadTreeTest.java index 4ea74081..9f12091b 100644 --- a/src/test/java/org/gephi/graph/impl/NodesQuadTreeTest.java +++ b/src/test/java/org/gephi/graph/impl/NodesQuadTreeTest.java @@ -1,7 +1,29 @@ +/* + * Copyright 2012-2013 Gephi Consortium + * + * Licensed 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.gephi.graph.impl; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSet; +import java.util.Arrays; import java.util.Collection; import java.util.Random; +import java.util.stream.Collectors; +import org.gephi.graph.api.Configuration; +import org.gephi.graph.api.Edge; +import org.gephi.graph.api.EdgeIterable; import org.gephi.graph.api.Node; import org.gephi.graph.api.NodeIterable; import org.gephi.graph.api.Rect2D; @@ -24,6 +46,14 @@ public void testGetAllNodesEmpty() { NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); Assert.assertTrue(q.getAllNodes().toCollection().isEmpty()); Assert.assertEquals(q.getAllNodes().toArray().length, 0); + Assert.assertEquals(q.getNodeCount(false), 1); + } + + @Test + public void testGetNodeCount() { + NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); + Assert.assertEquals(q.getNodeCount(false), 1); + Assert.assertEquals(q.getNodeCount(true), 0); } @Test @@ -37,6 +67,7 @@ public void testRemoveEmpty() { NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); NodeImpl node = new NodeImpl("0"); Assert.assertFalse(q.removeNode(node)); + Assert.assertEquals(q.getNodeCount(true), 0); } @Test @@ -45,6 +76,7 @@ public void testAddNode() { NodeImpl node = new NodeImpl("0"); Assert.assertTrue(q.addNode(node)); Assert.assertNotNull(node.getSpatialData().quadTreeNode); + Assert.assertEquals(q.getNodeCount(true), 1); } @Test @@ -64,6 +96,7 @@ public void testClear() { Assert.assertNull(node.getSpatialData().quadTreeNode); Assert.assertTrue(q.getAllNodes().toCollection().isEmpty()); Assert.assertFalse(q.removeNode(node)); + Assert.assertEquals(q.getNodeCount(true), 0); } @Test @@ -102,6 +135,8 @@ public void testDepth() { q.addNode(node); } Assert.assertTrue(q.getDepth() >= 1); + Assert.assertEquals(q.getNodeCount(true), 4); + Assert.assertEquals(q.getNodeCount(false), 5); } @Test @@ -125,7 +160,8 @@ public void testGetAll() { Collection rectContainingAll = q.getNodes(BOUNDS_RECT).toCollection(); Assert.assertEquals(rectContainingAll, all); - Collection bigRectContainingAll = q.getNodes(-BOUNDS * 2, -BOUNDS * 2, BOUNDS, BOUNDS).toCollection(); + Collection bigRectContainingAll = q.getNodes(new Rect2D(-BOUNDS * 2, -BOUNDS * 2, BOUNDS, BOUNDS)) + .toCollection(); Assert.assertEquals(bigRectContainingAll, all); } @@ -151,12 +187,14 @@ public void testOutOfBoundsStillWorks() { Collection all = q.getAllNodes().toCollection(); Assert.assertEquals(all.size(), 3); + all = q.getAllNodes().stream().collect(Collectors.toList()); + Assert.assertEquals(all.size(), 3); - assertEmpty(q.getNodes(80, 80, 89.99f, 89.99f)); + assertEmpty(q.getNodes(new Rect2D(80, 80, 89.99f, 89.99f))); - assertSame(q.getNodes(95, 95, 99, 99), n1); - assertSame(q.getNodes(0, 0, 101, 101), n1, n2); - assertSame(q.getNodes(4, 4, 91, 91), n1, n2); + assertSameSet(q.getNodes(new Rect2D(95, 95, 99, 99)), n1); + assertSameSet(q.getNodes(new Rect2D(0, 0, 101, 101)), n1, n2); + assertSameSet(q.getNodes(new Rect2D(4, 4, 91, 91)), n1, n2); } @Test @@ -179,11 +217,11 @@ public void testGetZone1() { q.addNode(n2); q.addNode(n3); - assertEmpty(q.getNodes(80, 80, 89.99f, 89.99f)); + assertEmpty(q.getNodes(new Rect2D(80, 80, 89.99f, 89.99f))); - assertSame(q.getNodes(95, 95, 99, 99), n1); - assertSame(q.getNodes(0, 0, 101, 101), n2, n1); - assertSame(q.getNodes(4, 4, 91, 91), n2, n1); + assertSameSet(q.getNodes(new Rect2D(95, 95, 99, 99)), n1); + assertSameSet(q.getNodes(new Rect2D(0, 0, 101, 101)), n2, n1); + assertSameSet(q.getNodes(new Rect2D(4, 4, 91, 91)), n2, n1); } @Test @@ -206,19 +244,11 @@ public void testGetZone2() { q.addNode(n2); q.addNode(n3); - assertEmpty(q.getNodes(80, 80, 89.99f, 89.99f)); - - assertSame(q.getNodes(95, 95, 99, 99), n1); - assertSame(q.getNodes(0, 0, 101, 101), n1, n2); - assertSame(q.getNodes(4, 4, 91, 91), n1, n2); - } + assertEmpty(q.getNodes(new Rect2D(80, 80, 89.99f, 89.99f))); - private void assertSame(NodeIterable iterable, Node... expected) { - Assert.assertEqualsNoOrder(iterable.toArray(), expected); - } - - private void assertEmpty(NodeIterable iterable) { - Assert.assertEquals(iterable.toCollection().size(), 0); + assertSameSet(q.getNodes(new Rect2D(95, 95, 99, 99)), n1); + assertSameSet(q.getNodes(new Rect2D(0, 0, 101, 101)), n1, n2); + assertSameSet(q.getNodes(new Rect2D(4, 4, 91, 91)), n1, n2); } @Test @@ -526,4 +556,285 @@ public void testGetBoundariesSingleNodeAtOrigin() { Assert.assertEquals(boundaries.maxX, 1f); Assert.assertEquals(boundaries.maxY, 1f); } + + @Test + public void testGetMaximumObjectsReached() { + NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); + addRandomNodes(q, GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE, 0); + Assert.assertEquals(q.getObjectCount(), GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE); + Assert.assertEquals(q.getNodeCount(true), 1); + Assert.assertEquals(q.getNodeCount(false), 1); + NodeImpl[] newNodes = addRandomNodes(q, 1, GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE); + Assert.assertEquals(q.getNodeCount(true), 4); + Assert.assertEquals(q.getObjectCount(), GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE + 1); + q.removeNode(newNodes[0]); + Assert.assertEquals(q.getObjectCount(), GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE); + } + + @Test + public void testCountsWithLargerDepth() { + NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); + int totalNodes = GraphStoreConfiguration.SPATIAL_INDEX_MAX_OBJECTS_PER_NODE * 10; + addRandomNodes(q, totalNodes, 0); + Assert.assertEquals(q.getObjectCount(), totalNodes); + Assert.assertTrue(q.getNodeCount(true) >= 10); + Assert.assertTrue(q.getNodeCount(false) >= 10); + } + + @Test + public void testIteratorWithLargerGraph() { + NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); + int totalNodes = 100000; + NodeImpl[] nodes = addRandomNodes(q, totalNodes, 0); + assertSameSet(q.getAllNodes(), nodes); + } + + @Test + public void testGetNodesInArea() { + Rect2D area = new Rect2D(-1000, -1000, 1000, 1000); + NodesQuadTree q = new NodesQuadTree(area); + int totalNodes = 30000; + NodeImpl[] nodes = addRandomNodes(q, totalNodes, 0, area); + Rect2D subarea = new Rect2D(-100, -100, 100, 100); + + assertSameSet(q + .getNodes(subarea, false), Arrays + .stream(nodes).filter(n -> subarea.intersects(n.getSpatialData().minX, n + .getSpatialData().minY, n.getSpatialData().maxX, n.getSpatialData().maxY)) + .toArray(NodeImpl[]::new)); + } + + @Test + public void testGetNodesInAreaApproximate() { + Rect2D area = new Rect2D(-1000, -1000, 1000, 1000); + NodesQuadTree q = new NodesQuadTree(area); + int totalNodes = 30000; + NodeImpl[] nodes = addRandomNodes(q, totalNodes, 0, area); + Rect2D subarea = new Rect2D(-100, -100, -1, -1); + + // Approximate should return all nodes that are in the quadtree nodes + // intersecting the area + assertSameSet(q.getNodes(subarea, true), Arrays.stream(nodes) + .filter(n -> subarea.intersects(n.getSpatialData().quadTreeNode.quadRect())).toArray(NodeImpl[]::new)); + } + + @Test + public void testGetNodesInAreaWithPredicate() { + NodesQuadTree q = new NodesQuadTree(BOUNDS_RECT); + + Rect2D rect = new Rect2D(-10, -10, 10, 10); + NodeImpl[] nodes = addRandomNodes(q, 2, 0, rect); + NodeImpl node1 = nodes[0]; + + assertSameSet(q.getNodes(rect, false, n -> n == node1), node1); + } + + @Test + public void testGetAllEdges() { + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodesQuadTree q = store.spatialIndex.nodesTree; + NodeImpl[] nodes = addRandomNodes(q, 2, 0); + EdgeImpl[] edges = addRandomEdges(store, nodes, 10); + + assertSameSet(q.getEdges(), edges); + } + + @Test + public void testGetAllEdgesLarge() { + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodesQuadTree q = store.spatialIndex.nodesTree; + NodeImpl[] nodes = addRandomNodes(q, 10000, 0); + EdgeImpl[] edges = addRandomEdges(store, nodes, 100000); + + assertSameSet(q.getEdges(), edges); + } + + @Test + public void testGetEdgesInArea() { + Rect2D area = new Rect2D(-1000, -1000, 1000, 1000); + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodesQuadTree q = store.spatialIndex.nodesTree; + NodeImpl[] nodes = addRandomNodes(q, 30000, 0, area); + EdgeImpl[] edges = addRandomEdges(store, nodes, 100000); + + Rect2D subarea = new Rect2D(-100, -100, 100, 100); + + assertSameSet(q.getEdges(subarea, false), Arrays.stream(edges) + .filter(e -> edgeIntersectsArea(e, subarea, false)).toArray(EdgeImpl[]::new)); + } + + @Test + public void testGetEdgesInAreaGlobal() { + Rect2D area = new Rect2D(-1000, -1000, 1000, 1000); + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodesQuadTree q = store.spatialIndex.nodesTree; + NodeImpl[] nodes = addRandomNodes(q, 30000, 0, area); + EdgeImpl[] edges = addRandomEdges(store, nodes, 100000); + + Rect2D subarea = new Rect2D(-600, -600, 600, 600); + + assertSameSet(q.getEdges(subarea, false), Arrays.stream(edges) + .filter(e -> edgeIntersectsArea(e, subarea, false)).toArray(EdgeImpl[]::new)); + } + + @Test + public void testGetEdgesInAreaApproximate() { + Rect2D area = new Rect2D(-1000, -1000, 1000, 1000); + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodesQuadTree q = store.spatialIndex.nodesTree; + NodeImpl[] nodes = addRandomNodes(q, 30000, 0, area); + EdgeImpl[] edges = addRandomEdges(store, nodes, 100000); + + Rect2D subarea = new Rect2D(-100, -100, -1, -1); + + assertSameSet(q.getEdges(subarea, true), Arrays.stream(edges).filter(e -> edgeIntersectsArea(e, subarea, true)) + .toArray(EdgeImpl[]::new)); + } + + @Test + public void testGetEdgesInAreaWithPredicate() { + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodesQuadTree q = store.spatialIndex.nodesTree; + + Rect2D rect = new Rect2D(-10, -10, 10, 10); + NodeImpl[] nodes = addRandomNodes(q, 2, 0, rect); + EdgeImpl[] edges = addRandomEdges(store, nodes, 2); + EdgeImpl edge1 = edges[0]; + + assertSameSet(q.getEdges(rect, false), edges); + assertSameSet(q.getEdges(rect, false, e -> e == edge1), edge1); + assertSameSet(q.getEdges(rect, true), edges); + assertSameSet(q.getEdges(rect, true, e -> e == edge1), edge1); + } + + @Test + public void testGetEdgesInAreaBidirectional() { + Rect2D rect = new Rect2D(100, 100, 100, 100); + GraphStore store = GraphGenerator.generateEmptyGraphStore(getConfig()); + NodeImpl[] nodes = GraphGenerator.generateNodeList(10000, store, rect); + store.addAllNodes(Arrays.asList(nodes)); + NodesQuadTree q = store.spatialIndex.nodesTree; + + nodes[0].setPosition(-1000, -1000); + EdgeImpl[] edges = addRandomEdges(store, new NodeImpl[] { nodes[0], nodes[1] }, 1); + + // Edge should be returned once, as only one node is in the area + assertSameSetAndCount(q.getEdges(new Rect2D(-2000, -2000, -999, -999), false), edges); + + nodes[1].setPosition(-1000, -1000); + + // Edge should be returned twice, once for each node + assertSameSetAndCount(q + .getEdges(new Rect2D(-2000, -2000, -999, -999), false), new EdgeImpl[] { edges[0], edges[0] }); + } + + // Utils + + private void assertSameSet(NodeIterable iterable, Node... expected) { + Assert.assertTrue(expected.length > 0, "Expected array must not be empty"); + ObjectSet set = new ObjectOpenHashSet<>(expected.length); + set.addAll(Arrays.asList(expected)); + Assert.assertEquals(iterable.toSet(), set); + Assert.assertEquals(iterable.stream().collect(Collectors.toSet()), set); + Assert.assertEquals(iterable.parallelStream().collect(Collectors.toSet()), set); + } + + private void assertSameSetAndCount(NodeIterable iterable, Node... expected) { + assertSameSet(iterable, expected); + Assert.assertEquals(iterable.toCollection().size(), expected.length); + Assert.assertEquals(iterable.stream().count(), expected.length); + Assert.assertEquals(iterable.parallelStream().count(), expected.length); + } + + private void assertSameSet(EdgeIterable iterable, Edge... expected) { + Assert.assertTrue(expected.length > 0, "Expected array must not be empty"); + ObjectSet set = new ObjectOpenHashSet<>(expected.length); + set.addAll(Arrays.asList(expected)); + Assert.assertEquals(iterable.toSet(), set); + Assert.assertEquals(iterable.stream().collect(Collectors.toSet()), set); + Assert.assertEquals(iterable.parallelStream().collect(Collectors.toSet()), set); + } + + private void assertSameSetAndCount(EdgeIterable iterable, Edge... expected) { + assertSameSet(iterable, expected); + Assert.assertEquals(iterable.toCollection().size(), expected.length); + Assert.assertEquals(iterable.stream().count(), expected.length); + Assert.assertEquals(iterable.parallelStream().count(), expected.length); + } + + private void assertEmpty(NodeIterable iterable) { + Assert.assertEquals(iterable.toCollection().size(), 0); + } + + private NodeImpl[] addRandomNodes(GraphStore store, int count, int startIndex) { + return addRandomNodes(store, count, startIndex, BOUNDS_RECT); + } + + private NodeImpl[] addRandomNodes(GraphStore store, int count, int startIndex, Rect2D area) { + NodeImpl[] nodes = generateNodes(count, startIndex, area); + for (NodeImpl n : nodes) { + store.addNode(n); + } + return nodes; + } + + private NodeImpl[] addRandomNodes(NodesQuadTree q, int count, int startIndex) { + return addRandomNodes(q, count, startIndex, BOUNDS_RECT); + } + + private NodeImpl[] addRandomNodes(NodesQuadTree q, int count, int startIndex, Rect2D area) { + NodeImpl[] nodes = generateNodes(count, startIndex, area); + for (NodeImpl n : nodes) { + q.addNode(n); + } + return nodes; + } + + private NodeImpl[] generateNodes(int count, int startIndex, Rect2D area) { + Random rand = new Random(); + NodeImpl[] nodes = new NodeImpl[count]; + for (int i = 0; i < count; i++) { + NodeImpl node = new NodeImpl(String.valueOf(startIndex++)); + float x = area.minX + rand.nextFloat() * (area.maxX - area.minX); + float y = area.minY + rand.nextFloat() * (area.maxY - area.minY); + node.setPosition(x, y); + node.setSize(1.0f); + nodes[i] = node; + } + return nodes; + } + + private EdgeImpl[] addRandomEdges(GraphStore store, NodeImpl[] nodes, int count) { + for (NodeImpl n : nodes) { + store.addNode(n); + } + EdgeImpl[] edges = new EdgeImpl[count]; + int edgeIndex = 0; + while (edgeIndex < count) { + NodeImpl source = nodes[new Random().nextInt(nodes.length)]; + NodeImpl target = nodes[new Random().nextInt(nodes.length)]; + if (source != target) { + EdgeImpl edge = new EdgeImpl(String.valueOf(edgeIndex), store, source, target, 0, 1.0, true); + edges[edgeIndex] = edge; + store.addEdge(edge); + edgeIndex++; + } + } + return edges; + } + + private Configuration getConfig() { + return Configuration.builder().enableSpatialIndex(true).build(); + } + + private boolean edgeIntersectsArea(EdgeImpl e, Rect2D area, boolean approximate) { + if (approximate) { + return area.intersects(e.source.getSpatialData().quadTreeNode.quadRect()) || area + .intersects(e.target.getSpatialData().quadTreeNode.quadRect()); + } + return area.intersects(e.source.getSpatialData().minX, e.source.getSpatialData().minY, e.source + .getSpatialData().maxX, e.source.getSpatialData().maxY) || area + .intersects(e.target.getSpatialData().minX, e.target.getSpatialData().minY, e.target + .getSpatialData().maxX, e.target.getSpatialData().maxY); + } } diff --git a/src/test/java/org/gephi/graph/impl/SpatialIndexImplTest.java b/src/test/java/org/gephi/graph/impl/SpatialIndexImplTest.java index 1868a6a5..8d775726 100644 --- a/src/test/java/org/gephi/graph/impl/SpatialIndexImplTest.java +++ b/src/test/java/org/gephi/graph/impl/SpatialIndexImplTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-2013 Gephi Consortium + * + * Licensed 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.gephi.graph.impl; import java.util.Arrays; @@ -37,7 +52,7 @@ public void testGetElementsBothNodesVisible() { SpatialIndexImpl spatialIndex = store.spatialIndex; assertSame(spatialIndex.getNodesInArea(BOUNDS_RECT), n1, n2); - assertSame(spatialIndex.getEdgesInArea(BOUNDS_RECT), e); + assertSame(spatialIndex.getEdgesInArea(BOUNDS_RECT), e, e); } @Test @@ -81,6 +96,19 @@ public void testGetElementsWithSelfLoop() { assertSame(spatialIndex.getEdgesInArea(BOUNDS_RECT), e); } + @Test + public void testClear() { + GraphStore store = GraphGenerator.generateTinyGraphStore(getConfig()); + + SpatialIndexImpl spatialIndex = store.spatialIndex; + Assert.assertEquals(spatialIndex.getObjectCount(), store.getNodeCount()); + Assert.assertEquals(spatialIndex.getNodesInArea(new Rect2D(-1, -1, 1, 1)).toCollection().size(), store + .getNodeCount()); + store.clear(); + Assert.assertEquals(spatialIndex.getObjectCount(), 0); + Assert.assertTrue(spatialIndex.getNodesInArea(new Rect2D(-1, -1, 1, 1)).toCollection().isEmpty()); + } + private void assertSame(NodeIterable iterable, Node... expected) { Assert.assertEquals(iterable.toCollection(), Arrays.asList(expected)); }