diff --git a/src/main/java/org/gephi/graph/api/GraphModel.java b/src/main/java/org/gephi/graph/api/GraphModel.java
index 64383bbd..1d4ab30b 100644
--- a/src/main/java/org/gephi/graph/api/GraphModel.java
+++ b/src/main/java/org/gephi/graph/api/GraphModel.java
@@ -19,6 +19,7 @@
import java.io.DataOutput;
import java.io.IOException;
import java.time.ZoneId;
+import java.util.function.Predicate;
import org.gephi.graph.impl.GraphModelImpl;
/**
@@ -488,16 +489,38 @@ public static interface DefaultColumns {
/**
* Creates a new graph view.
+ *
+ * By default, the view applies to both nodes and edges, so this is equivalent
+ * to {@link #createView(boolean, boolean) createView(true, true)}.
+ *
+ * New views are by default empty, i.e. no nodes and no edges are visible in the
+ * view.
*
* @return newly created graph view
*/
public GraphView createView();
+ /**
+ * Creates a new graph view.
+ *
+ * The node and edge filters allows to restrict the view filtering to only nodes
+ * or only edges. If node only, all edges connected to included nodes will be
+ * included too. If edge only, all nodes are included but only the edges
+ * matching the view are included.
+ *
+ * @param nodeFilter predicate to filter nodes, or null to include all nodes
+ * @param edgeFilter predicate to filter edges, or null to include all edges
+ * @return newly created graph view
+ */
+ public GraphView createView(Predicate nodeFilter, Predicate edgeFilter);
+
/**
* Creates a new graph view.
*
* The node and edge parameters allows to restrict the view filtering to only
- * nodes or only edges. By default, the view applies to both nodes and edges.
+ * nodes or only edges. If node only, all edges connected to included nodes will
+ * be included too. If edge only, all nodes are included but only the edges
+ * matching the view are included.
*
* @param node true to enable node view, false otherwise
* @param edge true to enable edge view, false otherwise
diff --git a/src/main/java/org/gephi/graph/impl/GraphModelImpl.java b/src/main/java/org/gephi/graph/impl/GraphModelImpl.java
index 062fba91..587359f0 100644
--- a/src/main/java/org/gephi/graph/impl/GraphModelImpl.java
+++ b/src/main/java/org/gephi/graph/impl/GraphModelImpl.java
@@ -17,6 +17,7 @@
import java.time.ZoneId;
import java.util.Arrays;
+import java.util.function.Predicate;
import org.gephi.graph.api.Configuration;
import org.gephi.graph.api.DirectedGraph;
import org.gephi.graph.api.DirectedSubgraph;
@@ -251,6 +252,11 @@ public GraphView createView() {
return store.viewStore.createView();
}
+ @Override
+ public GraphView createView(Predicate nodeFilter, Predicate edgeFilter) {
+ return store.viewStore.createView(nodeFilter, edgeFilter);
+ }
+
@Override
public GraphView createView(boolean node, boolean edge) {
return store.viewStore.createView(node, edge);
diff --git a/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java b/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java
index 3a7b55e4..062cd69b 100644
--- a/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java
+++ b/src/main/java/org/gephi/graph/impl/GraphViewDecorator.java
@@ -385,6 +385,9 @@ public boolean hasEdge(final Object id) {
@Override
public NodeIterable getNodes() {
+ if (!view.isNodeView()) {
+ return graphStore.getNodes();
+ }
return new NodeIterableWrapper(() -> new NodeViewIterator(graphStore.nodeStore.iterator()),
NodeViewSpliterator::new, graphStore.getAutoLock());
}
diff --git a/src/main/java/org/gephi/graph/impl/GraphViewImpl.java b/src/main/java/org/gephi/graph/impl/GraphViewImpl.java
index ae02fb75..0eece38f 100644
--- a/src/main/java/org/gephi/graph/impl/GraphViewImpl.java
+++ b/src/main/java/org/gephi/graph/impl/GraphViewImpl.java
@@ -22,6 +22,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
+import java.util.function.Predicate;
import org.gephi.graph.api.DirectedSubgraph;
import org.gephi.graph.api.Edge;
import org.gephi.graph.api.Graph;
@@ -77,6 +78,53 @@ public GraphViewImpl(final GraphStore store, boolean nodes, boolean edges) {
this.interval = Interval.INFINITY_INTERVAL;
}
+ public GraphViewImpl(final GraphStore store, Predicate nodePredicate, Predicate edgePredicate) {
+ this(store, nodePredicate != null, edgePredicate != null);
+
+ // Fill - optimized with iterators and manual counting
+ if (nodePredicate != null) {
+ int count = 0;
+ for (Node node : graphStore.nodeStore) {
+ if (nodePredicate.test(node)) {
+ nodeBitVector.set(node.getStoreId());
+ count++;
+ }
+ }
+ nodeCount = count;
+ incrementNodeVersion();
+ }
+
+ // Process edges with iterator
+ int count = 0;
+ for (Edge edge : graphStore.edgeStore) {
+ // Cache store IDs
+ int sourceId = edge.getSource().getStoreId();
+ int targetId = edge.getTarget().getStoreId();
+
+ // Filter by node predicate if needed
+ if (nodePredicate != null && (!nodeBitVector.get(sourceId) || !nodeBitVector.get(targetId))) {
+ continue;
+ }
+
+ // Filter by edge predicate if needed
+ if (edgePredicate != null && !edgePredicate.test(edge)) {
+ continue;
+ }
+
+ edgeBitVector.set(edge.getStoreId());
+ int type = edge.getType();
+ typeCounts[type]++;
+ count++;
+
+ if (((EdgeImpl) edge).isMutual() && !edge.isSelfLoop() && containsEdge(graphStore.edgeStore
+ .get(edge.getTarget(), edge.getSource(), type, false))) {
+ mutualEdgeTypeCounts[type]++;
+ mutualEdgesCount++;
+ }
+ }
+ edgeCount = count;
+ }
+
public GraphViewImpl(final GraphViewImpl view, boolean nodes, boolean edges) {
this.graphStore = view.graphStore;
this.nodeView = nodes;
@@ -962,14 +1010,6 @@ protected void destroyAllObservers() {
}
}
- protected void ensureNodeVectorSize(NodeImpl node) {
- // BitSet automatically grows as needed, no manual resizing required
- }
-
- protected void ensureEdgeVectorSize(EdgeImpl edge) {
- // BitSet automatically grows as needed, no manual resizing required
- }
-
protected void setEdgeType(EdgeImpl edgeImpl, int oldType, boolean wasMutual) {
ensureTypeCountArrayCapacity(edgeImpl.type);
typeCounts[oldType]--;
diff --git a/src/main/java/org/gephi/graph/impl/GraphViewStore.java b/src/main/java/org/gephi/graph/impl/GraphViewStore.java
index 17e8e2d2..01b86158 100644
--- a/src/main/java/org/gephi/graph/impl/GraphViewStore.java
+++ b/src/main/java/org/gephi/graph/impl/GraphViewStore.java
@@ -17,6 +17,7 @@
import it.unimi.dsi.fastutil.ints.IntRBTreeSet;
import it.unimi.dsi.fastutil.ints.IntSortedSet;
+import java.util.function.Predicate;
import org.gephi.graph.api.DirectedSubgraph;
import org.gephi.graph.api.Edge;
import org.gephi.graph.api.Graph;
@@ -54,6 +55,17 @@ public GraphViewImpl createView() {
return createView(true, true);
}
+ public GraphViewImpl createView(Predicate nodeFilter, Predicate edgeFilter) {
+ graphStore.autoWriteLock();
+ try {
+ GraphViewImpl graphView = new GraphViewImpl(graphStore, nodeFilter, edgeFilter);
+ addView(graphView);
+ return graphView;
+ } finally {
+ graphStore.autoWriteUnlock();
+ }
+ }
+
public GraphViewImpl createView(boolean nodes, boolean edges) {
graphStore.autoWriteLock();
try {
@@ -232,58 +244,38 @@ public void destroyGraphObserver(GraphObserverImpl graphObserver) {
graphViewImpl.destroyGraphObserver(graphObserver);
}
- protected void addNode(NodeImpl node) {
- if (views.length > 0) {
- for (GraphViewImpl view : views) {
- if (view != null) {
- view.ensureNodeVectorSize(node);
- }
- }
- }
- }
-
protected void removeNode(NodeImpl node) {
- if (views.length > 0) {
- for (GraphViewImpl view : views) {
- if (view != null) {
- view.removeNode(node);
- }
+ for (GraphViewImpl view : views) {
+ if (view != null) {
+ view.removeNode(node);
}
}
}
protected void addEdge(EdgeImpl edge) {
- if (views.length > 0) {
- for (GraphViewImpl view : views) {
- if (view != null) {
- view.ensureEdgeVectorSize(edge);
-
- if (view.nodeView && !view.edgeView) {
- view.addEdgeInNodeView(edge);
- }
+ for (GraphViewImpl view : views) {
+ if (view != null) {
+ if (view.nodeView && !view.edgeView) {
+ view.addEdgeInNodeView(edge);
}
}
}
}
protected void setEdgeType(EdgeImpl edge, int oldType, boolean wasMutual) {
- if (views.length > 0) {
- for (GraphViewImpl view : views) {
- if (view != null) {
- if ((view.nodeView && !view.edgeView) || (view.edgeView && view.containsEdge(edge))) {
- view.setEdgeType(edge, oldType, wasMutual);
- }
+ for (GraphViewImpl view : views) {
+ if (view != null) {
+ if ((view.nodeView && !view.edgeView) || (view.edgeView && view.containsEdge(edge))) {
+ view.setEdgeType(edge, oldType, wasMutual);
}
}
}
}
protected void removeEdge(EdgeImpl edge) {
- if (views.length > 0) {
- for (GraphViewImpl view : views) {
- if (view != null) {
- view.removeEdge(edge);
- }
+ for (GraphViewImpl view : views) {
+ if (view != null) {
+ view.removeEdge(edge);
}
}
}
diff --git a/src/main/java/org/gephi/graph/impl/NodeStore.java b/src/main/java/org/gephi/graph/impl/NodeStore.java
index 14e43744..08fa1d68 100644
--- a/src/main/java/org/gephi/graph/impl/NodeStore.java
+++ b/src/main/java/org/gephi/graph/impl/NodeStore.java
@@ -305,9 +305,6 @@ public boolean add(final Node n) {
currentBlock.add(node);
dictionary.put(node.getId(), node.storeId);
}
- if (viewStore != null) {
- viewStore.addNode(node);
- }
node.indexAttributes();
if (spatialIndex != null) {
diff --git a/src/test/java/org/gephi/graph/impl/GraphModelTest.java b/src/test/java/org/gephi/graph/impl/GraphModelTest.java
index 07125d01..12a95e1a 100644
--- a/src/test/java/org/gephi/graph/impl/GraphModelTest.java
+++ b/src/test/java/org/gephi/graph/impl/GraphModelTest.java
@@ -202,6 +202,203 @@ public void testCreateViewCustom() {
Assert.assertFalse(view.isEdgeView());
}
+ @Test
+ public void testCreateViewWithPredicates() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ GraphView view = graphModel.createView(n -> true, e -> true);
+ Assert.assertNotNull(view);
+ Assert.assertSame(view.getGraphModel(), graphModel);
+ Assert.assertTrue(view.isNodeView());
+ Assert.assertTrue(view.isEdgeView());
+ }
+
+ @Test
+ public void testCreateViewWithNodePredicateOnly() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ GraphView view = graphModel.createView(n -> true, null);
+ Assert.assertNotNull(view);
+ Assert.assertTrue(view.isNodeView());
+ Assert.assertFalse(view.isEdgeView());
+ }
+
+ @Test
+ public void testCreateViewWithEdgePredicateOnly() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ GraphView view = graphModel.createView(null, e -> true);
+ Assert.assertNotNull(view);
+ Assert.assertFalse(view.isNodeView());
+ Assert.assertTrue(view.isEdgeView());
+ }
+
+ @Test
+ public void testCreateViewWithBothPredicatesNull() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ GraphView view = graphModel.createView(null, null);
+ Assert.assertNotNull(view);
+ Assert.assertFalse(view.isNodeView());
+ Assert.assertFalse(view.isEdgeView());
+ }
+
+ @Test
+ public void testCreateViewWithNodePredicateFiltering() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ Table table = graphModel.getNodeTable();
+ Column col = table.addColumn("value", Integer.class);
+
+ Node n1 = graphModel.factory().newNode("1");
+ n1.setAttribute(col, 10);
+ Node n2 = graphModel.factory().newNode("2");
+ n2.setAttribute(col, 20);
+ Node n3 = graphModel.factory().newNode("3");
+ n3.setAttribute(col, 30);
+ graphModel.getStore().addAllNodes(Arrays.asList(new Node[] { n1, n2, n3 }));
+
+ // Create view with predicate that filters nodes with value > 15
+ GraphView view = graphModel.createView(n -> {
+ Integer value = (Integer) n.getAttribute(col);
+ return value != null && value > 15;
+ }, null);
+
+ Graph subgraph = graphModel.getGraph(view);
+ Assert.assertEquals(subgraph.getNodeCount(), 2);
+ Assert.assertTrue(subgraph.contains(n2));
+ Assert.assertTrue(subgraph.contains(n3));
+ Assert.assertFalse(subgraph.contains(n1));
+ }
+
+ @Test
+ public void testCreateViewWithEdgePredicateFiltering() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ Table table = graphModel.getEdgeTable();
+ Column col = table.addColumn("weight_custom", Double.class);
+
+ Node n1 = graphModel.factory().newNode("1");
+ Node n2 = graphModel.factory().newNode("2");
+ Node n3 = graphModel.factory().newNode("3");
+ graphModel.getStore().addAllNodes(Arrays.asList(new Node[] { n1, n2, n3 }));
+
+ Edge e1 = graphModel.factory().newEdge(n1, n2);
+ e1.setAttribute(col, 1.0);
+ Edge e2 = graphModel.factory().newEdge(n2, n3);
+ e2.setAttribute(col, 5.0);
+ Edge e3 = graphModel.factory().newEdge(n1, n3);
+ e3.setAttribute(col, 10.0);
+ graphModel.getStore().addAllEdges(Arrays.asList(new Edge[] { e1, e2, e3 }));
+
+ // Create view with predicate that filters edges with weight >= 5.0
+ GraphView view = graphModel.createView(null, e -> {
+ Double weight = (Double) e.getAttribute(col);
+ return weight != null && weight >= 5.0;
+ });
+
+ Graph subgraph = graphModel.getGraph(view);
+ Assert.assertEquals(subgraph.getNodeCount(), 3); // All nodes included
+ Assert.assertEquals(subgraph.getEdgeCount(), 2);
+ Assert.assertTrue(subgraph.contains(e2));
+ Assert.assertTrue(subgraph.contains(e3));
+ Assert.assertFalse(subgraph.contains(e1));
+ }
+
+ @Test
+ public void testCreateViewWithBothPredicatesFiltering() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ Table nodeTable = graphModel.getNodeTable();
+ Table edgeTable = graphModel.getEdgeTable();
+ Column nodeCol = nodeTable.addColumn("active", Boolean.class);
+ Column edgeCol = edgeTable.addColumn("strength", Double.class);
+
+ Node n1 = graphModel.factory().newNode("1");
+ n1.setAttribute(nodeCol, true);
+ Node n2 = graphModel.factory().newNode("2");
+ n2.setAttribute(nodeCol, false);
+ Node n3 = graphModel.factory().newNode("3");
+ n3.setAttribute(nodeCol, true);
+ graphModel.getStore().addAllNodes(Arrays.asList(new Node[] { n1, n2, n3 }));
+
+ Edge e1 = graphModel.factory().newEdge(n1, n2);
+ e1.setAttribute(edgeCol, 0.5);
+ Edge e2 = graphModel.factory().newEdge(n2, n3);
+ e2.setAttribute(edgeCol, 0.8);
+ Edge e3 = graphModel.factory().newEdge(n1, n3);
+ e3.setAttribute(edgeCol, 0.3);
+ graphModel.getStore().addAllEdges(Arrays.asList(new Edge[] { e1, e2, e3 }));
+
+ // Create view with both predicates
+ GraphView view = graphModel.createView(n -> Boolean.TRUE.equals(n.getAttribute(nodeCol)), e -> {
+ Double strength = (Double) e.getAttribute(edgeCol);
+ return strength != null && strength > 0.4;
+ });
+
+ Graph subgraph = graphModel.getGraph(view);
+
+ Assert.assertEquals(subgraph.getNodeCount(), 2); // n1 and n3
+ Assert.assertTrue(subgraph.contains(n1));
+ Assert.assertTrue(subgraph.contains(n3));
+ Assert.assertFalse(subgraph.contains(n2));
+
+ // No edges should be included:
+ // - e1 has n2 which is not in node view (active=false)
+ // - e2 has n2 which is not in node view (active=false)
+ // - e3 connects n1 and n3 (both in view) but strength 0.3 fails edge filter
+ Assert.assertEquals(subgraph.getEdgeCount(), 0);
+ Assert.assertFalse(subgraph.contains(e1));
+ Assert.assertFalse(subgraph.contains(e2));
+ Assert.assertFalse(subgraph.contains(e3));
+ }
+
+ @Test
+ public void testCreateViewWithBothPredicatesFilteringWithMatchingEdges() {
+ GraphModelImpl graphModel = new GraphModelImpl();
+ Table nodeTable = graphModel.getNodeTable();
+ Table edgeTable = graphModel.getEdgeTable();
+ Column nodeCol = nodeTable.addColumn("category", String.class);
+ Column edgeCol = edgeTable.addColumn("score", Double.class);
+
+ Node n1 = graphModel.factory().newNode("1");
+ n1.setAttribute(nodeCol, "A");
+ Node n2 = graphModel.factory().newNode("2");
+ n2.setAttribute(nodeCol, "B");
+ Node n3 = graphModel.factory().newNode("3");
+ n3.setAttribute(nodeCol, "A");
+ Node n4 = graphModel.factory().newNode("4");
+ n4.setAttribute(nodeCol, "A");
+ graphModel.getStore().addAllNodes(Arrays.asList(new Node[] { n1, n2, n3, n4 }));
+
+ Edge e1 = graphModel.factory().newEdge(n1, n2);
+ e1.setAttribute(edgeCol, 5.0);
+ Edge e2 = graphModel.factory().newEdge(n1, n3);
+ e2.setAttribute(edgeCol, 3.0);
+ Edge e3 = graphModel.factory().newEdge(n3, n4);
+ e3.setAttribute(edgeCol, 8.0);
+ Edge e4 = graphModel.factory().newEdge(n2, n4);
+ e4.setAttribute(edgeCol, 1.0);
+ graphModel.getStore().addAllEdges(Arrays.asList(new Edge[] { e1, e2, e3, e4 }));
+
+ // Create view: nodes with category "A" AND edges with score > 2.0
+ GraphView view = graphModel.createView(n -> "A".equals(n.getAttribute(nodeCol)), e -> {
+ Double score = (Double) e.getAttribute(edgeCol);
+ return score != null && score > 2.0;
+ });
+
+ Graph subgraph = graphModel.getGraph(view);
+
+ // Nodes: n1, n3, n4 (all category "A")
+ Assert.assertEquals(subgraph.getNodeCount(), 3);
+ Assert.assertTrue(subgraph.contains(n1));
+ Assert.assertTrue(subgraph.contains(n3));
+ Assert.assertTrue(subgraph.contains(n4));
+ Assert.assertFalse(subgraph.contains(n2));
+
+ // Edges: only e2 (n1-n3, score 3.0) and e3 (n3-n4, score 8.0)
+ // e1 excluded: n2 not in view
+ // e4 excluded: n2 not in view
+ Assert.assertEquals(subgraph.getEdgeCount(), 2);
+ Assert.assertFalse(subgraph.contains(e1));
+ Assert.assertTrue(subgraph.contains(e2));
+ Assert.assertTrue(subgraph.contains(e3));
+ Assert.assertFalse(subgraph.contains(e4));
+ }
+
@Test
public void testCopyView() {
GraphModelImpl graphModel = new GraphModelImpl();
diff --git a/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java b/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java
index 377d3a6c..5092c72c 100644
--- a/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java
+++ b/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java
@@ -25,6 +25,7 @@
import org.gephi.graph.api.GraphFactory;
import org.gephi.graph.api.GraphView;
import org.gephi.graph.api.Node;
+import org.gephi.graph.api.Subgraph;
import org.gephi.graph.api.UndirectedSubgraph;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -48,6 +49,7 @@ public void testFill() {
}
for (Node n : graphStore.getNodes()) {
Assert.assertTrue(graph.contains(n));
+ Assert.assertEquals(graph.getDegree(n), graphStore.getDegree(n));
}
for (int i = 0; i < graphStore.edgeTypeStore.length; i++) {
Assert.assertEquals(graph.getEdgeCount(i), graphStore.getEdgeCount(i));
@@ -108,6 +110,20 @@ public void testAddEdgeMainView() {
Assert.assertTrue(view.containsEdge(edge));
}
+ @Test
+ public void testEdgeViewNodeBehaviors() {
+ GraphStore graphStore = GraphGenerator.generateSmallGraphStore();
+ GraphViewStore store = graphStore.viewStore;
+ GraphViewImpl view = store.createView(false, true);
+ Subgraph subgraph = store.getGraph(view);
+ Assert.assertEquals(subgraph.getNodeCount(), graphStore.getNodeCount());
+ Assert.assertEquals(subgraph.getNodes().stream().count(), graphStore.getNodeCount());
+ for (Node node : subgraph.getNodes()) {
+ Assert.assertSame(subgraph.getNode(node.getId()), node);
+ Assert.assertEquals(subgraph.getDegree(node), 0);
+ }
+ }
+
@Test
public void testViewDeepEquals() {
GraphStore graphStore = GraphGenerator.generateSmallMultiTypeGraphStore();
@@ -524,6 +540,23 @@ public void testNodeViewEdgeUpdate() {
Assert.assertTrue(view.containsEdge(edge));
}
+ @Test
+ public void testEdgeViewUpdate() {
+ GraphStore graphStore = GraphGenerator.generateSmallGraphStore();
+ GraphViewStore store = graphStore.viewStore;
+ GraphViewImpl view = store.createView(false, true);
+
+ GraphFactory factory = graphStore.factory;
+ Node n1 = factory.newNode("foo1");
+ Node n2 = factory.newNode("foo2");
+ graphStore.addNode(n1);
+ graphStore.addNode(n2);
+ Assert.assertEquals(view.getNodeCount(), graphStore.getNodeCount());
+ Edge e1 = factory.newEdge("foo", n1, n2, 0, 0.0, true);
+ graphStore.addEdge(e1);
+ Assert.assertFalse(view.containsEdge(e1));
+ }
+
@Test
public void testIsNodeView() {
GraphStore graphStore = GraphGenerator.generateSmallGraphStore();