diff --git a/modules/MultiNodeLineage/pom.xml b/modules/MultiNodeLineage/pom.xml new file mode 100644 index 000000000..ded902d7d --- /dev/null +++ b/modules/MultiNodeLineage/pom.xml @@ -0,0 +1,61 @@ + + + 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.gephi + graph-api + + + org.gephi + statistics-api + + + + + + + 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/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..b2d104481 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/BreadthFirstSearch.java @@ -0,0 +1,38 @@ +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; +import static uk.co.timsummertonbrier.multinodelineage.GetNodeById.getNodeById; + +public class BreadthFirstSearch { + + public static Set run( + DirectedGraph graph, + String originId, + NodeChildSupplier nodeChildSupplier + ) { + Set seenIds = new HashSet<>(); + Queue queue = new LinkedList<>(); + + Node originNode = getNodeById(graph, 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/GetNodeById.java b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/GetNodeById.java new file mode 100644 index 000000000..5e0ed742d --- /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) { + var 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/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 new file mode 100644 index 000000000..f8fa7eac2 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineage.java @@ -0,0 +1,152 @@ +package uk.co.timsummertonbrier.multinodelineage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +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; +import static uk.co.timsummertonbrier.multinodelineage.GetNodeById.getNodeById; + +public class MultiNodeLineage implements Statistics { + + private List originNodeIds = new ArrayList<>(); + private Map lineages; + private GraphModel graphModel; + private DirectedGraph graph; + private List errors; + + @Override + public void execute(GraphModel graphModel) { + init(graphModel); + if (!validate()) { + return; + } + originNodeIds.forEach(originNodeId -> calculateLineage(originNodeId)); + recordResultsAsAttributes(); + } + + @Override + public String getReport() { + if (errors.isEmpty()) { + return getSuccessfulReport(); + } + return getErrorReport(); + } + + public void setOriginNodeIds(List originNodeIds) { + this.originNodeIds = originNodeIds; + } + + private void init(GraphModel graphModel) { + this.graphModel = graphModel; + graph = graphModel.getDirectedGraphVisible(); + lineages = new HashMap<>(); + errors = new ArrayList<>(); + } + + private boolean validate() { + errors.addAll(new MultiNodeLineageValidator(graphModel, graph, originNodeIds).validate()); + return errors.isEmpty(); + } + + private void calculateLineage(String originNodeId) { + Set ancestorIds = BreadthFirstSearch.run( + graph, + originNodeId, + (DirectedGraph g, Node n) -> g.getPredecessors(n) + ); + ancestorIds.remove(originNodeId); + + Set descendantIds = BreadthFirstSearch.run( + graph, + originNodeId, + (DirectedGraph g, Node n) -> g.getSuccessors(n) + ); + descendantIds.remove(originNodeId); + + lineages.put(originNodeId, new Lineage(ancestorIds, descendantIds)); + } + + private void recordResultsAsAttributes() { + setupColumns(); + + lineages.forEach((originNodeId, lineage) -> { + getNodeById(graph, originNodeId).setAttribute("IsOrigin", true); + + lineage.getAncestorIds().forEach(ancestorId -> { + Node ancestorNode = getNodeById(graph, ancestorId); + ancestorNode.setAttribute("IsAncestor", true); + addNodeIdToList(ancestorNode, "AncestorOf", originNodeId); + }); + + lineage.getDescendantIds().forEach(descendantId -> { + Node descendantNode = getNodeById(graph, 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); + } + } + + 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(); + } + + 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/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..3cad6eaa2 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineagePanel.java @@ -0,0 +1,53 @@ + +package uk.co.timsummertonbrier.multinodelineage; + +import java.awt.Dimension; +import java.util.Arrays; +import java.util.Collections; +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 static final long serialVersionUID = 1759842236L; + + private final JTextField originNodeIdsTextField = new JTextField(); + + public MultiNodeLineagePanel() { + initComponents(); + } + + public List getOriginNodeIds() { + String allOriginNodeIds = originNodeIdsTextField.getText(); + if (allOriginNodeIds.isEmpty()) { + return Collections.emptyList(); + } + return Arrays.asList(allOriginNodeIds.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; + } + +} 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..4d924c013 --- /dev/null +++ b/modules/MultiNodeLineage/src/main/java/uk/co/timsummertonbrier/multinodelineage/MultiNodeLineageValidator.java @@ -0,0 +1,89 @@ +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(); + validateGraphIsDirected(); + validateEachNodeId(); + return errors; + } + + private void validateOriginNodeIdsIsNotEmpty() { + if (originNodeIds.isEmpty()) { + errors.add("No origin node ID(s) were supplied"); + } + } + + 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()) { + 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 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 diff --git a/modules/MultiNodeLineage/src/main/nbm/manifest.mf b/modules/MultiNodeLineage/src/main/nbm/manifest.mf new file mode 100644 index 000000000..3cae9de5b --- /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. 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 diff --git a/pom.xml b/pom.xml index afb8f2bf5..62e5f0159 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ + modules/MultiNodeLineage @@ -39,6 +40,28 @@ true + + + gephi + https://raw.github.com/gephi/gephi/mvn-thirdparty-repo/ + + true + + + true + + + + + jzy3d + https://maven.jzy3d.org/releases/ + + true + + + true + +