From dd5289194e4f082898a5b083778c0c013997329f Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Thu, 2 Oct 2025 15:03:57 +0100 Subject: [PATCH 01/12] Generate MultiNodeLineage plugin This is the result of running: mvn org.gephi:gephi-maven-plugin:generate --- modules/MultiNodeLineage/README.md | 4 ++ modules/MultiNodeLineage/pom.xml | 53 +++++++++++++++++++ .../MultiNodeLineage/src/main/nbm/manifest.mf | 5 ++ pom.xml | 1 + 4 files changed, 63 insertions(+) create mode 100644 modules/MultiNodeLineage/README.md create mode 100644 modules/MultiNodeLineage/pom.xml create mode 100644 modules/MultiNodeLineage/src/main/nbm/manifest.mf diff --git a/modules/MultiNodeLineage/README.md b/modules/MultiNodeLineage/README.md new file mode 100644 index 000000000..4f2175194 --- /dev/null +++ b/modules/MultiNodeLineage/README.md @@ -0,0 +1,4 @@ +## Multi Node Lineage + +This README supports Markdown, see [syntax](https://help.github.com/articles/markdown-basics/) + diff --git a/modules/MultiNodeLineage/pom.xml b/modules/MultiNodeLineage/pom.xml new file mode 100644 index 000000000..38e376f66 --- /dev/null +++ b/modules/MultiNodeLineage/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + gephi-plugin-parent + org.gephi + 0.10.0 + + + uk.co.timsummertonbrier + multi-node-lineage + 1.0.0 + nbm + + Multi Node Lineage + + + + + + + + + org.apache.netbeans.utilities + nbm-maven-plugin + + MIT + Tim Summerton-Brier + + + https://github.com/sizlo/gephi-plugins + + + + + + + + + + + + oss-sonatype + oss-sonatype + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + + + + diff --git a/modules/MultiNodeLineage/src/main/nbm/manifest.mf b/modules/MultiNodeLineage/src/main/nbm/manifest.mf new file mode 100644 index 000000000..0477c8bb7 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/nbm/manifest.mf @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +OpenIDE-Module-Name: Multi Node Lineage +OpenIDE-Module-Short-Description: Identifies the ancestors and descendants of a set of nodes. +OpenIDE-Module-Long-Description: Identifies the ancestors and descendants of a set of nodes. TODO add more details. +OpenIDE-Module-Display-Category: Metric diff --git a/pom.xml b/pom.xml index afb8f2bf5..3e151814f 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ + modules/MultiNodeLineage From bd46f73a725c89bbbd7d4733b16d6fda8c2ce28f Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Thu, 2 Oct 2025 15:53:17 +0100 Subject: [PATCH 02/12] Add maven repositories required to build --- pom.xml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pom.xml b/pom.xml index 3e151814f..62e5f0159 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,28 @@ true + + + gephi + https://raw.github.com/gephi/gephi/mvn-thirdparty-repo/ + + true + + + true + + + + + jzy3d + https://maven.jzy3d.org/releases/ + + true + + + true + + From f01cbb8573f8e01c2daf22854401891b5b1e8024 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Mon, 6 Oct 2025 18:02:10 +0100 Subject: [PATCH 03/12] Add UI for MultiNodeLineage statistic plugin --- modules/MultiNodeLineage/pom.xml | 8 +++ .../multinodelineage/MultiNodeLineage.java | 26 ++++++++ .../MultiNodeLineageBuilder.java | 28 ++++++++ .../MultiNodeLineagePanel.java | 46 +++++++++++++ .../multinodelineage/MultiNodeLineageUI.java | 65 +++++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageBuilder.java create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageUI.java diff --git a/modules/MultiNodeLineage/pom.xml b/modules/MultiNodeLineage/pom.xml index 38e376f66..ded902d7d 100644 --- a/modules/MultiNodeLineage/pom.xml +++ b/modules/MultiNodeLineage/pom.xml @@ -16,6 +16,14 @@ + + org.gephi + graph-api + + + org.gephi + statistics-api + diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java new file mode 100644 index 000000000..9078c0e86 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -0,0 +1,26 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.gephi.graph.api.GraphModel; +import org.gephi.statistics.spi.Statistics; + +public class MultiNodeLineage implements Statistics { + + private List originNodeIds = new ArrayList<>(); + + @Override + public void execute(GraphModel graphModel) { + + } + + @Override + public String getReport() { + return originNodeIds.stream().collect(Collectors.joining("\n")); + } + + public void setOriginNodeIds(List originNodeIds) { + this.originNodeIds = originNodeIds; + } +} \ No newline at end of file diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageBuilder.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageBuilder.java new file mode 100644 index 000000000..211ecca14 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageBuilder.java @@ -0,0 +1,28 @@ + +package uk.co.timsummertonbrier.multinodelineage; + +import org.gephi.statistics.spi.Statistics; +import org.gephi.statistics.spi.StatisticsBuilder; +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service = StatisticsBuilder.class) +public class MultiNodeLineageBuilder implements StatisticsBuilder { + + @Override + public String getName() { + return "Multi Node Lineage"; + } + + @Override + public Statistics getStatistics() { + return new MultiNodeLineage(); + } + + @Override + public Class getStatisticsClass() { + return MultiNodeLineage.class; + } + + + +} diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java new file mode 100644 index 000000000..33f757413 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java @@ -0,0 +1,46 @@ + +package uk.co.timsummertonbrier.multinodelineage; + +import java.awt.Dimension; +import java.util.Arrays; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +public class MultiNodeLineagePanel extends JPanel { + + private final JTextField originNodeIdsTextField = new JTextField(); + + public MultiNodeLineagePanel() { + initComponents(); + } + + public List getOriginNodeIds() { + return Arrays.asList(originNodeIdsTextField.getText().split(",")); + } + + private void initComponents() { + setPreferredSize(new Dimension(450, 160)); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + addText("Identifies the ancestors and descendants of a set of nodes. Attributes are added to the nodes to record the results. These attributes can be used for further analysis (e.g filtering)."); + addGap(); + addText("Enter the ID(s) of the origin node(s) you wish to process. Separate multiple IDs with a comma, e.g: node1,node2,node3"); + addGap(); + add(originNodeIdsTextField); + } + + private void addText(String text) { + add(new JLabel("" + text)); + } + + private void addGap() { + add(Box.createRigidArea(new Dimension(0, 20))); + } + +} diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageUI.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageUI.java new file mode 100644 index 000000000..cc353184b --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageUI.java @@ -0,0 +1,65 @@ + +package uk.co.timsummertonbrier.multinodelineage; + +import javax.swing.JPanel; +import org.gephi.statistics.spi.Statistics; +import org.gephi.statistics.spi.StatisticsUI; +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service = StatisticsUI.class) +public class MultiNodeLineageUI implements StatisticsUI { + + private MultiNodeLineage statistics; + private MultiNodeLineagePanel panel; + + @Override + public JPanel getSettingsPanel() { + panel = new MultiNodeLineagePanel(); + return panel; + } + + @Override + public void setup(Statistics ststcs) { + statistics = (MultiNodeLineage) ststcs; + } + + @Override + public void unsetup() { + if (panel != null) { + statistics.setOriginNodeIds(panel.getOriginNodeIds()); + } + statistics = null; + panel = null; + } + + @Override + public Class getStatisticsClass() { + return MultiNodeLineage.class; + } + + @Override + public String getValue() { + return ""; + } + + @Override + public String getDisplayName() { + return "Multi Node Lineage"; + } + + @Override + public String getShortDescription() { + return "Identifies the ancestors and descendants of a set of nodes."; + } + + @Override + public String getCategory() { + return StatisticsUI.CATEGORY_NODE_OVERVIEW; + } + + @Override + public int getPosition() { + return 9999; + } + +} From 04625284e9f08afe6972327b567b404c8b6038d3 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 11:52:57 +0100 Subject: [PATCH 04/12] Implement MultiNodeLineage using breadth first search This performs breadth first search for each origin node twice, once looking for ancestors, and once looking for descendants. The results are recorded in attributes on nodes, these are: IsOrigin: was this node an origin of any of the searches IsAncestor: was this node found as an ancestor in any of the searches IsDescendant: was this node found as a descendant in any of the searches AncestorOf: comma separated list of node ids this node is an ancestor of DescendantOf: comma separated list of node ids this node is a descendant of --- .../multinodelineage/BreadthFirstSearch.java | 37 ++++++ .../multinodelineage/Lineage.java | 21 ++++ .../multinodelineage/MultiNodeLineage.java | 113 +++++++++++++++++- .../multinodelineage/NodeChildSupplier.java | 9 ++ 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/Lineage.java create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/NodeChildSupplier.java diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java new file mode 100644 index 000000000..e37da55fc --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java @@ -0,0 +1,37 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; +import org.gephi.graph.api.DirectedGraph; +import org.gephi.graph.api.Node; + +public class BreadthFirstSearch { + + public static Set run( + DirectedGraph graph, + String originId, + NodeChildSupplier nodeChildSupplier + ) { + Set seenIds = new HashSet<>(); + Queue queue = new LinkedList<>(); + + Node originNode = graph.getNode(originId); + queue.add(originNode); + seenIds.add(originId); + + while (!queue.isEmpty()) { + Node current = queue.remove(); + for (Node child: nodeChildSupplier.getChildren(graph, current)) { + if (!seenIds.contains(child.getId().toString())) { + seenIds.add(child.getId().toString()); + queue.add(child); + } + } + } + + return seenIds; + } + +} \ No newline at end of file diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/Lineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/Lineage.java new file mode 100644 index 000000000..ab6867161 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/Lineage.java @@ -0,0 +1,21 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import java.util.Set; + +public class Lineage { + private final Set ancestorIds; + private final Set descendantIds; + + public Lineage(Set ancestorIds, Set descendantIds) { + this.ancestorIds = ancestorIds; + this.descendantIds = descendantIds; + } + + public Set getAncestorIds() { + return ancestorIds; + } + + public Set getDescendantIds() { + return descendantIds; + } +} \ No newline at end of file diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index 9078c0e86..bc9760643 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -1,26 +1,133 @@ package uk.co.timsummertonbrier.multinodelineage; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; +import java.util.Set; +import org.gephi.graph.api.Column; +import org.gephi.graph.api.DirectedGraph; import org.gephi.graph.api.GraphModel; +import org.gephi.graph.api.Node; +import org.gephi.graph.api.Table; import org.gephi.statistics.spi.Statistics; public class MultiNodeLineage implements Statistics { private List originNodeIds = new ArrayList<>(); + private Map lineages; + private GraphModel graphModel; + private DirectedGraph graph; @Override public void execute(GraphModel graphModel) { - + // TODO: + // - Add validation + // - if graph is int id, all ids are ints + // - all ids are present in graph + // - Output error messages to report + // - Test on int id graph + // - Test on non-directed graph + init(graphModel); + originNodeIds.forEach(originNodeId -> calculateLineage(originNodeId)); + recordResultsAsAttributes(); } @Override public String getReport() { - return originNodeIds.stream().collect(Collectors.joining("\n")); + StringBuilder report = new StringBuilder(); + + report.append("

