JAL-1691 linked scrollling in split frame (Desktop)
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 7 Apr 2015 08:18:26 +0000 (09:18 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 7 Apr 2015 08:18:26 +0000 (09:18 +0100)
src/jalview/datamodel/SearchResults.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AlignmentPanel.java
src/jalview/util/MappingUtils.java
test/jalview/util/MappingUtilsTest.java

index e62e58a..2a33f6c 100755 (executable)
@@ -94,6 +94,11 @@ public class SearchResults
       final int to = Math.min(end, chars.length + 1);
       return String.valueOf(Arrays.copyOfRange(chars, from, to));
     }
+
+    public void setSequence(SequenceI seq)
+    {
+      this.sequence = seq;
+    }
   }
 
   /**
index 44b4167..d84d3a3 100644 (file)
@@ -44,6 +44,7 @@ import java.awt.Font;
 import java.awt.Rectangle;
 import java.util.ArrayList;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Set;
 import java.util.Vector;
 
@@ -61,7 +62,9 @@ import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SearchResults;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -71,6 +74,8 @@ import jalview.structure.CommandListener;
 import jalview.structure.SelectionSource;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
+import jalview.util.Comparison;
+import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.ws.params.AutoCalcSetting;
@@ -1130,4 +1135,74 @@ public class AlignViewport extends AlignmentViewport implements
   {
     this.gatherViewsHere = gatherViewsHere;
   }
+
+  /**
+   * If this viewport has a (Protein/cDNA) complement, then scroll the
+   * complementary alignment to match this one.
+   * 
+   * @param horizontal
+   *          true for horizontal scroll event, false for vertical
+   */
+  public void scrollComplementaryAlignment(boolean horizontal)
+  {
+    /*
+     * If no complement, or it is not following scrolling, do nothing.
+     */
+    // TODO pull up followHighlight to AlignmentViewport/AlignViewportI
+    final AlignViewport codingComplement = (AlignViewport) getCodingComplement();
+    if (codingComplement == null || !codingComplement.followHighlight)
+    {
+      return;
+    }
+    boolean iAmProtein = !getAlignment().isNucleotide();
+    AlignmentI proteinAlignment = iAmProtein ? getAlignment()
+            : codingComplement.getAlignment();
+    if (proteinAlignment == null)
+    {
+      return;
+    }
+    final Set<AlignedCodonFrame> mappings = proteinAlignment
+            .getCodonFrames();
+
+    /*
+     * Heuristic: find the first mapped sequence (if any) with a non-gapped
+     * residue in the middle column of the visible region. Scroll the
+     * complementary alignment to line up the corresponding residue.
+     */
+    int seqOffset = 0;
+    SequenceI sequence = null;
+    int middleColumn = getStartRes() + (getEndRes() - getStartRes()) / 2;
+    final HiddenSequences hiddenSequences = getAlignment()
+            .getHiddenSequences();
+    for (int seqNo = getStartSeq(); seqNo < getEndSeq(); seqNo++, seqOffset++)
+    {
+      sequence = getAlignment().getSequenceAt(seqNo);
+      if (hiddenSequences != null && hiddenSequences.isHidden(sequence))
+      {
+        continue;
+      }
+      if (Comparison.isGap(sequence.getCharAt(middleColumn)))
+      {
+        continue;
+      }
+      List<AlignedCodonFrame> seqMappings = MappingUtils
+              .findMappingsForSequence(sequence, mappings);
+      if (!seqMappings.isEmpty())
+      {
+        break;
+      }
+    }
+
+    if (sequence == null)
+    {
+      /*
+       * No ungapped mapped sequence in middle column - do nothing
+       */
+      return;
+    }
+    SearchResults sr = MappingUtils.buildSearchResults(sequence,
+            sequence.findPosition(middleColumn), mappings);
+    codingComplement.getAlignPanel().scrollAsComplement(sr, seqOffset,
+            horizontal);
+  }
 }
index beafa8c..851c58b 100644 (file)
  */
 package jalview.gui;
 
-import jalview.analysis.AnnotationSorter;
-import jalview.api.AlignViewportI;
-import jalview.api.AlignmentViewPanel;
-import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SearchResults;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceGroup;
-import jalview.datamodel.SequenceI;
-import jalview.jbgui.GAlignmentPanel;
-import jalview.math.AlignmentDimension;
-import jalview.schemes.ResidueProperties;
-import jalview.structure.StructureSelectionManager;
-import jalview.util.MessageManager;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Container;
@@ -52,9 +37,25 @@ import java.beans.PropertyChangeListener;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.PrintWriter;
+import java.util.List;
 
 import javax.swing.SwingUtilities;
 
