--- /dev/null
+package jalview.ext.treeviewer;
+
+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.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.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.SwingUtilities;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+/**
+ * Class for binding the 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<SequenceI, TreeNodeI> sequencesBoundToNodes;
+
+ private Map<TreeNodeI, SequenceI> 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<SequenceI, TreeNodeI> alignMappedToNodes,
+ final Map<TreeNodeI, SequenceI> 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<Long>(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<TreeNodeI> foundNodes = getNodesAboveThreshold(
+ threshold,
+ tree.getRoot());
+
+
+ }
+
+
+ }
+
+ public List<TreeNodeI> getNodesAboveThreshold(float threshold,
+ TreeNodeI node)
+ {
+
+ List<TreeNodeI> 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<TreeNodeI> colourNodesAboveThreshold(
+ List<TreeNodeI> 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<SequenceI> groupSeqs = new ArrayList<>();
+ SequenceI seq = nodesBoundToSequences.get(childNode);
+ if (seq != null)
+ {
+ groupSeqs.add(seq);
+ parentAvport.setSequenceColour(seq, randomColour);
+ }
+
+ List<TreeNodeI> 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<SequenceI> 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(parentAvport,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();
+ addOrRemoveInCollection(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<TreeNodeI> childNodes = parentNode.getAllDescendants();
+
+
+ for (TreeNodeI childNode : childNodes)
+ {
+ SequenceI matchingSequence = nodesBoundToSequences.get(childNode);
+ if (matchingSequence != null)
+ {
+ long nodeId = childNode.getId();
+ addOrRemoveInCollection(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<CommandI> 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 collection
+ * @param nodeId
+ */
+ public static <E> void addOrRemoveInCollection(Collection<Long> collection,
+ long nodeId)
+ {
+ if (collection.contains(nodeId))
+ {
+ collection.remove(nodeId);
+ }
+ else
+ {
+ collection.add(nodeId);
+ }
+
+ }
+
+ 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<SequenceI, TreeNodeI> getAlignmentWithNodes()
+ {
+ return sequencesBoundToNodes;
+ }
+
+ @Override
+ public Map<TreeNodeI, SequenceI> getNodesWithAlignment()
+ {
+ return nodesBoundToSequences;
+ }
+
+ @Override
+ public void hideCollapsedSequences_actionPerformed()
+ {
+ parentAvport.showAllHiddenSeqs();
+
+ for (TreeNodeI node : treeView.getTree().getAllNodes())
+ {
+ if (node.isCollapsed())
+ {
+ SequenceI seqToHide = nodesBoundToSequences.get(node);
+ if (seqToHide != null)
+ {
+ parentAvport.hideSequence(new SequenceI[] { seqToHide });
+
+
+ }
+
+ }
+ }
+
+
+ }
+
+}
+
+
+