JAL-1800 SearchResults.equals() to allow smarter mouseover of codons
[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.
91      */
92     @Override
93     public String toString()
94     {
95       char[] chars = sequence.getSequence();
96       // convert start/end to base 0 (with bounds check)
97       final int from = Math.max(start - 1, 0);
98       final int to = Math.min(end, chars.length + 1);
99       // return String.valueOf(Arrays.copyOfRange(chars, from, to));
100       return String.valueOf(from)
101               + String.valueOf(Arrays.copyOfRange(chars, from, to));
102     }
103
104     public void setSequence(SequenceI seq)
105     {
106       this.sequence = seq;
107     }
108
109     /**
110      * Hashcode is the hashcode of the matched sequence plus a hash of start and
111      * end positions. Match objects that pass the test for equals are guaranteed
112      * to have the same hashcode.
113      */
114     @Override
115     public int hashCode()
116     {
117       int hash = sequence == null ? 0 : sequence.hashCode();
118       hash += 31 * start;
119       hash += 67 * end;
120       return hash;
121     }
122
123     /**
124      * Two Match objects are equal if they are for the same sequence, start and
125      * end positions
126      */
127     @Override
128     public boolean equals(Object obj)
129     {
130       if (obj == null || !(obj instanceof Match))
131       {
132         return false;
133       }
134       Match m = (Match) obj;
135       return (this.sequence == m.sequence && this.start == m.start && this.end == m.end);
136     }
137   }
138
139   /**
140    * This method replaces the old search results which merely held an alignment
141    * index of search matches. This broke when sequences were moved around the
142    * alignment
143    * 
144    * @param seq
145    *          Sequence
146    * @param start
147    *          int
148    * @param end
149    *          int
150    */
151   public void addResult(SequenceI seq, int start, int end)
152   {
153     matches.add(new Match(seq, start, end));
154   }
155
156   /**
157    * Quickly check if the given sequence is referred to in the search results
158    * 
159    * @param sequence
160    *          (specific alignment sequence or a dataset sequence)
161    * @return true if the results involve sequence
162    */
163   public boolean involvesSequence(SequenceI sequence)
164   {
165     SequenceI ds = sequence.getDatasetSequence();
166     for (Match m : matches)
167     {
168       if (m.sequence != null
169               && (m.sequence == sequence || m.sequence == ds))
170       {
171         return true;
172       }
173     }
174     return false;
175   }
176
177   /**
178    * This Method returns the search matches which lie between the start and end
179    * points of the sequence in question. It is optimised for returning objects
180    * for drawing on SequenceCanvas
181    */
182   public int[] getResults(SequenceI sequence, int start, int end)
183   {
184     if (matches.isEmpty())
185     {
186       return null;
187     }
188
189     int[] result = null;
190     int[] tmp = null;
191     int resultLength, matchStart = 0, matchEnd = 0;
192     boolean mfound;
193     for (Match m : matches)
194     {
195       mfound = false;
196       if (m.sequence == sequence)
197       {
198         mfound = true;
199         // locate aligned position
200         matchStart = sequence.findIndex(m.start) - 1;
201         matchEnd = sequence.findIndex(m.end) - 1;
202       }
203       else if (m.sequence == sequence.getDatasetSequence())
204       {
205         mfound = true;
206         // locate region in local context
207         matchStart = sequence.findIndex(m.start) - 1;
208         matchEnd = sequence.findIndex(m.end) - 1;
209       }
210       if (mfound)
211       {
212         if (matchStart <= end && matchEnd >= start)
213         {
214           if (matchStart < start)
215           {
216             matchStart = start;
217           }
218
219           if (matchEnd > end)
220           {
221             matchEnd = end;
222           }
223
224           if (result == null)
225           {
226             result = new int[]
227             { matchStart, matchEnd };
228           }
229           else
230           {
231             resultLength = result.length;
232             tmp = new int[resultLength + 2];
233             System.arraycopy(result, 0, tmp, 0, resultLength);
234             result = tmp;
235             result[resultLength] = matchStart;
236             result[resultLength + 1] = matchEnd;
237           }
238         }
239         else
240         {
241           // debug
242           // System.err.println("Outwith bounds!" + matchStart+">"+end +"  or "
243           // + matchEnd+"<"+start);
244         }
245       }
246     }
247     return result;
248   }
249
250   public int getSize()
251   {
252     return matches.size();
253   }
254
255   public SequenceI getResultSequence(int index)
256   {
257     return matches.get(index).sequence;
258   }
259
260   /**
261    * Returns the start position of the i'th match in the search results.
262    * 
263    * @param i
264    * @return
265    */
266   public int getResultStart(int i)
267   {
268     return matches.get(i).start;
269   }
270
271   /**
272    * Returns the end position of the i'th match in the search results.
273    * 
274    * @param i
275    * @return
276    */
277   public int getResultEnd(int i)
278   {
279     return matches.get(i).end;
280   }
281
282   /**
283    * Returns true if no search result matches are held.
284    * 
285    * @return
286    */
287   public boolean isEmpty()
288   {
289     return matches.isEmpty();
290   }
291
292   /**
293    * Returns the list of matches.
294    * 
295    * @return
296    */
297   public List<Match> getResults()
298   {
299     return matches;
300   }
301
302   /**
303    * Return the results as a string of characters. Meant for use when the
304    * context ensures that all matches are to regions of the same sequence
305    * (otherwise the result is meaningless).
306    * 
307    * @return
308    */
309   @Override
310   public String toString()
311   {
312     StringBuilder result = new StringBuilder(256);
313     for (Match m : matches)
314     {
315       result.append(m.toString());
316     }
317     return result.toString();
318   }
319
320   /**
321    * Hashcode is has derived from the list of matches. This ensures that when
322    * two SearchResults objects satisfy the test for equals(), then they have the
323    * same hashcode.
324    */
325   @Override
326   public int hashCode()
327   {
328     return matches.hashCode();
329   }
330
331   /**
332    * Two SearchResults are considered equal if they contain the same matches in
333    * the same order.
334    */
335   @Override
336   public boolean equals(Object obj)
337   {
338     if (obj == null || !(obj instanceof SearchResults))
339     {
340       return false;
341     }
342     SearchResults sr = (SearchResults) obj;
343     return ((ArrayList<Match>) this.matches).equals(sr.matches);
344   }
345 }