+import jalview.analysis.AnnotationSorter;
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.jbgui.GAlignmentPanel;
+import jalview.math.AlignmentDimension;
+import jalview.schemes.ResidueProperties;
+import jalview.structure.StructureSelectionManager;
+import jalview.util.MessageManager;
+
 /**
  * DOCUMENT ME!
  * 
@@ -91,6 +92,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
   int vextent = 0;
 
+  /*
+   * Flag set while scrolling to follow complementary cDNA/protein scroll. When
+   * true, suppresses invoking the same method recursively.
+   */
+  private boolean followingComplementScroll;
+
   /**
    * Creates a new AlignmentPanel object.
    * 
@@ -290,29 +297,44 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   /**
-   * scroll the view to show the position of the highlighted region in results
+   * Scroll the view to show the position of the highlighted region in results
    * (if any) and redraw the overview
    * 
    * @param results
    */
   public boolean scrollToPosition(SearchResults results)
   {
-    return scrollToPosition(results, true);
+    return scrollToPosition(results, true, false);
   }
 
   /**
-   * scroll the view to show the position of the highlighted region in results
+   * Scroll the view to show the position of the highlighted region in results
+   * (if any)
+   * 
+   * @param searchResults
+   * @param redrawOverview
+   * @return
+   */
+  public boolean scrollToPosition(SearchResults searchResults, boolean redrawOverview)
+  {
+    return scrollToPosition(searchResults, redrawOverview, false);
+  }
+
+  /**
+   * Scroll the view to show the position of the highlighted region in results
    * (if any)
    * 
    * @param results
    * @param redrawOverview
    *          - when set, the overview will be recalculated (takes longer)
+   * @param centre
+   *          if true, try to centre the search results horizontally in the view
    * @return false if results were not found
    */
   public boolean scrollToPosition(SearchResults results,
-          boolean redrawOverview)
+          boolean redrawOverview, boolean centre)
   {
-    int startv, endv, starts, ends, width;
+    int startv, endv, starts, ends;
     // TODO: properly locate search results in view when large numbers of hidden
     // columns exist before highlighted region
     // do we need to scroll the panel?
@@ -336,6 +358,17 @@ public class AlignmentPanel extends GAlignmentPanel implements
       int end = r[1];
       // System.err.println("Seq : "+seqIndex+" Scroll to "+start+","+end); //
       // DEBUG
+
+      /*
+       * To centre results, scroll to positions half the visible width
+       * left/right of the start/end positions
+       */
+      if (centre)
+      {
+        int offset = (av.getEndRes() - av.getStartRes() + 1) / 2 - 1;
+        start = Math.max(start - offset, 0);
+        end = Math.min(end + offset, seq.getEnd() - 1);
+      }
       if (start < 0)
       {
         return false;
@@ -361,20 +394,35 @@ public class AlignmentPanel extends GAlignmentPanel implements
       {
         if ((startv = av.getStartRes()) >= start)
         {
+          /*
+           * Scroll left to make start of search results visible
+           */
           setScrollValues(start - 1, seqIndex);
         }
         else if ((endv = av.getEndRes()) <= end)
         {
+          /*
+           * Scroll right to make end of search results visible
+           */
           setScrollValues(startv + 1 + end - endv, seqIndex);
         }
         else if ((starts = av.getStartSeq()) > seqIndex)
         {
+          /*
+           * Scroll up to make start of search results visible
+           */
           setScrollValues(av.getStartRes(), seqIndex);
         }
         else if ((ends = av.getEndSeq()) <= seqIndex)
         {
+          /*
+           * Scroll down to make end of search results visible
+           */
           setScrollValues(av.getStartRes(), starts + seqIndex - ends + 1);
         }
+        /*
+         * Else results are already visible - no need to scroll
+         */
       }
       else
       {
@@ -756,6 +804,18 @@ public class AlignmentPanel extends GAlignmentPanel implements
         }
       }
     }
+    /*
+     * If there is one, scroll the (Protein/cDNA) complementary alignment to
+     * match, unless we are ourselves doing that.
+     */
+    if (isFollowingComplementScroll())
+    {
+      setFollowingComplementScroll(false);
+    }
+    else
+    {
+      av.scrollComplementaryAlignment(evt.getSource() == hscroll);
+    }
   }
 
   /**
@@ -1651,4 +1711,73 @@ public class AlignmentPanel extends GAlignmentPanel implements
   {
     this.idPanel = idPanel;
   }
+
+  /**
+   * Follow a scrolling change in the (cDNA/Protein) complementary alignment.
+   * The aim is to keep the two alignments 'lined up' on their centre columns.
+   * 
+   * @param sr
+   *          holds mapped region(s) of this alignment that we are scrolling
+   *          'to'; may be modified for sequence offset by this method
+   * @param seqOffset
+   *          the number of visible sequences to show above the mapped region
+   * @param horizontal
+   *          if true, horizontal scrolling, else vertical
+   */
+  public void scrollAsComplement(SearchResults sr, int seqOffset,
+          boolean horizontal)
+  {
+    /*
+     * To avoid jumpy vertical scrolling (if some sequences are gapped or not
+     * mapped), we can make the scroll-to location a sequence above the one
+     * actually mapped.
+     */
+    SequenceI mappedTo = sr.getResultSequence(0);
+    List<SequenceI> seqs = av.getAlignment().getSequences();
+
+    /*
+     * This is like AlignmentI.findIndex(seq) but here we are matching the
+     * dataset sequence not the aligned sequence
+     */
+    int sequenceIndex = 0;
+    boolean matched = false;
+    for (SequenceI seq : seqs)
+    {
+      if (mappedTo == seq.getDatasetSequence())
+      {
+        matched = true;
+        break;
+      }
+      sequenceIndex++;
+    }
+    if (!matched)
+    {
+      return; // failsafe, shouldn't happen
+    }
+    sequenceIndex = Math.max(0, sequenceIndex - seqOffset);
+    sr.getResults().get(0)
+            .setSequence(av.getAlignment().getSequenceAt(sequenceIndex));
+
+    /*
+     * Scroll to position but centring the target residue. Also set a state flag
+     * to prevent adjustmentValueChanged performing this recursively.
+     */
+    setFollowingComplementScroll(true);
+    scrollToPosition(sr, true, true);
+  }
+
+  /**
+   * Set a flag to say we are scrolling to follow a (cDNA/protein) complement.
+   * 
+   * @param b
+   */
+  protected void setFollowingComplementScroll(boolean b)
+  {
+    this.followingComplementScroll = b;
+  }
+
+  protected boolean isFollowingComplementScroll()
+  {
+    return this.followingComplementScroll;
+  }
 }
