Skip to content

Commit 455daf7

Browse files
authored
228 Add get boundaries to Spatial Index (#229)
* Add getBoundaries to SpatialIndex * Fix issue with empty boundaries
1 parent a3f96ef commit 455daf7

File tree

7 files changed

+606
-9
lines changed

7 files changed

+606
-9
lines changed

src/main/java/org/gephi/graph/api/Rect2D.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,26 @@ public boolean contains(float minX, float minY, float maxX, float maxY) {
173173
public boolean intersects(float minX, float minY, float maxX, float maxY) {
174174
return this.minX <= maxX && minX <= this.maxX && this.maxY >= minY && maxY >= this.minY;
175175
}
176+
177+
@Override
178+
public boolean equals(Object obj) {
179+
if (this == obj) {
180+
return true;
181+
}
182+
if (obj == null || getClass() != obj.getClass()) {
183+
return false;
184+
}
185+
Rect2D rect2D = (Rect2D) obj;
186+
return Float.compare(rect2D.minX, minX) == 0 && Float.compare(rect2D.minY, minY) == 0 && Float
187+
.compare(rect2D.maxX, maxX) == 0 && Float.compare(rect2D.maxY, maxY) == 0;
188+
}
189+
190+
@Override
191+
public int hashCode() {
192+
int result = (minX != +0.0f ? Float.floatToIntBits(minX) : 0);
193+
result = 31 * result + (minY != +0.0f ? Float.floatToIntBits(minY) : 0);
194+
result = 31 * result + (maxX != +0.0f ? Float.floatToIntBits(maxX) : 0);
195+
result = 31 * result + (maxY != +0.0f ? Float.floatToIntBits(maxY) : 0);
196+
return result;
197+
}
176198
}

src/main/java/org/gephi/graph/api/SpatialIndex.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ public interface SpatialIndex {
3737
* @return edges in the area
3838
*/
3939
EdgeIterable getEdgesInArea(Rect2D rect);
40+
41+
/**
42+
* Returns the bounding rectangle that contains all nodes in the graph. The
43+
* boundaries are calculated based on each node's position and size.
44+
*
45+
* @return the bounding rectangle, or null if there are no nodes
46+
*/
47+
Rect2D getBoundaries();
4048
}

src/main/java/org/gephi/graph/impl/GraphViewDecorator.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,46 @@ public EdgeIterable getEdgesInArea(Rect2D rect) {
845845
return new EdgeIterableWrapper(new EdgeViewIterator(iterator), graphStore.spatialIndex.nodesTree.lock);
846846
}
847847

848+
@Override
849+
public Rect2D getBoundaries() {
850+
graphStore.autoReadLock();
851+
try {
852+
float minX = Float.POSITIVE_INFINITY;
853+
float minY = Float.POSITIVE_INFINITY;
854+
float maxX = Float.NEGATIVE_INFINITY;
855+
float maxY = Float.NEGATIVE_INFINITY;
856+
857+
boolean hasNodes = false;
858+
859+
// Iterate only through nodes visible in this view
860+
for (Node node : getNodes()) {
861+
hasNodes = true;
862+
final float x = node.x();
863+
final float y = node.y();
864+
final float size = node.size();
865+
866+
final float nodeMinX = x - size;
867+
final float nodeMinY = y - size;
868+
final float nodeMaxX = x + size;
869+
final float nodeMaxY = y + size;
870+
871+
if (nodeMinX < minX)
872+
minX = nodeMinX;
873+
if (nodeMinY < minY)
874+
minY = nodeMinY;
875+
if (nodeMaxX > maxX)
876+
maxX = nodeMaxX;
877+
if (nodeMaxY > maxY)
878+
maxY = nodeMaxY;
879+
}
880+
881+
return hasNodes ? new Rect2D(minX, minY, maxX, maxY) : new Rect2D(Float.NEGATIVE_INFINITY,
882+
Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY);
883+
} finally {
884+
graphStore.autoReadUnlock();
885+
}
886+
}
887+
848888
protected final class NodeViewIterator implements Iterator<Node> {
849889

850890
private final Iterator<Node> nodeIterator;

src/main/java/org/gephi/graph/impl/NodesQuadTree.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
/**
1717
* Adapted from https://bitbucket.org/C3/quadtree/wiki/Home
1818
*
19-
* TODO: unit tests!!
20-
*
2119
* @author Eduardo Ramos
2220
*/
2321
public class NodesQuadTree {
@@ -171,6 +169,44 @@ public int getDepth() {
171169
return depth;
172170
}
173171

172+
public Rect2D getBoundaries() {
173+
readLock();
174+
try {
175+
NodeIterable allNodes = getAllNodes();
176+
177+
float minX = Float.POSITIVE_INFINITY;
178+
float minY = Float.POSITIVE_INFINITY;
179+
float maxX = Float.NEGATIVE_INFINITY;
180+
float maxY = Float.NEGATIVE_INFINITY;
181+
182+
boolean hasNodes = false;
183+
184+
for (Node node : allNodes) {
185+
SpatialNodeDataImpl spatialData = ((NodeImpl) node).getSpatialData();
186+
if (spatialData != null) {
187+
hasNodes = true;
188+
if (spatialData.minX < minX) {
189+
minX = spatialData.minX;
190+
}
191+
if (spatialData.minY < minY) {
192+
minY = spatialData.minY;
193+
}
194+
if (spatialData.maxX > maxX) {
195+
maxX = spatialData.maxX;
196+
}
197+
if (spatialData.maxY > maxY) {
198+
maxY = spatialData.maxY;
199+
}
200+
}
201+
}
202+
203+
return hasNodes ? new Rect2D(minX, minY, maxX, maxY) : new Rect2D(Float.NEGATIVE_INFINITY,
204+
Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY);
205+
} finally {
206+
readUnlock();
207+
}
208+
}
209+
174210
protected class QuadTreeNode {
175211

176212
private Set<NodeImpl> objects = null;

src/main/java/org/gephi/graph/impl/SpatialIndexImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ protected void moveNode(final NodeImpl node) {
5959
nodesTree.updateNode(node, minX, minY, maxX, maxY);
6060
}
6161

62+
@Override
63+
public Rect2D getBoundaries() {
64+
return nodesTree.getBoundaries();
65+
}
66+
6267
protected class EdgeIterator implements Iterator<Edge> {
6368

6469
private final Iterator<Node> nodeItr;

src/test/java/org/gephi/graph/impl/GraphViewDecoratorTest.java

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
import java.util.Arrays;
2121
import java.util.Collections;
2222
import java.util.Random;
23+
import org.gephi.graph.api.Configuration;
2324
import org.gephi.graph.api.DirectedSubgraph;
2425
import org.gephi.graph.api.Edge;
2526
import org.gephi.graph.api.Element;
2627
import org.gephi.graph.api.ElementIterable;
2728
import org.gephi.graph.api.Interval;
2829
import org.gephi.graph.api.Node;
30+
import org.gephi.graph.api.Rect2D;
31+
import org.gephi.graph.api.SpatialIndex;
2932
import org.gephi.graph.api.UndirectedSubgraph;
3033
import org.testng.Assert;
3134
import org.testng.annotations.Test;
@@ -918,6 +921,189 @@ private GraphStore convertToStore(GraphViewImpl view) {
918921
return store;
919922
}
920923

924+
@Test
925+
public void testGetBoundariesEmptyView() {
926+
GraphStore graphStore = GraphGenerator.generateEmptyGraphStore(getSpatialConfig());
927+
GraphViewStore store = graphStore.viewStore;
928+
GraphViewImpl view = store.createView();
929+
930+
DirectedSubgraph graph = store.getDirectedGraph(view);
931+
932+
Assert.assertEquals(new Rect2D(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY,
933+
Float.POSITIVE_INFINITY), graph.getSpatialIndex().getBoundaries());
934+
}
935+
936+
@Test
937+
public void testGetBoundariesSingleNodeInView() {
938+
GraphStore graphStore = GraphGenerator.generateEmptyGraphStore(getSpatialConfig());
939+
GraphViewStore store = graphStore.viewStore;
940+
GraphViewImpl view = store.createView();
941+
942+
// Add a node to the graph store
943+
NodeImpl node1 = (NodeImpl) graphStore.factory.newNode("1");
944+
node1.setPosition(100, 200);
945+
node1.setSize(10);
946+
graphStore.addNode(node1);
947+
948+
NodeImpl node2 = (NodeImpl) graphStore.factory.newNode("2");
949+
node2.setPosition(500, 600);
950+
node2.setSize(20);
951+
graphStore.addNode(node2);
952+
953+
// Add only node1 to the view
954+
view.addNode(node1);
955+
956+
DirectedSubgraph graph = store.getDirectedGraph(view);
957+
958+
// Should return boundaries only for node1
959+
Rect2D boundaries = graph.getSpatialIndex().getBoundaries();
960+
Assert.assertNotNull(boundaries);
961+
Assert.assertEquals(boundaries.minX, 90f); // 100 - 10
962+
Assert.assertEquals(boundaries.minY, 190f); // 200 - 10
963+
Assert.assertEquals(boundaries.maxX, 110f); // 100 + 10
964+
Assert.assertEquals(boundaries.maxY, 210f); // 200 + 10
965+
}
966+
967+
@Test
968+
public void testGetBoundariesMultipleNodesInView() {
969+
GraphStore graphStore = GraphGenerator.generateEmptyGraphStore(getSpatialConfig());
970+
GraphViewStore store = graphStore.viewStore;
971+
GraphViewImpl view = store.createView();
972+
973+
// Add nodes to the graph store
974+
NodeImpl node1 = (NodeImpl) graphStore.factory.newNode("1");
975+
node1.setPosition(0, 0);
976+
node1.setSize(5);
977+
graphStore.addNode(node1);
978+
979+
NodeImpl node2 = (NodeImpl) graphStore.factory.newNode("2");
980+
node2.setPosition(100, 200);
981+
node2.setSize(10);
982+
graphStore.addNode(node2);
983+
984+
NodeImpl node3 = (NodeImpl) graphStore.factory.newNode("3");
985+
node3.setPosition(500, 600); // This node won't be in the view
986+
node3.setSize(20);
987+
graphStore.addNode(node3);
988+
989+
// Add only node1 and node2 to the view
990+
view.addNode(node1);
991+
view.addNode(node2);
992+
993+
DirectedSubgraph graph = store.getDirectedGraph(view);
994+
995+
// Should return boundaries only for node1 and node2
996+
Rect2D boundaries = graph.getSpatialIndex().getBoundaries();
997+
Assert.assertNotNull(boundaries);
998+
Assert.assertEquals(boundaries.minX, -5f); // node1: 0 - 5
999+
Assert.assertEquals(boundaries.minY, -5f); // node1: 0 - 5
1000+
Assert.assertEquals(boundaries.maxX, 110f); // node2: 100 + 10
1001+
Assert.assertEquals(boundaries.maxY, 210f); // node2: 200 + 10
1002+
}
1003+
1004+
@Test
1005+
public void testGetBoundariesViewSubsetVsFullGraph() {
1006+
GraphStore graphStore = GraphGenerator.generateEmptyGraphStore(getSpatialConfig());
1007+
GraphViewStore store = graphStore.viewStore;
1008+
GraphViewImpl view = store.createView();
1009+
1010+
// Add nodes to the graph store
1011+
NodeImpl node1 = (NodeImpl) graphStore.factory.newNode("1");
1012+
node1.setPosition(0, 0);
1013+
node1.setSize(5);
1014+
graphStore.addNode(node1);
1015+
1016+
NodeImpl node2 = (NodeImpl) graphStore.factory.newNode("2");
1017+
node2.setPosition(100, 200);
1018+
node2.setSize(10);
1019+
graphStore.addNode(node2);
1020+
1021+
NodeImpl node3 = (NodeImpl) graphStore.factory.newNode("3");
1022+
node3.setPosition(-50, -100);
1023+
node3.setSize(15);
1024+
graphStore.addNode(node3);
1025+
1026+
// Add only first two nodes to the view
1027+
view.addNode(node1);
1028+
view.addNode(node2);
1029+
1030+
DirectedSubgraph viewGraph = store.getDirectedGraph(view);
1031+
DirectedSubgraph fullGraph = graphStore;
1032+
1033+
// Get boundaries for both
1034+
Rect2D viewBoundaries = viewGraph.getSpatialIndex().getBoundaries();
1035+
Rect2D fullBoundaries = graphStore.spatialIndex.getBoundaries();
1036+
1037+
// View boundaries should only include node1 and node2
1038+
Assert.assertNotNull(viewBoundaries);
1039+
Assert.assertEquals(viewBoundaries.minX, -5f); // node1: 0 - 5
1040+
Assert.assertEquals(viewBoundaries.minY, -5f); // node1: 0 - 5
1041+
Assert.assertEquals(viewBoundaries.maxX, 110f); // node2: 100 + 10
1042+
Assert.assertEquals(viewBoundaries.maxY, 210f); // node2: 200 + 10
1043+
1044+
// Full graph boundaries should include all nodes
1045+
Assert.assertNotNull(fullBoundaries);
1046+
Assert.assertEquals(fullBoundaries.minX, -65f); // node3: -50 - 15
1047+
Assert.assertEquals(fullBoundaries.minY, -115f); // node3: -100 - 15
1048+
Assert.assertEquals(fullBoundaries.maxX, 110f); // node2: 100 + 10
1049+
Assert.assertEquals(fullBoundaries.maxY, 210f); // node2: 200 + 10
1050+
1051+
// They should be different
1052+
Assert.assertFalse(viewBoundaries.minX == fullBoundaries.minX);
1053+
Assert.assertFalse(viewBoundaries.minY == fullBoundaries.minY);
1054+
}
1055+
1056+
@Test
1057+
public void testGetBoundariesAfterViewChanges() {
1058+
GraphStore graphStore = GraphGenerator.generateEmptyGraphStore(getSpatialConfig());
1059+
GraphViewStore store = graphStore.viewStore;
1060+
GraphViewImpl view = store.createView();
1061+
1062+
// Add nodes to the graph store
1063+
NodeImpl node1 = (NodeImpl) graphStore.factory.newNode("1");
1064+
node1.setPosition(0, 0);
1065+
node1.setSize(5);
1066+
graphStore.addNode(node1);
1067+
1068+
NodeImpl node2 = (NodeImpl) graphStore.factory.newNode("2");
1069+
node2.setPosition(100, 200);
1070+
node2.setSize(10);
1071+
graphStore.addNode(node2);
1072+
1073+
DirectedSubgraph graph = store.getDirectedGraph(view);
1074+
1075+
// Initially empty view
1076+
Rect2D boundaries = graph.getSpatialIndex().getBoundaries();
1077+
Rect2D expected = new Rect2D(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY,
1078+
Float.POSITIVE_INFINITY);
1079+
Assert.assertEquals(expected, boundaries);
1080+
1081+
// Add first node to view
1082+
view.addNode(node1);
1083+
Rect2D boundaries1 = graph.getSpatialIndex().getBoundaries();
1084+
Assert.assertNotNull(boundaries1);
1085+
Assert.assertEquals(boundaries1.minX, -5f);
1086+
Assert.assertEquals(boundaries1.maxX, 5f);
1087+
1088+
// Add second node to view
1089+
view.addNode(node2);
1090+
Rect2D boundaries2 = graph.getSpatialIndex().getBoundaries();
1091+
Assert.assertNotNull(boundaries2);
1092+
Assert.assertEquals(boundaries2.minX, -5f);
1093+
Assert.assertEquals(boundaries2.maxX, 110f);
1094+
1095+
// Remove first node from view
1096+
view.removeNode(node1);
1097+
Rect2D boundaries3 = graph.getSpatialIndex().getBoundaries();
1098+
Assert.assertNotNull(boundaries3);
1099+
Assert.assertEquals(boundaries3.minX, 90f); // Only node2 remains
1100+
Assert.assertEquals(boundaries3.maxX, 110f);
1101+
1102+
// Remove last node from view
1103+
view.removeNode(node2);
1104+
Assert.assertEquals(expected, graph.getSpatialIndex().getBoundaries());
1105+
}
1106+
9211107
private void addSomeElements(GraphStore store, GraphViewImpl view) {
9221108
double perc = 0.8;
9231109
Random rand = new Random(98324);
@@ -934,4 +1120,9 @@ private void addSomeElements(GraphStore store, GraphViewImpl view) {
9341120
}
9351121
}
9361122
}
1123+
1124+
// Configuration with spatial index enabled
1125+
private Configuration getSpatialConfig() {
1126+
return Configuration.builder().enableSpatialIndex(true).build();
1127+
}
9371128
}

0 commit comments

Comments
 (0)