Multi Node Lineage has run successfully.

\n"); + report.append("

The results have been recorded as new attributes on each Node (see Data Laboratory).

\n"); + + lineages.forEach((originNodeId, lineage) -> + report.append(reportSectionForOneOrigin(originNodeId, lineage)) + ); + + return report.toString(); } public void setOriginNodeIds(List originNodeIds) { this.originNodeIds = originNodeIds; } + + private void init(GraphModel graphModel) { + this.graphModel = graphModel; + graph = graphModel.getDirectedGraphVisible(); + lineages = new HashMap<>(); + } + + private void calculateLineage(String originNodeId) { + Set ancestorIds = BreadthFirstSearch.run( + graph, + originNodeId, + (DirectedGraph g, Node n) -> g.getSuccessors(n) + ); + ancestorIds.remove(originNodeId); + + Set descendantIds = BreadthFirstSearch.run( + graph, + originNodeId, + (DirectedGraph g, Node n) -> g.getPredecessors(n) + ); + descendantIds.remove(originNodeId); + + lineages.put(originNodeId, new Lineage(ancestorIds, descendantIds)); + } + + private String reportSectionForOneOrigin(String originNodeId, Lineage lineage) { + return String.format( + "

Node %s has %d ancestors and %d descendants.

\n", + originNodeId, + lineage.getAncestorIds().size(), + lineage.getDescendantIds().size() + ); + } + + private void recordResultsAsAttributes() { + setupColumns(); + + lineages.forEach((originNodeId, lineage) -> { + graph.getNode(originNodeId).setAttribute("IsOrigin", true); + + lineage.getAncestorIds().forEach(ancestorId -> { + Node ancestorNode = graph.getNode(ancestorId); + ancestorNode.setAttribute("IsAncestor", true); + addNodeIdToList(ancestorNode, "AncestorOf", originNodeId); + }); + + lineage.getDescendantIds().forEach(descendantId -> { + Node descendantNode = graph.getNode(descendantId); + descendantNode.setAttribute("IsDescendant", true); + addNodeIdToList(descendantNode, "DescendantOf", originNodeId); + }); + }); + } + + private void setupColumns() { + Table table = graphModel.getNodeTable(); + recreateColumn(table, "IsOrigin", Boolean.class, false); + recreateColumn(table, "IsAncestor", Boolean.class, false); + recreateColumn(table, "IsDescendant", Boolean.class, false); + recreateColumn(table, "AncestorOf", String.class, ""); + recreateColumn(table, "DescendantOf", String.class, ""); + } + + private Column recreateColumn(Table table, String name, Class type, Object defaultValue) { + // Remove existing column to delete the results from previous runs + if (table.hasColumn(name)) { + table.removeColumn(name); + } + return table.addColumn(name, name, type, defaultValue); + } + + private void addNodeIdToList(Node node, String listColumnName, String idToAdd) { + String existingList = (String) node.getAttribute(listColumnName); + + if (existingList.isEmpty()) { + node.setAttribute(listColumnName, idToAdd); + } else { + node.setAttribute(listColumnName, existingList + "," + idToAdd); + } + } } \ No newline at end of file diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/NodeChildSupplier.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/NodeChildSupplier.java new file mode 100644 index 000000000..03a451e76 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/NodeChildSupplier.java @@ -0,0 +1,9 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import org.gephi.graph.api.DirectedGraph; +import org.gephi.graph.api.Node; +import org.gephi.graph.api.NodeIterable; + +public interface NodeChildSupplier { + NodeIterable getChildren(DirectedGraph graph, Node parent); +} \ No newline at end of file From cf64052f7c7102d20cbaeea341421c4be59b6b7f Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 12:25:47 +0100 Subject: [PATCH 05/12] Validate requested origin node ids are present in the graph --- .../multinodelineage/MultiNodeLineage.java | 59 +++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index bc9760643..5a55a4311 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -2,9 +2,11 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.gephi.graph.api.Column; import org.gephi.graph.api.DirectedGraph; import org.gephi.graph.api.GraphModel; @@ -18,6 +20,7 @@ public class MultiNodeLineage implements Statistics { private Map lineages; private GraphModel graphModel; private DirectedGraph graph; + private List errors; @Override public void execute(GraphModel graphModel) { @@ -29,22 +32,19 @@ public void execute(GraphModel graphModel) { // - Test on int id graph // - Test on non-directed graph init(graphModel); + if (!validate()) { + return; + } originNodeIds.forEach(originNodeId -> calculateLineage(originNodeId)); recordResultsAsAttributes(); } @Override public String getReport() { - StringBuilder report = new StringBuilder(); - - report.append("

