JAL-3195 revised SequenceIdMatcher (tests todo)
[jalview.git] / src / jalview / analysis / SequenceIdMatcher.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.analysis;
22
23 import jalview.datamodel.DBRefEntry;
24 import jalview.datamodel.SequenceI;
25
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31
32 /**
33  * Routines for approximate Sequence Id resolution by name using string
34  * containment (on word boundaries) rather than equivalence. It also attempts to
35  * resolve ties where no exact match is available by picking the the id closest
36  * to the query.
37  */
38 public class SequenceIdMatcher
39 {
40   private HashMap<SeqIdName, List<SequenceI>> names;
41
42   private Map<SeqIdName, List<SequenceI>> excludes;
43
44   public SequenceIdMatcher(List<SequenceI> seqs)
45   {
46     names = new HashMap<>();
47     excludes = new HashMap<>();
48     addAll(seqs);
49   }
50
51   /**
52    * Adds sequences to this matcher
53    * 
54    * @param seqs
55    */
56   public void addAll(List<SequenceI> seqs)
57   {
58     for (SequenceI seq : seqs)
59     {
60       add(seq);
61     }
62   }
63
64   /**
65    * Adds one sequence to this matcher
66    * 
67    * @param seq
68    */
69   public void add(SequenceI seq)
70   {
71     SeqIdName key = new SeqIdName(seq.getDisplayId(true));
72     addMatchCandidate(key, seq);
73     SequenceI dbseq = seq;
74     while (dbseq.getDatasetSequence() != null)
75     {
76       dbseq = dbseq.getDatasetSequence();
77     }
78     // add in any interesting identifiers
79     if (dbseq.getDBRefs() != null)
80     {
81       DBRefEntry dbr[] = dbseq.getDBRefs();
82       for (int r = 0; r < dbr.length; r++)
83       {
84         DBRefEntry dbref = dbr[r];
85         SeqIdName sid = new SeqIdName(dbref.getAccessionId());
86         if (dbref.getMap() != null
87                 && dbref.getMap().getMap().isTripletMap())
88         {
89           /*
90            * dbref with 3:1 or 1:3 mapping (e.g. CDS/protein);
91            * mark as not a valid match for this id
92            */
93           List<SequenceI> excluded = excludes.get(sid);
94           if (excluded == null)
95           {
96             excludes.put(sid, excluded = new ArrayList<>());
97           }
98           excluded.add(seq);
99           System.out.println("Excluding " + sid + "->" + seq);
100           continue;
101         }
102         addMatchCandidate(sid, seq);
103       }
104     }
105   }
106
107   void addMatchCandidate(SeqIdName key, SequenceI seq)
108   {
109     List<SequenceI> namesList = names.get(key);
110     if (namesList == null)
111     {
112       names.put(key, namesList = new ArrayList<>());
113     }
114     if (!namesList.contains(seq))
115     {
116       namesList.add(seq);
117       System.out.println("Adding " + key + "->" + seq);
118     }
119   }
120
121   /**
122    * convenience method to make a matcher from concrete array
123    * 
124    * @param sequences
125    */
126   public SequenceIdMatcher(SequenceI[] sequences)
127   {
128     this(Arrays.asList(sequences));
129   }
130
131   /**
132    * returns the closest SequenceI in matches to SeqIdName and returns all the
133    * matches to the names hash.
134    * 
135    * @param candName
136    *          SeqIdName
137    * @param matches
138    *          List of SequenceI objects
139    * @return SequenceI closest SequenceI to SeqIdName
140    */
141   private SequenceI pickbestMatch(SeqIdName candName,
142           List<SequenceI> matches)
143   {
144     List<SequenceI> st = pickbestMatches(candName, matches);
145     return st == null || st.size() == 0 ? null : st.get(0);
146   }
147
148   /**
149    * returns the closest SequenceI in matches to SeqIdName and returns all the
150    * matches to the names hash.
151    * 
152    * @param candName
153    *          SeqIdName
154    * @param matches
155    *          Vector of SequenceI objects
156    * @return Object[] { SequenceI closest SequenceI to SeqIdName, SequenceI[]
157    *         ties }
158    */
159   private List<SequenceI> pickbestMatches(SeqIdName candName,
160           List<SequenceI> matches)
161   {
162     List<SequenceI> best = new ArrayList<>();
163     if (candName == null || matches == null || matches.size() == 0)
164     {
165       return null;
166     }
167     SequenceI match = matches.remove(0);
168     best.add(match);
169     addMatchCandidate(new SeqIdName(match.getName()), match);
170     int matchlen = match.getName().length();
171     int namlen = candName.id.length();
172     while (matches.size() > 0)
173     {
174       // look through for a better one.
175       SequenceI cand = matches.remove(0);
176       addMatchCandidate(new SeqIdName(cand.getName()), cand);
177       int q, w, candlen = cand.getName().length();
178       // keep the one with an id 'closer' to the given seqnam string
179       if ((q = Math.abs(matchlen - namlen)) > (w = Math
180               .abs(candlen - namlen)) && candlen > matchlen)
181       {
182         best.clear();
183         match = cand;
184         matchlen = candlen;
185         best.add(match);
186       }
187       if (q == w && candlen == matchlen)
188       {
189         // record any ties
190         best.add(cand);
191       }
192     }
193     if (best.size() == 0)
194     {
195       return null;
196     }
197     ;
198     return best;
199   }
200
201   /**
202    * get SequenceI with closest SequenceI.getName() to seq.getName()
203    * 
204    * @param seq
205    *          SequenceI
206    * @return SequenceI
207    */
208   public SequenceI findIdMatch(SequenceI seq)
209   {
210     SeqIdName nam = new SeqIdName(seq.getName());
211     return findIdMatch(nam);
212   }
213
214   public SequenceI findIdMatch(String seqnam)
215   {
216     SeqIdName nam = new SeqIdName(seqnam);
217     return findIdMatch(nam);
218   }
219
220   /**
221    * Find all matches for a given sequence name.
222    * 
223    * @param seqnam
224    *          string to query Matcher with.
225    * @return a new array or (possibly) null
226    */
227   public SequenceI[] findAllIdMatches(String seqnam)
228   {
229
230     SeqIdName nam = new SeqIdName(seqnam);
231     List<SequenceI> m = findAllIdMatches(nam);
232     if (m != null)
233     {
234       return m.toArray(new SequenceI[m.size()]);
235     }
236     return null;
237   }
238
239   /**
240    * findIdMatch
241    * 
242    * Return pointers to sequences (or sequence object containers) which have
243    * same Id as a given set of different sequence objects
244    * 
245    * @param seqs
246    *          SequenceI[]
247    * @return SequenceI[]
248    */
249   public SequenceI[] findIdMatch(SequenceI[] seqs)
250   {
251     SequenceI[] namedseqs = null;
252     int i = 0;
253     SeqIdName nam;
254
255     if (seqs.length > 0)
256     {
257       namedseqs = new SequenceI[seqs.length];
258       do
259       {
260         nam = new SeqIdName(seqs[i].getName());
261
262         if (names.containsKey(nam))
263         {
264           namedseqs[i] = findIdMatch(nam);
265         }
266         else
267         {
268           namedseqs[i] = null;
269         }
270       } while (++i < seqs.length);
271     }
272
273     return namedseqs;
274   }
275
276   /**
277    * core findIdMatch search method
278    * 
279    * @param nam
280    *          SeqIdName
281    * @return SequenceI
282    */
283   private SequenceI findIdMatch(SeqIdName nam)
284   {
285     List<SequenceI> matches = new ArrayList<>();
286     while (names.containsKey(nam))
287     {
288       List<SequenceI> candidates = names.remove(nam);
289       List<SequenceI> except = excludes.get(nam);
290       int j = candidates.size();
291       for (int i = 0; i < j; i++)
292       {
293         SequenceI candidate = candidates.get(i);
294         if (!except.contains(candidate))
295         {
296           matches.add(candidate);
297         }
298       }
299     }
300     return pickbestMatch(nam, matches);
301   }
302
303   /**
304    * core findIdMatch search method for finding all equivalent matches
305    * 
306    * @param nam
307    *          SeqIdName
308    * @return SequenceI[]
309    */
310   private List<SequenceI> findAllIdMatches(
311           jalview.analysis.SequenceIdMatcher.SeqIdName nam)
312   {
313     List<SequenceI> matches = new ArrayList<>();
314     while (names.containsKey(nam))
315     {
316       matches.addAll(names.remove(nam));
317     }
318     List<SequenceI> r = pickbestMatches(nam, matches);
319     return r;
320   }
321
322   class SeqIdName
323   {
324     String id;
325
326     SeqIdName(String s)
327     {
328       if (s != null)
329       {
330         id = s.toLowerCase();
331       }
332       else
333       {
334         id = "";
335       }
336     }
337
338     @Override
339     public int hashCode()
340     {
341       return ((id.length() >= 4) ? id.substring(0, 4).hashCode()
342               : id.hashCode());
343     }
344
345     @Override
346     public boolean equals(Object s)
347     {
348       if (s == null)
349       {
350         return false;
351       }
352       if (s instanceof SeqIdName)
353       {
354         return this.stringequals(((SeqIdName) s).id);
355       }
356       else
357       {
358         if (s instanceof String)
359         {
360           return this.stringequals(((String) s).toLowerCase());
361         }
362       }
363
364       return false;
365     }
366
367     /**
368      * Characters that define the end of a unique sequence ID at the beginning
369      * of an arbitrary ID string JBPNote: This is a heuristic that will fail for
370      * arbritrarily extended sequence id's (like portions of an aligned set of
371      * repeats from one sequence)
372      */
373     private String WORD_SEP = "~. |#\\/<>!\"" + ((char) 0x00A4)
374             + "$%^*)}[@',?_";
375
376     /**
377      * matches if one ID properly contains another at a whitespace boundary.
378      * TODO: (JBPNote) These are not efficient. should use char[] for speed
379      * todo: (JBPNote) Set separator characters appropriately
380      * 
381      * @param s
382      * @return boolean
383      */
384     private boolean stringequals(String s)
385     {
386       if (id.length() > s.length())
387       {
388         return id.startsWith(s)
389                 ? (WORD_SEP.indexOf(id.charAt(s.length())) > -1)
390                 : false;
391       }
392       else
393       {
394         return s.startsWith(id)
395                 ? (s.equals(id) ? true
396                         : (WORD_SEP.indexOf(s.charAt(id.length())) > -1))
397                 : false;
398       }
399     }
400
401     /**
402      * toString method returns the wrapped sequence id. For debugging purposes
403      * only, behaviour not guaranteed not to change.
404      */
405     @Override
406     public String toString()
407     {
408       return id;
409     }
410   }
411 }