JAL-345 JAL-1738 SearchResultI method to mark columns in a bitset for highlighted...
[jalview.git] / src / jalview / datamodel / SearchResults.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.datamodel;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.BitSet;
26 import java.util.List;
27
28 /**
29  * Holds a list of search result matches, where each match is a contiguous
30  * stretch of a single sequence.
31  * 
32  * @author gmcarstairs amwaterhouse
33  *
34  */
35 public class SearchResults implements SearchResultsI
36 {
37
38   private List<Match> matches = new ArrayList<Match>();
39
40   /**
41    * One match consists of a sequence reference, start and end positions.
42    * Discontiguous ranges in a sequence require two or more Match objects.
43    */
44   public class Match implements SearchResultMatchI
45   {
46     SequenceI sequence;
47
48     /**
49      * Start position of match in sequence (base 1)
50      */
51     int start;
52
53     /**
54      * End position (inclusive) (base 1)
55      */
56     int end;
57
58     /**
59      * Constructor
60      * 
61      * @param seq
62      *          a sequence
63      * @param start
64      *          start position of matched range (base 1)
65      * @param end
66      *          end of matched range (inclusive, base 1)
67      */
68     public Match(SequenceI seq, int start, int end)
69     {
70       sequence = seq;
71
72       /*
73        * always hold in forwards order, even if given in reverse order
74        * (such as from a mapping to a reverse strand); this avoids
75        * trouble for routines that highlight search results etc
76        */
77       if (start <= end)
78       {
79         this.start = start;
80         this.end = end;
81       }
82       else
83       {
84         this.start = end;
85         this.end = start;
86       }
87     }
88
89     /* (non-Javadoc)
90      * @see jalview.datamodel.SearchResultMatchI#getSequence()
91      */
92     @Override
93     public SequenceI getSequence()
94     {
95       return sequence;
96     }
97
98     /* (non-Javadoc)
99      * @see jalview.datamodel.SearchResultMatchI#getStart()
100      */
101     @Override
102     public int getStart()
103     {
104       return start;
105     }
106
107     /* (non-Javadoc)
108      * @see jalview.datamodel.SearchResultMatchI#getEnd()
109      */
110     @Override
111     public int getEnd()
112     {
113       return end;
114     }
115
116     /**
117      * Returns the string of characters in the matched region, prefixed by the
118      * start position, e.g. "12CGT" or "208K"
119      */
120     @Override
121     public String toString()
122     {
123       final int from = Math.max(start - 1, 0);
124       String startPosition = String.valueOf(from);
125       return startPosition + getCharacters();
126     }
127
128     /* (non-Javadoc)
129      * @see jalview.datamodel.SearchResultMatchI#getCharacters()
130      */
131     @Override
132     public String getCharacters()
133     {
134       char[] chars = sequence.getSequence();
135       // convert start/end to base 0 (with bounds check)
136       final int from = Math.max(start - 1, 0);
137       final int to = Math.min(end, chars.length + 1);
138       return String.valueOf(Arrays.copyOfRange(chars, from, to));
139     }
140
141     public void setSequence(SequenceI seq)
142     {
143       this.sequence = seq;
144     }
145
146     /**
147      * Hashcode is the hashcode of the matched sequence plus a hash of start and
148      * end positions. Match objects that pass the test for equals are guaranteed
149      * to have the same hashcode.
150      */
151     @Override
152     public int hashCode()
153     {
154       int hash = sequence == null ? 0 : sequence.hashCode();
155       hash += 31 * start;
156       hash += 67 * end;
157       return hash;
158     }
159
160     /**
161      * Two Match objects are equal if they are for the same sequence, start and
162      * end positions
163      */
164     @Override
165     public boolean equals(Object obj)
166     {
167       if (obj == null || !(obj instanceof Match))
168       {
169         return false;
170       }
171       Match m = (Match) obj;
172       return (this.sequence == m.sequence && this.start == m.start && this.end == m.end);
173     }
174   }
175
176   /* (non-Javadoc)
177    * @see jalview.datamodel.SearchResultsI#addResult(jalview.datamodel.SequenceI, int, int)
178    */
179   @Override
180   public void addResult(SequenceI seq, int start, int end)
181   {
182     matches.add(new Match(seq, start, end));
183   }
184
185   /* (non-Javadoc)
186    * @see jalview.datamodel.SearchResultsI#involvesSequence(jalview.datamodel.SequenceI)
187    */
188   @Override
189   public boolean involvesSequence(SequenceI sequence)
190   {
191     SequenceI ds = sequence.getDatasetSequence();
192     for (Match m : matches)
193     {
194       if (m.sequence != null
195               && (m.sequence == sequence || m.sequence == ds))
196       {
197         return true;
198       }
199     }
200     return false;
201   }
202
203   /* (non-Javadoc)
204    * @see jalview.datamodel.SearchResultsI#getResults(jalview.datamodel.SequenceI, int, int)
205    */
206   @Override
207   public int[] getResults(SequenceI sequence, int start, int end)
208   {
209     if (matches.isEmpty())
210     {
211       return null;
212     }
213
214     int[] result = null;
215     int[] tmp = null;
216     int resultLength, matchStart = 0, matchEnd = 0;
217     boolean mfound;
218     for (Match m : matches)
219     {
220       mfound = false;
221       if (m.sequence == sequence)
222       {
223         mfound = true;
224         // locate aligned position
225         matchStart = sequence.findIndex(m.start) - 1;
226         matchEnd = sequence.findIndex(m.end) - 1;
227       }
228       else if (m.sequence == sequence.getDatasetSequence())
229       {
230         mfound = true;
231         // locate region in local context
232         matchStart = sequence.findIndex(m.start) - 1;
233         matchEnd = sequence.findIndex(m.end) - 1;
234       }
235       if (mfound)
236       {
237         if (matchStart <= end && matchEnd >= start)
238         {
239           if (matchStart < start)
240           {
241             matchStart = start;
242           }
243
244           if (matchEnd > end)
245           {
246             matchEnd = end;
247           }
248
249           if (result == null)
250           {
251             result = new int[] { matchStart, matchEnd };
252           }
253           else
254           {
255             resultLength = result.length;
256             tmp = new int[resultLength + 2];
257             System.arraycopy(result, 0, tmp, 0, resultLength);
258             result = tmp;
259             result[resultLength] = matchStart;
260             result[resultLength + 1] = matchEnd;
261           }
262         }
263         else
264         {
265           // debug
266           // System.err.println("Outwith bounds!" + matchStart+">"+end +"  or "
267           // + matchEnd+"<"+start);
268         }
269       }
270     }
271     return result;
272   }
273
274   @Override
275   public int markColumns(SequenceCollectionI sqcol, BitSet bs)
276   {
277     int count = 0;
278     for (SequenceI s : sqcol.getSequences())
279     {
280       BitSet mask = new BitSet();
281       int[] cols = getResults(s, sqcol.getStartRes(), sqcol.getEndRes());
282       if (cols != null)
283       {
284         for (int pair = 0; pair < cols.length; pair += 2)
285         {
286           mask.set(cols[pair], cols[pair + 1] + 1);
287         }
288       }
289       // find columns that were already selected
290       BitSet compl = (BitSet) mask.clone();
291       compl.and(bs);
292       count += compl.cardinality();
293       // and mark ranges not already marked
294       bs.or(mask);
295     }
296     return count;
297   }
298
299   /* (non-Javadoc)
300    * @see jalview.datamodel.SearchResultsI#getSize()
301    */
302   @Override
303   public int getSize()
304   {
305     return matches.size();
306   }
307
308   /* (non-Javadoc)
309    * @see jalview.datamodel.SearchResultsI#getResultSequence(int)
310    */
311   @Override
312   public SequenceI getResultSequence(int index)
313   {
314     return matches.get(index).sequence;
315   }
316
317   /* (non-Javadoc)
318    * @see jalview.datamodel.SearchResultsI#getResultStart(int)
319    */
320   @Override
321   public int getResultStart(int i)
322   {
323     return matches.get(i).start;
324   }
325
326   /* (non-Javadoc)
327    * @see jalview.datamodel.SearchResultsI#getResultEnd(int)
328    */
329   @Override
330   public int getResultEnd(int i)
331   {
332     return matches.get(i).end;
333   }
334
335   /* (non-Javadoc)
336    * @see jalview.datamodel.SearchResultsI#isEmpty()
337    */
338   @Override
339   public boolean isEmpty()
340   {
341     return matches.isEmpty();
342   }
343
344   /* (non-Javadoc)
345    * @see jalview.datamodel.SearchResultsI#getResults()
346    */
347   @Override
348   public List<Match> getResults()
349   {
350     return matches;
351   }
352
353   /**
354    * Return the results as a string of characters (bases) prefixed by start
355    * position(s). Meant for use when the context ensures that all matches are to
356    * regions of the same sequence (otherwise the result is meaningless).
357    * 
358    * @return
359    */
360   @Override
361   public String toString()
362   {
363     StringBuilder result = new StringBuilder(256);
364     for (SearchResultMatchI m : matches)
365     {
366       result.append(m.toString());
367     }
368     return result.toString();
369   }
370
371   /**
372    * Return the results as a string of characters (bases). Meant for use when
373    * the context ensures that all matches are to regions of the same sequence
374    * (otherwise the result is meaningless).
375    * 
376    * @return
377    */
378   public String getCharacters()
379   {
380     StringBuilder result = new StringBuilder(256);
381     for (SearchResultMatchI m : matches)
382     {
383       result.append(m.getCharacters());
384     }
385     return result.toString();
386   }
387
388   /**
389    * Hashcode is has derived from the list of matches. This ensures that when
390    * two SearchResults objects satisfy the test for equals(), then they have the
391    * same hashcode.
392    */
393   @Override
394   public int hashCode()
395   {
396     return matches.hashCode();
397   }
398
399   /**
400    * Two SearchResults are considered equal if they contain the same matches in
401    * the same order.
402    */
403   @Override
404   public boolean equals(Object obj)
405   {
406     if (obj == null || !(obj instanceof SearchResults))
407     {
408       return false;
409     }
410     SearchResults sr = (SearchResults) obj;
411     return ((ArrayList<Match>) this.matches).equals(sr.matches);
412   }
413 }