JAL-354 JAL-1738 add returns SearchResultMatchI instance so it can be cached
[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<SearchResultMatchI> matches = new ArrayList<SearchResultMatchI>();
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 SearchResultMatchI))
168       {
169         return false;
170       }
171       SearchResultMatchI m = (SearchResultMatchI) obj;
172       return (sequence == m.getSequence() && start == m.getStart() && end == m
173               .getEnd());
174     }
175   }
176
177   /* (non-Javadoc)
178    * @see jalview.datamodel.SearchResultsI#addResult(jalview.datamodel.SequenceI, int, int)
179    */
180   @Override
181   public SearchResultMatchI addResult(SequenceI seq, int start, int end)
182   {
183     Match m = new Match(seq, start, end);
184     matches.add(m);
185     return m;
186   }
187
188   /* (non-Javadoc)
189    * @see jalview.datamodel.SearchResultsI#involvesSequence(jalview.datamodel.SequenceI)
190    */
191   @Override
192   public boolean involvesSequence(SequenceI sequence)
193   {
194     SequenceI ds = sequence.getDatasetSequence();
195     Match m;
196     for (SearchResultMatchI _m : matches)
197     {
198       m = (Match) _m;
199       if (m.sequence != null
200               && (m.sequence == sequence || m.sequence == ds))
201       {
202         return true;
203       }
204     }
205     return false;
206   }
207
208   /* (non-Javadoc)
209    * @see jalview.datamodel.SearchResultsI#getResults(jalview.datamodel.SequenceI, int, int)
210    */
211   @Override
212   public int[] getResults(SequenceI sequence, int start, int end)
213   {
214     if (matches.isEmpty())
215     {
216       return null;
217     }
218
219     int[] result = null;
220     int[] tmp = null;
221     int resultLength, matchStart = 0, matchEnd = 0;
222     boolean mfound;
223     Match m;
224     for (SearchResultMatchI _m : matches)
225     {
226       m = (Match) _m;
227
228       mfound = false;
229       if (m.sequence == sequence)
230       {
231         mfound = true;
232         // locate aligned position
233         matchStart = sequence.findIndex(m.start) - 1;
234         matchEnd = sequence.findIndex(m.end) - 1;
235       }
236       else if (m.sequence == sequence.getDatasetSequence())
237       {
238         mfound = true;
239         // locate region in local context
240         matchStart = sequence.findIndex(m.start) - 1;
241         matchEnd = sequence.findIndex(m.end) - 1;
242       }
243       if (mfound)
244       {
245         if (matchStart <= end && matchEnd >= start)
246         {
247           if (matchStart < start)
248           {
249             matchStart = start;
250           }
251
252           if (matchEnd > end)
253           {
254             matchEnd = end;
255           }
256
257           if (result == null)
258           {
259             result = new int[] { matchStart, matchEnd };
260           }
261           else
262           {
263             resultLength = result.length;
264             tmp = new int[resultLength + 2];
265             System.arraycopy(result, 0, tmp, 0, resultLength);
266             result = tmp;
267             result[resultLength] = matchStart;
268             result[resultLength + 1] = matchEnd;
269           }
270         }
271         else
272         {
273           // debug
274           // System.err.println("Outwith bounds!" + matchStart+">"+end +"  or "
275           // + matchEnd+"<"+start);
276         }
277       }
278     }
279     return result;
280   }
281
282   @Override
283   public int markColumns(SequenceCollectionI sqcol, BitSet bs)
284   {
285     int count = 0;
286     BitSet mask = new BitSet();
287     for (SequenceI s : sqcol.getSequences())
288     {
289       int[] cols = getResults(s, sqcol.getStartRes(), sqcol.getEndRes());
290       if (cols != null)
291       {
292         for (int pair = 0; pair < cols.length; pair += 2)
293         {
294           mask.set(cols[pair], cols[pair + 1] + 1);
295         }
296       }
297     }
298     // compute columns that were newly selected
299     BitSet original = (BitSet) bs.clone();
300     original.and(mask);
301     count = mask.cardinality() - original.cardinality();
302     // and mark ranges not already marked
303     bs.or(mask);
304     return count;
305   }
306
307   /* (non-Javadoc)
308    * @see jalview.datamodel.SearchResultsI#getSize()
309    */
310   @Override
311   public int getSize()
312   {
313     return matches.size();
314   }
315
316   /* (non-Javadoc)
317    * @see jalview.datamodel.SearchResultsI#getResultSequence(int)
318    */
319   @Override
320   public SequenceI getResultSequence(int index)
321   {
322     return matches.get(index).getSequence();
323   }
324
325   /* (non-Javadoc)
326    * @see jalview.datamodel.SearchResultsI#getResultStart(int)
327    */
328   @Override
329   public int getResultStart(int i)
330   {
331     return matches.get(i).getStart();
332   }
333
334   /* (non-Javadoc)
335    * @see jalview.datamodel.SearchResultsI#getResultEnd(int)
336    */
337   @Override
338   public int getResultEnd(int i)
339   {
340     return matches.get(i).getEnd();
341   }
342
343   /* (non-Javadoc)
344    * @see jalview.datamodel.SearchResultsI#isEmpty()
345    */
346   @Override
347   public boolean isEmpty()
348   {
349     return matches.isEmpty();
350   }
351
352   /* (non-Javadoc)
353    * @see jalview.datamodel.SearchResultsI#getResults()
354    */
355   @Override
356   public List<SearchResultMatchI> getResults()
357   {
358     return matches;
359   }
360
361   /**
362    * Return the results as a string of characters (bases) prefixed by start
363    * position(s). Meant for use when the context ensures that all matches are to
364    * regions of the same sequence (otherwise the result is meaningless).
365    * 
366    * @return
367    */
368   @Override
369   public String toString()
370   {
371     StringBuilder result = new StringBuilder(256);
372     for (SearchResultMatchI m : matches)
373     {
374       result.append(m.toString());
375     }
376     return result.toString();
377   }
378
379   /**
380    * Return the results as a string of characters (bases). Meant for use when
381    * the context ensures that all matches are to regions of the same sequence
382    * (otherwise the result is meaningless).
383    * 
384    * @return
385    */
386   public String getCharacters()
387   {
388     StringBuilder result = new StringBuilder(256);
389     for (SearchResultMatchI m : matches)
390     {
391       result.append(m.getCharacters());
392     }
393     return result.toString();
394   }
395
396   /**
397    * Hashcode is has derived from the list of matches. This ensures that when
398    * two SearchResults objects satisfy the test for equals(), then they have the
399    * same hashcode.
400    */
401   @Override
402   public int hashCode()
403   {
404     return matches.hashCode();
405   }
406
407   /**
408    * Two SearchResults are considered equal if they contain the same matches in
409    * the same order.
410    */
411   @Override
412   public boolean equals(Object obj)
413   {
414     if (obj == null || !(obj instanceof SearchResultsI))
415     {
416       return false;
417     }
418     SearchResultsI sr = (SearchResultsI) obj;
419     return matches.equals(sr.getResults());
420   }
421 }