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