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