JAL-4028 experiment - THISISAPLACEHOLDER sequences get additional sequence data ...
[jalview.git] / src / jalview / datamodel / features / FeatureStore.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 java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Set;
28
29 import intervalstore.api.IntervalStoreI;
30 import intervalstore.impl.BinarySearcher;
31 import intervalstore.impl.BinarySearcher.Compare;
32 import intervalstore.impl.IntervalStore;
33 import jalview.datamodel.SequenceFeature;
34
35 /**
36  * A data store for a set of sequence features that supports efficient lookup of
37  * features overlapping a given range. Intended for (but not limited to) storage
38  * of features for one sequence and feature type.
39  * 
40  * @author gmcarstairs
41  *
42  */
43 public class FeatureStore
44 {
45   /*
46    * Non-positional features have no (zero) start/end position.
47    * Kept as a separate list in case this criterion changes in future.
48    */
49   List<SequenceFeature> nonPositionalFeatures;
50
51   /*
52    * contact features ordered by first contact position
53    */
54   List<SequenceFeature> contactFeatureStarts;
55
56   /*
57    * contact features ordered by second contact position
58    */
59   List<SequenceFeature> contactFeatureEnds;
60
61   /*
62    * IntervalStore holds remaining features and provides efficient
63    * query for features overlapping any given interval
64    */
65   IntervalStoreI<SequenceFeature> features;
66
67   /*
68    * Feature groups represented in stored positional features 
69    * (possibly including null)
70    */
71   Set<String> positionalFeatureGroups;
72
73   /*
74    * Feature groups represented in stored non-positional features 
75    * (possibly including null)
76    */
77   Set<String> nonPositionalFeatureGroups;
78
79   /*
80    * the total length of all positional features; contact features count 1 to
81    * the total and 1 to size(), consistent with an average 'feature length' of 1
82    */
83   int totalExtent;
84
85   /*
86    * range covered by positional features
87    */
88   int minExtent, maxExtent;
89
90   float positionalMinScore;
91
92   float positionalMaxScore;
93
94   float nonPositionalMinScore;
95
96   float nonPositionalMaxScore;
97
98   /**
99    * Constructor
100    */
101   public FeatureStore()
102   {
103     features = new IntervalStore<>();
104     positionalFeatureGroups = new HashSet<>();
105     nonPositionalFeatureGroups = new HashSet<>();
106     positionalMinScore = Float.NaN;
107     positionalMaxScore = Float.NaN;
108     nonPositionalMinScore = Float.NaN;
109     nonPositionalMaxScore = Float.NaN;
110     minExtent = 0;
111     maxExtent = 0;
112
113     // we only construct nonPositionalFeatures, contactFeatures if we need to
114   }
115
116   /**
117    * Adds one sequence feature to the store, and returns true, unless the
118    * feature is already contained in the store, in which case this method
119    * returns false. Containment is determined by SequenceFeature.equals()
120    * comparison.
121    * 
122    * @param feature
123    */
124   public boolean addFeature(SequenceFeature feature)
125   {
126     if (contains(feature))
127     {
128       return false;
129     }
130
131     /*
132      * keep a record of feature groups
133      */
134     if (!feature.isNonPositional())
135     {
136       positionalFeatureGroups.add(feature.getFeatureGroup());
137       if (minExtent == 0)
138       {
139         minExtent = feature.begin;
140         maxExtent = feature.end;
141       }
142       else
143       {
144         minExtent = Math.min(feature.begin, minExtent);
145         maxExtent = Math.max(feature.end, maxExtent);
146       }
147     }
148
149     if (feature.isContactFeature())
150     {
151       addContactFeature(feature);
152     }
153     else if (feature.isNonPositional())
154     {
155       addNonPositionalFeature(feature);
156     }
157     else
158     {
159       addNestedFeature(feature);
160     }
161
162     /*
163      * record the total extent of positional features, to make
164      * getTotalFeatureLength possible; we count the length of a 
165      * contact feature as 1
166      */
167     totalExtent += getFeatureLength(feature);
168
169     /*
170      * record the minimum and maximum score for positional
171      * and non-positional features
172      */
173     float score = feature.getScore();
174     if (!Float.isNaN(score))
175     {
176       if (feature.isNonPositional())
177       {
178         nonPositionalMinScore = min(nonPositionalMinScore, score);
179         nonPositionalMaxScore = max(nonPositionalMaxScore, score);
180       }
181       else
182       {
183         positionalMinScore = min(positionalMinScore, score);
184         positionalMaxScore = max(positionalMaxScore, score);
185       }
186     }
187
188     return true;
189   }
190
191   /**
192    * Answers true if this store contains the given feature (testing by
193    * SequenceFeature.equals), else false
194    * 
195    * @param feature
196    * @return
197    */
198   public boolean contains(SequenceFeature feature)
199   {
200     if (feature.isNonPositional())
201     {
202       return nonPositionalFeatures == null ? false
203               : nonPositionalFeatures.contains(feature);
204     }
205
206     if (feature.isContactFeature())
207     {
208       return contactFeatureStarts == null ? false
209               : listContains(contactFeatureStarts, feature);
210     }
211
212     return features == null ? false : features.contains(feature);
213   }
214
215   /**
216    * Answers the 'length' of the feature, counting 0 for non-positional features
217    * and 1 for contact features
218    * 
219    * @param feature
220    * @return
221    */
222   protected static int getFeatureLength(SequenceFeature feature)
223   {
224     if (feature.isNonPositional())
225     {
226       return 0;
227     }
228     if (feature.isContactFeature())
229     {
230       return 1;
231     }
232     return 1 + feature.getEnd() - feature.getBegin();
233   }
234
235   /**
236    * Adds the feature to the list of non-positional features (with lazy
237    * instantiation of the list if it is null), and returns true. The feature
238    * group is added to the set of distinct feature groups for non-positional
239    * features. This method allows duplicate features, so test before calling to
240    * prevent this.
241    * 
242    * @param feature
243    */
244   protected boolean addNonPositionalFeature(SequenceFeature feature)
245   {
246     if (nonPositionalFeatures == null)
247     {
248       nonPositionalFeatures = new ArrayList<>();
249     }
250
251     nonPositionalFeatures.add(feature);
252
253     nonPositionalFeatureGroups.add(feature.getFeatureGroup());
254
255     return true;
256   }
257
258   /**
259    * Adds one feature to the IntervalStore that can manage nested features
260    * (creating the IntervalStore if necessary)
261    */
262   protected synchronized void addNestedFeature(SequenceFeature feature)
263   {
264     if (features == null)
265     {
266       features = new IntervalStore<>();
267     }
268     features.add(feature);
269   }
270
271   /**
272    * Add a contact feature to the lists that hold them ordered by start (first
273    * contact) and by end (second contact) position, ensuring the lists remain
274    * ordered, and returns true. This method allows duplicate features to be
275    * added, so test before calling to avoid this.
276    * 
277    * @param feature
278    * @return
279    */
280   protected synchronized boolean addContactFeature(SequenceFeature feature)
281   {
282     if (contactFeatureStarts == null)
283     {
284       contactFeatureStarts = new ArrayList<>();
285     }
286     if (contactFeatureEnds == null)
287     {
288       contactFeatureEnds = new ArrayList<>();
289     }
290
291     /*
292      * insert into list sorted by start (first contact position):
293      * binary search the sorted list to find the insertion point
294      */
295     int insertPosition = BinarySearcher.findFirst(contactFeatureStarts,
296             true, Compare.GE, feature.getBegin());
297     contactFeatureStarts.add(insertPosition, feature);
298
299     /*
300      * insert into list sorted by end (second contact position):
301      * binary search the sorted list to find the insertion point
302      */
303     insertPosition = BinarySearcher.findFirst(contactFeatureEnds, false,
304             Compare.GE, feature.getEnd());
305     contactFeatureEnds.add(insertPosition, feature);
306
307     return true;
308   }
309
310   /**
311    * Answers true if the list contains the feature, else false. This method is
312    * optimised for the condition that the list is sorted on feature start
313    * position ascending, and will give unreliable results if this does not hold.
314    * 
315    * @param features
316    * @param feature
317    * @return
318    */
319   protected static boolean listContains(List<SequenceFeature> features,
320           SequenceFeature feature)
321   {
322     if (features == null || feature == null)
323     {
324       return false;
325     }
326
327     /*
328      * locate the first entry in the list which does not precede the feature
329      */
330     // int pos = binarySearch(features,
331     // SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION));
332     int pos = BinarySearcher.findFirst(features, true, Compare.GE,
333             feature.getBegin());
334     int len = features.size();
335     while (pos < len)
336     {
337       SequenceFeature sf = features.get(pos);
338       if (sf.getBegin() > feature.getBegin())
339       {
340         return false; // no match found
341       }
342       if (sf.equals(feature))
343       {
344         return true;
345       }
346       pos++;
347     }
348     return false;
349   }
350
351   /**
352    * Returns a (possibly empty) list of features whose extent overlaps the given
353    * range. The returned list is not ordered. Contact features are included if
354    * either of the contact points lies within the range.
355    * 
356    * @param start
357    *          start position of overlap range (inclusive)
358    * @param end
359    *          end position of overlap range (inclusive)
360    * @return
361    */
362   public List<SequenceFeature> findOverlappingFeatures(long start, long end)
363   {
364     List<SequenceFeature> result = new ArrayList<>();
365
366     findContactFeatures(start, end, result);
367
368     if (features != null)
369     {
370       result.addAll(features.findOverlaps(start, end));
371     }
372
373     return result;
374   }
375
376   /**
377    * Adds contact features to the result list where either the second or the
378    * first contact position lies within the target range
379    * 
380    * @param from
381    * @param to
382    * @param result
383    */
384   protected void findContactFeatures(long from, long to,
385           List<SequenceFeature> result)
386   {
387     if (contactFeatureStarts != null)
388     {
389       findContactStartOverlaps(from, to, result);
390     }
391     if (contactFeatureEnds != null)
392     {
393       findContactEndOverlaps(from, to, result);
394     }
395   }
396
397   /**
398    * Adds to the result list any contact features whose end (second contact
399    * point), but not start (first contact point), lies in the query from-to
400    * range
401    * 
402    * @param from
403    * @param to
404    * @param result
405    */
406   protected void findContactEndOverlaps(long from, long to,
407           List<SequenceFeature> result)
408   {
409     /*
410      * find the first contact feature (if any) 
411      * whose end point is not before the target range
412      */
413     int index = BinarySearcher.findFirst(contactFeatureEnds, false,
414             Compare.GE, (int) from);
415
416     while (index < contactFeatureEnds.size())
417     {
418       SequenceFeature sf = contactFeatureEnds.get(index);
419       if (!sf.isContactFeature())
420       {
421         System.err.println("Error! non-contact feature type " + sf.getType()
422                 + " in contact features list");
423         index++;
424         continue;
425       }
426
427       int begin = sf.getBegin();
428       if (begin >= from && begin <= to)
429       {
430         /*
431          * this feature's first contact position lies in the search range
432          * so we don't include it in results a second time
433          */
434         index++;
435         continue;
436       }
437
438       if (sf.getEnd() > to)
439       {
440         /*
441          * this feature (and all following) has end point after the target range
442          */
443         break;
444       }
445
446       /*
447        * feature has end >= from and end <= to
448        * i.e. contact end point lies within overlap search range
449        */
450       result.add(sf);
451       index++;
452     }
453   }
454
455   /**
456    * Adds contact features whose start position lies in the from-to range to the
457    * result list
458    * 
459    * @param from
460    * @param to
461    * @param result
462    */
463   protected void findContactStartOverlaps(long from, long to,
464           List<SequenceFeature> result)
465   {
466     int index = BinarySearcher.findFirst(contactFeatureStarts, true,
467             Compare.GE, (int) from);
468
469     while (index < contactFeatureStarts.size())
470     {
471       SequenceFeature sf = contactFeatureStarts.get(index);
472       if (!sf.isContactFeature())
473       {
474         System.err.println("Error! non-contact feature " + sf.toString()
475                 + " in contact features list");
476         index++;
477         continue;
478       }
479       if (sf.getBegin() > to)
480       {
481         /*
482          * this feature's start (and all following) follows the target range
483          */
484         break;
485       }
486
487       /*
488        * feature has begin >= from and begin <= to
489        * i.e. contact start point lies within overlap search range
490        */
491       result.add(sf);
492       index++;
493     }
494   }
495
496   /**
497    * Answers a list of all positional features stored, in no guaranteed order
498    * 
499    * @return
500    */
501   public List<SequenceFeature> getPositionalFeatures()
502   {
503     List<SequenceFeature> result = new ArrayList<>();
504
505     /*
506      * add any contact features - from the list by start position
507      */
508     if (contactFeatureStarts != null)
509     {
510       result.addAll(contactFeatureStarts);
511     }
512
513     /*
514      * add any nested features
515      */
516     if (features != null)
517     {
518       result.addAll(features);
519     }
520
521     return result;
522   }
523
524   /**
525    * Answers a list of all contact features. If there are none, returns an
526    * immutable empty list.
527    * 
528    * @return
529    */
530   public List<SequenceFeature> getContactFeatures()
531   {
532     if (contactFeatureStarts == null)
533     {
534       return Collections.emptyList();
535     }
536     return new ArrayList<>(contactFeatureStarts);
537   }
538
539   /**
540    * Answers a list of all non-positional features. If there are none, returns
541    * an immutable empty list.
542    * 
543    * @return
544    */
545   public List<SequenceFeature> getNonPositionalFeatures()
546   {
547     if (nonPositionalFeatures == null)
548     {
549       return Collections.emptyList();
550     }
551     return new ArrayList<>(nonPositionalFeatures);
552   }
553
554   /**
555    * Deletes the given feature from the store, returning true if it was found
556    * (and deleted), else false. This method makes no assumption that the feature
557    * is in the 'expected' place in the store, in case it has been modified since
558    * it was added.
559    * 
560    * @param sf
561    */
562   public synchronized boolean delete(SequenceFeature sf)
563   {
564     boolean removed = false;
565
566     /*
567      * try contact positions (and if found, delete
568      * from both lists of contact positions)
569      */
570     if (!removed && contactFeatureStarts != null)
571     {
572       removed = contactFeatureStarts.remove(sf);
573       if (removed)
574       {
575         contactFeatureEnds.remove(sf);
576       }
577     }
578
579     boolean removedNonPositional = false;
580
581     /*
582      * if not found, try non-positional features
583      */
584     if (!removed && nonPositionalFeatures != null)
585     {
586       removedNonPositional = nonPositionalFeatures.remove(sf);
587       removed = removedNonPositional;
588     }
589
590     /*
591      * if not found, try nested features
592      */
593     if (!removed && features != null)
594     {
595       removed = features.remove(sf);
596     }
597
598     if (removed)
599     {
600       rescanAfterDelete();
601     }
602
603     return removed;
604   }
605
606   /**
607    * Rescan all features to recompute any cached values after an entry has been
608    * deleted. This is expected to be an infrequent event, so performance here is
609    * not critical.
610    */
611   protected synchronized void rescanAfterDelete()
612   {
613     positionalFeatureGroups.clear();
614     nonPositionalFeatureGroups.clear();
615     totalExtent = 0;
616     positionalMinScore = Float.NaN;
617     positionalMaxScore = Float.NaN;
618     nonPositionalMinScore = Float.NaN;
619     nonPositionalMaxScore = Float.NaN;
620
621     /*
622      * scan non-positional features for groups and scores
623      */
624     for (SequenceFeature sf : getNonPositionalFeatures())
625     {
626       nonPositionalFeatureGroups.add(sf.getFeatureGroup());
627       float score = sf.getScore();
628       nonPositionalMinScore = min(nonPositionalMinScore, score);
629       nonPositionalMaxScore = max(nonPositionalMaxScore, score);
630     }
631
632     /*
633      * scan positional features for groups, scores and extents
634      */
635     for (SequenceFeature sf : getPositionalFeatures())
636     {
637       positionalFeatureGroups.add(sf.getFeatureGroup());
638       float score = sf.getScore();
639       positionalMinScore = min(positionalMinScore, score);
640       positionalMaxScore = max(positionalMaxScore, score);
641       if (totalExtent == 0)
642       {
643         minExtent = sf.begin;
644         maxExtent = sf.begin;
645       }
646       else
647       {
648         minExtent = Math.min(sf.begin, minExtent);
649         maxExtent = Math.max(sf.end, maxExtent);
650       }
651       totalExtent += getFeatureLength(sf);
652     }
653   }
654
655   /**
656    * A helper method to return the minimum of two floats, where a non-NaN value
657    * is treated as 'less than' a NaN value (unlike Math.min which does the
658    * opposite)
659    * 
660    * @param f1
661    * @param f2
662    */
663   protected static float min(float f1, float f2)
664   {
665     if (Float.isNaN(f1))
666     {
667       return Float.isNaN(f2) ? f1 : f2;
668     }
669     else
670     {
671       return Float.isNaN(f2) ? f1 : Math.min(f1, f2);
672     }
673   }
674
675   /**
676    * A helper method to return the maximum of two floats, where a non-NaN value
677    * is treated as 'greater than' a NaN value (unlike Math.max which does the
678    * opposite)
679    * 
680    * @param f1
681    * @param f2
682    */
683   protected static float max(float f1, float f2)
684   {
685     if (Float.isNaN(f1))
686     {
687       return Float.isNaN(f2) ? f1 : f2;
688     }
689     else
690     {
691       return Float.isNaN(f2) ? f1 : Math.max(f1, f2);
692     }
693   }
694
695   /**
696    * Answers true if this store has no features, else false
697    * 
698    * @return
699    */
700   public boolean isEmpty()
701   {
702     boolean hasFeatures = (contactFeatureStarts != null
703             && !contactFeatureStarts.isEmpty())
704             || (nonPositionalFeatures != null
705                     && !nonPositionalFeatures.isEmpty())
706             || (features != null && features.size() > 0);
707
708     return !hasFeatures;
709   }
710
711   /**
712    * Answers the set of distinct feature groups stored, possibly including null,
713    * as an unmodifiable view of the set. The parameter determines whether the
714    * groups for positional or for non-positional features are returned.
715    * 
716    * @param positionalFeatures
717    * @return
718    */
719   public Set<String> getFeatureGroups(boolean positionalFeatures)
720   {
721     if (positionalFeatures)
722     {
723       return Collections.unmodifiableSet(positionalFeatureGroups);
724     }
725     else
726     {
727       return nonPositionalFeatureGroups == null
728               ? Collections.<String> emptySet()
729               : Collections.unmodifiableSet(nonPositionalFeatureGroups);
730     }
731   }
732
733   /**
734    * Answers the number of positional (or non-positional) features stored.
735    * Contact features count as 1.
736    * 
737    * @param positional
738    * @return
739    */
740   public int getFeatureCount(boolean positional)
741   {
742     if (!positional)
743     {
744       return nonPositionalFeatures == null ? 0
745               : nonPositionalFeatures.size();
746     }
747
748     int size = 0;
749
750     if (contactFeatureStarts != null)
751     {
752       // note a contact feature (start/end) counts as one
753       size += contactFeatureStarts.size();
754     }
755
756     if (features != null)
757     {
758       size += features.size();
759     }
760
761     return size;
762   }
763
764   /**
765    * Answers the total length of positional features (or zero if there are
766    * none). Contact features contribute a value of 1 to the total.
767    * 
768    * @return
769    */
770   public int getTotalFeatureLength()
771   {
772     return totalExtent;
773   }
774
775   /**
776    * Answers the minimum score held for positional or non-positional features.
777    * This may be Float.NaN if there are no features, are none has a non-NaN
778    * score.
779    * 
780    * @param positional
781    * @return
782    */
783   public float getMinimumScore(boolean positional)
784   {
785     return positional ? positionalMinScore : nonPositionalMinScore;
786   }
787
788   /**
789    * Answers the maximum score held for positional or non-positional features.
790    * This may be Float.NaN if there are no features, are none has a non-NaN
791    * score.
792    * 
793    * @param positional
794    * @return
795    */
796   public float getMaximumScore(boolean positional)
797   {
798     return positional ? positionalMaxScore : nonPositionalMaxScore;
799   }
800
801   /**
802    * Answers a list of all either positional or non-positional features whose
803    * feature group matches the given group (which may be null)
804    * 
805    * @param positional
806    * @param group
807    * @return
808    */
809   public List<SequenceFeature> getFeaturesForGroup(boolean positional,
810           String group)
811   {
812     List<SequenceFeature> result = new ArrayList<>();
813
814     /*
815      * if we know features don't include the target group, no need
816      * to inspect them for matches
817      */
818     if (positional && !positionalFeatureGroups.contains(group)
819             || !positional && !nonPositionalFeatureGroups.contains(group))
820     {
821       return result;
822     }
823
824     List<SequenceFeature> sfs = positional ? getPositionalFeatures()
825             : getNonPositionalFeatures();
826     for (SequenceFeature sf : sfs)
827     {
828       String featureGroup = sf.getFeatureGroup();
829       if (group == null && featureGroup == null
830               || group != null && group.equals(featureGroup))
831       {
832         result.add(sf);
833       }
834     }
835     return result;
836   }
837
838   /**
839    * Adds the shift amount to the start and end of all positional features whose
840    * start position is at or after fromPosition. Returns true if at least one
841    * feature was shifted, else false.
842    * 
843    * @param fromPosition
844    * @param shiftBy
845    * @return
846    */
847   public synchronized boolean shiftFeatures(int fromPosition, int shiftBy)
848   {
849     /*
850      * Because begin and end are final fields (to ensure the data store's
851      * integrity), we have to delete each feature and re-add it as amended.
852      * (Although a simple shift of all values would preserve data integrity!)
853      */
854     boolean modified = false;
855     for (SequenceFeature sf : getPositionalFeatures())
856     {
857       if (sf.getBegin() >= fromPosition)
858       {
859         modified = true;
860         int newBegin = sf.getBegin() + shiftBy;
861         int newEnd = sf.getEnd() + shiftBy;
862
863         /*
864          * sanity check: don't shift left of the first residue
865          */
866         if (newEnd > 0)
867         {
868           newBegin = Math.max(1, newBegin);
869           SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd,
870                   sf.getFeatureGroup(), sf.getScore());
871           addFeature(sf2);
872         }
873         delete(sf);
874       }
875     }
876     return modified;
877   }
878
879   public int getBegin()
880   {
881     return minExtent;
882   }
883
884   public int getEnd()
885   {
886     return maxExtent;
887   }
888 }