JAL-1800 separated SearchResults.getCharacters() from toString()
[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[]
236             { matchStart, matchEnd };
237           }
238           else
239           {
240             resultLength = result.length;
241             tmp = new int[resultLength + 2];
242             System.arraycopy(result, 0, tmp, 0, resultLength);
243             result = tmp;
244             result[resultLength] = matchStart;
245             result[resultLength + 1] = matchEnd;
246           }
247         }
248         else
249         {
250           // debug
251           // System.err.println("Outwith bounds!" + matchStart+">"+end +"  or "
252           // + matchEnd+"<"+start);
253         }
254       }
255     }
256     return result;
257   }
258
259   public int getSize()
260   {
261     return matches.size();
262   }
263
264   public SequenceI getResultSequence(int index)
265   {
266     return matches.get(index).sequence;
267   }
268
269   /**
270    * Returns the start position of the i'th match in the search results.
271    * 
272    * @param i
273    * @return
274    */
275   public int getResultStart(int i)
276   {
277     return matches.get(i).start;
278   }
279
280   /**
281    * Returns the end position of the i'th match in the search results.
282    * 
283    * @param i
284    * @return
285    */
286   public int getResultEnd(int i)
287   {
288     return matches.get(i).end;
289   }
290
291   /**
292    * Returns true if no search result matches are held.
293    * 
294    * @return
295    */
296   public boolean isEmpty()
297   {
298     return matches.isEmpty();
299   }
300
301   /**
302    * Returns the list of matches.
303    * 
304    * @return
305    */
306   public List<Match> getResults()
307   {
308     return matches;
309   }
310
311   /**
312    * Return the results as a string of characters (bases) prefixed by start
313    * position(s). Meant for use when the context ensures that all matches are to
314    * regions of the same sequence (otherwise the result is meaningless).
315    * 
316    * @return
317    */
318   @Override
319   public String toString()
320   {
321     StringBuilder result = new StringBuilder(256);
322     for (Match m : matches)
323     {
324       result.append(m.toString());
325     }
326     return result.toString();
327   }
328
329   /**
330    * Return the results as a string of characters (bases). Meant for use when
331    * the context ensures that all matches are to regions of the same sequence
332    * (otherwise the result is meaningless).
333    * 
334    * @return
335    */
336   public String getCharacters()
337   {
338     StringBuilder result = new StringBuilder(256);
339     for (Match m : matches)
340     {
341       result.append(m.getCharacters());
342     }
343     return result.toString();
344   }
345
346   /**
347    * Hashcode is has derived from the list of matches. This ensures that when
348    * two SearchResults objects satisfy the test for equals(), then they have the
349    * same hashcode.
350    */
351   @Override
352   public int hashCode()
353   {
354     return matches.hashCode();
355   }
356
357   /**
358    * Two SearchResults are considered equal if they contain the same matches in
359    * the same order.
360    */
361   @Override
362   public boolean equals(Object obj)
363   {
364     if (obj == null || !(obj instanceof SearchResults))
365     {
366       return false;
367     }
368     SearchResults sr = (SearchResults) obj;
369     return ((ArrayList<Match>) this.matches).equals(sr.matches);
370   }
371 }