Multi Node Lineage has run successfully.

\n"); - report.append("

The results have been recorded as new attributes on each Node (see Data Laboratory).

\n"); - - lineages.forEach((originNodeId, lineage) -> - report.append(reportSectionForOneOrigin(originNodeId, lineage)) - ); - - return report.toString(); + if (errors.isEmpty()) { + return getSuccessfulReport(); + } + return getErrorReport(); } public void setOriginNodeIds(List originNodeIds) { @@ -55,6 +55,24 @@ private void init(GraphModel graphModel) { this.graphModel = graphModel; graph = graphModel.getDirectedGraphVisible(); lineages = new HashMap<>(); + errors = new ArrayList<>(); + } + + private boolean validate() { + Set notFoundNodeIds = new HashSet<>(); + + originNodeIds.forEach(id -> { + if (graph.getNode(id) == null) { + notFoundNodeIds.add(id); + } + }); + + if (!notFoundNodeIds.isEmpty()) { + String notFoundNodeIdsString = notFoundNodeIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(", ")); + errors.add("The following requested origin node IDs could not be found in the graph: " + notFoundNodeIdsString); + } + + return errors.isEmpty(); } private void calculateLineage(String originNodeId) { @@ -130,4 +148,25 @@ private void addNodeIdToList(Node node, String listColumnName, String idToAdd) { node.setAttribute(listColumnName, existingList + "," + idToAdd); } } + + private String getSuccessfulReport() { + StringBuilder report = new StringBuilder(); + + report.append("

