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