package uk.ac.rhul.cs.cl1.ui.cytoscape;
import giny.model.Node;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.List;
import javax.swing.JOptionPane;
import uk.ac.rhul.cs.cl1.ClusterONE;
import uk.ac.rhul.cs.cl1.ClusterONEAlgorithmParameters;
import uk.ac.rhul.cs.cl1.CohesivenessFunction;
import uk.ac.rhul.cs.cl1.MutableNodeSet;
import uk.ac.rhul.cs.cl1.NodeSet;
import uk.ac.rhul.cs.cl1.QualityFunction;
import uk.ac.rhul.cs.cl1.ValuedNodeSet;
import uk.ac.rhul.cs.utils.Pair;
import cytoscape.CyNetwork;
import cytoscape.Cytoscape;
import cytoscape.data.CyAttributes;
import cytoscape.task.ui.JTaskConfig;
import cytoscape.task.util.TaskManager;
import cytoscape.view.CyMenus;
import cytoscape.view.CyNetworkView;
import cytoscape.view.CytoscapeDesktop;
public class CytoscapePlugin extends cytoscape.plugin.CytoscapePlugin implements PropertyChangeListener {
/**
* Attribute name used by ClusterONE to store status information for each node.
*
* A node can have one and only one of the following status values:
*
* <ul>
* <li>0 = the node is an outlier (it is not included in any cluster)</li>
* <li>1 = the node is included in only a single cluster</li>
* <li>2 = the node is an overlap (it is included in more than one cluster)</li>
* </ul>
*/
public static final String ATTRIBUTE_STATUS = "cl1.Status";
/**
* Attribute name used by ClusterONE to store affinities of vertices to a
* given cluster.
*/
public static final String ATTRIBUTE_AFFINITY = "cl1.Affinity";
/**
* Local cache for converted ClusterONE representations of Cytoscape networks
*/
private static CyNetworkCache networkCache = new CyNetworkCache();
/**
* Action class responding for popup menu invocations in network views
*/
private NodeContextMenuAction nodeContextMenuAction = new NodeContextMenuAction();
public CytoscapePlugin() {
/* Set up menus */
CyMenus cyMenus = Cytoscape.getDesktop().getCyMenus();
cyMenus.addAction(new ShowControlPanelAction());
cyMenus.addAction(GrowClusterAction.getGlobalInstance());
cyMenus.addAction(AffinityColouringAction.getGlobalInstance());
cyMenus.addAction(new HelpAction("introduction"));
cyMenus.addAction(new AboutAction());
/* Disable actions depending on the control panel */
GrowClusterAction.getGlobalInstance().setEnabled(false);
AffinityColouringAction.getGlobalInstance().setEnabled(false);
/* Set up the attributes that will be used by ClusterONE */
CyAttributes nodeAttributes = Cytoscape.getNodeAttributes();
nodeAttributes.setAttributeDescription(ATTRIBUTE_STATUS,
"This attribute is used by the ClusterONE plugin to indicate the status "+
"of a node after a ClusterONE run. The status codes are as follows:\n\n"+
"Outlier = the node is not part of any cluster (i.e. it is an outlier)\n"+
"Cluster = the node is part of exactly one cluster\n"+
"Overlap = the node is part of multiple clusters (i.e. it is an overlap)"
);
nodeAttributes.setAttributeDescription(ATTRIBUTE_AFFINITY,
"This attribute is used by the ClusterONE plugin to indicate the "+
"affinity of a node to a given cluster. The attribute values can be "+
"(re)calculated manually by right-clicking on a cluster in the "+
"ClusterONE result table and selecting the appropriate menu item."
);
/* Register ourselves as a listener for newly created networks and network view
* focus events */
Cytoscape.getDesktop().getSwingPropertyChangeSupport().addPropertyChangeListener(
CytoscapeDesktop.NETWORK_VIEW_CREATED, this
);
/* Register ourselves as a listener for network changes */
Cytoscape.getPropertyChangeSupport().addPropertyChangeListener(this);
}
/**
* Converts a {@link CyNetwork} to a {@link Graph} using the global {@link CyNetworkCache}
*
* @param network the network being converted
* @param weightAttr the attribute name used for the weights
* @return the converted graph or null if there was an error
*/
public static Graph convertCyNetworkToGraph(CyNetwork network, String weightAttr) {
Graph graph = null;
try {
graph = networkCache.convertCyNetworkToGraph(network, weightAttr);
} catch (NonNumericAttributeException ex) {
JOptionPane.showMessageDialog(Cytoscape.getDesktop(),
"Weight attribute values must be numeric.",
"Error - invalid weight attribute",
JOptionPane.ERROR_MESSAGE);
return null;
}
return graph;
}
/**
* Returns a reference to the network cache held by the plugin
*/
public static CyNetworkCache getNetworkCache() {
return networkCache;
}
/**
* Runs ClusterONE with the given parameters on the given Cytoscape network
*
* @param network the network we are running the algorithm on
* @param parameters the algorithm parameters of ClusterONE
* @param weightAttr edge attribute holding edge weights
* @param setAttributes whether to set ClusterONE related node/edge attributes on the
* network in the end
*/
protected static Pair<List<ValuedNodeSet>, List<Node>> runAlgorithm(CyNetwork network,
ClusterONEAlgorithmParameters parameters, String weightAttr,
boolean setAttributes) {
networkCache.invalidate(network);
Graph graph = convertCyNetworkToGraph(network, weightAttr);
if (graph == null)
return null;
List<ValuedNodeSet> clusters = runAlgorithm(graph, parameters, weightAttr);
if (clusters != null && setAttributes)
setStatusAttributesOnGraph(graph, clusters);
return Pair.create(clusters, graph.getNodeMapping());
}
/**
* Runs ClusterONE with the given parameters on the given graph
*
* @param graph the graph we are running the algorithm on
* @param parameters the algorithm parameters of ClusterONE
* @param weightAttr edge attribute holding edge weights
*/
protected static List<ValuedNodeSet> runAlgorithm(Graph graph,
ClusterONEAlgorithmParameters parameters, String weightAttr) {
if (graph.getEdgeCount() == 0) {
JOptionPane.showMessageDialog(Cytoscape.getDesktop(),
"The selected network contains no edges",
"Error - no edges in network",
JOptionPane.ERROR_MESSAGE);
return null;
}
JTaskConfig config = new JTaskConfig();
config.displayCancelButton(true);
config.displayStatus(true);
ClusterONECytoscapeTask task = new ClusterONECytoscapeTask(parameters);
task.setGraph(graph);
TaskManager.executeTask(task, config);
return task.getResults();
}
/**
* Sets some ClusterONE specific node status attributes on a CyNetwork that
* will be used by VizMapper later.
*
* @param graph the ClusterONE graph representation
* @param results results of the analysis
*/
private static void setStatusAttributesOnGraph(Graph graph, List<ValuedNodeSet> results) {
int[] occurrences = new int[graph.getNodeCount()];
Arrays.fill(occurrences, 0);
for (NodeSet nodeSet: results) {
for (Integer nodeIdx: nodeSet) {
occurrences[nodeIdx]++;
}
}
CyAttributes nodeAttributes = Cytoscape.getNodeAttributes();
String[] values = {"Outlier", "Cluster", "Overlap"};
byte attrType = nodeAttributes.getType(ATTRIBUTE_STATUS);
if (attrType != CyAttributes.TYPE_UNDEFINED && attrType != CyAttributes.TYPE_STRING) {
int response = JOptionPane.showConfirmDialog(Cytoscape.getDesktop(),
"A node attribute named "+ATTRIBUTE_STATUS+" already exists and "+
"it is not a string attribute.\nDo you want to remove the existing "+
"attribute and re-register it as a string attribute?",
"Attribute type mismatch",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (response == JOptionPane.NO_OPTION)
return;
nodeAttributes.deleteAttribute(ATTRIBUTE_STATUS);
}
int i = 0;
for (Node node: graph.getNodeMapping()) {
if (occurrences[i] > 2)
occurrences[i] = 2;
nodeAttributes.setAttribute(node.getIdentifier(), ATTRIBUTE_STATUS,
values[occurrences[i]]);
i++;
}
}
/**
* Sets the ClusterONE specific node affinity attributes on a CyNetwork that
* will be used by VizMapper later.
*
* @param graph the ClusterONE graph representation
* @param nodes the list of the selected node indices
*/
public static void setAffinityAttributesOnGraph(Graph graph, List<Integer> nodes) {
CyAttributes nodeAttributes = Cytoscape.getNodeAttributes();
byte attrType = nodeAttributes.getType(ATTRIBUTE_AFFINITY);
if (attrType != CyAttributes.TYPE_UNDEFINED && attrType != CyAttributes.TYPE_FLOATING) {
int response = JOptionPane.showConfirmDialog(Cytoscape.getDesktop(),
"A node attribute named "+ATTRIBUTE_AFFINITY+" already exists and "+
"it is not a floating point attribute.\nDo you want to remove the existing "+
"attribute and re-register it as a floating point attribute?",
"Attribute type mismatch",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (response == JOptionPane.NO_OPTION)
return;
nodeAttributes.deleteAttribute(ATTRIBUTE_AFFINITY);
}
int i = 0;
MutableNodeSet nodeSet = new MutableNodeSet(graph, nodes);
QualityFunction func = new CohesivenessFunction(); // TODO: fix it, it should not be hardwired
double currentQuality = func.calculate(nodeSet);
double affinity;
for (Node node: graph.getNodeMapping()) {
if (nodeSet.contains(i))
/* multiplying by -1 here: we want internal nodes to have a positive
* affinity if they "should" belong to the cluster
*/
affinity = - (func.getRemovalAffinity(nodeSet, i) - currentQuality);
else
affinity = func.getAdditionAffinity(nodeSet, i) - currentQuality;
if (Double.isNaN(affinity))
affinity = 0.0;
nodeAttributes.setAttribute(node.getIdentifier(), ATTRIBUTE_AFFINITY, affinity);
i++;
}
/* Set the appropriate ClusterONE visual style */
VisualStyleManager.ensureVizMapperStylesRegistered(false);
VisualStyleManager.updateAffinityStyleRange();
Cytoscape.getVisualMappingManager().setVisualStyle(VisualStyleManager.VISUAL_STYLE_BY_AFFINITY);
Cytoscape.getVisualMappingManager().applyAppearances();
Cytoscape.getCurrentNetworkView().redrawGraph(false, true);
}
/**
* Method triggered when a new network is created
*/
public void propertyChange(PropertyChangeEvent e) {
String property = e.getPropertyName();
if (property == null)
return;
if (CytoscapeDesktop.NETWORK_VIEW_CREATED.equals(property)) {
/* Register the appropriate node context menu for newly created networks */
CyNetworkView view = (CyNetworkView)e.getNewValue();
view.addNodeContextMenuListener(nodeContextMenuAction);
} else if (Cytoscape.NETWORK_MODIFIED.equals(property)) {
/* If a network was modified, remove it from the network cache */
networkCache.invalidate(Cytoscape.getCurrentNetwork());
} else if (Cytoscape.NETWORK_DESTROYED.equals(property)) {
/* If a network was destroyed, remove it from the network cache */
networkCache.invalidate(Cytoscape.getCurrentNetwork());
}
}
/**
* Shows a message dialog box that informs the user about a possible bug in ClusterONE.
*
* @param message the message to be shown
*/
public static void showBugMessage(String message) {
StringBuilder sb = new StringBuilder(message);
sb.append("\n\n");
sb.append("This is possibly a bug in ");
sb.append(ClusterONE.applicationName);
sb.append(".\nPlease inform the developers about what you were doing and\n");
sb.append("what the expected result would have been.");
JOptionPane.showMessageDialog(Cytoscape.getDesktop(),
sb.toString(), "Possible bug in "+ClusterONE.applicationName,
JOptionPane.ERROR_MESSAGE);
}
/**
* Shows an error message in a dialog box
*
* @param message the error message to be shown
*/
public static void showErrorMessage(String message) {
JOptionPane.showMessageDialog(Cytoscape.getDesktop(), message,
ClusterONE.applicationName, JOptionPane.ERROR_MESSAGE);
}
/**
* Shows a message dialog box that informs the user about something
*
* @param message the message to be shown
*/
public static void showInformationMessage(String message) {
JOptionPane.showMessageDialog(Cytoscape.getDesktop(), message,
ClusterONE.applicationName, JOptionPane.INFORMATION_MESSAGE);
}
}