From 9612728299dc4c2242a6a968875a194624983920 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 7 Apr 2015 09:18:26 +0100 Subject: [PATCH] JAL-1691 linked scrollling in split frame (Desktop) --- src/jalview/datamodel/SearchResults.java | 5 + src/jalview/gui/AlignViewport.java | 75 +++++++++++++ src/jalview/gui/AlignmentPanel.java | 169 ++++++++++++++++++++++++++---- src/jalview/util/MappingUtils.java | 24 +++++ test/jalview/util/MappingUtilsTest.java | 74 +++++++++++++ 5 files changed, 327 insertions(+), 20 deletions(-) diff --git a/src/jalview/datamodel/SearchResults.java b/src/jalview/datamodel/SearchResults.java index e62e58a..2a33f6c 100755 --- a/src/jalview/datamodel/SearchResults.java +++ b/src/jalview/datamodel/SearchResults.java @@ -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; + } } /** diff --git a/src/jalview/gui/AlignViewport.java b/src/jalview/gui/AlignViewport.java index 44b4167..d84d3a3 100644 --- a/src/jalview/gui/AlignViewport.java +++ b/src/jalview/gui/AlignViewport.java @@ -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 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 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); + } } diff --git a/src/jalview/gui/AlignmentPanel.java b/src/jalview/gui/AlignmentPanel.java index beafa8c..851c58b 100644 --- a/src/jalview/gui/AlignmentPanel.java +++ b/src/jalview/gui/AlignmentPanel.java @@ -20,21 +20,6 @@ */ 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 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; + } } diff --git a/src/jalview/util/MappingUtils.java b/src/jalview/util/MappingUtils.java index ece1bac..c6cbdc5 100644 --- a/src/jalview/util/MappingUtils.java +++ b/src/jalview/util/MappingUtils.java @@ -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 findMappingsForSequence( + SequenceI sequence, Set mappings) + { + List result = new ArrayList(); + if (sequence == null || mappings == null) + { + return result; + } + for (AlignedCodonFrame mapping : mappings) { + if (mapping.involvesSequence(sequence)) { + result.add(mapping); + } + } + return result; + } } diff --git a/test/jalview/util/MappingUtilsTest.java b/test/jalview/util/MappingUtilsTest.java index 41efa73..4e144fc 100644 --- a/test/jalview/util/MappingUtilsTest.java +++ b/test/jalview/util/MappingUtilsTest.java @@ -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 mappings = new HashSet(); + mappings.add(acf1); + mappings.add(acf2); + mappings.add(acf3); + + /* + * Seq1 has three mappings + */ + List 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()); +} } -- 1.7.10.2