From 4924c107e70d763821067bcb3e1586bc14589918 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Tue, 3 Mar 2015 10:35:19 +0000 Subject: [PATCH 1/1] JAL-845 implement alignment of protein to match cDNA alignment --- src/jalview/analysis/AlignmentUtils.java | 130 ++++++++++- src/jalview/api/SplitContainerI.java | 21 +- src/jalview/datamodel/AlignedCodon.java | 21 +- src/jalview/datamodel/AlignedCodonFrame.java | 26 +++ src/jalview/datamodel/Alignment.java | 5 +- .../datamodel/IncompleteCodonException.java | 13 ++ src/jalview/datamodel/Mapping.java | 239 ++++++++++++++++++++ src/jalview/datamodel/SearchResults.java | 19 +- src/jalview/gui/AlignFrame.java | 2 +- src/jalview/gui/Jalview2XML.java | 14 +- src/jalview/gui/SplitFrame.java | 38 +++- src/jalview/io/VamsasAppDatastore.java | 10 +- src/jalview/io/vamsas/Rangetype.java | 16 +- src/jalview/jbgui/GSplitFrame.java | 7 +- src/jalview/util/MapList.java | 48 +++- src/jalview/util/MappingUtils.java | 4 +- src/jalview/ws/AWSThread.java | 12 +- src/jalview/ws/jws2/MsaWSThread.java | 45 ++-- test/jalview/analysis/AlignmentUtilsTests.java | 89 ++++++-- .../datamodel/AlignedCodonIteratorTest.java | 135 +++++++++++ test/jalview/datamodel/SearchResultsTest.java | 6 +- test/jalview/util/MapListTest.java | 44 +++- 22 files changed, 845 insertions(+), 99 deletions(-) create mode 100644 src/jalview/datamodel/IncompleteCodonException.java create mode 100644 test/jalview/datamodel/AlignedCodonIteratorTest.java diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index 0ae782e..74406dc 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -20,9 +20,11 @@ */ package jalview.analysis; +import jalview.datamodel.AlignedCodon; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; +import jalview.datamodel.Mapping; import jalview.datamodel.SearchResults; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; @@ -30,10 +32,15 @@ import jalview.schemes.ResidueProperties; import jalview.util.MapList; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; /** * grab bag of useful alignment manipulation operations Expect these to be @@ -207,10 +214,11 @@ public class AlignmentUtils /** * Build mapping of protein to cDNA alignment. Mappings are made between - * sequences which have the same name and compatible lengths. Has a 3-valued - * result: either Mapped (at least one sequence mapping was created), - * AlreadyMapped (all possible sequence mappings already exist), or NotMapped - * (no possible sequence mappings exist). + * sequences which have the same name and compatible lengths. Any new mappings + * are added to the protein alignment. Has a 3-valued result: either Mapped + * (at least one sequence mapping was created), AlreadyMapped (all possible + * sequence mappings already exist), or NotMapped (no possible sequence + * mappings exist). * * @param proteinAlignment * @param cdnaAlignment @@ -780,4 +788,118 @@ public class AlignmentUtils alignedSeq.setSequence(newseq.toString()); return alignedSeq; } + + /** + * Realigns the given protein to match the alignment of the dna, using codon + * mappings to translate aligned codon positions to protein residues. + * + * @param protein + * the alignment whose sequences are realigned by this method + * @param dna + * the dna alignment whose alignment we are 'copying' + * @return the number of sequences that were realigned + */ + public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna) + { + Set mappings = protein.getCodonFrames(); + + /* + * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of + * {dnaSequence, {proteinSequence, codonProduct}} at that position. The + * comparator keeps the codon positions ordered. + */ + Map> alignedCodons = new TreeMap>( + new CodonComparator()); + for (SequenceI dnaSeq : dna.getSequences()) + { + for (AlignedCodonFrame mapping : mappings) + { + Mapping seqMap = mapping.getMappingForSequence(dnaSeq); + SequenceI prot = mapping.findAlignedSequence( + dnaSeq.getDatasetSequence(), protein); + if (prot != null) + { + addCodonPositions(dnaSeq, prot, protein.getGapCharacter(), + seqMap, alignedCodons); + } + } + } + return alignProteinAs(protein, alignedCodons); + } + + /** + * Update the aligned protein sequences to match the codon alignments given in + * the map. + * + * @param protein + * @param alignedCodons + * an ordered map of codon positions (columns), with sequence/peptide + * values present in each column + * @return + */ + protected static int alignProteinAs(AlignmentI protein, + Map> alignedCodons) + { + /* + * Prefill aligned sequences with gaps before inserting aligned protein + * residues. + */ + int alignedWidth = alignedCodons.size(); + char[] gaps = new char[alignedWidth]; + Arrays.fill(gaps, protein.getGapCharacter()); + String allGaps = String.valueOf(gaps); + for (SequenceI seq : protein.getSequences()) + { + seq.setSequence(allGaps); + } + + int column = 0; + for (AlignedCodon codon : alignedCodons.keySet()) + { + final Map columnResidues = alignedCodons.get(codon); + for (Entry entry : columnResidues + .entrySet()) + { + // place translated codon at its column position in sequence + entry.getKey().getSequence()[column] = entry.getValue().charAt(0); + } + column++; + } + return 0; + } + + /** + * Populate the map of aligned codons by traversing the given sequence + * mapping, locating the aligned positions of mapped codons, and adding those + * positions and their translation products to the map. + * + * @param dna + * the aligned sequence we are mapping from + * @param protein + * the sequence to be aligned to the codons + * @param gapChar + * the gap character in the dna sequence + * @param seqMap + * a mapping to a sequence translation + * @param alignedCodons + * the map we are building up + */ + static void addCodonPositions(SequenceI dna, SequenceI protein, + char gapChar, + Mapping seqMap, + Map> alignedCodons) + { + Iterator codons = seqMap.getCodonIterator(dna, gapChar); + while (codons.hasNext()) + { + AlignedCodon codon = codons.next(); + Map seqProduct = alignedCodons.get(codon); + if (seqProduct == null) + { + seqProduct = new HashMap(); + alignedCodons.put(codon, seqProduct); + } + seqProduct.put(protein, codon.product); + } + } } diff --git a/src/jalview/api/SplitContainerI.java b/src/jalview/api/SplitContainerI.java index 22f5201..2abf4d8 100644 --- a/src/jalview/api/SplitContainerI.java +++ b/src/jalview/api/SplitContainerI.java @@ -1,5 +1,9 @@ package jalview.api; +import jalview.datamodel.AlignmentI; + +import java.awt.Component; + /** * Describes a visual container that can show two alignments. * @@ -16,6 +20,21 @@ public interface SplitContainerI * @param show */ // TODO need an interface for AlignFrame? - void setComplementVisible(Object alignFrame, boolean show); + void setComplementVisible(Component alignFrame, boolean show); + + /** + * Returns the alignment that is complementary to the one in the given + * AlignFrame, or null. + */ + AlignmentI getComplement(Component af); + + /** + * Returns the frame title for the alignment that is complementary to the one + * in the given AlignFrame, or null. + * + * @param af + * @return + */ + String getComplementTitle(Component af); } diff --git a/src/jalview/datamodel/AlignedCodon.java b/src/jalview/datamodel/AlignedCodon.java index d0e62a1..0daa3fb 100644 --- a/src/jalview/datamodel/AlignedCodon.java +++ b/src/jalview/datamodel/AlignedCodon.java @@ -2,7 +2,8 @@ package jalview.datamodel; /** * Holds the aligned column positions (base 0) for one codon in a nucleotide - * sequence. The object is immutable once created. + * sequence, and (optionally) its peptide translation. The object is immutable + * once created. * * Example: in "G-AT-C-GA" the aligned codons are (0, 2, 3) and (5, 7, 8). * @@ -17,11 +18,19 @@ public final class AlignedCodon public final int pos3; + public final String product; + public AlignedCodon(int i, int j, int k) { + this(i, j, k, null); + } + + public AlignedCodon(int i, int j, int k, String prod) + { pos1 = i; pos2 = j; pos3 = k; + product = prod; } /** @@ -42,7 +51,10 @@ public final class AlignedCodon } /** - * Two aligned codons are equal if all their base positions are the same. + * Two aligned codons are equal if all their base positions are the same. We + * don't care about the protein product. This test is required for correct + * alignment of translated gapped dna alignments (the same codon positions in + * different sequences occupy the same column in the translated alignment). */ @Override public boolean equals(Object o) @@ -66,6 +78,9 @@ public final class AlignedCodon @Override public String toString() { - return "[" + pos1 + ", " + pos2 + ", " + pos3 + "]"; + StringBuilder sb = new StringBuilder(); + sb.append("[").append(pos1).append(", ").append(pos2).append(", ") + .append(pos3).append("]"); + return sb.toString(); } } diff --git a/src/jalview/datamodel/AlignedCodonFrame.java b/src/jalview/datamodel/AlignedCodonFrame.java index 9c17306..cbddf1c 100644 --- a/src/jalview/datamodel/AlignedCodonFrame.java +++ b/src/jalview/datamodel/AlignedCodonFrame.java @@ -123,6 +123,32 @@ public class AlignedCodonFrame } /** + * Returns the first mapping found which is to or from the given sequence, or + * null. + * + * @param seq + * @return + */ + public Mapping getMappingForSequence(SequenceI seq) + { + if (dnaSeqs == null) + { + return null; + } + SequenceI seqDs = seq.getDatasetSequence(); + seqDs = seqDs != null ? seqDs : seq; + + for (int ds = 0; ds < dnaSeqs.length; ds++) + { + if (dnaSeqs[ds] == seqDs || dnaToProt[ds].to == seqDs) + { + return dnaToProt[ds]; + } + } + return null; + } + + /** * Return the corresponding aligned or dataset aa sequence for given dna * sequence, null if not found. * diff --git a/src/jalview/datamodel/Alignment.java b/src/jalview/datamodel/Alignment.java index 56545e1..4558d8d 100755 --- a/src/jalview/datamodel/Alignment.java +++ b/src/jalview/datamodel/Alignment.java @@ -1688,10 +1688,7 @@ public class Alignment implements AlignmentI boolean thatIsProtein = !al.isNucleotide(); if (!thatIsProtein && !thisIsNucleotide) { - System.err - .println("Alignment of protein from cDNA not yet implemented"); - return 0; - // todo: build it - a variant of Dna.CdnaTranslate() + return AlignmentUtils.alignProteinAsDna(this, al); } char thisGapChar = this.getGapCharacter(); diff --git a/src/jalview/datamodel/IncompleteCodonException.java b/src/jalview/datamodel/IncompleteCodonException.java new file mode 100644 index 0000000..f716a53 --- /dev/null +++ b/src/jalview/datamodel/IncompleteCodonException.java @@ -0,0 +1,13 @@ +package jalview.datamodel; + +/** + * An exception to indicate that less than 3 nucleotide bases are available when + * trying to form a codon. + * + * @author gmcarstairs + * + */ +public class IncompleteCodonException extends RuntimeException +{ + +} diff --git a/src/jalview/datamodel/Mapping.java b/src/jalview/datamodel/Mapping.java index f2c16d0..be8fd58 100644 --- a/src/jalview/datamodel/Mapping.java +++ b/src/jalview/datamodel/Mapping.java @@ -22,11 +22,245 @@ package jalview.datamodel; import jalview.util.MapList; +import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.Vector; public class Mapping { /** + * An iterator that serves the aligned codon positions (with their protein + * products). + * + * @author gmcarstairs + * + */ + public class AlignedCodonIterator implements Iterator + { + /* + * The gap character used in the aligned sequence + */ + private final char gap; + + /* + * The characters of the aligned sequence e.g. "-cGT-ACgTG-" + */ + private final char[] alignedSeq; + + /* + * Next position (base 0) in the aligned sequence + */ + private int alignedColumn = 0; + + /* + * Count of bases up to and including alignedColumn position + */ + private int alignedBases = 0; + + /* + * [start, end] from ranges (base 1) + */ + private Iterator fromRanges; + + /* + * [start, end] to ranges (base 1) + */ + private Iterator toRanges; + + /* + * The current [start, end] (base 1) from range + */ + private int[] currentFromRange = null; + + /* + * The current [start, end] (base 1) to range + */ + private int[] currentToRange = null; + + /* + * The next 'from' position (base 1) to process + */ + private int fromPosition = 0; + + /* + * The next 'to' position (base 1) to process + */ + private int toPosition = 0; + + /** + * Constructor + * + * @param cs + * the aligned sequence characters + * @param gapChar + */ + public AlignedCodonIterator(char[] cs, char gapChar) + { + this.alignedSeq = cs; + this.gap = gapChar; + fromRanges = map.getFromRanges().iterator(); + toRanges = map.getToRanges().iterator(); + if (fromRanges.hasNext()) + { + currentFromRange = fromRanges.next(); + fromPosition = currentFromRange[0]; + } + if (toRanges.hasNext()) + { + currentToRange = toRanges.next(); + toPosition = currentToRange[0]; + } + } + + /** + * Returns true unless we have already traversed the whole mapping. + */ + @Override + public boolean hasNext() + { + if (fromRanges.hasNext()) + { + return true; + } + if (currentFromRange == null || fromPosition >= currentFromRange[1]) + { + return false; + } + return true; + } + + /** + * Returns the next codon's aligned positions, and translated value. + * + * @throws NoSuchElementException + * if hasNext() would have returned false + * @throws IncompleteCodonException + * if not enough mapped bases are left to make up a codon + */ + @Override + public AlignedCodon next() throws IncompleteCodonException + { + if (!hasNext()) + { + throw new NoSuchElementException(); + } + + int[] codon = getNextCodon(); + int[] alignedCodon = getAlignedCodon(codon); + + String peptide = getPeptide(); + return new AlignedCodon(alignedCodon[0], alignedCodon[1], + alignedCodon[2], peptide); + } + + /** + * Retrieve the translation as the 'mapped to' position in the mapped to + * sequence. + * + * @return + */ + private String getPeptide() + { + // TODO should ideally handle toRatio other than 1 as well... + // i.e. code like getNextCodon() + if (toPosition <= currentToRange[1]) { + char pep = Mapping.this.to.getSequence()[toPosition - 1]; + toPosition++; + return String.valueOf(pep); + } + if (!toRanges.hasNext()) + { + throw new NoSuchElementException("Ran out of peptide at position " + + toPosition); + } + currentToRange = toRanges.next(); + toPosition = currentToRange[0]; + return getPeptide(); + } + + /** + * Get the (base 1) dataset positions for the next codon in the mapping. + * + * @throws IncompleteCodonException + * if less than 3 remaining bases are mapped + */ + private int[] getNextCodon() + { + int[] codon = new int[3]; + int codonbase = 0; + + while (codonbase < 3) + { + if (fromPosition <= currentFromRange[1]) + { + /* + * Add next position from the current start-end range + */ + codon[codonbase++] = fromPosition++; + } + else + { + /* + * Move to the next range - if there is one + */ + if (!fromRanges.hasNext()) + { + throw new IncompleteCodonException(); + } + currentFromRange = fromRanges.next(); + fromPosition = currentFromRange[0]; + } + } + return codon; + } + + /** + * Get the aligned column positions (base 0) for the given sequence + * positions (base 1), by counting ungapped characters in the aligned + * sequence. + * + * @param codon + * @return + */ + private int[] getAlignedCodon(int[] codon) + { + int[] aligned = new int[codon.length]; + for (int i = 0; i < codon.length; i++) + { + aligned[i] = getAlignedColumn(codon[i]); + } + return aligned; + } + + /** + * Get the aligned column position (base 0) for the given sequence position + * (base 1). + * + * @param sequencePos + * @return + */ + private int getAlignedColumn(int sequencePos) + { + while (alignedBases < sequencePos + && alignedColumn < alignedSeq.length) + { + if (alignedSeq[alignedColumn++] != gap) + { + alignedBases++; + } + } + return alignedColumn - 1; + } + + @Override + public void remove() + { + // ignore + } + + } + + /** * Contains the start-end pairs mapping from the associated sequence to the * sequence in the database coordinate system. It also takes care of step * difference between coordinate systems. @@ -477,4 +711,9 @@ public class Mapping super.finalize(); } + public Iterator getCodonIterator(SequenceI seq, char gapChar) + { + return new AlignedCodonIterator(seq.getSequence(), gapChar); + } + } diff --git a/src/jalview/datamodel/SearchResults.java b/src/jalview/datamodel/SearchResults.java index a53d103..fb420b9 100755 --- a/src/jalview/datamodel/SearchResults.java +++ b/src/jalview/datamodel/SearchResults.java @@ -40,16 +40,26 @@ public class SearchResults { SequenceI sequence; - /* + /** * Start position of match in sequence (base 1) */ int start; - /* + /** * End position (inclusive) (base 1) */ int end; + /** + * Constructor + * + * @param seq + * a sequence + * @param start + * start position of matched range (base 1) + * @param end + * end of matched range (inclusive, base 1) + */ public Match(SequenceI seq, int start, int end) { sequence = seq; @@ -79,7 +89,10 @@ public class SearchResults public String toString() { char[] chars = sequence.getSequence(); - return String.valueOf(Arrays.copyOfRange(chars, start - 1, end)); + // convert start/end to base 0 (with bounds check) + final int from = Math.max(start - 1, 0); + final int to = Math.min(end, chars.length + 1); + return String.valueOf(Arrays.copyOfRange(chars, from, to)); } } diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 2b92142..9604458 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -4907,7 +4907,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, final SequenceI[] seqs = viewport.getSelectionGroup() == null ? viewport .getAlignment().getSequencesArray() : viewport .getSelectionAsNewSequence(); - viewport.openSplitFrame(af, seqs); + viewport.openSplitFrame(af, seqs, al.getCodonFrames()); // Desktop.addInternalFrame(af, newTitle, DEFAULT_WIDTH, DEFAULT_HEIGHT); } } diff --git a/src/jalview/gui/Jalview2XML.java b/src/jalview/gui/Jalview2XML.java index 8342151..f01e775 100644 --- a/src/jalview/gui/Jalview2XML.java +++ b/src/jalview/gui/Jalview2XML.java @@ -1787,20 +1787,20 @@ public class Jalview2XML mp = new Mapping(); jalview.util.MapList mlst = jmp.getMap(); - int r[] = mlst.getFromRanges(); - for (int s = 0; s < r.length; s += 2) + List r = mlst.getFromRanges(); + for (int[] range : r) { MapListFrom mfrom = new MapListFrom(); - mfrom.setStart(r[s]); - mfrom.setEnd(r[s + 1]); + mfrom.setStart(range[0]); + mfrom.setEnd(range[1]); mp.addMapListFrom(mfrom); } r = mlst.getToRanges(); - for (int s = 0; s < r.length; s += 2) + for (int[] range : r) { MapListTo mto = new MapListTo(); - mto.setStart(r[s]); - mto.setEnd(r[s + 1]); + mto.setStart(range[0]); + mto.setEnd(range[1]); mp.addMapListTo(mto); } mp.setMapFromUnit(mlst.getFromRatio()); diff --git a/src/jalview/gui/SplitFrame.java b/src/jalview/gui/SplitFrame.java index 81a6ddc..3382f30 100644 --- a/src/jalview/gui/SplitFrame.java +++ b/src/jalview/gui/SplitFrame.java @@ -1,5 +1,6 @@ package jalview.gui; +import jalview.api.SplitContainerI; import jalview.api.ViewStyleI; import jalview.datamodel.AlignmentI; import jalview.jbgui.GAlignFrame; @@ -37,7 +38,7 @@ import javax.swing.event.InternalFrameEvent; * @author gmcarstairs * */ -public class SplitFrame extends GSplitFrame +public class SplitFrame extends GSplitFrame implements SplitContainerI { private static final long serialVersionUID = 1L; @@ -540,4 +541,39 @@ public class SplitFrame extends GSplitFrame { Desktop.instance.gatherViews(this); } + + /** + * Returns the alignment in the complementary frame to the one given. + */ + @Override + public AlignmentI getComplement(Component alignFrame) + { + if (alignFrame == this.getTopFrame()) + { + return ((AlignFrame) getBottomFrame()).viewport.getAlignment(); + } + else if (alignFrame == this.getBottomFrame()) + { + return ((AlignFrame) getTopFrame()).viewport.getAlignment(); + } + return null; + } + + /** + * Returns the title of the complementary frame to the one given. + */ + @Override + public String getComplementTitle(Component alignFrame) + { + if (alignFrame == this.getTopFrame()) + { + return ((AlignFrame) getBottomFrame()).getTitle(); + } + else if (alignFrame == this.getBottomFrame()) + { + return ((AlignFrame) getTopFrame()).getTitle(); + } + return null; + } } + diff --git a/src/jalview/io/VamsasAppDatastore.java b/src/jalview/io/VamsasAppDatastore.java index 1d36865..d00f0b1 100644 --- a/src/jalview/io/VamsasAppDatastore.java +++ b/src/jalview/io/VamsasAppDatastore.java @@ -2570,15 +2570,15 @@ public class VamsasAppDatastore * initialise a range type object from a set of start/end inclusive intervals * * @param mrt - * @param range + * @param ranges */ - private void initRangeType(RangeType mrt, int[] range) + private void initRangeType(RangeType mrt, List ranges) { - for (int i = 0; i < range.length; i += 2) + for (int[] range : ranges) { Seg vSeg = new Seg(); - vSeg.setStart(range[i]); - vSeg.setEnd(range[i + 1]); + vSeg.setStart(range[0]); + vSeg.setEnd(range[1]); mrt.addSeg(vSeg); } } diff --git a/src/jalview/io/vamsas/Rangetype.java b/src/jalview/io/vamsas/Rangetype.java index 3cc977f..49227fd 100644 --- a/src/jalview/io/vamsas/Rangetype.java +++ b/src/jalview/io/vamsas/Rangetype.java @@ -20,6 +20,10 @@ */ package jalview.io.vamsas; +import jalview.io.VamsasAppDatastore; +import jalview.util.MessageManager; + +import java.util.List; import java.util.Vector; import uk.ac.vamsas.client.Vobject; @@ -28,8 +32,6 @@ import uk.ac.vamsas.objects.core.MapType; import uk.ac.vamsas.objects.core.Mapped; import uk.ac.vamsas.objects.core.RangeType; import uk.ac.vamsas.objects.core.Seg; -import jalview.io.VamsasAppDatastore; -import jalview.util.MessageManager; /** * Enhances DatastoreItem objects with additional functions to do with RangeType @@ -221,15 +223,15 @@ public abstract class Rangetype extends DatastoreItem * initialise a range type object from a set of start/end inclusive intervals * * @param mrt - * @param range + * @param ranges */ - protected void initRangeType(RangeType mrt, int[] range) + protected void initRangeType(RangeType mrt, List ranges) { - for (int i = 0; i < range.length; i += 2) + for (int[] range : ranges) { Seg vSeg = new Seg(); - vSeg.setStart(range[i]); - vSeg.setEnd(range[i + 1]); + vSeg.setStart(range[0]); + vSeg.setEnd(range[1]); vSeg.setInclusive(true); mrt.addSeg(vSeg); } diff --git a/src/jalview/jbgui/GSplitFrame.java b/src/jalview/jbgui/GSplitFrame.java index 062fe9f..27b3324 100644 --- a/src/jalview/jbgui/GSplitFrame.java +++ b/src/jalview/jbgui/GSplitFrame.java @@ -1,6 +1,5 @@ package jalview.jbgui; -import jalview.api.SplitContainerI; import jalview.util.Platform; import java.awt.Component; @@ -12,7 +11,7 @@ import javax.swing.JInternalFrame; import javax.swing.JSplitPane; import javax.swing.plaf.basic.BasicInternalFrameUI; -public class GSplitFrame extends JInternalFrame implements SplitContainerI +public class GSplitFrame extends JInternalFrame { private static final long serialVersionUID = 1L; @@ -120,8 +119,7 @@ public class GSplitFrame extends JInternalFrame implements SplitContainerI * Make the complement of the specified split component visible or hidden, * adjusting the position of the split divide. */ - @Override - public void setComplementVisible(Object alignFrame, boolean show) + public void setComplementVisible(Component alignFrame, boolean show) { if (alignFrame == this.topFrame) { @@ -138,5 +136,4 @@ public class GSplitFrame extends JInternalFrame implements SplitContainerI } validate(); } - } diff --git a/src/jalview/util/MapList.java b/src/jalview/util/MapList.java index 32f0256..48aebbf 100644 --- a/src/jalview/util/MapList.java +++ b/src/jalview/util/MapList.java @@ -101,23 +101,23 @@ public class MapList } /** - * Returns the flattened 'from' ranges as [start1, end1, start2, end2, ...] + * Returns the 'from' ranges as {[start1, end1], [start2, end2], ...} * * @return */ - public int[] getFromRanges() + public List getFromRanges() { - return getRanges(fromShifts); + return fromShifts; } /** - * Returns the flattened 'to' ranges as [start1, end1, start2, end2, ...] + * Returns the 'to' ranges as {[start1, end1], [start2, end2], ...} * * @return */ - public int[] getToRanges() + public List getToRanges() { - return getRanges(toShifts); + return toShifts; } /** @@ -191,6 +191,8 @@ public class MapList */ public MapList(int from[], int to[], int fromRatio, int toRatio) { + this.fromRatio = fromRatio; + this.toRatio = toRatio; fromLowest = from[0]; fromHighest = from[1]; for (int i = 0; i < from.length; i += 2) @@ -211,8 +213,6 @@ public class MapList toShifts.add(new int[] { to[i], to[i + 1] }); } - this.fromRatio = fromRatio; - this.toRatio = toRatio; } /** @@ -249,6 +249,38 @@ public class MapList } /** + * Constructor given ranges as lists of [start, end] positions + * + * @param fromRange + * @param toRange + * @param fromRatio + * @param toRatio + */ + public MapList(List fromRange, List toRange, + int fromRatio, int toRatio) + { + this.fromShifts = fromRange; + this.toShifts = toRange; + this.fromRatio = fromRatio; + this.toRatio = toRatio; + + fromLowest = Integer.MAX_VALUE; + fromHighest = 0; + for (int[] range : fromRange) { + fromLowest = Math.min(fromLowest, range[0]); + fromHighest = Math.max(fromHighest, range[1]); + } + + toLowest = Integer.MAX_VALUE; + toHighest = 0; + for (int[] range : toRange) + { + toLowest = Math.min(toLowest, range[0]); + toHighest = Math.max(toHighest, range[1]); + } + } + + /** * get all mapped positions from 'from' to 'to' * * @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int diff --git a/src/jalview/util/MappingUtils.java b/src/jalview/util/MappingUtils.java index 1f2e8db..6ddaa99 100644 --- a/src/jalview/util/MappingUtils.java +++ b/src/jalview/util/MappingUtils.java @@ -284,9 +284,11 @@ public final class MappingUtils .getCodonFrames(); /* - * Copy group name, colours, but not sequences + * Copy group name, name colours, but not sequences or sequence colour + * scheme */ SequenceGroup mappedGroup = new SequenceGroup(sg); + sg.cs = mapTo.getGlobalColourScheme(); mappedGroup.clear(); // TODO set width of mapped group diff --git a/src/jalview/ws/AWSThread.java b/src/jalview/ws/AWSThread.java index 1c9931d..3359546 100644 --- a/src/jalview/ws/AWSThread.java +++ b/src/jalview/ws/AWSThread.java @@ -90,7 +90,10 @@ public abstract class AWSThread extends Thread */ protected String WsUrl = null; - private boolean fromSplitFrame; + /* + * The AlignFrame from which the service was requested. + */ + private AlignFrame alignFrame; /** * generic web service job/subjob poll loop @@ -365,8 +368,7 @@ public abstract class AWSThread extends Thread AlignmentView alview, String wsurl2) { super(); - // this.alignFrame = alframe; - this.fromSplitFrame = alframe.getSplitViewContainer() != null; + this.alignFrame = alframe; currentView = alframe.getCurrentView().getAlignment(); featureSettings = alframe.getFeatureRenderer().getSettings(); defGapChar = alframe.getViewport().getGapCharacter(); @@ -385,8 +387,8 @@ public abstract class AWSThread extends Thread } } - protected boolean isFromSplitFrame() + protected AlignFrame getRequestingAlignFrame() { - return this.fromSplitFrame; + return this.alignFrame; } } diff --git a/src/jalview/ws/jws2/MsaWSThread.java b/src/jalview/ws/jws2/MsaWSThread.java index 60694e3..0178130 100644 --- a/src/jalview/ws/jws2/MsaWSThread.java +++ b/src/jalview/ws/jws2/MsaWSThread.java @@ -451,7 +451,7 @@ class MsaWSThread extends AWS2Thread implements WSClientI * @param presorder * boolean */ - MsaWSThread(MsaWS server, String wsUrl, WebserviceInfo wsinfo, + private MsaWSThread(MsaWS server, String wsUrl, WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, AlignmentView alview, String wsname, boolean subgaps, boolean presorder) { @@ -991,24 +991,35 @@ class MsaWSThread extends AWS2Thread implements WSClientI * If alignment was requested from one half of a SplitFrame, show in a * SplitFrame with the other pane similarly aligned. */ - if (this.isFromSplitFrame()) + AlignFrame requestedBy = getRequestingAlignFrame(); + if (requestedBy != null && requestedBy.getSplitViewContainer() != null) { - // TODO will only work for protein, as it holds the codon frame mappings - // may need this thread to hold a reference to the requesting AlignFrame - AlignmentI complement = al.getAlignedComplement(al.getCodonFrames()); - AlignFrame af2 = new AlignFrame(complement, AlignFrame.DEFAULT_WIDTH, - AlignFrame.DEFAULT_HEIGHT); - String linkedTitle = MessageManager - .getString("label.linked_view_title"); - JInternalFrame splitFrame = new SplitFrame(al.isNucleotide() ? af - : af2, al.isNucleotide() ? af2 : af); - Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1); - } - else - { - Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH, - AlignFrame.DEFAULT_HEIGHT); + AlignmentI complement = requestedBy.getSplitViewContainer() + .getComplement(requestedBy); + String complementTitle = requestedBy.getSplitViewContainer() + .getComplementTitle(requestedBy); + AlignmentI copyComplement = new Alignment(complement); + copyComplement.alignAs(al); + if (copyComplement.getHeight() > 0) + { + af.setTitle(alTitle); + AlignFrame af2 = new AlignFrame(copyComplement, + AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); + af2.setTitle(complementTitle); + String linkedTitle = MessageManager + .getString("label.linked_view_title"); + JInternalFrame splitFrame = new SplitFrame(al.isNucleotide() ? af + : af2, al.isNucleotide() ? af2 : af); + Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1); + return; + } } + + /* + * Not from SplitFrame, or failed to created a complementary alignment + */ + Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); } /** diff --git a/test/jalview/analysis/AlignmentUtilsTests.java b/test/jalview/analysis/AlignmentUtilsTests.java index c29658b..2711a36 100644 --- a/test/jalview/analysis/AlignmentUtilsTests.java +++ b/test/jalview/analysis/AlignmentUtilsTests.java @@ -36,6 +36,7 @@ import jalview.util.MapList; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -186,9 +187,11 @@ public class AlignmentUtilsTests assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 1, 9 }, mapList.getFromRanges())); + { 1, 9 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); /* * Inspect mappings for Mouse protein @@ -208,9 +211,11 @@ public class AlignmentUtilsTests assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 1, 9 }, mapList.getFromRanges())); + { 1, 9 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); } } @@ -259,20 +264,26 @@ public class AlignmentUtilsTests Mapping[] protMappings = humanMapping.getProtMappings(); // two mappings, both to cDNA with stop codon assertEquals(2, protMappings.length); + MapList mapList = protMappings[0].getMap(); assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 1, 9 }, mapList.getFromRanges())); + { 1, 9 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); + mapList = protMappings[1].getMap(); assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 1, 9 }, mapList.getFromRanges())); + { 1, 9 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); /* * Inspect mapping for Mouse protein - should map to 1st/3rd/5th cDNA seqs @@ -296,27 +307,33 @@ public class AlignmentUtilsTests assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 4, 12 }, mapList.getFromRanges())); + { 4, 12 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); // second mapping to cDNA with stop codon mapList = protMappings[1].getMap(); assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 1, 9 }, mapList.getFromRanges())); + { 1, 9 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); // third mapping to cDNA with start and stop codon mapList = protMappings[2].getMap(); assertEquals(3, mapList.getFromRatio()); assertEquals(1, mapList.getToRatio()); assertTrue(Arrays.equals(new int[] - { 4, 12 }, mapList.getFromRanges())); + { 4, 12 }, mapList.getFromRanges().get(0))); + assertEquals(1, mapList.getFromRanges().size()); assertTrue(Arrays.equals(new int[] - { 1, 3 }, mapList.getToRanges())); + { 1, 3 }, mapList.getToRanges().get(0))); + assertEquals(1, mapList.getToRanges().size()); } /** @@ -507,4 +524,48 @@ public class AlignmentUtilsTests assertEquals("---TGCCAT---TAC------CAG---", aligned.getSequenceAsString()); assertSame(aligned.getDatasetSequence(), dna.getDatasetSequence()); } + + /** + * Test the method that realigns protein to match mapped codon alignment. + */ + @Test + public void testAlignProteinAsDna() + { + // seq1 codons are [1,2,3] [4,5,6] [7,8,9] [10,11,12] + SequenceI dna1 = new Sequence("Seq1", "TGCCATTACCAG-"); + // seq2 codons are [1,3,4] [5,6,7] [8,9,10] [11,12,13] + SequenceI dna2 = new Sequence("Seq2", "T-GCCATTACCAG"); + // seq3 codons are [1,2,3] [4,5,7] [8,9,10] [11,12,13] + SequenceI dna3 = new Sequence("Seq3", "TGCCA-TTACCAG"); + AlignmentI dna = new Alignment(new SequenceI[] + { dna1, dna2, dna3 }); + dna.setDataset(null); + + // protein alignment will be realigned like dna + SequenceI prot1 = new Sequence("Seq1", "CHYQ"); + SequenceI prot2 = new Sequence("Seq2", "CHYQ"); + SequenceI prot3 = new Sequence("Seq3", "CHYQ"); + AlignmentI protein = new Alignment(new SequenceI[] + { prot1, prot2, prot3 }); + protein.setDataset(null); + + MapList map = new MapList(new int[] + { 1, 12 }, new int[] + { 1, 4 }, 3, 1); + AlignedCodonFrame acf = new AlignedCodonFrame(); + acf.addMap(dna1.getDatasetSequence(), prot1.getDatasetSequence(), map); + acf.addMap(dna2.getDatasetSequence(), prot2.getDatasetSequence(), map); + acf.addMap(dna3.getDatasetSequence(), prot3.getDatasetSequence(), map); + protein.setCodonFrames(Collections.singleton(acf)); + + /* + * Translated codon order is [1,2,3] [1,3,4] [4,5,6] [4,5,7] [5,6,7] [7,8,9] + * [8,9,10] [10,11,12] [11,12,13] + */ + AlignmentUtils.alignProteinAsDna(protein, dna); + assertEquals("C-H--Y-Q-", prot1.getSequenceAsString()); + assertEquals("-C--H-Y-Q", prot2.getSequenceAsString()); + assertEquals("C--H--Y-Q", prot3.getSequenceAsString()); + } + } diff --git a/test/jalview/datamodel/AlignedCodonIteratorTest.java b/test/jalview/datamodel/AlignedCodonIteratorTest.java new file mode 100644 index 0000000..671c51d --- /dev/null +++ b/test/jalview/datamodel/AlignedCodonIteratorTest.java @@ -0,0 +1,135 @@ +package jalview.datamodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import jalview.util.MapList; + +import java.util.Iterator; + +import org.junit.Test; + +/** + * Unit tests for Mapping$AlignedCodonIterator + * + * @author gmcarstairs + * + */ +public class AlignedCodonIteratorTest +{ + /** + * Test normal case for iterating over aligned codons. + */ + @Test + public void testNext() + { + SequenceI from = new Sequence("Seq1", "-CgC-C-cCtAG-AtG-Gc"); + from.createDatasetSequence(); + SequenceI to = new Sequence("Seq1", "-PQ-R-"); + to.createDatasetSequence(); + MapList map = new MapList(new int[] + { 1, 1, 3, 4, 6, 6, 8, 10, 12, 13 }, new int[] + { 1, 3 }, 3, 1); + Mapping m = new Mapping(to.getDatasetSequence(), map); + + Iterator codons = m.getCodonIterator(from, '-'); + AlignedCodon codon = codons.next(); + assertEquals("[1, 3, 5]", codon.toString()); + assertEquals("P", codon.product); + codon = codons.next(); + assertEquals("[8, 10, 11]", codon.toString()); + assertEquals("Q", codon.product); + codon = codons.next(); + assertEquals("[13, 15, 17]", codon.toString()); + assertEquals("R", codon.product); + assertFalse(codons.hasNext()); + } + + /** + * Test weird case where the mapping skips over a peptide. + */ + @Test + public void testNext_unmappedPeptide() + { + SequenceI from = new Sequence("Seq1", "-CgC-C-cCtAG-AtG-Gc"); + from.createDatasetSequence(); + SequenceI to = new Sequence("Seq1", "-PQ-TR-"); + to.createDatasetSequence(); + MapList map = new MapList(new int[] + { 1, 1, 3, 4, 6, 6, 8, 10, 12, 13 }, new int[] + { 1, 2, 4, 4 }, 3, 1); + Mapping m = new Mapping(to.getDatasetSequence(), map); + + Iterator codons = m.getCodonIterator(from, '-'); + AlignedCodon codon = codons.next(); + assertEquals("[1, 3, 5]", codon.toString()); + assertEquals("P", codon.product); + codon = codons.next(); + assertEquals("[8, 10, 11]", codon.toString()); + assertEquals("Q", codon.product); + codon = codons.next(); + assertEquals("[13, 15, 17]", codon.toString()); + assertEquals("R", codon.product); + assertFalse(codons.hasNext()); + } + + /** + * Test for exception thrown for an incomplete codon. + */ + @Test + public void testNext_incompleteCodon() + { + SequenceI from = new Sequence("Seq1", "-CgC-C-cCgTt"); + from.createDatasetSequence(); + SequenceI to = new Sequence("Seq1", "-PQ-R-"); + to.createDatasetSequence(); + MapList map = new MapList(new int[] + { 1, 1, 3, 4, 6, 6, 8, 8 }, new int[] + { 1, 3 }, 3, 1); + Mapping m = new Mapping(to.getDatasetSequence(), map); + + Iterator codons = m.getCodonIterator(from, '-'); + AlignedCodon codon = codons.next(); + assertEquals("[1, 3, 5]", codon.toString()); + assertEquals("P", codon.product); + try + { + codon = codons.next(); + fail("expected exception"); + } catch (IncompleteCodonException e) + { + // expected + } + } + + /** + * Test normal case for iterating over aligned codons. + */ + @Test + public void testAnother() + { + SequenceI from = new Sequence("Seq1", "TGCCATTACCAG-"); + from.createDatasetSequence(); + SequenceI to = new Sequence("Seq1", "CHYQ"); + to.createDatasetSequence(); + MapList map = new MapList(new int[] + { 1, 12 }, new int[] + { 1, 4 }, 3, 1); + Mapping m = new Mapping(to.getDatasetSequence(), map); + + Iterator codons = m.getCodonIterator(from, '-'); + AlignedCodon codon = codons.next(); + assertEquals("[0, 1, 2]", codon.toString()); + assertEquals("C", codon.product); + codon = codons.next(); + assertEquals("[3, 4, 5]", codon.toString()); + assertEquals("H", codon.product); + codon = codons.next(); + assertEquals("[6, 7, 8]", codon.toString()); + assertEquals("Y", codon.product); + codon = codons.next(); + assertEquals("[9, 10, 11]", codon.toString()); + assertEquals("Q", codon.product); + assertFalse(codons.hasNext()); + } +} diff --git a/test/jalview/datamodel/SearchResultsTest.java b/test/jalview/datamodel/SearchResultsTest.java index 3738614..15b0314 100644 --- a/test/jalview/datamodel/SearchResultsTest.java +++ b/test/jalview/datamodel/SearchResultsTest.java @@ -12,13 +12,13 @@ public class SearchResultsTest { SequenceI seq = new Sequence("", "abcdefghijklm"); SearchResults sr = new SearchResults(); - sr.addResult(seq, 0, 0); + sr.addResult(seq, 1, 1); assertEquals("a", sr.toString()); - sr.addResult(seq, 2, 4); + sr.addResult(seq, 3, 5); assertEquals("acde", sr.toString()); seq = new Sequence("", "pqrstuvwxy"); - sr.addResult(seq, 5, 6); + sr.addResult(seq, 6, 7); assertEquals("acdeuv", sr.toString()); } } diff --git a/test/jalview/util/MapListTest.java b/test/jalview/util/MapListTest.java index 1913a70..f69fe40 100644 --- a/test/jalview/util/MapListTest.java +++ b/test/jalview/util/MapListTest.java @@ -434,9 +434,9 @@ public class MapListTest assertEquals(18, ml.getFromHighest()); assertEquals(1, ml.getToLowest()); assertEquals(6, ml.getToHighest()); - assertEquals("[2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 16, 18]", - Arrays.toString(ml.getFromRanges())); - assertEquals("[1, 1, 3, 4, 6, 6]", Arrays.toString(ml.getToRanges())); + assertEquals("{[2, 3], [5, 7], [9, 10], [12, 12], [14, 14], [16, 18]}", + prettyPrint(ml.getFromRanges())); + assertEquals("{[1, 1], [3, 4], [6, 6]}", prettyPrint(ml.getToRanges())); /* * Also copy constructor @@ -447,9 +447,33 @@ public class MapListTest assertEquals(18, ml2.getFromHighest()); assertEquals(1, ml2.getToLowest()); assertEquals(6, ml2.getToHighest()); - assertEquals("[2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 16, 18]", - Arrays.toString(ml2.getFromRanges())); - assertEquals("[1, 1, 3, 4, 6, 6]", Arrays.toString(ml2.getToRanges())); + assertEquals("{[2, 3], [5, 7], [9, 10], [12, 12], [14, 14], [16, 18]}", + prettyPrint(ml2.getFromRanges())); + assertEquals("{[1, 1], [3, 4], [6, 6]}", prettyPrint(ml2.getToRanges())); + } + + /** + * Convert a List of {[i, j], [k, l], ...} to "[[i, j], [k, l], ...]" + * + * @param ranges + * @return + */ + private String prettyPrint(List ranges) + { + StringBuilder sb = new StringBuilder(ranges.size() * 5); + boolean first = true; + sb.append("{"); + for (int[] range : ranges) + { + if (!first) + { + sb.append(", "); + } + sb.append(Arrays.toString(range)); + first = false; + } + sb.append("}"); + return sb.toString(); } /** @@ -469,9 +493,9 @@ public class MapListTest assertEquals(ml.getFromRatio(), ml2.getToRatio()); assertEquals(ml.getToHighest(), ml2.getFromHighest()); assertEquals(ml.getFromHighest(), ml2.getToHighest()); - assertEquals(Arrays.toString(ml.getFromRanges()), - Arrays.toString(ml2.getToRanges())); - assertEquals(Arrays.toString(ml.getToRanges()), - Arrays.toString(ml2.getFromRanges())); + assertEquals(prettyPrint(ml.getFromRanges()), + prettyPrint(ml2.getToRanges())); + assertEquals(prettyPrint(ml.getToRanges()), + prettyPrint(ml2.getFromRanges())); } } -- 1.7.10.2