Skip to content
61 changes: 61 additions & 0 deletions modules/MultiNodeLineage/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>gephi-plugin-parent</artifactId>
<groupId>org.gephi</groupId>
<version>0.10.0</version>
</parent>

<groupId>uk.co.timsummertonbrier</groupId>
<artifactId>multi-node-lineage</artifactId>
<version>1.0.0</version>
<packaging>nbm</packaging>

<name>Multi Node Lineage</name>

<dependencies>
<!-- Insert dependencies here -->
<dependency>
<groupId>org.gephi</groupId>
<artifactId>graph-api</artifactId>
</dependency>
<dependency>
<groupId>org.gephi</groupId>
<artifactId>statistics-api</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.netbeans.utilities</groupId>
<artifactId>nbm-maven-plugin</artifactId>
<configuration>
<licenseName>MIT</licenseName>
<author>Tim Summerton-Brier</author>
<authorEmail></authorEmail>
<authorUrl></authorUrl>
<sourceCodeUrl>https://github.com/sizlo/gephi-plugins</sourceCodeUrl>
<publicPackages>
<!-- Insert public packages -->
</publicPackages>
</configuration>
</plugin>
</plugins>
</build>

<!-- Snapshot Repositories (only needed if developing against a SNAPSHOT version) -->
<repositories>
<repository>
<id>oss-sonatype</id>
<name>oss-sonatype</name>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>


Original file line number Diff line number Diff line change
@@ -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<String> run(
DirectedGraph graph,
String originId,
NodeChildSupplier nodeChildSupplier
) {
Set<String> seenIds = new HashSet<>();
Queue<Node> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package uk.co.timsummertonbrier.multinodelineage;

import java.util.Set;

public class Lineage {
private final Set<String> ancestorIds;
private final Set<String> descendantIds;

public Lineage(Set<String> ancestorIds, Set<String> descendantIds) {
this.ancestorIds = ancestorIds;
this.descendantIds = descendantIds;
}

public Set<String> getAncestorIds() {
return ancestorIds;
}

public Set<String> getDescendantIds() {
return descendantIds;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> originNodeIds = new ArrayList<>();
private Map<String, Lineage> lineages;
private GraphModel graphModel;
private DirectedGraph graph;
private List<String> 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<String> 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<String> ancestorIds = BreadthFirstSearch.run(
graph,
originNodeId,
(DirectedGraph g, Node n) -> g.getPredecessors(n)
);
ancestorIds.remove(originNodeId);

Set<String> 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 <T> Column recreateColumn(Table table, String name, Class<T> 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("<p>Multi Node Lineage has run successfully.</p>\n");
report.append("<p>The results have been recorded as new attributes on each Node (see Data Laboratory).</p>\n");

lineages.forEach((originNodeId, lineage) ->
report.append(reportSectionForOneOrigin(originNodeId, lineage))
);

return report.toString();
}

private String getErrorReport() {
StringBuilder report = new StringBuilder();
report.append("<p>Error running Multi Node Lineage.</p>\n");
errors.forEach(error -> report.append("<p>").append(error).append("</p>\n"));
return report.toString();
}

private String reportSectionForOneOrigin(String originNodeId, Lineage lineage) {
return String.format(
"<p>Node %s has %d ancestors and %d descendants.</p>\n",
originNodeId,
lineage.getAncestorIds().size(),
lineage.getDescendantIds().size()
);
}

}
Original file line number Diff line number Diff line change
@@ -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<? extends Statistics> getStatisticsClass() {
return MultiNodeLineage.class;
}



}
Original file line number Diff line number Diff line change
@@ -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<String> 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("<html><body style='width: 300px'>" + text));
}

private void addGap() {
add(Box.createRigidArea(new Dimension(0, 20)));
}

}
Loading