Multi Node Lineage has run successfully.

\n"); + report.append("

The results have been recorded as new attributes on each Node (see Data Laboratory).

\n"); + + lineages.forEach((originNodeId, lineage) -> + report.append(reportSectionForOneOrigin(originNodeId, lineage)) + ); + + return report.toString(); + } + + private String getErrorReport() { + StringBuilder report = new StringBuilder(); + report.append("

Error running Multi Node Lineage.

\n"); + errors.forEach(error -> report.append("

").append(error).append("

\n")); + return report.toString(); + } + } \ No newline at end of file From 13498ee5c8325579d09f4ab7a69bfe01eb0b78b3 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 12:47:13 +0100 Subject: [PATCH 06/12] Validate that origin node ids isn't empty --- .../multinodelineage/MultiNodeLineage.java | 6 ++++-- .../multinodelineage/MultiNodeLineagePanel.java | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index 5a55a4311..fc6a944e1 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -27,8 +27,6 @@ public void execute(GraphModel graphModel) { // TODO: // - Add validation // - if graph is int id, all ids are ints - // - all ids are present in graph - // - Output error messages to report // - Test on int id graph // - Test on non-directed graph init(graphModel); @@ -59,6 +57,10 @@ private void init(GraphModel graphModel) { } private boolean validate() { + if (originNodeIds.isEmpty()) { + errors.add("No origin node ID(s) were supplied"); + } + Set notFoundNodeIds = new HashSet<>(); originNodeIds.forEach(id -> { diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java index 33f757413..45d4e02b5 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java @@ -3,6 +3,7 @@ import java.awt.Dimension; import java.util.Arrays; +import java.util.Collections; import java.util.List; import javax.swing.BorderFactory; import javax.swing.Box; @@ -20,7 +21,11 @@ public MultiNodeLineagePanel() { } public List getOriginNodeIds() { - return Arrays.asList(originNodeIdsTextField.getText().split(",")); + String allOriginNodeIds = originNodeIdsTextField.getText(); + if (allOriginNodeIds.isEmpty()) { + return Collections.emptyList(); + } + return Arrays.asList(allOriginNodeIds.split(",")); } private void initComponents() { From 4bb19aea7721f0ae162156b113a827afe2c0e0b8 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 13:32:02 +0100 Subject: [PATCH 07/12] Add support for int node IDs --- .../multinodelineage/BreadthFirstSearch.java | 3 +- .../multinodelineage/GetNodeById.java | 20 +++++ .../multinodelineage/MultiNodeLineage.java | 51 ++++-------- .../MultiNodeLineageValidator.java | 77 +++++++++++++++++++ 4 files changed, 114 insertions(+), 37 deletions(-) create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java create mode 100644 modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java index e37da55fc..b2d104481 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java @@ -6,6 +6,7 @@ import java.util.Set; import org.gephi.graph.api.DirectedGraph; import org.gephi.graph.api.Node; +import static uk.co.timsummertonbrier.multinodelineage.GetNodeById.getNodeById; public class BreadthFirstSearch { @@ -17,7 +18,7 @@ public static Set run( Set seenIds = new HashSet<>(); Queue queue = new LinkedList<>(); - Node originNode = graph.getNode(originId); + Node originNode = getNodeById(graph, originId); queue.add(originNode); seenIds.add(originId); diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java new file mode 100644 index 000000000..ecba0ea56 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java @@ -0,0 +1,20 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import org.gephi.graph.api.DirectedGraph; +import org.gephi.graph.api.Node; + +public class GetNodeById { + + public static Node getNodeById(DirectedGraph graph, String id) { + Class nodeIdType = graph.getModel().getConfiguration().getNodeIdType(); + + if (nodeIdType == Integer.class) { + return graph.getNode(Integer.valueOf(id)); + } else if (nodeIdType == String.class) { + return graph.getNode(id); + } else { + throw new RuntimeException("Unsupported node ID type: " + nodeIdType); + } + } + +} \ No newline at end of file diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index fc6a944e1..579255f71 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -2,17 +2,16 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import org.gephi.graph.api.Column; import org.gephi.graph.api.DirectedGraph; import org.gephi.graph.api.GraphModel; import org.gephi.graph.api.Node; import org.gephi.graph.api.Table; import org.gephi.statistics.spi.Statistics; +import static uk.co.timsummertonbrier.multinodelineage.GetNodeById.getNodeById; public class MultiNodeLineage implements Statistics { @@ -24,11 +23,7 @@ public class MultiNodeLineage implements Statistics { @Override public void execute(GraphModel graphModel) { - // TODO: - // - Add validation - // - if graph is int id, all ids are ints - // - Test on int id graph - // - Test on non-directed graph + // TODO: Test on non-directed graph init(graphModel); if (!validate()) { return; @@ -57,23 +52,7 @@ private void init(GraphModel graphModel) { } private boolean validate() { - if (originNodeIds.isEmpty()) { - errors.add("No origin node ID(s) were supplied"); - } - - Set notFoundNodeIds = new HashSet<>(); - - originNodeIds.forEach(id -> { - if (graph.getNode(id) == null) { - notFoundNodeIds.add(id); - } - }); - - if (!notFoundNodeIds.isEmpty()) { - String notFoundNodeIdsString = notFoundNodeIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(", ")); - errors.add("The following requested origin node IDs could not be found in the graph: " + notFoundNodeIdsString); - } - + errors.addAll(new MultiNodeLineageValidator(graphModel, graph, originNodeIds).validate()); return errors.isEmpty(); } @@ -94,30 +73,21 @@ private void calculateLineage(String originNodeId) { lineages.put(originNodeId, new Lineage(ancestorIds, descendantIds)); } - - private String reportSectionForOneOrigin(String originNodeId, Lineage lineage) { - return String.format( - "

