ad0e472f36f9bb9bf1deafd3389f1b4e3272e83d
[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.List;
26
27 /**
28  * Holds a list of search result matches, where each match is a contiguous
29  * stretch of a single sequence.
30  * 
31  * @author gmcarstairs
32  *
33  */
34 public class SearchResults
35 {
36
37   private List<Match> matches = new ArrayList<Match>();
38
39   /**
40    * One match consists of a sequence reference, start and end positions.
41    * Discontiguous ranges in a sequence require two or more Match objects.
42    */
43   public class Match
44   {
45     SequenceI sequence;
46
47     /**
48      * Start position of match in sequence (base 1)
49      */
50     int start;
51
52     /**
53      * End position (inclusive) (base 1)
54      */
55     int end;
56
57     /**
58      * Constructor
59      * 
60      * @param seq
61      *          a sequence
62      * @param start
63      *          start position of matched range (base 1)
64      * @param end
65      *          end of matched range (inclusive, base 1)
66      */
67     public Match(SequenceI seq, int start, int end)
68     {
69       sequence = seq;
70       this.start = start;
71       this.end = end;
72     }
73
74     public SequenceI getSequence()
75     {
76       return sequence;
77     }
78
79     public int getStart()
80     {
81       return start;
82     }
83
84     public int getEnd()
85     {
86       return end;
87     }
88
89     /**
90      * Returns the string of characters in the matched region, prefixed by the
91      * start position, e.g. "12CGT" or "208K"
92      */
93     @Override
94     public String toString()
95     {
96       final int from = Math.max(start - 1, 0);
97       String startPosition = String.valueOf(from);
98       return startPosition + getCharacters();
99     }
100
101     /**
102      * Returns the string of characters in the matched region.
103      */
104     public String getCharacters()
105     {
106       char[] chars = sequence.getSequence();
107       // convert start/end to base 0 (with bounds check)
108       final int from = Math.max(start - 1, 0);
109       final int to = Math.min(end, chars.length + 1);
110       return String.valueOf(Arrays.copyOfRange(chars, from, to));
111     }
112
113     public void setSequence(SequenceI seq)
114     {
115       this.sequence = seq;
116     }
117
118     /**
119      * Hashcode is the hashcode of the matched sequence plus a hash of start and
120      * end positions. Match objects that pass the test for equals are guaranteed
121      * to have the same hashcode.
122      */
123     @Override
124     public int hashCode()
125     {
126       int hash = sequence == null ? 0 : sequence.hashCode();
127       hash += 31 * start;
128       hash += 67 * end;
129       return hash;
130     }
131
132     /**
133      * Two Match objects are equal if they are for the same sequence, start and
134      * end positions
135      */
136     @Override
137     public boolean equals(Object obj)
138     {
139       if (obj == null || !(obj instanceof Match))
140       {
141         return false;
142       }
143       Match m = (Match) obj;
144       return (this.sequence == m.sequence && this.start == m.start && this.end == m.end);
145     }
146   }
147
148   /**
149    * This method replaces the old search results which merely held an alignment
150    * index of search matches. This broke when sequences were moved around the
151    * alignment
152    * 
153    * @param seq
154    *          Sequence
155    * @param start
156    *          int
157    * @param end
158    *          int
159    */
160   public void addResult(SequenceI seq, int start, int end)
161   {
162     matches.add(new Match(seq, start, end));
163   }
164
165   /**
166    * Quickly check if the given sequence is referred to in the search results
167    * 
168    * @param sequence
169    *          (specific alignment sequence or a dataset sequence)
170    * @return true if the results involve sequence
171    */
172   public boolean involvesSequence(SequenceI sequence)
173   {
174     SequenceI ds = sequence.getDatasetSequence();
175     for (Match m : matches)
176     {
177       if (m.sequence != null
178               && (m.sequence == sequence || m.sequence == ds))
179       {
180         return true;
181       }
182     }
183     return false;
184   }
185
186   /**
187    * This Method returns the search matches which lie between the start and end
188    * points of the sequence in question. It is optimised for returning objects
189    * for drawing on SequenceCanvas
190    */
191   public int[] getResults(SequenceI sequence, int start, int end)
192   {
193     if (matches.isEmpty())
194     {
195       return null;
196     }
197
198     int[] result = null;
199     int[] tmp = null;
200     int resultLength, matchStart = 0, matchEnd = 0;
201     boolean mfound;
202     for (Match m : matches)
203     {
204       mfound = false;
205       if (m.sequence == sequence)
206       {
207         mfound = true;
208         // locate aligned position
209         matchStart = sequence.findIndex(m.start) - 1;
210         matchEnd = sequence.findIndex(m.end) - 1;
211       }
212       else if (m.sequence == sequence.getDatasetSequence())
213       {
214         mfound = true;
215         // locate region in local context
216         matchStart = sequence.findIndex(m.start) - 1;
217         matchEnd = sequence.findIndex(m.end) - 1;
218       }
219       if (mfound)
220       {
221         if (matchStart <= end && matchEnd >= start)
222         {
223           if (matchStart < start)
224           {
225             matchStart = start;
226           }
227
228           if (matchEnd > end)
229           {
230             matchEnd = end;
231           }
232
233           if (result == null)
234           {
235             result = new int[] { matchStart, matchEnd };
236           }
237           else
238           {
239             resultLength = result.length;
240             tmp = new int[resultLength + 2];
241             System.arraycopy(result, 0, tmp, 0, resultLength);
242             result = tmp;
243             result[resultLength] = matchStart;
244             result[resultLength + 1] = matchEnd;
245           }
246         }
247         else
248         {
249           // debug
250           // System.err.println("Outwith bounds!" + matchStart+">"+end +"  or "
251           // + matchEnd+"<"+start);
252         }
253       }
254     }
255     return result;
256   }
257
258   public int getSize()
259   {
260     return matches.size();
261   }
262
263   public SequenceI getResultSequence(int index)
264   {
265     return matches.get(index).sequence;
266   }
267
268   /**
269    * Returns the start position of the i'th match in the search results.
270    * 
271    * @param i
272    * @return
273    */
274   public int getResultStart(int i)
275   {
276     return matches.get(i).start;
277   }
278
279   /**
280    * Returns the end position of the i'th match in the search results.
281    * 
282    * @param i
283    * @return
284    */
285   public int getResultEnd(int i)
286   {
287     return matches.get(i).end;
288   }
289
290   /**
291    * Returns true if no search result matches are held.
292    * 
293    * @return
294    */
295   public boolean isEmpty()
296   {
297     return matches.isEmpty();
298   }
299
300   /**
301    * Returns the list of matches.
302    * 
303    * @return
304    */
305   public List<Match> getResults()
306   {
307     return matches;
308   }
309
310   /**
311    * Return the results as a string of characters (bases) prefixed by start
312    * position(s). Meant for use when the context ensures that all matches are to
313    * regions of the same sequence (otherwise the result is meaningless).
314    * 
315    * @return
316    */
317   @Override
318   public String toString()
319   {
320     StringBuilder result = new StringBuilder(256);
321     for (Match m : matches)
322     {
323       result.append(m.toString());
324     }
325     return result.toString();
326   }
327
328   /**
329    * Return the results as a string of characters (bases). Meant for use when
330    * the context ensures that all matches are to regions of the same sequence
331    * (otherwise the result is meaningless).
332    * 
333    * @return
334    */
335   public String getCharacters()
336   {
337     StringBuilder result = new StringBuilder(256);
338     for (Match m : matches)
339     {
340       result.append(m.getCharacters());
341     }
342     return result.toString();
343   }
344
345   /**
346    * Hashcode is has derived from the list of matches. This ensures that when
347    * two SearchResults objects satisfy the test for equals(), then they have the
348    * same hashcode.
349    */
350   @Override
351   public int hashCode()
352   {
353     return matches.hashCode();
354   }
355
356   /**
357    * Two SearchResults are considered equal if they contain the same matches in
358    * the same order.
359    */
360   @Override
361   public boolean equals(Object obj)
362   {
363     if (obj == null || !(obj instanceof SearchResults))
364     {
365       return false;
366     }
367     SearchResults sr = (SearchResults) obj;
368     return ((ArrayList<Match>) this.matches).equals(sr.matches);
369   }
370 }