JAL-3383 code and unit test tweaks
[jalview.git] / src / jalview / datamodel / features / SequenceFeatures.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.features;
22
23 import jalview.datamodel.SequenceFeature;
24 import jalview.io.gff.SequenceOntologyFactory;
25 import jalview.io.gff.SequenceOntologyI;
26
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Map.Entry;
34 import java.util.Set;
35 import java.util.TreeMap;
36
37 import intervalstore.api.IntervalI;
38
39 /**
40  * A class that stores sequence features in a way that supports efficient
41  * querying by type and location (overlap). Intended for (but not limited to)
42  * storage of features for one sequence.
43  * 
44  * @author gmcarstairs
45  *
46  */
47 public class SequenceFeatures implements SequenceFeaturesI
48 {
49   /*
50    * map from feature type to structured store of features for that type
51    * null types are permitted (but not a good idea!)
52    */
53   private Map<String, FeatureStore> featureStore;
54
55   /**
56    * Constructor
57    */
58   public SequenceFeatures()
59   {
60     /*
61      * use a TreeMap so that features are returned in alphabetical order of type
62      * ? wrap as a synchronized map for add and delete operations
63      */
64     // featureStore = Collections
65     // .synchronizedSortedMap(new TreeMap<String, FeatureStore>());
66     featureStore = new TreeMap<>();
67   }
68
69   /**
70    * Constructor given a list of features
71    */
72   public SequenceFeatures(List<SequenceFeature> features)
73   {
74     this();
75     if (features != null)
76     {
77       for (SequenceFeature feature : features)
78       {
79         add(feature);
80       }
81     }
82   }
83
84   /**
85    * {@inheritDoc}
86    */
87   @Override
88   public boolean add(SequenceFeature sf)
89   {
90     String type = sf.getType();
91     if (type == null)
92     {
93       System.err.println("Feature type may not be null: " + sf.toString());
94       return false;
95     }
96
97     if (featureStore.get(type) == null)
98     {
99       featureStore.put(type, new FeatureStore());
100     }
101     return featureStore.get(type).addFeature(sf);
102   }
103
104   /**
105    * {@inheritDoc}
106    */
107   @Override
108   public List<SequenceFeature> findFeatures(int from, int to,
109           String... type)
110   {
111     List<SequenceFeature> result = new ArrayList<>();
112     for (FeatureStore featureSet : varargToTypes(type))
113     {
114       featureSet.findFeatures(from, to, result);
115     }
116     return result;
117   }
118
119   /**
120    * {@inheritDoc}
121    */
122   @Override
123   public List<SequenceFeature> getAllFeatures(String... type)
124   {
125     List<SequenceFeature> result = new ArrayList<>();
126
127     result.addAll(getPositionalFeatures(type));
128
129     result.addAll(getNonPositionalFeatures());
130
131     return result;
132   }
133
134   /**
135    * {@inheritDoc}
136    */
137   @Override
138   public List<SequenceFeature> getFeaturesByOntology(String... ontologyTerm)
139   {
140     if (ontologyTerm == null || ontologyTerm.length == 0)
141     {
142       return new ArrayList<>();
143     }
144
145     Set<String> featureTypes = getFeatureTypes(ontologyTerm);
146     if (featureTypes.isEmpty())
147     {
148       /*
149        * no features of the specified type or any sub-type
150        */
151       return new ArrayList<>();
152     }
153
154     return getAllFeatures(
155             featureTypes.toArray(new String[featureTypes.size()]));
156   }
157
158   /**
159    * {@inheritDoc}
160    */
161   @Override
162   public int getFeatureCount(boolean positional, String... type)
163   {
164     int result = 0;
165
166     for (FeatureStore featureSet : varargToTypes(type))
167     {
168       result += featureSet.getFeatureCount(positional);
169     }
170     return result;
171   }
172
173   /**
174    * {@inheritDoc}
175    */
176   @Override
177   public int getTotalFeatureLength(String... type)
178   {
179     int result = 0;
180
181     for (FeatureStore featureSet : varargToTypes(type))
182     {
183       result += featureSet.getTotalFeatureLength();
184     }
185     return result;
186   }
187
188   /**
189    * {@inheritDoc}
190    */
191   @Override
192   public List<SequenceFeature> getPositionalFeatures(String... type)
193   {
194     List<SequenceFeature> result = new ArrayList<>();
195
196     for (FeatureStore featureSet : varargToTypes(type))
197     {
198       featureSet.getPositionalFeatures(result);
199     }
200     return result;
201   }
202
203   /**
204    * A convenience method that converts a vararg for feature types to an
205    * Iterable over matched feature sets in key order
206    * 
207    * @param type
208    * @return
209    */
210   protected Iterable<FeatureStore> varargToTypes(String... type)
211   {
212     if (type == null || type.length == 0)
213     {
214       /*
215        * no vararg parameter supplied - return all
216        */
217       return featureStore.values();
218     }
219
220     List<FeatureStore> types = new ArrayList<>();
221     List<String> args = Arrays.asList(type);
222     for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
223     {
224       if (args.contains(featureType.getKey()))
225       {
226         types.add(featureType.getValue());
227       }
228     }
229     return types;
230   }
231
232   /**
233    * {@inheritDoc}
234    */
235   @Override
236   public List<SequenceFeature> getContactFeatures(String... type)
237   {
238     List<SequenceFeature> result = new ArrayList<>();
239
240     for (FeatureStore featureSet : varargToTypes(type))
241     {
242       featureSet.getContactFeatures(result);
243     }
244     return result;
245   }
246
247   /**
248    * {@inheritDoc}
249    */
250   @Override
251   public List<SequenceFeature> getNonPositionalFeatures(String... type)
252   {
253     List<SequenceFeature> result = new ArrayList<>();
254
255     for (FeatureStore featureSet : varargToTypes(type))
256     {
257       featureSet.getNonPositionalFeatures(result);
258     }
259     return result;
260   }
261
262   /**
263    * {@inheritDoc}
264    */
265   @Override
266   public boolean delete(SequenceFeature sf)
267   {
268     for (FeatureStore featureSet : featureStore.values())
269     {
270       if (featureSet.delete(sf))
271       {
272         return true;
273       }
274     }
275     return false;
276   }
277
278   /**
279    * {@inheritDoc}
280    */
281   @Override
282   public boolean hasFeatures()
283   {
284     for (FeatureStore featureSet : featureStore.values())
285     {
286       if (!featureSet.isEmpty())
287       {
288         return true;
289       }
290     }
291     return false;
292   }
293
294   /**
295    * {@inheritDoc}
296    */
297   @Override
298   public Set<String> getFeatureGroups(boolean positionalFeatures,
299           String... type)
300   {
301     Set<String> groups = new HashSet<>();
302
303     for (FeatureStore featureSet : varargToTypes(type))
304     {
305       groups.addAll(featureSet.getFeatureGroups(positionalFeatures));
306     }
307
308     return groups;
309   }
310
311   /**
312    * {@inheritDoc}
313    */
314   @Override
315   public Set<String> getFeatureTypesForGroups(boolean positionalFeatures,
316           String... groups)
317   {
318     Set<String> result = new HashSet<>();
319
320     for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
321     {
322       Set<String> featureGroups = featureType.getValue()
323               .getFeatureGroups(positionalFeatures);
324       for (String group : groups)
325       {
326         if (featureGroups.contains(group))
327         {
328           /*
329            * yes this feature type includes one of the query groups
330            */
331           result.add(featureType.getKey());
332           break;
333         }
334       }
335     }
336
337     return result;
338   }
339
340   /**
341    * {@inheritDoc}
342    */
343   @Override
344   public Set<String> getFeatureTypes(String... soTerm)
345   {
346     Set<String> types = new HashSet<>();
347     for (Entry<String, FeatureStore> entry : featureStore.entrySet())
348     {
349       String type = entry.getKey();
350       if (!entry.getValue().isEmpty() && isOntologyTerm(type, soTerm))
351       {
352         types.add(type);
353       }
354     }
355     return types;
356   }
357
358   /**
359    * Answers true if the given type matches one of the specified terms (or is a
360    * sub-type of one in the Sequence Ontology), or if no terms are supplied.
361    * Answers false if filter terms are specified and the given term does not
362    * match any of them.
363    * 
364    * @param type
365    * @param soTerm
366    * @return
367    */
368   protected boolean isOntologyTerm(String type, String... soTerm)
369   {
370     if (soTerm == null || soTerm.length == 0)
371     {
372       return true;
373     }
374     SequenceOntologyI so = SequenceOntologyFactory.getSequenceOntology();
375     for (String term : soTerm)
376     {
377       if (type.equals(term) || so.isA(type, term))
378       {
379         return true;
380       }
381     }
382     return false;
383   }
384
385   /**
386    * {@inheritDoc}
387    */
388   @Override
389   public float getMinimumScore(String type, boolean positional)
390   {
391     return featureStore.containsKey(type)
392             ? featureStore.get(type).getMinimumScore(positional)
393             : Float.NaN;
394   }
395
396   /**
397    * {@inheritDoc}
398    */
399   @Override
400   public float getMaximumScore(String type, boolean positional)
401   {
402     return featureStore.containsKey(type)
403             ? featureStore.get(type).getMaximumScore(positional)
404             : Float.NaN;
405   }
406
407   /**
408    * A convenience method to sort features by start position ascending (if on
409    * forward strand), or end position descending (if on reverse strand)
410    * 
411    * @param features
412    * @param forwardStrand
413    */
414   public static void sortFeatures(List<? extends IntervalI> features,
415           final boolean forwardStrand)
416   {
417     Collections.sort(features,
418             forwardStrand ? IntervalI.COMPARE_BEGIN_ASC
419                     : IntervalI.COMPARE_END_DESC);
420   }
421
422   /**
423    * {@inheritDoc} This method is 'semi-optimised': it only inspects features
424    * for types that include the specified group, but has to inspect every
425    * feature of those types for matching feature group. This is efficient unless
426    * a sequence has features that share the same type but are in different
427    * groups - an unlikely case.
428    * <p>
429    * For example, if RESNUM feature is created with group = PDBID, then features
430    * would only be retrieved for those sequences associated with the target
431    * PDBID (group).
432    */
433   @Override
434   public List<SequenceFeature> getFeaturesForGroup(boolean positional,
435           String group, String... type)
436   {
437     List<SequenceFeature> result = new ArrayList<>();
438     for (FeatureStore featureSet : varargToTypes(type))
439     {
440       if (featureSet.getFeatureGroups(positional).contains(group))
441       {
442         result.addAll(featureSet.getFeaturesForGroup(positional, group));
443       }
444     }
445     return result;
446   }
447
448   /**
449    * {@inheritDoc}
450    */
451   @Override
452   public boolean shiftFeatures(int fromPosition, int shiftBy)
453   {
454     boolean modified = false;
455     for (FeatureStore fs : featureStore.values())
456     {
457       modified |= fs.shiftFeatures(fromPosition, shiftBy);
458     }
459     return modified;
460   }
461
462   /**
463    * {@inheritDoc}
464    */
465   @Override
466   public void deleteAll()
467   {
468     featureStore.clear();
469   }
470
471   @Override
472   public List<SequenceFeature> findFeatures(int pos, String type,
473           List<SequenceFeature> list)
474   {
475     FeatureStore fs = featureStore.get(type);
476     if (fs == null)
477     {
478       return list == null ? new ArrayList<>() : list;
479     }
480     return fs.findFeatures(pos, pos, list);
481   }
482
483   @Override
484   public boolean hasFeatures(String type)
485   {
486     return featureStore.containsKey(type)
487             && !featureStore.get(type).isEmpty();
488   }
489
490 }