JAL-1953 2.11.2 with Archeopteryx!
[jalview.git] / src / jalview / ext / treeviewer / JalviewBinding.java
diff --git a/src/jalview/ext/treeviewer/JalviewBinding.java b/src/jalview/ext/treeviewer/JalviewBinding.java
new file mode 100644 (file)
index 0000000..fd1735e
--- /dev/null
@@ -0,0 +1,763 @@
+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 });
+
+
+        }
+
+      }
+    }
+
+
+  }
+
+}
+
+
+