index ece1bac..c6cbdc5 100644 (file)
@@ -608,4 +608,28 @@ public final class MappingUtils
     }
     return result;
   }
+
+  /**
+   * Returns a list of any mappings that are from or to the given (aligned or
+   * dataset) sequence.
+   * 
+   * @param sequence
+   * @param mappings
+   * @return
+   */
+  public static List<AlignedCodonFrame> findMappingsForSequence(
+          SequenceI sequence, Set<AlignedCodonFrame> mappings)
+  {
+    List<AlignedCodonFrame> result = new ArrayList<AlignedCodonFrame>();
+    if (sequence == null || mappings == null)
+    {
+      return result;
+    }
+    for (AlignedCodonFrame mapping : mappings) {
+      if (mapping.involvesSequence(sequence)) {
+        result.add(mapping);
+      }
+    }
+    return result;
+  }
 }
index 41efa73..4e144fc 100644 (file)
@@ -8,6 +8,8 @@ import java.awt.Color;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import org.junit.Test;
@@ -21,6 +23,7 @@ import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResults.Match;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
 import jalview.gui.AlignViewport;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.FormatAdapter;
@@ -587,4 +590,75 @@ public class MappingUtilsTest
     assertEquals(0, mappedGroup.getStartRes());
     assertEquals(4, mappedGroup.getEndRes());
   }
+
+  @Test
+  public void testFindMappingsForSequence()
+  {
+    SequenceI seq1 = new Sequence("Seq1", "ABC");
+    SequenceI seq2 = new Sequence("Seq2", "ABC");
+    SequenceI seq3 = new Sequence("Seq3", "ABC");
+    SequenceI seq4 = new Sequence("Seq4", "ABC");
+    seq1.createDatasetSequence();
+    seq2.createDatasetSequence();
+    seq3.createDatasetSequence();
+    seq4.createDatasetSequence();
+
+    /*
+     * Create mappings from seq1 to seq2, seq2 to seq1, seq3 to seq1
+     */
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    MapList map = new MapList(new int[]
+    { 1, 3 }, new int[]
+    { 1, 3 },1, 1);
+    acf1.addMap(seq1.getDatasetSequence(), seq2.getDatasetSequence(), map);
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+    acf2.addMap(seq2.getDatasetSequence(), seq1.getDatasetSequence(), map);
+    AlignedCodonFrame acf3 = new AlignedCodonFrame();
+    acf3.addMap(seq3.getDatasetSequence(), seq1.getDatasetSequence(), map);
+    
+    Set<AlignedCodonFrame> mappings = new HashSet<AlignedCodonFrame>();
+    mappings.add(acf1);
+    mappings.add(acf2);
+    mappings.add(acf3);
+    
+    /*
+     * Seq1 has three mappings
+     */
+    List<AlignedCodonFrame> result = MappingUtils.findMappingsForSequence(
+            seq1, mappings);
+    assertEquals(3, result.size());
+    assertTrue(result.contains(acf1));
+    assertTrue(result.contains(acf2));
+    assertTrue(result.contains(acf3));
+
+    /*
+     * Seq2 has two mappings
+     */
+    result = MappingUtils.findMappingsForSequence(seq2, mappings);
+    assertEquals(2, result.size());
+    assertTrue(result.contains(acf1));
+    assertTrue(result.contains(acf2));
+
+    /*
+     * Seq3 has one mapping
+     */
+    result = MappingUtils.findMappingsForSequence(seq3, mappings);
+    assertEquals(1, result.size());
+    assertTrue(result.contains(acf3));
+
+    /*
+     * Seq4 has no mappings
+     */
+    result = MappingUtils.findMappingsForSequence(seq4, mappings);
+    assertEquals(0, result.size());
+
+    result = MappingUtils.findMappingsForSequence(null, mappings);
+    assertEquals(0, result.size());
+
+    result = MappingUtils.findMappingsForSequence(seq1, null);
+    assertEquals(0, result.size());
+
+    result = MappingUtils.findMappingsForSequence(null, null);
+    assertEquals(0, result.size());
+}
 }