Node %s has %d ancestors and %d descendants.

\n", - originNodeId, - lineage.getAncestorIds().size(), - lineage.getDescendantIds().size() - ); - } private void recordResultsAsAttributes() { setupColumns(); lineages.forEach((originNodeId, lineage) -> { - graph.getNode(originNodeId).setAttribute("IsOrigin", true); + getNodeById(graph, originNodeId).setAttribute("IsOrigin", true); lineage.getAncestorIds().forEach(ancestorId -> { - Node ancestorNode = graph.getNode(ancestorId); + Node ancestorNode = getNodeById(graph, ancestorId); ancestorNode.setAttribute("IsAncestor", true); addNodeIdToList(ancestorNode, "AncestorOf", originNodeId); }); lineage.getDescendantIds().forEach(descendantId -> { - Node descendantNode = graph.getNode(descendantId); + Node descendantNode = getNodeById(graph, descendantId); descendantNode.setAttribute("IsDescendant", true); addNodeIdToList(descendantNode, "DescendantOf", originNodeId); }); @@ -170,5 +140,14 @@ private String getErrorReport() { errors.forEach(error -> report.append("

").append(error).append("

\n")); return report.toString(); } + + private String reportSectionForOneOrigin(String originNodeId, Lineage lineage) { + return String.format( + "

