JAL-4241 Flter empty columns from annotation service input
[jalview.git] / src / jalview / ws2 / actions / annotation / AnnotationJob.java
1 package jalview.ws2.actions.annotation;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.BitSet;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9
10 import jalview.analysis.AlignSeq;
11 import jalview.analysis.SeqsetUtils;
12 import jalview.datamodel.Sequence;
13 import jalview.datamodel.SequenceCollectionI;
14 import jalview.datamodel.SequenceI;
15 import jalview.schemes.ResidueProperties;
16 import jalview.util.Comparison;
17 import jalview.ws2.actions.BaseJob;
18
19 public class AnnotationJob extends BaseJob
20 {
21   final boolean[] gapMap;
22
23   final Map<String, SequenceI> seqNames;
24
25   final int regionStart, regionEnd;
26
27   final int minSize;
28
29   public AnnotationJob(List<SequenceI> inputSeqs, boolean[] gapMap,
30       Map<String, SequenceI> seqNames, int start, int end, int minSize)
31   {
32     super(inputSeqs);
33     this.gapMap = gapMap;
34     this.seqNames = seqNames;
35     this.regionStart = start;
36     this.regionEnd = end;
37     this.minSize = minSize;
38   }
39
40   @Override
41   public boolean isInputValid()
42   {
43     int nvalid = 0;
44     for (SequenceI sq : getInputSequences())
45       if (sq.getStart() <= sq.getEnd())
46         nvalid++;
47     return nvalid >= minSize;
48   }
49
50   public static AnnotationJob create(SequenceCollectionI inputSeqs,
51       boolean bySequence, boolean submitGaps, boolean requireAligned,
52       boolean filterNonStandardResidues, int minSize)
53   {
54     List<SequenceI> sequences = new ArrayList<>();
55     int minlen = 10;
56     int width = 0;
57     Map<String, SequenceI> namesMap = bySequence ? new HashMap<>() : null;
58     BitSet residueMap = new BitSet();
59     int start = inputSeqs.getStartRes();
60     int end = inputSeqs.getEndRes();
61     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
62     // correctly
63     // TODO: push attributes into WsJob instance (so they can be safely
64     // persisted/restored
65     for (SequenceI sq : inputSeqs.getSequences())
66     {
67       int sqLen = (bySequence)
68           ? sq.findPosition(end + 1) - sq.findPosition(start + 1)
69           : sq.getEnd() - sq.getStart();
70       if (sqLen < minlen)
71         continue;
72       width = Math.max(width, sq.getLength());
73       String newName = SeqsetUtils.unique_name(sequences.size() + 1);
74       if (namesMap != null)
75         namesMap.put(newName, sq);
76       char[] seqChars = sq.getSequence(start, end + 1);
77       if (filterNonStandardResidues)
78         replaceNonStandardResidues(seqChars, Comparison.GAP_DASH,
79             sq.isProtein());
80       Sequence seq;
81       if (submitGaps)
82       {
83         seq = new Sequence(newName, seqChars);
84         updateResidueMap(residueMap, seq);
85       } else
86       {
87         // TODO: add ability to exclude hidden regions
88         seq = new Sequence(newName,
89             AlignSeq.extractGaps(Comparison.GapChars, new String(seqChars)));
90         // for annotation need to also record map to sequence start/end
91         // position in range
92         // then transfer back to original sequence on return.
93       }
94       sequences.add(seq);
95     }
96     boolean[] gapMapArray = null;
97     if (submitGaps)
98     {
99       adjustColumns(sequences, residueMap, requireAligned);
100       gapMapArray = new boolean[width];
101       for (int i = 0; i < width; i++)
102         gapMapArray[i] = residueMap.get(i);
103     }
104     return new AnnotationJob(sequences, gapMapArray, namesMap, start, end,
105         minSize);
106   }
107
108   static void replaceNonStandardResidues(char[] seq, char replacement,
109       boolean isProtein)
110   {
111     for (int i = 0; i < seq.length; i++)
112     {
113       char chr = seq[i];
114       if (isProtein ? ResidueProperties.aaIndex[chr] >= 20
115           : ResidueProperties.nucleotideIndex[chr] >= 5)
116       {
117         seq[i] = replacement;
118       }
119     }
120   }
121
122   /**
123    * Add residue positions of the given sequence to the residues map. Perform an
124    * "or" operation between the given residue map and the inverse of the gap map
125    * of the given sequence.
126    * 
127    * @param residueMap
128    *          mapping to be updated in-place
129    * @param seq
130    *          the sequence whose residue positions are added to the map
131    */
132   static void updateResidueMap(BitSet residueMap, SequenceI seq)
133   {
134     var gaps = seq.gapBitset();
135     gaps.flip(0, seq.getLength());
136     residueMap.or(gaps);
137   }
138
139   /**
140    * Remove columns not included in the mask from the sequences in-place. If
141    * {@code padToLength} is set, the shorter sequences are padded with gaps at
142    * the end.
143    * 
144    * @param sequences
145    *          list of sequences to be modified
146    * @param mask
147    *          mask of columns that will remain
148    * @param padToLength
149    *          if gaps should be added to the end of shorter sequences
150    */
151   static void adjustColumns(List<SequenceI> sequences, BitSet mask,
152       boolean padToLength)
153   {
154     int width = mask.cardinality();
155     for (SequenceI seq : sequences)
156     {
157       char[] chars = SeqsetUtils.filterSequence(seq.getSequence(), mask);
158       if (padToLength && chars.length < width)
159       {
160         int limit = chars.length;
161         chars = Arrays.copyOf(chars, width);
162         Arrays.fill(chars, limit, chars.length, Comparison.GAP_DASH);
163       }
164       seq.setEnd(seq.getStart());
165       seq.setSequence(chars);
166     }
167   }
168 }