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();