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