6204f183b1f176cf0c1732cdcf2b6910eb20c1b9
[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   /**
359    * A helper method that adds to the result list any features from the
360    * collection provided whose feature group matches the specified group
361    * 
362    * @param group
363    * @param sfs
364    * @param result
365    */
366   private void addFeaturesForGroup(String group,
367           Collection<SequenceFeature> sfs, List<SequenceFeature> result)
368   {
369     if (sfs == null)
370     {
371       return;
372     }
373     for (SequenceFeature sf : sfs)
374     {
375       String featureGroup = sf.getFeatureGroup();
376       if (group == null && featureGroup == null
377               || group != null && group.equals(featureGroup))
378       {
379         result.add(sf);
380       }
381     }
382   }
383
384   /**
385    * Adds the feature to the list of non-positional features (with lazy
386    * instantiation of the list if it is null), and returns true. The feature
387    * group is added to the set of distinct feature groups for non-positional
388    * features. This method allows duplicate features, so test before calling to
389    * prevent this.
390    * 
391    * @param feature
392    */
393   protected boolean addNonPositionalFeature(SequenceFeature feature)
394   {
395     if (nonPositionalFeatures == null)
396     {
397       nonPositionalFeatures = new ArrayList<>();
398     }
399
400     nonPositionalFeatures.add(feature);
401
402     nonPositionalFeatureGroups.add(feature.getFeatureGroup());
403
404     return true;
405   }
406
407   /**
408    * Answers true if this store contains the given feature (testing by
409    * SequenceFeature.equals), else false
410    * 
411    * @param feature
412    * @return
413    */
414   public boolean contains(SequenceFeature feature)
415   {
416     if (feature.isNonPositional())
417     {
418       return containsNonPositionalFeature(feature);
419     }
420
421     if (feature.isContactFeature())
422     {
423       return containsContactFeature(feature);
424     }
425
426     return containsPositionalFeature(feature);
427   }
428
429   private boolean containsPositionalFeature(SequenceFeature feature)
430   {
431     return features == null || feature.begin > maxStart ? false
432             : features.contains(feature);
433   }
434
435   /**
436    * Answers true if this store already contains a contact feature equal to the
437    * given feature (by {@code SequenceFeature.equals()} test), else false
438    * 
439    * @param feature
440    * @return
441    */
442   private boolean containsContactFeature(SequenceFeature feature)
443   {
444     return contactFeatureStarts != null && feature.begin <= maxContactStart
445             && listContains(contactFeatureStarts, feature);
446   }
447
448   /**
449    * Answers true if this store already contains a non-positional feature equal
450    * to the given feature (by {@code SequenceFeature.equals()} test), else false
451    * 
452    * @param feature
453    * @return
454    */
455   private boolean containsNonPositionalFeature(SequenceFeature feature)
456   {
457     return nonPositionalFeatures == null ? false
458             : nonPositionalFeatures.contains(feature);
459   }
460
461   /**
462    * Deletes the given feature from the store, returning true if it was found
463    * (and deleted), else false. This method makes no assumption that the feature
464    * is in the 'expected' place in the store, in case it has been modified since
465    * it was added.
466    * 
467    * @param sf
468    */
469   public synchronized boolean delete(SequenceFeature sf)
470   {
471     boolean removed = false;
472
473     /*
474      * try contact positions (and if found, delete
475      * from both lists of contact positions)
476      */
477     if (!removed && contactFeatureStarts != null)
478     {
479       removed = contactFeatureStarts.remove(sf);
480       if (removed)
481       {
482         contactFeatureEnds.remove(sf);
483       }
484     }
485
486     /*
487      * if not found, try non-positional features
488      */
489     if (!removed && nonPositionalFeatures != null)
490     {
491       removed = nonPositionalFeatures.remove(sf);
492     }
493
494     /*
495      * if not found, try nested features
496      */
497     if (!removed && features != null)
498     {
499       removed = features.remove(sf);
500     }
501
502     if (removed)
503     {
504       rescanAfterDelete();
505     }
506
507     return removed;
508   }
509
510   public List<SequenceFeature> findFeatures(long start, long end)
511   {
512     return findFeatures(start, end, null);
513   }
514
515   /**
516    * Returns a (possibly empty) list of features whose extent overlaps the given
517    * range. The returned list is not ordered. Contact features are included if
518    * either of the contact points lies within the range. If the {@code result}
519    * parameter is not null, new entries are added to this list and the (possibly
520    * extended) list returned.
521    * 
522    * @param start
523    *          start position of overlap range (inclusive)
524    * @param end
525    *          end position of overlap range (inclusive)
526    * @param result
527    * @return
528    */
529   public List<SequenceFeature> findFeatures(long start, long end,
530           List<SequenceFeature> result)
531   {
532     if (result == null)
533     {
534       result = new ArrayList<>();
535     }
536
537     findContactFeatures(start, end, result);
538     features.findOverlaps(start, end, result);
539
540     return result;
541   }
542
543   public List<SequenceFeature> getContactFeatures()
544   {
545     return getContactFeatures(new ArrayList<>());
546   }
547
548   /**
549    * Answers a list of all contact features. If there are none, returns an
550    * immutable empty list.
551    * 
552    * @return
553    */
554   public List<SequenceFeature> getContactFeatures(
555           List<SequenceFeature> result)
556   {
557     if (contactFeatureStarts != null)
558     {
559       result.addAll(contactFeatureStarts);
560     }
561     return result;
562   }
563
564   /**
565    * Answers the number of positional (or non-positional) features stored.
566    * Contact features count as 1.
567    * 
568    * @param positional
569    * @return
570    */
571   public int getFeatureCount(boolean positional)
572   {
573     if (!positional)
574     {
575       return nonPositionalFeatures == null ? 0
576               : nonPositionalFeatures.size();
577     }
578
579     int size = 0;
580
581     if (contactFeatureStarts != null)
582     {
583       // note a contact feature (start/end) counts as one
584       size += contactFeatureStarts.size();
585     }
586
587     if (features != null)
588     {
589       size += features.size();
590     }
591     return size;
592   }
593
594   /**
595    * Answers the set of distinct feature groups stored, possibly including null,
596    * as an unmodifiable view of the set. The parameter determines whether the
597    * groups for positional or for non-positional features are returned.
598    * 
599    * @param positionalFeatures
600    * @return
601    */
602   public Set<String> getFeatureGroups(boolean positionalFeatures)
603   {
604     if (positionalFeatures)
605     {
606       return Collections.unmodifiableSet(positionalFeatureGroups);
607     }
608     else
609     {
610       return nonPositionalFeatureGroups == null
611               ? Collections.<String> emptySet()
612               : Collections.unmodifiableSet(nonPositionalFeatureGroups);
613     }
614   }
615
616   /**
617    * Answers a list of all either positional or non-positional features whose
618    * feature group matches the given group (which may be null)
619    * 
620    * @param positional
621    * @param group
622    * @return
623    */
624   public List<SequenceFeature> getFeaturesForGroup(boolean positional,
625           String group)
626   {
627     List<SequenceFeature> result = new ArrayList<>();
628
629     /*
630      * if we know features don't include the target group, no need
631      * to inspect them for matches
632      */
633     if (positional && !positionalFeatureGroups.contains(group)
634             || !positional && !nonPositionalFeatureGroups.contains(group))
635     {
636       return result;
637     }
638
639     if (positional)
640     {
641       addFeaturesForGroup(group, contactFeatureStarts, result);
642       addFeaturesForGroup(group, features, result);
643     }
644     else
645     {
646       addFeaturesForGroup(group, nonPositionalFeatures, result);
647     }
648     return result;
649   }
650
651   /**
652    * Answers the maximum score held for positional or non-positional features.
653    * This may be Float.NaN if there are no features, are none has a non-NaN
654    * score.
655    * 
656    * @param positional
657    * @return
658    */
659   public float getMaximumScore(boolean positional)
660   {
661     return positional ? positionalMaxScore : nonPositionalMaxScore;
662   }
663
664   /**
665    * Answers the minimum score held for positional or non-positional features.
666    * This may be Float.NaN if there are no features, are none has a non-NaN
667    * score.
668    * 
669    * @param positional
670    * @return
671    */
672   public float getMinimumScore(boolean positional)
673   {
674     return positional ? positionalMinScore : nonPositionalMinScore;
675   }
676
677   /**
678    * Answers a (possibly empty) list of all non-positional features
679    * 
680    * @return
681    */
682   public List<SequenceFeature> getNonPositionalFeatures()
683   {
684     List<SequenceFeature> result = new ArrayList<>();
685     getNonPositionalFeatures(result);
686     return result;
687   }
688
689   /**
690    * Adds any stored non-positional features to the result list
691    * 
692    * @return
693    */
694   public void getNonPositionalFeatures(List<SequenceFeature> result)
695   {
696     if (nonPositionalFeatures != null)
697     {
698       result.addAll(nonPositionalFeatures);
699     }
700   }
701
702   /**
703    * Returns a (possibly empty) list of all positional features stored
704    * 
705    * @return
706    */
707   public List<SequenceFeature> getPositionalFeatures()
708   {
709     List<SequenceFeature> result = new ArrayList<>();
710     getPositionalFeatures(result);
711
712     return result;
713   }
714
715   /**
716    * Adds all positional features stored to the result list, in no guaranteed
717    * order, and with no check for duplicates
718    */
719   public void getPositionalFeatures(List<SequenceFeature> result)
720   {
721     /*
722      * add any contact features - from the list by start position
723      */
724     if (contactFeatureStarts != null)
725     {
726       result.addAll(contactFeatureStarts);
727     }
728
729     /*
730      * add any nested features
731      */
732     if (features != null)
733     {
734       result.addAll(features);
735     }
736   }
737
738   /**
739    * Answers the total length of positional features (or zero if there are
740    * none). Contact features contribute a value of 1 to the total.
741    * 
742    * @return
743    */
744   public int getTotalFeatureLength()
745   {
746     return totalExtent;
747   }
748
749   /**
750    * Answers true if this store has no features, else false
751    * 
752    * @return
753    */
754   public boolean isEmpty()
755   {
756     boolean hasFeatures = (contactFeatureStarts != null
757             && !contactFeatureStarts.isEmpty())
758             || (nonPositionalFeatures != null
759                     && !nonPositionalFeatures.isEmpty())
760             || (features != null && features.size() > 0);
761
762     return !hasFeatures;
763   }
764
765   /**
766    * Rescan all features to recompute any cached values after an entry has been
767    * deleted. This is expected to be an infrequent event, so performance here is
768    * not critical.
769    */
770   protected synchronized void rescanAfterDelete()
771   {
772     positionalFeatureGroups.clear();
773     nonPositionalFeatureGroups.clear();
774     totalExtent = 0;
775     positionalMinScore = Float.NaN;
776     positionalMaxScore = Float.NaN;
777     nonPositionalMinScore = Float.NaN;
778     nonPositionalMaxScore = Float.NaN;
779     /*
780      * scan non-positional features for groups and scores
781      */
782     if (nonPositionalFeatures != null)
783     {
784       for (int i = 0, n = nonPositionalFeatures.size(); i < n; i++)
785       {
786         SequenceFeature sf = nonPositionalFeatures.get(i);
787         nonPositionalFeatureGroups.add(sf.getFeatureGroup());
788         float score = sf.getScore();
789         nonPositionalMinScore = min(nonPositionalMinScore, score);
790         nonPositionalMaxScore = max(nonPositionalMaxScore, score);
791       }
792     }
793
794     rescanPositional(contactFeatureStarts);
795     rescanPositional(features);
796   }
797
798   /**
799    * Scans the given features and updates cached feature groups, minimum and
800    * maximum feature score, and total feature extent (length) for positional
801    * features
802    * 
803    * @param sfs
804    */
805   private void rescanPositional(Collection<SequenceFeature> sfs)
806   {
807     if (sfs == null)
808     {
809       return;
810     }
811     for (SequenceFeature sf : sfs)
812     {
813       positionalFeatureGroups.add(sf.getFeatureGroup());
814       float score = sf.getScore();
815       positionalMinScore = min(positionalMinScore, score);
816       positionalMaxScore = max(positionalMaxScore, score);
817       totalExtent += getFeatureLength(sf);
818     }
819   }
820
821   /**
822    * Adds the shift amount to the start and end of all positional features whose
823    * start position is at or after fromPosition. Returns true if at least one
824    * feature was shifted, else false.
825    * 
826    * @param fromPosition
827    * @param shiftBy
828    * @return
829    */
830   public synchronized boolean shiftFeatures(int fromPosition, int shiftBy)
831   {
832     /*
833      * Because begin and end are final fields (to ensure the data store's
834      * integrity), we have to delete each feature and re-add it as amended.
835      * (Although a simple shift of all values would preserve data integrity!)
836      */
837     boolean modified = false;
838     List<SequenceFeature> list = getPositionalFeatures();
839     for (int i = 0, n = list.size(); i < n; i++)
840     {
841       SequenceFeature sf = list.get(i);
842       if (sf.getBegin() >= fromPosition)
843       {
844         modified = true;
845         int newBegin = sf.getBegin() + shiftBy;
846         int newEnd = sf.getEnd() + shiftBy;
847
848         /*
849          * sanity check: don't shift left of the first residue
850          */
851         if (newEnd > 0)
852         {
853           newBegin = Math.max(1, newBegin);
854           SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd,
855                   sf.getFeatureGroup(), sf.getScore());
856           addFeature(sf2);
857         }
858         delete(sf);
859       }
860     }
861     return modified;
862   }
863
864   /**
865    * Answers the position (0, 1...) in the list of the first entry whose end
866    * position is not less than {@ pos}. If no such entry is found, answers the
867    * length of the list.
868    * 
869    * @param list
870    * @param pos
871    * @return
872    */
873   protected int findFirstEnd(List<SequenceFeature> list, long pos)
874   {
875     return BinarySearcher.findFirst(list, false, Compare.GE, (int) pos);
876   }
877
878   /**
879    * Adds contact features to the result list where either the second or the
880    * first contact position lies within the target range
881    * 
882    * @param from
883    * @param to
884    * @param result
885    */
886   protected void findContactFeatures(long from, long to,
887           List<SequenceFeature> result)
888   {
889     if (contactFeatureStarts != null)
890     {
891       findContactStartOverlaps(from, to, result);
892       findContactEndOverlaps(from, to, result);
893     }
894   }
895
896   /**
897    * Adds to the result list any contact features whose end (second contact
898    * point), but not start (first contact point), lies in the query from-to
899    * range
900    * 
901    * @param from
902    * @param to
903    * @param result
904    */
905   private void findContactEndOverlaps(long from, long to,
906           List<SequenceFeature> result)
907   {
908     /*
909      * find the first contact feature (if any) 
910      * whose end point is not before the target range
911      */
912     int index = findFirstEnd(contactFeatureEnds, from);
913
914     int n = contactFeatureEnds.size();
915     while (index < n)
916     {
917       SequenceFeature sf = contactFeatureEnds.get(index);
918       if (!sf.isContactFeature())
919       {
920         System.err.println("Error! non-contact feature type " + sf.getType()
921                 + " in contact features list");
922         index++;
923         continue;
924       }
925
926       int begin = sf.getBegin();
927       if (begin >= from && begin <= to)
928       {
929         /*
930          * this feature's first contact position lies in the search range
931          * so we don't include it in results a second time
932          */
933         index++;
934         continue;
935       }
936
937       if (sf.getEnd() > to)
938       {
939         /*
940          * this feature (and all following) has end point after the target range
941          */
942         break;
943       }
944
945       /*
946        * feature has end >= from and end <= to
947        * i.e. contact end point lies within overlap search range
948        */
949       result.add(sf);
950       index++;
951     }
952   }
953
954   /**
955    * Adds contact features whose start position lies in the from-to range to the
956    * result list
957    * 
958    * @param from
959    * @param to
960    * @param result
961    */
962   private void findContactStartOverlaps(long from, long to,
963           List<SequenceFeature> result)
964   {
965     int index = BinarySearcher.findFirst(contactFeatureStarts, true,
966             Compare.GE, (int) from);
967
968     while (index < contactFeatureStarts.size())
969     {
970       SequenceFeature sf = contactFeatureStarts.get(index);
971       if (!sf.isContactFeature())
972       {
973         System.err.println("Error! non-contact feature " + sf.toString()
974                 + " in contact features list");
975         index++;
976         continue;
977       }
978       if (sf.getBegin() > to)
979       {
980         /*
981          * this feature's start (and all following) follows the target range
982          */
983         break;
984       }
985
986       /*
987        * feature has begin >= from and begin <= to
988        * i.e. contact start point lies within overlap search range
989        */
990       result.add(sf);
991       index++;
992     }
993   }
994
995 }