Node %s has %d ancestors and %d descendants.

\n", + originNodeId, + lineage.getAncestorIds().size(), + lineage.getDescendantIds().size() + ); + } } \ No newline at end of file diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java new file mode 100644 index 000000000..06d7af6a1 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java @@ -0,0 +1,77 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.gephi.graph.api.DirectedGraph; +import org.gephi.graph.api.GraphModel; +import static uk.co.timsummertonbrier.multinodelineage.GetNodeById.getNodeById; + +public class MultiNodeLineageValidator { + + private final GraphModel graphModel; + private final DirectedGraph graph; + private final List originNodeIds; + private final List errors; + private final List notIntNodeIds; + private final List notFoundNodeIds; + + public MultiNodeLineageValidator(GraphModel graphModel, DirectedGraph graph, List originNodeIds) { + this.graphModel = graphModel; + this.graph = graph; + this.originNodeIds = originNodeIds; + errors = new ArrayList<>(); + notIntNodeIds = new ArrayList<>(); + notFoundNodeIds = new ArrayList<>(); + } + + public List validate() { + validateOriginNodeIdsIsNotEmpty(); + validateEachNodeId(); + return errors; + } + + private void validateOriginNodeIdsIsNotEmpty() { + if (originNodeIds.isEmpty()) { + errors.add("No origin node ID(s) were supplied"); + } + } + + private void validateEachNodeId() { + originNodeIds.forEach(id -> checkNodeId(id)); + if (!notIntNodeIds.isEmpty()) { + errors.add("This graph uses integer node IDs, and the following requested origin node IDs are not integers: " + listToString(notIntNodeIds)); + } + if (!notFoundNodeIds.isEmpty()) { + errors.add("The following requested origin node IDs could not be found in the graph: " + listToString(notFoundNodeIds)); + } + } + + private boolean usingIntegerNodeIds() { + return graphModel.getConfiguration().getNodeIdType() == Integer.class; + } + + private void checkNodeId(String id) { + if (usingIntegerNodeIds() && !isInteger(id)) { + notIntNodeIds.add(id); + // Exit as there is no point in checking if this node id exists + return; + } + if (getNodeById(graph, id) == null) { + notFoundNodeIds.add(id); + } + } + + private boolean isInteger(String str) { + try { + Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + private String listToString(List list) { + return list.stream().map(id -> "'" + id + "'").collect(Collectors.joining(", ")); + } +} \ No newline at end of file From 2ab8be5a73627a0ad29ddde78cc2c43cb7063339 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 13:40:16 +0100 Subject: [PATCH 08/12] Add long description --- modules/MultiNodeLineage/src/main/nbm/manifest.mf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/MultiNodeLineage/src/main/nbm/manifest.mf b/modules/MultiNodeLineage/src/main/nbm/manifest.mf index 0477c8bb7..3cae9de5b 100644 --- a/modules/MultiNodeLineage/src/main/nbm/manifest.mf +++ b/modules/MultiNodeLineage/src/main/nbm/manifest.mf @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module-Name: Multi Node Lineage OpenIDE-Module-Short-Description: Identifies the ancestors and descendants of a set of nodes. -OpenIDE-Module-Long-Description: Identifies the ancestors and descendants of a set of nodes. TODO add more details. +OpenIDE-Module-Long-Description: Identifies the ancestors and descendants of a set of nodes. Attributes are added to the nodes to record the results. These attributes can be used for further analysis (e.g filtering). OpenIDE-Module-Display-Category: Metric From 751cf55c31f788fe9ca03ec1864f9debdb8e3bbf Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 13:50:37 +0100 Subject: [PATCH 09/12] Fix bug where ancestors and descendants were the wrong way round --- .../timsummertonbrier/multinodelineage/MultiNodeLineage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index 579255f71..21e2a349d 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -60,14 +60,14 @@ private void calculateLineage(String originNodeId) { Set ancestorIds = BreadthFirstSearch.run( graph, originNodeId, - (DirectedGraph g, Node n) -> g.getSuccessors(n) + (DirectedGraph g, Node n) -> g.getPredecessors(n) ); ancestorIds.remove(originNodeId); Set descendantIds = BreadthFirstSearch.run( graph, originNodeId, - (DirectedGraph g, Node n) -> g.getPredecessors(n) + (DirectedGraph g, Node n) -> g.getSuccessors(n) ); descendantIds.remove(originNodeId); From 68cac4f61243070c4d1c4aead7c797737b259346 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 14:01:10 +0100 Subject: [PATCH 10/12] Validate the graph is directed --- .../multinodelineage/MultiNodeLineage.java | 1 - .../multinodelineage/MultiNodeLineageValidator.java | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index 21e2a349d..229b47fd7 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -23,7 +23,6 @@ public class MultiNodeLineage implements Statistics { @Override public void execute(GraphModel graphModel) { - // TODO: Test on non-directed graph init(graphModel); if (!validate()) { return; diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java index 06d7af6a1..4d924c013 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java @@ -27,6 +27,7 @@ public MultiNodeLineageValidator(GraphModel graphModel, DirectedGraph graph, Lis public List validate() { validateOriginNodeIdsIsNotEmpty(); + validateGraphIsDirected(); validateEachNodeId(); return errors; } @@ -37,6 +38,16 @@ private void validateOriginNodeIdsIsNotEmpty() { } } + private void validateGraphIsDirected() { + // In testing calculating lineage on undirected graphs does "work", but + // it produces confusing results where some nodes are ancestors and some + // are descendants. In reality on an undirected graph all connected + // nodes should be ancestors and descendants. + if (!graphModel.isDirected()) { + errors.add("This graph is not directed. Calculating lineage only makes sense on directed graphs."); + } + } + private void validateEachNodeId() { originNodeIds.forEach(id -> checkNodeId(id)); if (!notIntNodeIds.isEmpty()) { @@ -74,4 +85,5 @@ private boolean isInteger(String str) { private String listToString(List list) { return list.stream().map(id -> "'" + id + "'").collect(Collectors.joining(", ")); } + } \ No newline at end of file From ac8c624b29250f2d3e04f2fa297ef9199112f621 Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 14:10:22 +0100 Subject: [PATCH 11/12] Fix build warnings --- .../uk/co/timsummertonbrier/multinodelineage/GetNodeById.java | 2 +- .../co/timsummertonbrier/multinodelineage/MultiNodeLineage.java | 2 +- .../multinodelineage/MultiNodeLineagePanel.java | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java index ecba0ea56..5e0ed742d 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java @@ -6,7 +6,7 @@ public class GetNodeById { public static Node getNodeById(DirectedGraph graph, String id) { - Class nodeIdType = graph.getModel().getConfiguration().getNodeIdType(); + var nodeIdType = graph.getModel().getConfiguration().getNodeIdType(); if (nodeIdType == Integer.class) { return graph.getNode(Integer.valueOf(id)); diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java index 229b47fd7..f8fa7eac2 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -102,7 +102,7 @@ private void setupColumns() { recreateColumn(table, "DescendantOf", String.class, ""); } - private Column recreateColumn(Table table, String name, Class type, Object defaultValue) { + private Column recreateColumn(Table table, String name, Class type, Object defaultValue) { // Remove existing column to delete the results from previous runs if (table.hasColumn(name)) { table.removeColumn(name); diff --git a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java index 45d4e02b5..3cad6eaa2 100644 --- a/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java @@ -14,6 +14,8 @@ public class MultiNodeLineagePanel extends JPanel { + private static final long serialVersionUID = 1759842236L; + private final JTextField originNodeIdsTextField = new JTextField(); public MultiNodeLineagePanel() { From d0f02cc4a5a07a519cfb4996969ac48bbe7317fa Mon Sep 17 00:00:00 2001 From: Tim Summerton-Brier Date: Tue, 7 Oct 2025 18:23:19 +0100 Subject: [PATCH 12/12] Remove empty README --- modules/MultiNodeLineage/README.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 modules/MultiNodeLineage/README.md diff --git a/modules/MultiNodeLineage/README.md b/modules/MultiNodeLineage/README.md deleted file mode 100644 index 4f2175194..000000000 --- a/modules/MultiNodeLineage/README.md +++ /dev/null @@ -1,4 +0,0 @@ -## Multi Node Lineage - -This README supports Markdown, see [syntax](https://help.github.com/articles/markdown-basics/) -