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 extends Node> 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 super Node> predicate) {
+ return quadTreeRoot.getNodes(searchRect, approximate, predicate);
}
public NodeIterable getAllNodes() {
return quadTreeRoot.getAllNodes();
}
+ public NodeIterable getAllNodes(Predicate super Node> 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 super Edge> 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 super Node> 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 super Node> predicate) {
+ return new FilteredQuadTreeNodeIterable(searchRect, approximate, predicate);
+ }
+
private NodeIterable getAllNodes() {
return new QuadTreeNodesIterable(null);
}
+ private NodeIterable getAllNodes(Predicate super Node> 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 super Edge> 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 super Node> predicate;
+
+ public FilteredQuadTreeNodeIterable(Rect2D searchRect, boolean approximate, Predicate super Node> 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 super Edge> predicate;
+
+ public FilteredQuadTreeEdgeIterable(Rect2D searchRect, boolean approximate, Predicate super Edge> 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 super Edge> 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 super Edge> 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 super Node> 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 super Node> 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 super NodeImpl> 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 super NodeImpl> 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 super T> 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 super T> predicate;
+ protected final Object[] holder = new Object[1];
+
+ protected FilteredSpliterator(S parentSpliterator, Predicate super T> predicate) {
+ this.parentSpliterator = parentSpliterator;
+ this.predicate = predicate;
+ }
+
+ protected abstract P createSplitInstance(S splitParent, Predicate super T> predicate);
+
+ protected abstract boolean testPredicate(T element);
+
+ @Override
+ public boolean tryAdvance(Consumer super T> 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 super Node> predicate) {
+ super(new QuadTreeNodesSpliterator(root, searchRect, approximate), predicate);
+ }
+
+ private FilteredQuadTreeNodesSpliterator(QuadTreeNodesSpliterator parentSpliterator, Predicate super Node> predicate) {
+ super(parentSpliterator, predicate);
+ }
+
+ @Override
+ protected FilteredQuadTreeNodesSpliterator createSplitInstance(QuadTreeNodesSpliterator splitParent, Predicate super Node> 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 super Edge> predicate) {
+ super(new QuadTreeEdgesSpliterator(root, searchRect, approximate), predicate);
+ }
+
+ private FilteredQuadTreeEdgesSpliterator(QuadTreeEdgesSpliterator parentSpliterator, Predicate super Edge> predicate) {
+ super(parentSpliterator, predicate);
+ }
+
+ @Override
+ protected FilteredQuadTreeEdgesSpliterator createSplitInstance(QuadTreeEdgesSpliterator splitParent, Predicate super Edge> 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 super Edge> additionalPredicate;
+
+ public QuadTreeGlobalEdgesSpliterator(Rect2D searchRect, boolean approximate, Set overlappingQuadNodes, Predicate super Edge> 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 super Edge> 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 super Edge> 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 super Node> predicate) {
+ return nodesTree.getNodes(rect, false, predicate);
+ }
+
+ public NodeIterable getApproximateNodesInArea(Rect2D rect, Predicate super Node> predicate) {
+ return nodesTree.getNodes(rect, true, predicate);
+ }
+
+ public EdgeIterable getEdgesInArea(Rect2D rect, Predicate super Edge> predicate) {
+ return nodesTree.getEdges(rect, false, predicate);
+ }
+
+ public EdgeIterable getApproximateEdgesInArea(Rect2D rect, Predicate super Edge> 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 super Node> 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