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