package jalview.ext.archaeopteryx; import jalview.analysis.AlignmentSorter; import jalview.analysis.Conservation; import jalview.api.AlignViewportI; import jalview.commands.CommandI; import jalview.commands.OrderCommand; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; import jalview.datamodel.HiddenColumns; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.ext.treeviewer.LoadedTreeSequenceAssociation; import jalview.ext.treeviewer.TreeFrameI; import jalview.ext.treeviewer.TreeI; import jalview.ext.treeviewer.TreeNodeI; import jalview.ext.treeviewer.TreePanelI; import jalview.ext.treeviewer.TreeViewerBindingI; import jalview.ext.treeviewer.TreeViewerUtils; import jalview.gui.AlignViewport; import jalview.gui.AlignmentPanel; import jalview.gui.Desktop; import jalview.gui.JvOptionPane; import jalview.gui.PaintRefresher; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemeProperty; import jalview.schemes.UserColourScheme; import jalview.structure.SelectionSource; import jalview.structure.StructureSelectionManager; import jalview.util.MappingUtils; import jalview.util.MessageManager; import jalview.viewmodel.AlignmentViewport; import java.awt.Color; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.SwingUtilities; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; /** * Class for binding the Archaeopteryx tree viewer to the Jalview alignment that * it originates from, meaning that selecting sequences in the tree viewer also * selects them in the alignment view and vice versa. * * @author kjvanderheide * */ public final class JalviewBinding implements TreeViewerBindingI { private final TreeFrameI aptxFrame; private TreePanelI treeView; private AlignmentViewport parentAvport; private final StructureSelectionManager ssm; private Map sequencesBoundToNodes; private Map nodesBoundToSequences; private float rootX; private float furthestNodeX; private int nrTreeGroups = 0; private boolean applyToAllViews = false; /** * * @param archaeopteryx * * @param jalviewAlignmentViewport * alignment viewport from which the tree was calculated. * * @param alignMappedToNodes * map with sequences used to calculate the tree and matching tree * nodes as key, value pair respectively. * * @param nodesMappedToAlign * map with tree nodes and matching sequences used to calculate the * tree as key, value pair respectively. */ public JalviewBinding(final TreeFrameI archaeopteryx, final AlignmentViewport jalviewAlignmentViewport, final Map alignMappedToNodes, final Map nodesMappedToAlign) { if (archaeopteryx.getNumberOfTrees() > 1) { JvOptionPane.showMessageDialog(Desktop.desktop, MessageManager.getString("label.tabs_detected_archaeopteryx"), MessageManager.getString("label.problem_reading_tree_file"), JvOptionPane.WARNING_MESSAGE); } // deal with/prohibit null values here as that will cause problems aptxFrame = archaeopteryx; parentAvport = jalviewAlignmentViewport; sequencesBoundToNodes = alignMappedToNodes; nodesBoundToSequences = nodesMappedToAlign; treeView = archaeopteryx.getTreePanel(); ssm = parentAvport.getStructureSelectionManager(); aptxFrame.setViewBinding(this); ssm.addSelectionListener(this); treeView.addMouseListener(this); treeView.registerWithPaintRefresher( parentAvport.getSequenceSetId()); aptxFrame.addFrameListener(new InternalFrameAdapter() { @Override public void internalFrameClosed(InternalFrameEvent e) { TreeViewerUtils.getActiveTreeViews().remove(aptxFrame); ssm.removeSelectionListener(JalviewBinding.this); } }); // treeTabs.addChangeListener(new ChangeListener() // { // // @Override // public void stateChanged(ChangeEvent e) // { // // SwingUtilities.invokeLater(new Runnable() // { // // @Override // /** // * Resend the selection to the tree view when tabs get switched, this // * has to be buried in invokeLater as Forester first resets the tree // * view on switching tabs, without invokeLater this would get called // * before Forester resets which would nullify the selection. // */ // public void run() // { // treeView = archaeopteryx.getMainPanel().getCurrentTreePanel(); // parentAvport.sendSelection(); // // PaintRefresher.Refresh(treeView, // // parentAvport.getSequenceSetId()); // // } // }); // // } // // }); } @Override public void actionPerformed(ActionEvent e) { // reset hidden sequences first parentAvport.showAllHiddenSeqs(); if (treeView.showingSubTree()) { LoadedTreeSequenceAssociation bindAptxNodes = new LoadedTreeSequenceAssociation( parentAvport.getAlignment().getSequencesArray(), treeView.getTree()); bindAptxNodes.associateNodesToSequences(); sequencesBoundToNodes = bindAptxNodes.getAlignmentWithNodes(); nodesBoundToSequences = bindAptxNodes.getNodesWithAlignment(); TreeViewerUtils.associateNodesWithJalviewSequences(aptxFrame, parentAvport, sequencesBoundToNodes, nodesBoundToSequences); for (SequenceI seq : parentAvport.getAlignment().getSequencesArray()) { if (!sequencesBoundToNodes.containsKey(seq)) { parentAvport.hideSequence(new SequenceI[] { seq }); } } } else { Rectangle visibleView = treeView.getVisibleArea(); for (TreeNodeI node : treeView.getTree().getRoot() .getAllDescendants()) { if (!(node.getXcoord() > visibleView.getMinX() && node.getXcoord() < visibleView.getMaxX() && node.getYcoord() > visibleView.getMinY() && node.getYcoord() < visibleView.getMaxY())) { parentAvport .hideSequence(new SequenceI[] { nodesBoundToSequences.get(node) }); } } } } @Override public void mouseClicked(MouseEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override /** * invokeLater so that this always runs after Forester's mouseClicked */ public void run() { final TreeNodeI node = treeView.findNode(e.getX(), e.getY()); if (node != null) { if ((e.getModifiers() & InputEvent.SHIFT_MASK) == 0) // clear previous // selection if shift // IS NOT pressed { parentAvport.setSelectionGroup(null); } showNodeSelectionOnAlign(node); } else { partitionTree(e.getX()); } treeView.notifyPaintRefresher(parentAvport.getSequenceSetId(), false, false); treeView.repaint(); } }); } @Override public void mousePressed(final MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void selection(final SequenceGroup seqsel, final ColumnSelection colsel, final HiddenColumns hidden, final SelectionSource source) { if (source == parentAvport) // check if source is alignment from where the // tree originates { treeView.setMatchingNodes( new HashSet(seqsel.getSequences().size())); for (SequenceI selectedSequence : seqsel.getSequences()) { TreeNodeI matchingNode = sequencesBoundToNodes .get(selectedSequence); if (matchingNode != null) { treeView.addToMatchingNodes(matchingNode); // if (!matchingNode.getBranchData().isHasBranchColor()) // { // // Color foundNodesColour = treeView.getTreeColorSet() // // .getFoundColor0(); // // matchingNode.getBranchData() // // .setBranchColor(new BranchColor(foundNodesColour)); // // } } } treeView.repaint(); } } /** * Partially refactored from TreeCanvas */ @Override public void partitionTree(final int x) { TreeI tree = treeView.getTree(); if (!tree.isEmpty()) { // should be calculated on each partition as the tree can theoretically // change in the meantime TreeNodeI furthestNode = tree.getFurthestNode(); furthestNodeX = furthestNode.getXcoord(); rootX = tree.getRoot().getXcoord(); // don't bother if 0 distance tree or clicked x lies outside of tree // if (furthestNodeX != rootX && !(x > furthestNodeX)) float threshold = (x - rootX) / (furthestNodeX - rootX); List foundNodes = getNodesAboveThreshold( threshold, tree.getRoot()); } } public List getNodesAboveThreshold(float threshold, TreeNodeI node) { List nodesAboveThreshold = new ArrayList<>(); parentAvport.setSelectionGroup(null); parentAvport.getAlignment().deleteAllGroups(); parentAvport.clearSequenceColours(); if (parentAvport.getCodingComplement() != null) { parentAvport.getCodingComplement().setSelectionGroup(null); parentAvport.getCodingComplement().getAlignment().deleteAllGroups(); parentAvport.getCodingComplement().clearSequenceColours(); } colourNodesAboveThreshold(nodesAboveThreshold, threshold, node); return nodesAboveThreshold; } /** * Partially refactored from TreeCanvas colourGroups (can be made nicer). * * @param nodeList * @param threshold * @param treeLength * @param node * @return */ private List colourNodesAboveThreshold( List nodeList, float threshold, TreeNodeI node) { for (TreeNodeI childNode : node.getDirectChildren()) { childNode.setBranchColor(Color.black); float nodeCutoff = (childNode.getXcoord() - rootX) / (furthestNodeX - rootX); if (nodeCutoff > threshold) { nodeList.add(childNode); Color randomColour = new Color((int) (Math.random() * 255), (int) (Math.random() * 255), (int) (Math.random() * 255)); childNode.setBranchColor(randomColour); List groupSeqs = new ArrayList<>(); SequenceI seq = nodesBoundToSequences.get(childNode); if (seq != null) { groupSeqs.add(seq); parentAvport.setSequenceColour(seq, randomColour); } List descendantNodes = childNode .getAllDescendants(); // .forEach instead? for (TreeNodeI descNode : descendantNodes) { seq = nodesBoundToSequences.get(descNode); if (seq != null) { groupSeqs.add(seq); parentAvport.setSequenceColour(seq, randomColour); } descNode.setBranchColor(randomColour); } if (groupSeqs != null) { nrTreeGroups++; groupThresholdSequences(groupSeqs, randomColour); }} else { colourNodesAboveThreshold(nodeList, threshold, childNode); } } for (AlignmentPanel associatedPanel : getAssociatedPanels()) { associatedPanel.updateAnnotation(); final AlignViewportI codingComplement = associatedPanel.getAlignViewport() .getCodingComplement(); if (codingComplement != null) { // GROSS ((AlignViewport) codingComplement).getAlignPanel() .updateAnnotation(); } } return nodeList; } public void groupThresholdSequences(List groupedSeqs, Color groupColour) { SequenceGroup treeGroup = new SequenceGroup(groupedSeqs, null, null, true, true, false, 0, parentAvport.getAlignment().getWidth() - 1); ColourSchemeI cs = null; if (parentAvport.getGlobalColourScheme() != null) { if (parentAvport.getGlobalColourScheme() instanceof UserColourScheme) { cs = new UserColourScheme( ((UserColourScheme) parentAvport.getGlobalColourScheme()) .getColours()); } else { cs = ColourSchemeProperty.getColourScheme(treeGroup, ColourSchemeProperty.getColourName( parentAvport.getGlobalColourScheme())); } } treeGroup.setColourScheme(cs); treeGroup.getGroupColourScheme().setThreshold( parentAvport.getResidueShading().getThreshold(), parentAvport.isIgnoreGapsConsensus()); treeGroup.setName("Tree Group " + nrTreeGroups); treeGroup.setIdColour(groupColour); for (AlignmentPanel associatedPanel : getAssociatedPanels()) { AlignViewportI altViewport = associatedPanel .getAlignViewport(); if (altViewport.getGlobalColourScheme() != null && altViewport.getResidueShading() .conservationApplied()) { Conservation conserv = new Conservation(treeGroup.getName(), treeGroup.getSequences(null), treeGroup.getStartRes(), treeGroup.getEndRes()); conserv.calculate(); conserv.verdict(false, altViewport.getConsPercGaps()); treeGroup.getGroupColourScheme().setConservation(conserv); } altViewport.getAlignment().addGroup(treeGroup); // TODO can we push all of the below into AlignViewportI? final AlignViewportI codingComplement = altViewport .getCodingComplement(); if (codingComplement != null) { SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(treeGroup, parentAvport, codingComplement); if (mappedGroup.getSequences().size() > 0) { codingComplement.getAlignment().addGroup(mappedGroup); for (SequenceI seq : mappedGroup.getSequences()) { codingComplement.setSequenceColour(seq, groupColour.brighter()); } } } } } @Override public void showNodeSelectionOnAlign(final TreeNodeI node) { if (node.isInternal()) { showMatchingChildSequences(node); } else { showMatchingSequence(node); } } @Override public void showMatchingSequence(final TreeNodeI nodeToMatch) { SequenceI matchingSequence = nodesBoundToSequences.get(nodeToMatch); if (matchingSequence != null) { long nodeId = nodeToMatch.getId(); addOrRemoveInSet(treeView.getMatchingNodesIds(), nodeId); treeSelectionChanged(matchingSequence); parentAvport.sendSelection(); } } @Override public void showMatchingChildSequences(final TreeNodeI parentNode) { // redundancy here, Forester already iterates through tree to get all // descendants List childNodes = parentNode.getAllDescendants(); for (TreeNodeI childNode : childNodes) { SequenceI matchingSequence = nodesBoundToSequences.get(childNode); if (matchingSequence != null) { long nodeId = childNode.getId(); addOrRemoveInSet(treeView.getMatchingNodesIds(), nodeId); treeSelectionChanged(matchingSequence); } } parentAvport.sendSelection(); } @Override public void treeSelectionChanged(final SequenceI sequence) { if (!parentAvport.isClosed()) // alignment view could be closed { SequenceGroup selected = parentAvport.getSelectionGroup(); if (selected == null) { selected = new SequenceGroup(); parentAvport.setSelectionGroup(selected); } selected.setEndRes(parentAvport.getAlignment().getWidth() - 1); selected.addOrRemove(sequence, true); } } @Override public void sortByTree_actionPerformed() { // if (applyToAllViews) final ArrayList commands = new ArrayList<>(); for (AlignmentPanel ap : PaintRefresher .getAssociatedPanels(parentAvport.getSequenceSetId())) { commands.add(sortAlignmentIn(ap.av.getAlignPanel())); ap.alignFrame.addHistoryItem(new CommandI() { @Override public void undoCommand(AlignmentI[] views) { for (CommandI tsort : commands) { tsort.undoCommand(views); } } @Override public int getSize() { return commands.size(); } @Override public String getDescription() { return "Tree Sort (many views)"; } @Override public void doCommand(AlignmentI[] views) { for (CommandI tsort : commands) { tsort.doCommand(views); } } }); ap.alignFrame.updateEditMenuBar(); } } // else // { // alignPanel.alignFrame.addHistoryItem(sortAlignmentIn(alignPanel)); // } @Override public CommandI sortAlignmentIn(AlignmentPanel ap) { AlignmentViewport viewport = ap.av; SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray(); try { AlignmentSorter.sortByTree(viewport.getAlignment(), nodesBoundToSequences, treeView.getTree()); CommandI undo; undo = new OrderCommand("Tree Sort", oldOrder, viewport.getAlignment()); ap.paintAlignment(true, false); return undo; } catch (Exception e) { System.err.println(e.getMessage()); } return null; } /** * TO BE MOVED * * @param set * @param objectToCheck */ public static void addOrRemoveInSet(Set set, E objectToCheck) { if (set.contains(objectToCheck)) { set.remove(objectToCheck); } else { set.add(objectToCheck); } } public AlignmentViewport getParentAvport() { return parentAvport; } public void setParentAvport(final AlignmentViewport parentAvport) { this.parentAvport = parentAvport; } public AlignmentPanel[] getAssociatedPanels() { return PaintRefresher .getAssociatedPanels(parentAvport.getSequenceSetId()); } @Override public Map getAlignmentWithNodes() { return sequencesBoundToNodes; } @Override public Map getNodesWithAlignment() { return nodesBoundToSequences; } }