Merge remote-tracking branch 'origin/bug/JAL-2722' into portforward/JAL-2675_2102b1to2103 portforward/JAL-2675_2102b1to2103
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 6 Sep 2017 10:43:37 +0000 (11:43 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 6 Sep 2017 10:43:37 +0000 (11:43 +0100)
173 files changed:
RELEASE
build.xml
examples/example.json
help/html/features/overview.gif [deleted file]
help/html/features/overview.html
help/html/features/overview.png [new file with mode: 0644]
help/html/releases.html
help/html/whatsNew.html
lib/jabaws-min-client-2.2.0.jar
resources/lang/Messages.properties
resources/lang/Messages_es.properties
resources/uniprot_mapping.xml
src/MCview/PDBChain.java
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/AlignmentSorter.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Conservation.java
src/jalview/analysis/CrossRef.java
src/jalview/analysis/Dna.java
src/jalview/analysis/Rna.java
src/jalview/analysis/SeqsetUtils.java
src/jalview/analysis/scoremodels/FeatureDistanceModel.java
src/jalview/api/FeatureColourI.java
src/jalview/api/FeatureRenderer.java
src/jalview/api/FeaturesDisplayedI.java
src/jalview/api/RendererListenerI.java
src/jalview/appletgui/APopupMenu.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/FeatureColourChooser.java
src/jalview/appletgui/FeatureRenderer.java
src/jalview/appletgui/FeatureSettings.java
src/jalview/appletgui/Finder.java
src/jalview/appletgui/IdPanel.java
src/jalview/appletgui/SeqCanvas.java
src/jalview/appletgui/SeqPanel.java
src/jalview/commands/EditCommand.java
src/jalview/commands/RemoveGapColCommand.java
src/jalview/commands/RemoveGapsCommand.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/AlignedCodonFrame.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/AlignmentI.java
src/jalview/datamodel/BinarySequence.java
src/jalview/datamodel/ContiguousI.java [new file with mode: 0644]
src/jalview/datamodel/Mapping.java
src/jalview/datamodel/Range.java [new file with mode: 0644]
src/jalview/datamodel/SearchResults.java
src/jalview/datamodel/SearchResultsI.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceCursor.java [new file with mode: 0644]
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/SequenceI.java
src/jalview/datamodel/features/FeatureLocationI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureStore.java [new file with mode: 0644]
src/jalview/datamodel/features/NCList.java [new file with mode: 0644]
src/jalview/datamodel/features/NCNode.java [new file with mode: 0644]
src/jalview/datamodel/features/RangeComparator.java [new file with mode: 0644]
src/jalview/datamodel/features/SequenceFeatures.java [new file with mode: 0644]
src/jalview/datamodel/features/SequenceFeaturesI.java [new file with mode: 0644]
src/jalview/datamodel/xdb/embl/EmblEntry.java
src/jalview/datamodel/xdb/uniprot/UniprotEntry.java [moved from src/jalview/datamodel/UniprotEntry.java with 90% similarity]
src/jalview/datamodel/xdb/uniprot/UniprotFeature.java [new file with mode: 0644]
src/jalview/datamodel/xdb/uniprot/UniprotFile.java [moved from src/jalview/datamodel/UniprotFile.java with 96% similarity]
src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java [moved from src/jalview/datamodel/UniprotProteinName.java with 97% similarity]
src/jalview/datamodel/xdb/uniprot/UniprotSequence.java [moved from src/jalview/datamodel/UniprotSequence.java with 97% similarity]
src/jalview/ext/ensembl/EnsemblCdna.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/jmol/JmolParser.java
src/jalview/ext/rbvi/chimera/AtomSpecModel.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/AppVarna.java
src/jalview/gui/CutAndPasteTransfer.java
src/jalview/gui/FeatureColourChooser.java
src/jalview/gui/FeatureRenderer.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/IdPanel.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/Jalview2XML_V1.java
src/jalview/gui/OverviewCanvas.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/gui/TreePanel.java
src/jalview/io/BLCFile.java
src/jalview/io/ClansFile.java [deleted file]
src/jalview/io/ClustalFile.java
src/jalview/io/FeaturesFile.java
src/jalview/io/IdentifyFile.java
src/jalview/io/JSONFile.java
src/jalview/io/JnetAnnotationMaker.java
src/jalview/io/MSFfile.java
src/jalview/io/MatrixFile.java [deleted file]
src/jalview/io/PfamFile.java
src/jalview/io/PhylipFile.java
src/jalview/io/PileUpfile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/StructureFile.java
src/jalview/io/gff/ExonerateHelper.java
src/jalview/io/gff/Gff3Helper.java
src/jalview/io/gff/GffHelperBase.java
src/jalview/io/gff/InterProScanHelper.java
src/jalview/io/vamsas/Datasetsequence.java
src/jalview/io/vamsas/Sequencefeature.java
src/jalview/renderer/OverviewRenderer.java
src/jalview/renderer/OverviewResColourFinder.java
src/jalview/renderer/ResidueShader.java
src/jalview/renderer/seqfeatures/FeatureColourFinder.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/schemes/ClustalxColourScheme.java
src/jalview/schemes/FeatureColour.java
src/jalview/util/Comparison.java
src/jalview/util/IntRangeComparator.java [new file with mode: 0644]
src/jalview/util/RangeComparator.java [deleted file]
src/jalview/viewmodel/ViewportRanges.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/seqfeatures/FeaturesDisplayed.java
src/jalview/workers/ColumnCounterSetWorker.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/dbsources/Uniprot.java
src/jalview/ws/jws2/AADisorderClient.java
src/jalview/ws/rest/params/SeqVector.java
test/MCview/PDBChainTest.java
test/jalview/analysis/AlignmentSorterTest.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/analysis/RnaTest.java
test/jalview/analysis/SeqsetUtilsTest.java
test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java
test/jalview/commands/EditCommandTest.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/datamodel/SeqCigarTest.java
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/datamodel/features/FeatureStoreTest.java [new file with mode: 0644]
test/jalview/datamodel/features/NCListTest.java [new file with mode: 0644]
test/jalview/datamodel/features/NCNodeTest.java [new file with mode: 0644]
test/jalview/datamodel/features/RangeComparatorTest.java [new file with mode: 0644]
test/jalview/datamodel/features/SequenceFeaturesTest.java [new file with mode: 0644]
test/jalview/ext/ensembl/EnsemblCdnaTest.java
test/jalview/ext/ensembl/EnsemblCdsTest.java
test/jalview/ext/ensembl/EnsemblGeneTest.java
test/jalview/ext/ensembl/EnsemblGenomeTest.java
test/jalview/ext/ensembl/EnsemblSeqProxyTest.java
test/jalview/ext/jmol/JmolParserTest.java
test/jalview/ext/paradise/TestAnnotate3D.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/gui/AlignViewportTest.java
test/jalview/io/AnnotatedPDBFileInputTest.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/JSONFileTest.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/StockholmFileTest.java
test/jalview/io/gff/Gff3HelperTest.java
test/jalview/io/gff/InterProScanHelperTest.java
test/jalview/renderer/OverviewResColourFinderTest.java
test/jalview/renderer/ResidueShaderTest.java
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java
test/jalview/renderer/seqfeatures/FeatureRendererTest.java [new file with mode: 0644]
test/jalview/schemes/AnnotationColourGradientTest.java
test/jalview/schemes/FeatureColourTest.java
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/viewmodel/ViewportRangesTest.java
test/jalview/ws/dbsources/UniprotTest.java
test/jalview/ws/seqfetcher/DbRefFetcherTest.java
utils/InstallAnywhere/Jalview.iap_xml

diff --git a/RELEASE b/RELEASE
index 0229909..e6b0cf8 100644 (file)
--- a/RELEASE
+++ b/RELEASE
@@ -1,2 +1,2 @@
-jalview.release=releases/Release_2_10_2_Branch
+jalview.release=releases/Release_2_10_2b1_Branch
 jalview.version=2.10.2b1
index 9e21593..eb30ef0 100755 (executable)
--- a/build.xml
+++ b/build.xml
 
     <jnlpf toFile="${jnlpFile}" />
     <!-- add a j2se entry for java 9 -->
-    <replace file="${jnlpFile}" value="j2se version=&quot;7+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee&quot;">
-          <replacetoken>j2se version="9+"</replacetoken>
+    <replace file="${jnlpFile}" value="j2se version=&quot;1.7+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee&quot;">
+          <replacetoken>j2se version="1.9+"</replacetoken>
            
         </replace>
   </target>
index 5f6e784..93c19db 100644 (file)
@@ -1 +1 @@
-{"seqs":[{"name":"FER_CAPAN/3-34","start":3,"svid":"1.0","end":34,"id":"1665704504","seq":"SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALF","order":1},{"name":"FER1_SOLLC/3-34","start":3,"svid":"1.0","end":34,"id":"1003594867","seq":"SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALF","order":2},{"name":"Q93XJ9_SOLTU/3-34","start":3,"svid":"1.0","end":34,"id":"1332961135","seq":"SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALF","order":3},{"name":"FER1_PEA/6-37","start":6,"svid":"1.0","end":37,"id":"1335040546","seq":"ALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFL","order":4},{"name":"Q7XA98_TRIPR/6-39","start":6,"svid":"1.0","end":39,"id":"1777084554","seq":"ALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGF","order":5},{"name":"FER_TOCH/3-34","start":3,"svid":"1.0","end":34,"id":"823528539","seq":"FILGTMISKSFLFRKPAVTSL-KAISNVGE--ALF","order":6}],"appSettings":{"globalColorScheme":"zappo","webStartUrl":"www.jalview.org/services/launchApp","application":"Jalview","hiddenSeqs":"823528539","showSeqFeatures":"true","version":"2.9","hiddenCols":"32-33;34-34"},"seqGroups":[{"displayText":true,"startRes":21,"groupName":"JGroup:1883305585","endRes":29,"colourText":false,"sequenceRefs":["1003594867","1332961135","1335040546","1777084554"],"svid":"1.0","showNonconserved":false,"colourScheme":"Zappo","displayBoxes":true}],"alignAnnotation":[{"svid":"1.0","annotations":[{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"}],"description":"New description","label":"Secondary Structure"}],"svid":"1.0","seqFeatures":[{"fillColor":"#7d1633","score":0,"sequenceRef":"1332961135","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1335040546","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1777084554","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"}]}
\ No newline at end of file
+{"seqs":[{"name":"FER_CAPAN/3-34","start":3,"svid":"1.0","end":34,"id":"1665704504","seq":"SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALF","order":1},{"name":"FER1_SOLLC/3-34","start":3,"svid":"1.0","end":34,"id":"1003594867","seq":"SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALF","order":2},{"name":"Q93XJ9_SOLTU/3-34","start":3,"svid":"1.0","end":34,"id":"1332961135","seq":"SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALF","order":3},{"name":"FER1_PEA/6-37","start":6,"svid":"1.0","end":37,"id":"1335040546","seq":"ALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFL","order":4},{"name":"Q7XA98_TRIPR/6-39","start":6,"svid":"1.0","end":39,"id":"1777084554","seq":"ALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGF","order":5},{"name":"FER_TOCH/3-34","start":3,"svid":"1.0","end":34,"id":"823528539","seq":"FILGTMISKSFLFRKPAVTSL-KAISNVGE--ALF","order":6}],"appSettings":{"globalColorScheme":"zappo","webStartUrl":"www.jalview.org/services/launchApp","application":"Jalview","hiddenSeqs":"823528539","showSeqFeatures":"true","version":"2.9","hiddenCols":"32-33;34-34"},"seqGroups":[{"displayText":true,"startRes":21,"groupName":"JGroup:1883305585","endRes":29,"colourText":false,"sequenceRefs":["1003594867","1332961135","1335040546","1777084554"],"svid":"1.0","showNonconserved":false,"colourScheme":"Zappo","displayBoxes":true}],"alignAnnotation":[{"svid":"1.0","annotations":[{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"}],"description":"New description","label":"Secondary Structure"}],"svid":"1.0","seqFeatures":[{"fillColor":"#7d1633","score":0,"otherDetails":{"status":"+"},"sequenceRef":"1332961135","featureGroup":"Pfam","svid":"1.0","description":"My description","xStart":0,"xEnd":0,"type":"Domain"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1332961135","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1335040546","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1777084554","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"}]}
\ No newline at end of file
diff --git a/help/html/features/overview.gif b/help/html/features/overview.gif
deleted file mode 100755 (executable)
index b5b1bc5..0000000
Binary files a/help/html/features/overview.gif and /dev/null differ
index 2a656a3..6401fdd 100755 (executable)
@@ -47,7 +47,7 @@
       overview was introduced in Jalview 2.10.2</em>.
   
   <p>
-    <img src="overview.gif" width="407" height="137">
+    <img src="overview.png" width="800" height="196" alt="1hdc.pfam example alignment">
   </p>
   <p>&nbsp;</p>
   <em>Previous to 2.10.2b1, gaps were white, and sequences shown as
diff --git a/help/html/features/overview.png b/help/html/features/overview.png
new file mode 100644 (file)
index 0000000..d7aaa19
Binary files /dev/null and b/help/html/features/overview.png differ
index 925efdb..5a1e324 100755 (executable)
@@ -71,7 +71,7 @@ li:before {
       <td width="60" nowrap>
         <div align="center">
           <strong><a name="Jalview.2.10.2b1">2.10.2b1</a><br />
-            <em>TBA</em></strong>
+            <em>5/9/2017</em></strong>
         </div>
       </td>
       <td><div align="left">
@@ -122,6 +122,20 @@ li:before {
               <!-- JAL-2691 -->Not all chains are mapped when multimeric
               assemblies are imported via CIF file
             </li>
+            <li>
+              <!-- JAL-2704 -->Gap colour in custom colourscheme is not
+              displayed when threshold or conservation colouring is also
+              enabled.
+            </li>
+            <li>
+              <!-- JAL-2549 -->JABAWS 2.2 services report wrong JABAWS
+              server version
+            </li>
+            <li>
+              <!-- JAL-2673 -->Jalview continues to scroll after
+              dragging a selected region off the visible region of the
+              alignment
+            </li>
           </ul>
         </div></td>
     
index 169e7fe..090b695 100755 (executable)
 </head>
 <body>
   <p>
-  <strong>Jalview 2.10.2b1 bugfix release</strong>
+    <strong>Jalview 2.10.2b1 bugfix release</strong>
   </p>
   <p>
     This is patch release for 2.10.2. See the <a
-      href="releases.html#Jalview.2.10.2b1">release notes</a>.
+      href="releases.html#Jalview.2.10.2b1">release notes</a> for full
+    details about the bugs addressed. This release also introduces
+    additional improvements to the overview panel, and patches for
+    several minor issues including the ability to correctly recover
+    cross-references for Uniprot protein sequences from Ensembl.
   </p>
-  <ul>
-    <li>Gaps are now rendered as dark grey in overview window</li>
-  </ul>
   <p>
     <strong>What's new in Jalview 2.10.2 ?</strong>
   </p>
index bf1e8b1..37426c3 100644 (file)
Binary files a/lib/jabaws-min-client-2.2.0.jar and b/lib/jabaws-min-client-2.2.0.jar differ
index 9f3ab70..fe74476 100644 (file)
@@ -913,7 +913,6 @@ label.as_percentage = As Percentage
 error.not_implemented = Not implemented
 error.no_such_method_as_clone1_for = No such method as clone1 for {0}
 error.null_from_clone1 = Null from clone1!
-error.implementation_error_sortbyfeature = Implementation Error - sortByFeature method must be one of FEATURE_SCORE, FEATURE_LABEL or FEATURE_DENSITY.
 error.not_yet_implemented = Not yet implemented
 error.unknown_type_dna_or_pep = Unknown Type {0} - dna or pep are the only allowed values.
 error.implementation_error_dont_know_threshold_annotationcolourgradient = Implementation error: don't know about threshold setting for current AnnotationColourGradient.
index 1e3a391..f40f31d 100644 (file)
@@ -838,7 +838,6 @@ label.as_percentage = Como Porcentaje
 error.not_implemented = No implementado
 error.no_such_method_as_clone1_for = No existe ese método como un clone1 de {0}
 error.null_from_clone1 = Nulo de clone1!
-error.implementation_error_sortbyfeature = Error de implementación - sortByFeature debe ser uno de FEATURE_SCORE, FEATURE_LABEL o FEATURE_DENSITY.
 error.not_yet_implemented = No se ha implementado todavía
 error.unknown_type_dna_or_pep = Tipo desconocido {0} - dna o pep son los Ãºnicos valores permitidos
 error.implementation_error_dont_know_threshold_annotationcolourgradient = Error de implementación: no se conoce el valor umbral para el AnnotationColourGradient actual.
index 4a981ad..6344d1e 100755 (executable)
  * The Jalview Authors are detailed in the 'AUTHORS' file.
 -->
 <mapping>
-       <class name="jalview.datamodel.UniprotFile">
+       <class name="jalview.datamodel.xdb.uniprot.UniprotFile">
                  <map-to xml="uniprot"/>               
-                 <field name="UniprotEntries" type="jalview.datamodel.UniprotEntry" collection="vector">
+                 <field name="UniprotEntries" type="jalview.datamodel.xdb.uniprot.UniprotEntry" collection="vector">
                      <bind-xml name="entry"/>
                 </field>               
         </class>
                 
-        <class name="jalview.datamodel.UniprotEntry">
+        <class name="jalview.datamodel.xdb.uniprot.UniprotEntry">
                <field name="name" type="string" collection="vector"/>
                <field name="accession" type="string" collection="vector"/>
-               <field name="protein" type="jalview.datamodel.UniprotProteinName"/>
-           <field name="UniprotSequence" type="jalview.datamodel.UniprotSequence">
+               <field name="protein" type="jalview.datamodel.xdb.uniprot.UniprotProteinName"/>
+           <field name="UniprotSequence" type="jalview.datamodel.xdb.uniprot.UniprotSequence">
                <bind-xml name="sequence"/> 
            </field>
-           <field name="feature" type="jalview.datamodel.SequenceFeature" collection="vector"/>
+           <field name="feature" type="jalview.datamodel.xdb.uniprot.UniprotFeature" collection="vector"/>
            <field name="dbReference" type="jalview.datamodel.PDBEntry" collection="vector"/>
                       
         </class>
-       <class name="jalview.datamodel.UniprotProteinName">
+       <class name="jalview.datamodel.xdb.uniprot.UniprotProteinName">
                <field name="name" collection="vector" type="string">
                        <bind-xml name="fullName" location="recommendedName" node="element"/>
                </field>
        </class>
         <!-- uniprot protein name is now a collection of collections - the INCLUDES and CONTAINS entries of the uniprot
                record. This means this doesn't exist anymore...
-        <class name="jalview.datamodel.UniprotProteinName">
+        <class name="jalview.datamodel.xdb.uniprot.UniprotProteinName">
                <field name="name" type="string" collection="vector">
                        <bind-xml name="name"/>
                </field>                
         </class>
         -->
         
-        <class name="jalview.datamodel.SequenceFeature">
+        <class name="jalview.datamodel.xdb.uniprot.UniprotFeature">
                <field name="type">
                        <bind-xml node="attribute"/>
                </field>
@@ -71,7 +71,7 @@
                 </field>
         </class>
        
-          <class name="jalview.datamodel.UniprotSequence">
+          <class name="jalview.datamodel.xdb.uniprot.UniprotSequence">
                     <field name="content" type="string">
                        <bind-xml name="sequence" node="text"/>
                     </field>
index f2dd3d0..f4bd31c 100755 (executable)
@@ -41,11 +41,6 @@ public class PDBChain
 {
   public static final String RESNUM_FEATURE = "RESNUM";
 
-  /**
-   * SequenceFeature group for PDB File features added to sequences
-   */
-  private static final String PDBFILEFEATURE = "PDBFile";
-
   private static final String IEASTATUS = "IEA:jalview";
 
   public String id;
@@ -83,10 +78,10 @@ public class PDBChain
 
   public String pdbid = "";
 
-  public PDBChain(String pdbid, String id)
+  public PDBChain(String thePdbid, String theId)
   {
-    this.pdbid = pdbid == null ? pdbid : pdbid.toLowerCase();
-    this.id = id;
+    this.pdbid = thePdbid == null ? thePdbid : thePdbid.toLowerCase();
+    this.id = theId;
   }
 
   /**
@@ -167,15 +162,14 @@ public class PDBChain
   }
 
   /**
-   * copy over the RESNUM seqfeatures from the internal chain sequence to the
+   * Copies over the RESNUM seqfeatures from the internal chain sequence to the
    * mapped sequence
    * 
    * @param seq
    * @param status
    *          The Status of the transferred annotation
-   * @return the features added to sq (or its dataset)
    */
-  public SequenceFeature[] transferRESNUMFeatures(SequenceI seq,
+  public void transferRESNUMFeatures(SequenceI seq,
           String status)
   {
     SequenceI sq = seq;
@@ -184,10 +178,11 @@ public class PDBChain
       sq = sq.getDatasetSequence();
       if (sq == sequence)
       {
-        return null;
+        return;
       }
     }
-    /**
+
+    /*
      * Remove any existing features for this chain if they exist ?
      * SequenceFeature[] seqsfeatures=seq.getSequenceFeatures(); int
      * totfeat=seqsfeatures.length; // Remove any features for this exact chain
@@ -197,21 +192,19 @@ public class PDBChain
     {
       status = PDBChain.IEASTATUS;
     }
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-    if (features == null)
-    {
-      return null;
-    }
-    for (int i = 0; i < features.length; i++)
+
+    List<SequenceFeature> features = sequence.getSequenceFeatures();
+    for (SequenceFeature feature : features)
     {
-      if (features[i].getFeatureGroup() != null
-              && features[i].getFeatureGroup().equals(pdbid))
+      if (feature.getFeatureGroup() != null
+              && feature.getFeatureGroup().equals(pdbid))
       {
-        SequenceFeature tx = new SequenceFeature(features[i]);
-        tx.setBegin(1 + residues.elementAt(tx.getBegin() - offset).atoms
-                .elementAt(0).alignmentMapping);
-        tx.setEnd(1 + residues.elementAt(tx.getEnd() - offset).atoms
-                .elementAt(0).alignmentMapping);
+        int newBegin = 1 + residues.elementAt(feature.getBegin() - offset).atoms
+                .elementAt(0).alignmentMapping;
+        int newEnd = 1 + residues.elementAt(feature.getEnd() - offset).atoms
+                .elementAt(0).alignmentMapping;
+        SequenceFeature tx = new SequenceFeature(feature, newBegin, newEnd,
+                feature.getFeatureGroup(), feature.getScore());
         tx.setStatus(status
                 + ((tx.getStatus() == null || tx.getStatus().length() == 0)
                         ? ""
@@ -222,7 +215,6 @@ public class PDBChain
         }
       }
     }
-    return features;
   }
 
   /**
@@ -354,25 +346,25 @@ public class PDBChain
               && residues.lastElement().atoms
                       .get(0).resNumber == currAtom.resNumber)
       {
-        SequenceFeature sf = new SequenceFeature("INSERTION",
-                currAtom.resName + ":" + currAtom.resNumIns + " " + pdbid
-                        + id,
-                "", offset + count - 1, offset + count - 1, "PDB_INS");
+        String desc = currAtom.resName + ":" + currAtom.resNumIns + " "
+                + pdbid + id;
+        SequenceFeature sf = new SequenceFeature("INSERTION", desc, offset
+                + count - 1, offset + count - 1, "PDB_INS");
         resFeatures.addElement(sf);
         residues.lastElement().atoms.addAll(resAtoms);
       }
       else
       {
-
         // Make a new Residue object with the new atoms vector
         residues.addElement(new Residue(resAtoms, resNumber - 1, count));
 
         Residue tmpres = residues.lastElement();
         Atom tmpat = tmpres.atoms.get(0);
         // Make A new SequenceFeature for the current residue numbering
-        SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE,
-                tmpat.resName + ":" + tmpat.resNumIns + " " + pdbid + id,
-                "", offset + count, offset + count, pdbid);
+        String desc = tmpat.resName
+                + ":" + tmpat.resNumIns + " " + pdbid + id;
+        SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE, desc,
+                offset + count, offset + count, pdbid);
         resFeatures.addElement(sf);
         resAnnotation.addElement(new Annotation(tmpat.tfactor));
         // Keep totting up the sequence
index f8e8379..e4f2dfa 100755 (executable)
@@ -151,10 +151,9 @@ public class AAFrequency
                   "WARNING: Consensus skipping null sequence - possible race condition.");
           continue;
         }
-        char[] seq = sequences[row].getSequence();
-        if (seq.length > column)
+        if (sequences[row].getLength() > column)
         {
-          char c = seq[column];
+          char c = sequences[row].getCharAt(column);
           residueCounts.add(c);
           if (Comparison.isNucleotide(c))
           {
index 6b8ea4a..b5cefe0 100755 (executable)
@@ -29,11 +29,11 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.SequenceNode;
-import jalview.util.MessageManager;
 import jalview.util.QuickSort;
 
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 
 /**
@@ -53,7 +53,7 @@ import java.util.List;
  */
 public class AlignmentSorter
 {
-  /**
+  /*
    * todo: refactor searches to follow a basic pattern: (search property, last
    * search state, current sort direction)
    */
@@ -71,19 +71,18 @@ public class AlignmentSorter
 
   static boolean sortTreeAscending = true;
 
-  /**
-   * last Annotation Label used by sortByScore
+  /*
+   * last Annotation Label used for sort by Annotation score
    */
-  private static String lastSortByScore;
-
-  private static boolean sortByScoreAscending = true;
+  private static String lastSortByAnnotation;
 
-  /**
-   * compact representation of last arguments to SortByFeatureScore
+  /*
+   * string hash of last arguments to sortByFeature
+   * (sort order toggles if this is unchanged between sorts)
    */
-  private static String lastSortByFeatureScore;
+  private static String sortByFeatureCriteria;
 
-  private static boolean sortByFeatureScoreAscending = true;
+  private static boolean sortByFeatureAscending = true;
 
   private static boolean sortLengthAscending;
 
@@ -659,9 +658,9 @@ public class AlignmentSorter
     }
 
     jalview.util.QuickSort.sort(scores, seqs);
-    if (lastSortByScore != scoreLabel)
+    if (lastSortByAnnotation != scoreLabel)
     {
-      lastSortByScore = scoreLabel;
+      lastSortByAnnotation = scoreLabel;
       setOrder(alignment, seqs);
     }
     else
@@ -683,98 +682,40 @@ public class AlignmentSorter
   public static String FEATURE_DENSITY = "density";
 
   /**
-   * sort the alignment using the features on each sequence found between start
-   * and stop with the given featureLabel (and optional group qualifier)
+   * Sort sequences by feature score or density, optionally restricted by
+   * feature types, feature groups, or alignment start/end positions.
+   * <p>
+   * If the sort is repeated for the same combination of types and groups, sort
+   * order is reversed.
    * 
-   * @param featureLabel
-   *          (may not be null)
-   * @param groupLabel
-   *          (may be null)
-   * @param start
-   *          (-1 to include non-positional features)
-   * @param stop
-   *          (-1 to only sort on non-positional features)
+   * @param featureTypes
+   *          a list of feature types to include (or null for all)
+   * @param groups
+   *          a list of feature groups to include (or null for all)
+   * @param startCol
+   *          start column position to include (base zero)
+   * @param endCol
+   *          end column position to include (base zero)
    * @param alignment
-   *          - aligned sequences containing features
+   *          the alignment to be sorted
    * @param method
-   *          - one of the string constants FEATURE_SCORE, FEATURE_LABEL,
-   *          FEATURE_DENSITY
+   *          either "average_score" or "density" ("text" not yet implemented)
    */
-  public static void sortByFeature(String featureLabel, String groupLabel,
-          int start, int stop, AlignmentI alignment, String method)
-  {
-    sortByFeature(
-            featureLabel == null ? null : Arrays.asList(new String[]
-            { featureLabel }),
-            groupLabel == null ? null : Arrays.asList(new String[]
-            { groupLabel }), start, stop, alignment, method);
-  }
-
-  private static boolean containsIgnoreCase(final String lab,
-          final List<String> labs)
-  {
-    if (labs == null)
-    {
-      return true;
-    }
-    if (lab == null)
-    {
-      return false;
-    }
-    for (String label : labs)
-    {
-      if (lab.equalsIgnoreCase(label))
-      {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public static void sortByFeature(List<String> featureLabels,
-          List<String> groupLabels, int start, int stop,
+  public static void sortByFeature(List<String> featureTypes,
+          List<String> groups, final int startCol, final int endCol,
           AlignmentI alignment, String method)
   {
     if (method != FEATURE_SCORE && method != FEATURE_LABEL
             && method != FEATURE_DENSITY)
     {
-      throw new Error(MessageManager
-              .getString("error.implementation_error_sortbyfeature"));
+      String msg = String
+              .format("Implementation Error - sortByFeature method must be either '%s' or '%s'",
+                      FEATURE_SCORE, FEATURE_DENSITY);
+      System.err.println(msg);
+      return;
     }
 
-    boolean ignoreScore = method != FEATURE_SCORE;
-    StringBuffer scoreLabel = new StringBuffer();
-    scoreLabel.append(start + stop + method);
-    // This doesn't quite work yet - we'd like to have a canonical ordering that
-    // can be preserved from call to call
-    if (featureLabels != null)
-    {
-      for (String label : featureLabels)
-      {
-        scoreLabel.append(label);
-      }
-    }
-    if (groupLabels != null)
-    {
-      for (String label : groupLabels)
-      {
-        scoreLabel.append(label);
-      }
-    }
-
-    /*
-     * if resorting the same feature, toggle sort order
-     */
-    if (lastSortByFeatureScore == null
-            || !scoreLabel.toString().equals(lastSortByFeatureScore))
-    {
-      sortByFeatureScoreAscending = true;
-    }
-    else
-    {
-      sortByFeatureScoreAscending = !sortByFeatureScoreAscending;
-    }
-    lastSortByFeatureScore = scoreLabel.toString();
+    flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol, endCol);
 
     SequenceI[] seqs = alignment.getSequencesArray();
 
@@ -783,57 +724,44 @@ public class AlignmentSorter
     int hasScores = 0; // number of scores present on set
     double[] scores = new double[seqs.length];
     int[] seqScores = new int[seqs.length];
-    Object[] feats = new Object[seqs.length];
-    double min = 0, max = 0;
+    Object[][] feats = new Object[seqs.length][];
+    double min = 0d;
+    double max = 0d;
+
     for (int i = 0; i < seqs.length; i++)
     {
-      SequenceFeature[] sf = seqs[i].getSequenceFeatures();
-      if (sf == null)
-      {
-        sf = new SequenceFeature[0];
-      }
-      else
-      {
-        SequenceFeature[] tmp = new SequenceFeature[sf.length];
-        for (int s = 0; s < tmp.length; s++)
-        {
-          tmp[s] = sf[s];
-        }
-        sf = tmp;
-      }
-      int sstart = (start == -1) ? start : seqs[i].findPosition(start);
-      int sstop = (stop == -1) ? stop : seqs[i].findPosition(stop);
+      /*
+       * get sequence residues overlapping column region
+       * and features for residue positions and specified types
+       */
+      String[] types = featureTypes == null ? null : featureTypes
+              .toArray(new String[featureTypes.size()]);
+      List<SequenceFeature> sfs = seqs[i].findFeatures(startCol + 1,
+              endCol + 1, types);
+
       seqScores[i] = 0;
       scores[i] = 0.0;
-      int n = sf.length;
-      for (int f = 0; f < sf.length; f++)
+
+      Iterator<SequenceFeature> it = sfs.listIterator();
+      while (it.hasNext())
       {
-        // filter for selection criteria
-        SequenceFeature feature = sf[f];
+        SequenceFeature sf = it.next();
 
         /*
-         * double-check feature overlaps columns (JAL-2544)
-         * (could avoid this with a findPositions(fromCol, toCol) method)
-         * findIndex returns base 1 column values, startCol/endCol are base 0
+         * accept all features with null or empty group, otherwise
+         * check group is one of the currently visible groups
          */
-        boolean noOverlap = seqs[i].findIndex(feature.getBegin()) > stop + 1
-                || seqs[i].findIndex(feature.getEnd()) < start + 1;
-        boolean skipFeatureType = featureLabels != null && !AlignmentSorter
-                .containsIgnoreCase(feature.type, featureLabels);
-        boolean skipFeatureGroup = groupLabels != null
-                && (feature.getFeatureGroup() != null
-                        && !AlignmentSorter.containsIgnoreCase(
-                                feature.getFeatureGroup(), groupLabels));
-        if (noOverlap || skipFeatureType || skipFeatureGroup)
+        String featureGroup = sf.getFeatureGroup();
+        if (groups != null && featureGroup != null
+                && !"".equals(featureGroup)
+                && !groups.contains(featureGroup))
         {
-          // forget about this feature
-          sf[f] = null;
-          n--;
+          it.remove();
         }
         else
         {
-          // or, also take a look at the scores if necessary.
-          if (!ignoreScore && !Float.isNaN(feature.getScore()))
+          float score = sf.getScore();
+          if (FEATURE_SCORE.equals(method) && !Float.isNaN(score))
           {
             if (seqScores[i] == 0)
             {
@@ -841,34 +769,26 @@ public class AlignmentSorter
             }
             seqScores[i]++;
             hasScore[i] = true;
-            scores[i] += feature.getScore(); // take the first instance of this
-            // score.
+            scores[i] += score;
+            // take the first instance of this score // ??
           }
         }
       }
-      SequenceFeature[] fs;
-      feats[i] = fs = new SequenceFeature[n];
-      if (n > 0)
+
+      feats[i] = sfs.toArray(new SequenceFeature[sfs.size()]);
+      if (!sfs.isEmpty())
       {
-        n = 0;
-        for (int f = 0; f < sf.length; f++)
-        {
-          if (sf[f] != null)
-          {
-            ((SequenceFeature[]) feats[i])[n++] = sf[f];
-          }
-        }
         if (method == FEATURE_LABEL)
         {
-          // order the labels by alphabet
-          String[] labs = new String[fs.length];
-          for (int l = 0; l < labs.length; l++)
+          // order the labels by alphabet (not yet implemented)
+          String[] labs = new String[sfs.size()];
+          for (int l = 0; l < sfs.size(); l++)
           {
-            labs[l] = (fs[l].getDescription() != null
-                    ? fs[l].getDescription()
-                    : fs[l].getType());
+            SequenceFeature sf = sfs.get(l);
+            String description = sf.getDescription();
+            labs[l] = (description != null ? description : sf.getType());
           }
-          QuickSort.sort(labs, ((Object[]) feats[i]));
+          QuickSort.sort(labs, feats[i]);
         }
       }
       if (hasScore[i])
@@ -878,23 +798,18 @@ public class AlignmentSorter
         // update the score bounds.
         if (hasScores == 1)
         {
-          max = min = scores[i];
+          min = scores[i];
+          max = min;
         }
         else
         {
-          if (max < scores[i])
-          {
-            max = scores[i];
-          }
-          if (min > scores[i])
-          {
-            min = scores[i];
-          }
+          max = Math.max(max, scores[i]);
+          min = Math.min(min, scores[i]);
         }
       }
     }
 
-    if (method == FEATURE_SCORE)
+    if (FEATURE_SCORE.equals(method))
     {
       if (hasScores == 0)
       {
@@ -919,9 +834,9 @@ public class AlignmentSorter
           }
         }
       }
-      QuickSort.sortByDouble(scores, seqs, sortByFeatureScoreAscending);
+      QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
     }
-    else if (method == FEATURE_DENSITY)
+    else if (FEATURE_DENSITY.equals(method))
     {
       for (int i = 0; i < seqs.length; i++)
       {
@@ -931,18 +846,53 @@ public class AlignmentSorter
         // System.err.println("Sorting on Density: seq "+seqs[i].getName()+
         // " Feats: "+featureCount+" Score : "+scores[i]);
       }
-      QuickSort.sortByDouble(scores, seqs, sortByFeatureScoreAscending);
+      QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
     }
-    else
+
+    setOrder(alignment, seqs);
+  }
+
+  /**
+   * Builds a string hash of criteria for sorting, and if unchanged from last
+   * time, reverse the sort order
+   * 
+   * @param method
+   * @param featureTypes
+   * @param groups
+   * @param startCol
+   * @param endCol
+   */
+  protected static void flipFeatureSortIfUnchanged(String method,
+          List<String> featureTypes, List<String> groups,
+          final int startCol, final int endCol)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    sb.append(startCol).append(method).append(endCol);
+    if (featureTypes != null)
     {
-      if (method == FEATURE_LABEL)
-      {
-        throw new Error(
-                MessageManager.getString("error.not_yet_implemented"));
-      }
+      Collections.sort(featureTypes);
+      sb.append(featureTypes.toString());
     }
+    if (groups != null)
+    {
+      Collections.sort(groups);
+      sb.append(groups.toString());
+    }
+    String scoreCriteria = sb.toString();
 
-    setOrder(alignment, seqs);
+    /*
+     * if resorting on the same criteria, toggle sort order
+     */
+    if (sortByFeatureCriteria == null
+            || !scoreCriteria.equals(sortByFeatureCriteria))
+    {
+      sortByFeatureAscending = true;
+    }
+    else
+    {
+      sortByFeatureAscending = !sortByFeatureAscending;
+    }
+    sortByFeatureCriteria = scoreCriteria;
   }
 
 }
index 1b8f84f..2b9b9f9 100644 (file)
@@ -35,14 +35,14 @@ import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.io.gff.SequenceOntologyFactory;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
+import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
-import jalview.util.RangeComparator;
 import jalview.util.StringUtils;
 
 import java.io.UnsupportedEncodingException;
@@ -51,7 +51,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -654,15 +653,16 @@ public class AlignmentUtils
     int toOffset = alignTo.getStart() - 1;
     int sourceGapMappedLength = 0;
     boolean inExon = false;
-    final char[] thisSeq = alignTo.getSequence();
-    final char[] thatAligned = alignFrom.getSequence();
-    StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
+    final int toLength = alignTo.getLength();
+    final int fromLength = alignFrom.getLength();
+    StringBuilder thisAligned = new StringBuilder(2 * toLength);
 
     /*
      * Traverse the 'model' aligned sequence
      */
-    for (char sourceChar : thatAligned)
+    for (int i = 0; i < fromLength; i++)
     {
+      char sourceChar = alignFrom.getCharAt(i);
       if (sourceChar == sourceGap)
       {
         sourceGapMappedLength += ratio;
@@ -702,9 +702,9 @@ public class AlignmentUtils
        */
       int intronLength = 0;
       while (basesWritten + toOffset < mappedCodonEnd
-              && thisSeqPos < thisSeq.length)
+              && thisSeqPos < toLength)
       {
-        final char c = thisSeq[thisSeqPos++];
+        final char c = alignTo.getCharAt(thisSeqPos++);
         if (c != myGapChar)
         {
           basesWritten++;
@@ -730,7 +730,7 @@ public class AlignmentUtils
             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
                     trailingCopiedGap.length(), intronLength, startOfCodon);
-            for (int i = 0; i < gapsToAdd; i++)
+            for (int k = 0; k < gapsToAdd; k++)
             {
               thisAligned.append(myGapChar);
             }
@@ -758,9 +758,9 @@ public class AlignmentUtils
      * At end of model aligned sequence. Copy any remaining target sequence, optionally
      * including (intron) gaps.
      */
-    while (thisSeqPos < thisSeq.length)
+    while (thisSeqPos < toLength)
     {
-      final char c = thisSeq[thisSeqPos++];
+      final char c = alignTo.getCharAt(thisSeqPos++);
       if (c != myGapChar || preserveUnmappedGaps)
       {
         thisAligned.append(c);
@@ -952,7 +952,7 @@ public class AlignmentUtils
       SequenceI peptide = mapping.findAlignedSequence(cdsSeq, protein);
       if (peptide != null)
       {
-        int peptideLength = peptide.getLength();
+        final int peptideLength = peptide.getLength();
         Mapping map = mapping.getMappingBetween(cdsSeq, peptide);
         if (map != null)
         {
@@ -961,9 +961,9 @@ public class AlignmentUtils
           {
             mapList = mapList.getInverse();
           }
-          int cdsLength = cdsDss.getLength();
-          int mappedFromLength = MappingUtils
-                  .getLength(mapList.getFromRanges());
+          final int cdsLength = cdsDss.getLength();
+          int mappedFromLength = MappingUtils.getLength(mapList
+                  .getFromRanges());
           int mappedToLength = MappingUtils
                   .getLength(mapList.getToRanges());
           boolean addStopCodon = (cdsLength == mappedFromLength
@@ -988,14 +988,15 @@ public class AlignmentUtils
            * walk over the aligned peptide sequence and insert mapped 
            * codons for residues in the aligned cds sequence 
            */
-          char[] alignedPeptide = peptide.getSequence();
-          char[] nucleotides = cdsDss.getSequence();
           int copiedBases = 0;
           int cdsStart = cdsDss.getStart();
           int proteinPos = peptide.getStart() - 1;
           int cdsCol = 0;
-          for (char residue : alignedPeptide)
+
+          for (int col = 0; col < peptideLength; col++)
           {
+            char residue = peptide.getCharAt(col);
+
             if (Comparison.isGap(residue))
             {
               cdsCol += CODON_LENGTH;
@@ -1013,7 +1014,7 @@ public class AlignmentUtils
               {
                 for (int j = codon[0]; j <= codon[1]; j++)
                 {
-                  char mappedBase = nucleotides[j - cdsStart];
+                  char mappedBase = cdsDss.getCharAt(j - cdsStart);
                   alignedCds[cdsCol++] = mappedBase;
                   copiedBases++;
                 }
@@ -1025,7 +1026,7 @@ public class AlignmentUtils
            * append stop codon if not mapped from protein,
            * closing it up to the end of the mapped sequence
            */
-          if (copiedBases == nucleotides.length - CODON_LENGTH)
+          if (copiedBases == cdsLength - CODON_LENGTH)
           {
             for (int i = alignedCds.length - 1; i >= 0; i--)
             {
@@ -1035,10 +1036,9 @@ public class AlignmentUtils
                 break;
               }
             }
-            for (int i = nucleotides.length
-                    - CODON_LENGTH; i < nucleotides.length; i++)
+            for (int i = cdsLength - CODON_LENGTH; i < cdsLength; i++)
             {
-              alignedCds[cdsCol++] = nucleotides[i];
+              alignedCds[cdsCol++] = cdsDss.getCharAt(i);
             }
           }
           cdsSeq.setSequence(new String(alignedCds));
@@ -1208,21 +1208,26 @@ public class AlignmentUtils
           List<SequenceI> unmappedProtein)
   {
     /*
-     * Prefill aligned sequences with gaps before inserting aligned protein
-     * residues.
+     * prefill peptide sequences with gaps 
      */
     int alignedWidth = alignedCodons.size();
     char[] gaps = new char[alignedWidth];
     Arrays.fill(gaps, protein.getGapCharacter());
-    String allGaps = String.valueOf(gaps);
+    Map<SequenceI, char[]> peptides = new HashMap<>();
     for (SequenceI seq : protein.getSequences())
     {
       if (!unmappedProtein.contains(seq))
       {
-        seq.setSequence(allGaps);
+        peptides.put(seq, Arrays.copyOf(gaps, gaps.length));
       }
     }
 
+    /*
+     * Traverse the codons left to right (as defined by CodonComparator)
+     * and insert peptides in each column where the sequence is mapped.
+     * This gives a peptide 'alignment' where residues are aligned if their
+     * corresponding codons occupy the same columns in the cdna alignment.
+     */
     int column = 0;
     for (AlignedCodon codon : alignedCodons.keySet())
     {
@@ -1230,12 +1235,20 @@ public class AlignmentUtils
               .get(codon);
       for (Entry<SequenceI, AlignedCodon> entry : columnResidues.entrySet())
       {
-        // place translated codon at its column position in sequence
-        entry.getKey().getSequence()[column] = entry.getValue().product
-                .charAt(0);
+        char residue = entry.getValue().product.charAt(0);
+        peptides.get(entry.getKey())[column] = residue;
       }
       column++;
     }
+
+    /*
+     * and finally set the constructed sequences
+     */
+    for (Entry<SequenceI, char[]> entry : peptides.entrySet())
+    {
+      entry.getKey().setSequence(new String(entry.getValue()));
+    }
+
     return 0;
   }
 
@@ -2061,11 +2074,11 @@ public class AlignmentUtils
    * 
    * @param fromSeq
    * @param toSeq
+   * @param mapping
+   *          the mapping from 'fromSeq' to 'toSeq'
    * @param select
    *          if not null, only features of this type are copied (including
    *          subtypes in the Sequence Ontology)
-   * @param mapping
-   *          the mapping from 'fromSeq' to 'toSeq'
    * @param omitting
    */
   public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
@@ -2077,75 +2090,74 @@ public class AlignmentUtils
       copyTo = copyTo.getDatasetSequence();
     }
 
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    /*
+     * get features, optionally restricted by an ontology term
+     */
+    List<SequenceFeature> sfs = select == null ? fromSeq.getFeatures()
+            .getPositionalFeatures() : fromSeq.getFeatures()
+            .getFeaturesByOntology(select);
+
     int count = 0;
-    SequenceFeature[] sfs = fromSeq.getSequenceFeatures();
-    if (sfs != null)
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
+      String type = sf.getType();
+      boolean omit = false;
+      for (String toOmit : omitting)
       {
-        String type = sf.getType();
-        if (select != null && !so.isA(type, select))
-        {
-          continue;
-        }
-        boolean omit = false;
-        for (String toOmit : omitting)
+        if (type.equals(toOmit))
         {
-          if (type.equals(toOmit))
-          {
-            omit = true;
-          }
-        }
-        if (omit)
-        {
-          continue;
+          omit = true;
         }
+      }
+      if (omit)
+      {
+        continue;
+      }
 
-        /*
-         * locate the mapped range - null if either start or end is
-         * not mapped (no partial overlaps are calculated)
-         */
-        int start = sf.getBegin();
-        int end = sf.getEnd();
-        int[] mappedTo = mapping.locateInTo(start, end);
-        /*
-         * if whole exon range doesn't map, try interpreting it
-         * as 5' or 3' exon overlapping the CDS range
-         */
-        if (mappedTo == null)
-        {
-          mappedTo = mapping.locateInTo(end, end);
-          if (mappedTo != null)
-          {
-            /*
-             * end of exon is in CDS range - 5' overlap
-             * to a range from the start of the peptide
-             */
-            mappedTo[0] = 1;
-          }
-        }
-        if (mappedTo == null)
+      /*
+       * locate the mapped range - null if either start or end is
+       * not mapped (no partial overlaps are calculated)
+       */
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+      int[] mappedTo = mapping.locateInTo(start, end);
+      /*
+       * if whole exon range doesn't map, try interpreting it
+       * as 5' or 3' exon overlapping the CDS range
+       */
+      if (mappedTo == null)
+      {
+        mappedTo = mapping.locateInTo(end, end);
+        if (mappedTo != null)
         {
-          mappedTo = mapping.locateInTo(start, start);
-          if (mappedTo != null)
-          {
-            /*
-             * start of exon is in CDS range - 3' overlap
-             * to a range up to the end of the peptide
-             */
-            mappedTo[1] = toSeq.getLength();
-          }
+          /*
+           * end of exon is in CDS range - 5' overlap
+           * to a range from the start of the peptide
+           */
+          mappedTo[0] = 1;
         }
+      }
+      if (mappedTo == null)
+      {
+        mappedTo = mapping.locateInTo(start, start);
         if (mappedTo != null)
         {
-          SequenceFeature copy = new SequenceFeature(sf);
-          copy.setBegin(Math.min(mappedTo[0], mappedTo[1]));
-          copy.setEnd(Math.max(mappedTo[0], mappedTo[1]));
-          copyTo.addSequenceFeature(copy);
-          count++;
+          /*
+           * start of exon is in CDS range - 3' overlap
+           * to a range up to the end of the peptide
+           */
+          mappedTo[1] = toSeq.getLength();
         }
       }
+      if (mappedTo != null)
+      {
+        int newBegin = Math.min(mappedTo[0], mappedTo[1]);
+        int newEnd = Math.max(mappedTo[0], mappedTo[1]);
+        SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+                sf.getFeatureGroup(), sf.getScore());
+        copyTo.addSequenceFeature(copy);
+        count++;
+      }
     }
     return count;
   }
@@ -2210,49 +2222,44 @@ public class AlignmentUtils
   public static List<int[]> findCdsPositions(SequenceI dnaSeq)
   {
     List<int[]> result = new ArrayList<int[]>();
-    SequenceFeature[] sfs = dnaSeq.getSequenceFeatures();
-    if (sfs == null)
+
+    List<SequenceFeature> sfs = dnaSeq.getFeatures().getFeaturesByOntology(
+            SequenceOntologyI.CDS);
+    if (sfs.isEmpty())
     {
       return result;
     }
-
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    SequenceFeatures.sortFeatures(sfs, true);
     int startPhase = 0;
 
     for (SequenceFeature sf : sfs)
     {
+      int phase = 0;
+      try
+      {
+        phase = Integer.parseInt(sf.getPhase());
+      } catch (NumberFormatException e)
+      {
+        // ignore
+      }
       /*
-       * process a CDS feature (or a sub-type of CDS)
+       * phase > 0 on first codon means 5' incomplete - skip to the start
+       * of the next codon; example ENST00000496384
        */
-      if (so.isA(sf.getType(), SequenceOntologyI.CDS))
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
+      if (result.isEmpty())
       {
-        int phase = 0;
-        try
-        {
-          phase = Integer.parseInt(sf.getPhase());
-        } catch (NumberFormatException e)
-        {
-          // ignore
-        }
-        /*
-         * phase > 0 on first codon means 5' incomplete - skip to the start
-         * of the next codon; example ENST00000496384
-         */
-        int begin = sf.getBegin();
-        int end = sf.getEnd();
-        if (result.isEmpty())
+        begin += phase;
+        if (begin > end)
         {
-          begin += phase;
-          if (begin > end)
-          {
-            // shouldn't happen!
-            System.err.println(
-                    "Error: start phase extends beyond start CDS in "
-                            + dnaSeq.getName());
-          }
+          // shouldn't happen!
+          System.err
+                  .println("Error: start phase extends beyond start CDS in "
+                          + dnaSeq.getName());
         }
-        result.add(new int[] { begin, end });
       }
+      result.add(new int[] { begin, end });
     }
 
     /*
@@ -2272,7 +2279,7 @@ public class AlignmentUtils
      * ranges are assembled in order. Other cases should not use this method,
      * but instead construct an explicit mapping for CDS (e.g. EMBL parsing).
      */
-    Collections.sort(result, new RangeComparator(true));
+    Collections.sort(result, IntRangeComparator.ASCENDING);
     return result;
   }
 
@@ -2325,24 +2332,6 @@ public class AlignmentUtils
       count += computePeptideVariants(peptide, peptidePos, codonVariants);
     }
 
-    /*
-     * sort to get sequence features in start position order
-     * - would be better to store in Sequence as a TreeSet or NCList?
-     */
-    if (peptide.getSequenceFeatures() != null)
-    {
-      Arrays.sort(peptide.getSequenceFeatures(),
-              new Comparator<SequenceFeature>()
-              {
-                @Override
-                public int compare(SequenceFeature o1, SequenceFeature o2)
-                {
-                  int c = Integer.compare(o1.getBegin(), o2.getBegin());
-                  return c == 0 ? Integer.compare(o1.getEnd(), o2.getEnd())
-                          : c;
-                }
-              });
-    }
     return count;
   }
 
@@ -2470,10 +2459,9 @@ public class AlignmentUtils
       String trans3Char = StringUtils
               .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
       String desc = "p." + residue3Char + peptidePos + trans3Char;
-      // set score to 0f so 'graduated colour' option is offered! JAL-2060
       SequenceFeature sf = new SequenceFeature(
               SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
-              peptidePos, 0f, var.getSource());
+              peptidePos, var.getSource());
       StringBuilder attributes = new StringBuilder(32);
       String id = (String) var.variant.getValue(ID);
       if (id != null)
@@ -2531,10 +2519,10 @@ public class AlignmentUtils
      * LinkedHashMap ensures we keep the peptide features in sequence order
      */
     LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<Integer, List<DnaVariant>[]>();
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
 
-    SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures();
-    if (dnaFeatures == null)
+    List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
+    if (dnaFeatures.isEmpty())
     {
       return variants;
     }
@@ -2554,84 +2542,80 @@ public class AlignmentUtils
         // not handling multi-locus variant features
         continue;
       }
-      if (so.isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT))
+      int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
+      if (mapsTo == null)
       {
-        int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
-        if (mapsTo == null)
-        {
-          // feature doesn't lie within coding region
-          continue;
-        }
-        int peptidePosition = mapsTo[0];
-        List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
-        if (codonVariants == null)
-        {
-          codonVariants = new ArrayList[CODON_LENGTH];
-          codonVariants[0] = new ArrayList<DnaVariant>();
-          codonVariants[1] = new ArrayList<DnaVariant>();
-          codonVariants[2] = new ArrayList<DnaVariant>();
-          variants.put(peptidePosition, codonVariants);
-        }
+        // feature doesn't lie within coding region
+        continue;
+      }
+      int peptidePosition = mapsTo[0];
+      List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
+      if (codonVariants == null)
+      {
+        codonVariants = new ArrayList[CODON_LENGTH];
+        codonVariants[0] = new ArrayList<DnaVariant>();
+        codonVariants[1] = new ArrayList<DnaVariant>();
+        codonVariants[2] = new ArrayList<DnaVariant>();
+        variants.put(peptidePosition, codonVariants);
+      }
 
-        /*
-         * extract dna variants to a string array
-         */
-        String alls = (String) sf.getValue("alleles");
-        if (alls == null)
-        {
-          continue;
-        }
-        String[] alleles = alls.toUpperCase().split(",");
-        int i = 0;
-        for (String allele : alleles)
-        {
-          alleles[i++] = allele.trim(); // lose any space characters "A, G"
-        }
+      /*
+       * extract dna variants to a string array
+       */
+      String alls = (String) sf.getValue("alleles");
+      if (alls == null)
+      {
+        continue;
+      }
+      String[] alleles = alls.toUpperCase().split(",");
+      int i = 0;
+      for (String allele : alleles)
+      {
+        alleles[i++] = allele.trim(); // lose any space characters "A, G"
+      }
 
-        /*
-         * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
-         */
-        int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
-                : MappingUtils.flattenRanges(dnaToProtein
-                        .locateInFrom(peptidePosition, peptidePosition));
-        lastPeptidePostion = peptidePosition;
-        lastCodon = codon;
+      /*
+       * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
+       */
+      int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
+              : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
+                      peptidePosition, peptidePosition));
+      lastPeptidePostion = peptidePosition;
+      lastCodon = codon;
 
-        /*
-         * save nucleotide (and any variant) for each codon position
-         */
-        for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
+      /*
+       * save nucleotide (and any variant) for each codon position
+       */
+      for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
+      {
+        String nucleotide = String.valueOf(
+                dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase();
+        List<DnaVariant> codonVariant = codonVariants[codonPos];
+        if (codon[codonPos] == dnaCol)
         {
-          String nucleotide = String
-                  .valueOf(dnaSeq.getCharAt(codon[codonPos] - dnaStart))
-                  .toUpperCase();
-          List<DnaVariant> codonVariant = codonVariants[codonPos];
-          if (codon[codonPos] == dnaCol)
+          if (!codonVariant.isEmpty()
+                  && codonVariant.get(0).variant == null)
           {
-            if (!codonVariant.isEmpty()
-                    && codonVariant.get(0).variant == null)
-            {
-              /*
-               * already recorded base value, add this variant
-               */
-              codonVariant.get(0).variant = sf;
-            }
-            else
-            {
-              /*
-               * add variant with base value
-               */
-              codonVariant.add(new DnaVariant(nucleotide, sf));
-            }
+            /*
+             * already recorded base value, add this variant
+             */
+            codonVariant.get(0).variant = sf;
           }
-          else if (codonVariant.isEmpty())
+          else
           {
             /*
-             * record (possibly non-varying) base value
+             * add variant with base value
              */
-            codonVariant.add(new DnaVariant(nucleotide));
+            codonVariant.add(new DnaVariant(nucleotide, sf));
           }
         }
+        else if (codonVariant.isEmpty())
+        {
+          /*
+           * record (possibly non-varying) base value
+           */
+          codonVariant.add(new DnaVariant(nucleotide));
+        }
       }
     }
     return variants;
@@ -2910,9 +2894,7 @@ public class AlignmentUtils
               seqMap.getMap().getInverse());
     }
 
-    char[] fromChars = fromSeq.getSequence();
     int toStart = seq.getStart();
-    char[] toChars = seq.getSequence();
 
     /*
      * traverse [start, end, start, end...] ranges in fromSeq
@@ -2943,10 +2925,10 @@ public class AlignmentUtils
          * of the next character of the mapped-to sequence; stop when all
          * the characters of the range have been counted
          */
-        while (mappedCharPos <= range[1] && fromCol <= fromChars.length
+        while (mappedCharPos <= range[1] && fromCol <= fromSeq.getLength()
                 && fromCol >= 0)
         {
-          if (!Comparison.isGap(fromChars[fromCol - 1]))
+          if (!Comparison.isGap(fromSeq.getCharAt(fromCol - 1)))
           {
             /*
              * mapped from sequence has a character in this column
@@ -2958,7 +2940,7 @@ public class AlignmentUtils
               seqsMap = new HashMap<SequenceI, Character>();
               map.put(fromCol, seqsMap);
             }
-            seqsMap.put(seq, toChars[mappedCharPos - toStart]);
+            seqsMap.put(seq, seq.getCharAt(mappedCharPos - toStart));
             mappedCharPos++;
           }
           fromCol += (forward ? 1 : -1);
index ba4f705..131b39c 100755 (executable)
@@ -736,28 +736,23 @@ public class Conservation
   public void completeAnnotations(AlignmentAnnotation conservation,
           AlignmentAnnotation quality2, int istart, int alWidth)
   {
-    char[] sequence = getConsSequence().getSequence();
-    float minR;
-    float minG;
-    float minB;
-    float maxR;
-    float maxG;
-    float maxB;
-    minR = 0.3f;
-    minG = 0.0f;
-    minB = 0f;
-    maxR = 1.0f - minR;
-    maxG = 0.9f - minG;
-    maxB = 0f - minB; // scalable range for colouring both Conservation and
-    // Quality
+    SequenceI cons = getConsSequence();
+
+    /*
+     * colour scale for Conservation and Quality;
+     */
+    float minR = 0.3f;
+    float minG = 0.0f;
+    float minB = 0f;
+    float maxR = 1.0f - minR;
+    float maxG = 0.9f - minG;
+    float maxB = 0f - minB;
 
     float min = 0f;
     float max = 11f;
     float qmin = 0f;
     float qmax = 0f;
 
-    char c;
-
     if (conservation != null && conservation.annotations != null
             && conservation.annotations.length < alWidth)
     {
@@ -780,7 +775,7 @@ public class Conservation
     {
       float value = 0;
 
-      c = sequence[i];
+      char c = cons.getCharAt(i);
 
       if (Character.isDigit(c))
       {
@@ -866,8 +861,8 @@ public class Conservation
    */
   String getTooltip(int column)
   {
-    char[] sequence = getConsSequence().getSequence();
-    char val = column < sequence.length ? sequence[column] : '-';
+    SequenceI cons = getConsSequence();
+    char val = column < cons.getLength() ? cons.getCharAt(column) : '-';
     boolean hasConservation = val != '-' && val != '0';
     int consp = column - start;
     String tip = (hasConservation && consp > -1 && consp < consSymbs.length)
index 1a56393..e6bae9b 100644 (file)
@@ -619,28 +619,25 @@ public class CrossRef
                  * duplication (e.g. same variation from two 
                  * transcripts)
                  */
-                SequenceFeature[] sfs = ms.getSequenceFeatures();
-                if (sfs != null)
+                List<SequenceFeature> sfs = ms.getFeatures()
+                        .getAllFeatures();
+                for (SequenceFeature feat : sfs)
                 {
-                  for (SequenceFeature feat : sfs)
+                  /*
+                   * make a flyweight feature object which ignores Parent
+                   * attribute in equality test; this avoids creating many
+                   * otherwise duplicate exon features on genomic sequence
+                   */
+                  SequenceFeature newFeature = new SequenceFeature(feat)
                   {
-                    /*
-                     * make a flyweight feature object which ignores Parent
-                     * attribute in equality test; this avoids creating many
-                     * otherwise duplicate exon features on genomic sequence
-                     */
-                    SequenceFeature newFeature = new SequenceFeature(feat)
+                    @Override
+                    public boolean equals(Object o)
                     {
-                      @Override
-                      public boolean equals(Object o)
-                      {
-                        return super.equals(o, true);
-                      }
-                    };
-                    matched.addSequenceFeature(newFeature);
-                  }
+                      return super.equals(o, true);
+                    }
+                  };
+                  matched.addSequenceFeature(newFeature);
                 }
-
               }
               cf.addMap(retrievedSequence, map.getTo(), map.getMap());
             } catch (Exception e)
@@ -785,15 +782,15 @@ public class CrossRef
     {
       return false;
     }
-    char[] c1 = seq1.getSequence();
-    char[] c2 = seq2.getSequence();
-    if (c1.length != c2.length)
+
+    if (seq1.getLength() != seq2.getLength())
     {
       return false;
     }
-    for (int i = 0; i < c1.length; i++)
+    int length = seq1.getLength();
+    for (int i = 0; i < length; i++)
     {
-      int diff = c1[i] - c2[i];
+      int diff = seq1.getCharAt(i) - seq2.getCharAt(i);
       /*
        * same char or differ in case only ('a'-'A' == 32)
        */
index 0128624..a10b037 100644 (file)
@@ -45,7 +45,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 
 public class Dna
 {
@@ -120,8 +119,7 @@ public class Dna
    * @param ac2
    * @return
    */
-  public static final int compareCodonPos(AlignedCodon ac1,
-          AlignedCodon ac2)
+  public static final int compareCodonPos(AlignedCodon ac1, AlignedCodon ac2)
   {
     return comparator.compare(ac1, ac2);
     // return jalview_2_8_2compare(ac1, ac2);
@@ -689,7 +687,7 @@ public class Dna
          */
         MapList map = new MapList(scontigs, new int[] { 1, resSize }, 3, 1);
 
-        transferCodedFeatures(selection, newseq, map, null, null);
+        transferCodedFeatures(selection, newseq, map);
 
         /*
          * Construct a dataset sequence for our new peptide.
@@ -758,25 +756,15 @@ public class Dna
 
   /**
    * Given a peptide newly translated from a dna sequence, copy over and set any
-   * features on the peptide from the DNA. If featureTypes is null, all features
-   * on the dna sequence are searched (rather than just the displayed ones), and
-   * similarly for featureGroups.
+   * features on the peptide from the DNA.
    * 
    * @param dna
    * @param pep
    * @param map
-   * @param featureTypes
-   *          hash whose keys are the displayed feature type strings
-   * @param featureGroups
-   *          hash where keys are feature groups and values are Boolean objects
-   *          indicating if they are displayed.
    */
   private static void transferCodedFeatures(SequenceI dna, SequenceI pep,
-          MapList map, Map<String, Object> featureTypes,
-          Map<String, Boolean> featureGroups)
+          MapList map)
   {
-    SequenceFeature[] sfs = dna.getSequenceFeatures();
-    Boolean fgstate;
     DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
             DBRefSource.DNACODINGDBS);
     if (dnarefs != null)
@@ -790,24 +778,15 @@ public class Dna
         }
       }
     }
-    if (sfs != null)
+    for (SequenceFeature sf : dna.getFeatures().getAllFeatures())
     {
-      for (SequenceFeature sf : sfs)
-      {
-        fgstate = (featureGroups == null) ? null
-                : featureGroups.get(sf.featureGroup);
-        if ((featureTypes == null || featureTypes.containsKey(sf.getType()))
-                && (fgstate == null || fgstate.booleanValue()))
+        if (FeatureProperties.isCodingFeature(null, sf.getType()))
         {
-          if (FeatureProperties.isCodingFeature(null, sf.getType()))
+          // if (map.intersectsFrom(sf[f].begin, sf[f].end))
           {
-            // if (map.intersectsFrom(sf[f].begin, sf[f].end))
-            {
 
-            }
           }
         }
-      }
     }
   }
 
index 94544e7..0d39abf 100644 (file)
@@ -31,10 +31,11 @@ import jalview.datamodel.SequenceFeature;
 import jalview.util.MessageManager;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 import java.util.Stack;
-import java.util.Vector;
 
 public class Rna
 {
@@ -134,11 +135,11 @@ public class Rna
    * @return
    * @throw {@link WUSSParseException}
    */
-  public static Vector<SimpleBP> getSimpleBPs(CharSequence line)
+  protected static List<SimpleBP> getSimpleBPs(CharSequence line)
           throws WUSSParseException
   {
     Hashtable<Character, Stack<Integer>> stacks = new Hashtable<Character, Stack<Integer>>();
-    Vector<SimpleBP> pairs = new Vector<SimpleBP>();
+    List<SimpleBP> pairs = new ArrayList<SimpleBP>();
     int i = 0;
     while (i < line.length())
     {
@@ -197,25 +198,9 @@ public class Rna
     return pairs;
   }
 
-  public static SequenceFeature[] getBasePairs(List<SimpleBP> bps)
-          throws WUSSParseException
-  {
-    SequenceFeature[] outPairs = new SequenceFeature[bps.size()];
-    for (int p = 0; p < bps.size(); p++)
-    {
-      SimpleBP bp = bps.get(p);
-      outPairs[p] = new SequenceFeature("RNA helix", "", "", bp.getBP5(),
-              bp.getBP3(), "");
-    }
-    return outPairs;
-  }
+  
 
-  public static List<SimpleBP> getModeleBP(CharSequence line)
-          throws WUSSParseException
-  {
-    Vector<SimpleBP> bps = getSimpleBPs(line);
-    return new ArrayList<SimpleBP>(bps);
-  }
+  
 
   /**
    * Function to get the end position corresponding to a given start position
@@ -232,88 +217,6 @@ public class Rna
    */
 
   /**
-   * Figures out which helix each position belongs to and stores the helix
-   * number in the 'featureGroup' member of a SequenceFeature Based off of RALEE
-   * code ralee-helix-map.
-   * 
-   * @param pairs
-   *          Array of SequenceFeature (output from Rna.GetBasePairs)
-   */
-  public static void HelixMap(SequenceFeature[] pairs)
-  {
-
-    int helix = 0; // Number of helices/current helix
-    int lastopen = 0; // Position of last open bracket reviewed
-    int lastclose = 9999999; // Position of last close bracket reviewed
-    int i = pairs.length; // Number of pairs
-
-    int open; // Position of an open bracket under review
-    int close; // Position of a close bracket under review
-    int j; // Counter
-
-    Hashtable<Integer, Integer> helices = new Hashtable<Integer, Integer>();
-    // Keep track of helix number for each position
-
-    // Go through each base pair and assign positions a helix
-    for (i = 0; i < pairs.length; i++)
-    {
-
-      open = pairs[i].getBegin();
-      close = pairs[i].getEnd();
-
-      // System.out.println("open " + open + " close " + close);
-      // System.out.println("lastclose " + lastclose + " lastopen " + lastopen);
-
-      // we're moving from right to left based on closing pair
-      /*
-       * catch things like <<..>>..<<..>> |
-       */
-      if (open > lastclose)
-      {
-        helix++;
-      }
-
-      /*
-       * catch things like <<..<<..>>..<<..>>>> |
-       */
-      j = pairs.length - 1;
-      while (j >= 0)
-      {
-        int popen = pairs[j].getBegin();
-
-        // System.out.println("j " + j + " popen " + popen + " lastopen "
-        // +lastopen + " open " + open);
-        if ((popen < lastopen) && (popen > open))
-        {
-          if (helices.containsValue(popen)
-                  && ((helices.get(popen)) == helix))
-          {
-            continue;
-          }
-          else
-          {
-            helix++;
-            break;
-          }
-        }
-
-        j -= 1;
-      }
-
-      // Put positions and helix information into the hashtable
-      helices.put(open, helix);
-      helices.put(close, helix);
-
-      // Record helix as featuregroup
-      pairs[i].setFeatureGroup(Integer.toString(helix));
-
-      lastopen = open;
-      lastclose = close;
-
-    }
-  }
-
-  /**
    * Answers true if the character is a recognised symbol for RNA secondary
    * structure. Currently accepts a-z, A-Z, ()[]{}<>.
    * 
@@ -502,4 +405,76 @@ public class Rna
       return c;
     }
   }
+
+  public static SequenceFeature[] getHelixMap(CharSequence rnaAnnotation)
+          throws WUSSParseException
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    int helix = 0; // Number of helices/current helix
+    int lastopen = 0; // Position of last open bracket reviewed
+    int lastclose = 9999999; // Position of last close bracket reviewed
+
+    Map<Integer, Integer> helices = new HashMap<Integer, Integer>();
+    // Keep track of helix number for each position
+
+    // Go through each base pair and assign positions a helix
+    List<SimpleBP> bps = getSimpleBPs(rnaAnnotation);
+    for (SimpleBP basePair : bps)
+    {
+      final int open = basePair.getBP5();
+      final int close = basePair.getBP3();
+
+      // System.out.println("open " + open + " close " + close);
+      // System.out.println("lastclose " + lastclose + " lastopen " + lastopen);
+
+      // we're moving from right to left based on closing pair
+      /*
+       * catch things like <<..>>..<<..>> |
+       */
+      if (open > lastclose)
+      {
+        helix++;
+      }
+
+      /*
+       * catch things like <<..<<..>>..<<..>>>> |
+       */
+      int j = bps.size() - 1;
+      while (j >= 0)
+      {
+        int popen = bps.get(j).getBP5();
+
+        // System.out.println("j " + j + " popen " + popen + " lastopen "
+        // +lastopen + " open " + open);
+        if ((popen < lastopen) && (popen > open))
+        {
+          if (helices.containsValue(popen)
+                  && ((helices.get(popen)) == helix))
+          {
+            continue;
+          }
+          else
+          {
+            helix++;
+            break;
+          }
+        }
+        j -= 1;
+      }
+
+      // Put positions and helix information into the hashtable
+      helices.put(open, helix);
+      helices.put(close, helix);
+
+      // Record helix as featuregroup
+      result.add(new SequenceFeature("RNA helix", "", open, close,
+              String.valueOf(helix)));
+
+      lastopen = open;
+      lastclose = close;
+    }
+
+    return result.toArray(new SequenceFeature[result.size()]);
+  }
 }
index 2b21e5e..fabd0c6 100755 (executable)
@@ -27,6 +27,7 @@ import jalview.datamodel.SequenceI;
 
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Vector;
 
 public class SeqsetUtils
@@ -50,15 +51,11 @@ public class SeqsetUtils
     {
       sqinfo.put("Description", seq.getDescription());
     }
-    Vector sfeat = new Vector();
-    jalview.datamodel.SequenceFeature[] sfarray = seq.getSequenceFeatures();
-    if (sfarray != null && sfarray.length > 0)
-    {
-      for (int i = 0; i < sfarray.length; i++)
-      {
-        sfeat.addElement(sfarray[i]);
-      }
-    }
+
+    Vector<SequenceFeature> sfeat = new Vector<SequenceFeature>();
+    List<SequenceFeature> sfs = seq.getFeatures().getAllFeatures();
+    sfeat.addAll(sfs);
+
     if (seq.getDatasetSequence() == null)
     {
       sqinfo.put("SeqFeatures", sfeat);
@@ -95,7 +92,8 @@ public class SeqsetUtils
     String oldname = (String) sqinfo.get("Name");
     Integer start = (Integer) sqinfo.get("Start");
     Integer end = (Integer) sqinfo.get("End");
-    Vector sfeatures = (Vector) sqinfo.get("SeqFeatures");
+    Vector<SequenceFeature> sfeatures = (Vector<SequenceFeature>) sqinfo
+            .get("SeqFeatures");
     Vector<PDBEntry> pdbid = (Vector<PDBEntry>) sqinfo.get("PdbId");
     String description = (String) sqinfo.get("Description");
     Sequence seqds = (Sequence) sqinfo.get("datasetSequence");
@@ -118,14 +116,9 @@ public class SeqsetUtils
       sq.setEnd(end.intValue());
     }
 
-    if ((sfeatures != null) && (sfeatures.size() > 0))
+    if (sfeatures != null && !sfeatures.isEmpty())
     {
-      SequenceFeature[] sfarray = new SequenceFeature[sfeatures.size()];
-      for (int is = 0, isize = sfeatures.size(); is < isize; is++)
-      {
-        sfarray[is] = (SequenceFeature) sfeatures.elementAt(is);
-      }
-      sq.setSequenceFeatures(sfarray);
+      sq.setSequenceFeatures(sfeatures);
     }
     if (description != null)
     {
index c20d4f1..e506be2 100644 (file)
@@ -177,10 +177,12 @@ public class FeatureDistanceModel extends DistanceScoreModel
   /**
    * Builds and returns a map containing a (possibly empty) list (one per
    * SeqCigar) of visible feature types at the given column position. The map
-   * has no entry for sequences which are gapped at the column position.
+   * does not include entries for features which straddle a gapped column
+   * positions.
    * 
    * @param seqs
    * @param columnPosition
+   *          (0..)
    * @return
    */
   protected Map<SeqCigar, Set<String>> findFeatureTypesAtColumn(
@@ -192,9 +194,12 @@ public class FeatureDistanceModel extends DistanceScoreModel
       int spos = seq.findPosition(columnPosition);
       if (spos != -1)
       {
+        /*
+         * position is not a gap
+         */
         Set<String> types = new HashSet<String>();
-        List<SequenceFeature> sfs = fr.findFeaturesAtRes(seq.getRefSeq(),
-                spos);
+        List<SequenceFeature> sfs = fr.findFeaturesAtResidue(
+                seq.getRefSeq(), spos);
         for (SequenceFeature sf : sfs)
         {
           types.add(sf.getType());
index 01eb7fa..0ded079 100644 (file)
@@ -146,7 +146,9 @@ public interface FeatureColourI
   boolean hasThreshold();
 
   /**
-   * Returns the computed colour for the given sequence feature
+   * Returns the computed colour for the given sequence feature. Answers null if
+   * the score of this feature instance is outside the range to render (if any),
+   * i.e. lies below or above a configured threshold.
    * 
    * @param feature
    * @return
@@ -154,17 +156,6 @@ public interface FeatureColourI
   Color getColor(SequenceFeature feature);
 
   /**
-   * Answers true if the feature has a simple colour, or is coloured by label,
-   * or has a graduated colour and the score of this feature instance is within
-   * the range to render (if any), i.e. does not lie below or above any
-   * threshold set.
-   * 
-   * @param feature
-   * @return
-   */
-  boolean isColored(SequenceFeature feature);
-
-  /**
    * Update the min-max range for a graduated colour scheme
    * 
    * @param min
index 7123b8c..9d2d7f4 100644 (file)
@@ -60,6 +60,7 @@ public interface FeatureRenderer
    * 
    * @param sequence
    * @param column
+   *          aligned column position (1..)
    * @param g
    * @return
    */
@@ -147,14 +148,27 @@ public interface FeatureRenderer
   void setGroupVisibility(String group, boolean visible);
 
   /**
-   * Returns features at the specified position on the given sequence.
+   * Returns features at the specified aligned column on the given sequence.
+   * Non-positional features are not included. If the column has a gap, then
+   * enclosing features are included (but not contact features).
+   * 
+   * @param sequence
+   * @param column
+   *          aligned column position (1..)
+   * @return
+   */
+  List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column);
+
+  /**
+   * Returns features at the specified residue position on the given sequence.
    * Non-positional features are not included.
    * 
    * @param sequence
-   * @param res
+   * @param resNo
+   *          residue position (start..)
    * @return
    */
-  List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res);
+  List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence, int resNo);
 
   /**
    * get current displayed types, in ordering of rendering (on top last)
@@ -165,9 +179,9 @@ public interface FeatureRenderer
   List<String> getDisplayedFeatureTypes();
 
   /**
-   * get current displayed groups
+   * Returns a (possibly empty) list of currently visible feature groups
    * 
-   * @return a (possibly empty) list of feature groups
+   * @return
    */
   List<String> getDisplayedFeatureGroups();
 
@@ -200,4 +214,5 @@ public interface FeatureRenderer
    * @return
    */
   float getTransparency();
+
 }
index 32b0565..e69785f 100644 (file)
 package jalview.api;
 
 import java.util.Collection;
-import java.util.Iterator;
+import java.util.Set;
 
 public interface FeaturesDisplayedI
 {
 
-  Iterator<String> getVisibleFeatures();
+  /**
+   * answers an unmodifiable view of the set of visible feature types
+   */
+  Set<String> getVisibleFeatures();
 
   boolean isVisible(String featureType);
 
@@ -36,6 +39,12 @@ public interface FeaturesDisplayedI
 
   void setVisible(String featureType);
 
+  /**
+   * Sets all the specified feature types to visible. Visibility of other
+   * feature types is not changed.
+   * 
+   * @param featureTypes
+   */
   void setAllVisible(Collection<String> featureTypes);
 
   boolean isRegistered(String type);
index 01b1a30..0ce4116 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.api;
 
 import java.beans.PropertyChangeListener;
index 6fdb49c..9d44479 100644 (file)
@@ -835,7 +835,7 @@ public class APopupMenu extends java.awt.PopupMenu
         if (start <= end)
         {
           seqs.add(sg.getSequenceAt(i));
-          features.add(new SequenceFeature(null, null, null, start, end,
+          features.add(new SequenceFeature(null, null, start, end,
                   "Jalview"));
         }
       }
@@ -847,7 +847,8 @@ public class APopupMenu extends java.awt.PopupMenu
         {
           ap.alignFrame.sequenceFeatures.setState(true);
           ap.av.setShowSequenceFeatures(true);
-          ap.highlightSearchResults(null);
+          ap.av.setSearchResults(null); // clear highlighting
+          ap.repaint(); // draw new/amended features
         }
       }
     }
index b48dec9..ed04a0a 100644 (file)
@@ -1426,21 +1426,32 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     return null;
   }
 
+  private List<String> getDisplayedFeatureGroups()
+  {
+    if (alignPanel.getFeatureRenderer() != null
+            && viewport.getFeaturesDisplayed() != null)
+    {
+      return alignPanel.getFeatureRenderer().getDisplayedFeatureGroups();
+
+    }
+    return null;
+  }
+
   public String outputFeatures(boolean displayTextbox, String format)
   {
     String features;
     FeaturesFile formatter = new FeaturesFile();
     if (format.equalsIgnoreCase("Jalview"))
     {
-      features = formatter.printJalviewFormat(
-              viewport.getAlignment().getSequencesArray(),
-              getDisplayedFeatureCols());
+      features = formatter.printJalviewFormat(viewport.getAlignment()
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
     else
     {
-      features = formatter.printGffFormat(
-              viewport.getAlignment().getSequencesArray(),
-              getDisplayedFeatureCols());
+      features = formatter.printGffFormat(viewport.getAlignment()
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
 
     if (displayTextbox)
@@ -1725,7 +1736,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       EditCommand editCommand = (EditCommand) command;
       al = editCommand.getAlignment();
-      Vector comps = (Vector) PaintRefresher.components
+      Vector comps = PaintRefresher.components
               .get(viewport.getSequenceSetId());
       for (int i = 0; i < comps.size(); i++)
       {
@@ -2485,7 +2496,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     PaintRefresher.Register(newaf.alignPanel.seqPanel.seqCanvas,
             newaf.alignPanel.av.getSequenceSetId());
 
-    Vector comps = (Vector) PaintRefresher.components
+    Vector comps = PaintRefresher.components
             .get(viewport.getSequenceSetId());
     int viewSize = -1;
     for (int i = 0; i < comps.size(); i++)
index 0d479f9..5a073c6 100644 (file)
@@ -42,6 +42,8 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseEvent;
@@ -232,6 +234,14 @@ public class FeatureColourChooser extends Panel implements ActionListener,
     threshold.addItem(MessageManager
             .getString("label.threshold_feature_below_threshold"));
     thresholdValue.addActionListener(this);
+    thresholdValue.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
     slider.setBackground(Color.white);
     slider.setEnabled(false);
     slider.setSize(new Dimension(93, 21));
@@ -272,19 +282,7 @@ public class FeatureColourChooser extends Panel implements ActionListener,
   {
     if (evt.getSource() == thresholdValue)
     {
-      try
-      {
-        float f = new Float(thresholdValue.getText()).floatValue();
-        slider.setValue((int) (f * SCALE_FACTOR_1K));
-        adjustmentValueChanged(null);
-
-        /*
-         * force repaint of any Overview window or structure
-         */
-        changeColour(true);
-      } catch (NumberFormatException ex)
-      {
-      }
+      thresholdValue_actionPerformed();
     }
     else if (evt.getSource() == minColour)
     {
@@ -300,6 +298,26 @@ public class FeatureColourChooser extends Panel implements ActionListener,
     }
   }
 
+  /**
+   * Action on input of a value for colour score threshold
+   */
+  protected void thresholdValue_actionPerformed()
+  {
+    try
+    {
+      float f = new Float(thresholdValue.getText()).floatValue();
+      slider.setValue((int) (f * SCALE_FACTOR_1K));
+      adjustmentValueChanged(null);
+
+      /*
+       * force repaint of any Overview window or structure
+       */
+      changeColour(true);
+    } catch (NumberFormatException ex)
+    {
+    }
+  }
+
   @Override
   public void itemStateChanged(ItemEvent evt)
   {
index 194b18f..b35c079 100644 (file)
@@ -399,11 +399,14 @@ public class FeatureRenderer
     /*
      * only update default type and group if we used defaults
      */
-    String enteredType = name.getText().trim();
+    final String enteredType = name.getText().trim();
+    final String enteredGroup = group.getText().trim();
+    final String enteredDesc = description.getText().replace('\n', ' ');
+
     if (dialog.accept && useLastDefaults)
     {
       lastFeatureAdded = enteredType;
-      lastFeatureGroupAdded = group.getText().trim();
+      lastFeatureGroupAdded = enteredGroup;
     }
 
     if (!create)
@@ -411,29 +414,36 @@ public class FeatureRenderer
       SequenceFeature sf = features.get(featureIndex);
       if (dialog.accept)
       {
-        sf.type = enteredType;
-        sf.featureGroup = group.getText().trim();
-        if (sf.featureGroup != null && sf.featureGroup.length() < 1)
-        {
-          sf.featureGroup = null;
-        }
-        sf.description = description.getText().replace('\n', ' ');
         if (!colourPanel.isGcol)
         {
           // update colour - otherwise its already done.
-          setColour(sf.type,
+          setColour(enteredType,
                   new FeatureColour(colourPanel.getBackground()));
         }
+        int newBegin = sf.begin;
+        int newEnd = sf.end;
         try
         {
-          sf.begin = Integer.parseInt(start.getText());
-          sf.end = Integer.parseInt(end.getText());
+          newBegin = Integer.parseInt(start.getText());
+          newEnd = Integer.parseInt(end.getText());
         } catch (NumberFormatException ex)
         {
-          //
+          // 
         }
-        boolean typeOrGroupChanged = (!featureType.equals(sf.type)
-                || !featureGroup.equals(sf.featureGroup));
+
+        /*
+         * replace the feature by deleting it and adding a new one
+         * (to ensure integrity of SequenceFeatures data store)
+         */
+        sequences.get(0).deleteFeature(sf);
+        SequenceFeature newSf = new SequenceFeature(sf, enteredType,
+                newBegin, newEnd, enteredGroup, sf.getScore());
+        newSf.setDescription(enteredDesc);
+        ffile.parseDescriptionHTML(newSf, false);
+        // amend features dialog only updates one sequence at a time
+        sequences.get(0).addSequenceFeature(newSf);
+        boolean typeOrGroupChanged = (!featureType.equals(newSf.getType()) || !featureGroup
+                .equals(newSf.getFeatureGroup()));
 
         ffile.parseDescriptionHTML(sf, false);
         if (typeOrGroupChanged)
@@ -457,12 +467,11 @@ public class FeatureRenderer
       {
         for (int i = 0; i < sequences.size(); i++)
         {
-          features.get(i).type = enteredType;
-          features.get(i).featureGroup = group.getText().trim();
-          features.get(i).description = description.getText().replace('\n',
-                  ' ');
-          sequences.get(i).addSequenceFeature(features.get(i));
-          ffile.parseDescriptionHTML(features.get(i), false);
+          SequenceFeature sf = features.get(i);
+          SequenceFeature sf2 = new SequenceFeature(enteredType,
+                  enteredDesc, sf.getBegin(), sf.getEnd(), enteredGroup);
+          ffile.parseDescriptionHTML(sf2, false);
+          sequences.get(i).addSequenceFeature(sf2);
         }
 
         Color newColour = colourPanel.getBackground();
index 5e15364..20d4d74 100755 (executable)
@@ -23,7 +23,7 @@ package jalview.appletgui;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -56,13 +56,12 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Vector;
 
 public class FeatureSettings extends Panel
         implements ItemListener, MouseListener, MouseMotionListener,
@@ -378,36 +377,38 @@ public class FeatureSettings extends Panel
   // Group selection states
   void resetTable(boolean groupsChanged)
   {
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector<String> visibleChecks = new Vector<String>();
+    List<String> displayableTypes = new ArrayList<String>();
     Set<String> foundGroups = new HashSet<String>();
+
     AlignmentI alignment = av.getAlignment();
 
     for (int i = 0; i < alignment.getHeight(); i++)
     {
-      if (alignment.getSequenceAt(i).getSequenceFeatures() == null)
-      {
-        continue;
-      }
+      SequenceI seq = alignment.getSequenceAt(i);
 
-      tmpfeatures = alignment.getSequenceAt(i).getSequenceFeatures();
-      int index = 0;
-      while (index < tmpfeatures.length)
+      /*
+       * get the sequence's groups for positional features
+       * and keep track of which groups are visible
+       */
+      Set<String> groups = seq.getFeatures().getFeatureGroups(true);
+      Set<String> visibleGroups = new HashSet<String>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-        foundGroups.add(group);
-
+        // if (group == null || fr.checkGroupVisibility(group, true))
         if (group == null || checkGroupState(group))
         {
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
+          visibleGroups.add(group);
         }
-        index++;
       }
+      foundGroups.addAll(groups);
+
+      /*
+       * get distinct feature types for visible groups
+       * record distinct visible types
+       */
+      Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
+              visibleGroups.toArray(new String[visibleGroups.size()]));
+      displayableTypes.addAll(types);
     }
 
     /*
@@ -424,7 +425,7 @@ public class FeatureSettings extends Panel
     {
       comps = featurePanel.getComponents();
       check = (MyCheckbox) comps[i];
-      if (!visibleChecks.contains(check.type))
+      if (!displayableTypes.contains(check.type))
       {
         featurePanel.remove(i);
         cSize--;
@@ -441,24 +442,24 @@ public class FeatureSettings extends Panel
       {
         String item = rol.get(ro);
 
-        if (!visibleChecks.contains(item))
+        if (!displayableTypes.contains(item))
         {
           continue;
         }
 
-        visibleChecks.removeElement(item);
+        displayableTypes.remove(item);
 
         addCheck(false, item);
       }
     }
 
-    // now add checkboxes which should be visible,
-    // if they have not already been added
-    Enumeration<String> en = visibleChecks.elements();
-
-    while (en.hasMoreElements())
+    /*
+     * now add checkboxes which should be visible,
+     * if they have not already been added
+     */
+    for (String type : displayableTypes)
     {
-      addCheck(groupsChanged, en.nextElement().toString());
+      addCheck(groupsChanged, type);
     }
 
     featurePanel.setLayout(
index 3e089bd..675b862 100644 (file)
@@ -122,7 +122,7 @@ public class Finder extends Panel implements ActionListener
     for (SearchResultMatchI match : searchResults.getResults())
     {
       seqs.add(match.getSequence().getDatasetSequence());
-      features.add(new SequenceFeature(searchString, "Search Results", null,
+      features.add(new SequenceFeature(searchString, "Search Results",
               match.getStart(), match.getEnd(), "Search Results"));
     }
 
index 8ac02be..7d9d278 100755 (executable)
@@ -20,7 +20,6 @@
  */
 package jalview.appletgui;
 
-import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -57,11 +56,11 @@ public class IdPanel extends Panel
 
   UrlProviderI urlProvider = null;
 
-  public IdPanel(AlignViewport av, AlignmentPanel parent)
+  public IdPanel(AlignViewport viewport, AlignmentPanel parent)
   {
-    this.av = av;
+    this.av = viewport;
     alignPanel = parent;
-    idCanvas = new IdCanvas(av);
+    idCanvas = new IdCanvas(viewport);
     setLayout(new BorderLayout());
     add(idCanvas, BorderLayout.CENTER);
     idCanvas.addMouseListener(this);
@@ -72,12 +71,12 @@ public class IdPanel extends Panel
 
     // make a list of label,url pairs
     HashMap<String, String> urlList = new HashMap<String, String>();
-    if (av.applet != null)
+    if (viewport.applet != null)
     {
       for (int i = 1; i < 10; i++)
       {
-        label = av.applet.getParameter("linkLabel_" + i);
-        url = av.applet.getParameter("linkURL_" + i);
+        label = viewport.applet.getParameter("linkLabel_" + i);
+        url = viewport.applet.getParameter("linkURL_" + i);
 
         // only add non-null parameters
         if (label != null)
@@ -89,7 +88,7 @@ public class IdPanel extends Panel
       if (!urlList.isEmpty())
       {
         // set default as first entry in list
-        String defaultUrl = av.applet.getParameter("linkLabel_1");
+        String defaultUrl = viewport.applet.getParameter("linkLabel_1");
         UrlProviderFactoryI factory = new AppletUrlProviderFactory(
                 defaultUrl, urlList);
         urlProvider = factory.createUrlProvider();
@@ -106,64 +105,57 @@ public class IdPanel extends Panel
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
-    // look for non-pos features
     StringBuffer tooltiptext = new StringBuffer();
-    if (sequence != null)
+    if (sequence == null)
     {
-      if (sequence.getDescription() != null)
+      return;
+    }
+    if (sequence.getDescription() != null)
+    {
+      tooltiptext.append(sequence.getDescription());
+      tooltiptext.append("\n");
+    }
+
+    for (SequenceFeature sf : sequence.getFeatures()
+            .getNonPositionalFeatures())
+    {
+      boolean nl = false;
+      if (sf.getFeatureGroup() != null)
       {
-        tooltiptext.append(sequence.getDescription());
-        tooltiptext.append("\n");
+        tooltiptext.append(sf.getFeatureGroup());
+        nl = true;
       }
-
-      SequenceFeature sf[] = sequence.getSequenceFeatures();
-      for (int sl = 0; sf != null && sl < sf.length; sl++)
+      if (sf.getType() != null)
       {
-        if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0)
-        {
-          boolean nl = false;
-          if (sf[sl].getFeatureGroup() != null)
-          {
-            tooltiptext.append(sf[sl].getFeatureGroup());
-            nl = true;
-          }
-          ;
-          if (sf[sl].getType() != null)
-          {
-            tooltiptext.append(" ");
-            tooltiptext.append(sf[sl].getType());
-            nl = true;
-          }
-          ;
-          if (sf[sl].getDescription() != null)
-          {
-            tooltiptext.append(" ");
-            tooltiptext.append(sf[sl].getDescription());
-            nl = true;
-          }
-          ;
-          if (!Float.isNaN(sf[sl].getScore()) && sf[sl].getScore() != 0f)
-          {
-            tooltiptext.append(" Score = ");
-            tooltiptext.append(sf[sl].getScore());
-            nl = true;
-          }
-          ;
-          if (sf[sl].getStatus() != null && sf[sl].getStatus().length() > 0)
-          {
-            tooltiptext.append(" (");
-            tooltiptext.append(sf[sl].getStatus());
-            tooltiptext.append(")");
-            nl = true;
-          }
-          ;
-          if (nl)
-          {
-            tooltiptext.append("\n");
-          }
-        }
+        tooltiptext.append(" ");
+        tooltiptext.append(sf.getType());
+        nl = true;
+      }
+      if (sf.getDescription() != null)
+      {
+        tooltiptext.append(" ");
+        tooltiptext.append(sf.getDescription());
+        nl = true;
+      }
+      if (!Float.isNaN(sf.getScore()) && sf.getScore() != 0f)
+      {
+        tooltiptext.append(" Score = ");
+        tooltiptext.append(sf.getScore());
+        nl = true;
+      }
+      if (sf.getStatus() != null && sf.getStatus().length() > 0)
+      {
+        tooltiptext.append(" (");
+        tooltiptext.append(sf.getStatus());
+        tooltiptext.append(")");
+        nl = true;
+      }
+      if (nl)
+      {
+        tooltiptext.append("\n");
       }
     }
+
     if (tooltiptext.length() == 0)
     {
       // nothing to display - so clear tooltip if one is visible
@@ -290,10 +282,12 @@ public class IdPanel extends Panel
     if ((e.getModifiers()
             & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq);
+      SequenceI sq = av.getAlignment().getSequenceAt(seq);
 
-      // build a new links menu based on the current links + any non-positional
-      // features
+      /*
+       *  build a new links menu based on the current links
+       *  and any non-positional features
+       */
       List<String> nlinks;
       if (urlProvider != null)
       {
@@ -303,17 +297,14 @@ public class IdPanel extends Panel
       {
         nlinks = new ArrayList<String>();
       }
-      SequenceFeature sf[] = sq == null ? null : sq.getSequenceFeatures();
-      for (int sl = 0; sf != null && sl < sf.length; sl++)
+
+      for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
       {
-        if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0)
+        if (sf.links != null)
         {
-          if (sf[sl].links != null && sf[sl].links.size() > 0)
+          for (String link : sf.links)
           {
-            for (int l = 0, lSize = sf[sl].links.size(); l < lSize; l++)
-            {
-              nlinks.add(sf[sl].links.elementAt(l));
-            }
+            nlinks.add(link);
           }
         }
       }
@@ -426,9 +417,9 @@ public class IdPanel extends Panel
 
     boolean up = true;
 
-    public ScrollThread(boolean up)
+    public ScrollThread(boolean isUp)
     {
-      this.up = up;
+      this.up = isUp;
       start();
     }
 
index 28c5291..f59967d 100755 (executable)
@@ -559,8 +559,8 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     return annotations.adjustPanelHeight();
   }
 
-  private void drawPanel(Graphics g1, int startRes, int endRes,
-          int startSeq, int endSeq, int offset)
+  private void drawPanel(Graphics g1, final int startRes, final int endRes,
+          final int startSeq, final int endSeq, final int offset)
   {
 
     if (!av.hasHiddenColumns())
@@ -569,8 +569,8 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     }
     else
     {
-
       int screenY = 0;
+      final int screenYMax = endRes - startRes;
       int blockStart = startRes;
       int blockEnd = endRes;
 
@@ -588,13 +588,22 @@ public class SeqCanvas extends Panel implements ViewportListenerI
             continue;
           }
 
-          blockEnd = hideStart - 1;
+          /*
+           * draw up to just before the next hidden region, or the end of
+           * the visible region, whichever comes first
+           */
+          blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
+                  - screenY);
 
           g1.translate(screenY * avcharWidth, 0);
 
           draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
 
-          if (av.getShowHiddenMarkers())
+          /*
+           * draw the downline of the hidden column marker (ScalePanel draws the
+           * triangle on top) if we reached it
+           */
+          if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
           {
             g1.setColor(Color.blue);
             g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
@@ -607,14 +616,14 @@ public class SeqCanvas extends Panel implements ViewportListenerI
           screenY += blockEnd - blockStart + 1;
           blockStart = hideEnd + 1;
 
-          if (screenY > (endRes - startRes))
+          if (screenY > screenYMax)
           {
             // already rendered last block
             return;
           }
         }
       }
-      if (screenY <= (endRes - startRes))
+      if (screenY <= screenYMax)
       {
         // remaining visible region to render
         blockEnd = blockStart + (endRes - startRes) - screenY;
index be4f9e6..f36a8e2 100644 (file)
@@ -54,10 +54,8 @@ import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Vector;
 
 public class SeqPanel extends Panel implements MouseMotionListener,
@@ -530,7 +528,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     int seq = findSeq(evt);
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (seq < 0 || res < 0)
     {
@@ -566,14 +564,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         av.setSelectionGroup(null);
       }
 
-      int column = findRes(evt);
-      boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
-      List<SequenceFeature> features = findFeaturesAtRes(sequence,
-              sequence.findPosition(column));
-      if (isGapped)
-      {
-        removeAdjacentFeatures(features, column + 1, sequence);
-      }
+      int column = findColumn(evt);
+      List<SequenceFeature> features = findFeaturesAtColumn(sequence,
+              column + 1);
 
       if (!features.isEmpty())
       {
@@ -583,8 +576,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         seqCanvas.highlightSearchResults(highlight);
         seqCanvas.getFeatureRenderer().amendFeatures(
                 Collections.singletonList(sequence), features, false, ap);
-
-        seqCanvas.highlightSearchResults(null);
+        av.setSearchResults(null); // clear highlighting
+        seqCanvas.repaint(); // draw new/amended features
       }
     }
   }
@@ -610,7 +603,14 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   int wrappedBlock = -1;
 
-  int findRes(MouseEvent evt)
+  /**
+   * Returns the aligned sequence position (base 0) at the mouse position, or
+   * the closest visible one
+   * 
+   * @param evt
+   * @return
+   */
+  int findColumn(MouseEvent evt)
   {
     int res = 0;
     int x = evt.getX();
@@ -714,7 +714,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   {
 
     int seq = findSeq(evt);
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (seq < av.getAlignment().getHeight()
             && res < av.getAlignment().getSequenceAt(seq).getLength())
@@ -786,7 +786,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   @Override
   public void mouseMoved(MouseEvent evt)
   {
-    final int column = findRes(evt);
+    final int column = findColumn(evt);
     int seq = findSeq(evt);
 
     if (seq >= av.getAlignment().getHeight() || seq < 0 || column < 0)
@@ -869,12 +869,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
      */
     if (av.isShowSequenceFeatures())
     {
-      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
-              sequence.findPosition(column));
-      if (isGapped)
-      {
-        removeAdjacentFeatures(allFeatures, column + 1, sequence);
-      }
+      List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
+              column + 1);
       for (SequenceFeature sf : allFeatures)
       {
         tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
@@ -907,63 +903,18 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   }
 
   /**
-   * Removes from the list of features any that start after, or end before, the
-   * given column position. This allows us to retain only those features
-   * adjacent to a gapped position that straddle the position. Contact features
-   * that 'straddle' the position are also removed, since they are not 'at' the
-   * position.
+   * Returns features at the specified aligned column on the given sequence.
+   * Non-positional features are not included. If the column has a gap, then
+   * enclosing features are included (but not contact features).
    * 
-   * @param features
-   * @param column
-   *          alignment column (1..)
    * @param sequence
+   * @param column
+   *          (1..)
+   * @return
    */
-  protected void removeAdjacentFeatures(List<SequenceFeature> features,
-          int column, SequenceI sequence)
+  List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
   {
-    // TODO should this be an AlignViewController method (shared by gui)?
-    ListIterator<SequenceFeature> it = features.listIterator();
-    while (it.hasNext())
-    {
-      SequenceFeature sf = it.next();
-      if (sf.isContactFeature()
-              || sequence.findIndex(sf.getBegin()) > column
-              || sequence.findIndex(sf.getEnd()) < column)
-      {
-        it.remove();
-      }
-    }
-  }
-
-  List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res)
-  {
-    List<SequenceFeature> result = new ArrayList<>();
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-    if (features != null)
-    {
-      for (int i = 0; i < features.length; i++)
-      {
-        if (av.getFeaturesDisplayed() == null || !av.getFeaturesDisplayed()
-                .isVisible(features[i].getType()))
-        {
-          continue;
-        }
-
-        if (features[i].featureGroup != null && !seqCanvas.fr
-                .checkGroupVisibility(features[i].featureGroup, false))
-        {
-          continue;
-        }
-
-        if ((features[i].getBegin() <= res)
-                && (features[i].getEnd() >= res))
-        {
-          result.add(features[i]);
-        }
-      }
-    }
-
-    return result;
+    return seqCanvas.getFeatureRenderer().findFeaturesAtColumn(sequence, column);
   }
 
   Tooltip tooltip;
@@ -1043,7 +994,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (res < 0)
     {
@@ -1225,7 +1176,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         // Find the next gap before the end
         // of the visible region boundary
         boolean blank = false;
-        for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
+        for (; fixedRight > lastres; fixedRight--)
         {
           blank = true;
 
@@ -1451,11 +1402,11 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   {
     if (scrollThread != null)
     {
-      scrollThread.running = false;
+      scrollThread.threadRunning = false;
       scrollThread = null;
     }
 
-    int res = findRes(evt);
+    int column = findColumn(evt);
     int seq = findSeq(evt);
     oldSeq = seq;
     startWrapBlock = wrappedBlock;
@@ -1467,16 +1418,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
-    if (sequence == null || res > sequence.getLength())
+    if (sequence == null || column > sequence.getLength())
     {
       return;
     }
 
     stretchGroup = av.getSelectionGroup();
 
-    if (stretchGroup == null || !stretchGroup.contains(sequence, res))
+    if (stretchGroup == null || !stretchGroup.contains(sequence, column))
     {
-      stretchGroup = av.getAlignment().findGroup(sequence, res);
+      stretchGroup = av.getAlignment().findGroup(sequence, column);
       if (stretchGroup != null)
       {
         // only update the current selection if the popup menu has a group to
@@ -1489,8 +1440,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     if ((evt.getModifiers()
             & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
-              sequence.findPosition(res));
+      List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
+              sequence.findPosition(column + 1));
 
       Vector<String> links = null;
       for (SequenceFeature sf : allFeatures)
@@ -1499,12 +1450,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         {
           if (links == null)
           {
-            links = new Vector<>();
-          }
-          for (int j = 0; j < sf.links.size(); j++)
-          {
-            links.addElement(sf.links.elementAt(j));
+            links = new Vector<String>();
           }
+          links.addAll(sf.links);
         }
       }
       APopupMenu popup = new APopupMenu(ap, null, links);
@@ -1515,7 +1463,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     if (av.cursorMode)
     {
-      seqCanvas.cursorX = findRes(evt);
+      seqCanvas.cursorX = findColumn(evt);
       seqCanvas.cursorY = findSeq(evt);
       seqCanvas.repaint();
       return;
@@ -1527,8 +1475,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       // define a new group here
       SequenceGroup sg = new SequenceGroup();
-      sg.setStartRes(res);
-      sg.setEndRes(res);
+      sg.setStartRes(column);
+      sg.setEndRes(column);
       sg.addSequence(sequence, false);
       av.setSelectionGroup(sg);
       stretchGroup = sg;
@@ -1586,7 +1534,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   public void doMouseDraggedDefineMode(MouseEvent evt)
   {
-    int res = findRes(evt);
+    int res = findColumn(evt);
     int y = findSeq(evt);
 
     if (wrappedBlock != startWrapBlock)
@@ -1701,7 +1649,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       mouseExited(evt);
     }
 
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
       scrollThread.setEvent(evt);
     }
@@ -1717,9 +1665,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       oldSeq = 0;
     }
 
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
-      scrollThread.running = false;
+      scrollThread.stopScrolling();
       scrollThread = null;
     }
   }
@@ -1742,9 +1690,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   {
     if (evt == null)
     {
-      if (scrollThread != null)
+      if ((scrollThread != null) && (scrollThread.isRunning()))
       {
-        scrollThread.running = false;
+        scrollThread.stopScrolling();
         scrollThread = null;
       }
       mouseDragging = false;
@@ -1767,7 +1715,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   {
     MouseEvent evt;
 
-    boolean running = false;
+    private volatile boolean threadRunning = true;
 
     public ScrollThread()
     {
@@ -1781,14 +1729,18 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     public void stopScrolling()
     {
-      running = false;
+      threadRunning = false;
+    }
+
+    public boolean isRunning()
+    {
+      return threadRunning;
     }
 
     @Override
     public void run()
     {
-      running = true;
-      while (running)
+      while (threadRunning)
       {
 
         if (evt != null)
@@ -1797,23 +1749,23 @@ public class SeqPanel extends Panel implements MouseMotionListener,
           if (mouseDragging && evt.getY() < 0
                   && av.getRanges().getStartSeq() > 0)
           {
-            running = av.getRanges().scrollUp(true);
+            av.getRanges().scrollUp(true);
           }
 
           if (mouseDragging && evt.getY() >= getSize().height && av
                   .getAlignment().getHeight() > av.getRanges().getEndSeq())
           {
-            running = av.getRanges().scrollUp(false);
+            av.getRanges().scrollUp(false);
           }
 
           if (mouseDragging && evt.getX() < 0)
           {
-            running = av.getRanges().scrollRight(false);
+            av.getRanges().scrollRight(false);
           }
 
           else if (mouseDragging && evt.getX() >= getSize().width)
           {
-            running = av.getRanges().scrollRight(true);
+            av.getRanges().scrollRight(true);
           }
         }
 
index d319249..cac843f 100644 (file)
@@ -122,15 +122,15 @@ public class EditCommand implements CommandI
   {
   }
 
-  public EditCommand(String description)
+  public EditCommand(String desc)
   {
-    this.description = description;
+    this.description = desc;
   }
 
-  public EditCommand(String description, Action command, SequenceI[] seqs,
+  public EditCommand(String desc, Action command, SequenceI[] seqs,
           int position, int number, AlignmentI al)
   {
-    this.description = description;
+    this.description = desc;
     if (command == Action.CUT || command == Action.PASTE)
     {
       setEdit(new Edit(command, seqs, position, number, al));
@@ -139,10 +139,10 @@ public class EditCommand implements CommandI
     performEdit(0, null);
   }
 
-  public EditCommand(String description, Action command, String replace,
+  public EditCommand(String desc, Action command, String replace,
           SequenceI[] seqs, int position, int number, AlignmentI al)
   {
-    this.description = description;
+    this.description = desc;
     if (command == Action.REPLACE)
     {
       setEdit(new Edit(command, seqs, position, number, al, replace));
@@ -551,17 +551,19 @@ public class EditCommand implements CommandI
         {
           // modify the oldds if necessary
           if (oldds != sequence.getDatasetSequence()
-                  || sequence.getSequenceFeatures() != null)
+                  || sequence.getFeatures().hasFeatures())
           {
             if (command.oldds == null)
             {
               command.oldds = new SequenceI[command.seqs.length];
             }
             command.oldds[i] = oldds;
-            adjustFeatures(command, i,
+            // FIXME JAL-2541 JAL-2526 get correct positions if on a gap
+            adjustFeatures(
+                    command,
+                    i,
                     sequence.findPosition(command.position),
-                    sequence.findPosition(
-                            command.position + command.number),
+                    sequence.findPosition(command.position + command.number),
                     false);
           }
         }
@@ -799,6 +801,8 @@ public class EditCommand implements CommandI
       AlignmentAnnotation[] tmp;
       for (int s = 0; s < command.seqs.length; s++)
       {
+        command.seqs[s].sequenceChanged();
+
         if (modifyVisibility)
         {
           // Rows are only removed or added to sequence object.
@@ -1105,8 +1109,8 @@ public class EditCommand implements CommandI
     }
   }
 
-  final static void adjustFeatures(Edit command, int index, int i, int j,
-          boolean insert)
+  final static void adjustFeatures(Edit command, int index, final int i,
+          final int j, boolean insert)
   {
     SequenceI seq = command.seqs[index];
     SequenceI sequence = seq.getDatasetSequence();
@@ -1126,56 +1130,73 @@ public class EditCommand implements CommandI
       return;
     }
 
-    SequenceFeature[] sf = sequence.getSequenceFeatures();
+    List<SequenceFeature> sf = sequence.getFeatures()
+            .getPositionalFeatures();
 
-    if (sf == null)
+    if (sf.isEmpty())
     {
       return;
     }
 
-    SequenceFeature[] oldsf = new SequenceFeature[sf.length];
+    List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
 
     int cSize = j - i;
 
-    for (int s = 0; s < sf.length; s++)
+    for (SequenceFeature feature : sf)
     {
-      SequenceFeature copy = new SequenceFeature(sf[s]);
+      SequenceFeature copy = new SequenceFeature(feature);
 
-      oldsf[s] = copy;
+      oldsf.add(copy);
 
-      if (sf[s].getEnd() < i)
+      if (feature.getEnd() < i)
       {
         continue;
       }
 
-      if (sf[s].getBegin() > j)
+      if (feature.getBegin() > j)
       {
-        sf[s].setBegin(copy.getBegin() - cSize);
-        sf[s].setEnd(copy.getEnd() - cSize);
+        int newBegin = copy.getBegin() - cSize;
+        int newEnd = copy.getEnd() - cSize;
+        SequenceFeature newSf = new SequenceFeature(feature, newBegin,
+                newEnd, feature.getFeatureGroup(), feature.getScore());
+        sequence.deleteFeature(feature);
+        sequence.addSequenceFeature(newSf);
+        // feature.setBegin(newBegin);
+        // feature.setEnd(newEnd);
         continue;
       }
 
-      if (sf[s].getBegin() >= i)
+      int newBegin = feature.getBegin();
+      int newEnd = feature.getEnd();
+      if (newBegin >= i)
       {
-        sf[s].setBegin(i);
+        newBegin = i;
+        // feature.setBegin(i);
       }
 
-      if (sf[s].getEnd() < j)
+      if (newEnd < j)
       {
-        sf[s].setEnd(j - 1);
+        newEnd = j - 1;
+        // feature.setEnd(j - 1);
       }
+      newEnd = newEnd - cSize;
+      // feature.setEnd(feature.getEnd() - (cSize));
 
-      sf[s].setEnd(sf[s].getEnd() - (cSize));
-
-      if (sf[s].getBegin() > sf[s].getEnd())
+      sequence.deleteFeature(feature);
+      if (newEnd >= newBegin)
       {
-        sequence.deleteFeature(sf[s]);
+        sequence.addSequenceFeature(new SequenceFeature(feature, newBegin,
+                newEnd, feature.getFeatureGroup(), feature.getScore()));
       }
+      // if (feature.getBegin() > feature.getEnd())
+      // {
+      // sequence.deleteFeature(feature);
+      // }
     }
 
     if (command.editedFeatures == null)
     {
-      command.editedFeatures = new Hashtable<SequenceI, SequenceFeature[]>();
+      command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
     }
 
     command.editedFeatures.put(seq, oldsf);
@@ -1302,7 +1323,7 @@ public class EditCommand implements CommandI
 
     Hashtable<String, Annotation[]> deletedAnnotations;
 
-    Hashtable<SequenceI, SequenceFeature[]> editedFeatures;
+    Hashtable<SequenceI, List<SequenceFeature>> editedFeatures;
 
     AlignmentI al;
 
@@ -1318,51 +1339,51 @@ public class EditCommand implements CommandI
 
     char gapChar;
 
-    public Edit(Action command, SequenceI[] seqs, int position, int number,
-            char gapChar)
+    public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            char gap)
     {
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.gapChar = gapChar;
+      this.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      this.gapChar = gap;
     }
 
-    Edit(Action command, SequenceI[] seqs, int position, int number,
-            AlignmentI al)
+    Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            AlignmentI align)
     {
-      this.gapChar = al.getGapCharacter();
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.al = al;
-
-      alIndex = new int[seqs.length];
-      for (int i = 0; i < seqs.length; i++)
+      this.gapChar = align.getGapCharacter();
+      this.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      this.al = align;
+
+      alIndex = new int[sqs.length];
+      for (int i = 0; i < sqs.length; i++)
       {
-        alIndex[i] = al.findIndex(seqs[i]);
+        alIndex[i] = align.findIndex(sqs[i]);
       }
 
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
+      fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
-    Edit(Action command, SequenceI[] seqs, int position, int number,
-            AlignmentI al, String replace)
+    Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            AlignmentI align, String replace)
     {
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.al = al;
-      this.gapChar = al.getGapCharacter();
-      string = new char[seqs.length][];
-      for (int i = 0; i < seqs.length; i++)
+      this.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      this.al = align;
+      this.gapChar = align.getGapCharacter();
+      string = new char[sqs.length][];
+      for (int i = 0; i < sqs.length; i++)
       {
         string[i] = replace.toCharArray();
       }
 
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
+      fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
     public SequenceI[] getSequences()
index 3266874..32e5fb4 100644 (file)
  */
 package jalview.commands;
 
-/*
- * Jalview - A Sequence Alignment Editor and Viewer Copyright (C) 2007 AM
- * Waterhouse, J Procter, G Barton, M Clamp, S Searle
- * 
- * This program is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 2 of the License, or (at your option) any later
- * version.
- * 
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
- * details.
- * 
- * You should have received a copy of the GNU General Public License along with
- * this program; if not, write to the Free Software Foundation, Inc., 51
- * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- */
-
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 
@@ -110,6 +91,7 @@ public class RemoveGapColCommand extends EditCommand
     performEdit(0, null);
   }
 
+  @Override
   public int getSize()
   {
     // We're interested in the number of columns deleted,
index c5c35f1..27831dd 100644 (file)
  */
 package jalview.commands;
 
-/*
- * Jalview - A Sequence Alignment Editor and Viewer Copyright (C) 2007 AM
- * Waterhouse, J Procter, G Barton, M Clamp, S Searle
- * 
- * This program is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 2 of the License, or (at your option) any later
- * version.
- * 
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
- * details.
- * 
- * You should have received a copy of the GNU General Public License along with
- * this program; if not, write to the Free Software Foundation, Inc., 51
- * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- */
-
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 
index 24fc181..dd05843 100644 (file)
@@ -234,91 +234,66 @@ public class AlignViewController implements AlignViewControllerI
   static int findColumnsWithFeature(String featureType,
           SequenceCollectionI sqcol, BitSet bs)
   {
-    final int startPosition = sqcol.getStartRes() + 1; // converted to base 1
-    final int endPosition = sqcol.getEndRes() + 1;
+    final int startColumn = sqcol.getStartRes() + 1; // converted to base 1
+    final int endColumn = sqcol.getEndRes() + 1;
     List<SequenceI> seqs = sqcol.getSequences();
     int nseq = 0;
     for (SequenceI sq : seqs)
     {
-      boolean sequenceHasFeature = false;
       if (sq != null)
       {
-        SequenceFeature[] sfs = sq.getSequenceFeatures();
-        if (sfs != null)
+        // int ist = sq.findPosition(sqcol.getStartRes());
+        List<SequenceFeature> sfs = sq.findFeatures(startColumn,
+                endColumn, featureType);
+
+        if (!sfs.isEmpty())
         {
-          int ist = sq.findIndex(sq.getStart());
-          int iend = sq.findIndex(sq.getEnd());
-          if (iend < startPosition || ist > endPosition)
-          {
-            // sequence not in region
-            continue;
-          }
-          for (SequenceFeature sf : sfs)
+          nseq++;
+        }
+
+        for (SequenceFeature sf : sfs)
+        {
+          int sfStartCol = sq.findIndex(sf.getBegin());
+          int sfEndCol = sq.findIndex(sf.getEnd());
+
+          if (sf.isContactFeature())
           {
-            // future functionality - featureType == null means mark columns
-            // containing all displayed features
-            if (sf != null && (featureType.equals(sf.getType())))
+            /*
+             * 'contact' feature - check for 'start' or 'end'
+             * position within the selected region
+             */
+            if (sfStartCol >= startColumn && sfStartCol <= endColumn)
+            {
+              bs.set(sfStartCol - 1);
+            }
+            if (sfEndCol >= startColumn && sfEndCol <= endColumn)
             {
-              // optimisation - could consider 'spos,apos' like cursor argument
-              // - findIndex wastes time by starting from first character and
-              // counting
-
-              int sfStartCol = sq.findIndex(sf.getBegin());
-              int sfEndCol = sq.findIndex(sf.getEnd());
-
-              if (sf.isContactFeature())
-              {
-                /*
-                 * 'contact' feature - check for 'start' or 'end'
-                 * position within the selected region
-                 */
-                if (sfStartCol >= startPosition
-                        && sfStartCol <= endPosition)
-                {
-                  bs.set(sfStartCol - 1);
-                  sequenceHasFeature = true;
-                }
-                if (sfEndCol >= startPosition && sfEndCol <= endPosition)
-                {
-                  bs.set(sfEndCol - 1);
-                  sequenceHasFeature = true;
-                }
-                continue;
-              }
-
-              /*
-               * contiguous feature - select feature positions (if any) 
-               * within the selected region
-               */
-              if (sfStartCol > endPosition || sfEndCol < startPosition)
-              {
-                // feature is outside selected region
-                continue;
-              }
-              sequenceHasFeature = true;
-              if (sfStartCol < startPosition)
-              {
-                sfStartCol = startPosition;
-              }
-              if (sfStartCol < ist)
-              {
-                sfStartCol = ist;
-              }
-              if (sfEndCol > endPosition)
-              {
-                sfEndCol = endPosition;
-              }
-              for (; sfStartCol <= sfEndCol; sfStartCol++)
-              {
-                bs.set(sfStartCol - 1); // convert to base 0
-              }
+              bs.set(sfEndCol - 1);
             }
+            continue;
           }
-        }
 
-        if (sequenceHasFeature)
-        {
-          nseq++;
+          /*
+           * contiguous feature - select feature positions (if any) 
+           * within the selected region
+           */
+          if (sfStartCol < startColumn)
+          {
+            sfStartCol = startColumn;
+          }
+          // not sure what the point of this is
+          // if (sfStartCol < ist)
+          // {
+          // sfStartCol = ist;
+          // }
+          if (sfEndCol > endColumn)
+          {
+            sfEndCol = endColumn;
+          }
+          for (; sfStartCol <= sfEndCol; sfStartCol++)
+          {
+            bs.set(sfStartCol - 1); // convert to base 0
+          }
         }
       }
     }
index 83eeb3d..ec11fc1 100644 (file)
@@ -505,12 +505,11 @@ public class AlignedCodonFrame
          * Read off the mapped nucleotides (converting to position base 0)
          */
         codonPos = MappingUtils.flattenRanges(codonPos);
-        char[] dna = dnaSeq.getSequence();
         int start = dnaSeq.getStart();
-        result.add(
-                new char[]
-                { dna[codonPos[0] - start], dna[codonPos[1] - start],
-                    dna[codonPos[2] - start] });
+        char c1 = dnaSeq.getCharAt(codonPos[0] - start);
+        char c2 = dnaSeq.getCharAt(codonPos[1] - start);
+        char c3 = dnaSeq.getCharAt(codonPos[2] - start);
+        result.add(new char[] { c1, c2, c3 });
       }
     }
     return result.isEmpty() ? null : result;
index 5733719..8c5e4ac 100755 (executable)
@@ -1471,8 +1471,8 @@ public class Alignment implements AlignmentI
   {
     // TODO JAL-1270 needs test coverage
     // currently tested for use in jalview.gui.SequenceFetcher
-    boolean samegap = toappend.getGapCharacter() == getGapCharacter();
     char oldc = toappend.getGapCharacter();
+    boolean samegap = oldc == getGapCharacter();
     boolean hashidden = toappend.getHiddenSequences() != null
             && toappend.getHiddenSequences().hiddenSequences != null;
     // get all sequences including any hidden ones
@@ -1490,14 +1490,7 @@ public class Alignment implements AlignmentI
         {
           if (!samegap)
           {
-            char[] oldseq = addedsq.getSequence();
-            for (int c = 0; c < oldseq.length; c++)
-            {
-              if (oldseq[c] == oldc)
-              {
-                oldseq[c] = gapCharacter;
-              }
-            }
+            addedsq.replace(oldc, gapCharacter);
           }
           toappendsq.add(addedsq);
         }
index 6bbd566..c464af2 100755 (executable)
@@ -96,14 +96,13 @@ public class AlignmentAnnotation
    * Updates the _rnasecstr field Determines the positions that base pair and
    * the positions of helices based on secondary structure from a Stockholm file
    * 
-   * @param RNAannot
+   * @param rnaAnnotation
    */
-  private void _updateRnaSecStr(CharSequence RNAannot)
+  private void _updateRnaSecStr(CharSequence rnaAnnotation)
   {
     try
     {
-      bps = Rna.getModeleBP(RNAannot);
-      _rnasecstr = Rna.getBasePairs(bps);
+      _rnasecstr = Rna.getHelixMap(rnaAnnotation);
       invalidrnastruc = -1;
     } catch (WUSSParseException px)
     {
@@ -114,8 +113,6 @@ public class AlignmentAnnotation
     {
       return;
     }
-    Rna.HelixMap(_rnasecstr);
-    // setRNAStruc(RNAannot);
 
     if (_rnasecstr != null && _rnasecstr.length > 0)
     {
@@ -273,12 +270,6 @@ public class AlignmentAnnotation
     }
   }
 
-  // JBPNote: what does this do ?
-  public void ConcenStru(CharSequence RNAannot) throws WUSSParseException
-  {
-    bps = Rna.getModeleBP(RNAannot);
-  }
-
   /**
    * Creates a new AlignmentAnnotation object.
    * 
index 1b5207f..084b80e 100755 (executable)
@@ -5,16 +5,16 @@
  * This file is part of Jalview.
  * 
  * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General License 
+ * modify it under the terms of the GNU General Public License 
  * as published by the Free Software Foundation, either version 3
  * of the License, or (at your option) any later version.
  *  
  * Jalview is distributed in the hope that it will be useful, but 
  * WITHOUT ANY WARRANTY; without even the implied warranty 
  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General License for more details.
+ * PURPOSE.  See the GNU General Public License for more details.
  * 
- * You should have received a copy of the GNU General License
+ * You should have received a copy of the GNU General Public License
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
index 53bffa9..c33abb3 100755 (executable)
@@ -70,7 +70,7 @@ public class BinarySequence extends Sequence
     int nores = (isNa) ? ResidueProperties.maxNucleotideIndex
             : ResidueProperties.maxProteinIndex;
 
-    dbinary = new double[getSequence().length * nores];
+    dbinary = new double[getLength() * nores];
 
     return nores;
   }
@@ -88,7 +88,7 @@ public class BinarySequence extends Sequence
   {
     int nores = initMatrixGetNoRes();
     final int[] sindex = getSymbolmatrix();
-    for (int i = 0; i < getSequence().length; i++)
+    for (int i = 0; i < getLength(); i++)
     {
       int aanum = nores - 1;
 
@@ -132,7 +132,7 @@ public class BinarySequence extends Sequence
   {
     int nores = initMatrixGetNoRes();
 
-    for (int i = 0, iSize = getSequence().length; i < iSize; i++)
+    for (int i = 0, iSize = getLength(); i < iSize; i++)
     {
       int aanum = nores - 1;
 
diff --git a/src/jalview/datamodel/ContiguousI.java b/src/jalview/datamodel/ContiguousI.java
new file mode 100644 (file)
index 0000000..f2ae4b7
--- /dev/null
@@ -0,0 +1,8 @@
+package jalview.datamodel;
+
+public interface ContiguousI
+{
+  int getBegin(); // todo want long for genomic positions?
+
+  int getEnd();
+}
index fe396ce..328b96a 100644 (file)
@@ -46,7 +46,7 @@ public class Mapping
     /*
      * The characters of the aligned sequence e.g. "-cGT-ACgTG-"
      */
-    private final char[] alignedSeq;
+    private final SequenceI alignedSeq;
 
     /*
      * the sequence start residue
@@ -102,7 +102,7 @@ public class Mapping
      */
     public AlignedCodonIterator(SequenceI seq, char gapChar)
     {
-      this.alignedSeq = seq.getSequence();
+      this.alignedSeq = seq;
       this.start = seq.getStart();
       this.gap = gapChar;
       fromRanges = map.getFromRanges().iterator();
@@ -176,7 +176,7 @@ public class Mapping
       if (toPosition <= currentToRange[1])
       {
         SequenceI seq = Mapping.this.to;
-        char pep = seq.getSequence()[toPosition - seq.getStart()];
+        char pep = seq.getCharAt(toPosition - seq.getStart());
         toPosition++;
         return String.valueOf(pep);
       }
@@ -257,9 +257,10 @@ public class Mapping
        * allow for offset e.g. treat pos 8 as 2 if sequence starts at 7
        */
       int truePos = sequencePos - (start - 1);
-      while (alignedBases < truePos && alignedColumn < alignedSeq.length)
+      int length = alignedSeq.getLength();
+      while (alignedBases < truePos && alignedColumn < length)
       {
-        char c = alignedSeq[alignedColumn++];
+        char c = alignedSeq.getCharAt(alignedColumn++);
         if (c != gap && !Comparison.isGap(c))
         {
           alignedBases++;
@@ -530,9 +531,8 @@ public class Mapping
         SequenceFeature[] vf = new SequenceFeature[frange.length / 2];
         for (int i = 0, v = 0; i < frange.length; i += 2, v++)
         {
-          vf[v] = new SequenceFeature(f);
-          vf[v].setBegin(frange[i]);
-          vf[v].setEnd(frange[i + 1]);
+          vf[v] = new SequenceFeature(f, frange[i], frange[i + 1],
+                  f.getFeatureGroup(), f.getScore());
           if (frange.length > 2)
           {
             vf[v].setDescription(f.getDescription() + "\nPart " + (v + 1));
@@ -541,27 +541,7 @@ public class Mapping
         return vf;
       }
     }
-    if (false) // else
-    {
-      int[] word = getWord(f.getBegin());
-      if (word[0] < word[1])
-      {
-        f.setBegin(word[0]);
-      }
-      else
-      {
-        f.setBegin(word[1]);
-      }
-      word = getWord(f.getEnd());
-      if (word[0] > word[1])
-      {
-        f.setEnd(word[0]);
-      }
-      else
-      {
-        f.setEnd(word[1]);
-      }
-    }
+
     // give up and just return the feature.
     return new SequenceFeature[] { f };
   }
diff --git a/src/jalview/datamodel/Range.java b/src/jalview/datamodel/Range.java
new file mode 100644 (file)
index 0000000..7886713
--- /dev/null
@@ -0,0 +1,52 @@
+package jalview.datamodel;
+
+/**
+ * An immutable data bean that models a start-end range
+ */
+public class Range implements ContiguousI
+{
+  public final int start;
+
+  public final int end;
+
+  @Override
+  public int getBegin()
+  {
+    return start;
+  }
+
+  @Override
+  public int getEnd()
+  {
+    return end;
+  }
+
+  public Range(int i, int j)
+  {
+    start = i;
+    end = j;
+  }
+
+  @Override
+  public String toString()
+  {
+    return String.valueOf(start) + "-" + String.valueOf(end);
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return start * 31 + end;
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj instanceof Range)
+    {
+      Range r = (Range) obj;
+      return (start == r.start && end == r.end);
+    }
+    return false;
+  }
+}
index a270e37..cde50e5 100755 (executable)
@@ -34,7 +34,7 @@ import java.util.List;
 public class SearchResults implements SearchResultsI
 {
 
-  private List<SearchResultMatchI> matches = new ArrayList<SearchResultMatchI>();
+  private List<SearchResultMatchI> matches = new ArrayList<>();
 
   /**
    * One match consists of a sequence reference, start and end positions.
@@ -42,17 +42,17 @@ public class SearchResults implements SearchResultsI
    */
   public class Match implements SearchResultMatchI
   {
-    SequenceI sequence;
+    final SequenceI sequence;
 
     /**
      * Start position of match in sequence (base 1)
      */
-    int start;
+    final int start;
 
     /**
      * End position (inclusive) (base 1)
      */
-    int end;
+    final int end;
 
     /**
      * create a Match on a range of sequence. Match always holds region in
@@ -133,11 +133,6 @@ public class SearchResults implements SearchResultsI
       return sb.toString();
     }
 
-    public void setSequence(SequenceI seq)
-    {
-      this.sequence = seq;
-    }
-
     /**
      * Hashcode is the hashcode of the matched sequence plus a hash of start and
      * end positions. Match objects that pass the test for equals are guaranteed
@@ -219,20 +214,15 @@ public class SearchResults implements SearchResultsI
       m = (Match) _m;
 
       mfound = false;
-      if (m.sequence == sequence)
-      {
-        mfound = true;
-        // locate aligned position
-        matchStart = sequence.findIndex(m.start) - 1;
-        matchEnd = sequence.findIndex(m.end) - 1;
-      }
-      else if (m.sequence == sequence.getDatasetSequence())
+      if (m.sequence == sequence
+              || m.sequence == sequence.getDatasetSequence())
       {
         mfound = true;
-        // locate region in local context
         matchStart = sequence.findIndex(m.start) - 1;
-        matchEnd = sequence.findIndex(m.end) - 1;
+        matchEnd = m.start == m.end ? matchStart : sequence
+                .findIndex(m.end) - 1;
       }
+
       if (mfound)
       {
         if (matchStart <= end && matchEnd >= start)
@@ -363,4 +353,10 @@ public class SearchResults implements SearchResultsI
     SearchResultsI sr = (SearchResultsI) obj;
     return matches.equals(sr.getResults());
   }
+
+  @Override
+  public void addSearchResults(SearchResultsI toAdd)
+  {
+    matches.addAll(toAdd.getResults());
+  }
 }
index 52a0467..c3dc0e8 100644 (file)
@@ -44,6 +44,13 @@ public interface SearchResultsI
   SearchResultMatchI addResult(SequenceI seq, int start, int end);
 
   /**
+   * adds all match results in the argument to this set
+   * 
+   * @param toAdd
+   */
+  void addSearchResults(SearchResultsI toAdd);
+
+  /**
    * Answers true if the search results include the given sequence (or its
    * dataset sequence), else false
    * 
index 0103237..2f1da7f 100755 (executable)
@@ -22,6 +22,8 @@ package jalview.datamodel;
 
 import jalview.analysis.AlignSeq;
 import jalview.api.DBRefEntryI;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.datamodel.features.SequenceFeaturesI;
 import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
 import jalview.util.MapList;
@@ -33,8 +35,11 @@ import java.util.BitSet;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Vector;
 
+import com.stevesoft.pat.Regex;
+
 import fr.orsay.lri.varna.models.rna.RNA;
 
 /**
@@ -46,6 +51,11 @@ import fr.orsay.lri.varna.models.rna.RNA;
  */
 public class Sequence extends ASequence implements SequenceI
 {
+  private static final Regex limitrx = new Regex(
+          "[/][0-9]{1,}[-][0-9]{1,}$");
+
+  private static final Regex endrx = new Regex("[0-9]{1,}$");
+
   SequenceI datasetSequence;
 
   String name;
@@ -79,10 +89,22 @@ public class Sequence extends ASequence implements SequenceI
    */
   int index = -1;
 
-  /**
-   * array of sequence features - may not be null for a valid sequence object
+  private SequenceFeatures sequenceFeatureStore;
+
+  /*
+   * A cursor holding the approximate current view position to the sequence,
+   * as determined by findIndex or findPosition or findPositions.
+   * Using a cursor as a hint allows these methods to be more performant for
+   * large sequences.
+   */
+  private SequenceCursor cursor;
+
+  /*
+   * A number that should be incremented whenever the sequence is edited.
+   * If the value matches the cursor token, then we can trust the cursor,
+   * if not then it should be recomputed. 
    */
-  public SequenceFeature[] sequenceFeatures;
+  private int changeCount;
 
   /**
    * Creates a new Sequence object.
@@ -99,11 +121,13 @@ public class Sequence extends ASequence implements SequenceI
    */
   public Sequence(String name, String sequence, int start, int end)
   {
+    this();
     initSeqAndName(name, sequence.toCharArray(), start, end);
   }
 
   public Sequence(String name, char[] sequence, int start, int end)
   {
+    this();
     initSeqAndName(name, sequence, start, end);
   }
 
@@ -127,11 +151,6 @@ public class Sequence extends ASequence implements SequenceI
     checkValidRange();
   }
 
-  com.stevesoft.pat.Regex limitrx = new com.stevesoft.pat.Regex(
-          "[/][0-9]{1,}[-][0-9]{1,}$");
-
-  com.stevesoft.pat.Regex endrx = new com.stevesoft.pat.Regex("[0-9]{1,}$");
-
   void parseId()
   {
     if (name == null)
@@ -178,6 +197,14 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
+   * default constructor
+   */
+  private Sequence()
+  {
+    sequenceFeatureStore = new SequenceFeatures();
+  }
+
+  /**
    * Creates a new Sequence object.
    * 
    * @param name
@@ -216,8 +243,8 @@ public class Sequence extends ASequence implements SequenceI
    */
   public Sequence(SequenceI seq, AlignmentAnnotation[] alAnnotation)
   {
+    this();
     initSeqFrom(seq, alAnnotation);
-
   }
 
   /**
@@ -233,33 +260,38 @@ public class Sequence extends ASequence implements SequenceI
   protected void initSeqFrom(SequenceI seq,
           AlignmentAnnotation[] alAnnotation)
   {
-    {
-      char[] oseq = seq.getSequence();
-      initSeqAndName(seq.getName(), Arrays.copyOf(oseq, oseq.length),
-              seq.getStart(), seq.getEnd());
-    }
+    char[] oseq = seq.getSequence(); // returns a copy of the array
+    initSeqAndName(seq.getName(), oseq, seq.getStart(), seq.getEnd());
+
     description = seq.getDescription();
     if (seq != datasetSequence)
     {
       setDatasetSequence(seq.getDatasetSequence());
     }
-    if (datasetSequence == null && seq.getDBRefs() != null)
+    
+    /*
+     * only copy DBRefs and seqfeatures if we really are a dataset sequence
+     */
+    if (datasetSequence == null)
     {
-      // only copy DBRefs and seqfeatures if we really are a dataset sequence
-      DBRefEntry[] dbr = seq.getDBRefs();
-      for (int i = 0; i < dbr.length; i++)
-      {
-        addDBRef(new DBRefEntry(dbr[i]));
-      }
-      if (seq.getSequenceFeatures() != null)
+      if (seq.getDBRefs() != null)
       {
-        SequenceFeature[] sf = seq.getSequenceFeatures();
-        for (int i = 0; i < sf.length; i++)
+        DBRefEntry[] dbr = seq.getDBRefs();
+        for (int i = 0; i < dbr.length; i++)
         {
-          addSequenceFeature(new SequenceFeature(sf[i]));
+          addDBRef(new DBRefEntry(dbr[i]));
         }
       }
+
+      /*
+       * make copies of any sequence features
+       */
+      for (SequenceFeature sf : seq.getSequenceFeatures())
+      {
+        addSequenceFeature(new SequenceFeature(sf));
+      }
     }
+
     if (seq.getAnnotation() != null)
     {
       AlignmentAnnotation[] sqann = seq.getAnnotation();
@@ -296,123 +328,67 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   @Override
-  public void setSequenceFeatures(SequenceFeature[] features)
+  public void setSequenceFeatures(List<SequenceFeature> features)
   {
-    if (datasetSequence == null)
-    {
-      sequenceFeatures = features;
-    }
-    else
+    if (datasetSequence != null)
     {
-      if (datasetSequence.getSequenceFeatures() != features
-              && datasetSequence.getSequenceFeatures() != null
-              && datasetSequence.getSequenceFeatures().length > 0)
-      {
-        new Exception(
-                "Warning: JAL-2046 side effect ? Possible implementation error: overwriting dataset sequence features by setting sequence features on alignment")
-                        .printStackTrace();
-      }
       datasetSequence.setSequenceFeatures(features);
+      return;
     }
+    sequenceFeatureStore = new SequenceFeatures(features);
   }
 
   @Override
   public synchronized boolean addSequenceFeature(SequenceFeature sf)
   {
-    if (sequenceFeatures == null && datasetSequence != null)
-    {
-      return datasetSequence.addSequenceFeature(sf);
-    }
-    if (sequenceFeatures == null)
+    if (sf.getType() == null)
     {
-      sequenceFeatures = new SequenceFeature[0];
+      System.err.println("SequenceFeature type may not be null: "
+              + sf.toString());
+      return false;
     }
 
-    for (int i = 0; i < sequenceFeatures.length; i++)
+    if (datasetSequence != null)
     {
-      if (sequenceFeatures[i].equals(sf))
-      {
-        return false;
-      }
+      return datasetSequence.addSequenceFeature(sf);
     }
 
-    SequenceFeature[] temp = new SequenceFeature[sequenceFeatures.length
-            + 1];
-    System.arraycopy(sequenceFeatures, 0, temp, 0, sequenceFeatures.length);
-    temp[sequenceFeatures.length] = sf;
-
-    sequenceFeatures = temp;
-    return true;
+    return sequenceFeatureStore.add(sf);
   }
 
   @Override
   public void deleteFeature(SequenceFeature sf)
   {
-    if (sequenceFeatures == null)
-    {
-      if (datasetSequence != null)
-      {
-        datasetSequence.deleteFeature(sf);
-      }
-      return;
-    }
-
-    int index = 0;
-    for (index = 0; index < sequenceFeatures.length; index++)
-    {
-      if (sequenceFeatures[index].equals(sf))
-      {
-        break;
-      }
-    }
-
-    if (index == sequenceFeatures.length)
-    {
-      return;
-    }
-
-    int sfLength = sequenceFeatures.length;
-    if (sfLength < 2)
+    if (datasetSequence != null)
     {
-      sequenceFeatures = null;
+      datasetSequence.deleteFeature(sf);
     }
     else
     {
-      SequenceFeature[] temp = new SequenceFeature[sfLength - 1];
-      System.arraycopy(sequenceFeatures, 0, temp, 0, index);
-
-      if (index < sfLength)
-      {
-        System.arraycopy(sequenceFeatures, index + 1, temp, index,
-                sequenceFeatures.length - index - 1);
-      }
-
-      sequenceFeatures = temp;
+      sequenceFeatureStore.delete(sf);
     }
   }
 
   /**
-   * Returns the sequence features (if any), looking first on the sequence, then
-   * on its dataset sequence, and so on until a non-null value is found (or
-   * none). This supports retrieval of sequence features stored on the sequence
-   * (as in the applet) or on the dataset sequence (as in the Desktop version).
+   * {@inheritDoc}
    * 
    * @return
    */
   @Override
-  public SequenceFeature[] getSequenceFeatures()
+  public List<SequenceFeature> getSequenceFeatures()
   {
-    SequenceFeature[] features = sequenceFeatures;
-
-    SequenceI seq = this;
-    int count = 0; // failsafe against loop in sequence.datasetsequence...
-    while (features == null && seq.getDatasetSequence() != null
-            && count++ < 10)
+    if (datasetSequence != null)
     {
-      seq = seq.getDatasetSequence();
-      features = ((Sequence) seq).sequenceFeatures;
+      return datasetSequence.getSequenceFeatures();
     }
-    return features;
+    return sequenceFeatureStore.getAllFeatures();
+  }
+
+  @Override
+  public SequenceFeaturesI getFeatures()
+  {
+    return datasetSequence != null ? datasetSequence.getFeatures()
+            : sequenceFeatureStore;
   }
 
   @Override
@@ -568,6 +544,7 @@ public class Sequence extends ASequence implements SequenceI
   {
     this.sequence = seq.toCharArray();
     checkValidRange();
+    sequenceChanged();
   }
 
   @Override
@@ -585,7 +562,9 @@ public class Sequence extends ASequence implements SequenceI
   @Override
   public char[] getSequence()
   {
-    return sequence;
+    // return sequence;
+    return sequence == null ? null : Arrays.copyOf(sequence,
+            sequence.length);
   }
 
   /*
@@ -688,58 +667,370 @@ public class Sequence extends ASequence implements SequenceI
     return this.description;
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see jalview.datamodel.SequenceI#findIndex(int)
+  /**
+   * {@inheritDoc}
    */
   @Override
   public int findIndex(int pos)
   {
-    // returns the alignment position for a residue
+    /*
+     * use a valid, hopefully nearby, cursor if available
+     */
+    if (isValidCursor(cursor))
+    {
+      return findIndex(pos, cursor);
+    }
+
     int j = start;
     int i = 0;
-    // Rely on end being at least as long as the length of the sequence.
+    int startColumn = 0;
+
+    /*
+     * traverse sequence from the start counting gaps; make a note of
+     * the column of the first residue to save in the cursor
+     */
     while ((i < sequence.length) && (j <= end) && (j <= pos))
     {
-      if (!jalview.util.Comparison.isGap(sequence[i]))
+      if (!Comparison.isGap(sequence[i]))
       {
+        if (j == start)
+        {
+          startColumn = i;
+        }
         j++;
       }
-
       i++;
     }
 
-    if ((j == end) && (j < pos))
+    if (j == end && j < pos)
     {
       return end + 1;
     }
-    else
+
+    updateCursor(pos, i, startColumn);
+    return i;
+  }
+
+  /**
+   * Updates the cursor to the latest found residue and column position
+   * 
+   * @param residuePos
+   *          (start..)
+   * @param column
+   *          (1..)
+   * @param startColumn
+   *          column position of the first sequence residue
+   */
+  protected void updateCursor(int residuePos, int column, int startColumn)
+  {
+    /*
+     * preserve end residue column provided cursor was valid
+     */
+    int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
+    if (residuePos == this.end)
     {
-      return i;
+      endColumn = column;
     }
+
+    cursor = new SequenceCursor(this, residuePos, column, startColumn,
+            endColumn, this.changeCount);
   }
 
+  /**
+   * Answers the aligned column position (1..) for the given residue position
+   * (start..) given a 'hint' of a residue/column location in the neighbourhood.
+   * The hint may be left of, at, or to the right of the required position.
+   * 
+   * @param pos
+   * @param curs
+   * @return
+   */
+  protected int findIndex(int pos, SequenceCursor curs)
+  {
+    if (!isValidCursor(curs))
+    {
+      /*
+       * wrong or invalidated cursor, compute de novo
+       */
+      return findIndex(pos);
+    }
+
+    if (curs.residuePosition == pos)
+    {
+      return curs.columnPosition;
+    }
+
+    /*
+     * move left or right to find pos from hint.position
+     */
+    int col = curs.columnPosition - 1; // convert from base 1 to 0-based array
+                                       // index
+    int newPos = curs.residuePosition;
+    int delta = newPos > pos ? -1 : 1;
+
+    while (newPos != pos)
+    {
+      col += delta; // shift one column left or right
+      if (col < 0 || col == sequence.length)
+      {
+        break;
+      }
+      if (!Comparison.isGap(sequence[col]))
+      {
+        newPos += delta;
+      }
+    }
+
+    col++; // convert back to base 1
+    updateCursor(pos, col, curs.firstColumnPosition);
+
+    return col;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
   @Override
-  public int findPosition(int i)
+  public int findPosition(final int column)
   {
+    /*
+     * use a valid, hopefully nearby, cursor if available
+     */
+    if (isValidCursor(cursor))
+    {
+      return findPosition(column + 1, cursor);
+    }
+    
+    // TODO recode this more naturally i.e. count residues only
+    // as they are found, not 'in anticipation'
+
+    /*
+     * traverse the sequence counting gaps; note the column position
+     * of the first residue, to save in the cursor
+     */
+    int firstResidueColumn = 0;
+    int lastPosFound = 0;
+    int lastPosFoundColumn = 0;
+    int seqlen = sequence.length;
+
+    if (seqlen > 0 && !Comparison.isGap(sequence[0]))
+    {
+      lastPosFound = start;
+      lastPosFoundColumn = 0;
+    }
+
     int j = 0;
     int pos = start;
-    int seqlen = sequence.length;
-    while ((j < i) && (j < seqlen))
+
+    while (j < column && j < seqlen)
     {
-      if (!jalview.util.Comparison.isGap(sequence[j]))
+      if (!Comparison.isGap(sequence[j]))
       {
+        lastPosFound = pos;
+        lastPosFoundColumn = j;
+        if (pos == this.start)
+        {
+          firstResidueColumn = j;
+        }
         pos++;
       }
-
       j++;
     }
+    if (j < seqlen && !Comparison.isGap(sequence[j]))
+    {
+      lastPosFound = pos;
+      lastPosFoundColumn = j;
+      if (pos == this.start)
+      {
+        firstResidueColumn = j;
+      }
+    }
+
+    /*
+     * update the cursor to the last residue position found (if any)
+     * (converting column position to base 1)
+     */
+    if (lastPosFound != 0)
+    {
+      updateCursor(lastPosFound, lastPosFoundColumn + 1,
+              firstResidueColumn + 1);
+    }
 
     return pos;
   }
 
   /**
+   * Answers true if the given cursor is not null, is for this sequence object,
+   * and has a token value that matches this object's changeCount, else false.
+   * This allows us to ignore a cursor as 'stale' if the sequence has been
+   * modified since the cursor was created.
+   * 
+   * @param curs
+   * @return
+   */
+  protected boolean isValidCursor(SequenceCursor curs)
+  {
+    if (curs == null || curs.sequence != this || curs.token != changeCount)
+    {
+      return false;
+    }
+    /*
+     * sanity check against range
+     */
+    if (curs.columnPosition < 0 || curs.columnPosition > sequence.length)
+    {
+      return false;
+    }
+    if (curs.residuePosition < start || curs.residuePosition > end)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Answers the sequence position (start..) for the given aligned column
+   * position (1..), given a hint of a cursor in the neighbourhood. The cursor
+   * may lie left of, at, or to the right of the column position.
+   * 
+   * @param col
+   * @param curs
+   * @return
+   */
+  protected int findPosition(final int col, SequenceCursor curs)
+  {
+    if (!isValidCursor(curs))
+    {
+      /*
+       * wrong or invalidated cursor, compute de novo
+       */
+      return findPosition(col - 1);// ugh back to base 0
+    }
+
+    if (curs.columnPosition == col)
+    {
+      cursor = curs; // in case this method becomes public
+      return curs.residuePosition; // easy case :-)
+    }
+
+    if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col)
+    {
+      /*
+       * sequence lies entirely to the left of col
+       * - return last residue + 1
+       */
+      return end + 1;
+    }
+
+    if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col)
+    {
+      /*
+       * sequence lies entirely to the right of col
+       * - return first residue
+       */
+      return start;
+    }
+
+    // todo could choose closest to col out of column,
+    // firstColumnPosition, lastColumnPosition as a start point
+
+    /*
+     * move left or right to find pos from cursor position
+     */
+    int firstResidueColumn = curs.firstColumnPosition;
+    int column = curs.columnPosition - 1; // to base 0
+    int newPos = curs.residuePosition;
+    int delta = curs.columnPosition > col ? -1 : 1;
+    boolean gapped = false;
+    int lastFoundPosition = curs.residuePosition;
+    int lastFoundPositionColumn = curs.columnPosition;
+
+    while (column != col - 1)
+    {
+      column += delta; // shift one column left or right
+      if (column < 0 || column == sequence.length)
+      {
+        break;
+      }
+      gapped = Comparison.isGap(sequence[column]);
+      if (!gapped)
+      {
+        newPos += delta;
+        lastFoundPosition = newPos;
+        lastFoundPositionColumn = column + 1;
+        if (lastFoundPosition == this.start)
+        {
+          firstResidueColumn = column + 1;
+        }
+      }
+    }
+
+    if (cursor == null || lastFoundPosition != cursor.residuePosition)
+    {
+      updateCursor(lastFoundPosition, lastFoundPositionColumn,
+              firstResidueColumn);
+    }
+
+    /*
+     * hack to give position to the right if on a gap
+     * or beyond the length of the sequence (see JAL-2562)
+     */
+    if (delta > 0 && (gapped || column >= sequence.length))
+    {
+      newPos++;
+    }
+
+    return newPos;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Range findPositions(int fromColumn, int toColumn)
+  {
+    if (toColumn < fromColumn || fromColumn < 1)
+    {
+      return null;
+    }
+
+    /*
+     * find the first non-gapped position, if any
+     */
+    int firstPosition = 0;
+    int col = fromColumn - 1;
+    int length = sequence.length;
+    while (col < length && col < toColumn)
+    {
+      if (!Comparison.isGap(sequence[col]))
+      {
+        firstPosition = findPosition(col++);
+        break;
+      }
+      col++;
+    }
+
+    if (firstPosition == 0)
+    {
+      return null;
+    }
+
+    /*
+     * find the last non-gapped position
+     */
+    int lastPosition = firstPosition;
+    while (col < length && col < toColumn)
+    {
+      if (!Comparison.isGap(sequence[col++]))
+      {
+        lastPosition++;
+      }
+    }
+
+    return new Range(firstPosition, lastPosition);
+  }
+
+  /**
    * Returns an int array where indices correspond to each residue in the
    * sequence and the element value gives its position in the alignment
    * 
@@ -929,6 +1220,7 @@ public class Sequence extends ASequence implements SequenceI
     start = newstart;
     end = newend;
     sequence = tmp;
+    sequenceChanged();
   }
 
   @Override
@@ -959,6 +1251,7 @@ public class Sequence extends ASequence implements SequenceI
     }
 
     sequence = tmp;
+    sequenceChanged();
   }
 
   @Override
@@ -1154,7 +1447,7 @@ public class Sequence extends ASequence implements SequenceI
 
   private boolean _isNa;
 
-  private long _seqhash = 0;
+  private int _seqhash = 0;
 
   /**
    * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
@@ -1194,8 +1487,8 @@ public class Sequence extends ASequence implements SequenceI
 
       dsseq.setDescription(description);
       // move features and database references onto dataset sequence
-      dsseq.sequenceFeatures = sequenceFeatures;
-      sequenceFeatures = null;
+      dsseq.sequenceFeatureStore = sequenceFeatureStore;
+      sequenceFeatureStore = null;
       dsseq.dbrefs = dbrefs;
       dbrefs = null;
       // TODO: search and replace any references to this sequence with
@@ -1254,11 +1547,11 @@ public class Sequence extends ASequence implements SequenceI
       return null;
     }
 
-    Vector subset = new Vector();
-    Enumeration e = annotation.elements();
+    Vector<AlignmentAnnotation> subset = new Vector<AlignmentAnnotation>();
+    Enumeration<AlignmentAnnotation> e = annotation.elements();
     while (e.hasMoreElements())
     {
-      AlignmentAnnotation ann = (AlignmentAnnotation) e.nextElement();
+      AlignmentAnnotation ann = e.nextElement();
       if (ann.label != null && ann.label.equals(label))
       {
         subset.addElement(ann);
@@ -1273,7 +1566,7 @@ public class Sequence extends ASequence implements SequenceI
     e = subset.elements();
     while (e.hasMoreElements())
     {
-      anns[i++] = (AlignmentAnnotation) e.nextElement();
+      anns[i++] = e.nextElement();
     }
     subset.removeAllElements();
     return anns;
@@ -1326,13 +1619,12 @@ public class Sequence extends ASequence implements SequenceI
     if (entry.getSequenceFeatures() != null)
     {
 
-      SequenceFeature[] sfs = entry.getSequenceFeatures();
-      for (int si = 0; si < sfs.length; si++)
+      List<SequenceFeature> sfs = entry.getSequenceFeatures();
+      for (SequenceFeature feature : sfs)
       {
-        SequenceFeature sf[] = (mp != null) ? mp.locateFeature(sfs[si])
-                : new SequenceFeature[]
-                { new SequenceFeature(sfs[si]) };
-        if (sf != null && sf.length > 0)
+       SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
+                : new SequenceFeature[] { new SequenceFeature(feature) };
+        if (sf != null)
         {
           for (int sfi = 0; sfi < sf.length; sfi++)
           {
@@ -1345,10 +1637,10 @@ public class Sequence extends ASequence implements SequenceI
     // transfer PDB entries
     if (entry.getAllPDBEntries() != null)
     {
-      Enumeration e = entry.getAllPDBEntries().elements();
+      Enumeration<PDBEntry> e = entry.getAllPDBEntries().elements();
       while (e.hasMoreElements())
       {
-        PDBEntry pdb = (PDBEntry) e.nextElement();
+        PDBEntry pdb = e.nextElement();
         addPDBId(pdb);
       }
     }
@@ -1516,4 +1808,97 @@ public class Sequence extends ASequence implements SequenceI
     }
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeatures(int fromColumn, int toColumn,
+          String... types)
+  {
+    int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0
+    int endPos = fromColumn == toColumn ? startPos
+            : findPosition(toColumn - 1);
+
+    List<SequenceFeature> result = getFeatures().findFeatures(startPos,
+            endPos, types);
+
+    /*
+     * if end column is gapped, endPos may be to the right, 
+     * and we may have included adjacent or enclosing features;
+     * remove any that are not enclosing, non-contact features
+     */
+    if (endPos > this.end || Comparison.isGap(sequence[toColumn - 1]))
+    {
+      ListIterator<SequenceFeature> it = result.listIterator();
+      while (it.hasNext())
+      {
+        SequenceFeature sf = it.next();
+        int sfBegin = sf.getBegin();
+        int sfEnd = sf.getEnd();
+        int featureStartColumn = findIndex(sfBegin);
+        if (featureStartColumn > toColumn)
+        {
+          it.remove();
+        }
+        else if (featureStartColumn < fromColumn)
+        {
+          int featureEndColumn = sfEnd == sfBegin ? featureStartColumn
+                  : findIndex(sfEnd);
+          if (featureEndColumn < fromColumn)
+          {
+            it.remove();
+          }
+          else if (featureEndColumn > toColumn && sf.isContactFeature())
+          {
+            /*
+             * remove an enclosing feature if it is a contact feature
+             */
+            it.remove();
+          }
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * Invalidates any stale cursors (forcing recalculation) by incrementing the
+   * token that has to match the one presented by the cursor
+   */
+  @Override
+  public void sequenceChanged()
+  {
+    changeCount++;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int replace(char c1, char c2)
+  {
+    if (c1 == c2)
+    {
+      return 0;
+    }
+    int count = 0;
+    synchronized (sequence)
+    {
+      for (int c = 0; c < sequence.length; c++)
+      {
+        if (sequence[c] == c1)
+        {
+          sequence[c] = c2;
+          count++;
+        }
+      }
+    }
+    if (count > 0)
+    {
+      sequenceChanged();
+    }
+
+    return count;
+  }
 }
diff --git a/src/jalview/datamodel/SequenceCursor.java b/src/jalview/datamodel/SequenceCursor.java
new file mode 100644 (file)
index 0000000..b5929bf
--- /dev/null
@@ -0,0 +1,125 @@
+package jalview.datamodel;
+
+/**
+ * An immutable object representing one or more residue and corresponding
+ * alignment column positions for a sequence
+ */
+public class SequenceCursor
+{
+  /**
+   * the aligned sequence this cursor applies to
+   */
+  public final SequenceI sequence;
+
+  /**
+   * residue position in sequence (start...), 0 if undefined
+   */
+  public final int residuePosition;
+
+  /**
+   * column position (1...) corresponding to residuePosition, or 0 if undefined
+   */
+  public final int columnPosition;
+
+  /**
+   * column position (1...) of first residue in the sequence, or 0 if undefined
+   */
+  public final int firstColumnPosition;
+
+  /**
+   * column position (1...) of last residue in the sequence, or 0 if undefined
+   */
+  public final int lastColumnPosition;
+
+  /**
+   * a token which may be used to check whether this cursor is still valid for
+   * its sequence (allowing it to be ignored if the sequence has changed)
+   */
+  public final int token;
+
+  /**
+   * Constructor
+   * 
+   * @param seq
+   *          sequence this cursor applies to
+   * @param resPos
+   *          residue position in sequence (start..)
+   * @param column
+   *          column position in alignment (1..)
+   * @param tok
+   *          a token that may be validated by the sequence to check the cursor
+   *          is not stale
+   */
+  public SequenceCursor(SequenceI seq, int resPos, int column, int tok)
+  {
+    this(seq, resPos, column, 0, 0, tok);
+  }
+
+  /**
+   * Constructor
+   * 
+   * @param seq
+   *          sequence this cursor applies to
+   * @param resPos
+   *          residue position in sequence (start..)
+   * @param column
+   *          column position in alignment (1..)
+   * @param firstResCol
+   *          column position of the first residue in the sequence (1..), or 0
+   *          if not known
+   * @param lastResCol
+   *          column position of the last residue in the sequence (1..), or 0 if
+   *          not known
+   * @param tok
+   *          a token that may be validated by the sequence to check the cursor
+   *          is not stale
+   */
+  public SequenceCursor(SequenceI seq, int resPos, int column, int firstResCol,
+          int lastResCol, int tok)
+  {
+    sequence = seq;
+    residuePosition = resPos;
+    columnPosition = column;
+    firstColumnPosition = firstResCol;
+    lastColumnPosition = lastResCol;
+    token = tok;
+  }
+
+  @Override
+  public int hashCode()
+  {
+    int hash = 31 * residuePosition;
+    hash = 31 * hash + columnPosition;
+    hash = 31 * hash + token;
+    if (sequence != null)
+    {
+      hash += sequence.hashCode();
+    }
+    return hash;
+  }
+
+  /**
+   * Two cursors are equal if they refer to the same sequence object and have
+   * the same residue position, column position and token value
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (!(obj instanceof SequenceCursor))
+    {
+      return false;
+    }
+    SequenceCursor sc = (SequenceCursor) obj;
+    return sequence == sc.sequence && residuePosition == sc.residuePosition
+            && columnPosition == sc.columnPosition && token == sc.token;
+  }
+
+  @Override
+  public String toString()
+  {
+    String name = sequence == null ? "" : sequence.getName();
+    return String.format("%s:Pos%d:Col%d:startCol%d:endCol%d:tok%d", name,
+            residuePosition, columnPosition, firstColumnPosition,
+            lastColumnPosition, token);
+  }
+}
index 71732aa..9c4087e 100755 (executable)
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.FeatureLocationI;
+
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Vector;
 
 /**
@@ -30,8 +33,14 @@ import java.util.Vector;
  * @author $author$
  * @version $Revision$
  */
-public class SequenceFeature
+public class SequenceFeature implements FeatureLocationI
 {
+  /*
+   * score value if none is set; preferably Float.Nan, but see
+   * JAL-2060 and JAL-2554 for a couple of blockers to that
+   */
+  private static final float NO_SCORE = 0f;
+
   private static final String STATUS = "status";
 
   private static final String STRAND = "STRAND";
@@ -48,13 +57,22 @@ public class SequenceFeature
    */
   private static final String ATTRIBUTES = "ATTRIBUTES";
 
-  public int begin;
+  /*
+   * type, begin, end, featureGroup, score and contactFeature are final 
+   * to ensure that the integrity of SequenceFeatures data store 
+   * can't be broken by direct update of these fields
+   */
+  public final String type;
 
-  public int end;
+  public final int begin;
 
-  public float score;
+  public final int end;
 
-  public String type;
+  public final String featureGroup;
+
+  public final float score;
+
+  private final boolean contactFeature;
 
   public String description;
 
@@ -66,14 +84,6 @@ public class SequenceFeature
 
   public Vector<String> links;
 
-  // Feature group can be set from a features file
-  // as a group of features between STARTGROUP and ENDGROUP markers
-  public String featureGroup;
-
-  public SequenceFeature()
-  {
-  }
-
   /**
    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
    * otherDetails map, so the new and original SequenceFeature may reference the
@@ -83,96 +93,99 @@ public class SequenceFeature
    */
   public SequenceFeature(SequenceFeature cpy)
   {
-    if (cpy != null)
-    {
-      begin = cpy.begin;
-      end = cpy.end;
-      score = cpy.score;
-      if (cpy.type != null)
-      {
-        type = new String(cpy.type);
-      }
-      if (cpy.description != null)
-      {
-        description = new String(cpy.description);
-      }
-      if (cpy.featureGroup != null)
-      {
-        featureGroup = new String(cpy.featureGroup);
-      }
-      if (cpy.otherDetails != null)
-      {
-        try
-        {
-          otherDetails = (Map<String, Object>) ((HashMap<String, Object>) cpy.otherDetails)
-                  .clone();
-        } catch (Exception e)
-        {
-          // ignore
-        }
-      }
-      if (cpy.links != null && cpy.links.size() > 0)
-      {
-        links = new Vector<String>();
-        for (int i = 0, iSize = cpy.links.size(); i < iSize; i++)
-        {
-          links.addElement(cpy.links.elementAt(i));
-        }
-      }
-    }
+    this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
+            .getScore());
   }
 
   /**
-   * Constructor including a Status value
+   * Constructor
    * 
-   * @param type
-   * @param desc
-   * @param status
-   * @param begin
-   * @param end
-   * @param featureGroup
+   * @param theType
+   * @param theDesc
+   * @param theBegin
+   * @param theEnd
+   * @param group
    */
-  public SequenceFeature(String type, String desc, String status, int begin,
-          int end, String featureGroup)
+  public SequenceFeature(String theType, String theDesc, int theBegin,
+          int theEnd, String group)
   {
-    this(type, desc, begin, end, featureGroup);
-    setStatus(status);
+    this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
   }
 
   /**
-   * Constructor
+   * Constructor including a score value
    * 
-   * @param type
-   * @param desc
-   * @param begin
-   * @param end
-   * @param featureGroup
+   * @param theType
+   * @param theDesc
+   * @param theBegin
+   * @param theEnd
+   * @param theScore
+   * @param group
    */
-  SequenceFeature(String type, String desc, int begin, int end,
-          String featureGroup)
+  public SequenceFeature(String theType, String theDesc, int theBegin,
+          int theEnd, float theScore, String group)
   {
-    this.type = type;
-    this.description = desc;
-    this.begin = begin;
-    this.end = end;
-    this.featureGroup = featureGroup;
+    this.type = theType;
+    this.description = theDesc;
+    this.begin = theBegin;
+    this.end = theEnd;
+    this.featureGroup = group;
+    this.score = theScore;
+
+    /*
+     * for now, only "Disulfide/disulphide bond" is treated as a contact feature
+     */
+    this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
+            || "disulphide bond".equalsIgnoreCase(type);
   }
 
   /**
-   * Constructor including a score value
+   * A copy constructor that allows the value of final fields to be 'modified'
+   * 
+   * @param sf
+   * @param newType
+   * @param newBegin
+   * @param newEnd
+   * @param newGroup
+   * @param newScore
+   */
+  public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
+          int newEnd, String newGroup, float newScore)
+  {
+    this(newType, sf.getDescription(), newBegin, newEnd, newScore,
+            newGroup);
+
+    if (sf.otherDetails != null)
+    {
+      otherDetails = new HashMap<String, Object>();
+      for (Entry<String, Object> entry : sf.otherDetails.entrySet())
+      {
+        otherDetails.put(entry.getKey(), entry.getValue());
+      }
+    }
+    if (sf.links != null && sf.links.size() > 0)
+    {
+      links = new Vector<String>();
+      for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
+      {
+        links.addElement(sf.links.elementAt(i));
+      }
+    }
+  }
+
+  /**
+   * A copy constructor that allows the value of final fields to be 'modified'
    * 
-   * @param type
-   * @param desc
-   * @param begin
-   * @param end
-   * @param score
-   * @param featureGroup
+   * @param sf
+   * @param newBegin
+   * @param newEnd
+   * @param newGroup
+   * @param newScore
    */
-  public SequenceFeature(String type, String desc, int begin, int end,
-          float score, String featureGroup)
+  public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
+          String newGroup, float newScore)
   {
-    this(type, desc, begin, end, featureGroup);
-    this.score = score;
+    this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
   }
 
   /**
@@ -268,31 +281,23 @@ public class SequenceFeature
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public int getBegin()
   {
     return begin;
   }
 
-  public void setBegin(int start)
-  {
-    this.begin = start;
-  }
-
   /**
    * DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public int getEnd()
   {
     return end;
   }
 
-  public void setEnd(int end)
-  {
-    this.end = end;
-  }
-
   /**
    * DOCUMENT ME!
    * 
@@ -303,11 +308,6 @@ public class SequenceFeature
     return type;
   }
 
-  public void setType(String type)
-  {
-    this.type = type;
-  }
-
   /**
    * DOCUMENT ME!
    * 
@@ -328,11 +328,6 @@ public class SequenceFeature
     return featureGroup;
   }
 
-  public void setFeatureGroup(String featureGroup)
-  {
-    this.featureGroup = featureGroup;
-  }
-
   public void addLink(String labelLink)
   {
     if (links == null)
@@ -340,7 +335,10 @@ public class SequenceFeature
       links = new Vector<String>();
     }
 
-    links.insertElementAt(labelLink, 0);
+    if (!links.contains(labelLink))
+    {
+      links.insertElementAt(labelLink, 0);
+    }
   }
 
   public float getScore()
@@ -348,11 +346,6 @@ public class SequenceFeature
     return score;
   }
 
-  public void setScore(float value)
-  {
-    score = value;
-  }
-
   /**
    * Used for getting values which are not in the basic set. eg STRAND, PHASE
    * for GFF file
@@ -432,17 +425,6 @@ public class SequenceFeature
     return (String) getValue(ATTRIBUTES);
   }
 
-  public void setPosition(int pos)
-  {
-    begin = pos;
-    end = pos;
-  }
-
-  public int getPosition()
-  {
-    return begin;
-  }
-
   /**
    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
    * GFF), and 0 for unknown or not (validly) specified
@@ -538,14 +520,19 @@ public class SequenceFeature
    * positions, rather than ends of a range. Such features may be visualised or
    * reported differently to features on a range.
    */
+  @Override
   public boolean isContactFeature()
   {
-    // TODO abstract one day to a FeatureType class
-    if ("disulfide bond".equalsIgnoreCase(type)
-            || "disulphide bond".equalsIgnoreCase(type))
-    {
-      return true;
-    }
-    return false;
+    return contactFeature;
+  }
+
+  /**
+   * Answers true if the sequence has zero start and end position
+   * 
+   * @return
+   */
+  public boolean isNonPositional()
+  {
+    return begin == 0 && end == 0;
   }
 }
index 12ddf60..6e6d1aa 100755 (executable)
@@ -20,6 +20,8 @@
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.SequenceFeaturesI;
+
 import java.util.BitSet;
 import java.util.List;
 import java.util.Vector;
@@ -117,9 +119,9 @@ public interface SequenceI extends ASequenceI
   public String getSequenceAsString(int start, int end);
 
   /**
-   * Get the sequence as a character array
+   * Answers a copy of the sequence as a character array
    * 
-   * @return seqeunce and any gaps
+   * @return
    */
   public char[] getSequence();
 
@@ -175,7 +177,7 @@ public interface SequenceI extends ASequenceI
   public String getDescription();
 
   /**
-   * Return the alignment column for a sequence position
+   * Return the alignment column (from 1..) for a sequence position
    * 
    * @param pos
    *          lying from start to end
@@ -201,6 +203,16 @@ public interface SequenceI extends ASequenceI
   public int findPosition(int i);
 
   /**
+   * Returns the from-to sequence positions (start..) for the given column
+   * positions (1..), or null if no residues are included in the range
+   * 
+   * @param fromColum
+   * @param toColumn
+   * @return
+   */
+  public Range findPositions(int fromColum, int toColumn);
+
+  /**
    * Returns an int array where indices correspond to each residue in the
    * sequence and the element value gives its position in the alignment
    * 
@@ -261,22 +273,28 @@ public interface SequenceI extends ASequenceI
   public void insertCharAt(int position, int count, char ch);
 
   /**
-   * Gets array holding sequence features associated with this sequence. The
-   * array may be held by the sequence's dataset sequence if that is defined.
+   * Answers a list of all sequence features associated with this sequence. The
+   * list may be held by the sequence's dataset sequence if that is defined.
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getSequenceFeatures();
+
+  /**
+   * Answers the object holding features for the sequence
    * 
-   * @return hard reference to array
+   * @return
    */
-  public SequenceFeature[] getSequenceFeatures();
+  SequenceFeaturesI getFeatures();
 
   /**
-   * Replaces the array of sequence features associated with this sequence with
-   * a new array reference. If this sequence has a dataset sequence, then this
-   * method will update the dataset sequence's feature array
+   * Replaces the sequence features associated with this sequence with the given
+   * features. If this sequence has a dataset sequence, then this method will
+   * update the dataset sequence's features instead.
    * 
    * @param features
-   *          New array of sequence features
    */
-  public void setSequenceFeatures(SequenceFeature[] features);
+  public void setSequenceFeatures(List<SequenceFeature> features);
 
   /**
    * DOCUMENT ME!
@@ -341,7 +359,7 @@ public interface SequenceI extends ASequenceI
 
   /**
    * Adds the given sequence feature and returns true, or returns false if it is
-   * already present on the sequence
+   * already present on the sequence, or if the feature type is null.
    * 
    * @param sf
    * @return
@@ -479,9 +497,41 @@ public interface SequenceI extends ASequenceI
   public List<DBRefEntry> getPrimaryDBRefs();
 
   /**
+   * Returns a (possibly empty) list of sequence features that overlap the given
+   * alignment column range, optionally restricted to one or more specified
+   * feature types. If the range is all gaps, then features which enclose it are
+   * included (but not contact features).
+   * 
+   * @param fromCol
+   *          start column of range inclusive (1..)
+   * @param toCol
+   *          end column of range inclusive (1..)
+   * @param types
+   *          optional feature types to restrict results to
+   * @return
+   */
+  List<SequenceFeature> findFeatures(int fromCol, int toCol, String... types);
+
+  /**
+   * Method to call to indicate that the sequence (characters or alignment/gaps)
+   * has been modified. Provided to allow any cursors on residue/column
+   * positions to be invalidated.
+   */
+  void sequenceChanged();
+  
+  /**
    * 
    * @return BitSet corresponding to index [0,length) where Comparison.isGap()
    *         returns true.
    */
   BitSet getInsertionsAsBits();
+
+  /**
+   * Replaces every occurrence of c1 in the sequence with c2 and returns the
+   * number of characters changed
+   * 
+   * @param c1
+   * @param c2
+   */
+  public int replace(char c1, char c2);
 }
diff --git a/src/jalview/datamodel/features/FeatureLocationI.java b/src/jalview/datamodel/features/FeatureLocationI.java
new file mode 100644 (file)
index 0000000..e651c13
--- /dev/null
@@ -0,0 +1,12 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+
+/**
+ * An extension of ContiguousI that allows start/end values to be interpreted
+ * instead as two contact positions
+ */
+public interface FeatureLocationI extends ContiguousI
+{
+  boolean isContactFeature();
+}
diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java
new file mode 100644 (file)
index 0000000..51bee57
--- /dev/null
@@ -0,0 +1,1064 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A data store for a set of sequence features that supports efficient lookup of
+ * features overlapping a given range. Intended for (but not limited to) storage
+ * of features for one sequence and feature type.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureStore
+{
+  /**
+   * a class providing criteria for performing a binary search of a list
+   */
+  abstract static class SearchCriterion
+  {
+    /**
+     * Answers true if the entry passes the search criterion test
+     * 
+     * @param entry
+     * @return
+     */
+    abstract boolean compare(SequenceFeature entry);
+
+    /**
+     * serves a search condition for finding the first feature whose start
+     * position follows a given target location
+     * 
+     * @param target
+     * @return
+     */
+    static SearchCriterion byStart(final long target)
+    {
+      return new SearchCriterion() {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return entry.getBegin() >= target;
+        }
+      };
+    }
+
+    /**
+     * serves a search condition for finding the first feature whose end
+     * position is at or follows a given target location
+     * 
+     * @param target
+     * @return
+     */
+    static SearchCriterion byEnd(final long target)
+    {
+      return new SearchCriterion()
+      {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return entry.getEnd() >= target;
+        }
+      };
+    }
+
+    /**
+     * serves a search condition for finding the first feature which follows the
+     * given range as determined by a supplied comparator
+     * 
+     * @param target
+     * @return
+     */
+    static SearchCriterion byFeature(final ContiguousI to,
+            final Comparator<ContiguousI> rc)
+    {
+      return new SearchCriterion()
+      {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return rc.compare(entry, to) >= 0;
+        }
+      };
+    }
+  }
+
+  /*
+   * Non-positional features have no (zero) start/end position.
+   * Kept as a separate list in case this criterion changes in future.
+   */
+  List<SequenceFeature> nonPositionalFeatures;
+
+  /*
+   * An ordered list of features, with the promise that no feature in the list 
+   * properly contains any other. This constraint allows bounded linear search
+   * of the list for features overlapping a region.
+   * Contact features are not included in this list.
+   */
+  List<SequenceFeature> nonNestedFeatures;
+
+  /*
+   * contact features ordered by first contact position
+   */
+  List<SequenceFeature> contactFeatureStarts;
+
+  /*
+   * contact features ordered by second contact position
+   */
+  List<SequenceFeature> contactFeatureEnds;
+
+  /*
+   * Nested Containment List is used to hold any features that are nested 
+   * within (properly contained by) any other feature. This is a recursive tree
+   * which supports depth-first scan for features overlapping a range.
+   * It is used here as a 'catch-all' fallback for features that cannot be put
+   * into a simple ordered list without invalidating the search methods.
+   */
+  NCList<SequenceFeature> nestedFeatures;
+
+  /*
+   * Feature groups represented in stored positional features 
+   * (possibly including null)
+   */
+  Set<String> positionalFeatureGroups;
+
+  /*
+   * Feature groups represented in stored non-positional features 
+   * (possibly including null)
+   */
+  Set<String> nonPositionalFeatureGroups;
+
+  /*
+   * the total length of all positional features; contact features count 1 to
+   * the total and 1 to size(), consistent with an average 'feature length' of 1
+   */
+  int totalExtent;
+
+  float positionalMinScore;
+
+  float positionalMaxScore;
+
+  float nonPositionalMinScore;
+
+  float nonPositionalMaxScore;
+
+  /**
+   * Constructor
+   */
+  public FeatureStore()
+  {
+    nonNestedFeatures = new ArrayList<SequenceFeature>();
+    positionalFeatureGroups = new HashSet<String>();
+    nonPositionalFeatureGroups = new HashSet<String>();
+    positionalMinScore = Float.NaN;
+    positionalMaxScore = Float.NaN;
+    nonPositionalMinScore = Float.NaN;
+    nonPositionalMaxScore = Float.NaN;
+
+    // we only construct nonPositionalFeatures, contactFeatures
+    // or the NCList if we need to
+  }
+
+  /**
+   * Adds one sequence feature to the store, and returns true, unless the
+   * feature is already contained in the store, in which case this method
+   * returns false. Containment is determined by SequenceFeature.equals()
+   * comparison.
+   * 
+   * @param feature
+   */
+  public boolean addFeature(SequenceFeature feature)
+  {
+    if (contains(feature))
+    {
+      return false;
+    }
+
+    /*
+     * keep a record of feature groups
+     */
+    if (!feature.isNonPositional())
+    {
+      positionalFeatureGroups.add(feature.getFeatureGroup());
+    }
+
+    boolean added = false;
+
+    if (feature.isContactFeature())
+    {
+      added = addContactFeature(feature);
+    }
+    else if (feature.isNonPositional())
+    {
+      added = addNonPositionalFeature(feature);
+    }
+    else
+    {
+      added = addNonNestedFeature(feature);
+      if (!added)
+      {
+        /*
+         * detected a nested feature - put it in the NCList structure
+         */
+        added = addNestedFeature(feature);
+      }
+    }
+
+    if (added)
+    {
+      /*
+       * record the total extent of positional features, to make
+       * getTotalFeatureLength possible; we count the length of a 
+       * contact feature as 1
+       */
+      totalExtent += getFeatureLength(feature);
+
+      /*
+       * record the minimum and maximum score for positional
+       * and non-positional features
+       */
+      float score = feature.getScore();
+      if (!Float.isNaN(score))
+      {
+        if (feature.isNonPositional())
+        {
+          nonPositionalMinScore = min(nonPositionalMinScore, score);
+          nonPositionalMaxScore = max(nonPositionalMaxScore, score);
+        }
+        else
+        {
+          positionalMinScore = min(positionalMinScore, score);
+          positionalMaxScore = max(positionalMaxScore, score);
+        }
+      }
+    }
+
+    return added;
+  }
+
+  /**
+   * Answers true if this store contains the given feature (testing by
+   * SequenceFeature.equals), else false
+   * 
+   * @param feature
+   * @return
+   */
+  public boolean contains(SequenceFeature feature)
+  {
+    if (feature.isNonPositional())
+    {
+      return nonPositionalFeatures == null ? false : nonPositionalFeatures
+              .contains(feature);
+    }
+
+    if (feature.isContactFeature())
+    {
+      return contactFeatureStarts == null ? false : listContains(
+              contactFeatureStarts, feature);
+    }
+
+    if (listContains(nonNestedFeatures, feature))
+    {
+      return true;
+    }
+
+    return nestedFeatures == null ? false : nestedFeatures
+            .contains(feature);
+  }
+
+  /**
+   * Answers the 'length' of the feature, counting 0 for non-positional features
+   * and 1 for contact features
+   * 
+   * @param feature
+   * @return
+   */
+  protected static int getFeatureLength(SequenceFeature feature)
+  {
+    if (feature.isNonPositional())
+    {
+      return 0;
+    }
+    if (feature.isContactFeature())
+    {
+      return 1;
+    }
+    return 1 + feature.getEnd() - feature.getBegin();
+  }
+
+  /**
+   * Adds the feature to the list of non-positional features (with lazy
+   * instantiation of the list if it is null), and returns true. The feature
+   * group is added to the set of distinct feature groups for non-positional
+   * features. This method allows duplicate features, so test before calling to
+   * prevent this.
+   * 
+   * @param feature
+   */
+  protected boolean addNonPositionalFeature(SequenceFeature feature)
+  {
+    if (nonPositionalFeatures == null)
+    {
+      nonPositionalFeatures = new ArrayList<SequenceFeature>();
+    }
+
+    nonPositionalFeatures.add(feature);
+
+    nonPositionalFeatureGroups.add(feature.getFeatureGroup());
+
+    return true;
+  }
+
+  /**
+   * Adds one feature to the NCList that can manage nested features (creating
+   * the NCList if necessary), and returns true. If the feature is already
+   * stored in the NCList (by equality test), then it is not added, and this
+   * method returns false.
+   */
+  protected synchronized boolean addNestedFeature(SequenceFeature feature)
+  {
+    if (nestedFeatures == null)
+    {
+      nestedFeatures = new NCList<>(feature);
+      return true;
+    }
+    return nestedFeatures.add(feature, false);
+  }
+
+  /**
+   * Add a feature to the list of non-nested features, maintaining the ordering
+   * of the list. A check is made for whether the feature is nested in (properly
+   * contained by) an existing feature. If there is no nesting, the feature is
+   * added to the list and the method returns true. If nesting is found, the
+   * feature is not added and the method returns false.
+   * 
+   * @param feature
+   * @return
+   */
+  protected boolean addNonNestedFeature(SequenceFeature feature)
+  {
+    synchronized (nonNestedFeatures)
+    {
+      /*
+       * find the first stored feature which doesn't precede the new one
+       */
+      int insertPosition = binarySearch(nonNestedFeatures,
+              SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION));
+
+      /*
+       * fail if we detect feature enclosure - of the new feature by
+       * the one preceding it, or of the next feature by the new one
+       */
+      if (insertPosition > 0)
+      {
+        if (encloses(nonNestedFeatures.get(insertPosition - 1), feature))
+        {
+          return false;
+        }
+      }
+      if (insertPosition < nonNestedFeatures.size())
+      {
+        if (encloses(feature, nonNestedFeatures.get(insertPosition)))
+        {
+          return false;
+        }
+      }
+
+      /*
+       * checks passed - add the feature
+       */
+      nonNestedFeatures.add(insertPosition, feature);
+
+      return true;
+    }
+  }
+
+  /**
+   * Answers true if range1 properly encloses range2, else false
+   * 
+   * @param range1
+   * @param range2
+   * @return
+   */
+  protected boolean encloses(ContiguousI range1, ContiguousI range2)
+  {
+    int begin1 = range1.getBegin();
+    int begin2 = range2.getBegin();
+    int end1 = range1.getEnd();
+    int end2 = range2.getEnd();
+    if (begin1 == begin2 && end1 > end2)
+    {
+      return true;
+    }
+    if (begin1 < begin2 && end1 >= end2)
+    {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Add a contact feature to the lists that hold them ordered by start (first
+   * contact) and by end (second contact) position, ensuring the lists remain
+   * ordered, and returns true. This method allows duplicate features to be
+   * added, so test before calling to avoid this.
+   * 
+   * @param feature
+   * @return
+   */
+  protected synchronized boolean addContactFeature(SequenceFeature feature)
+  {
+    if (contactFeatureStarts == null)
+    {
+      contactFeatureStarts = new ArrayList<SequenceFeature>();
+    }
+    if (contactFeatureEnds == null)
+    {
+      contactFeatureEnds = new ArrayList<SequenceFeature>();
+    }
+
+    /*
+     * binary search the sorted list to find the insertion point
+     */
+    int insertPosition = binarySearch(contactFeatureStarts,
+            SearchCriterion.byFeature(feature,
+                    RangeComparator.BY_START_POSITION));
+    contactFeatureStarts.add(insertPosition, feature);
+    // and resort to mak siccar...just in case insertion point not quite right
+    Collections.sort(contactFeatureStarts, RangeComparator.BY_START_POSITION);
+
+    insertPosition = binarySearch(contactFeatureStarts,
+            SearchCriterion.byFeature(feature,
+                    RangeComparator.BY_END_POSITION));
+    contactFeatureEnds.add(feature);
+    Collections.sort(contactFeatureEnds, RangeComparator.BY_END_POSITION);
+
+    return true;
+  }
+
+  /**
+   * Answers true if the list contains the feature, else false. This method is
+   * optimised for the condition that the list is sorted on feature start
+   * position ascending, and will give unreliable results if this does not hold.
+   * 
+   * @param features
+   * @param feature
+   * @return
+   */
+  protected static boolean listContains(List<SequenceFeature> features,
+          SequenceFeature feature)
+  {
+    if (features == null || feature == null)
+    {
+      return false;
+    }
+
+    /*
+     * locate the first entry in the list which does not precede the feature
+     */
+    int pos = binarySearch(features,
+            SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION));
+    int len = features.size();
+    while (pos < len)
+    {
+      SequenceFeature sf = features.get(pos);
+      if (sf.getBegin() > feature.getBegin())
+      {
+        return false; // no match found
+      }
+      if (sf.equals(feature))
+      {
+        return true;
+      }
+      pos++;
+    }
+    return false;
+  }
+
+  /**
+   * Returns a (possibly empty) list of features whose extent overlaps the given
+   * range. The returned list is not ordered. Contact features are included if
+   * either of the contact points lies within the range.
+   * 
+   * @param start
+   *          start position of overlap range (inclusive)
+   * @param end
+   *          end position of overlap range (inclusive)
+   * @return
+   */
+  public List<SequenceFeature> findOverlappingFeatures(long start, long end)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    findNonNestedFeatures(start, end, result);
+
+    findContactFeatures(start, end, result);
+
+    if (nestedFeatures != null)
+    {
+      result.addAll(nestedFeatures.findOverlaps(start, end));
+    }
+
+    return result;
+  }
+
+  /**
+   * Adds contact features to the result list where either the second or the
+   * first contact position lies within the target range
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findContactFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    if (contactFeatureStarts != null)
+    {
+      findContactStartFeatures(from, to, result);
+    }
+    if (contactFeatureEnds != null)
+    {
+      findContactEndFeatures(from, to, result);
+    }
+  }
+
+  /**
+   * Adds to the result list any contact features whose end (second contact
+   * point), but not start (first contact point), lies in the query from-to
+   * range
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findContactEndFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    /*
+     * find the first contact feature (if any) that does not lie 
+     * entirely before the target range
+     */
+    int startPosition = binarySearch(contactFeatureEnds,
+            SearchCriterion.byEnd(from));
+    for (; startPosition < contactFeatureEnds.size(); startPosition++)
+    {
+      SequenceFeature sf = contactFeatureEnds.get(startPosition);
+      if (!sf.isContactFeature())
+      {
+        System.err.println("Error! non-contact feature type "
+                + sf.getType() + " in contact features list");
+        continue;
+      }
+
+      int begin = sf.getBegin();
+      if (begin >= from && begin <= to)
+      {
+        /*
+         * this feature's first contact position lies in the search range
+         * so we don't include it in results a second time
+         */
+        continue;
+      }
+
+      int end = sf.getEnd();
+      if (end >= from && end <= to)
+      {
+        result.add(sf);
+      }
+      if (end > to)
+      {
+        break;
+      }
+    }
+  }
+
+  /**
+   * Adds non-nested features to the result list that lie within the target
+   * range. Non-positional features (start=end=0), contact features and nested
+   * features are excluded.
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findNonNestedFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    /*
+     * find the first feature whose end position is
+     * after the target range start
+     */
+    int startIndex = binarySearch(nonNestedFeatures,
+            SearchCriterion.byEnd(from));
+
+    final int startIndex1 = startIndex;
+    int i = startIndex1;
+    while (i < nonNestedFeatures.size())
+    {
+      SequenceFeature sf = nonNestedFeatures.get(i);
+      if (sf.getBegin() > to)
+      {
+        break;
+      }
+      if (sf.getBegin() <= to && sf.getEnd() >= from)
+      {
+        result.add(sf);
+      }
+      i++;
+    }
+  }
+
+  /**
+   * Adds contact features whose start position lies in the from-to range to the
+   * result list
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findContactStartFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    int startPosition = binarySearch(contactFeatureStarts,
+            SearchCriterion.byStart(from));
+
+    for (; startPosition < contactFeatureStarts.size(); startPosition++)
+    {
+      SequenceFeature sf = contactFeatureStarts.get(startPosition);
+      if (!sf.isContactFeature())
+      {
+        System.err.println("Error! non-contact feature type "
+                + sf.getType() + " in contact features list");
+        continue;
+      }
+      int begin = sf.getBegin();
+      if (begin >= from && begin <= to)
+      {
+        result.add(sf);
+      }
+    }
+  }
+
+  /**
+   * Answers a list of all positional features stored, in no guaranteed order
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getPositionalFeatures()
+  {
+    /*
+     * add non-nested features (may be all features for many cases)
+     */
+    List<SequenceFeature> result = new ArrayList<>();
+    result.addAll(nonNestedFeatures);
+
+    /*
+     * add any contact features - from the list by start position
+     */
+    if (contactFeatureStarts != null)
+    {
+      result.addAll(contactFeatureStarts);
+    }
+
+    /*
+     * add any nested features
+     */
+    if (nestedFeatures != null)
+    {
+      result.addAll(nestedFeatures.getEntries());
+    }
+
+    return result;
+  }
+
+  /**
+   * Answers a list of all contact features. If there are none, returns an
+   * immutable empty list.
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getContactFeatures()
+  {
+    if (contactFeatureStarts == null)
+    {
+      return Collections.emptyList();
+    }
+    return new ArrayList<>(contactFeatureStarts);
+  }
+
+  /**
+   * Answers a list of all non-positional features. If there are none, returns
+   * an immutable empty list.
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getNonPositionalFeatures()
+  {
+    if (nonPositionalFeatures == null)
+    {
+      return Collections.emptyList();
+    }
+    return new ArrayList<>(nonPositionalFeatures);
+  }
+
+  /**
+   * Deletes the given feature from the store, returning true if it was found
+   * (and deleted), else false. This method makes no assumption that the feature
+   * is in the 'expected' place in the store, in case it has been modified since
+   * it was added.
+   * 
+   * @param sf
+   */
+  public synchronized boolean delete(SequenceFeature sf)
+  {
+    /*
+     * try the non-nested positional features first
+     */
+    boolean removed = nonNestedFeatures.remove(sf);
+
+    /*
+     * if not found, try contact positions (and if found, delete
+     * from both lists of contact positions)
+     */
+    if (!removed && contactFeatureStarts != null)
+    {
+      removed = contactFeatureStarts.remove(sf);
+      if (removed)
+      {
+        contactFeatureEnds.remove(sf);
+      }
+    }
+
+    boolean removedNonPositional = false;
+
+    /*
+     * if not found, try non-positional features
+     */
+    if (!removed && nonPositionalFeatures != null)
+    {
+      removedNonPositional = nonPositionalFeatures.remove(sf);
+      removed = removedNonPositional;
+    }
+
+    /*
+     * if not found, try nested features
+     */
+    if (!removed && nestedFeatures != null)
+    {
+      removed = nestedFeatures.delete(sf);
+    }
+
+    if (removed)
+    {
+      rescanAfterDelete();
+    }
+
+    return removed;
+  }
+
+  /**
+   * Rescan all features to recompute any cached values after an entry has been
+   * deleted. This is expected to be an infrequent event, so performance here is
+   * not critical.
+   */
+  protected synchronized void rescanAfterDelete()
+  {
+    positionalFeatureGroups.clear();
+    nonPositionalFeatureGroups.clear();
+    totalExtent = 0;
+    positionalMinScore = Float.NaN;
+    positionalMaxScore = Float.NaN;
+    nonPositionalMinScore = Float.NaN;
+    nonPositionalMaxScore = Float.NaN;
+
+    /*
+     * scan non-positional features for groups and scores
+     */
+    for (SequenceFeature sf : getNonPositionalFeatures())
+    {
+      nonPositionalFeatureGroups.add(sf.getFeatureGroup());
+      float score = sf.getScore();
+      nonPositionalMinScore = min(nonPositionalMinScore, score);
+      nonPositionalMaxScore = max(nonPositionalMaxScore, score);
+    }
+
+    /*
+     * scan positional features for groups, scores and extents
+     */
+    for (SequenceFeature sf : getPositionalFeatures())
+    {
+      positionalFeatureGroups.add(sf.getFeatureGroup());
+      float score = sf.getScore();
+      positionalMinScore = min(positionalMinScore, score);
+      positionalMaxScore = max(positionalMaxScore, score);
+      totalExtent += getFeatureLength(sf);
+    }
+  }
+
+  /**
+   * A helper method to return the minimum of two floats, where a non-NaN value
+   * is treated as 'less than' a NaN value (unlike Math.min which does the
+   * opposite)
+   * 
+   * @param f1
+   * @param f2
+   */
+  protected static float min(float f1, float f2)
+  {
+    if (Float.isNaN(f1))
+    {
+      return Float.isNaN(f2) ? f1 : f2;
+    }
+    else
+    {
+      return Float.isNaN(f2) ? f1 : Math.min(f1, f2);
+    }
+  }
+
+  /**
+   * A helper method to return the maximum of two floats, where a non-NaN value
+   * is treated as 'greater than' a NaN value (unlike Math.max which does the
+   * opposite)
+   * 
+   * @param f1
+   * @param f2
+   */
+  protected static float max(float f1, float f2)
+  {
+    if (Float.isNaN(f1))
+    {
+      return Float.isNaN(f2) ? f1 : f2;
+    }
+    else
+    {
+      return Float.isNaN(f2) ? f1 : Math.max(f1, f2);
+    }
+  }
+
+  /**
+   * Answers true if this store has no features, else false
+   * 
+   * @return
+   */
+  public boolean isEmpty()
+  {
+    boolean hasFeatures = !nonNestedFeatures.isEmpty()
+            || (contactFeatureStarts != null && !contactFeatureStarts
+                    .isEmpty())
+            || (nonPositionalFeatures != null && !nonPositionalFeatures
+                    .isEmpty())
+            || (nestedFeatures != null && nestedFeatures.size() > 0);
+
+    return !hasFeatures;
+  }
+
+  /**
+   * Answers the set of distinct feature groups stored, possibly including null,
+   * as an unmodifiable view of the set. The parameter determines whether the
+   * groups for positional or for non-positional features are returned.
+   * 
+   * @param positionalFeatures
+   * @return
+   */
+  public Set<String> getFeatureGroups(boolean positionalFeatures)
+  {
+    if (positionalFeatures)
+    {
+      return Collections.unmodifiableSet(positionalFeatureGroups);
+    }
+    else
+    {
+      return nonPositionalFeatureGroups == null ? Collections
+              .<String> emptySet() : Collections
+              .unmodifiableSet(nonPositionalFeatureGroups);
+    }
+  }
+
+  /**
+   * Performs a binary search of the (sorted) list to find the index of the
+   * first entry which returns true for the given comparator function. Returns
+   * the length of the list if there is no such entry.
+   * 
+   * @param features
+   * @param sc
+   * @return
+   */
+  protected static int binarySearch(List<SequenceFeature> features,
+          SearchCriterion sc)
+  {
+    int start = 0;
+    int end = features.size() - 1;
+    int matched = features.size();
+
+    while (start <= end)
+    {
+      int mid = (start + end) / 2;
+      SequenceFeature entry = features.get(mid);
+      boolean compare = sc.compare(entry);
+      if (compare)
+      {
+        matched = mid;
+        end = mid - 1;
+      }
+      else
+      {
+        start = mid + 1;
+      }
+    }
+
+    return matched;
+  }
+
+  /**
+   * Answers the number of positional (or non-positional) features stored.
+   * Contact features count as 1.
+   * 
+   * @param positional
+   * @return
+   */
+  public int getFeatureCount(boolean positional)
+  {
+    if (!positional)
+    {
+      return nonPositionalFeatures == null ? 0 : nonPositionalFeatures
+              .size();
+    }
+
+    int size = nonNestedFeatures.size();
+
+    if (contactFeatureStarts != null)
+    {
+      // note a contact feature (start/end) counts as one
+      size += contactFeatureStarts.size();
+    }
+
+    if (nestedFeatures != null)
+    {
+      size += nestedFeatures.size();
+    }
+
+    return size;
+  }
+
+  /**
+   * Answers the total length of positional features (or zero if there are
+   * none). Contact features contribute a value of 1 to the total.
+   * 
+   * @return
+   */
+  public int getTotalFeatureLength()
+  {
+    return totalExtent;
+  }
+
+  /**
+   * Answers the minimum score held for positional or non-positional features.
+   * This may be Float.NaN if there are no features, are none has a non-NaN
+   * score.
+   * 
+   * @param positional
+   * @return
+   */
+  public float getMinimumScore(boolean positional)
+  {
+    return positional ? positionalMinScore : nonPositionalMinScore;
+  }
+
+  /**
+   * Answers the maximum score held for positional or non-positional features.
+   * This may be Float.NaN if there are no features, are none has a non-NaN
+   * score.
+   * 
+   * @param positional
+   * @return
+   */
+  public float getMaximumScore(boolean positional)
+  {
+    return positional ? positionalMaxScore : nonPositionalMaxScore;
+  }
+
+  /**
+   * Answers a list of all either positional or non-positional features whose
+   * feature group matches the given group (which may be null)
+   * 
+   * @param positional
+   * @param group
+   * @return
+   */
+  public List<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    /*
+     * if we know features don't include the target group, no need
+     * to inspect them for matches
+     */
+    if (positional && !positionalFeatureGroups.contains(group)
+            || !positional && !nonPositionalFeatureGroups.contains(group))
+    {
+      return result;
+    }
+
+    List<SequenceFeature> sfs = positional ? getPositionalFeatures()
+            : getNonPositionalFeatures();
+    for (SequenceFeature sf : sfs)
+    {
+      String featureGroup = sf.getFeatureGroup();
+      if (group == null && featureGroup == null || group != null
+              && group.equals(featureGroup))
+      {
+        result.add(sf);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Adds the shift value to the start and end of all positional features.
+   * Returns true if at least one feature was updated, else false.
+   * 
+   * @param shift
+   * @return
+   */
+  public synchronized boolean shiftFeatures(int shift)
+  {
+    /*
+     * Because begin and end are final fields (to ensure the data store's
+     * integrity), we have to delete each feature and re-add it as amended.
+     * (Although a simple shift of all values would preserve data integrity!)
+     */
+    boolean modified = false;
+    for (SequenceFeature sf : getPositionalFeatures())
+    {
+      modified = true;
+      int newBegin = sf.getBegin() + shift;
+      int newEnd = sf.getEnd() + shift;
+
+      /*
+       * sanity check: don't shift left of the first residue
+       */
+      if (newEnd > 0)
+      {
+        newBegin = Math.max(1, newBegin);
+        SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd,
+                sf.getFeatureGroup(), sf.getScore());
+        addFeature(sf2);
+      }
+      delete(sf);
+    }
+    return modified;
+  }
+}
diff --git a/src/jalview/datamodel/features/NCList.java b/src/jalview/datamodel/features/NCList.java
new file mode 100644 (file)
index 0000000..b8160d3
--- /dev/null
@@ -0,0 +1,626 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An adapted implementation of NCList as described in the paper
+ * 
+ * <pre>
+ * Nested Containment List (NCList): a new algorithm for accelerating
+ * interval query of genome alignment and interval databases
+ * - Alexander V. Alekseyenko, Christopher J. Lee
+ * https://doi.org/10.1093/bioinformatics/btl647
+ * </pre>
+ */
+public class NCList<T extends ContiguousI>
+{
+  /*
+   * the number of ranges represented
+   */
+  private int size;
+
+  /*
+   * a list, in start position order, of sublists of ranges ordered so 
+   * that each contains (or is the same as) the one that follows it
+   */
+  private List<NCNode<T>> subranges;
+
+  /**
+   * Constructor given a list of things that are each located on a contiguous
+   * interval. Note that the constructor may reorder the list.
+   * <p>
+   * We assume here that for each range, start &lt;= end. Behaviour for reverse
+   * ordered ranges is undefined.
+   * 
+   * @param ranges
+   */
+  public NCList(List<T> ranges)
+  {
+    this();
+    build(ranges);
+  }
+
+  /**
+   * Sort and group ranges into sublists where each sublist represents a region
+   * and its contained subregions
+   * 
+   * @param ranges
+   */
+  protected void build(List<T> ranges)
+  {
+    /*
+     * sort by start ascending so that contained intervals 
+     * follow their containing interval
+     */
+    Collections.sort(ranges, RangeComparator.BY_START_POSITION);
+
+    List<Range> sublists = buildSubranges(ranges);
+
+    /*
+     * convert each subrange to an NCNode consisting of a range and
+     * (possibly) its contained NCList
+     */
+    for (Range sublist : sublists)
+    {
+      subranges.add(new NCNode<T>(ranges.subList(sublist.start,
+              sublist.end + 1)));
+    }
+
+    size = ranges.size();
+  }
+
+  public NCList(T entry)
+  {
+    this();
+    subranges.add(new NCNode<>(entry));
+    size = 1;
+  }
+
+  public NCList()
+  {
+    subranges = new ArrayList<NCNode<T>>();
+  }
+
+  /**
+   * Traverses the sorted ranges to identify sublists, within which each
+   * interval contains the one that follows it
+   * 
+   * @param ranges
+   * @return
+   */
+  protected List<Range> buildSubranges(List<T> ranges)
+  {
+    List<Range> sublists = new ArrayList<>();
+    
+    if (ranges.isEmpty())
+    {
+      return sublists;
+    }
+
+    int listStartIndex = 0;
+    long lastEndPos = Long.MAX_VALUE;
+
+    for (int i = 0; i < ranges.size(); i++)
+    {
+      ContiguousI nextInterval = ranges.get(i);
+      long nextStart = nextInterval.getBegin();
+      long nextEnd = nextInterval.getEnd();
+      if (nextStart > lastEndPos || nextEnd > lastEndPos)
+      {
+        /*
+         * this interval is not contained in the preceding one 
+         * close off the last sublist
+         */
+        sublists.add(new Range(listStartIndex, i - 1));
+        listStartIndex = i;
+      }
+      lastEndPos = nextEnd;
+    }
+
+    sublists.add(new Range(listStartIndex, ranges.size() - 1));
+    return sublists;
+  }
+
+  /**
+   * Adds one entry to the stored set (with duplicates allowed)
+   * 
+   * @param entry
+   */
+  public void add(T entry)
+  {
+    add(entry, true);
+  }
+
+  /**
+   * Adds one entry to the stored set, and returns true, unless allowDuplicates
+   * is set to false and it is already contained (by object equality test), in
+   * which case it is not added and this method returns false.
+   * 
+   * @param entry
+   * @param allowDuplicates
+   * @return
+   */
+  public synchronized boolean add(T entry, boolean allowDuplicates)
+  {
+    if (!allowDuplicates && contains(entry))
+    {
+      return false;
+    }
+
+    size++;
+    long start = entry.getBegin();
+    long end = entry.getEnd();
+
+    /*
+     * cases:
+     * - precedes all subranges: add as NCNode on front of list
+     * - follows all subranges: add as NCNode on end of list
+     * - enclosed by a subrange - add recursively to subrange
+     * - encloses one or more subranges - push them inside it
+     * - none of the above - add as a new node and resort nodes list (?)
+     */
+
+    /*
+     * find the first subrange whose end does not precede entry's start
+     */
+    int candidateIndex = findFirstOverlap(start);
+    if (candidateIndex == -1)
+    {
+      /*
+       * all subranges precede this one - add it on the end
+       */
+      subranges.add(new NCNode<>(entry));
+      return true;
+    }
+
+    /*
+     * search for maximal span of subranges i-k that the new entry
+     * encloses; or a subrange that encloses the new entry
+     */
+    boolean enclosing = false;
+    int firstEnclosed = 0;
+    int lastEnclosed = 0;
+    boolean overlapping = false;
+
+    for (int j = candidateIndex; j < subranges.size(); j++)
+    {
+      NCNode<T> subrange = subranges.get(j);
+
+      if (end < subrange.getBegin() && !overlapping && !enclosing)
+      {
+        /*
+         * new entry lies between subranges j-1 j
+         */
+        subranges.add(j, new NCNode<>(entry));
+        return true;
+      }
+
+      if (subrange.getBegin() <= start && subrange.getEnd() >= end)
+      {
+        /*
+         * push new entry inside this subrange as it encloses it
+         */
+        subrange.add(entry);
+        return true;
+      }
+      
+      if (start <= subrange.getBegin())
+      {
+        if (end >= subrange.getEnd())
+        {
+          /*
+           * new entry encloses this subrange (and possibly preceding ones);
+           * continue to find the maximal list it encloses
+           */
+          if (!enclosing)
+          {
+            firstEnclosed = j;
+          }
+          lastEnclosed = j;
+          enclosing = true;
+          continue;
+        }
+        else
+        {
+          /*
+           * entry spans from before this subrange to inside it
+           */
+          if (enclosing)
+          {
+            /*
+             * entry encloses one or more preceding subranges
+             */
+            addEnclosingRange(entry, firstEnclosed, lastEnclosed);
+            return true;
+          }
+          else
+          {
+            /*
+             * entry spans two subranges but doesn't enclose any
+             * so just add it 
+             */
+            subranges.add(j, new NCNode<>(entry));
+            return true;
+          }
+        }
+      }
+      else
+      {
+        overlapping = true;
+      }
+    }
+
+    /*
+     * drops through to here if new range encloses all others
+     * or overlaps the last one
+     */
+    if (enclosing)
+    {
+      addEnclosingRange(entry, firstEnclosed, lastEnclosed);
+    }
+    else
+    {
+      subranges.add(new NCNode<>(entry));
+    }
+
+    return true;
+  }
+  
+  /**
+   * Answers true if this NCList contains the given entry (by object equality
+   * test), else false
+   * 
+   * @param entry
+   * @return
+   */
+  public boolean contains(T entry)
+  {
+    /*
+     * find the first sublist that might overlap, i.e. 
+     * the first whose end position is >= from
+     */
+    int candidateIndex = findFirstOverlap(entry.getBegin());
+
+    if (candidateIndex == -1)
+    {
+      return false;
+    }
+
+    int to = entry.getEnd();
+
+    for (int i = candidateIndex; i < subranges.size(); i++)
+    {
+      NCNode<T> candidate = subranges.get(i);
+      if (candidate.getBegin() > to)
+      {
+        /*
+         * we are past the end of our target range
+         */
+        break;
+      }
+      if (candidate.contains(entry))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Update the tree so that the range of the new entry encloses subranges i to
+   * j (inclusive). That is, replace subranges i-j (inclusive) with a new
+   * subrange that contains them.
+   * 
+   * @param entry
+   * @param i
+   * @param j
+   */
+  protected synchronized void addEnclosingRange(T entry, final int i,
+          final int j)
+  {
+    NCList<T> newNCList = new NCList<>();
+    newNCList.addNodes(subranges.subList(i, j + 1));
+    NCNode<T> newNode = new NCNode<>(entry, newNCList);
+    for (int k = j; k >= i; k--)
+    {
+      subranges.remove(k);
+    }
+    subranges.add(i, newNode);
+  }
+
+  protected void addNodes(List<NCNode<T>> nodes)
+  {
+    for (NCNode<T> node : nodes)
+    {
+      subranges.add(node);
+      size += node.size();
+    }
+  }
+
+  /**
+   * Returns a (possibly empty) list of items whose extent overlaps the given
+   * range
+   * 
+   * @param from
+   *          start of overlap range (inclusive)
+   * @param to
+   *          end of overlap range (inclusive)
+   * @return
+   */
+  public List<T> findOverlaps(long from, long to)
+  {
+    List<T> result = new ArrayList<>();
+
+    findOverlaps(from, to, result);
+    
+    return result;
+  }
+
+  /**
+   * Recursively searches the NCList adding any items that overlap the from-to
+   * range to the result list
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findOverlaps(long from, long to, List<T> result)
+  {
+    /*
+     * find the first sublist that might overlap, i.e. 
+     * the first whose end position is >= from
+     */
+    int candidateIndex = findFirstOverlap(from);
+
+    if (candidateIndex == -1)
+    {
+      return;
+    }
+
+    for (int i = candidateIndex; i < subranges.size(); i++)
+    {
+      NCNode<T> candidate = subranges.get(i);
+      if (candidate.getBegin() > to)
+      {
+        /*
+         * we are past the end of our target range
+         */
+        break;
+      }
+      candidate.findOverlaps(from, to, result);
+    }
+
+  }
+
+  /**
+   * Search subranges for the first one whose end position is not before the
+   * target range's start position, i.e. the first one that may overlap the
+   * target range. Returns the index in the list of the first such range found,
+   * or -1 if none found.
+   * 
+   * @param from
+   * @return
+   */
+  protected int findFirstOverlap(long from)
+  {
+    /*
+     * The NCList paper describes binary search for this step,
+     * but this not implemented here as (a) I haven't understood it yet
+     * and (b) it seems to imply complications for adding to an NCList
+     */
+
+    int i = 0;
+    if (subranges != null)
+    {
+      for (NCNode<T> subrange : subranges)
+      {
+        if (subrange.getEnd() >= from)
+        {
+          return i;
+        }
+        i++;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Formats the tree as a bracketed list e.g.
+   * 
+   * <pre>
+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * </pre>
+   */
+  @Override
+  public String toString()
+  {
+    return subranges.toString();
+  }
+
+  /**
+   * Returns a string representation of the data where containment is shown by
+   * indentation on new lines
+   * 
+   * @return
+   */
+  public String prettyPrint()
+  {
+    StringBuilder sb = new StringBuilder(512);
+    int offset = 0;
+    int indent = 2;
+    prettyPrint(sb, offset, indent);
+    sb.append(System.lineSeparator());
+    return sb.toString();
+  }
+
+  /**
+   * @param sb
+   * @param offset
+   * @param indent
+   */
+  void prettyPrint(StringBuilder sb, int offset, int indent)
+  {
+    boolean first = true;
+    for (NCNode<T> subrange : subranges)
+    {
+      if (!first)
+      {
+        sb.append(System.lineSeparator());
+      }
+      first = false;
+      subrange.prettyPrint(sb, offset, indent);
+    }
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList, else false.
+   * 
+   * @return
+   */
+  public boolean isValid()
+  {
+    return isValid(Integer.MIN_VALUE, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList bounded within the given start-end range, else false.
+   * <p>
+   * Each subrange must lie within start-end (inclusive). Subranges must be
+   * ordered by start position ascending.
+   * <p>
+   * 
+   * @param start
+   * @param end
+   * @return
+   */
+  boolean isValid(final int start, final int end)
+  {
+    int lastStart = start;
+    for (NCNode<T> subrange : subranges)
+    {
+      if (subrange.getBegin() < lastStart)
+      {
+        System.err.println("error in NCList: range " + subrange.toString()
+                + " starts before " + lastStart);
+        return false;
+      }
+      if (subrange.getEnd() > end)
+      {
+        System.err.println("error in NCList: range " + subrange.toString()
+                + " ends after " + end);
+        return false;
+      }
+      lastStart = subrange.getBegin();
+
+      if (!subrange.isValid())
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Answers the lowest start position enclosed by the ranges
+   * 
+   * @return
+   */
+  public int getStart()
+  {
+    return subranges.isEmpty() ? 0 : subranges.get(0).getBegin();
+  }
+
+  /**
+   * Returns the number of ranges held (deep count)
+   * 
+   * @return
+   */
+  public int size()
+  {
+    return size;
+  }
+
+  /**
+   * Returns a list of all entries stored
+   * 
+   * @return
+   */
+  public List<T> getEntries()
+  {
+    List<T> result = new ArrayList<>();
+    getEntries(result);
+    return result;
+  }
+
+  /**
+   * Adds all contained entries to the given list
+   * 
+   * @param result
+   */
+  void getEntries(List<T> result)
+  {
+    for (NCNode<T> subrange : subranges)
+    {
+      subrange.getEntries(result);
+    }
+  }
+
+  /**
+   * Deletes the given entry from the store, returning true if it was found (and
+   * deleted), else false. This method makes no assumption that the entry is in
+   * the 'expected' place in the store, in case it has been modified since it
+   * was added. Only the first 'same object' match is deleted, not 'equal' or
+   * multiple objects.
+   * 
+   * @param entry
+   */
+  public synchronized boolean delete(T entry)
+  {
+    if (entry == null)
+    {
+      return false;
+    }
+    for (int i = 0; i < subranges.size(); i++)
+    {
+      NCNode<T> subrange = subranges.get(i);
+      NCList<T> subRegions = subrange.getSubRegions();
+
+      if (subrange.getRegion() == entry)
+      {
+        /*
+         * if the subrange is rooted on this entry, promote its
+         * subregions (if any) to replace the subrange here;
+         * NB have to resort subranges after doing this since e.g.
+         * [10-30 [12-20 [16-18], 13-19]]
+         * after deleting 12-20, 16-18 is promoted to sibling of 13-19
+         * but should follow it in the list of subranges of 10-30 
+         */
+        subranges.remove(i);
+        if (subRegions != null)
+        {
+          subranges.addAll(subRegions.subranges);
+          Collections.sort(subranges, RangeComparator.BY_START_POSITION);
+        }
+        size--;
+        return true;
+      }
+      else
+      {
+        if (subRegions != null && subRegions.delete(entry))
+        {
+          size--;
+          subrange.deleteSubRegionsIfEmpty();
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/jalview/datamodel/features/NCNode.java b/src/jalview/datamodel/features/NCNode.java
new file mode 100644 (file)
index 0000000..007f3b1
--- /dev/null
@@ -0,0 +1,255 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Each node of the NCList tree consists of a range, and (optionally) the NCList
+ * of ranges it encloses
+ *
+ * @param <V>
+ */
+class NCNode<V extends ContiguousI> implements ContiguousI
+{
+  /*
+   * deep size (number of ranges included)
+   */
+  private int size;
+
+  private V region;
+
+  /*
+   * null, or an object holding contained subregions of this nodes region
+   */
+  private NCList<V> subregions;
+
+  /**
+   * Constructor given a list of ranges
+   * 
+   * @param ranges
+   */
+  NCNode(List<V> ranges)
+  {
+    build(ranges);
+  }
+
+  /**
+   * Constructor given a single range
+   * 
+   * @param range
+   */
+  NCNode(V range)
+  {
+    List<V> ranges = new ArrayList<>();
+    ranges.add(range);
+    build(ranges);
+  }
+
+  NCNode(V entry, NCList<V> newNCList)
+  {
+    region = entry;
+    subregions = newNCList;
+    size = 1 + newNCList.size();
+  }
+
+  /**
+   * @param ranges
+   */
+  protected void build(List<V> ranges)
+  {
+    size = ranges.size();
+
+    if (!ranges.isEmpty())
+    {
+      region = ranges.get(0);
+    }
+    if (ranges.size() > 1)
+    {
+      subregions = new NCList<V>(ranges.subList(1, ranges.size()));
+    }
+  }
+
+  @Override
+  public int getBegin()
+  {
+    return region.getBegin();
+  }
+
+  @Override
+  public int getEnd()
+  {
+    return region.getEnd();
+  }
+
+  /**
+   * Formats the node as a bracketed list e.g.
+   * 
+   * <pre>
+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * </pre>
+   */
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(10 * size);
+    sb.append(region.getBegin()).append("-").append(region.getEnd());
+    if (subregions != null)
+    {
+      sb.append(" ").append(subregions.toString());
+    }
+    return sb.toString();
+  }
+
+  void prettyPrint(StringBuilder sb, int offset, int indent) {
+    for (int i = 0 ; i < offset ; i++) {
+      sb.append(" ");
+    }
+    sb.append(region.getBegin()).append("-").append(region.getEnd());
+    if (subregions != null)
+    {
+      sb.append(System.lineSeparator());
+      subregions.prettyPrint(sb, offset + 2, indent);
+    }
+  }
+  /**
+   * Add any ranges that overlap the from-to range to the result list
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  void findOverlaps(long from, long to, List<V> result)
+  {
+    if (region.getBegin() <= to && region.getEnd() >= from)
+    {
+      result.add(region);
+    }
+    if (subregions != null)
+    {
+      subregions.findOverlaps(from, to, result);
+    }
+  }
+
+  /**
+   * Add one range to this subrange
+   * 
+   * @param entry
+   */
+  synchronized void add(V entry)
+  {
+    if (entry.getBegin() < region.getBegin() || entry.getEnd() > region.getEnd()) {
+      throw new IllegalArgumentException(String.format(
+              "adding improper subrange %d-%d to range %d-%d",
+              entry.getBegin(), entry.getEnd(), region.getBegin(),
+              region.getEnd()));
+    }
+    if (subregions == null)
+    {
+      subregions = new NCList<V>(entry);
+    }
+    else
+    {
+      subregions.add(entry);
+    }
+    size++;
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList, else false.
+   * 
+   * @return
+   */
+  boolean isValid()
+  {
+    /*
+     * we don't handle reverse ranges
+     */
+    if (region != null && region.getBegin() > region.getEnd())
+    {
+      return false;
+    }
+    if (subregions == null)
+    {
+      return true;
+    }
+    return subregions.isValid(getBegin(), getEnd());
+  }
+
+  /**
+   * Adds all contained entries to the given list
+   * 
+   * @param entries
+   */
+  void getEntries(List<V> entries)
+  {
+    entries.add(region);
+    if (subregions != null)
+    {
+      subregions.getEntries(entries);
+    }
+  }
+
+  /**
+   * Answers true if this object contains the given entry (by object equals
+   * test), else false
+   * 
+   * @param entry
+   * @return
+   */
+  boolean contains(V entry)
+  {
+    if (entry == null)
+    {
+      return false;
+    }
+    if (entry.equals(region))
+    {
+      return true;
+    }
+    return subregions == null ? false : subregions.contains(entry);
+  }
+
+  /**
+   * Answers the 'root' region modelled by this object
+   * 
+   * @return
+   */
+  V getRegion()
+  {
+    return region;
+  }
+
+  /**
+   * Answers the (possibly null) contained regions within this object
+   * 
+   * @return
+   */
+  NCList<V> getSubRegions()
+  {
+    return subregions;
+  }
+
+  /**
+   * Nulls the subregion reference if it is empty (after a delete entry
+   * operation)
+   */
+  void deleteSubRegionsIfEmpty()
+  {
+    if (subregions != null && subregions.size() == 0)
+    {
+      subregions = null;
+    }
+  }
+
+  /**
+   * Answers the (deep) size of this node i.e. the number of ranges it models
+   * 
+   * @return
+   */
+  int size()
+  {
+    return size;
+  }
+}
diff --git a/src/jalview/datamodel/features/RangeComparator.java b/src/jalview/datamodel/features/RangeComparator.java
new file mode 100644 (file)
index 0000000..26ffee1
--- /dev/null
@@ -0,0 +1,78 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+
+import java.util.Comparator;
+
+/**
+ * A comparator that orders ranges by either start position or end position
+ * ascending. If the position matches, ordering is resolved by end position (or
+ * start position).
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class RangeComparator implements Comparator<ContiguousI>
+{
+  public static final Comparator<ContiguousI> BY_START_POSITION = new RangeComparator(
+          true);
+
+  public static final Comparator<ContiguousI> BY_END_POSITION = new RangeComparator(
+          false);
+
+  boolean byStart;
+
+  /**
+   * Constructor
+   * 
+   * @param byStartPosition
+   *          if true, order based on start position, if false by end position
+   */
+  RangeComparator(boolean byStartPosition)
+  {
+    byStart = byStartPosition;
+  }
+
+  @Override
+  public int compare(ContiguousI o1, ContiguousI o2)
+  {
+    int len1 = o1.getEnd() - o1.getBegin();
+    int len2 = o2.getEnd() - o2.getBegin();
+
+    if (byStart)
+    {
+      return compare(o1.getBegin(), o2.getBegin(), len1, len2);
+    }
+    else
+    {
+      return compare(o1.getEnd(), o2.getEnd(), len1, len2);
+    }
+  }
+
+  /**
+   * Compares two ranges for ordering
+   * 
+   * @param pos1
+   *          first range positional ordering criterion
+   * @param pos2
+   *          second range positional ordering criterion
+   * @param len1
+   *          first range length ordering criterion
+   * @param len2
+   *          second range length ordering criterion
+   * @return
+   */
+  public int compare(long pos1, long pos2, int len1, int len2)
+  {
+    int order = Long.compare(pos1, pos2);
+    if (order == 0)
+    {
+      /*
+       * if tied on position order, longer length sorts to left
+       * i.e. the negation of normal ordering by length
+       */
+      order = -Integer.compare(len1, len2);
+    }
+    return order;
+  }
+}
diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java
new file mode 100644 (file)
index 0000000..52da8c7
--- /dev/null
@@ -0,0 +1,455 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.SequenceFeature;
+import jalview.io.gff.SequenceOntologyFactory;
+import jalview.io.gff.SequenceOntologyI;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * A class that stores sequence features in a way that supports efficient
+ * querying by type and location (overlap). Intended for (but not limited to)
+ * storage of features for one sequence.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class SequenceFeatures implements SequenceFeaturesI
+{
+  /**
+   * a comparator for sorting features by start position ascending
+   */
+  private static Comparator<ContiguousI> FORWARD_STRAND = new Comparator<ContiguousI>()
+  {
+    @Override
+    public int compare(ContiguousI o1, ContiguousI o2)
+    {
+      return Integer.compare(o1.getBegin(), o2.getBegin());
+    }
+  };
+
+  /**
+   * a comparator for sorting features by end position descending
+   */
+  private static Comparator<ContiguousI> REVERSE_STRAND = new Comparator<ContiguousI>()
+  {
+    @Override
+    public int compare(ContiguousI o1, ContiguousI o2)
+    {
+      return Integer.compare(o2.getEnd(), o1.getEnd());
+    }
+  };
+
+  /*
+   * map from feature type to structured store of features for that type
+   * null types are permitted (but not a good idea!)
+   */
+  private Map<String, FeatureStore> featureStore;
+
+  /**
+   * Constructor
+   */
+  public SequenceFeatures()
+  {
+    /*
+     * use a TreeMap so that features are returned in alphabetical order of type
+     * ? wrap as a synchronized map for add and delete operations
+     */
+    // featureStore = Collections
+    // .synchronizedSortedMap(new TreeMap<String, FeatureStore>());
+    featureStore = new TreeMap<String, FeatureStore>();
+  }
+
+  /**
+   * Constructor given a list of features
+   */
+  public SequenceFeatures(List<SequenceFeature> features)
+  {
+    this();
+    if (features != null)
+    {
+      for (SequenceFeature feature : features)
+      {
+        add(feature);
+      }
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean add(SequenceFeature sf)
+  {
+    String type = sf.getType();
+    if (type == null)
+    {
+      System.err.println("Feature type may not be null: " + sf.toString());
+      return false;
+    }
+
+    if (featureStore.get(type) == null)
+    {
+      featureStore.put(type, new FeatureStore());
+    }
+    return featureStore.get(type).addFeature(sf);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeatures(int from, int to,
+          String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.findOverlappingFeatures(from, to));
+    }
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getAllFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    result.addAll(getPositionalFeatures(type));
+
+    result.addAll(getNonPositionalFeatures());
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getFeaturesByOntology(String... ontologyTerm)
+  {
+    if (ontologyTerm == null || ontologyTerm.length == 0)
+    {
+      return new ArrayList<>();
+    }
+
+    Set<String> featureTypes = getFeatureTypes(ontologyTerm);
+    return getAllFeatures(featureTypes.toArray(new String[featureTypes
+            .size()]));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getFeatureCount(boolean positional, String... type)
+  {
+    int result = 0;
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result += featureSet.getFeatureCount(positional);
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getTotalFeatureLength(String... type)
+  {
+    int result = 0;
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result += featureSet.getTotalFeatureLength();
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getPositionalFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.getPositionalFeatures());
+    }
+    return result;
+  }
+
+  /**
+   * A convenience method that converts a vararg for feature types to an
+   * Iterable over matched feature sets in key order
+   * 
+   * @param type
+   * @return
+   */
+  protected Iterable<FeatureStore> varargToTypes(String... type)
+  {
+    if (type == null || type.length == 0)
+    {
+      /*
+       * no vararg parameter supplied - return all
+       */
+      return featureStore.values();
+    }
+
+    List<FeatureStore> types = new ArrayList<>();
+    List<String> args = Arrays.asList(type);
+    for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
+    {
+      if (args.contains(featureType.getKey()))
+      {
+        types.add(featureType.getValue());
+      }
+    }
+    return types;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getContactFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.getContactFeatures());
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getNonPositionalFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.getNonPositionalFeatures());
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean delete(SequenceFeature sf)
+  {
+    for (FeatureStore featureSet : featureStore.values())
+    {
+      if (featureSet.delete(sf))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean hasFeatures()
+  {
+    for (FeatureStore featureSet : featureStore.values())
+    {
+      if (!featureSet.isEmpty())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureGroups(boolean positionalFeatures,
+          String... type)
+  {
+    Set<String> groups = new HashSet<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      groups.addAll(featureSet.getFeatureGroups(positionalFeatures));
+    }
+
+    return groups;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureTypesForGroups(boolean positionalFeatures,
+          String... groups)
+  {
+    Set<String> result = new HashSet<>();
+
+    for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
+    {
+      Set<String> featureGroups = featureType.getValue().getFeatureGroups(
+              positionalFeatures);
+      for (String group : groups)
+      {
+        if (featureGroups.contains(group))
+        {
+          /*
+           * yes this feature type includes one of the query groups
+           */
+          result.add(featureType.getKey());
+          break;
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureTypes(String... soTerm)
+  {
+    Set<String> types = new HashSet<>();
+    for (Entry<String, FeatureStore> entry : featureStore.entrySet())
+    {
+      String type = entry.getKey();
+      if (!entry.getValue().isEmpty() && isOntologyTerm(type, soTerm))
+      {
+        types.add(type);
+      }
+    }
+    return types;
+  }
+
+  /**
+   * Answers true if the given type is one of the specified sequence ontology
+   * terms (or a sub-type of one), or if no terms are supplied. Answers false if
+   * filter terms are specified and the given term does not match any of them.
+   * 
+   * @param type
+   * @param soTerm
+   * @return
+   */
+  protected boolean isOntologyTerm(String type, String... soTerm)
+  {
+    if (soTerm == null || soTerm.length == 0)
+    {
+      return true;
+    }
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    for (String term : soTerm)
+    {
+      if (so.isA(type, term))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public float getMinimumScore(String type, boolean positional)
+  {
+    return featureStore.containsKey(type) ? featureStore.get(type)
+            .getMinimumScore(positional) : Float.NaN;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public float getMaximumScore(String type, boolean positional)
+  {
+    return featureStore.containsKey(type) ? featureStore.get(type)
+            .getMaximumScore(positional) : Float.NaN;
+  }
+
+  /**
+   * A convenience method to sort features by start position ascending (if on
+   * forward strand), or end position descending (if on reverse strand)
+   * 
+   * @param features
+   * @param forwardStrand
+   */
+  public static void sortFeatures(List<SequenceFeature> features,
+          final boolean forwardStrand)
+  {
+    Collections.sort(features, forwardStrand ? FORWARD_STRAND
+            : REVERSE_STRAND);
+  }
+
+  /**
+   * {@inheritDoc} This method is 'semi-optimised': it only inspects features
+   * for types that include the specified group, but has to inspect every
+   * feature of those types for matching feature group. This is efficient unless
+   * a sequence has features that share the same type but are in different
+   * groups - an unlikely case.
+   * <p>
+   * For example, if RESNUM feature is created with group = PDBID, then features
+   * would only be retrieved for those sequences associated with the target
+   * PDBID (group).
+   */
+  @Override
+  public List<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group, String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      if (featureSet.getFeatureGroups(positional).contains(group))
+      {
+        result.addAll(featureSet.getFeaturesForGroup(positional, group));
+      }
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean shiftFeatures(int shift)
+  {
+    boolean modified = false;
+    for (FeatureStore fs : featureStore.values())
+    {
+      modified |= fs.shiftFeatures(shift);
+    }
+    return modified;
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/datamodel/features/SequenceFeaturesI.java b/src/jalview/datamodel/features/SequenceFeaturesI.java
new file mode 100644 (file)
index 0000000..58beca2
--- /dev/null
@@ -0,0 +1,204 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.List;
+import java.util.Set;
+
+public interface SequenceFeaturesI
+{
+
+  /**
+   * Adds one sequence feature to the store, and returns true, unless the
+   * feature is already contained in the store, in which case this method
+   * returns false. Containment is determined by SequenceFeature.equals()
+   * comparison. Answers false, and does not add the feature, if feature type is
+   * null.
+   * 
+   * @param sf
+   */
+  boolean add(SequenceFeature sf);
+
+  /**
+   * Returns a (possibly empty) list of features, optionally restricted to
+   * specified types, which overlap the given (inclusive) sequence position
+   * range
+   * 
+   * @param from
+   * @param to
+   * @param type
+   * @return
+   */
+  List<SequenceFeature> findFeatures(int from, int to,
+          String... type);
+
+  /**
+   * Answers a list of all features stored, in no particular guaranteed order.
+   * Positional features may optionally be restricted to specified types, but
+   * all non-positional features (if any) are always returned.
+   * <p>
+   * To filter non-positional features by type, use
+   * getNonPositionalFeatures(type).
+   * 
+   * @param type
+   * @return
+   */
+  List<SequenceFeature> getAllFeatures(String... type);
+
+  /**
+   * Answers a list of all positional (or non-positional) features which are in
+   * the specified feature group, optionally restricted to features of specified
+   * types.
+   * 
+   * @param positional
+   *          if true returns positional features, else non-positional features
+   * @param group
+   *          the feature group to be matched (which may be null)
+   * @param type
+   *          optional feature types to filter by
+   * @return
+   */
+  List<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group, String... type);
+
+  /**
+   * Answers a list of all features stored, whose type either matches one of the
+   * given ontology terms, or is a specialisation of a term in the Sequence
+   * Ontology. Results are returned in no particular guaranteed order.
+   * 
+   * @param ontologyTerm
+   * @return
+   */
+  List<SequenceFeature> getFeaturesByOntology(String... ontologyTerm);
+
+  /**
+   * Answers the number of (positional or non-positional) features, optionally
+   * restricted to specified feature types. Contact features are counted as 1.
+   * 
+   * @param positional
+   * @param type
+   * @return
+   */
+  int getFeatureCount(boolean positional, String... type);
+
+  /**
+   * Answers the total length of positional features, optionally restricted to
+   * specified feature types. Contact features are counted as length 1.
+   * 
+   * @param type
+   * @return
+   */
+  int getTotalFeatureLength(String... type);
+
+  /**
+   * Answers a list of all positional features, optionally restricted to
+   * specified types, in no particular guaranteed order
+   * 
+   * @param type
+   * @return
+   */
+  List<SequenceFeature> getPositionalFeatures(
+          String... type);
+
+  /**
+   * Answers a list of all contact features, optionally restricted to specified
+   * types, in no particular guaranteed order
+   * 
+   * @return
+   */
+  List<SequenceFeature> getContactFeatures(String... type);
+
+  /**
+   * Answers a list of all non-positional features, optionally restricted to
+   * specified types, in no particular guaranteed order
+   * 
+   * @param type
+   *          if no type is specified, all are returned
+   * @return
+   */
+  List<SequenceFeature> getNonPositionalFeatures(
+          String... type);
+
+  /**
+   * Deletes the given feature from the store, returning true if it was found
+   * (and deleted), else false. This method makes no assumption that the feature
+   * is in the 'expected' place in the store, in case it has been modified since
+   * it was added.
+   * 
+   * @param sf
+   */
+  boolean delete(SequenceFeature sf);
+
+  /**
+   * Answers true if this store contains at least one feature, else false
+   * 
+   * @return
+   */
+  boolean hasFeatures();
+
+  /**
+   * Returns a set of the distinct feature groups present in the collection. The
+   * set may include null. The boolean parameter determines whether the groups
+   * for positional or for non-positional features are returned. The optional
+   * type parameter may be used to restrict to groups for specified feature
+   * types.
+   * 
+   * @param positionalFeatures
+   * @param type
+   * @return
+   */
+  Set<String> getFeatureGroups(boolean positionalFeatures,
+          String... type);
+
+  /**
+   * Answers the set of distinct feature types for which there is at least one
+   * feature with one of the given feature group(s). The boolean parameter
+   * determines whether the groups for positional or for non-positional features
+   * are returned.
+   * 
+   * @param positionalFeatures
+   * @param groups
+   * @return
+   */
+  Set<String> getFeatureTypesForGroups(
+          boolean positionalFeatures, String... groups);
+
+  /**
+   * Answers a set of the distinct feature types for which a feature is stored.
+   * The types may optionally be restricted to those which match, or are a
+   * subtype of, given sequence ontology terms
+   * 
+   * @return
+   */
+  Set<String> getFeatureTypes(String... soTerm);
+
+  /**
+   * Answers the minimum score held for positional or non-positional features
+   * for the specified type. This may be Float.NaN if there are no features, or
+   * none has a non-NaN score.
+   * 
+   * @param type
+   * @param positional
+   * @return
+   */
+  float getMinimumScore(String type, boolean positional);
+
+  /**
+   * Answers the maximum score held for positional or non-positional features
+   * for the specified type. This may be Float.NaN if there are no features, or
+   * none has a non-NaN score.
+   * 
+   * @param type
+   * @param positional
+   * @return
+   */
+  float getMaximumScore(String type, boolean positional);
+
+  /**
+   * Adds the shift amount to the start and end of all positional features,
+   * returning true if at least one feature was shifted, else false
+   * 
+   * @param shift
+   */
+  abstract boolean shiftFeatures(int shift);
+}
\ No newline at end of file
index 2de100b..bbe6a20 100644 (file)
@@ -371,8 +371,8 @@ public class EmblEntry
         System.err.println(
                 "Implementation Notice: EMBLCDS records not properly supported yet - Making up the CDNA region of this sequence... may be incorrect ("
                         + sourceDb + ":" + getAccession() + ")");
-        if (translationLength
-                * 3 == (1 - codonStart + dna.getSequence().length))
+        int dnaLength = dna.getLength();
+        if (translationLength * 3 == (1 - codonStart + dnaLength))
         {
           System.err.println(
                   "Not allowing for additional stop codon at end of cDNA fragment... !");
@@ -383,8 +383,7 @@ public class EmblEntry
                   new int[]
                   { 1, translationLength }, 3, 1);
         }
-        if ((translationLength + 1)
-                * 3 == (1 - codonStart + dna.getSequence().length))
+        if ((translationLength + 1) * 3 == (1 - codonStart + dnaLength))
         {
           System.err.println(
                   "Allowing for additional stop codon at end of cDNA fragment... will probably cause an error in VAMSAs!");
@@ -456,13 +455,27 @@ public class EmblEntry
       /*
        * add cds features to dna sequence
        */
-      for (int xint = 0; exons != null && xint < exons.length; xint += 2)
+      String cds = feature.getName(); // "CDS"
+      for (int xint = 0; exons != null && xint < exons.length - 1; xint += 2)
       {
-        SequenceFeature sf = makeCdsFeature(exons, xint, proteinName,
-                proteinId, vals, codonStart);
-        sf.setType(feature.getName()); // "CDS"
+        int exonStart = exons[xint];
+        int exonEnd = exons[xint + 1];
+        int begin = Math.min(exonStart, exonEnd);
+        int end = Math.max(exonStart, exonEnd);
+        int exonNumber = xint / 2 + 1;
+        String desc = String.format("Exon %d for protein '%s' EMBLCDS:%s",
+                exonNumber, proteinName, proteinId);
+
+        SequenceFeature sf = makeCdsFeature(cds, desc, begin, end,
+                sourceDb, vals);
+
         sf.setEnaLocation(feature.getLocation());
-        sf.setFeatureGroup(sourceDb);
+        boolean forwardStrand = exonStart <= exonEnd;
+        sf.setStrand(forwardStrand ? "+" : "-");
+        sf.setPhase(String.valueOf(codonStart - 1));
+        sf.setValue(FeatureProperties.EXONPOS, exonNumber);
+        sf.setValue(FeatureProperties.EXONPRODUCT, proteinName);
+
         dna.addSequenceFeature(sf);
       }
     }
@@ -577,33 +590,24 @@ public class EmblEntry
   /**
    * Helper method to construct a SequenceFeature for one cds range
    * 
-   * @param exons
-   *          array of cds [start, end, ...] positions
-   * @param exonStartIndex
-   *          offset into the exons array
-   * @param proteinName
-   * @param proteinAccessionId
+   * @param type
+   *          feature type ("CDS")
+   * @param desc
+   *          description
+   * @param begin
+   *          start position
+   * @param end
+   *          end position
+   * @param group
+   *          feature group
    * @param vals
    *          map of 'miscellaneous values' for feature
-   * @param codonStart
-   *          codon start position for CDS (1/2/3, normally 1)
    * @return
    */
-  protected SequenceFeature makeCdsFeature(int[] exons, int exonStartIndex,
-          String proteinName, String proteinAccessionId,
-          Map<String, String> vals, int codonStart)
-  {
-    int exonNumber = exonStartIndex / 2 + 1;
-    SequenceFeature sf = new SequenceFeature();
-    sf.setBegin(Math.min(exons[exonStartIndex], exons[exonStartIndex + 1]));
-    sf.setEnd(Math.max(exons[exonStartIndex], exons[exonStartIndex + 1]));
-    sf.setDescription(String.format("Exon %d for protein '%s' EMBLCDS:%s",
-            exonNumber, proteinName, proteinAccessionId));
-    sf.setPhase(String.valueOf(codonStart - 1));
-    sf.setStrand(
-            exons[exonStartIndex] <= exons[exonStartIndex + 1] ? "+" : "-");
-    sf.setValue(FeatureProperties.EXONPOS, exonNumber);
-    sf.setValue(FeatureProperties.EXONPRODUCT, proteinName);
+  protected SequenceFeature makeCdsFeature(String type, String desc,
+          int begin, int end, String group, Map<String, String> vals)
+  {
+    SequenceFeature sf = new SequenceFeature(type, desc, begin, end, group);
     if (!vals.isEmpty())
     {
       StringBuilder sb = new StringBuilder();
similarity index 90%
rename from src/jalview/datamodel/UniprotEntry.java
rename to src/jalview/datamodel/xdb/uniprot/UniprotEntry.java
index 4cf0f13..a3537c9 100755 (executable)
@@ -18,7 +18,9 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
+
+import jalview.datamodel.PDBEntry;
 
 import java.util.Vector;
 
@@ -36,7 +38,7 @@ public class UniprotEntry
 
   Vector<String> accession;
 
-  Vector<SequenceFeature> feature;
+  Vector<UniprotFeature> feature;
 
   Vector<PDBEntry> dbrefs;
 
@@ -47,12 +49,12 @@ public class UniprotEntry
     accession = items;
   }
 
-  public void setFeature(Vector<SequenceFeature> items)
+  public void setFeature(Vector<UniprotFeature> items)
   {
     feature = items;
   }
 
-  public Vector<SequenceFeature> getFeature()
+  public Vector<UniprotFeature> getFeature()
   {
     return feature;
   }
diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java b/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java
new file mode 100644 (file)
index 0000000..4a359ff
--- /dev/null
@@ -0,0 +1,78 @@
+package jalview.datamodel.xdb.uniprot;
+
+/**
+ * A data model class for binding from Uniprot XML via uniprot_mapping.xml
+ */
+public class UniprotFeature
+{
+  private String type;
+
+  private String description;
+
+  private String status;
+
+  private int begin;
+
+  private int end;
+
+  public String getType()
+  {
+    return type;
+  }
+
+  public void setType(String t)
+  {
+    this.type = t;
+  }
+
+  public String getDescription()
+  {
+    return description;
+  }
+
+  public void setDescription(String d)
+  {
+    this.description = d;
+  }
+
+  public String getStatus()
+  {
+    return status;
+  }
+
+  public void setStatus(String s)
+  {
+    this.status = s;
+  }
+
+  public int getBegin()
+  {
+    return begin;
+  }
+
+  public void setBegin(int b)
+  {
+    this.begin = b;
+  }
+
+  public int getEnd()
+  {
+    return end;
+  }
+
+  public void setEnd(int e)
+  {
+    this.end = e;
+  }
+
+  public int getPosition()
+  {
+    return begin;
+  }
+
+  public void setPosition(int p)
+  {
+    this.begin = p;
+    this.end = p;
+  }
+}
similarity index 96%
rename from src/jalview/datamodel/UniprotFile.java
rename to src/jalview/datamodel/xdb/uniprot/UniprotFile.java
index f0e38d8..9cc0391 100755 (executable)
@@ -18,7 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
 
 import java.util.Vector;
 
@@ -18,7 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
 
 import java.util.Vector;
 
similarity index 97%
rename from src/jalview/datamodel/UniprotSequence.java
rename to src/jalview/datamodel/xdb/uniprot/UniprotSequence.java
index 1150f1e..bdba73f 100755 (executable)
@@ -18,7 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
 
 /**
  * Data model for the sequence returned by a Uniprot query
index dc000c6..6d031b7 100644 (file)
@@ -24,6 +24,9 @@ import jalview.datamodel.SequenceFeature;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 
+import java.util.HashMap;
+import java.util.Map;
+
 import com.stevesoft.pat.Regex;
 
 /**
@@ -44,6 +47,13 @@ public class EnsemblCdna extends EnsemblSeqProxy
   private static final Regex ACCESSION_REGEX = new Regex(
           "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)");
 
+  private static Map<String, String> params = new HashMap<String, String>();
+
+  static
+  {
+    params.put("object_type", "transcript");
+  }
+
   /*
    * fetch exon features on genomic sequence (to identify the cdna regions)
    * and cds and variation features (to retain)
@@ -128,4 +138,14 @@ public class EnsemblCdna extends EnsemblSeqProxy
     return false;
   }
 
+  /**
+   * Parameter object_type=cdna added to ensure cdna and not peptide is returned
+   * (JAL-2529)
+   */
+  @Override
+  protected Map<String, String> getAdditionalParameters()
+  {
+    return params;
+  }
+
 }
index edeeedd..365c1c2 100644 (file)
@@ -26,6 +26,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.FeatureColour;
@@ -283,22 +284,20 @@ public class EnsemblGene extends EnsemblSeqProxy
    */
   protected void clearGeneFeatures(SequenceI gene)
   {
-    SequenceFeature[] sfs = gene.getSequenceFeatures();
-    if (sfs != null)
+    /*
+     * Note we include NMD_transcript_variant here because it behaves like 
+     * 'transcript' in Ensembl, although strictly speaking it is not 
+     * (it is a sub-type of sequence_variant)    
+     */
+    String[] soTerms = new String[] {
+        SequenceOntologyI.NMD_TRANSCRIPT_VARIANT,
+        SequenceOntologyI.TRANSCRIPT, SequenceOntologyI.EXON,
+        SequenceOntologyI.CDS };
+    List<SequenceFeature> sfs = gene.getFeatures().getFeaturesByOntology(
+            soTerms);
+    for (SequenceFeature sf : sfs)
     {
-      SequenceOntologyI so = SequenceOntologyFactory.getInstance();
-      List<SequenceFeature> filtered = new ArrayList<SequenceFeature>();
-      for (SequenceFeature sf : sfs)
-      {
-        String type = sf.getType();
-        if (!isTranscript(type) && !so.isA(type, SequenceOntologyI.EXON)
-                && !so.isA(type, SequenceOntologyI.CDS))
-        {
-          filtered.add(sf);
-        }
-      }
-      gene.setSequenceFeatures(
-              filtered.toArray(new SequenceFeature[filtered.size()]));
+      gene.deleteFeature(sf);
     }
   }
 
@@ -348,6 +347,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     {
       splices = findFeatures(gene, SequenceOntologyI.CDS, parentId);
     }
+    SequenceFeatures.sortFeatures(splices, true);
 
     int transcriptLength = 0;
     final char[] geneChars = gene.getSequence();
@@ -398,7 +398,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     mapTo.add(new int[] { 1, transcriptLength });
     MapList mapping = new MapList(mappedFrom, mapTo, 1, 1);
     EnsemblCdna cdna = new EnsemblCdna(getDomain());
-    cdna.transferFeatures(gene.getSequenceFeatures(),
+    cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(),
             transcript.getDatasetSequence(), mapping, parentId);
 
     /*
@@ -439,19 +439,18 @@ public class EnsemblGene extends EnsemblSeqProxy
     List<SequenceFeature> transcriptFeatures = new ArrayList<SequenceFeature>();
 
     String parentIdentifier = GENE_PREFIX + accId;
-    SequenceFeature[] sfs = geneSequence.getSequenceFeatures();
+    // todo optimise here by transcript type!
+    List<SequenceFeature> sfs = geneSequence.getFeatures()
+            .getPositionalFeatures();
 
-    if (sfs != null)
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
+      if (isTranscript(sf.getType()))
       {
-        if (isTranscript(sf.getType()))
+        String parent = (String) sf.getValue(PARENT);
+        if (parentIdentifier.equals(parent))
         {
-          String parent = (String) sf.getValue(PARENT);
-          if (parentIdentifier.equals(parent))
-          {
-            transcriptFeatures.add(sf);
-          }
+          transcriptFeatures.add(sf);
         }
       }
     }
index 16b858a..577111e 100644 (file)
@@ -30,6 +30,7 @@ import jalview.datamodel.DBRefSource;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.exceptions.JalviewException;
 import jalview.io.FastaFile;
 import jalview.io.FileParse;
@@ -37,8 +38,8 @@ import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
+import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
-import jalview.util.RangeComparator;
 
 import java.io.IOException;
 import java.net.MalformedURLException;
@@ -46,8 +47,9 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Base class for Ensembl sequence fetchers
@@ -470,11 +472,31 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     urlstring.append("?type=").append(getSourceEnsemblType().getType());
     urlstring.append(("&Accept=text/x-fasta"));
 
+    Map<String, String> params = getAdditionalParameters();
+    if (params != null)
+    {
+      for (Entry<String, String> entry : params.entrySet())
+      {
+        urlstring.append("&").append(entry.getKey()).append("=")
+                .append(entry.getValue());
+      }
+    }
+
     URL url = new URL(urlstring.toString());
     return url;
   }
 
   /**
+   * Override this method to add any additional x=y URL parameters needed
+   * 
+   * @return
+   */
+  protected Map<String, String> getAdditionalParameters()
+  {
+    return null;
+  }
+
+  /**
    * A sequence/id POST request currently allows up to 50 queries
    * 
    * @see http://rest.ensembl.org/documentation/info/sequence_id_post
@@ -538,8 +560,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
           String accId, int start)
   {
-    SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
-    if (sfs == null)
+    // SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
+    List<SequenceFeature> sfs = sourceSequence.getFeatures()
+            .getPositionalFeatures();
+    if (sfs.isEmpty())
     {
       return null;
     }
@@ -609,7 +633,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
      * a final sort is needed since Ensembl returns CDS sorted within source
      * (havana / ensembl_havana)
      */
-    Collections.sort(regions, new RangeComparator(direction == 1));
+    Collections.sort(regions, direction == 1 ? IntRangeComparator.ASCENDING
+            : IntRangeComparator.DESCENDING);
 
     List<int[]> to = Arrays
             .asList(new int[]
@@ -661,13 +686,15 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
     if (mappedRange != null)
     {
-      SequenceFeature copy = new SequenceFeature(sf);
-      copy.setBegin(Math.min(mappedRange[0], mappedRange[1]));
-      copy.setEnd(Math.max(mappedRange[0], mappedRange[1]));
-      if (".".equals(copy.getFeatureGroup()))
+      String group = sf.getFeatureGroup();
+      if (".".equals(group))
       {
-        copy.setFeatureGroup(getDbSource());
+        group = getDbSource();
       }
+      int newBegin = Math.min(mappedRange[0], mappedRange[1]);
+      int newEnd = Math.max(mappedRange[0], mappedRange[1]);
+      SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+              group, sf.getScore());
       targetSequence.addSequenceFeature(copy);
 
       /*
@@ -765,8 +792,9 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       return false;
     }
 
-    // long start = System.currentTimeMillis();
-    SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
+//    long start = System.currentTimeMillis();
+    List<SequenceFeature> sfs = sourceSequence.getFeatures()
+            .getPositionalFeatures();
     MapList mapping = getGenomicRangesFromFeatures(sourceSequence,
             accessionId, targetSequence.getStart());
     if (mapping == null)
@@ -776,10 +804,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
     boolean result = transferFeatures(sfs, targetSequence, mapping,
             accessionId);
-    // System.out.println("transferFeatures (" + (sfs.length) + " --> "
-    // + targetSequence.getSequenceFeatures().length + ") to "
-    // + targetSequence.getName()
-    // + " took " + (System.currentTimeMillis() - start) + "ms");
+//    System.out.println("transferFeatures (" + (sfs.size()) + " --> "
+//            + targetSequence.getFeatures().getFeatureCount(true) + ") to "
+//            + targetSequence.getName() + " took "
+//            + (System.currentTimeMillis() - start) + "ms");
     return result;
   }
 
@@ -788,13 +816,13 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
    * converted using the mapping. Features which do not overlap are ignored.
    * Features whose parent is not the specified identifier are also ignored.
    * 
-   * @param features
+   * @param sfs
    * @param targetSequence
    * @param mapping
    * @param parentId
    * @return
    */
-  protected boolean transferFeatures(SequenceFeature[] features,
+  protected boolean transferFeatures(List<SequenceFeature> sfs,
           SequenceI targetSequence, MapList mapping, String parentId)
   {
     final boolean forwardStrand = mapping.isFromForwardStrand();
@@ -804,10 +832,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
      * position descending if reverse strand) so as to add them in
      * 'forwards' order to the target sequence
      */
-    sortFeatures(features, forwardStrand);
+    SequenceFeatures.sortFeatures(sfs, forwardStrand);
 
     boolean transferred = false;
-    for (SequenceFeature sf : features)
+    for (SequenceFeature sf : sfs)
     {
       if (retainFeature(sf, parentId))
       {
@@ -819,33 +847,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
-   * Sort features by start position ascending (if on forward strand), or end
-   * position descending (if on reverse strand)
-   * 
-   * @param features
-   * @param forwardStrand
-   */
-  protected static void sortFeatures(SequenceFeature[] features,
-          final boolean forwardStrand)
-  {
-    Arrays.sort(features, new Comparator<SequenceFeature>()
-    {
-      @Override
-      public int compare(SequenceFeature o1, SequenceFeature o2)
-      {
-        if (forwardStrand)
-        {
-          return Integer.compare(o1.getBegin(), o2.getBegin());
-        }
-        else
-        {
-          return Integer.compare(o2.getEnd(), o1.getEnd());
-        }
-      }
-    });
-  }
-
-  /**
    * Answers true if the feature type is one we want to keep for the sequence.
    * Some features are only retrieved in order to identify the sequence range,
    * and may then be discarded as redundant information (e.g. "CDS" feature for
@@ -887,35 +888,30 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
   /**
    * Returns a (possibly empty) list of features on the sequence which have the
-   * specified sequence ontology type (or a sub-type of it), and the given
+   * specified sequence ontology term (or a sub-type of it), and the given
    * identifier as parent
    * 
    * @param sequence
-   * @param type
+   * @param term
    * @param parentId
    * @return
    */
   protected List<SequenceFeature> findFeatures(SequenceI sequence,
-          String type, String parentId)
+          String term, String parentId)
   {
     List<SequenceFeature> result = new ArrayList<SequenceFeature>();
 
-    SequenceFeature[] sfs = sequence.getSequenceFeatures();
-    if (sfs != null)
+    List<SequenceFeature> sfs = sequence.getFeatures()
+            .getFeaturesByOntology(term);
+    for (SequenceFeature sf : sfs)
     {
-      SequenceOntologyI so = SequenceOntologyFactory.getInstance();
-      for (SequenceFeature sf : sfs)
+      String parent = (String) sf.getValue(PARENT);
+      if (parent != null && parent.equals(parentId))
       {
-        if (so.isA(sf.getType(), type))
-        {
-          String parent = (String) sf.getValue(PARENT);
-          if (parent.equals(parentId))
-          {
-            result.add(sf);
-          }
-        }
+        result.add(sf);
       }
     }
+
     return result;
   }
 
index ddf3b1a..dc3d0ee 100644 (file)
@@ -355,10 +355,10 @@ public class JmolParser extends StructureFile implements JmolStatusListener
           SequenceI sq, char[] secstr, char[] secstrcode, String chainId,
           int firstResNum)
   {
-    char[] seq = sq.getSequence();
+    int length = sq.getLength();
     boolean ssFound = false;
-    Annotation asecstr[] = new Annotation[seq.length + firstResNum - 1];
-    for (int p = 0; p < seq.length; p++)
+    Annotation asecstr[] = new Annotation[length + firstResNum - 1];
+    for (int p = 0; p < length; p++)
     {
       if (secstr[p] >= 'A' && secstr[p] <= 'z')
       {
index 8c0ea66..f923f7f 100644 (file)
@@ -20,7 +20,7 @@
  */
 package jalview.ext.rbvi.chimera;
 
-import jalview.util.RangeComparator;
+import jalview.util.IntRangeComparator;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -127,7 +127,7 @@ public class AtomSpecModel
         /*
          * sort ranges into ascending start position order
          */
-        Collections.sort(rangeList, new RangeComparator(true));
+        Collections.sort(rangeList, IntRangeComparator.ASCENDING);
 
         int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
         int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
index dd91087..dad8511 100644 (file)
@@ -411,12 +411,8 @@ public class ChimeraCommands
           StructureMapping mapping, SequenceI seq,
           Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
   {
-    SequenceFeature[] sfs = seq.getSequenceFeatures();
-    if (sfs == null)
-    {
-      return;
-    }
-
+    List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
+            visibleFeatures.toArray(new String[visibleFeatures.size()]));
     for (SequenceFeature sf : sfs)
     {
       String type = sf.getType();
@@ -427,7 +423,7 @@ public class ChimeraCommands
        */
       boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
               .equals(sf.getFeatureGroup());
-      if (isFromViewer || !visibleFeatures.contains(type))
+      if (isFromViewer)
       {
         continue;
       }
index c78abdc..13b715e 100644 (file)
@@ -2444,7 +2444,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     viewport.setSelectionGroup(null);
     viewport.getColumnSelection().clear();
     viewport.setSelectionGroup(null);
-    alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null);
     alignPanel.getIdPanel().getIdCanvas().searchResults = null;
     // JAL-2034 - should delegate to
     // alignPanel to decide if overview needs
@@ -4387,7 +4386,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // Java's Transferable for native dnd
     evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
     Transferable t = evt.getTransferable();
-    List<String> files = new ArrayList<>();
+    final AlignFrame thisaf = this;
+    final List<String> files = new ArrayList<>();
     List<DataSourceType> protocols = new ArrayList<>();
 
     try
@@ -4399,133 +4399,144 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
     if (files != null)
     {
-      try
+      new Thread(new Runnable()
       {
-        // check to see if any of these files have names matching sequences in
-        // the alignment
-        SequenceIdMatcher idm = new SequenceIdMatcher(
-                viewport.getAlignment().getSequencesArray());
-        /**
-         * Object[] { String,SequenceI}
-         */
-        ArrayList<Object[]> filesmatched = new ArrayList<>();
-        ArrayList<String> filesnotmatched = new ArrayList<>();
-        for (int i = 0; i < files.size(); i++)
+        @Override
+        public void run()
         {
-          String file = files.get(i).toString();
-          String pdbfn = "";
-          DataSourceType protocol = FormatAdapter.checkProtocol(file);
-          if (protocol == DataSourceType.FILE)
-          {
-            File fl = new File(file);
-            pdbfn = fl.getName();
-          }
-          else if (protocol == DataSourceType.URL)
-          {
-            URL url = new URL(file);
-            pdbfn = url.getFile();
-          }
-          if (pdbfn.length() > 0)
+          try
           {
-            // attempt to find a match in the alignment
-            SequenceI[] mtch = idm.findAllIdMatches(pdbfn);
-            int l = 0, c = pdbfn.indexOf(".");
-            while (mtch == null && c != -1)
+            // check to see if any of these files have names matching sequences
+            // in
+            // the alignment
+            SequenceIdMatcher idm = new SequenceIdMatcher(
+                    viewport.getAlignment().getSequencesArray());
+            /**
+             * Object[] { String,SequenceI}
+             */
+            ArrayList<Object[]> filesmatched = new ArrayList<>();
+            ArrayList<String> filesnotmatched = new ArrayList<>();
+            for (int i = 0; i < files.size(); i++)
             {
-              do
+              String file = files.get(i).toString();
+              String pdbfn = "";
+              DataSourceType protocol = FormatAdapter.checkProtocol(file);
+              if (protocol == DataSourceType.FILE)
               {
-                l = c;
-              } while ((c = pdbfn.indexOf(".", l)) > l);
-              if (l > -1)
-              {
-                pdbfn = pdbfn.substring(0, l);
+                File fl = new File(file);
+                pdbfn = fl.getName();
               }
-              mtch = idm.findAllIdMatches(pdbfn);
-            }
-            if (mtch != null)
-            {
-              FileFormatI type = null;
-              try
+              else if (protocol == DataSourceType.URL)
               {
-                type = new IdentifyFile().identify(file, protocol);
-              } catch (Exception ex)
-              {
-                type = null;
+                URL url = new URL(file);
+                pdbfn = url.getFile();
               }
-              if (type != null && type.isStructureFile())
+              if (pdbfn.length() > 0)
               {
-                filesmatched.add(new Object[] { file, protocol, mtch });
-                continue;
+                // attempt to find a match in the alignment
+                SequenceI[] mtch = idm.findAllIdMatches(pdbfn);
+                int l = 0, c = pdbfn.indexOf(".");
+                while (mtch == null && c != -1)
+                {
+                  do
+                  {
+                    l = c;
+                  } while ((c = pdbfn.indexOf(".", l)) > l);
+                  if (l > -1)
+                  {
+                    pdbfn = pdbfn.substring(0, l);
+                  }
+                  mtch = idm.findAllIdMatches(pdbfn);
+                }
+                if (mtch != null)
+                {
+                  FileFormatI type = null;
+                  try
+                  {
+                    type = new IdentifyFile().identify(file, protocol);
+                  } catch (Exception ex)
+                  {
+                    type = null;
+                  }
+                  if (type != null && type.isStructureFile())
+                  {
+                    filesmatched.add(new Object[] { file, protocol, mtch });
+                    continue;
+                  }
+                }
+                // File wasn't named like one of the sequences or wasn't a PDB
+                // file.
+                filesnotmatched.add(file);
               }
             }
-            // File wasn't named like one of the sequences or wasn't a PDB file.
-            filesnotmatched.add(file);
-          }
-        }
-        int assocfiles = 0;
-        if (filesmatched.size() > 0)
-        {
-          if (Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false)
-                  || JvOptionPane.showConfirmDialog(this,
-                          MessageManager.formatMessage(
-                                  "label.automatically_associate_structure_files_with_sequences_same_name",
-                                  new Object[]
-                                  { Integer.valueOf(filesmatched.size())
-                                          .toString() }),
-                          MessageManager.getString(
-                                  "label.automatically_associate_structure_files_by_name"),
-                          JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION)
-
-          {
-            for (Object[] fm : filesmatched)
+            int assocfiles = 0;
+            if (filesmatched.size() > 0)
             {
-              // try and associate
-              // TODO: may want to set a standard ID naming formalism for
-              // associating PDB files which have no IDs.
-              for (SequenceI toassoc : (SequenceI[]) fm[2])
+              if (Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false)
+                      || JvOptionPane.showConfirmDialog(thisaf,
+                              MessageManager.formatMessage(
+                                      "label.automatically_associate_structure_files_with_sequences_same_name",
+                                      new Object[]
+                                      { Integer.valueOf(filesmatched.size())
+                                              .toString() }),
+                              MessageManager.getString(
+                                      "label.automatically_associate_structure_files_by_name"),
+                              JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION)
+
               {
-                PDBEntry pe = new AssociatePdbFileWithSeq()
-                        .associatePdbWithSeq((String) fm[0],
-                                (DataSourceType) fm[1], toassoc, false,
-                                Desktop.instance);
-                if (pe != null)
+                for (Object[] fm : filesmatched)
                 {
-                  System.err.println("Associated file : " + ((String) fm[0])
-                          + " with " + toassoc.getDisplayId(true));
-                  assocfiles++;
+                  // try and associate
+                  // TODO: may want to set a standard ID naming formalism for
+                  // associating PDB files which have no IDs.
+                  for (SequenceI toassoc : (SequenceI[]) fm[2])
+                  {
+                    PDBEntry pe = new AssociatePdbFileWithSeq()
+                            .associatePdbWithSeq((String) fm[0],
+                                    (DataSourceType) fm[1], toassoc, false,
+                                    Desktop.instance);
+                    if (pe != null)
+                    {
+                      System.err.println("Associated file : "
+                              + ((String) fm[0]) + " with "
+                              + toassoc.getDisplayId(true));
+                      assocfiles++;
+                    }
+                  }
+                  alignPanel.paintAlignment(true);
                 }
               }
-              alignPanel.paintAlignment(true);
             }
-          }
-        }
-        if (filesnotmatched.size() > 0)
-        {
-          if (assocfiles > 0 && (Cache.getDefault(
-                  "AUTOASSOCIATE_PDBANDSEQS_IGNOREOTHERS", false)
-                  || JvOptionPane.showConfirmDialog(this,
-                          "<html>" + MessageManager.formatMessage(
-                                  "label.ignore_unmatched_dropped_files_info",
-                                  new Object[]
-                                  { Integer.valueOf(filesnotmatched.size())
-                                          .toString() })
-                                  + "</html>",
-                          MessageManager.getString(
-                                  "label.ignore_unmatched_dropped_files"),
-                          JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION))
-          {
-            return;
-          }
-          for (String fn : filesnotmatched)
+            if (filesnotmatched.size() > 0)
+            {
+              if (assocfiles > 0 && (Cache.getDefault(
+                      "AUTOASSOCIATE_PDBANDSEQS_IGNOREOTHERS", false)
+                      || JvOptionPane.showConfirmDialog(thisaf,
+                              "<html>" + MessageManager.formatMessage(
+                                      "label.ignore_unmatched_dropped_files_info",
+                                      new Object[]
+                                      { Integer.valueOf(
+                                              filesnotmatched.size())
+                                              .toString() })
+                                      + "</html>",
+                              MessageManager.getString(
+                                      "label.ignore_unmatched_dropped_files"),
+                              JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION))
+              {
+                return;
+              }
+              for (String fn : filesnotmatched)
+              {
+                loadJalviewDataFile(fn, null, null, null);
+              }
+
+            }
+          } catch (Exception ex)
           {
-            loadJalviewDataFile(fn, null, null, null);
+            ex.printStackTrace();
           }
-
         }
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
-      }
+      }).start();
     }
   }
 
index fe216c0..ab57b0f 100644 (file)
@@ -30,10 +30,12 @@ import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.io.HTMLOutput;
 import jalview.jbgui.GAlignmentPanel;
 import jalview.math.AlignmentDimension;
 import jalview.schemes.ResidueProperties;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.Comparison;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.viewmodel.ViewportListenerI;
@@ -340,19 +342,11 @@ public class AlignmentPanel extends GAlignmentPanel implements
    */
   public void highlightSearchResults(SearchResultsI results)
   {
-    scrollToPosition(results);
-    getSeqPanel().seqCanvas.highlightSearchResults(results);
-  }
+    boolean scrolled = scrollToPosition(results, 0, true, false);
 
-  /**
-   * Scroll the view to show the position of the highlighted region in results
-   * (if any) and redraw the overview
-   * 
-   * @param results
-   */
-  public boolean scrollToPosition(SearchResultsI results)
-  {
-    return scrollToPosition(results, 0, true, false);
+    boolean noFastPaint = scrolled && av.getWrapAlignment();
+
+    getSeqPanel().seqCanvas.highlightSearchResults(results, noFastPaint);
   }
 
   /**
@@ -370,8 +364,10 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   /**
-   * Scroll the view to show the position of the highlighted region in results
-   * (if any)
+   * Scrolls the view (if necessary) to show the position of the first
+   * highlighted region in results (if any). Answers true if the view was
+   * scrolled, or false if no matched region was found, or it is already
+   * visible.
    * 
    * @param results
    * @param verticalOffset
@@ -381,116 +377,117 @@ public class AlignmentPanel extends GAlignmentPanel implements
    *          - when set, the overview will be recalculated (takes longer)
    * @param centre
    *          if true, try to centre the search results horizontally in the view
-   * @return false if results were not found
+   * @return
    */
-  public boolean scrollToPosition(SearchResultsI results,
+  protected boolean scrollToPosition(SearchResultsI results,
           int verticalOffset, boolean redrawOverview, boolean centre)
   {
     int startv, endv, starts, ends;
-    // TODO: properly locate search results in view when large numbers of hidden
-    // columns exist before highlighted region
-    // do we need to scroll the panel?
-    // TODO: tons of nullpointerexceptions raised here.
-    if (results != null && results.getSize() > 0 && av != null
-            && av.getAlignment() != null)
-    {
-      int seqIndex = av.getAlignment().findIndex(results);
-      if (seqIndex == -1)
-      {
-        return false;
-      }
-      SequenceI seq = av.getAlignment().getSequenceAt(seqIndex);
 
-      int[] r = results.getResults(seq, 0, av.getAlignment().getWidth());
-      if (r == null)
-      {
-        return false;
-      }
-      int start = r[0];
-      int end = r[1];
+    if (results == null || results.isEmpty() || av == null
+            || av.getAlignment() == null)
+    {
+      return false;
+    }
+    int seqIndex = av.getAlignment().findIndex(results);
+    if (seqIndex == -1)
+    {
+      return false;
+    }
+    SequenceI seq = av.getAlignment().getSequenceAt(seqIndex);
 
-      /*
-       * To centre results, scroll to positions half the visible width
-       * left/right of the start/end positions
-       */
-      if (centre)
-      {
-        int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2
-                - 1;
-        start = Math.max(start - offset, 0);
-        end = end + offset - 1;
-      }
-      if (start < 0)
-      {
-        return false;
-      }
-      if (end == seq.getEnd())
-      {
-        return false;
-      }
-      if (av.hasHiddenColumns())
+    int[] r = results.getResults(seq, 0, av.getAlignment().getWidth());
+    if (r == null)
+    {
+      return false;
+    }
+    int start = r[0];
+    int end = r[1];
+
+    /*
+     * To centre results, scroll to positions half the visible width
+     * left/right of the start/end positions
+     */
+    if (centre)
+    {
+      int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2 - 1;
+      start = Math.max(start - offset, 0);
+      end = end + offset - 1;
+    }
+    if (start < 0)
+    {
+      return false;
+    }
+    if (end == seq.getEnd())
+    {
+      return false;
+    }
+
+    if (av.hasHiddenColumns())
+    {
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      start = hidden.findColumnPosition(start);
+      end = hidden.findColumnPosition(end);
+      if (start == end)
       {
-        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-        start = hidden.findColumnPosition(start);
-        end = hidden.findColumnPosition(end);
-        if (start == end)
+        if (!hidden.isVisible(r[0]))
         {
-          if (!hidden.isVisible(r[0]))
-          {
-            // don't scroll - position isn't visible
-            return false;
-          }
+          // don't scroll - position isn't visible
+          return false;
         }
       }
+    }
 
-      /*
-       * allow for offset of target sequence (actually scroll to one above it)
-       */
-      seqIndex = Math.max(0, seqIndex - verticalOffset);
+    /*
+     * allow for offset of target sequence (actually scroll to one above it)
+     */
+    seqIndex = Math.max(0, seqIndex - verticalOffset);
+    boolean scrollNeeded = true;
 
-      if (!av.getWrapAlignment())
+    if (!av.getWrapAlignment())
+    {
+      if ((startv = vpRanges.getStartRes()) >= start)
       {
-        if ((startv = vpRanges.getStartRes()) >= start)
-        {
-          /*
-           * Scroll left to make start of search results visible
-           */
-          setScrollValues(start, seqIndex);
-        }
-        else if ((endv = vpRanges.getEndRes()) <= end)
-        {
-          /*
-           * Scroll right to make end of search results visible
-           */
-          setScrollValues(startv + end - endv, seqIndex);
-        }
-        else if ((starts = vpRanges.getStartSeq()) > seqIndex)
-        {
-          /*
-           * Scroll up to make start of search results visible
-           */
-          setScrollValues(vpRanges.getStartRes(), seqIndex);
-        }
-        else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
-        {
-          /*
-           * Scroll down to make end of search results visible
-           */
-          setScrollValues(vpRanges.getStartRes(),
-                  starts + seqIndex - ends + 1);
-        }
         /*
-         * Else results are already visible - no need to scroll
+         * Scroll left to make start of search results visible
          */
+        setScrollValues(start, seqIndex);
       }
-      else
+      else if ((endv = vpRanges.getEndRes()) <= end)
+      {
+        /*
+         * Scroll right to make end of search results visible
+         */
+        setScrollValues(startv + end - endv, seqIndex);
+      }
+      else if ((starts = vpRanges.getStartSeq()) > seqIndex)
+      {
+        /*
+         * Scroll up to make start of search results visible
+         */
+        setScrollValues(vpRanges.getStartRes(), seqIndex);
+      }
+      else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
       {
-        vpRanges.scrollToWrappedVisible(start);
+        /*
+         * Scroll down to make end of search results visible
+         */
+        setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends
+                + 1);
       }
+      /*
+       * Else results are already visible - no need to scroll
+       */
+      scrollNeeded = false;
+    }
+    else
+    {
+      scrollNeeded = vpRanges.scrollToWrappedVisible(start);
     }
 
     paintAlignment(redrawOverview);
-    return true;
+
+    return scrollNeeded;
   }
 
   /**
@@ -1437,35 +1434,33 @@ public class AlignmentPanel extends GAlignmentPanel implements
     {
       try
       {
-        int s, sSize = av.getAlignment().getHeight(), res,
-                alwidth = av.getAlignment().getWidth(), g, gSize, f, fSize,
-                sy;
+        int sSize = av.getAlignment().getHeight();
+        int alwidth = av.getAlignment().getWidth();
         PrintWriter out = new PrintWriter(new FileWriter(imgMapFile));
-        out.println(jalview.io.HTMLOutput.getImageMapHTML());
+        out.println(HTMLOutput.getImageMapHTML());
         out.println("<img src=\"" + imageName
                 + "\" border=\"0\" usemap=\"#Map\" >"
                 + "<map name=\"Map\">");
 
-        for (s = 0; s < sSize; s++)
+        for (int s = 0; s < sSize; s++)
         {
-          sy = s * av.getCharHeight() + scaleHeight;
+          int sy = s * av.getCharHeight() + scaleHeight;
 
           SequenceI seq = av.getAlignment().getSequenceAt(s);
-          SequenceFeature[] features = seq.getSequenceFeatures();
           SequenceGroup[] groups = av.getAlignment().findAllGroups(seq);
-          for (res = 0; res < alwidth; res++)
+          for (int column = 0; column < alwidth; column++)
           {
-            StringBuilder text = new StringBuilder();
+            StringBuilder text = new StringBuilder(512);
             String triplet = null;
             if (av.getAlignment().isNucleotide())
             {
-              triplet = ResidueProperties.nucleotideName
-                      .get(seq.getCharAt(res) + "");
+              triplet = ResidueProperties.nucleotideName.get(seq
+                      .getCharAt(column) + "");
             }
             else
             {
-              triplet = ResidueProperties.aa2Triplet
-                      .get(seq.getCharAt(res) + "");
+              triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(column)
+                      + "");
             }
 
             if (triplet == null)
@@ -1473,84 +1468,73 @@ public class AlignmentPanel extends GAlignmentPanel implements
               continue;
             }
 
-            int alIndex = seq.findPosition(res);
-            gSize = groups.length;
-            for (g = 0; g < gSize; g++)
+            int seqPos = seq.findPosition(column);
+            int gSize = groups.length;
+            for (int g = 0; g < gSize; g++)
             {
               if (text.length() < 1)
               {
                 text.append("<area shape=\"rect\" coords=\"")
-                        .append((idWidth + res * av.getCharWidth()))
+                        .append((idWidth + column * av.getCharWidth()))
                         .append(",").append(sy).append(",")
-                        .append((idWidth + (res + 1) * av.getCharWidth()))
+                        .append((idWidth + (column + 1) * av.getCharWidth()))
                         .append(",").append((av.getCharHeight() + sy))
                         .append("\"").append(" onMouseOver=\"toolTip('")
-                        .append(alIndex).append(" ").append(triplet);
+                        .append(seqPos).append(" ").append(triplet);
               }
 
-              if (groups[g].getStartRes() < res
-                      && groups[g].getEndRes() > res)
+              if (groups[g].getStartRes() < column
+                      && groups[g].getEndRes() > column)
               {
                 text.append("<br><em>").append(groups[g].getName())
                         .append("</em>");
               }
             }
 
-            if (features != null)
+            if (text.length() < 1)
             {
-              if (text.length() < 1)
-              {
-                text.append("<area shape=\"rect\" coords=\"")
-                        .append((idWidth + res * av.getCharWidth()))
-                        .append(",").append(sy).append(",")
-                        .append((idWidth + (res + 1) * av.getCharWidth()))
-                        .append(",").append((av.getCharHeight() + sy))
-                        .append("\"").append(" onMouseOver=\"toolTip('")
-                        .append(alIndex).append(" ").append(triplet);
-              }
-              fSize = features.length;
-              for (f = 0; f < fSize; f++)
+              text.append("<area shape=\"rect\" coords=\"")
+                      .append((idWidth + column * av.getCharWidth()))
+                      .append(",").append(sy).append(",")
+                      .append((idWidth + (column + 1) * av.getCharWidth()))
+                      .append(",").append((av.getCharHeight() + sy))
+                      .append("\"").append(" onMouseOver=\"toolTip('")
+                      .append(seqPos).append(" ").append(triplet);
+            }
+            if (!Comparison.isGap(seq.getCharAt(column)))
+            {
+              List<SequenceFeature> features = seq.findFeatures(column, column);
+              for (SequenceFeature sf : features)
               {
-
-                if ((features[f].getBegin() <= seq.findPosition(res))
-                        && (features[f].getEnd() >= seq.findPosition(res)))
+                if (sf.isContactFeature())
                 {
-                  if (features[f].isContactFeature())
-                  {
-                    if (features[f].getBegin() == seq.findPosition(res)
-                            || features[f].getEnd() == seq
-                                    .findPosition(res))
-                    {
-                      text.append("<br>").append(features[f].getType())
-                              .append(" ").append(features[f].getBegin())
-                              .append(":").append(features[f].getEnd());
-                    }
-                  }
-                  else
+                  text.append("<br>").append(sf.getType()).append(" ")
+                          .append(sf.getBegin()).append(":")
+                          .append(sf.getEnd());
+                }
+                else
+                {
+                  text.append("<br>");
+                  text.append(sf.getType());
+                  String description = sf.getDescription();
+                  if (description != null
+                          && !sf.getType().equals(description))
                   {
-                    text.append("<br>");
-                    text.append(features[f].getType());
-                    if (features[f].getDescription() != null && !features[f]
-                            .getType().equals(features[f].getDescription()))
-                    {
-                      text.append(" ").append(features[f].getDescription());
-                    }
-
-                    if (features[f].getValue("status") != null)
-                    {
-                      text.append(" (")
-                              .append(features[f].getValue("status"))
-                              .append(")");
-                    }
+                    description = description.replace("\"", "&quot;");
+                    text.append(" ").append(description);
                   }
                 }
-
+                String status = sf.getStatus();
+                if (status != null && !"".equals(status))
+                {
+                  text.append(" (").append(status).append(")");
+                }
+              }
+              if (text.length() > 1)
+              {
+                text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
+                out.println(text.toString());
               }
-            }
-            if (text.length() > 1)
-            {
-              text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
-              out.println(text.toString());
             }
           }
         }
@@ -1819,7 +1803,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
    * @param verticalOffset
    *          the number of visible sequences to show above the mapped region
    */
-  public void scrollToCentre(SearchResultsI sr, int verticalOffset)
+  protected void scrollToCentre(SearchResultsI sr, int verticalOffset)
   {
     /*
      * To avoid jumpy vertical scrolling (if some sequences are gapped or not
index beb77ca..a619997 100644 (file)
@@ -34,6 +34,7 @@ import java.awt.Color;
 import java.awt.FlowLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.util.List;
 import java.util.Map;
 
 import javax.swing.BorderFactory;
@@ -156,28 +157,22 @@ public class AnnotationExporter extends JPanel
             .getString("label.no_features_on_alignment");
     if (features)
     {
-      Map<String, FeatureColourI> displayedFeatureColours = ap
-              .getFeatureRenderer().getDisplayedFeatureCols();
       FeaturesFile formatter = new FeaturesFile();
       SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
       Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
               .getDisplayedFeatureCols();
+      List<String> featureGroups = ap.getFeatureRenderer()
+              .getDisplayedFeatureGroups();
       boolean includeNonPositional = ap.av.isShowNPFeats();
       if (GFFFormat.isSelected())
       {
-        text = new FeaturesFile().printGffFormat(
-                ap.av.getAlignment().getDataset().getSequencesArray(),
-                displayedFeatureColours, true, ap.av.isShowNPFeats());
-        text = formatter.printGffFormat(sequences, featureColours, true,
-                includeNonPositional);
+        text = formatter.printGffFormat(sequences, featureColours,
+                featureGroups, includeNonPositional);
       }
       else
       {
-        text = new FeaturesFile().printJalviewFormat(
-                ap.av.getAlignment().getDataset().getSequencesArray(),
-                displayedFeatureColours, true, ap.av.isShowNPFeats()); // ap.av.featuresDisplayed);
-        text = formatter.printJalviewFormat(sequences, featureColours, true,
-                includeNonPositional);
+        text = formatter.printJalviewFormat(sequences, featureColours,
+                featureGroups, includeNonPositional);
       }
     }
     else
index 1bbe8d8..ea16f23 100644 (file)
@@ -628,11 +628,10 @@ public class AppVarna extends JInternalFrame
     ShiftList offset = new ShiftList();
     int ofstart = -1;
     int sleng = seq.getLength();
-    char[] seqChars = seq.getSequence();
 
     for (int i = 0; i < sleng; i++)
     {
-      if (Comparison.isGap(seqChars[i]))
+      if (Comparison.isGap(seq.getCharAt(i)))
       {
         if (ofstart == -1)
         {
index 7b8ade6..2a96daf 100644 (file)
@@ -298,8 +298,9 @@ public class CutAndPasteTransfer extends GCutAndPasteTransfer
                   AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
           af.getViewport().setShowSequenceFeatures(showSeqFeatures);
           af.getViewport().setFeaturesDisplayed(fd);
-          ColourSchemeI cs = ColourSchemeMapper
-                  .getJalviewColourScheme(colourSchemeName, al);
+          af.setMenusForViewport();
+          ColourSchemeI cs = ColourSchemeMapper.getJalviewColourScheme(
+                  colourSchemeName, al);
           if (cs != null)
           {
             af.changeColour(cs);
index 396df04..b27328d 100644 (file)
@@ -31,6 +31,8 @@ import java.awt.Dimension;
 import java.awt.FlowLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 
@@ -288,6 +290,14 @@ public class FeatureColourChooser extends JalviewDialog
         thresholdValue_actionPerformed();
       }
     });
+    thresholdValue.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
     slider.setPaintLabels(false);
     slider.setPaintTicks(true);
     slider.setBackground(Color.white);
index 358798d..17f5a71 100644 (file)
@@ -41,6 +41,7 @@ import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
@@ -234,19 +235,26 @@ public class FeatureRenderer
     if (!create && features.size() > 1)
     {
       /*
-       * more than one feature at selected position - add a drop-down
-       * to choose the feature to amend
+       * more than one feature at selected position - 
+       * add a drop-down to choose the feature to amend
+       * space pad text if necessary to make entries distinct
        */
       gridPanel = new JPanel(new GridLayout(4, 1));
       JPanel choosePanel = new JPanel();
       choosePanel.add(new JLabel(
               MessageManager.getString("label.select_feature") + ":"));
       final JComboBox<String> overlaps = new JComboBox<String>();
+      List<String> added = new ArrayList<>();
       for (SequenceFeature sf : features)
       {
-        String text = sf.getType() + "/" + sf.getBegin() + "-" + sf.getEnd()
-                + " (" + sf.getFeatureGroup() + ")";
+        String text = String.format("%s/%d-%d (%s)", sf.getType(),
+                sf.getBegin(), sf.getEnd(), sf.getFeatureGroup());
+        while (added.contains(text))
+        {
+          text += " ";
+        }
         overlaps.addItem(text);
+        added.add(text);
       }
       choosePanel.add(overlaps);
 
@@ -270,9 +278,8 @@ public class FeatureRenderer
             highlight.addResult(sequences.get(0), sf.getBegin(),
                     sf.getEnd());
 
-            alignPanel.getSeqPanel().seqCanvas
-                    .highlightSearchResults(highlight);
-
+            alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(
+                    highlight, false);
           }
           FeatureColourI col = getFeatureStyle(name.getText());
           if (col == null)
@@ -386,7 +393,10 @@ public class FeatureRenderer
 
     FeaturesFile ffile = new FeaturesFile();
 
-    String enteredType = name.getText().trim();
+    final String enteredType = name.getText().trim();
+    final String enteredGroup = group.getText().trim();
+    final String enteredDescription = description.getText().replaceAll("\n", " ");
+
     if (reply == JvOptionPane.OK_OPTION && enteredType.length() > 0)
     {
       /*
@@ -395,7 +405,7 @@ public class FeatureRenderer
       if (useLastDefaults)
       {
         lastFeatureAdded = enteredType;
-        lastFeatureGroupAdded = group.getText().trim();
+        lastFeatureGroupAdded = enteredGroup;
         // TODO: determine if the null feature group is valid
         if (lastFeatureGroupAdded.length() < 1)
         {
@@ -421,26 +431,37 @@ public class FeatureRenderer
       {
         /*
          * YES_OPTION corresponds to the Amend button
-         * need to refresh Feature Settings if type, group or colour changed
+         * need to refresh Feature Settings if type, group or colour changed;
+         * note we don't force the feature to be visible - the user has been
+         * warned if a hidden feature type or group was entered
          */
-        sf.type = enteredType;
-        sf.featureGroup = group.getText().trim();
-        sf.description = description.getText().replaceAll("\n", " ");
-        boolean refreshSettings = (!featureType.equals(sf.type)
-                || !featureGroup.equals(sf.featureGroup));
+        boolean refreshSettings = (!featureType.equals(enteredType) || !featureGroup
+                .equals(enteredGroup));
         refreshSettings |= (fcol != oldcol);
-
-        setColour(sf.type, fcol);
-
+        setColour(enteredType, fcol);
+        int newBegin = sf.begin;
+        int newEnd = sf.end;
         try
         {
-          sf.begin = ((Integer) start.getValue()).intValue();
-          sf.end = ((Integer) end.getValue()).intValue();
+          newBegin = ((Integer) start.getValue()).intValue();
+          newEnd = ((Integer) end.getValue()).intValue();
         } catch (NumberFormatException ex)
         {
+          // JSpinner doesn't accept invalid format data :-)
         }
 
-        ffile.parseDescriptionHTML(sf, false);
+        /*
+         * replace the feature by deleting it and adding a new one
+         * (to ensure integrity of SequenceFeatures data store)
+         */
+        sequences.get(0).deleteFeature(sf);
+        SequenceFeature newSf = new SequenceFeature(sf, enteredType,
+                newBegin, newEnd, enteredGroup, sf.getScore());
+        newSf.setDescription(enteredDescription);
+        ffile.parseDescriptionHTML(newSf, false);
+        // amend features dialog only updates one sequence at a time
+        sequences.get(0).addSequenceFeature(newSf);
+
         if (refreshSettings)
         {
           featuresAdded();
@@ -455,12 +476,11 @@ public class FeatureRenderer
         for (int i = 0; i < sequences.size(); i++)
         {
           SequenceFeature sf = features.get(i);
-          sf.type = enteredType;
-          // fix for JAL-1538 - always set feature group here
-          sf.featureGroup = group.getText().trim();
-          sf.description = description.getText().replaceAll("\n", " ");
-          sequences.get(i).addSequenceFeature(sf);
-          ffile.parseDescriptionHTML(sf, false);
+          SequenceFeature sf2 = new SequenceFeature(enteredType,
+                  enteredDescription, sf.getBegin(), sf.getEnd(),
+                  enteredGroup);
+          ffile.parseDescriptionHTML(sf2, false);
+          sequences.get(i).addSequenceFeature(sf2);
         }
 
         setColour(enteredType, fcol);
index 109d0b6..bd74db5 100644 (file)
@@ -23,7 +23,7 @@ package jalview.gui;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
 import jalview.bin.Cache;
-import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
@@ -478,50 +478,26 @@ public class FeatureSettings extends JPanel
   private boolean handlingUpdate = false;
 
   /**
-   * contains a float[3] for each feature type string. created by setTableData
+   * holds {featureCount, totalExtent} for each feature type
    */
   Map<String, float[]> typeWidth = null;
 
   @Override
   synchronized public void discoverAllFeatureData()
   {
-    Vector<String> allFeatures = new Vector<String>();
-    Vector<String> allGroups = new Vector<String>();
-    SequenceFeature[] tmpfeatures;
-    String group;
-    for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
-    {
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+    Set<String> allGroups = new HashSet<String>();
+    AlignmentI alignment = af.getViewport().getAlignment();
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+    for (int i = 0; i < alignment.getHeight(); i++)
+    {
+      SequenceI seq = alignment.getSequenceAt(i);
+      for (String group : seq.getFeatures().getFeatureGroups(true))
       {
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
+        if (group != null && !allGroups.contains(group))
         {
-          index++;
-          continue;
+          allGroups.add(group);
+          checkGroupState(group);
         }
-
-        if (tmpfeatures[index].getFeatureGroup() != null)
-        {
-          group = tmpfeatures[index].featureGroup;
-          if (!allGroups.contains(group))
-          {
-            allGroups.addElement(group);
-            checkGroupState(group);
-          }
-        }
-
-        if (!allFeatures.contains(tmpfeatures[index].getType()))
-        {
-          allFeatures.addElement(tmpfeatures[index].getType());
-        }
-        index++;
       }
     }
 
@@ -575,7 +551,7 @@ public class FeatureSettings extends JPanel
 
   synchronized void resetTable(String[] groupChanged)
   {
-    if (resettingTable == true)
+    if (resettingTable)
     {
       return;
     }
@@ -583,71 +559,59 @@ public class FeatureSettings extends JPanel
     typeWidth = new Hashtable<String, float[]>();
     // TODO: change avWidth calculation to 'per-sequence' average and use long
     // rather than float
-    float[] avWidth = null;
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector<String> visibleChecks = new Vector<String>();
+
+    Set<String> displayableTypes = new HashSet<String>();
     Set<String> foundGroups = new HashSet<String>();
 
-    // Find out which features should be visible depending on which groups
-    // are selected / deselected
-    // and recompute average width ordering
+    /*
+     * determine which feature types may be visible depending on 
+     * which groups are selected, and recompute average width data
+     */
     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
     {
 
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+      SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+      /*
+       * get the sequence's groups for positional features
+       * and keep track of which groups are visible
+       */
+      Set<String> groups = seq.getFeatures().getFeatureGroups(true);
+      Set<String> visibleGroups = new HashSet<String>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-        foundGroups.add(group);
-
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
-        {
-          index++;
-          continue;
-        }
-
         if (group == null || checkGroupState(group))
         {
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
-        }
-        if (!typeWidth.containsKey(tmpfeatures[index].getType()))
-        {
-          typeWidth.put(tmpfeatures[index].getType(),
-                  avWidth = new float[3]);
+          visibleGroups.add(group);
         }
-        else
-        {
-          avWidth = typeWidth.get(tmpfeatures[index].getType());
-        }
-        avWidth[0]++;
-        if (tmpfeatures[index].getBegin() > tmpfeatures[index].getEnd())
-        {
-          avWidth[1] += 1 + tmpfeatures[index].getBegin()
-                  - tmpfeatures[index].getEnd();
-        }
-        else
+      }
+      foundGroups.addAll(groups);
+
+      /*
+       * get distinct feature types for visible groups
+       * record distinct visible types, and their count and total length
+       */
+      Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
+              visibleGroups.toArray(new String[visibleGroups.size()]));
+      for (String type : types)
+      {
+        displayableTypes.add(type);
+        float[] avWidth = typeWidth.get(type);
+        if (avWidth == null)
         {
-          avWidth[1] += 1 + tmpfeatures[index].getEnd()
-                  - tmpfeatures[index].getBegin();
+          avWidth = new float[2];
+          typeWidth.put(type, avWidth);
         }
-        index++;
+        // todo this could include features with a non-visible group
+        // - do we greatly care?
+        // todo should we include non-displayable features here, and only
+        // update when features are added?
+        avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
+        avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
       }
     }
 
-    int fSize = visibleChecks.size();
-    Object[][] data = new Object[fSize][3];
+    Object[][] data = new Object[displayableTypes.size()][3];
     int dataIndex = 0;
 
     if (fr.hasRenderOrder())
@@ -663,9 +627,9 @@ public class FeatureSettings extends JPanel
       List<String> frl = fr.getRenderOrder();
       for (int ro = frl.size() - 1; ro > -1; ro--)
       {
-        type = frl.get(ro);
+        String type = frl.get(ro);
 
-        if (!visibleChecks.contains(type))
+        if (!displayableTypes.contains(type))
         {
           continue;
         }
@@ -675,16 +639,17 @@ public class FeatureSettings extends JPanel
         data[dataIndex][2] = new Boolean(
                 af.getViewport().getFeaturesDisplayed().isVisible(type));
         dataIndex++;
-        visibleChecks.removeElement(type);
+        displayableTypes.remove(type);
       }
     }
 
-    fSize = visibleChecks.size();
-    for (int i = 0; i < fSize; i++)
+    /*
+     * process any extra features belonging only to 
+     * a group which was just selected
+     */
+    while (!displayableTypes.isEmpty())
     {
-      // These must be extra features belonging to the group
-      // which was just selected
-      type = visibleChecks.elementAt(i).toString();
+      String type = displayableTypes.iterator().next();
       data[dataIndex][0] = type;
 
       data[dataIndex][1] = fr.getFeatureStyle(type);
@@ -697,6 +662,7 @@ public class FeatureSettings extends JPanel
 
       data[dataIndex][2] = new Boolean(true);
       dataIndex++;
+      displayableTypes.remove(type);
     }
 
     if (originalData == null)
index f526592..84540f4 100755 (executable)
@@ -223,8 +223,9 @@ public class Finder extends GFinder
     for (SearchResultMatchI match : searchResults.getResults())
     {
       seqs.add(match.getSequence().getDatasetSequence());
-      features.add(new SequenceFeature(searchString, desc, null,
-              match.getStart(), match.getEnd(), desc));
+      features.add(new SequenceFeature(searchString, desc,
+              match
+              .getStart(), match.getEnd(), desc));
     }
 
     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
index 4ccfb2f..3cc0ed3 100755 (executable)
@@ -325,23 +325,19 @@ public class IdPanel extends JPanel
   {
     int seq2 = alignPanel.getSeqPanel().findSeq(e);
     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
-    // build a new links menu based on the current links + any non-positional
-    // features
+
+    /*
+     *  build a new links menu based on the current links
+     *  and any non-positional features
+     */
     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-    SequenceFeature sfs[] = sq == null ? null : sq.getSequenceFeatures();
-    if (sfs != null)
+    for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
     {
-      for (SequenceFeature sf : sfs)
+      if (sf.links != null)
       {
-        if (sf.begin == sf.end && sf.begin == 0)
+        for (String link : sf.links)
         {
-          if (sf.links != null && sf.links.size() > 0)
-          {
-            for (int l = 0, lSize = sf.links.size(); l < lSize; l++)
-            {
-              nlinks.add(sf.links.elementAt(l));
-            }
-          }
+          nlinks.add(link);
         }
       }
     }
index 4c2b8b6..1658f0f 100644 (file)
@@ -32,6 +32,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.GraphLine;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.RnaViewerModel;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.StructureViewerModel;
@@ -882,48 +883,43 @@ public class Jalview2XML
 
       // TODO: omit sequence features from each alignment view's XML dump if we
       // are storing dataset
-      if (jds.getSequenceFeatures() != null)
+      List<jalview.datamodel.SequenceFeature> sfs = jds
+              .getSequenceFeatures();
+      for (SequenceFeature sf : sfs)
       {
-        jalview.datamodel.SequenceFeature[] sf = jds.getSequenceFeatures();
-        int index = 0;
-        while (index < sf.length)
-        {
-          Features features = new Features();
+        Features features = new Features();
 
-          features.setBegin(sf[index].getBegin());
-          features.setEnd(sf[index].getEnd());
-          features.setDescription(sf[index].getDescription());
-          features.setType(sf[index].getType());
-          features.setFeatureGroup(sf[index].getFeatureGroup());
-          features.setScore(sf[index].getScore());
-          if (sf[index].links != null)
+        features.setBegin(sf.getBegin());
+        features.setEnd(sf.getEnd());
+        features.setDescription(sf.getDescription());
+        features.setType(sf.getType());
+        features.setFeatureGroup(sf.getFeatureGroup());
+        features.setScore(sf.getScore());
+        if (sf.links != null)
+        {
+          for (int l = 0; l < sf.links.size(); l++)
           {
-            for (int l = 0; l < sf[index].links.size(); l++)
-            {
-              OtherData keyValue = new OtherData();
-              keyValue.setKey("LINK_" + l);
-              keyValue.setValue(sf[index].links.elementAt(l).toString());
-              features.addOtherData(keyValue);
-            }
+            OtherData keyValue = new OtherData();
+            keyValue.setKey("LINK_" + l);
+            keyValue.setValue(sf.links.elementAt(l).toString());
+            features.addOtherData(keyValue);
           }
-          if (sf[index].otherDetails != null)
+        }
+        if (sf.otherDetails != null)
+        {
+          String key;
+          Iterator<String> keys = sf.otherDetails.keySet().iterator();
+          while (keys.hasNext())
           {
-            String key;
-            Iterator<String> keys = sf[index].otherDetails.keySet()
-                    .iterator();
-            while (keys.hasNext())
-            {
-              key = keys.next();
-              OtherData keyValue = new OtherData();
-              keyValue.setKey(key);
-              keyValue.setValue(sf[index].otherDetails.get(key).toString());
-              features.addOtherData(keyValue);
-            }
+            key = keys.next();
+            OtherData keyValue = new OtherData();
+            keyValue.setKey(key);
+            keyValue.setValue(sf.otherDetails.get(key).toString());
+            features.addOtherData(keyValue);
           }
-
-          jseq.addFeatures(features);
-          index++;
         }
+
+        jseq.addFeatures(features);
       }
 
       if (jdatasq.getAllPDBEntries() != null)
@@ -2988,12 +2984,11 @@ public class Jalview2XML
           Features[] features = jseqs[i].getFeatures();
           for (int f = 0; f < features.length; f++)
           {
-            jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature(
-                    features[f].getType(), features[f].getDescription(),
-                    features[f].getStatus(), features[f].getBegin(),
-                    features[f].getEnd(), features[f].getFeatureGroup());
-
-            sf.setScore(features[f].getScore());
+            SequenceFeature sf = new SequenceFeature(features[f].getType(),
+                    features[f].getDescription(), features[f].getBegin(),
+                    features[f].getEnd(), features[f].getScore(),
+                    features[f].getFeatureGroup());
+            sf.setStatus(features[f].getStatus());
             for (int od = 0; od < features[f].getOtherDataCount(); od++)
             {
               OtherData keyValue = features[f].getOtherData(od);
index e94ee0e..331e738 100755 (executable)
@@ -36,6 +36,7 @@ import jalview.binding.Tree;
 import jalview.binding.UserColours;
 import jalview.binding.Viewport;
 import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceFeature;
 import jalview.io.FileFormat;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
@@ -226,11 +227,10 @@ public class Jalview2XML_V1
         Features[] features = JSEQ[i].getFeatures();
         for (int f = 0; f < features.length; f++)
         {
-          jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature(
-                  features[f].getType(), features[f].getDescription(),
-                  features[f].getStatus(), features[f].getBegin(),
+          SequenceFeature sf = new SequenceFeature(features[f].getType(),
+                  features[f].getDescription(), features[f].getBegin(),
                   features[f].getEnd(), null);
-
+          sf.setStatus(features[f].getStatus());
           al.getSequenceAt(i).getDatasetSequence().addSequenceFeature(sf);
         }
       }
index 04e34cd..7371eb5 100644 (file)
@@ -26,24 +26,15 @@ import jalview.renderer.OverviewRenderer;
 import jalview.renderer.OverviewResColourFinder;
 import jalview.viewmodel.OverviewDimensions;
 
-import java.awt.AlphaComposite;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import java.awt.image.BufferedImage;
 
 import javax.swing.JComponent;
-import javax.swing.Timer;
 
 public class OverviewCanvas extends JComponent
 {
-  private static final long RUNNING_TIME = 1000;
-
-  private static final int SPEED = 40;
-
   private static final Color TRANS_GREY = new Color(100, 100, 100, 25);
 
   // This is set true if the alignment view changes whilst
@@ -58,8 +49,6 @@ public class OverviewCanvas extends JComponent
 
   private BufferedImage lastMiniMe = null;
 
-  private BufferedImage veryLastMiniMe = null;
-
   // Can set different properties in this seqCanvas than
   // main visible SeqCanvas
   private SequenceRenderer sr;
@@ -72,15 +61,8 @@ public class OverviewCanvas extends JComponent
 
   private AlignViewportI av;
 
-
   private OverviewResColourFinder cf;
 
-  private float alpha = 0f;
-
-  private long startTime = -1;
-
-  private final Timer timer;
-
   private ProgressPanel progressPanel;
 
   public OverviewCanvas(OverviewDimensions overviewDims,
@@ -96,43 +78,12 @@ public class OverviewCanvas extends JComponent
 
     boolean useLegacy = Cache.getDefault(Preferences.USE_LEGACY_GAP, false);
     Color gapCol = Cache.getDefaultColour(Preferences.GAP_COLOUR,
-            Preferences.OVERVIEW_DEFAULT_GAP);
+            jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_GAP);
     Color hiddenCol = Cache.getDefaultColour(Preferences.HIDDEN_COLOUR,
-            Preferences.OVERVIEW_DEFAULT_HIDDEN);
+            jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_HIDDEN);
     cf = new OverviewResColourFinder(useLegacy, gapCol, hiddenCol);
 
     setSize(od.getWidth(), od.getHeight());
-
-    timer = new Timer(SPEED, new ActionListener()
-    {
-
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        if (startTime < 0)
-        {
-          startTime = System.currentTimeMillis();
-        }
-        else
-        {
-
-          long time = System.currentTimeMillis();
-          long duration = time - startTime;
-          if (duration >= RUNNING_TIME)
-          {
-            startTime = -1;
-            ((Timer) e.getSource()).stop();
-            alpha = 0f;
-          }
-          else
-          {
-            alpha = 1f - ((float) duration / (float) RUNNING_TIME);
-          }
-          repaint();
-        }
-      }
-    });
-
   }
 
   /**
@@ -184,7 +135,6 @@ public class OverviewCanvas extends JComponent
           FeatureRenderer transferRenderer)
   {
     miniMe = null;
-    veryLastMiniMe = lastMiniMe;
 
     if (showSequenceFeatures)
     {
@@ -227,8 +177,6 @@ public class OverviewCanvas extends JComponent
     {
       updaterunning = false;
       lastMiniMe = miniMe;
-      alpha = 1f;
-      timer.start();
     }
   }
 
@@ -296,25 +244,8 @@ public class OverviewCanvas extends JComponent
       }
       else // not a resize
       {
-        if (alpha != 0) // this is a timer triggered dissolve
-        {
-          Graphics2D g2d = (Graphics2D) g.create();
-          
-          // draw the original image
-          g2d.drawImage(veryLastMiniMe, 0, 0, getWidth(), getHeight(),
-                  this);
-
-          // draw the new image on top with varying degrees of transparency
-          g2d.setComposite(AlphaComposite.SrcOver.derive(1f - alpha));
-          g2d.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
-
-          g2d.dispose();
-        }
-        else
-        {
-          // fall back to normal behaviour
-          g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
-        }
+        // fall back to normal behaviour
+        g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
       }
     }
 
index 2ef71cc..846ba64 100644 (file)
@@ -1946,8 +1946,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       if (start <= end)
       {
         seqs.add(sg.getSequenceAt(i).getDatasetSequence());
-        features.add(
-                new SequenceFeature(null, null, null, start, end, null));
+        features.add(new SequenceFeature(null, null, start, end, null));
       }
     }
 
@@ -1960,7 +1959,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
               .amendFeatures(seqs, features, true, ap))
       {
         ap.alignFrame.setShowSeqFeatures(true);
-        ap.highlightSearchResults(null);
+        ap.av.setSearchResults(null); // clear highlighting
+        ap.repaint(); // draw new/amended features
       }
     }
   }
index 7ceb04e..6635dbe 100755 (executable)
@@ -121,13 +121,6 @@ public class Preferences extends GPreferences
 
   private static final int MAX_FONT_SIZE = 30;
 
-  public static final Color OVERVIEW_DEFAULT_GAP = Color.lightGray;
-
-  public static final Color OVERVIEW_DEFAULT_LEGACY_GAP = Color.white;
-
-  public static final Color OVERVIEW_DEFAULT_HIDDEN = Color.darkGray
-          .darker();
-
   /**
    * Holds name and link separated with | character. Sequence ID must be
    * $SEQUENCE_ID$ or $SEQUENCE_ID=/.possible | chars ./=$
@@ -327,10 +320,11 @@ public class Preferences extends GPreferences
      * Set overview panel defaults
      */
     gapColour.setBackground(
-            Cache.getDefaultColour(GAP_COLOUR, OVERVIEW_DEFAULT_GAP));
+            Cache.getDefaultColour(GAP_COLOUR,
+                    jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_GAP));
     hiddenColour.setBackground(
             Cache.getDefaultColour(HIDDEN_COLOUR,
-                    OVERVIEW_DEFAULT_HIDDEN));
+                    jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_HIDDEN));
     useLegacyGap.setSelected(Cache.getDefault(USE_LEGACY_GAP, false));
     gapLabel.setEnabled(!useLegacyGap.isSelected());
     gapColour.setEnabled(!useLegacyGap.isSelected());
@@ -1108,11 +1102,13 @@ public class Preferences extends GPreferences
     boolean enabled = useLegacyGap.isSelected();
     if (enabled)
     {
-      gapColour.setBackground(OVERVIEW_DEFAULT_LEGACY_GAP);
+      gapColour.setBackground(
+              jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_LEGACY_GAP);
     }
     else
     {
-      gapColour.setBackground(OVERVIEW_DEFAULT_GAP);
+      gapColour.setBackground(
+              jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_GAP);
     }
     gapColour.setEnabled(!enabled);
     gapLabel.setEnabled(!enabled);
@@ -1124,7 +1120,8 @@ public class Preferences extends GPreferences
     useLegacyGap.setSelected(false);
     useLegacyGaps_actionPerformed(null);
     showHiddenAtStart.setSelected(true);
-    hiddenColour.setBackground(OVERVIEW_DEFAULT_HIDDEN);
+    hiddenColour.setBackground(
+            jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_HIDDEN);
   }
 
   @Override
index 3d8b8aa..191a7d8 100755 (executable)
@@ -53,6 +53,8 @@ import javax.swing.JComponent;
  */
 public class SeqCanvas extends JComponent implements ViewportListenerI
 {
+  private static String ZEROS = "0000000000";
+
   final FeatureRenderer fr;
 
   final SequenceRenderer seqRdr;
@@ -65,9 +67,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
   boolean fastPaint = false;
 
-  int LABEL_WEST;
+  int labelWidthWest;
 
-  int LABEL_EAST;
+  int labelWidthEast;
 
   int cursorX = 0;
 
@@ -209,7 +211,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
       if (value != -1)
       {
-        int x = LABEL_WEST - fm.stringWidth(String.valueOf(value))
+        int x = labelWidthWest - fm.stringWidth(String.valueOf(value))
                 - charWidth / 2;
         g.drawString(value + "", x,
                 (ypos + (i * charHeight)) - (charHeight / 5));
@@ -290,10 +292,10 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     updateViewport();
 
     ViewportRanges ranges = av.getRanges();
-    int sr = ranges.getStartRes();
-    int er = ranges.getEndRes();
-    int ss = ranges.getStartSeq();
-    int es = ranges.getEndSeq();
+    int startRes = ranges.getStartRes();
+    int endRes = ranges.getEndRes();
+    int startSeq = ranges.getStartSeq();
+    int endSeq = ranges.getEndSeq();
     int transX = 0;
     int transY = 0;
 
@@ -303,20 +305,20 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
     {
-      transX = (er - sr - horizontal) * charWidth;
-      sr = er - horizontal;
+      transX = (endRes - startRes - horizontal) * charWidth;
+      startRes = endRes - horizontal;
     }
     else if (horizontal < 0)
     {
-      er = sr - horizontal;
+      endRes = startRes - horizontal;
     }
     else if (vertical > 0) // scroll down
     {
-      ss = es - vertical;
+      startSeq = endSeq - vertical;
 
-      if (ss < ranges.getStartSeq())
+      if (startSeq < ranges.getStartSeq())
       { // ie scrolling too fast, more than a page at a time
-        ss = ranges.getStartSeq();
+        startSeq = ranges.getStartSeq();
       }
       else
       {
@@ -325,32 +327,22 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     }
     else if (vertical < 0)
     {
-      es = ss - vertical;
+      endSeq = startSeq - vertical;
 
-      if (es > ranges.getEndSeq())
+      if (endSeq > ranges.getEndSeq())
       {
-        es = ranges.getEndSeq();
+        endSeq = ranges.getEndSeq();
       }
     }
 
     gg.translate(transX, transY);
-    drawPanel(gg, sr, er, ss, es, 0);
+    drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
     gg.translate(-transX, -transY);
 
     repaint();
     fastpainting = false;
   }
 
-  /**
-   * Definitions of startx and endx (hopefully): SMJS This is what I'm working
-   * towards! startx is the first residue (starting at 0) to display. endx is
-   * the last residue to display (starting at 0). starty is the first sequence
-   * to display (starting at 0). endy is the last sequence to display (starting
-   * at 0). NOTE 1: The av limits are set in setFont in this class and in the
-   * adjustment listener in SeqPanel when the scrollbars move.
-   */
-
-  // Set this to false to force a full panel paint
   @Override
   public void paintComponent(Graphics g)
   {
@@ -437,12 +429,16 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   public void drawPanelForPrinting(Graphics g1, int startRes, int endRes,
           int startSeq, int endSeq)
   {
+    drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
+
     BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
             startSeq, endSeq);
-    drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
-    ((Graphics2D) g1).setComposite(
-            AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
-    g1.drawImage(selectImage, 0, 0, this);
+    if (selectImage != null)
+    {
+      ((Graphics2D) g1).setComposite(AlphaComposite
+              .getInstance(AlphaComposite.SRC_OVER));
+      g1.drawImage(selectImage, 0, 0, this);
+    }
   }
 
   /**
@@ -552,57 +548,63 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   }
 
   /**
-   * DOCUMENT ME!
+   * Returns the visible width of the canvas in residues, after allowing for
+   * East or West scales (if shown)
    * 
-   * @param cwidth
-   *          DOCUMENT ME!
+   * @param canvasWidth
+   *          the width in pixels (possibly including scales)
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
-  public int getWrappedCanvasWidth(int cwidth)
+  public int getWrappedCanvasWidth(int canvasWidth)
   {
     FontMetrics fm = getFontMetrics(av.getFont());
 
-    LABEL_EAST = 0;
-    LABEL_WEST = 0;
+    labelWidthEast = 0;
+    labelWidthWest = 0;
 
     if (av.getScaleRightWrapped())
     {
-      LABEL_EAST = fm.stringWidth(getMask());
+      labelWidthEast = getLabelWidth(fm);
     }
 
     if (av.getScaleLeftWrapped())
     {
-      LABEL_WEST = fm.stringWidth(getMask());
+      labelWidthWest = labelWidthEast > 0 ? labelWidthEast
+              : getLabelWidth(fm);
     }
 
-    return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
+    return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
   }
 
   /**
-   * Generates a string of zeroes.
+   * Returns a pixel width suitable for showing the largest sequence coordinate
+   * (end position) in the alignment. Returns 2 plus the number of decimal
+   * digits to be shown (3 for 1-10, 4 for 11-99 etc).
    * 
-   * @return String
+   * @param fm
+   * @return
    */
-  String getMask()
+  protected int getLabelWidth(FontMetrics fm)
   {
-    String mask = "00";
+    /*
+     * find the biggest sequence end position we need to show
+     * (note this is not necessarily the sequence length)
+     */
     int maxWidth = 0;
-    int tmp;
-    for (int i = 0; i < av.getAlignment().getHeight(); i++)
+    AlignmentI alignment = av.getAlignment();
+    for (int i = 0; i < alignment.getHeight(); i++)
     {
-      tmp = av.getAlignment().getSequenceAt(i).getEnd();
-      if (tmp > maxWidth)
-      {
-        maxWidth = tmp;
-      }
+      maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
     }
 
+    int length = 2;
     for (int i = maxWidth; i > 0; i /= 10)
     {
-      mask += "0";
+      length++;
     }
-    return mask;
+
+    return fm.stringWidth(ZEROS.substring(0, length));
   }
 
   /**
@@ -623,20 +625,15 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     updateViewport();
     AlignmentI al = av.getAlignment();
 
-    FontMetrics fm = getFontMetrics(av.getFont());
-
-    LABEL_EAST = 0;
-    LABEL_WEST = 0;
-
-    if (av.getScaleRightWrapped())
+    int labelWidth = 0;
+    if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
     {
-      LABEL_EAST = fm.stringWidth(getMask());
+      FontMetrics fm = getFontMetrics(av.getFont());
+      labelWidth = getLabelWidth(fm);
     }
 
-    if (av.getScaleLeftWrapped())
-    {
-      LABEL_WEST = fm.stringWidth(getMask());
-    }
+    labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
+    labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
 
     int hgap = charHeight;
     if (av.getScaleAboveWrapped())
@@ -644,7 +641,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       hgap += charHeight;
     }
 
-    int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
+    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
     int cHeight = av.getAlignment().getHeight() * charHeight;
 
     av.setWrappedWidth(cWidth);
@@ -661,6 +658,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
               .findColumnPosition(maxwidth);
     }
 
+    int annotationHeight = getAnnotationHeight();
+
     while ((ypos <= canvasHeight) && (startRes < maxwidth))
     {
       endx = startRes + cWidth - 1;
@@ -680,12 +679,12 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
       if (av.getScaleRightWrapped())
       {
-        g.translate(canvasWidth - LABEL_EAST, 0);
+        g.translate(canvasWidth - labelWidthEast, 0);
         drawEastScale(g, startRes, endx, ypos);
-        g.translate(-(canvasWidth - LABEL_EAST), 0);
+        g.translate(-(canvasWidth - labelWidthEast), 0);
       }
 
-      g.translate(LABEL_WEST, 0);
+      g.translate(labelWidthWest, 0);
 
       if (av.getScaleAboveWrapped())
       {
@@ -748,9 +747,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
         g.translate(0, -cHeight - ypos - 3);
       }
       g.setClip(clip);
-      g.translate(-LABEL_WEST, 0);
+      g.translate(-labelWidthWest, 0);
 
-      ypos += cHeight + getAnnotationHeight() + hgap;
+      ypos += cHeight + annotationHeight + hgap;
 
       startRes += cWidth;
     }
@@ -770,7 +769,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       hgap += charHeight;
     }
 
-    int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
+    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
+            / charWidth;
     int cHeight = av.getAlignment().getHeight() * charHeight;
 
     int startx = startRes;
@@ -796,13 +796,13 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
         endx = maxwidth;
       }
 
-      g.translate(LABEL_WEST, 0);
+      g.translate(labelWidthWest, 0);
 
       drawUnwrappedSelection(g, group, startx, endx, 0,
               av.getAlignment().getHeight() - 1,
               ypos);
 
-      g.translate(-LABEL_WEST, 0);
+      g.translate(-labelWidthWest, 0);
 
       // update vertical offset
       ypos += cHeight + getAnnotationHeight() + hgap;
@@ -827,33 +827,37 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     return annotations.adjustPanelHeight();
   }
 
-  /*
-   * Draw an alignment panel for printing
+  /**
+   * Draws the visible region of the alignment on the graphics context. If there
+   * are hidden column markers in the visible region, then each sub-region
+   * between the markers is drawn separately, followed by the hidden column
+   * marker.
    * 
    * @param g1
    *          Graphics object to draw with
    * @param startRes
-   *          start residue of print area
+   *          offset of the first column in the visible region (0..)
    * @param endRes
-   *          end residue of print area
+   *          offset of the last column in the visible region (0..)
    * @param startSeq
-   *          start sequence of print area
+   *          offset of the first sequence in the visible region (0..)
    * @param endSeq
-   *          end sequence of print area
-   * @param offset
-   *          vertical offset
+   *          offset of the last sequence in the visible region (0..)
+   * @param yOffset
+   *          vertical offset at which to draw (for wrapped alignments)
    */
-  private void drawPanel(Graphics g1, int startRes, int endRes,
-          int startSeq, int endSeq, int offset)
+  public void drawPanel(Graphics g1, final int startRes, final int endRes,
+          final int startSeq, final int endSeq, final int yOffset)
   {
     updateViewport();
     if (!av.hasHiddenColumns())
     {
-      draw(g1, startRes, endRes, startSeq, endSeq, offset);
+      draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
     }
     else
     {
       int screenY = 0;
+      final int screenYMax = endRes - startRes;
       int blockStart = startRes;
       int blockEnd = endRes;
 
@@ -869,38 +873,47 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
           continue;
         }
 
-        blockEnd = hideStart - 1;
+        /*
+         * draw up to just before the next hidden region, or the end of
+         * the visible region, whichever comes first
+         */
+        blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
+                - screenY);
 
         g1.translate(screenY * charWidth, 0);
 
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
+        draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
 
-        if (av.getShowHiddenMarkers())
+        /*
+         * draw the downline of the hidden column marker (ScalePanel draws the
+         * triangle on top) if we reached it
+         */
+        if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
         {
           g1.setColor(Color.blue);
 
           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
-                  0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
-                  (endSeq - startSeq + 1) * charHeight + offset);
+                  0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
+                  (endSeq - startSeq + 1) * charHeight + yOffset);
         }
 
         g1.translate(-screenY * charWidth, 0);
         screenY += blockEnd - blockStart + 1;
         blockStart = hideEnd + 1;
 
-        if (screenY > (endRes - startRes))
+        if (screenY > screenYMax)
         {
           // already rendered last block
           return;
         }
       }
 
-      if (screenY <= (endRes - startRes))
+      if (screenY <= screenYMax)
       {
         // remaining visible region to render
-        blockEnd = blockStart + (endRes - startRes) - screenY;
+        blockEnd = blockStart + screenYMax - screenY;
         g1.translate(screenY * charWidth, 0);
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
+        draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
 
         g1.translate(-screenY * charWidth, 0);
       }
@@ -908,6 +921,21 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
   }
 
+  /**
+   * Draws a region of the visible alignment
+   * 
+   * @param g1
+   * @param startRes
+   *          offset of the first column in the visible region (0..)
+   * @param endRes
+   *          offset of the last column in the visible region (0..)
+   * @param startSeq
+   *          offset of the first sequence in the visible region (0..)
+   * @param endSeq
+   *          offset of the last sequence in the visible region (0..)
+   * @param yOffset
+   *          vertical offset at which to draw (for wrapped alignments)
+   */
   private void draw(Graphics g, int startRes, int endRes, int startSeq,
           int endSeq, int offset)
   {
@@ -936,11 +964,13 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
                 offset + ((i - startSeq) * charHeight), false);
       }
 
-      // / Highlight search Results once all sequences have been drawn
-      // ////////////////////////////////////////////////////////
+      /*
+       * highlight search Results once sequence has been drawn
+       */
       if (av.hasSearchResults())
       {
-        int[] visibleResults = av.getSearchResults().getResults(nextSeq,
+        SearchResultsI searchResults = av.getSearchResults();
+        int[] visibleResults = searchResults.getResults(nextSeq,
                 startRes, endRes);
         if (visibleResults != null)
         {
@@ -1294,18 +1324,169 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   }
   
   /**
-   * DOCUMENT ME!
+   * Highlights search results in the visible region by rendering as white text
+   * on a black background. Any previous highlighting is removed. Answers true
+   * if any highlight was left on the visible alignment (so status bar should be
+   * set to match), else false.
+   * <p>
+   * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
+   * alignment had to be scrolled to show the highlighted region, then it should
+   * be fully redrawn, otherwise a fast paint can be performed. This argument
+   * could be removed if fast paint of scrolled wrapped alignment is coded in
+   * future (JAL-2609).
    * 
    * @param results
-   *          DOCUMENT ME!
+   * @param noFastPaint
+   * @return
    */
-  public void highlightSearchResults(SearchResultsI results)
+  public boolean highlightSearchResults(SearchResultsI results,
+          boolean noFastPaint)
   {
-    img = null;
+    if (fastpainting)
+    {
+      return false;
+    }
+    boolean wrapped = av.getWrapAlignment();
+
+    try
+    {
+      fastPaint = !noFastPaint;
+      fastpainting = fastPaint;
+
+      updateViewport();
+
+      /*
+       * to avoid redrawing the whole visible region, we instead
+       * redraw just the minimal regions to remove previous highlights
+       * and add new ones
+       */
+      SearchResultsI previous = av.getSearchResults();
+      av.setSearchResults(results);
+      boolean redrawn = false;
+      boolean drawn = false;
+      if (wrapped)
+      {
+        redrawn = drawMappedPositionsWrapped(previous);
+        drawn = drawMappedPositionsWrapped(results);
+        redrawn |= drawn;
+      }
+      else
+      {
+        redrawn = drawMappedPositions(previous);
+        drawn = drawMappedPositions(results);
+        redrawn |= drawn;
+      }
 
-    av.setSearchResults(results);
+      /*
+       * if highlights were either removed or added, repaint
+       */
+      if (redrawn)
+      {
+        repaint();
+      }
 
-    repaint();
+      /*
+       * return true only if highlights were added
+       */
+      return drawn;
+
+    } finally
+    {
+      fastpainting = false;
+    }
+  }
+
+  /**
+   * Redraws the minimal rectangle in the visible region (if any) that includes
+   * mapped positions of the given search results. Whether or not positions are
+   * highlighted depends on the SearchResults set on the Viewport. This allows
+   * this method to be called to either clear or set highlighting. Answers true
+   * if any positions were drawn (in which case a repaint is still required),
+   * else false.
+   * 
+   * @param results
+   * @return
+   */
+  protected boolean drawMappedPositions(SearchResultsI results)
+  {
+    if (results == null)
+    {
+      return false;
+    }
+
+    /*
+     * calculate the minimal rectangle to redraw that 
+     * includes both new and existing search results
+     */
+    int firstSeq = Integer.MAX_VALUE;
+    int lastSeq = -1;
+    int firstCol = Integer.MAX_VALUE;
+    int lastCol = -1;
+    boolean matchFound = false;
+
+    ViewportRanges ranges = av.getRanges();
+    int firstVisibleColumn = ranges.getStartRes();
+    int lastVisibleColumn = ranges.getEndRes();
+    AlignmentI alignment = av.getAlignment();
+    if (av.hasHiddenColumns())
+    {
+      firstVisibleColumn = alignment.getHiddenColumns()
+              .adjustForHiddenColumns(firstVisibleColumn);
+      lastVisibleColumn = alignment.getHiddenColumns()
+              .adjustForHiddenColumns(lastVisibleColumn);
+    }
+
+    for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+            .getEndSeq(); seqNo++)
+    {
+      SequenceI seq = alignment.getSequenceAt(seqNo);
+
+      int[] visibleResults = results.getResults(seq, firstVisibleColumn,
+              lastVisibleColumn);
+      if (visibleResults != null)
+      {
+        for (int i = 0; i < visibleResults.length - 1; i += 2)
+        {
+          int firstMatchedColumn = visibleResults[i];
+          int lastMatchedColumn = visibleResults[i + 1];
+          if (firstMatchedColumn <= lastVisibleColumn
+                  && lastMatchedColumn >= firstVisibleColumn)
+          {
+            /*
+             * found a search results match in the visible region - 
+             * remember the first and last sequence matched, and the first
+             * and last visible columns in the matched positions
+             */
+            matchFound = true;
+            firstSeq = Math.min(firstSeq, seqNo);
+            lastSeq = Math.max(lastSeq, seqNo);
+            firstMatchedColumn = Math.max(firstMatchedColumn,
+                    firstVisibleColumn);
+            lastMatchedColumn = Math.min(lastMatchedColumn,
+                    lastVisibleColumn);
+            firstCol = Math.min(firstCol, firstMatchedColumn);
+            lastCol = Math.max(lastCol, lastMatchedColumn);
+          }
+        }
+      }
+    }
+
+    if (matchFound)
+    {
+      if (av.hasHiddenColumns())
+      {
+        firstCol = alignment.getHiddenColumns()
+                .findColumnPosition(firstCol);
+        lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
+      }
+      int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
+      int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
+      gg.translate(transX, transY);
+      drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
+      gg.translate(-transX, -transY);
+    }
+
+    return matchFound;
   }
 
   @Override
@@ -1362,4 +1543,143 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       }
     }
   }
+
+  /**
+   * Redraws any positions in the search results in the visible region of a
+   * wrapped alignment. Any highlights are drawn depending on the search results
+   * set on the Viewport, not the <code>results</code> argument. This allows
+   * this method to be called either to clear highlights (passing the previous
+   * search results), or to draw new highlights.
+   * 
+   * @param results
+   * @return
+   */
+  protected boolean drawMappedPositionsWrapped(SearchResultsI results)
+  {
+    if (results == null)
+    {
+      return false;
+    }
+  
+    boolean matchFound = false;
+
+    int wrappedWidth = av.getWrappedWidth();
+    int wrappedHeight = getRepeatHeightWrapped();
+
+    ViewportRanges ranges = av.getRanges();
+    int canvasHeight = getHeight();
+    int repeats = canvasHeight / wrappedHeight;
+    if (canvasHeight / wrappedHeight > 0)
+    {
+      repeats++;
+    }
+
+    int firstVisibleColumn = ranges.getStartRes();
+    int lastVisibleColumn = ranges.getStartRes() + repeats
+            * ranges.getViewportWidth() - 1;
+
+    AlignmentI alignment = av.getAlignment();
+    if (av.hasHiddenColumns())
+    {
+      firstVisibleColumn = alignment.getHiddenColumns()
+              .adjustForHiddenColumns(firstVisibleColumn);
+      lastVisibleColumn = alignment.getHiddenColumns()
+              .adjustForHiddenColumns(lastVisibleColumn);
+    }
+
+    int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
+
+    for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+            .getEndSeq(); seqNo++)
+    {
+      SequenceI seq = alignment.getSequenceAt(seqNo);
+
+      int[] visibleResults = results.getResults(seq, firstVisibleColumn,
+              lastVisibleColumn);
+      if (visibleResults != null)
+      {
+        for (int i = 0; i < visibleResults.length - 1; i += 2)
+        {
+          int firstMatchedColumn = visibleResults[i];
+          int lastMatchedColumn = visibleResults[i + 1];
+          if (firstMatchedColumn <= lastVisibleColumn
+                  && lastMatchedColumn >= firstVisibleColumn)
+          {
+            /*
+             * found a search results match in the visible region
+             */
+            firstMatchedColumn = Math.max(firstMatchedColumn,
+                    firstVisibleColumn);
+            lastMatchedColumn = Math.min(lastMatchedColumn,
+                    lastVisibleColumn);
+
+            /*
+             * draw each mapped position separately (as contiguous positions may
+             * wrap across lines)
+             */
+            for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
+            {
+              int displayColumn = mappedPos;
+              if (av.hasHiddenColumns())
+              {
+                displayColumn = alignment.getHiddenColumns()
+                        .findColumnPosition(displayColumn);
+              }
+
+              /*
+               * transX: offset from left edge of canvas to residue position
+               */
+              int transX = labelWidthWest
+                      + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
+                      * av.getCharWidth();
+
+              /*
+               * transY: offset from top edge of canvas to residue position
+               */
+              int transY = gapHeight;
+              transY += (displayColumn - ranges.getStartRes())
+                      / wrappedWidth * wrappedHeight;
+              transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
+
+              /*
+               * yOffset is from graphics origin to start of visible region
+               */
+              int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
+              if (transY < getHeight())
+              {
+                matchFound = true;
+                gg.translate(transX, transY);
+                drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
+                        yOffset);
+                gg.translate(-transX, -transY);
+              }
+            }
+          }
+        }
+      }
+    }
+  
+    return matchFound;
+  }
+
+  /**
+   * Answers the height in pixels of a repeating section of the wrapped
+   * alignment, including space above, scale above if shown, sequences, and
+   * annotation panel if shown
+   * 
+   * @return
+   */
+  protected int getRepeatHeightWrapped()
+  {
+    // gap (and maybe scale) above
+    int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
+
+    // add sequences
+    repeatHeight += av.getRanges().getViewportHeight() * charHeight;
+
+    // add annotations panel height if shown
+    repeatHeight += getAnnotationHeight();
+
+    return repeatHeight;
+  }
 }
index 328dfdc..e99e577 100644 (file)
@@ -62,7 +62,6 @@ import java.awt.event.MouseWheelListener;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.ListIterator;
 
 import javax.swing.JPanel;
 import javax.swing.SwingUtilities;
@@ -85,6 +84,16 @@ public class SeqPanel extends JPanel
   /** DOCUMENT ME!! */
   public AlignmentPanel ap;
 
+  /*
+   * last column position for mouseMoved event
+   */
+  private int lastMouseColumn;
+
+  /*
+   * last sequence offset for mouseMoved event
+   */
+  private int lastMouseSeq;
+
   protected int lastres;
 
   protected int startseq;
@@ -171,6 +180,9 @@ public class SeqPanel extends JPanel
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
+
+    lastMouseColumn = -1;
+    lastMouseSeq = -1;
   }
 
   int startWrapBlock = -1;
@@ -204,7 +216,7 @@ public class SeqPanel extends JPanel
 
       int y = evt.getY();
       y -= hgap;
-      x = Math.max(0, x - seqCanvas.LABEL_WEST);
+      x = Math.max(0, x - seqCanvas.labelWidthWest);
 
       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
       if (cwidth < 1)
@@ -669,6 +681,8 @@ public class SeqPanel extends JPanel
     }
     lastSearchResults = results;
 
+    boolean wasScrolled = false;
+
     if (av.isFollowHighlight())
     {
       // don't allow highlight of protein/cDNA to also scroll a complementary
@@ -676,14 +690,19 @@ public class SeqPanel extends JPanel
       // over residue to change abruptly, causing highlighted residue in panel 2
       // to change, causing a scroll in panel 1 etc)
       ap.setToScrollComplementPanel(false);
-      if (ap.scrollToPosition(results, false))
+      wasScrolled = ap.scrollToPosition(results, false);
+      if (wasScrolled)
       {
         seqCanvas.revalidate();
       }
       ap.setToScrollComplementPanel(true);
     }
-    setStatusMessage(results);
-    seqCanvas.highlightSearchResults(results);
+
+    boolean noFastPaint = wasScrolled && av.getWrapAlignment();
+    if (seqCanvas.highlightSearchResults(results, noFastPaint))
+    {
+      setStatusMessage(results);
+    }
   }
 
   @Override
@@ -719,8 +738,18 @@ public class SeqPanel extends JPanel
     int seq = findSeq(evt);
     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
     {
+      lastMouseSeq = -1;
       return;
     }
+    if (column == lastMouseColumn && seq == lastMouseSeq)
+    {
+      /*
+       * just a pixel move without change of residue
+       */
+      return;
+    }
+    lastMouseColumn = column;
+    lastMouseSeq = seq;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
@@ -771,11 +800,7 @@ public class SeqPanel extends JPanel
     if (av.isShowSequenceFeatures())
     {
       List<SequenceFeature> features = ap.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(), pos);
-      if (isGapped)
-      {
-        removeAdjacentFeatures(features, column + 1, sequence);
-      }
+              .findFeaturesAtColumn(sequence, column + 1);
       seqARep.appendFeatures(tooltipText, pos, features,
               this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
     }
@@ -786,45 +811,13 @@ public class SeqPanel extends JPanel
     }
     else
     {
-      if (lastTooltip == null
-              || !lastTooltip.equals(tooltipText.toString()))
-      {
-        String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
-                tooltipText.toString());
-        // String formatedTooltipText = tooltipText.toString();
-        setToolTipText(formatedTooltipText);
-        lastTooltip = tooltipText.toString();
-      }
-
-    }
-
-  }
-
-  /**
-   * Removes from the list of features any that start after, or end before, the
-   * given column position. This allows us to retain only those features
-   * adjacent to a gapped position that straddle the position. Contact features
-   * that 'straddle' the position are also removed, since they are not 'at' the
-   * position.
-   * 
-   * @param features
-   * @param column
-   *          alignment column (1..)
-   * @param sequence
-   */
-  protected void removeAdjacentFeatures(List<SequenceFeature> features,
-          final int column, SequenceI sequence)
-  {
-    // TODO should this be an AlignViewController method (and reused by applet)?
-    ListIterator<SequenceFeature> it = features.listIterator();
-    while (it.hasNext())
-    {
-      SequenceFeature sf = it.next();
-      if (sf.isContactFeature()
-              || sequence.findIndex(sf.getBegin()) > column
-              || sequence.findIndex(sf.getEnd()) < column)
+      String textString = tooltipText.toString();
+      if (lastTooltip == null || !lastTooltip.equals(textString))
       {
-        it.remove();
+        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
+                textString);
+        setToolTipText(formattedTooltipText);
+        lastTooltip = textString;
       }
     }
   }
@@ -883,19 +876,48 @@ public class SeqPanel extends JPanel
    *          aligned sequence object
    * @param column
    *          alignment column
-   * @param seq
+   * @param seqIndex
    *          index of sequence in alignment
    * @return sequence position of residue at column, or adjacent residue if at a
    *         gap
    */
-  int setStatusMessage(SequenceI sequence, final int column, int seq)
+  int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
+  {
+    char sequenceChar = sequence.getCharAt(column);
+    int pos = sequence.findPosition(column);
+    setStatusMessage(sequence, seqIndex, sequenceChar, pos);
+
+    return pos;
+  }
+
+  /**
+   * Builds the status message for the current cursor location and writes it to
+   * the status bar, for example
+   * 
+   * <pre>
+   * Sequence 3 ID: FER1_SOLLC
+   * Sequence 5 ID: FER1_PEA Residue: THR (4)
+   * Sequence 5 ID: FER1_PEA Residue: B (3)
+   * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
+   * </pre>
+   * 
+   * @param sequence
+   * @param seqIndex
+   *          sequence position in the alignment (1..)
+   * @param sequenceChar
+   *          the character under the cursor
+   * @param residuePos
+   *          the sequence residue position (if not over a gap)
+   */
+  protected void setStatusMessage(SequenceI sequence, int seqIndex,
+          char sequenceChar, int residuePos)
   {
     StringBuilder text = new StringBuilder(32);
 
     /*
      * Sequence number (if known), and sequence name.
      */
-    String seqno = seq == -1 ? "" : " " + (seq + 1);
+    String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
     text.append("Sequence").append(seqno).append(" ID: ")
             .append(sequence.getName());
 
@@ -904,13 +926,12 @@ public class SeqPanel extends JPanel
     /*
      * Try to translate the display character to residue name (null for gap).
      */
-    final String displayChar = String.valueOf(sequence.getCharAt(column));
-    boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
-    int pos = sequence.findPosition(column);
+    boolean isGapped = Comparison.isGap(sequenceChar);
 
     if (!isGapped)
     {
       boolean nucleotide = av.getAlignment().isNucleotide();
+      String displayChar = String.valueOf(sequenceChar);
       if (nucleotide)
       {
         residue = ResidueProperties.nucleotideName.get(displayChar);
@@ -924,11 +945,9 @@ public class SeqPanel extends JPanel
       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
               .append(": ").append(residue == null ? displayChar : residue);
 
-      text.append(" (").append(Integer.toString(pos)).append(")");
+      text.append(" (").append(Integer.toString(residuePos)).append(")");
     }
     ap.alignFrame.statusBar.setText(text.toString());
-
-    return pos;
   }
 
   /**
@@ -956,12 +975,9 @@ public class SeqPanel extends JPanel
 
       if (seq == ds)
       {
-        /*
-         * Convert position in sequence (base 1) to sequence character array
-         * index (base 0)
-         */
-        int start = m.getStart() - m.getSequence().getStart();
-        setStatusMessage(seq, start, sequenceIndex);
+        int start = m.getStart();
+        setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
+                start);
         return;
       }
     }
@@ -1095,7 +1111,7 @@ public class SeqPanel extends JPanel
     }
 
     mouseDragging = true;
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
       scrollThread.setEvent(evt);
     }
@@ -1258,7 +1274,7 @@ public class SeqPanel extends JPanel
         // Find the next gap before the end
         // of the visible region boundary
         boolean blank = false;
-        for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
+        for (; fixedRight > lastres; fixedRight--)
         {
           blank = true;
 
@@ -1521,9 +1537,9 @@ public class SeqPanel extends JPanel
       oldSeq = 0;
     }
 
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
-      scrollThread.running = false;
+      scrollThread.stopScrolling();
       scrollThread = null;
     }
   }
@@ -1542,7 +1558,7 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    if (mouseDragging)
+    if (mouseDragging && scrollThread == null)
     {
       scrollThread = new ScrollThread();
     }
@@ -1568,19 +1584,13 @@ public class SeqPanel extends JPanel
       }
 
       int column = findColumn(evt);
-      boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
 
       /*
        * find features at the position (if not gapped), or straddling
        * the position (if at a gap)
        */
       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(),
-                      sequence.findPosition(column));
-      if (isGapped)
-      {
-        removeAdjacentFeatures(features, column, sequence);
-      }
+              .findFeaturesAtColumn(sequence, column + 1);
 
       if (!features.isEmpty())
       {
@@ -1588,9 +1598,9 @@ public class SeqPanel extends JPanel
          * highlight the first feature at the position on the alignment
          */
         SearchResultsI highlight = new SearchResults();
-        highlight.addResult(sequence, features.get(0).getBegin(),
-                features.get(0).getEnd());
-        seqCanvas.highlightSearchResults(highlight);
+        highlight.addResult(sequence, features.get(0).getBegin(), features
+                .get(0).getEnd());
+        seqCanvas.highlightSearchResults(highlight, false);
 
         /*
          * open the Amend Features dialog; clear highlighting afterwards,
@@ -1599,7 +1609,8 @@ public class SeqPanel extends JPanel
         List<SequenceI> seqs = Collections.singletonList(sequence);
         seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
                 ap);
-        seqCanvas.highlightSearchResults(null);
+        av.setSearchResults(null); // clear highlighting
+        seqCanvas.repaint(); // draw new/amended features
       }
     }
   }
@@ -1768,12 +1779,11 @@ public class SeqPanel extends JPanel
    */
   void showPopupMenu(MouseEvent evt)
   {
-    final int res = findColumn(evt);
+    final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
     List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
-            .findFeaturesAtRes(sequence.getDatasetSequence(),
-                    sequence.findPosition(res));
+            .findFeaturesAtColumn(sequence, column + 1);
     List<String> links = new ArrayList<>();
     for (SequenceFeature sf : allFeatures)
     {
@@ -1949,7 +1959,7 @@ public class SeqPanel extends JPanel
 
     mouseDragging = true;
 
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
       scrollThread.setEvent(evt);
     }
@@ -1959,9 +1969,9 @@ public class SeqPanel extends JPanel
   {
     if (evt == null)
     {
-      if (scrollThread != null)
+      if ((scrollThread != null) && (scrollThread.isRunning()))
       {
-        scrollThread.running = false;
+        scrollThread.stopScrolling();
         scrollThread = null;
       }
       mouseDragging = false;
@@ -1984,7 +1994,7 @@ public class SeqPanel extends JPanel
   {
     MouseEvent evt;
 
-    boolean running = false;
+    private volatile boolean threadRunning = true;
 
     public ScrollThread()
     {
@@ -1998,37 +2008,40 @@ public class SeqPanel extends JPanel
 
     public void stopScrolling()
     {
-      running = false;
+      threadRunning = false;
+    }
+
+    public boolean isRunning()
+    {
+      return threadRunning;
     }
 
     @Override
     public void run()
     {
-      running = true;
-
-      while (running)
+      while (threadRunning)
       {
         if (evt != null)
         {
           if (mouseDragging && (evt.getY() < 0)
                   && (av.getRanges().getStartSeq() > 0))
           {
-            running = av.getRanges().scrollUp(true);
+            av.getRanges().scrollUp(true);
           }
 
           if (mouseDragging && (evt.getY() >= getHeight()) && (av
                   .getAlignment().getHeight() > av.getRanges().getEndSeq()))
           {
-            running = av.getRanges().scrollUp(false);
+            av.getRanges().scrollUp(false);
           }
 
           if (mouseDragging && (evt.getX() < 0))
           {
-            running = av.getRanges().scrollRight(false);
+            av.getRanges().scrollRight(false);
           }
           else if (mouseDragging && (evt.getX() >= getWidth()))
           {
-            running = av.getRanges().scrollRight(true);
+            av.getRanges().scrollRight(true);
           }
         }
 
index 804d1a5..e05230b 100755 (executable)
@@ -1004,15 +1004,11 @@ public class SequenceFetcher extends JPanel implements Runnable
         {
           for (SequenceI sq : alsqs)
           {
-            if ((sfs = sq.getSequenceFeatures()) != null)
+            if (sq.getFeatures().hasFeatures())
             {
-              if (sfs.length > 0)
-              {
-                af.setShowSeqFeatures(true);
-                break;
-              }
+              af.setShowSeqFeatures(true);
+              break;
             }
-
           }
         }
 
index 80f0c73..5e14fce 100755 (executable)
@@ -793,19 +793,17 @@ public class TreePanel extends GTreePanel
             }
             if (newname == null)
             {
-              SequenceFeature sf[] = sq.getSequenceFeatures();
-              for (int i = 0; sf != null && i < sf.length; i++)
+              List<SequenceFeature> features = sq.getFeatures()
+                      .getPositionalFeatures(labelClass);
+              for (SequenceFeature feature : features)
               {
-                if (sf[i].getType().equals(labelClass))
+                if (newname == null)
+                {
+                  newname = feature.getDescription();
+                }
+                else
                 {
-                  if (newname == null)
-                  {
-                    newname = new String(sf[i].getDescription());
-                  }
-                  else
-                  {
-                    newname = newname + "; " + sf[i].getDescription();
-                  }
+                  newname = newname + "; " + feature.getDescription();
                 }
               }
             }
index 6317e83..1b93892 100755 (executable)
@@ -246,10 +246,7 @@ public class BLCFile extends AlignFile
 
       out.append(newline);
 
-      if (s[i].getSequence().length > max)
-      {
-        max = s[i].getSequence().length;
-      }
+      max = Math.max(max, s[i].getLength());
 
       i++;
     }
diff --git a/src/jalview/io/ClansFile.java b/src/jalview/io/ClansFile.java
deleted file mode 100644 (file)
index d0b1c72..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.io;
-
-/**
- * Read or write a CLANS style score matrix file.
- */
-
-public class ClansFile extends FileParse
-{
-
-}
index 6c35ca1..c21b02c 100755 (executable)
@@ -209,10 +209,7 @@ public class ClustalFile extends AlignFile
     {
       String tmp = printId(s[i], jvsuffix);
 
-      if (s[i].getSequence().length > max)
-      {
-        max = s[i].getSequence().length;
-      }
+      max = Math.max(max, s[i].getLength());
 
       if (tmp.length() > maxid)
       {
@@ -244,14 +241,14 @@ public class ClustalFile extends AlignFile
         int start = i * len;
         int end = start + len;
 
-        if ((end < s[j].getSequence().length)
-                && (start < s[j].getSequence().length))
+        int length = s[j].getLength();
+        if ((end < length) && (start < length))
         {
           out.append(s[j].getSequenceAsString(start, end));
         }
         else
         {
-          if (start < s[j].getSequence().length)
+          if (start < length)
           {
             out.append(s[j].getSequenceAsString().substring(start));
           }
index 4be8d0e..d2282b1 100755 (executable)
@@ -44,8 +44,8 @@ import java.awt.Color;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -94,14 +94,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   /**
    * Constructor which does not parse the file immediately
    * 
-   * @param inFile
+   * @param file
    * @param paste
    * @throws IOException
    */
-  public FeaturesFile(String inFile, DataSourceType paste)
+  public FeaturesFile(String file, DataSourceType paste)
           throws IOException
   {
-    super(false, inFile, paste);
+    super(false, file, paste);
   }
 
   /**
@@ -117,14 +117,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * Constructor that optionally parses the file immediately
    * 
    * @param parseImmediately
-   * @param inFile
+   * @param file
    * @param type
    * @throws IOException
    */
-  public FeaturesFile(boolean parseImmediately, String inFile,
+  public FeaturesFile(boolean parseImmediately, String file,
           DataSourceType type) throws IOException
   {
-    super(parseImmediately, inFile, type);
+    super(parseImmediately, file, type);
   }
 
   /**
@@ -281,7 +281,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
      */
     for (SequenceI newseq : newseqs)
     {
-      if (newseq.getSequenceFeatures() != null)
+      if (newseq.getFeatures().hasFeatures())
       {
         align.addSequence(newseq);
       }
@@ -359,20 +359,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       Color colour = ColorUtils.createColourFromName(ft);
       featureColours.put(ft, new FeatureColour(colour));
     }
-    SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos, endPos,
-            featureGroup);
+    SequenceFeature sf = null;
     if (gffColumns.length > 6)
     {
       float score = Float.NaN;
       try
       {
         score = new Float(gffColumns[6]).floatValue();
-        // update colourgradient bounds if allowed to
       } catch (NumberFormatException ex)
       {
-        // leave as NaN
+        sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
       }
-      sf.setScore(score);
+      sf = new SequenceFeature(ft, desc, startPos, endPos, score,
+              featureGroup);
+    }
+    else
+    {
+      sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
     }
 
     parseDescriptionHTML(sf, removeHTML);
@@ -472,217 +475,191 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks(
             sf.getDescription(), removeHTML, newline);
 
-    sf.description = (removeHTML) ? parsed.getNonHtmlContent()
-            : sf.description;
+    if (removeHTML)
+    {
+      sf.setDescription(parsed.getNonHtmlContent());
+    }
+
     for (String link : parsed.getLinks())
     {
       sf.addLink(link);
     }
-
   }
 
   /**
-   * generate a features file for seqs includes non-pos features by default.
-   * 
-   * @param sequences
-   *          source of sequence features
-   * @param visible
-   *          hash of feature types and colours
-   * @return features file contents
-   */
-  public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible)
-  {
-    return printJalviewFormat(sequences, visible, true, true);
-  }
-
-  /**
-   * generate a features file for seqs with colours from visible (if any)
+   * Returns contents of a Jalview format features file, for visible features,
+   * as filtered by type and group. Features with a null group are displayed if
+   * their feature type is visible. Non-positional features may optionally be
+   * included (with no check on type or group).
    * 
    * @param sequences
    *          source of features
    * @param visible
-   *          hash of Colours for each feature type
-   * @param visOnly
-   *          when true only feature types in 'visible' will be output
-   * @param nonpos
-   *          indicates if non-positional features should be output (regardless
-   *          of group or type)
-   * @return features file contents
+   *          map of colour for each visible feature type
+   * @param visibleFeatureGroups
+   * @param includeNonPositional
+   *          if true, include non-positional features (regardless of group or
+   *          type)
+   * @return
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean visOnly,
-          boolean nonpos)
+          Map<String, FeatureColourI> visible,
+          List<String> visibleFeatureGroups, boolean includeNonPositional)
   {
-    StringBuilder out = new StringBuilder(256);
-    boolean featuresGen = false;
-    if (visOnly && !nonpos && (visible == null || visible.size() < 1))
+    if (!includeNonPositional && (visible == null || visible.isEmpty()))
     {
       // no point continuing.
       return "No Features Visible";
     }
 
-    if (visible != null && visOnly)
+    /*
+     * write out feature colours (if we know them)
+     */
+    // TODO: decide if feature links should also be written here ?
+    StringBuilder out = new StringBuilder(256);
+    if (visible != null)
     {
-      // write feature colours only if we're given them and we are generating
-      // viewed features
-      // TODO: decide if feature links should also be written here ?
-      Iterator<String> en = visible.keySet().iterator();
-      while (en.hasNext())
+      for (Entry<String, FeatureColourI> featureColour : visible.entrySet())
       {
-        String featureType = en.next().toString();
-        FeatureColourI colour = visible.get(featureType);
-        out.append(colour.toJalviewFormat(featureType)).append(newline);
+        FeatureColourI colour = featureColour.getValue();
+        out.append(colour.toJalviewFormat(featureColour.getKey())).append(
+                newline);
       }
     }
 
-    // Work out which groups are both present and visible
-    List<String> groups = new ArrayList<String>();
-    int groupIndex = 0;
-    boolean isnonpos = false;
+    String[] types = visible == null ? new String[0] : visible.keySet()
+            .toArray(new String[visible.keySet().size()]);
 
-    SequenceFeature[] features;
-    for (int i = 0; i < sequences.length; i++)
+    /*
+     * sort groups alphabetically, and ensure that features with a
+     * null or empty group are output after those in named groups
+     */
+    List<String> sortedGroups = new ArrayList<String>(visibleFeatureGroups);
+    sortedGroups.remove(null);
+    sortedGroups.remove("");
+    Collections.sort(sortedGroups);
+    sortedGroups.add(null);
+    sortedGroups.add("");
+
+    boolean foundSome = false;
+
+    /*
+     * first output any non-positional features
+     */
+    if (includeNonPositional)
     {
-      features = sequences[i].getSequenceFeatures();
-      if (features != null)
+      for (int i = 0; i < sequences.length; i++)
       {
-        for (int j = 0; j < features.length; j++)
+        String sequenceName = sequences[i].getName();
+        for (SequenceFeature feature : sequences[i].getFeatures()
+                .getNonPositionalFeatures())
         {
-          isnonpos = features[j].begin == 0 && features[j].end == 0;
-          if ((!nonpos && isnonpos) || (!isnonpos && visOnly
-                  && !visible.containsKey(features[j].type)))
-          {
-            continue;
-          }
-
-          if (features[j].featureGroup != null
-                  && !groups.contains(features[j].featureGroup))
-          {
-            groups.add(features[j].featureGroup);
-          }
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, feature));
         }
       }
     }
 
-    String group = null;
-    do
+    for (String group : sortedGroups)
     {
-      if (groups.size() > 0 && groupIndex < groups.size())
+      boolean isNamedGroup = (group != null && !"".equals(group));
+      if (isNamedGroup)
       {
-        group = groups.get(groupIndex);
         out.append(newline);
         out.append("STARTGROUP").append(TAB);
         out.append(group);
         out.append(newline);
       }
-      else
-      {
-        group = null;
-      }
 
+      /*
+       * output positional features within groups
+       */
       for (int i = 0; i < sequences.length; i++)
       {
-        features = sequences[i].getSequenceFeatures();
-        if (features != null)
+        String sequenceName = sequences[i].getName();
+        List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+        if (types.length > 0)
         {
-          for (SequenceFeature sequenceFeature : features)
-          {
-            isnonpos = sequenceFeature.begin == 0
-                    && sequenceFeature.end == 0;
-            if ((!nonpos && isnonpos) || (!isnonpos && visOnly
-                    && !visible.containsKey(sequenceFeature.type)))
-            {
-              // skip if feature is nonpos and we ignore them or if we only
-              // output visible and it isn't non-pos and it's not visible
-              continue;
-            }
-
-            if (group != null && (sequenceFeature.featureGroup == null
-                    || !sequenceFeature.featureGroup.equals(group)))
-            {
-              continue;
-            }
+          features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
+                  true, group, types));
+        }
 
-            if (group == null && sequenceFeature.featureGroup != null)
-            {
-              continue;
-            }
-            // we have features to output
-            featuresGen = true;
-            if (sequenceFeature.description == null
-                    || sequenceFeature.description.equals(""))
-            {
-              out.append(sequenceFeature.type).append(TAB);
-            }
-            else
-            {
-              if (sequenceFeature.links != null && sequenceFeature
-                      .getDescription().indexOf("<html>") == -1)
-              {
-                out.append("<html>");
-              }
-
-              out.append(sequenceFeature.description);
-              if (sequenceFeature.links != null)
-              {
-                for (int l = 0; l < sequenceFeature.links.size(); l++)
-                {
-                  String label = sequenceFeature.links.elementAt(l);
-                  String href = label.substring(label.indexOf("|") + 1);
-                  label = label.substring(0, label.indexOf("|"));
-
-                  if (sequenceFeature.description.indexOf(href) == -1)
-                  {
-                    out.append(
-                            " <a href=\"" + href + "\">" + label + "</a>");
-                  }
-                }
-
-                if (sequenceFeature.getDescription()
-                        .indexOf("</html>") == -1)
-                {
-                  out.append("</html>");
-                }
-              }
-
-              out.append(TAB);
-            }
-            out.append(sequences[i].getName());
-            out.append("\t-1\t");
-            out.append(sequenceFeature.begin);
-            out.append(TAB);
-            out.append(sequenceFeature.end);
-            out.append(TAB);
-            out.append(sequenceFeature.type);
-            if (!Float.isNaN(sequenceFeature.score))
-            {
-              out.append(TAB);
-              out.append(sequenceFeature.score);
-            }
-            out.append(newline);
-          }
+        for (SequenceFeature sequenceFeature : features)
+        {
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, sequenceFeature));
         }
       }
 
-      if (group != null)
+      if (isNamedGroup)
       {
         out.append("ENDGROUP").append(TAB);
         out.append(group);
         out.append(newline);
-        groupIndex++;
       }
-      else
+    }
+
+    return foundSome ? out.toString() : "No Features Visible";
+  }
+
+  /**
+   * @param out
+   * @param sequenceName
+   * @param sequenceFeature
+   */
+  protected String formatJalviewFeature(
+          String sequenceName, SequenceFeature sequenceFeature)
+  {
+    StringBuilder out = new StringBuilder(64);
+    if (sequenceFeature.description == null
+            || sequenceFeature.description.equals(""))
+    {
+      out.append(sequenceFeature.type).append(TAB);
+    }
+    else
+    {
+      if (sequenceFeature.links != null
+              && sequenceFeature.getDescription().indexOf("<html>") == -1)
       {
-        break;
+        out.append("<html>");
       }
 
-    } while (groupIndex < groups.size() + 1);
+      out.append(sequenceFeature.description);
+      if (sequenceFeature.links != null)
+      {
+        for (int l = 0; l < sequenceFeature.links.size(); l++)
+        {
+          String label = sequenceFeature.links.elementAt(l);
+          String href = label.substring(label.indexOf("|") + 1);
+          label = label.substring(0, label.indexOf("|"));
+
+          if (sequenceFeature.description.indexOf(href) == -1)
+          {
+            out.append(" <a href=\"" + href + "\">" + label + "</a>");
+          }
+        }
+
+        if (sequenceFeature.getDescription().indexOf("</html>") == -1)
+        {
+          out.append("</html>");
+        }
+      }
 
-    if (!featuresGen)
+      out.append(TAB);
+    }
+    out.append(sequenceName);
+    out.append("\t-1\t");
+    out.append(sequenceFeature.begin);
+    out.append(TAB);
+    out.append(sequenceFeature.end);
+    out.append(TAB);
+    out.append(sequenceFeature.type);
+    if (!Float.isNaN(sequenceFeature.score))
     {
-      return "No Features Visible";
+      out.append(TAB);
+      out.append(sequenceFeature.score);
     }
+    out.append(newline);
 
     return out.toString();
   }
@@ -740,102 +717,90 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns features output in GFF2 format, including hidden and non-positional
-   * features
-   * 
-   * @param sequences
-   *          the sequences whose features are to be output
-   * @param visible
-   *          a map whose keys are the type names of visible features
-   * @return
-   */
-  public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible)
-  {
-    return printGffFormat(sequences, visible, true, true);
-  }
-
-  /**
    * Returns features output in GFF2 format
    * 
    * @param sequences
    *          the sequences whose features are to be output
    * @param visible
    *          a map whose keys are the type names of visible features
-   * @param outputVisibleOnly
+   * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean outputVisibleOnly,
+          Map<String, FeatureColourI> visible,
+          List<String> visibleFeatureGroups,
           boolean includeNonPositionalFeatures)
   {
     StringBuilder out = new StringBuilder(256);
-    int version = gffVersion == 0 ? 2 : gffVersion;
-    out.append(String.format("%s %d\n", GFF_VERSION, version));
-    String source;
-    boolean isnonpos;
+
+    out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
+
+    if (!includeNonPositionalFeatures
+            && (visible == null || visible.isEmpty()))
+    {
+      return out.toString();
+    }
+
+    String[] types = visible == null ? new String[0] : visible.keySet()
+            .toArray(
+            new String[visible.keySet().size()]);
+
     for (SequenceI seq : sequences)
     {
-      SequenceFeature[] features = seq.getSequenceFeatures();
-      if (features != null)
+      List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+      if (includeNonPositionalFeatures)
       {
-        for (SequenceFeature sf : features)
-        {
-          isnonpos = sf.begin == 0 && sf.end == 0;
-          if (!includeNonPositionalFeatures && isnonpos)
-          {
-            /*
-             * ignore non-positional features if not wanted
-             */
-            continue;
-          }
-          // TODO why the test !isnonpos here?
-          // what about not visible non-positional features?
-          if (!isnonpos && outputVisibleOnly
-                  && !visible.containsKey(sf.type))
-          {
-            /*
-             * ignore not visible features if not wanted
-             */
-            continue;
-          }
+        features.addAll(seq.getFeatures().getNonPositionalFeatures());
+      }
+      if (visible != null && !visible.isEmpty())
+      {
+        features.addAll(seq.getFeatures().getPositionalFeatures(types));
+      }
 
-          source = sf.featureGroup;
-          if (source == null)
-          {
-            source = sf.getDescription();
-          }
+      for (SequenceFeature sf : features)
+      {
+        String source = sf.featureGroup;
+        if (!sf.isNonPositional() && source != null
+                && !visibleFeatureGroups.contains(source))
+        {
+          // group is not visible
+          continue;
+        }
 
-          out.append(seq.getName());
-          out.append(TAB);
-          out.append(source);
-          out.append(TAB);
-          out.append(sf.type);
-          out.append(TAB);
-          out.append(sf.begin);
-          out.append(TAB);
-          out.append(sf.end);
-          out.append(TAB);
-          out.append(sf.score);
-          out.append(TAB);
-
-          int strand = sf.getStrand();
-          out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
-          out.append(TAB);
-
-          String phase = sf.getPhase();
-          out.append(phase == null ? "." : phase);
-
-          // miscellaneous key-values (GFF column 9)
-          String attributes = sf.getAttributes();
-          if (attributes != null)
-          {
-            out.append(TAB).append(attributes);
-          }
+        if (source == null)
+        {
+          source = sf.getDescription();
+        }
 
-          out.append(newline);
+        out.append(seq.getName());
+        out.append(TAB);
+        out.append(source);
+        out.append(TAB);
+        out.append(sf.type);
+        out.append(TAB);
+        out.append(sf.begin);
+        out.append(TAB);
+        out.append(sf.end);
+        out.append(TAB);
+        out.append(sf.score);
+        out.append(TAB);
+
+        int strand = sf.getStrand();
+        out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
+        out.append(TAB);
+
+        String phase = sf.getPhase();
+        out.append(phase == null ? "." : phase);
+
+        // miscellaneous key-values (GFF column 9)
+        String attributes = sf.getAttributes();
+        if (attributes != null)
+        {
+          out.append(TAB).append(attributes);
         }
+
+        out.append(newline);
       }
     }
 
@@ -1096,10 +1061,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       // rename sequences if GFF handler requested this
       // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ?
-      SequenceFeature[] sfs = seq.getSequenceFeatures();
-      if (sfs != null)
+      List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures();
+      if (!sfs.isEmpty())
       {
-        String newName = (String) sfs[0].getValue(GffHelperI.RENAME_TOKEN);
+        String newName = (String) sfs.get(0).getValue(
+                GffHelperI.RENAME_TOKEN);
         if (newName != null)
         {
           seq.setName(newName);
index 7ad8fcd..ff959b0 100755 (executable)
@@ -275,6 +275,11 @@ public class IdentifyFile
           // read as a FASTA (probably)
           break;
         }
+        if (data.indexOf("{\"") > -1)
+        {
+          reply = FileFormat.Json;
+          break;
+        }
         int lessThan = data.indexOf("<");
         if ((lessThan > -1)) // possible Markup Language data i.e HTML,
                              // RNAML, XML
@@ -292,11 +297,6 @@ public class IdentifyFile
           }
         }
 
-        if (data.indexOf("{\"") > -1)
-        {
-          reply = FileFormat.Json;
-          break;
-        }
         if ((data.length() < 1) || (data.indexOf("#") == 0))
         {
           lineswereskipped = true;
index 5d9c804..fda22dc 100644 (file)
@@ -51,6 +51,7 @@ import jalview.schemes.ColourSchemeProperty;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.ResidueColourScheme;
 import jalview.util.ColorUtils;
+import jalview.util.Format;
 import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 
 import java.awt.Color;
@@ -227,8 +228,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
 
       if (exportSettings.isExportFeatures())
       {
-        jsonAlignmentPojo
-                .setSeqFeatures(sequenceFeatureToJsonPojo(sqs, fr));
+        jsonAlignmentPojo.setSeqFeatures(sequenceFeatureToJsonPojo(sqs));
       }
 
       if (exportSettings.isExportGroups() && seqGroups != null
@@ -308,8 +308,8 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
     return hiddenSections;
   }
 
-  public List<SequenceFeaturesPojo> sequenceFeatureToJsonPojo(
-          SequenceI[] sqs, FeatureRenderer fr)
+  protected List<SequenceFeaturesPojo> sequenceFeatureToJsonPojo(
+          SequenceI[] sqs)
   {
     displayedFeatures = (fr == null) ? null : fr.getFeaturesDisplayed();
     List<SequenceFeaturesPojo> sequenceFeaturesPojo = new ArrayList<>();
@@ -320,42 +320,38 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
 
     FeatureColourFinder finder = new FeatureColourFinder(fr);
 
+    String[] visibleFeatureTypes = displayedFeatures == null ? null
+            : displayedFeatures.getVisibleFeatures().toArray(
+                    new String[displayedFeatures.getVisibleFeatureCount()]);
+
     for (SequenceI seq : sqs)
     {
-      SequenceI dataSetSequence = seq.getDatasetSequence();
-      SequenceFeature[] seqFeatures = (dataSetSequence == null) ? null
-              : seq.getDatasetSequence().getSequenceFeatures();
-
-      seqFeatures = (seqFeatures == null) ? seq.getSequenceFeatures()
-              : seqFeatures;
-      if (seqFeatures == null)
-      {
-        continue;
-      }
-
+      /*
+       * get all features currently visible (and any non-positional features)
+       */
+      List<SequenceFeature> seqFeatures = seq.getFeatures().getAllFeatures(
+              visibleFeatureTypes);
       for (SequenceFeature sf : seqFeatures)
       {
-        if (displayedFeatures != null
-                && displayedFeatures.isVisible(sf.getType()))
-        {
-          SequenceFeaturesPojo jsonFeature = new SequenceFeaturesPojo(
-                  String.valueOf(seq.hashCode()));
-
-          String featureColour = (fr == null) ? null
-                  : jalview.util.Format.getHexString(
-                          finder.findFeatureColour(Color.white, seq,
-                                  seq.findIndex(sf.getBegin())));
-          jsonFeature.setXstart(seq.findIndex(sf.getBegin()) - 1);
-          jsonFeature.setXend(seq.findIndex(sf.getEnd()));
-          jsonFeature.setType(sf.getType());
-          jsonFeature.setDescription(sf.getDescription());
-          jsonFeature.setLinks(sf.links);
-          jsonFeature.setOtherDetails(sf.otherDetails);
-          jsonFeature.setScore(sf.getScore());
-          jsonFeature.setFillColor(featureColour);
-          jsonFeature.setFeatureGroup(sf.getFeatureGroup());
-          sequenceFeaturesPojo.add(jsonFeature);
-        }
+        SequenceFeaturesPojo jsonFeature = new SequenceFeaturesPojo(
+                String.valueOf(seq.hashCode()));
+
+        String featureColour = (fr == null) ? null : Format
+                .getHexString(finder.findFeatureColour(Color.white, seq,
+                        seq.findIndex(sf.getBegin())));
+        int xStart = sf.getBegin() == 0 ? 0
+                : seq.findIndex(sf.getBegin()) - 1;
+        int xEnd = sf.getEnd() == 0 ? 0 : seq.findIndex(sf.getEnd());
+        jsonFeature.setXstart(xStart);
+        jsonFeature.setXend(xEnd);
+        jsonFeature.setType(sf.getType());
+        jsonFeature.setDescription(sf.getDescription());
+        jsonFeature.setLinks(sf.links);
+        jsonFeature.setOtherDetails(sf.otherDetails);
+        jsonFeature.setScore(sf.getScore());
+        jsonFeature.setFillColor(featureColour);
+        jsonFeature.setFeatureGroup(sf.getFeatureGroup());
+        sequenceFeaturesPojo.add(jsonFeature);
       }
     }
     return sequenceFeaturesPojo;
@@ -687,12 +683,23 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
         Long end = (Long) jsonFeature.get("xEnd");
         String type = (String) jsonFeature.get("type");
         String featureGrp = (String) jsonFeature.get("featureGroup");
-        String descripiton = (String) jsonFeature.get("description");
+        String description = (String) jsonFeature.get("description");
         String seqRef = (String) jsonFeature.get("sequenceRef");
         Float score = Float.valueOf(jsonFeature.get("score").toString());
 
         Sequence seq = seqMap.get(seqRef);
-        SequenceFeature sequenceFeature = new SequenceFeature();
+
+        /*
+         * begin/end of 0 is for a non-positional feature
+         */
+        int featureBegin = begin.intValue() == 0 ? 0 : seq
+                .findPosition(begin.intValue());
+        int featureEnd = end.intValue() == 0 ? 0 : seq.findPosition(end
+                .intValue()) - 1;
+
+        SequenceFeature sequenceFeature = new SequenceFeature(type,
+                description, featureBegin, featureEnd, score, featureGrp);
+
         JSONArray linksJsonArray = (JSONArray) jsonFeature.get("links");
         if (linksJsonArray != null && linksJsonArray.size() > 0)
         {
@@ -703,12 +710,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
             sequenceFeature.addLink(link);
           }
         }
-        sequenceFeature.setFeatureGroup(featureGrp);
-        sequenceFeature.setScore(score);
-        sequenceFeature.setDescription(descripiton);
-        sequenceFeature.setType(type);
-        sequenceFeature.setBegin(seq.findPosition(begin.intValue()));
-        sequenceFeature.setEnd(seq.findPosition(end.intValue()) - 1);
+
         seq.addSequenceFeature(sequenceFeature);
         displayedFeatures.setVisible(type);
       }
index 55138e7..c9f1fcf 100755 (executable)
@@ -59,7 +59,7 @@ public class JnetAnnotationMaker
     // in the future we could search for the query
     // sequence in the alignment before calling this function.
     SequenceI seqRef = al.getSequenceAt(firstSeq);
-    int width = preds[0].getSequence().length;
+    int width = preds[0].getLength();
     int[] gapmap = al.getSequenceAt(firstSeq).gapMap();
     if ((delMap != null && delMap.length > width)
             || (delMap == null && gapmap.length != width))
index 4dbf950..df2bed2 100755 (executable)
@@ -294,7 +294,7 @@ public class MSFfile extends AlignFile
     }
 
     long maxNB = 0;
-    out.append("   MSF: " + s[0].getSequence().length + "   Type: "
+    out.append("   MSF: " + s[0].getLength() + "   Type: "
             + (is_NA ? "N" : "P") + "    Check:  " + (bigChecksum % 10000)
             + "   ..");
     out.append(newline);
@@ -310,9 +310,9 @@ public class MSFfile extends AlignFile
 
       nameBlock[i] = new String("  Name: " + printId(s[i], jvSuffix) + " ");
 
-      idBlock[i] = new String("Len: "
-              + maxLenpad.form(s[i].getSequence().length) + "  Check: "
-              + maxChkpad.form(checksums[i]) + "  Weight: 1.00" + newline);
+      idBlock[i] = new String("Len: " + maxLenpad.form(s[i].getLength())
+              + "  Check: " + maxChkpad.form(checksums[i])
+              + "  Weight: 1.00" + newline);
 
       if (s[i].getName().length() > maxid)
       {
@@ -369,8 +369,9 @@ public class MSFfile extends AlignFile
           int start = (i * 50) + (k * 10);
           int end = start + 10;
 
-          if ((end < s[j].getSequence().length)
-                  && (start < s[j].getSequence().length))
+          int length = s[j].getLength();
+          if ((end < length)
+                  && (start < length))
           {
             out.append(s[j].getSequence(start, end));
 
@@ -385,7 +386,7 @@ public class MSFfile extends AlignFile
           }
           else
           {
-            if (start < s[j].getSequence().length)
+            if (start < length)
             {
               out.append(s[j].getSequenceAsString().substring(start));
               out.append(newline);
diff --git a/src/jalview/io/MatrixFile.java b/src/jalview/io/MatrixFile.java
deleted file mode 100644 (file)
index 418eea2..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.io;
-
-/**
- * IO for asymmetric matrix with arbitrary dimension with labels, as displayed
- * by PCA viewer. Form is: tab separated entity defs header line TITLE\ttitle
- * DESC\tdesc PROPERTY\t<id or empty for whole matrix>\tname\ttype\tvalue
- * ROW\tRow i label (ID)/tPrinciple text/tprinciple description/t...
- * COLUMN\t(similar, optional).. .. <float>\t<float>...(column-wise data for row
- * i)
- */
-
-public class MatrixFile extends FileParse
-{
-
-}
index 68aeb68..6b9dc3f 100755 (executable)
@@ -157,10 +157,7 @@ public class PfamFile extends AlignFile
     {
       String tmp = printId(s[i], jvsuffix);
 
-      if (s[i].getSequence().length > max)
-      {
-        max = s[i].getSequence().length;
-      }
+      max = Math.max(max, s[i].getLength());
 
       if (tmp.length() > maxid)
       {
index 2a961a2..539826f 100644 (file)
@@ -248,7 +248,7 @@ public class PhylipFile extends AlignFile
     sb.append(" ");
     // if there are no sequences, then define the number of characters as 0
     sb.append(
-            (sqs.length > 0) ? Integer.toString(sqs[0].getSequence().length)
+(sqs.length > 0) ? Integer.toString(sqs[0].getLength())
                     : "0")
             .append(newline);
 
@@ -280,13 +280,13 @@ public class PhylipFile extends AlignFile
       // sequential has the entire sequence following the name
       if (sequential)
       {
-        sb.append(s.getSequence());
+        sb.append(s.getSequenceAsString());
       }
       else
       {
         // Jalview ensures all sequences are of same length so no need
         // to keep track of min/max length
-        sequenceLength = s.getSequence().length;
+        sequenceLength = s.getLength();
         // interleaved breaks the sequence into chunks for
         // interleavedColumns characters
         sb.append(s.getSequence(0,
index 7b5ce0d..f7f718b 100755 (executable)
@@ -92,7 +92,7 @@ public class PileUpfile extends MSFfile
       i++;
     }
 
-    out.append("   MSF: " + s[0].getSequence().length
+    out.append("   MSF: " + s[0].getLength()
             + "   Type: P    Check:  " + bigChecksum % 10000 + "   ..");
     out.append(newline);
     out.append(newline);
@@ -151,8 +151,8 @@ public class PileUpfile extends MSFfile
           int start = (i * 50) + (k * 10);
           int end = start + 10;
 
-          if ((end < s[j].getSequence().length)
-                  && (start < s[j].getSequence().length))
+          int length = s[j].getLength();
+          if ((end < length) && (start < length))
           {
             out.append(s[j].getSequence(start, end));
 
@@ -167,7 +167,7 @@ public class PileUpfile extends MSFfile
           }
           else
           {
-            if (start < s[j].getSequence().length)
+            if (start < length)
             {
               out.append(s[j].getSequenceAsString().substring(start));
               out.append(newline);
index 613c01f..f1ebcac 100644 (file)
@@ -57,7 +57,8 @@ public class SequenceAnnotationReport
   final String linkImageURL;
 
   /*
-   * Comparator to order DBRefEntry by Source + accession id (case-insensitive)
+   * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
+   * with 'Primary' sources placed before others
    */
   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
   {
@@ -353,100 +354,121 @@ public class SequenceAnnotationReport
     {
       ds = ds.getDatasetSequence();
     }
+    
+    if (showDbRefs)
+    {
+      maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
+    }
+
+    /*
+     * add non-positional features if wanted
+     */
+    if (showNpFeats)
+    {
+      for (SequenceFeature sf : sequence.getFeatures()
+              .getNonPositionalFeatures())
+      {
+        int sz = -sb.length();
+        appendFeature(sb, 0, minmax, sf);
+        sz += sb.length();
+        maxWidth = Math.max(maxWidth, sz);
+      }
+    }
+    sb.append("</i>");
+    return maxWidth;
+  }
+
+  /**
+   * A helper method that appends any DBRefs, returning the maximum line length
+   * added
+   * 
+   * @param sb
+   * @param ds
+   * @param summary
+   * @return
+   */
+  protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
+          boolean summary)
+  {
     DBRefEntry[] dbrefs = ds.getDBRefs();
-    if (showDbRefs && dbrefs != null)
+    if (dbrefs == null)
+    {
+      return 0;
+    }
+
+    // note this sorts the refs held on the sequence!
+    Arrays.sort(dbrefs, comparator);
+    boolean ellipsis = false;
+    String source = null;
+    String lastSource = null;
+    int countForSource = 0;
+    int sourceCount = 0;
+    boolean moreSources = false;
+    int maxLineLength = 0;
+    int lineLength = 0;
+
+    for (DBRefEntry ref : dbrefs)
     {
-      // note this sorts the refs held on the sequence!
-      Arrays.sort(dbrefs, comparator);
-      boolean ellipsis = false;
-      String source = null;
-      String lastSource = null;
-      int countForSource = 0;
-      int sourceCount = 0;
-      boolean moreSources = false;
-      int lineLength = 0;
-
-      for (DBRefEntry ref : dbrefs)
+      source = ref.getSource();
+      if (source == null)
       {
-        source = ref.getSource();
-        if (source == null)
-        {
-          // shouldn't happen
-          continue;
-        }
-        boolean sourceChanged = !source.equals(lastSource);
-        if (sourceChanged)
-        {
-          lineLength = 0;
-          countForSource = 0;
-          sourceCount++;
-        }
-        if (sourceCount > MAX_SOURCES && summary)
-        {
-          ellipsis = true;
-          moreSources = true;
-          break;
-        }
-        lastSource = source;
-        countForSource++;
-        if (countForSource == 1 || !summary)
-        {
-          sb.append("<br>");
-        }
-        if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
-        {
-          String accessionId = ref.getAccessionId();
-          lineLength += accessionId.length() + 1;
-          if (countForSource > 1 && summary)
-          {
-            sb.append(", ").append(accessionId);
-            lineLength++;
-          }
-          else
-          {
-            sb.append(source).append(" ").append(accessionId);
-            lineLength += source.length();
-          }
-          maxWidth = Math.max(maxWidth, lineLength);
-        }
-        if (countForSource == MAX_REFS_PER_SOURCE && summary)
-        {
-          sb.append(COMMA).append(ELLIPSIS);
-          ellipsis = true;
-        }
+        // shouldn't happen
+        continue;
       }
-      if (moreSources)
+      boolean sourceChanged = !source.equals(lastSource);
+      if (sourceChanged)
       {
-        sb.append("<br>").append(ELLIPSIS).append(COMMA).append(source)
-                .append(COMMA).append(ELLIPSIS);
+        lineLength = 0;
+        countForSource = 0;
+        sourceCount++;
       }
-      if (ellipsis)
+      if (sourceCount > MAX_SOURCES && summary)
       {
-        sb.append("<br>(");
-        sb.append(MessageManager.getString("label.output_seq_details"));
-        sb.append(")");
+        ellipsis = true;
+        moreSources = true;
+        break;
       }
-    }
-
-    /*
-     * add non-positional features if wanted
-     */
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-    if (showNpFeats && features != null)
-    {
-      for (int i = 0; i < features.length; i++)
+      lastSource = source;
+      countForSource++;
+      if (countForSource == 1 || !summary)
+      {
+        sb.append("<br>");
+      }
+      if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
       {
-        if (features[i].begin == 0 && features[i].end == 0)
+        String accessionId = ref.getAccessionId();
+        lineLength += accessionId.length() + 1;
+        if (countForSource > 1 && summary)
         {
-          int sz = -sb.length();
-          appendFeature(sb, 0, minmax, features[i]);
-          sz += sb.length();
-          maxWidth = Math.max(maxWidth, sz);
+          sb.append(", ").append(accessionId);
+          lineLength++;
         }
+        else
+        {
+          sb.append(source).append(" ").append(accessionId);
+          lineLength += source.length();
+        }
+        maxLineLength = Math.max(maxLineLength, lineLength);
+      }
+      if (countForSource == MAX_REFS_PER_SOURCE && summary)
+      {
+        sb.append(COMMA).append(ELLIPSIS);
+        ellipsis = true;
       }
     }
-    sb.append("</i>");
-    return maxWidth;
+    if (moreSources)
+    {
+      sb.append("<br>").append(source)
+              .append(COMMA).append(ELLIPSIS);
+    }
+    if (ellipsis)
+    {
+      sb.append("<br>(");
+      sb.append(MessageManager.getString("label.output_seq_details"));
+      sb.append(")");
+    }
+
+    return maxLineLength;
   }
 
   public void createTooltipAnnotationReport(final StringBuilder tip,
index e22ee0a..f5b5177 100644 (file)
@@ -74,6 +74,8 @@ import fr.orsay.lri.varna.models.rna.RNA;
  */
 public class StockholmFile extends AlignFile
 {
+  private static final String ANNOTATION = "annotation";
+
   private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
 
   private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
@@ -391,7 +393,7 @@ public class StockholmFile extends AlignFile
               while (j.hasMoreElements())
               {
                 String desc = j.nextElement().toString();
-                if ("annotations".equals(desc) && annotsAdded)
+                if (ANNOTATION.equals(desc) && annotsAdded)
                 {
                   // don't add features if we already added an annotation row
                   continue;
@@ -411,7 +413,7 @@ public class StockholmFile extends AlignFile
                     int new_pos = posmap[k]; // look up nearest seqeunce
                     // position to this column
                     SequenceFeature feat = new SequenceFeature(type, desc,
-                            new_pos, new_pos, 0f, null);
+                            new_pos, new_pos, null);
 
                     seqO.addSequenceFeature(feat);
                   }
@@ -634,7 +636,7 @@ public class StockholmFile extends AlignFile
               content = new Hashtable();
               features.put(this.id2type(type), content);
             }
-            String ns = (String) content.get("annotation");
+            String ns = (String) content.get(ANNOTATION);
 
             if (ns == null)
             {
@@ -642,7 +644,7 @@ public class StockholmFile extends AlignFile
             }
             // finally, append the annotation line
             ns += seq;
-            content.put("annotation", ns);
+            content.put(ANNOTATION, ns);
             // // end of wrapped annotation block.
             // // Now a new row is created with the current set of data
 
@@ -930,10 +932,7 @@ public class StockholmFile extends AlignFile
     while ((in < s.length) && (s[in] != null))
     {
       String tmp = printId(s[in], jvSuffix);
-      if (s[in].getSequence().length > max)
-      {
-        max = s[in].getSequence().length;
-      }
+      max = Math.max(max, s[in].getLength());
 
       if (tmp.length() > maxid)
       {
index 40bb8be..3025907 100644 (file)
@@ -397,8 +397,10 @@ public abstract class StructureFile extends AlignFile
 
   public static boolean isRNA(SequenceI seq)
   {
-    for (char c : seq.getSequence())
+    int length = seq.getLength();
+    for (int i = 0; i < length; i++)
     {
+      char c = seq.getCharAt(i);
       if ((c != 'A') && (c != 'C') && (c != 'G') && (c != 'U'))
       {
         return false;
index 91add1a..da0c245 100644 (file)
@@ -353,12 +353,16 @@ public class ExonerateHelper extends Gff2Helper
     return false;
   }
 
+  /**
+   * An override to set feature group to "exonerate" instead of the default GFF
+   * source value (column 2)
+   */
   @Override
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> set)
   {
-    SequenceFeature sf = super.buildSequenceFeature(gff, set);
-    sf.setFeatureGroup("exonerate");
+    SequenceFeature sf = super.buildSequenceFeature(gff, TYPE_COL,
+            "exonerate", set);
 
     return sf;
   }
index 594040a..c7e1d7a 100644 (file)
@@ -311,10 +311,9 @@ public class Gff3Helper extends GffHelperBase
          * give the mapped sequence a copy of the sequence feature, with 
          * start/end range adjusted 
          */
-        SequenceFeature sf2 = new SequenceFeature(sf);
-        sf2.setBegin(1);
         int sequenceFeatureLength = 1 + sf.getEnd() - sf.getBegin();
-        sf2.setEnd(sequenceFeatureLength);
+        SequenceFeature sf2 = new SequenceFeature(sf, 1,
+                sequenceFeatureLength, sf.getFeatureGroup(), sf.getScore());
         mappedSequence.addSequenceFeature(sf2);
 
         /*
@@ -363,9 +362,11 @@ public class Gff3Helper extends GffHelperBase
    */
   @Override
   protected SequenceFeature buildSequenceFeature(String[] gff,
+          int typeColumn, String group,
           Map<String, List<String>> attributes)
   {
-    SequenceFeature sf = super.buildSequenceFeature(gff, attributes);
+    SequenceFeature sf = super.buildSequenceFeature(gff, typeColumn, group,
+            attributes);
     String desc = getDescription(sf, attributes);
     if (desc != null)
     {
index 301aaec..1d4d3ac 100644 (file)
@@ -338,6 +338,19 @@ public abstract class GffHelperBase implements GffHelperI
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> attributes)
   {
+    return buildSequenceFeature(gff, TYPE_COL, gff[SOURCE_COL], attributes);
+  }
+
+  /**
+   * @param gff
+   * @param typeColumn
+   * @param group
+   * @param attributes
+   * @return
+   */
+  protected SequenceFeature buildSequenceFeature(String[] gff,
+          int typeColumn, String group, Map<String, List<String>> attributes)
+  {
     try
     {
       int start = Integer.parseInt(gff[START_COL]);
@@ -356,8 +369,8 @@ public abstract class GffHelperBase implements GffHelperI
         // e.g. '.' - leave as zero
       }
 
-      SequenceFeature sf = new SequenceFeature(gff[TYPE_COL],
-              gff[SOURCE_COL], start, end, score, gff[SOURCE_COL]);
+      SequenceFeature sf = new SequenceFeature(gff[typeColumn],
+              gff[SOURCE_COL], start, end, score, group);
 
       sf.setStrand(gff[STRAND_COL]);
 
index c05593f..948cdd2 100644 (file)
@@ -73,13 +73,19 @@ public class InterProScanHelper extends Gff3Helper
   }
 
   /**
-  * 
-  */
+   * An override that
+   * <ul>
+   * <li>uses Source (column 2) as feature type instead of the default column 3</li>
+   * <li>sets "InterProScan" as the feature group</li>
+   * <li>extracts "signature_desc" attribute as the feature description</li>
+   * </ul>
+   */
   @Override
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> attributes)
   {
-    SequenceFeature sf = super.buildSequenceFeature(gff, attributes);
+    SequenceFeature sf = super.buildSequenceFeature(gff, SOURCE_COL,
+            INTER_PRO_SCAN, attributes);
 
     /*
      * signature_desc is a more informative source of description
@@ -91,13 +97,6 @@ public class InterProScanHelper extends Gff3Helper
       sf.setDescription(description);
     }
 
-    /*
-     * Set sequence feature group as 'InterProScan', and type as the source
-     * database for this match (e.g. 'Pfam')
-     */
-    sf.setType(gff[SOURCE_COL]);
-    sf.setFeatureGroup(INTER_PRO_SCAN);
-
     return sf;
   }
 
index 462f5af..a399c04 100644 (file)
 package jalview.io.vamsas;
 
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.io.VamsasAppDatastore;
 
+import java.util.List;
+
 import uk.ac.vamsas.objects.core.DataSet;
 import uk.ac.vamsas.objects.core.DbRef;
 import uk.ac.vamsas.objects.core.Sequence;
@@ -45,12 +48,12 @@ public class Datasetsequence extends DatastoreItem
   // private AlignmentI jvdset;
 
   public Datasetsequence(VamsasAppDatastore vamsasAppDatastore,
-          SequenceI sq, String dict, DataSet dataset)
+          SequenceI sq, String theDict, DataSet theDataset)
   {
     super(vamsasAppDatastore, sq, uk.ac.vamsas.objects.core.Sequence.class);
-    this.dataset = dataset;
+    this.dataset = theDataset;
     // this.jvdset = jvdset;
-    this.dict = dict;
+    this.dict = theDict;
     doSync();
   }
 
@@ -61,6 +64,7 @@ public class Datasetsequence extends DatastoreItem
     doJvUpdate();
   }
 
+  @Override
   public void addFromDocument()
   {
     Sequence vseq = (Sequence) vobj;
@@ -73,6 +77,7 @@ public class Datasetsequence extends DatastoreItem
     modified = true;
   }
 
+  @Override
   public void updateFromDoc()
   {
     Sequence sq = (Sequence) vobj;
@@ -129,25 +134,21 @@ public class Datasetsequence extends DatastoreItem
    */
   private boolean updateSqFeatures()
   {
-    boolean modified = false;
+    boolean changed = false;
     SequenceI sq = (SequenceI) jvobj;
 
     // add or update any new features/references on dataset sequence
-    if (sq.getSequenceFeatures() != null)
+    List<SequenceFeature> sfs = sq.getSequenceFeatures();
+    for (SequenceFeature sf : sfs)
     {
-      int sfSize = sq.getSequenceFeatures().length;
-
-      for (int sf = 0; sf < sfSize; sf++)
-      {
-        modified |= new jalview.io.vamsas.Sequencefeature(datastore,
-                (jalview.datamodel.SequenceFeature) sq
-                        .getSequenceFeatures()[sf],
-                dataset, (Sequence) vobj).docWasUpdated();
-      }
+      changed |= new jalview.io.vamsas.Sequencefeature(datastore, sf,
+              dataset, (Sequence) vobj).docWasUpdated();
     }
-    return modified;
+
+    return changed;
   }
 
+  @Override
   public void addToDocument()
   {
     SequenceI sq = (SequenceI) jvobj;
@@ -218,6 +219,7 @@ public class Datasetsequence extends DatastoreItem
     return modifiedtheseq;
   }
 
+  @Override
   public void conflict()
   {
     log.warn(
@@ -228,6 +230,7 @@ public class Datasetsequence extends DatastoreItem
 
   boolean modified = false;
 
+  @Override
   public void updateToDoc()
   {
     SequenceI sq = (SequenceI) jvobj;
index 31d64f5..74f73d4 100644 (file)
@@ -281,9 +281,39 @@ public class Sequencefeature extends Rangetype
   private SequenceFeature getJalviewSeqFeature(RangeAnnotation dseta)
   {
     int[] se = getBounds(dseta);
-    SequenceFeature sf = new jalview.datamodel.SequenceFeature(
-            dseta.getType(), dseta.getDescription(), dseta.getStatus(),
-            se[0], se[1], dseta.getGroup());
+
+    /*
+     * try to identify feature score
+     */
+    boolean scoreFound = false;
+    float theScore = 0f;
+    String featureType = dseta.getType();
+    if (dseta.getScoreCount() > 0)
+    {
+      Enumeration scr = dseta.enumerateScore();
+      while (scr.hasMoreElements())
+      {
+        Score score = (Score) scr.nextElement();
+        if (score.getName().equals(featureType))
+        {
+          theScore = score.getContent();
+          scoreFound = true;
+        }
+      }
+    }
+
+    SequenceFeature sf = null;
+    if (scoreFound)
+    {
+      sf = new SequenceFeature(featureType, dseta.getDescription(), se[0],
+              se[1], theScore, dseta.getGroup());
+    }
+    else
+    {
+      sf = new SequenceFeature(featureType, dseta.getDescription(), se[0],
+              se[1], dseta.getGroup());
+    }
+    sf.setStatus(dseta.getStatus());
     if (dseta.getLinkCount() > 0)
     {
       Link[] links = dseta.getLink();
@@ -299,11 +329,7 @@ public class Sequencefeature extends Rangetype
       while (scr.hasMoreElements())
       {
         Score score = (Score) scr.nextElement();
-        if (score.getName().equals(sf.getType()))
-        {
-          sf.setScore(score.getContent());
-        }
-        else
+        if (!score.getName().equals(sf.getType()))
         {
           sf.setValue(score.getName(), "" + score.getContent());
         }
index 73f66f0..1c50aab 100644 (file)
@@ -113,6 +113,7 @@ public class OverviewRenderer
     int totalPixels = miniMe.getWidth() * alignmentHeight;
 
     int lastRowUpdate = 0;
+    int lastUpdate = 0;
     changeSupport.firePropertyChange(UPDATE, -1, 0);
 
     for (int alignmentRow : rows)
@@ -162,8 +163,10 @@ public class OverviewRenderer
             }
           }
 
-          sendProgressUpdate((pixelCol + 1) * (endRow - pixelRow),
-                  totalPixels, lastRowUpdate);
+          // store last update value
+          lastUpdate = sendProgressUpdate(
+                  (pixelCol + 1) * (endRow - pixelRow), totalPixels,
+                  lastRowUpdate, lastUpdate);
 
           pixelCol = endCol + 1;
         }
@@ -172,7 +175,10 @@ public class OverviewRenderer
 
       if (pixelRow != endRow + 1)
       {
-        lastRowUpdate = sendProgressUpdate(endRow + 1, alignmentHeight, 0);
+        // store row offset and last update value
+        lastRowUpdate = sendProgressUpdate(endRow + 1, alignmentHeight, 0,
+                lastUpdate);
+        lastUpdate = lastRowUpdate;
         pixelRow = endRow + 1;
       }
       seqIndex++;
@@ -182,28 +188,31 @@ public class OverviewRenderer
     // final update to progress bar if present
     if (redraw)
     {
-      sendProgressUpdate(pixelRow - 1, alignmentHeight, 0);
+      sendProgressUpdate(pixelRow - 1, alignmentHeight, 0, 0);
     }
     else
     {
-      sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0);
+      sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
     }
     return miniMe;
   }
 
   /*
    * Calculate progress update value and fire event
+   * @param rowOffset number of rows to offset calculation by
+   * @return new rowOffset - return value only to be used when at end of a row
    */
-  private int sendProgressUpdate(int position, int maximum, int offset)
+  private int sendProgressUpdate(int position, int maximum, int rowOffset,
+          int lastUpdate)
   {
-    int newUpdate = offset
+    int newUpdate = rowOffset
             + Math.round(MAX_PROGRESS * ((float) position / maximum));
-    if (newUpdate > offset)
+    if (newUpdate > lastUpdate)
     {
-      changeSupport.firePropertyChange(UPDATE, offset, newUpdate);
+      changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
       return newUpdate;
     }
-    return offset;
+    return newUpdate;
   }
 
   /*
index b606ba3..a497d92 100644 (file)
@@ -22,7 +22,6 @@ package jalview.renderer;
 
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.gui.Preferences;
 import jalview.util.Comparison;
 
 import java.awt.Color;
@@ -37,13 +36,19 @@ public class OverviewResColourFinder extends ResidueColourFinder
 
   boolean useLegacy = false;
 
+  public static final Color OVERVIEW_DEFAULT_GAP = Color.lightGray;
+
+  public static final Color OVERVIEW_DEFAULT_LEGACY_GAP = Color.white;
+
+  public static final Color OVERVIEW_DEFAULT_HIDDEN = Color.darkGray
+          .darker();
+
   /**
    * Constructor without colour settings (used by applet)
    */
   public OverviewResColourFinder()
   {
-    this(false, Preferences.OVERVIEW_DEFAULT_GAP,
-            Preferences.OVERVIEW_DEFAULT_HIDDEN);
+    this(false, OVERVIEW_DEFAULT_GAP, OVERVIEW_DEFAULT_HIDDEN);
   }
 
   /**
index 7e4f211..c031170 100644 (file)
@@ -235,6 +235,11 @@ public class ResidueShader implements ResidueShaderI
   @Override
   public Color findColour(char symbol, int position, SequenceI seq)
   {
+    if (colourScheme == null)
+    {
+      return Color.white; // Colour is 'None'
+    }
+
     /*
      * get 'base' colour
      */
@@ -243,14 +248,16 @@ public class ResidueShader implements ResidueShaderI
             : profile.getModalResidue();
     float pid = profile == null ? 0f
             : profile.getPercentageIdentity(ignoreGaps);
-    Color colour = colourScheme == null ? Color.white
-            : colourScheme.findColour(symbol, position, seq, modalResidue,
-                    pid);
+    Color colour = colourScheme.findColour(symbol, position, seq,
+            modalResidue, pid);
 
     /*
      * apply PID threshold and consensus fading if in force
      */
-    colour = adjustColour(symbol, position, colour);
+    if (!Comparison.isGap(symbol))
+    {
+      colour = adjustColour(symbol, position, colour);
+    }
 
     return colour;
   }
index 00cbd2c..cfe2735 100644 (file)
@@ -75,7 +75,7 @@ public class FeatureColourFinder
    * @param defaultColour
    * @param seq
    * @param column
-   *          alignment column position (base zero)
+   *          alignment column position (0..)
    * @return
    */
   public Color findFeatureColour(Color defaultColour, SequenceI seq,
@@ -101,7 +101,7 @@ public class FeatureColourFinder
       }
     }
 
-    Color c = featureRenderer.findFeatureColour(seq, column, g);
+    Color c = featureRenderer.findFeatureColour(seq, column + 1, g);
     if (c == null)
     {
       return defaultColour;
index 9f57510..e81e519 100644 (file)
@@ -21,6 +21,8 @@
 package jalview.renderer.seqfeatures;
 
 import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.Range;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.util.Comparison;
@@ -31,6 +33,7 @@ import java.awt.Color;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
+import java.util.List;
 
 public class FeatureRenderer extends FeatureRendererModel
 {
@@ -213,18 +216,11 @@ public class FeatureRenderer extends FeatureRendererModel
       return null;
     }
 
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
-    {
-      return null;
-    }
-
     if (Comparison.isGap(seq.getCharAt(column)))
     {
       /*
        * returning null allows the colour scheme to provide gap colour
-       * - normally white, but can be customised otherwise
+       * - normally white, but can be customised
        */
       return null;
     }
@@ -235,7 +231,7 @@ public class FeatureRenderer extends FeatureRendererModel
       /*
        * simple case - just find the topmost rendered visible feature colour
        */
-      renderedColour = findFeatureColour(seq, seq.findPosition(column));
+      renderedColour = findFeatureColour(seq, column);
     }
     else
     {
@@ -272,8 +268,11 @@ public class FeatureRenderer extends FeatureRendererModel
           final SequenceI seq, int start, int end, int y1,
           boolean colourOnly)
   {
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
+    /*
+     * if columns are all gapped, or sequence has no features, nothing to do
+     */
+    Range visiblePositions = seq.findPositions(start+1, end+1);
+    if (visiblePositions == null || !seq.getFeatures().hasFeatures())
     {
       return null;
     }
@@ -287,10 +286,6 @@ public class FeatureRenderer extends FeatureRendererModel
               transparency));
     }
 
-    int startPos = seq.findPosition(start);
-    int endPos = seq.findPosition(end);
-
-    int sfSize = sequenceFeatures.length;
     Color drawnColour = null;
 
     /*
@@ -304,54 +299,55 @@ public class FeatureRenderer extends FeatureRendererModel
         continue;
       }
 
-      // loop through all features in sequence to find
-      // current feature to render
-      for (int sfindex = 0; sfindex < sfSize; sfindex++)
+      FeatureColourI fc = getFeatureStyle(type);
+      List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
+              visiblePositions.getBegin(), visiblePositions.getEnd(), type);
+
+      filterFeaturesForDisplay(overlaps, fc);
+
+      for (SequenceFeature sf : overlaps)
       {
-        final SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
-        if (!sequenceFeature.type.equals(type))
+        Color featureColour = fc.getColor(sf);
+        if (featureColour == null)
         {
+          // score feature outwith threshold for colouring
           continue;
         }
 
         /*
-         * a feature type may be flagged as shown but the group 
-         * an instance of it belongs to may be hidden
+         * if feature starts/ends outside the visible range,
+         * restrict to visible positions (or if a contact feature,
+         * to a single position)
          */
-        if (featureGroupNotShown(sequenceFeature))
+        int visibleStart = sf.getBegin();
+        if (visibleStart < visiblePositions.getBegin())
         {
-          continue;
+          visibleStart = sf.isContactFeature() ? sf.getEnd()
+                  : visiblePositions.getBegin();
         }
-
-        /*
-         * check feature overlaps the target range
-         * TODO: efficient retrieval of features overlapping a range
-         */
-        if (sequenceFeature.getBegin() > endPos
-                || sequenceFeature.getEnd() < startPos)
+        int visibleEnd = sf.getEnd();
+        if (visibleEnd > visiblePositions.getEnd())
         {
-          continue;
+          visibleEnd = sf.isContactFeature() ? sf.getBegin()
+                  : visiblePositions.getEnd();
         }
 
-        Color featureColour = getColour(sequenceFeature);
-        if (featureColour == null)
-        {
-          // score feature outwith threshold for colouring
-          continue;
-        }
+        int featureStartCol = seq.findIndex(visibleStart);
+        int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
+                .findIndex(visibleEnd);
+
+        // Color featureColour = getColour(sequenceFeature);
 
-        boolean isContactFeature = sequenceFeature.isContactFeature();
+        boolean isContactFeature = sf.isContactFeature();
 
         if (isContactFeature)
         {
-          boolean drawn = renderFeature(g, seq,
-                  seq.findIndex(sequenceFeature.begin) - 1,
-                  seq.findIndex(sequenceFeature.begin) - 1, featureColour,
-                  start, end, y1, colourOnly);
-          drawn |= renderFeature(g, seq,
-                  seq.findIndex(sequenceFeature.end) - 1,
-                  seq.findIndex(sequenceFeature.end) - 1, featureColour,
-                  start, end, y1, colourOnly);
+          boolean drawn = renderFeature(g, seq, featureStartCol - 1,
+                  featureStartCol - 1, featureColour, start, end, y1,
+                  colourOnly);
+          drawn |= renderFeature(g, seq, featureEndCol - 1,
+                  featureEndCol - 1, featureColour, start, end, y1,
+                  colourOnly);
           if (drawn)
           {
             drawnColour = featureColour;
@@ -359,6 +355,10 @@ public class FeatureRenderer extends FeatureRendererModel
         }
         else
         {
+          /*
+           * showing feature score by height of colour
+           * is not implemented as a selectable option 
+           *
           if (av.isShowSequenceFeaturesHeight()
                   && !Float.isNaN(sequenceFeature.score))
           {
@@ -374,15 +374,16 @@ public class FeatureRenderer extends FeatureRendererModel
           }
           else
           {
+          */
             boolean drawn = renderFeature(g, seq,
-                    seq.findIndex(sequenceFeature.begin) - 1,
-                    seq.findIndex(sequenceFeature.end) - 1, featureColour,
+                    featureStartCol - 1,
+                    featureEndCol - 1, featureColour,
                     start, end, y1, colourOnly);
             if (drawn)
             {
               drawnColour = featureColour;
             }
-          }
+          /*}*/
         }
       }
     }
@@ -400,12 +401,15 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
+<<<<<<< HEAD
+=======
    * Answers true if the feature belongs to a feature group which is not
    * currently displayed, else false
    * 
    * @param sequenceFeature
    * @return
    */
+  @Override
   protected boolean featureGroupNotShown(
           final SequenceFeature sequenceFeature)
   {
@@ -417,6 +421,7 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
+>>>>>>> refs/heads/develop
    * Called when alignment in associated view has new/modified features to
    * discover and display.
    * 
@@ -428,23 +433,23 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
-   * Returns the sequence feature colour rendered at the given sequence
-   * position, or null if none found. The feature of highest render order (i.e.
-   * on top) is found, subject to both feature type and feature group being
-   * visible, and its colour returned.
+   * Returns the sequence feature colour rendered at the given column position,
+   * or null if none found. The feature of highest render order (i.e. on top) is
+   * found, subject to both feature type and feature group being visible, and
+   * its colour returned. This method is suitable when no feature transparency
+   * applied (only the topmost visible feature colour is rendered).
+   * <p>
+   * Note this method does not check for a gap in the column so would return the
+   * colour for features enclosing a gapped column. Check for gap before calling
+   * if different behaviour is wanted.
    * 
    * @param seq
-   * @param pos
+   * @param column
+   *          (1..)
    * @return
    */
-  Color findFeatureColour(SequenceI seq, int pos)
+  Color findFeatureColour(SequenceI seq, int column)
   {
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
-    {
-      return null;
-    }
-
     /*
      * check for new feature added while processing
      */
@@ -463,33 +468,17 @@ public class FeatureRenderer extends FeatureRendererModel
         continue;
       }
 
-      for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++)
+      List<SequenceFeature> overlaps = seq.findFeatures(column, column,
+              type);
+      for (SequenceFeature sequenceFeature : overlaps)
       {
-        SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
-        if (!sequenceFeature.type.equals(type))
-        {
-          continue;
-        }
-
-        if (featureGroupNotShown(sequenceFeature))
+        if (!featureGroupNotShown(sequenceFeature))
         {
-          continue;
-        }
-
-        /*
-         * check the column position is within the feature range
-         * (or is one of the two contact positions for a contact feature)
-         */
-        boolean featureIsAtPosition = sequenceFeature.begin <= pos
-                && sequenceFeature.end >= pos;
-        if (sequenceFeature.isContactFeature())
-        {
-          featureIsAtPosition = sequenceFeature.begin == pos
-                  || sequenceFeature.end == pos;
-        }
-        if (featureIsAtPosition)
-        {
-          return getColour(sequenceFeature);
+          Color col = getColour(sequenceFeature);
+          if (col != null)
+          {
+            return col;
+          }
         }
       }
     }
index ccbad44..ec13343 100755 (executable)
@@ -107,19 +107,18 @@ public class ClustalxColourScheme extends ResidueColourScheme
 
     for (SequenceI sq : seqs)
     {
-      char[] seq = sq.getSequence();
-
-      int end_j = seq.length - 1;
+      int end_j = sq.getLength() - 1;
+      int length = sq.getLength();
 
       for (int i = 0; i <= end_j; i++)
       {
-        if ((seq.length - 1) < i)
+        if (length - 1 < i)
         {
           res = 23;
         }
         else
         {
-          res = ResidueProperties.aaIndex[seq[i]];
+          res = ResidueProperties.aaIndex[sq.getCharAt(i)];
         }
         cons2[i][res]++;
       }
index a40a690..54d1c6c 100644 (file)
@@ -550,16 +550,27 @@ public class FeatureColour implements FeatureColourI
       return getColour();
     }
 
-    // todo should we check for above/below threshold here?
-    if (range == 0.0)
-    {
-      return getMaxColour();
-    }
+    /*
+     * graduated colour case, optionally with threshold
+     * Float.NaN is assigned minimum visible score colour
+     */
     float scr = feature.getScore();
     if (Float.isNaN(scr))
     {
       return getMinColour();
     }
+    if (isAboveThreshold() && scr <= threshold)
+    {
+      return null;
+    }
+    if (isBelowThreshold() && scr >= threshold)
+    {
+      return null;
+    }
+    if (range == 0.0)
+    {
+      return getMaxColour();
+    }
     float scl = (scr - base) / range;
     if (isHighToLow)
     {
@@ -601,44 +612,6 @@ public class FeatureColour implements FeatureColourI
     return (isHighToLow) ? (base + range) : base;
   }
 
-  /**
-   * Answers true if the feature has a simple colour, or is coloured by label,
-   * or has a graduated colour and the score of this feature instance is within
-   * the range to render (if any), i.e. does not lie below or above any
-   * threshold set.
-   * 
-   * @param feature
-   * @return
-   */
-  @Override
-  public boolean isColored(SequenceFeature feature)
-  {
-    if (isColourByLabel() || !isGraduatedColour())
-    {
-      return true;
-    }
-
-    float val = feature.getScore();
-    if (Float.isNaN(val))
-    {
-      return true;
-    }
-    if (Float.isNaN(this.threshold))
-    {
-      return true;
-    }
-
-    if (isAboveThreshold() && val <= threshold)
-    {
-      return false;
-    }
-    if (isBelowThreshold() && val >= threshold)
-    {
-      return false;
-    }
-    return true;
-  }
-
   @Override
   public boolean isSimpleColour()
   {
index 17d3a70..d4fc233 100644 (file)
@@ -285,35 +285,10 @@ public class Comparison
     {
       return false;
     }
-    char[][] letters = new char[seqs.length][];
-    for (int i = 0; i < seqs.length; i++)
-    {
-      if (seqs[i] != null)
-      {
-        char[] sequence = seqs[i].getSequence();
-        if (sequence != null)
-        {
-          letters[i] = sequence;
-        }
-      }
-    }
-
-    return areNucleotide(letters);
-  }
 
-  /**
-   * Answers true if more than 85% of the sequence residues (ignoring gaps) are
-   * A, G, C, T or U, else false. This is just a heuristic guess and may give a
-   * wrong answer (as AGCT are also amino acid codes).
-   * 
-   * @param letters
-   * @return
-   */
-  static final boolean areNucleotide(char[][] letters)
-  {
     int ntCount = 0;
     int aaCount = 0;
-    for (char[] seq : letters)
+    for (SequenceI seq : seqs)
     {
       if (seq == null)
       {
@@ -321,8 +296,10 @@ public class Comparison
       }
       // TODO could possibly make an informed guess just from the first sequence
       // to save a lengthy calculation
-      for (char c : seq)
+      int len = seq.getLength();
+      for (int i = 0; i < len; i++)
       {
+        char c = seq.getCharAt(i);
         if (isNucleotide(c))
         {
           ntCount++;
diff --git a/src/jalview/util/IntRangeComparator.java b/src/jalview/util/IntRangeComparator.java
new file mode 100644 (file)
index 0000000..cb32a0e
--- /dev/null
@@ -0,0 +1,31 @@
+package jalview.util;
+
+import java.util.Comparator;
+
+/**
+ * A comparator to order [from, to] ranges into ascending or descending order of
+ * their start position
+ */
+public class IntRangeComparator implements Comparator<int[]>
+{
+  public static final Comparator<int[]> ASCENDING = new IntRangeComparator(
+          true);
+
+  public static final Comparator<int[]> DESCENDING = new IntRangeComparator(
+          false);
+
+  boolean forwards;
+
+  IntRangeComparator(boolean forward)
+  {
+    forwards = forward;
+  }
+
+  @Override
+  public int compare(int[] o1, int[] o2)
+  {
+    int compared = Integer.compare(o1[0], o2[0]);
+    return forwards ? compared : -compared;
+  }
+
+}
\ No newline at end of file
diff --git a/src/jalview/util/RangeComparator.java b/src/jalview/util/RangeComparator.java
deleted file mode 100644 (file)
index 0a3ddd8..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.util;
-
-import java.util.Comparator;
-
-/**
- * A comparator to order [from, to] ranges into ascending or descending order of
- * their start position
- */
-public class RangeComparator implements Comparator<int[]>
-{
-  boolean forwards;
-
-  public RangeComparator(boolean forward)
-  {
-    forwards = forward;
-  }
-
-  @Override
-  public int compare(int[] o1, int[] o2)
-  {
-    int compared = Integer.compare(o1[0], o2[0]);
-    return forwards ? compared : -compared;
-  }
-
-}
\ No newline at end of file
index f8115a1..42d490e 100644 (file)
@@ -456,18 +456,42 @@ public class ViewportRanges extends ViewportProperties
   }
 
   /**
-   * Scroll a wrapped alignment so that the specified residue is visible. Fires
-   * a property change event.
+   * Scroll a wrapped alignment so that the specified residue is in the first
+   * repeat of the wrapped view. Fires a property change event. Answers true if
+   * the startRes changed, else false.
    * 
    * @param res
    *          residue position to scroll to
+   * @return
    */
-  public void scrollToWrappedVisible(int res)
+  public boolean scrollToWrappedVisible(int res)
   {
-    // get the start residue of the wrapped row which res is in
-    // and set that as our start residue
+    int oldStartRes = startRes;
     int width = getViewportWidth();
-    setStartRes((res / width) * width);
+
+    if (res >= oldStartRes && res < oldStartRes + width)
+    {
+      return false;
+    }
+
+    boolean up = res < oldStartRes;
+    int widthsToScroll = Math.abs((res - oldStartRes) / width);
+    if (up)
+    {
+      widthsToScroll++;
+    }
+
+    int residuesToScroll = width * widthsToScroll;
+    int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes
+            + residuesToScroll;
+    if (newStartRes < 0)
+    {
+      newStartRes = 0;
+    }
+
+    setStartRes(newStartRes);
+
+    return true;
   }
 
   /**
index 81d1b7e..2f30e94 100644 (file)
@@ -26,6 +26,7 @@ import jalview.api.FeaturesDisplayedI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.renderer.seqfeatures.FeatureRenderer;
 import jalview.schemes.FeatureColour;
 import jalview.util.ColorUtils;
@@ -116,11 +117,10 @@ public abstract class FeatureRendererModel
           synchronized (fd)
           {
             fd.clear();
-            java.util.Iterator<String> fdisp = _fr.getFeaturesDisplayed()
-                    .getVisibleFeatures();
-            while (fdisp.hasNext())
+            for (String type : _fr.getFeaturesDisplayed()
+                    .getVisibleFeatures())
             {
-              fd.setVisible(fdisp.next());
+              fd.setVisible(type);
             }
           }
         }
@@ -265,49 +265,40 @@ public abstract class FeatureRendererModel
   }
 
   @Override
-  public List<SequenceFeature> findFeaturesAtRes(SequenceI sequence,
-          int res)
+  public List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
   {
-    ArrayList<SequenceFeature> tmp = new ArrayList<SequenceFeature>();
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-
-    if (features != null)
+    /*
+     * include features at the position provided their feature type is 
+     * displayed, and feature group is null or marked for display
+     */
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+    if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
     {
-      for (int i = 0; i < features.length; i++)
-      {
-        if (!av.areFeaturesDisplayed() || !av.getFeaturesDisplayed()
-                .isVisible(features[i].getType()))
-        {
-          continue;
-        }
+      return result;
+    }
 
-        if (features[i].featureGroup != null && featureGroups != null
-                && featureGroups.containsKey(features[i].featureGroup)
-                && !featureGroups.get(features[i].featureGroup)
-                        .booleanValue())
-        {
-          continue;
-        }
+    Set<String> visibleFeatures = getFeaturesDisplayed()
+            .getVisibleFeatures();
+    String[] visibleTypes = visibleFeatures
+            .toArray(new String[visibleFeatures.size()]);
+    List<SequenceFeature> features = sequence.findFeatures(column, column,
+            visibleTypes);
 
-        // check if start/end are at res, and if not a contact feature, that res
-        // lies between start and end
-        if ((features[i].getBegin() == res || features[i].getEnd() == res)
-                || (!features[i].isContactFeature()
-                        && (features[i].getBegin() < res)
-                        && (features[i].getEnd() >= res)))
-        {
-          tmp.add(features[i]);
-        }
+    for (SequenceFeature sf : features)
+    {
+      if (!featureGroupNotShown(sf))
+      {
+        result.add(sf);
       }
     }
-    return tmp;
+    return result;
   }
 
   /**
    * Searches alignment for all features and updates colours
    * 
    * @param newMadeVisible
-   *          if true newly added feature types will be rendered immediatly
+   *          if true newly added feature types will be rendered immediately
    *          TODO: check to see if this method should actually be proxied so
    *          repaint events can be propagated by the renderer code
    */
@@ -329,8 +320,7 @@ public abstract class FeatureRendererModel
     }
     FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
 
-    ArrayList<String> allfeatures = new ArrayList<String>();
-    ArrayList<String> oldfeatures = new ArrayList<String>();
+    Set<String> oldfeatures = new HashSet<String>();
     if (renderOrder != null)
     {
       for (int i = 0; i < renderOrder.length; i++)
@@ -341,107 +331,110 @@ public abstract class FeatureRendererModel
         }
       }
     }
-    if (minmax == null)
-    {
-      minmax = new Hashtable<String, float[][]>();
-    }
 
-    Set<String> oldGroups = new HashSet<String>(featureGroups.keySet());
     AlignmentI alignment = av.getAlignment();
+    List<String> allfeatures = new ArrayList<String>();
+
     for (int i = 0; i < alignment.getHeight(); i++)
     {
       SequenceI asq = alignment.getSequenceAt(i);
-      SequenceFeature[] features = asq.getSequenceFeatures();
-
-      if (features == null)
-      {
-        continue;
-      }
-
-      int index = 0;
-      while (index < features.length)
+      for (String group : asq.getFeatures().getFeatureGroups(true))
       {
-        String fgrp = features[index].getFeatureGroup();
-        oldGroups.remove(fgrp);
-        if (!featuresDisplayed.isRegistered(features[index].getType()))
+        boolean groupDisplayed = true;
+        if (group != null)
         {
-          if (fgrp != null)
+          if (featureGroups.containsKey(group))
           {
-            Boolean groupDisplayed = featureGroups.get(fgrp);
-            if (groupDisplayed == null)
-            {
-              groupDisplayed = Boolean.valueOf(newMadeVisible);
-              featureGroups.put(fgrp, groupDisplayed);
-            }
-            if (!groupDisplayed.booleanValue())
-            {
-              index++;
-              continue;
-            }
+            groupDisplayed = featureGroups.get(group);
           }
-          if (!(features[index].begin == 0 && features[index].end == 0))
+          else
           {
-            // If beginning and end are 0, the feature is for the whole sequence
-            // and we don't want to render the feature in the normal way
-
-            if (newMadeVisible
-                    && !oldfeatures.contains(features[index].getType()))
-            {
-              // this is a new feature type on the alignment. Mark it for
-              // display.
-              featuresDisplayed.setVisible(features[index].getType());
-              setOrder(features[index].getType(), 0);
-            }
+            groupDisplayed = newMadeVisible;
+            featureGroups.put(group, groupDisplayed);
           }
         }
-        if (!allfeatures.contains(features[index].getType()))
+        if (groupDisplayed)
         {
-          allfeatures.add(features[index].getType());
-        }
-        if (!Float.isNaN(features[index].score))
-        {
-          int nonpos = features[index].getBegin() >= 1 ? 0 : 1;
-          float[][] mm = minmax.get(features[index].getType());
-          if (mm == null)
-          {
-            mm = new float[][] { null, null };
-            minmax.put(features[index].getType(), mm);
-          }
-          if (mm[nonpos] == null)
-          {
-            mm[nonpos] = new float[] { features[index].score,
-                features[index].score };
-
-          }
-          else
+          Set<String> types = asq.getFeatures().getFeatureTypesForGroups(
+                  true, group);
+          for (String type : types)
           {
-            if (mm[nonpos][0] > features[index].score)
+            if (!allfeatures.contains(type)) // or use HashSet and no test?
             {
-              mm[nonpos][0] = features[index].score;
-            }
-            if (mm[nonpos][1] < features[index].score)
-            {
-              mm[nonpos][1] = features[index].score;
+              allfeatures.add(type);
             }
+            updateMinMax(asq, type, true); // todo: for all features?
           }
         }
-        index++;
       }
     }
 
-    /*
-     * oldGroups now consists of groups that no longer 
-     * have any feature in them - remove these
-     */
-    for (String grp : oldGroups)
+    // uncomment to add new features in alphebetical order (but JAL-2575)
+    // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER);
+    if (newMadeVisible)
     {
-      featureGroups.remove(grp);
+      for (String type : allfeatures)
+      {
+        if (!oldfeatures.contains(type))
+        {
+          featuresDisplayed.setVisible(type);
+          setOrder(type, 0);
+        }
+      }
     }
 
     updateRenderOrder(allfeatures);
     findingFeatures = false;
   }
 
+  /**
+   * Updates the global (alignment) min and max values for a feature type from
+   * the score for a sequence, if the score is not NaN. Values are stored
+   * separately for positional and non-positional features.
+   * 
+   * @param seq
+   * @param featureType
+   * @param positional
+   */
+  protected void updateMinMax(SequenceI seq, String featureType,
+          boolean positional)
+  {
+    float min = seq.getFeatures().getMinimumScore(featureType, positional);
+    if (Float.isNaN(min))
+    {
+      return;
+    }
+
+    float max = seq.getFeatures().getMaximumScore(featureType, positional);
+
+    /*
+     * stored values are 
+     * { {positionalMin, positionalMax}, {nonPositionalMin, nonPositionalMax} }
+     */
+    if (minmax == null)
+    {
+      minmax = new Hashtable<String, float[][]>();
+    }
+    synchronized (minmax)
+    {
+      float[][] mm = minmax.get(featureType);
+      int index = positional ? 0 : 1;
+      if (mm == null)
+      {
+        mm = new float[][] { null, null };
+        minmax.put(featureType, mm);
+      }
+      if (mm[index] == null)
+      {
+        mm[index] = new float[] { min, max };
+      }
+      else
+      {
+        mm[index][0] = Math.min(mm[index][0], min);
+        mm[index][1] = Math.max(mm[index][1], max);
+      }
+    }
+  }
   protected Boolean firing = Boolean.FALSE;
 
   /**
@@ -568,7 +561,8 @@ public abstract class FeatureRendererModel
    * Returns the configured colour for a particular feature instance. This
    * includes calculation of 'colour by label', or of a graduated score colour,
    * if applicable. It does not take into account feature visibility or colour
-   * transparency.
+   * transparency. Returns null for a score feature whose score value lies
+   * outside any colour threshold.
    * 
    * @param feature
    * @return
@@ -576,21 +570,7 @@ public abstract class FeatureRendererModel
   public Color getColour(SequenceFeature feature)
   {
     FeatureColourI fc = getFeatureStyle(feature.getType());
-    return fc.isColored(feature) ? fc.getColor(feature) : null;
-  }
-
-  /**
-   * Answers true unless the feature has a score value which lies outside a
-   * minimum or maximum threshold configured for colouring. This method does not
-   * check feature type or group visibility.
-   * 
-   * @param sequenceFeature
-   * @return
-   */
-  protected boolean showFeature(SequenceFeature sequenceFeature)
-  {
-    FeatureColourI fc = getFeatureStyle(sequenceFeature.type);
-    return fc.isColored(sequenceFeature);
+    return fc.getColor(feature);
   }
 
   /**
@@ -680,7 +660,8 @@ public abstract class FeatureRendererModel
   }
 
   /**
-   * Sets the priority order for features
+   * Sets the priority order for features, with the highest priority (displayed
+   * on top) at the start of the data array
    * 
    * @param data
    *          { String(Type), Colour(Type), Boolean(Displayed) }
@@ -906,11 +887,10 @@ public abstract class FeatureRendererModel
     {
       return fcols;
     }
-    Iterator<String> features = getViewport().getFeaturesDisplayed()
+    Set<String> features = getViewport().getFeaturesDisplayed()
             .getVisibleFeatures();
-    while (features.hasNext())
+    for (String feature : features)
     {
-      String feature = features.next();
       fcols.put(feature, getFeatureStyle(feature));
     }
     return fcols;
@@ -952,25 +932,117 @@ public abstract class FeatureRendererModel
   public List<String> getDisplayedFeatureGroups()
   {
     List<String> _gps = new ArrayList<String>();
-    boolean valid = false;
     for (String gp : getFeatureGroups())
     {
       if (checkGroupVisibility(gp, false))
       {
-        valid = true;
         _gps.add(gp);
       }
-      if (!valid)
+    }
+    return _gps;
+  }
+
+  /**
+   * Answers true if the feature belongs to a feature group which is not
+   * currently displayed, else false
+   * 
+   * @param sequenceFeature
+   * @return
+   */
+  protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature)
+  {
+    return featureGroups != null
+            && sequenceFeature.featureGroup != null
+            && sequenceFeature.featureGroup.length() != 0
+            && featureGroups.containsKey(sequenceFeature.featureGroup)
+            && !featureGroups.get(sequenceFeature.featureGroup)
+                    .booleanValue();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
+          int resNo)
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+    if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
+    {
+      return result;
+    }
+
+    /*
+     * include features at the position provided their feature type is 
+     * displayed, and feature group is null or the empty string
+     * or marked for display
+     */
+    Set<String> visibleFeatures = getFeaturesDisplayed()
+            .getVisibleFeatures();
+    String[] visibleTypes = visibleFeatures
+            .toArray(new String[visibleFeatures.size()]);
+    List<SequenceFeature> features = sequence.getFeatures().findFeatures(
+            resNo, resNo, visibleTypes);
+  
+    for (SequenceFeature sf : features)
+    {
+      if (!featureGroupNotShown(sf))
       {
-        return null;
+        result.add(sf);
       }
-      else
+    }
+    return result;
+  }
+
+  /**
+   * Removes from the list of features any that have a feature group that is not
+   * displayed, or duplicate the location of a feature of the same type (unless
+   * a graduated colour scheme or colour by label is applied). Should be used
+   * only for features of the same feature colour (which normally implies the
+   * same feature type).
+   * 
+   * @param features
+   * @param fc
+   */
+  public void filterFeaturesForDisplay(List<SequenceFeature> features,
+          FeatureColourI fc)
+  {
+    if (features.isEmpty())
+    {
+      return;
+    }
+    SequenceFeatures.sortFeatures(features, true);
+    boolean simpleColour = fc == null || fc.isSimpleColour();
+    SequenceFeature lastFeature = null;
+
+    Iterator<SequenceFeature> it = features.iterator();
+    while (it.hasNext())
+    {
+      SequenceFeature sf = it.next();
+      if (featureGroupNotShown(sf))
       {
-        // gps = new String[_gps.size()];
-        // _gps.toArray(gps);
+        it.remove();
+        continue;
       }
+
+      /*
+       * a feature is redundant for rendering purposes if it has the
+       * same extent as another (so would just redraw the same colour);
+       * (checking type and isContactFeature as a fail-safe here, although
+       * currently they are guaranteed to match in this context)
+       */
+      if (simpleColour)
+      {
+        if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+                && sf.getEnd() == lastFeature.getEnd()
+                && sf.isContactFeature() == lastFeature.isContactFeature()
+                && sf.getType().equals(lastFeature.getType()))
+        {
+          it.remove();
+        }
+      }
+      lastFeature = sf;
     }
-    return _gps;
   }
 
 }
index 4c7e3c4..f44a2d1 100644 (file)
@@ -23,22 +23,21 @@ package jalview.viewmodel.seqfeatures;
 import jalview.api.FeaturesDisplayedI;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Set;
 
 public class FeaturesDisplayed implements FeaturesDisplayedI
 {
-  private HashSet<String> featuresDisplayed = new HashSet<String>();
+  private Set<String> featuresDisplayed = new HashSet<String>();
 
-  private HashSet<String> featuresRegistered = new HashSet<String>();
+  private Set<String> featuresRegistered = new HashSet<String>();
 
   public FeaturesDisplayed(FeaturesDisplayedI featuresDisplayed2)
   {
-    Iterator<String> fdisp = featuresDisplayed2.getVisibleFeatures();
-    String ftype;
-    while (fdisp.hasNext())
+    Set<String> fdisp = featuresDisplayed2.getVisibleFeatures();
+    for (String ftype : fdisp)
     {
-      ftype = fdisp.next();
       featuresDisplayed.add(ftype);
       featuresRegistered.add(ftype);
     }
@@ -46,13 +45,12 @@ public class FeaturesDisplayed implements FeaturesDisplayedI
 
   public FeaturesDisplayed()
   {
-    // TODO Auto-generated constructor stub
   }
 
   @Override
-  public Iterator<String> getVisibleFeatures()
+  public Set<String> getVisibleFeatures()
   {
-    return featuresDisplayed.iterator();
+    return Collections.unmodifiableSet(featuresDisplayed);
   }
 
   @Override
index 2422748..24cb717 100644 (file)
@@ -229,6 +229,7 @@ class ColumnCounterSetWorker extends AlignCalcWorker
    * 
    * @param alignment
    * @param col
+   *          (0..)
    * @param row
    * @param fr
    */
@@ -249,14 +250,12 @@ class ColumnCounterSetWorker extends AlignCalcWorker
     {
       return null;
     }
-    int pos = seq.findPosition(col);
 
     /*
      * compute a count for any displayed features at residue
      */
-    // NB have to adjust pos if using AlignmentView.getVisibleAlignment
     // see JAL-2075
-    List<SequenceFeature> features = fr.findFeaturesAtRes(seq, pos);
+    List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, col + 1);
     int[] count = this.counter.count(String.valueOf(res), features);
     return count;
   }
index ea6c5f2..fb8864d 100644 (file)
@@ -26,7 +26,6 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.Mapping;
-import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.CutAndPasteTransfer;
 import jalview.gui.DasSourceBrowser;
@@ -706,28 +705,13 @@ public class DBRefFetcher implements Runnable
 
           if (updateRefFrame)
           {
-            SequenceFeature[] sfs = sequence.getSequenceFeatures();
-            if (sfs != null)
+            /*
+             * relocate existing sequence features by offset
+             */
+            int startShift = absStart - sequenceStart + 1;
+            if (startShift != 0)
             {
-              /*
-               * relocate existing sequence features by offset
-               */
-              int start = sequenceStart;
-              int end = sequence.getEnd();
-              int startShift = 1 - absStart - start;
-
-              if (startShift != 0)
-              {
-                for (SequenceFeature sf : sfs)
-                {
-                  if (sf.getBegin() >= start && sf.getEnd() <= end)
-                  {
-                    sf.setBegin(sf.getBegin() + startShift);
-                    sf.setEnd(sf.getEnd() + startShift);
-                    modified = true;
-                  }
-                }
-              }
+              modified |= sequence.getFeatures().shiftFeatures(startShift);
             }
           }
         }
index 30808fb..c868576 100644 (file)
@@ -28,8 +28,9 @@ import jalview.datamodel.PDBEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.UniprotEntry;
-import jalview.datamodel.UniprotFile;
+import jalview.datamodel.xdb.uniprot.UniprotEntry;
+import jalview.datamodel.xdb.uniprot.UniprotFeature;
+import jalview.datamodel.xdb.uniprot.UniprotFile;
 import jalview.ws.ebi.EBIFetchClient;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
 
@@ -255,16 +256,17 @@ public class Uniprot extends DbSourceProxyImpl
 
         }
       }
-
     }
 
     sequence.setPDBId(onlyPdbEntries);
     if (entry.getFeature() != null)
     {
-      for (SequenceFeature sf : entry.getFeature())
+      for (UniprotFeature uf : entry.getFeature())
       {
-        sf.setFeatureGroup("Uniprot");
-        sequence.addSequenceFeature(sf);
+        SequenceFeature copy = new SequenceFeature(uf.getType(),
+                uf.getDescription(), uf.getBegin(), uf.getEnd(), "Uniprot");
+        copy.setStatus(uf.getStatus());
+        sequence.addSequenceFeature(copy);
       }
     }
     for (DBRefEntry dbr : dbRefs)
index fb6ffc4..c24ea05 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.ws.jws2;
 
-import jalview.api.AlignCalcWorkerI;
 import jalview.api.FeatureColourI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
@@ -63,9 +62,9 @@ public class AADisorderClient extends JabawsCalcWorker
   AlignFrame af;
 
   public AADisorderClient(Jws2Instance sh, AlignFrame alignFrame,
-          WsParamSetI preset, List<Argument> paramset)
+          WsParamSetI thePreset, List<Argument> paramset)
   {
-    super(sh, alignFrame, preset, paramset);
+    super(sh, alignFrame, thePreset, paramset);
     af = alignFrame;
     typeName = sh.action;
     methodName = sh.serviceType;
@@ -239,13 +238,13 @@ public class AADisorderClient extends JabawsCalcWorker
                 }
                 if (vals.hasNext())
                 {
-                  sf = new SequenceFeature(type[0], type[1], base + rn.from,
-                          base + rn.to, val = vals.next().floatValue(),
-                          methodName);
+                  val = vals.next().floatValue();
+                  sf = new SequenceFeature(type[0], type[1],
+                          base + rn.from, base + rn.to, val, methodName);
                 }
                 else
                 {
-                  sf = new SequenceFeature(type[0], type[1], null,
+                  sf = new SequenceFeature(type[0], type[1],
                           base + rn.from, base + rn.to, methodName);
                 }
                 dseq.addSequenceFeature(sf);
index e03cd64..55ca3ff 100644 (file)
@@ -65,7 +65,7 @@ public class SeqVector extends InputType
       {
         idvector.append(sep);
       }
-      idvector.append(seq.getSequence());
+      idvector.append(seq.getSequenceAsString());
     }
     return new StringBody(idvector.toString());
   }
index 7132939..defcdbc 100644 (file)
@@ -36,6 +36,7 @@ import jalview.schemes.TaylorColourScheme;
 import jalview.structure.StructureImportSettings;
 
 import java.awt.Color;
+import java.util.List;
 import java.util.Vector;
 
 import org.testng.annotations.BeforeClass;
@@ -258,19 +259,19 @@ public class PDBChainTest
     /*
      * check sequence features
      */
-    SequenceFeature[] sfs = c.sequence.getSequenceFeatures();
-    assertEquals(3, sfs.length);
-    assertEquals("RESNUM", sfs[0].type);
-    assertEquals("MET:4 1gaqA", sfs[0].description);
-    assertEquals(4, sfs[0].begin);
-    assertEquals(4, sfs[0].end);
-    assertEquals("RESNUM", sfs[0].type);
-    assertEquals("LYS:5 1gaqA", sfs[1].description);
-    assertEquals(5, sfs[1].begin);
-    assertEquals(5, sfs[1].end);
-    assertEquals("LEU:6 1gaqA", sfs[2].description);
-    assertEquals(6, sfs[2].begin);
-    assertEquals(6, sfs[2].end);
+    List<SequenceFeature> sfs = c.sequence.getSequenceFeatures();
+    assertEquals(3, sfs.size());
+    assertEquals("RESNUM", sfs.get(0).type);
+    assertEquals("MET:4 1gaqA", sfs.get(0).description);
+    assertEquals(4, sfs.get(0).begin);
+    assertEquals(4, sfs.get(0).end);
+    assertEquals("RESNUM", sfs.get(0).type);
+    assertEquals("LYS:5 1gaqA", sfs.get(1).description);
+    assertEquals(5, sfs.get(1).begin);
+    assertEquals(5, sfs.get(1).end);
+    assertEquals("LEU:6 1gaqA", sfs.get(2).description);
+    assertEquals(6, sfs.get(2).begin);
+    assertEquals(6, sfs.get(2).end);
   }
 
   private Atom makeAtom(int resnum, String name, String resname)
index 088611e..3b9be23 100644 (file)
@@ -31,10 +31,9 @@ public class AlignmentSorterTest
     /*
      * sort with no score features does nothing
      */
-    PA.setValue(AlignmentSorter.class, "lastSortByFeatureScore", null);
+    PA.setValue(AlignmentSorter.class, "sortByFeatureCriteria", null);
 
-    AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(),
-            al,
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
             AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(0), seq1);
     assertSame(al.getSequenceAt(1), seq2);
@@ -65,9 +64,9 @@ public class AlignmentSorterTest
      * sort by ascending score, no filter on feature type or group
      * NB sort order for the same feature set (none) gets toggled, so descending
      */
-    PA.setValue(AlignmentSorter.class, "sortByFeatureScoreAscending", true);
-    AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(),
-            al, AlignmentSorter.FEATURE_SCORE);
+    PA.setValue(AlignmentSorter.class, "sortByFeatureAscending", true);
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(3), seq3); // -0.5
     assertSame(al.getSequenceAt(2), seq2); // 2.5
     assertSame(al.getSequenceAt(1), seq1); // 3.0
@@ -76,8 +75,8 @@ public class AlignmentSorterTest
     /*
      * repeat sort toggles order - now ascending
      */
-    AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(),
-            al, AlignmentSorter.FEATURE_SCORE);
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(0), seq3); // -0.5
     assertSame(al.getSequenceAt(1), seq2); // 2.5
     assertSame(al.getSequenceAt(2), seq1); // 3.0
@@ -116,7 +115,7 @@ public class AlignmentSorterTest
      */
     // fails because seq1.findPosition(4) returns 4
     // although residue 4 is in column 5! - JAL-2544
-    AlignmentSorter.sortByFeature((String) null, null, 0, 4, al,
+    AlignmentSorter.sortByFeature(null, null, 0, 4, al,
             AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(0), seq3); // -4
     assertSame(al.getSequenceAt(1), seq1); // 2.0
index bada3ca..4439bb9 100644 (file)
@@ -40,6 +40,7 @@ import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.DataSourceType;
@@ -1179,12 +1180,12 @@ public class AlignmentUtilsTests
     /*
      * check cds2 acquired a variant feature in position 5
      */
-    SequenceFeature[] sfs = cds2Dss.getSequenceFeatures();
+    List<SequenceFeature> sfs = cds2Dss.getSequenceFeatures();
     assertNotNull(sfs);
-    assertEquals(1, sfs.length);
-    assertEquals("variant", sfs[0].type);
-    assertEquals(5, sfs[0].begin);
-    assertEquals(5, sfs[0].end);
+    assertEquals(1, sfs.size());
+    assertEquals("variant", sfs.get(0).type);
+    assertEquals(5, sfs.get(0).begin);
+    assertEquals(5, sfs.get(0).end);
   }
 
   /**
@@ -1489,39 +1490,39 @@ public class AlignmentUtilsTests
      * that partially overlap 5' or 3' (start or end) of target sequence
      */
     AlignmentUtils.transferFeatures(dna, cds, map, null);
-    SequenceFeature[] sfs = cds.getSequenceFeatures();
-    assertEquals(6, sfs.length);
+    List<SequenceFeature> sfs = cds.getSequenceFeatures();
+    assertEquals(6, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    SequenceFeature sf = sfs.get(0);
     assertEquals("type2", sf.getType());
     assertEquals("desc2", sf.getDescription());
     assertEquals(2f, sf.getScore());
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
 
-    sf = sfs[1];
+    sf = sfs.get(1);
     assertEquals("type3", sf.getType());
     assertEquals("desc3", sf.getDescription());
     assertEquals(3f, sf.getScore());
     assertEquals(1, sf.getBegin());
     assertEquals(3, sf.getEnd());
 
-    sf = sfs[2];
+    sf = sfs.get(2);
     assertEquals("type4", sf.getType());
     assertEquals(2, sf.getBegin());
     assertEquals(5, sf.getEnd());
 
-    sf = sfs[3];
+    sf = sfs.get(3);
     assertEquals("type5", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(6, sf.getEnd());
 
-    sf = sfs[4];
+    sf = sfs.get(4);
     assertEquals("type8", sf.getType());
     assertEquals(6, sf.getBegin());
     assertEquals(6, sf.getEnd());
 
-    sf = sfs[5];
+    sf = sfs.get(5);
     assertEquals("type9", sf.getType());
     assertEquals(6, sf.getBegin());
     assertEquals(6, sf.getEnd());
@@ -1551,10 +1552,10 @@ public class AlignmentUtilsTests
 
     // desc4 and desc8 are the 'omit these' varargs
     AlignmentUtils.transferFeatures(dna, cds, map, null, "type4", "type8");
-    SequenceFeature[] sfs = cds.getSequenceFeatures();
-    assertEquals(1, sfs.length);
+    List<SequenceFeature> sfs = cds.getSequenceFeatures();
+    assertEquals(1, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    SequenceFeature sf = sfs.get(0);
     assertEquals("type5", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(6, sf.getEnd());
@@ -1584,10 +1585,10 @@ public class AlignmentUtilsTests
 
     // "type5" is the 'select this type' argument
     AlignmentUtils.transferFeatures(dna, cds, map, "type5");
-    SequenceFeature[] sfs = cds.getSequenceFeatures();
-    assertEquals(1, sfs.length);
+    List<SequenceFeature> sfs = cds.getSequenceFeatures();
+    assertEquals(1, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    SequenceFeature sf = sfs.get(0);
     assertEquals("type5", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(6, sf.getEnd());
@@ -2078,24 +2079,29 @@ public class AlignmentUtilsTests
      * var6 P -> H COSMIC
      * var6 P -> R COSMIC
      */
-    SequenceFeature[] sfs = peptide.getSequenceFeatures();
-    assertEquals(5, sfs.length);
+    List<SequenceFeature> sfs = peptide.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertEquals(5, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    /*
+     * features are sorted by start position ascending, but in no
+     * particular order where start positions match; asserts here
+     * simply match the data returned (the order is not important)
+     */
+    SequenceFeature sf = sfs.get(0);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
-    assertEquals("p.Lys1Glu", sf.getDescription());
-    assertEquals("var1.125A>G", sf.getValue("ID"));
-    assertNull(sf.getValue("clinical_significance"));
-    assertEquals("ID=var1.125A>G", sf.getAttributes());
+    assertEquals("p.Lys1Asn", sf.getDescription());
+    assertEquals("var4", sf.getValue("ID"));
+    assertEquals("Benign", sf.getValue("clinical_significance"));
+    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
     assertEquals(1, sf.links.size());
-    // link to variation is urlencoded
     assertEquals(
-            "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
+            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
-    sf = sfs[1];
+    sf = sfs.get(1);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
     assertEquals("p.Lys1Gln", sf.getDescription());
@@ -2108,43 +2114,44 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(dbSnp, sf.getFeatureGroup());
 
-    sf = sfs[2];
+    sf = sfs.get(2);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
-    assertEquals("p.Lys1Asn", sf.getDescription());
-    assertEquals("var4", sf.getValue("ID"));
-    assertEquals("Benign", sf.getValue("clinical_significance"));
-    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
+    assertEquals("p.Lys1Glu", sf.getDescription());
+    assertEquals("var1.125A>G", sf.getValue("ID"));
+    assertNull(sf.getValue("clinical_significance"));
+    assertEquals("ID=var1.125A>G", sf.getAttributes());
     assertEquals(1, sf.links.size());
+    // link to variation is urlencoded
     assertEquals(
-            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
-    // var5 generates two distinct protein variant features
-    sf = sfs[3];
+    sf = sfs.get(3);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
-    assertEquals("p.Pro3His", sf.getDescription());
+    assertEquals("p.Pro3Arg", sf.getDescription());
     assertEquals("var6", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
     assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
+            "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
 
-    sf = sfs[4];
+    // var5 generates two distinct protein variant features
+    sf = sfs.get(4);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
-    assertEquals("p.Pro3Arg", sf.getDescription());
+    assertEquals("p.Pro3His", sf.getDescription());
     assertEquals("var6", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
     assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
+            "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
   }
index 814d2d4..1faf3f2 100644 (file)
@@ -27,9 +27,10 @@ import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
 
 import jalview.analysis.SecStrConsensus.SimpleBP;
+import jalview.datamodel.SequenceFeature;
 import jalview.gui.JvOptionPane;
 
-import java.util.Vector;
+import java.util.List;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -48,7 +49,7 @@ public class RnaTest
   public void testGetSimpleBPs() throws WUSSParseException
   {
     String rna = "([{})]"; // JAL-1081 example
-    Vector<SimpleBP> bps = Rna.getSimpleBPs(rna);
+    List<SimpleBP> bps = Rna.getSimpleBPs(rna);
     assertEquals(3, bps.size());
 
     /*
@@ -313,4 +314,54 @@ public class RnaTest
               .valueOf((char) i) + " "));
     }
   }
+
+  @Test(groups = "Functional")
+  public void testGetHelixMap_oneHelix() throws WUSSParseException
+  {
+    String rna = ".(..[{.<..>}..].)";
+    SequenceFeature[] sfs = Rna.getHelixMap(rna);
+    assertEquals(4, sfs.length);
+
+    /*
+     * pairs are added in the order in which the closing bracket is found
+     * (see testGetSimpleBPs)
+     */
+    assertEquals(7, sfs[0].getBegin());
+    assertEquals(10, sfs[0].getEnd());
+    assertEquals("0", sfs[0].getFeatureGroup());
+    assertEquals(5, sfs[1].getBegin());
+    assertEquals(11, sfs[1].getEnd());
+    assertEquals("0", sfs[1].getFeatureGroup());
+    assertEquals(4, sfs[2].getBegin());
+    assertEquals(14, sfs[2].getEnd());
+    assertEquals("0", sfs[2].getFeatureGroup());
+    assertEquals(1, sfs[3].getBegin());
+    assertEquals(16, sfs[3].getEnd());
+    assertEquals("0", sfs[3].getFeatureGroup());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetHelixMap_twoHelices() throws WUSSParseException
+  {
+    String rna = ".([.)]..{.<}.>";
+    SequenceFeature[] sfs = Rna.getHelixMap(rna);
+    assertEquals(4, sfs.length);
+  
+    /*
+     * pairs are added in the order in which the closing bracket is found
+     * (see testGetSimpleBPs)
+     */
+    assertEquals(1, sfs[0].getBegin());
+    assertEquals(4, sfs[0].getEnd());
+    assertEquals("0", sfs[0].getFeatureGroup());
+    assertEquals(2, sfs[1].getBegin());
+    assertEquals(5, sfs[1].getEnd());
+    assertEquals("0", sfs[1].getFeatureGroup());
+    assertEquals(8, sfs[2].getBegin());
+    assertEquals(11, sfs[2].getEnd());
+    assertEquals("1", sfs[2].getFeatureGroup());
+    assertEquals(10, sfs[3].getBegin());
+    assertEquals(13, sfs[3].getEnd());
+    assertEquals("1", sfs[3].getFeatureGroup());
+  }
 }
index 11cb10c..9839ba0 100644 (file)
@@ -62,26 +62,25 @@ public class SeqsetUtilsTest
     AlignmentI al = new Alignment(sqset);
     al.setDataset(null);
     AlignmentI ds = al.getDataset();
-    SequenceFeature sf1 = new SequenceFeature("f1", "foo", "bleh", 2, 3,
-            "far"), sf2 = new SequenceFeature("f2", "foo", "bleh", 2, 3,
-            "far");
+    SequenceFeature sf1 = new SequenceFeature("f1", "foo", 2, 3, "far");
+    SequenceFeature sf2 = new SequenceFeature("f2", "foo", 2, 3, "far");
     ds.getSequenceAt(0).addSequenceFeature(sf1);
     Hashtable unq = SeqsetUtils.uniquify(sqset, true);
     SequenceI[] sqset2 = new SequenceI[] {
         new Sequence(sqset[0].getName(), sqset[0].getSequenceAsString()),
         new Sequence(sqset[1].getName(), sqset[1].getSequenceAsString()) };
-    Assert.assertTrue(sqset[0].getSequenceFeatures()[0] == sf1);
-    Assert.assertEquals(sqset2[0].getSequenceFeatures(), null);
+    Assert.assertSame(sqset[0].getSequenceFeatures().get(0), sf1);
+    Assert.assertTrue(sqset2[0].getSequenceFeatures().isEmpty());
     ds.getSequenceAt(0).addSequenceFeature(sf2);
-    Assert.assertEquals(sqset[0].getSequenceFeatures().length, 2);
+    Assert.assertEquals(sqset[0].getSequenceFeatures().size(), 2);
     SeqsetUtils.deuniquify(unq, sqset2);
     // explicitly test that original sequence features still exist because they
     // are on the shared dataset sequence
-    Assert.assertEquals(sqset[0].getSequenceFeatures().length, 2);
-    Assert.assertEquals(sqset2[0].getSequenceFeatures().length, 2);
-    Assert.assertTrue(sqset[0].getSequenceFeatures()[0] == sqset2[0]
-            .getSequenceFeatures()[0]);
-    Assert.assertTrue(sqset[0].getSequenceFeatures()[1] == sqset2[0]
-            .getSequenceFeatures()[1]);
+    Assert.assertEquals(sqset[0].getSequenceFeatures().size(), 2);
+    Assert.assertEquals(sqset2[0].getSequenceFeatures().size(), 2);
+    Assert.assertSame(sqset[0].getSequenceFeatures().get(0), sqset2[0]
+            .getSequenceFeatures().get(0));
+    Assert.assertSame(sqset[0].getSequenceFeatures().get(1), sqset2[0]
+            .getSequenceFeatures().get(1));
   }
 }
index 0577fae..16ca70d 100644 (file)
@@ -23,6 +23,7 @@ package jalview.analysis.scoremodels;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
+import jalview.api.analysis.ScoreModelI;
 import jalview.api.analysis.SimilarityParamsI;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
@@ -84,18 +85,18 @@ public class FeatureDistanceModelTest
       SequenceI ds = al.getSequenceAt(i).getDatasetSequence();
       if (sf1[i * 2] > 0)
       {
-        ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", "sf1",
-                sf1[i * 2], sf1[i * 2 + 1], "sf1"));
+        ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", sf1[i * 2],
+                sf1[i * 2 + 1], "sf1"));
       }
       if (sf2[i * 2] > 0)
       {
-        ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", "sf2",
-                sf2[i * 2], sf2[i * 2 + 1], "sf2"));
+        ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", sf2[i * 2],
+                sf2[i * 2 + 1], "sf2"));
       }
       if (sf3[i * 2] > 0)
       {
-        ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", "sf3",
-                sf3[i * 2], sf3[i * 2 + 1], "sf3"));
+        ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", sf3[i * 2],
+                sf3[i * 2 + 1], "sf3"));
       }
     }
     alf.setShowSeqFeatures(true);
@@ -113,12 +114,12 @@ public class FeatureDistanceModelTest
   public void testFeatureScoreModel() throws Exception
   {
     AlignFrame alf = getTestAlignmentFrame();
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
 
-    MatrixI dm = fsm.findDistances(
+    MatrixI dm = sm.findDistances(
             alf.getViewport().getAlignmentView(true),
             SimilarityParams.Jalview);
     assertEquals(dm.getValue(0, 2), 0d,
@@ -133,11 +134,11 @@ public class FeatureDistanceModelTest
     AlignFrame alf = getTestAlignmentFrame();
     // hiding first two columns shouldn't affect the tree
     alf.getViewport().hideColumns(0, 1);
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
-    MatrixI dm = fsm.findDistances(
+    MatrixI dm = sm.findDistances(
             alf.getViewport().getAlignmentView(true),
             SimilarityParams.Jalview);
     assertEquals(dm.getValue(0, 2), 0d,
@@ -153,11 +154,12 @@ public class FeatureDistanceModelTest
     // hide columns and check tree changes
     alf.getViewport().hideColumns(3, 4);
     alf.getViewport().hideColumns(0, 1);
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    // getName() can become static in Java 8
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
-    MatrixI dm = fsm.findDistances(
+    MatrixI dm = sm.findDistances(
             alf.getViewport().getAlignmentView(true),
             SimilarityParams.Jalview);
     assertEquals(
@@ -197,22 +199,22 @@ public class FeatureDistanceModelTest
     Assert.assertEquals(af.getFeatureRenderer().getDisplayedFeatureTypes()
             .size(), 1, "Should be just one feature type displayed");
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 1)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 1)
             .size(), 0);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 2)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 2)
             .size(), 1);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 3)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 3)
             .size(), 0);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 4)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 4)
             .size(), 0);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 5)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 5)
             .size(), 1);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 6)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 6)
             .size(), 0);
   }
 
@@ -252,13 +254,15 @@ public class FeatureDistanceModelTest
     alf.setShowSeqFeatures(true);
     alf.getFeatureRenderer().findAllFeatures(true);
 
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
 
-    MatrixI distances = fsm.findDistances(alf.getViewport()
-            .getAlignmentView(true), SimilarityParams.Jalview);
+    AlignmentView alignmentView = alf.getViewport()
+            .getAlignmentView(true);
+    MatrixI distances = sm.findDistances(alignmentView,
+            SimilarityParams.Jalview);
     assertEquals(distances.width(), 2);
     assertEquals(distances.height(), 2);
     assertEquals(distances.getValue(0, 0), 0d);
@@ -279,9 +283,10 @@ public class FeatureDistanceModelTest
     AlignViewport viewport = af.getViewport();
     AlignmentView view = viewport.getAlignmentView(false);
 
-    FeatureDistanceModel sm = new FeatureDistanceModel();
-    sm.configureFromAlignmentView(af.alignPanel);
-  
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            af.alignPanel);
+
     /*
      * feature distance model always normalises by region width
      * gap-gap is always included (but scores zero)
index 3223042..155f00e 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.commands;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 
 import jalview.commands.EditCommand.Action;
@@ -28,11 +29,15 @@ import jalview.commands.EditCommand.Edit;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 
+import java.util.List;
 import java.util.Map;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -45,6 +50,14 @@ import org.testng.annotations.Test;
  */
 public class EditCommandTest
 {
+  /*
+   * compute n(n+1)/2 e.g. 
+   * func(5) = 5 + 4 + 3 + 2 + 1 = 15
+   */
+  private static int func(int i)
+  {
+    return i * (i + 1) / 2;
+  }
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -639,4 +652,222 @@ public class EditCommandTest
     assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
     assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
   }
+
+  /**
+   * Test a cut action's relocation of sequence features
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures()
+  {
+    /*
+     * create sequence features before, after and overlapping
+     * a cut of columns/residues 4-7
+     */
+    SequenceI seq0 = seqs[0];
+    seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
+            null));
+    seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
+            0f, null));
+    seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
+            null));
+    seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
+            0f, null));
+    seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
+            null));
+
+    Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
+    EditCommand.cut(ec, new AlignmentI[] { al });
+
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sfs, true);
+
+    assertEquals(4, sfs.size()); // feature internal to cut has been deleted
+    SequenceFeature sf = sfs.get(0);
+    assertEquals("before", sf.getType());
+    assertEquals(1, sf.getBegin());
+    assertEquals(3, sf.getEnd());
+    sf = sfs.get(1);
+    assertEquals("overlap left", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(3, sf.getEnd()); // truncated by cut
+    sf = sfs.get(2);
+    assertEquals("overlap right", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(5, sf.getEnd()); // truncated by cut
+    sf = sfs.get(3);
+    assertEquals("after", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(6, sf.getEnd()); // shifted left by cut
+  }
+
+  /**
+   * Test a cut action's relocation of sequence features, with full coverage of
+   * all possible feature and cut locations for a 5-position ungapped sequence
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures_exhaustive()
+  {
+    /*
+     * create a sequence features on each subrange of 1-5
+     */
+    SequenceI seq0 = new Sequence("seq", "ABCDE");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    alignment.setDataset(null);
+    for (int from = 1; from <= seq0.getLength(); from++)
+    {
+      for (int to = from; to <= seq0.getLength(); to++)
+      {
+        String desc = String.format("%d-%d", from, to);
+        SequenceFeature sf = new SequenceFeature("test", desc, from, to,
+                0f,
+                null);
+        sf.setValue("from", Integer.valueOf(from));
+        sf.setValue("to", Integer.valueOf(to));
+        seq0.addSequenceFeature(sf);
+      }
+    }
+    // sanity check
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(func(5), sfs.size());
+
+    /*
+     * now perform all possible cuts of subranges of 1-5 (followed by Undo)
+     * and validate the resulting remaining sequence features!
+     */
+    SequenceI[] sqs = new SequenceI[] { seq0 };
+
+    // goal is to have this passing for all from/to values!!
+    // for (int from = 0; from < seq0.getLength(); from++)
+    // {
+    // for (int to = from; to < seq0.getLength(); to++)
+    for (int from = 1; from < 3; from++)
+    {
+      for (int to = 2; to < 3; to++)
+      {
+        testee.appendEdit(Action.CUT, sqs, from, (to - from + 1),
+                alignment, true);
+
+        sfs = seq0.getSequenceFeatures();
+
+        /*
+         * confirm the number of features has reduced by the
+         * number of features within the cut region i.e. by
+         * func(length of cut)
+         */
+        String msg = String.format("Cut %d-%d ", from, to);
+        if (to - from == 4)
+        {
+          // all columns cut
+          assertNull(sfs);
+        }
+        else
+        {
+          assertEquals(msg + "wrong number of features left", func(5)
+                  - func(to - from + 1), sfs.size());
+        }
+
+        /*
+         * inspect individual features
+         */
+        if (sfs != null)
+        {
+          for (SequenceFeature sf : sfs)
+          {
+            checkFeatureRelocation(sf, from + 1, to + 1);
+          }
+        }
+        /*
+         * undo ready for next cut
+         */
+        testee.undoCommand(new AlignmentI[] { alignment });
+        assertEquals(func(5), seq0.getSequenceFeatures().size());
+      }
+    }
+  }
+
+  /**
+   * Helper method to check a feature has been correctly relocated after a cut
+   * 
+   * @param sf
+   * @param from
+   *          start of cut (first residue cut)
+   * @param to
+   *          end of cut (last residue cut)
+   */
+  private void checkFeatureRelocation(SequenceFeature sf, int from, int to)
+  {
+    // TODO handle the gapped sequence case as well
+    int cutSize = to - from + 1;
+    int oldFrom = ((Integer) sf.getValue("from")).intValue();
+    int oldTo = ((Integer) sf.getValue("to")).intValue();
+
+    String msg = String.format(
+            "Feature %s relocated to %d-%d after cut of %d-%d",
+            sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
+    if (oldTo < from)
+    {
+      // before cut region so unchanged
+      assertEquals("1: " + msg, oldFrom, sf.getBegin());
+      assertEquals("2: " + msg, oldTo, sf.getEnd());
+    }
+    else if (oldFrom > to)
+    {
+      // follows cut region - shift by size of cut
+      assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin());
+      assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFrom < from && oldTo > to)
+    {
+      // feature encloses cut region - shrink it right
+      assertEquals("5: " + msg, oldFrom, sf.getBegin());
+      assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFrom < from)
+    {
+      // feature overlaps left side of cut region - truncated right
+      assertEquals("7: " + msg, from - 1, sf.getEnd());
+    }
+    else if (oldTo > to)
+    {
+      // feature overlaps right side of cut region - truncated left
+      assertEquals("8: " + msg, from, sf.getBegin());
+      assertEquals("9: " + msg, from + oldTo - to - 1, sf.getEnd());
+    }
+    else
+    {
+      // feature internal to cut - should have been deleted!
+      Assert.fail(msg + " - should have been deleted");
+    }
+  }
+
+  /**
+   * Test a cut action's relocation of sequence features
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_gappedWithFeatures()
+  {
+    /*
+     * create sequence features before, after and overlapping
+     * a cut of columns/residues 4-7
+     */
+    SequenceI seq0 = new Sequence("seq", "A-BCC");
+    seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f,
+            null));
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    // cut columns of A-B
+    Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3
+                                                                  // base 0
+    EditCommand.cut(ec, new AlignmentI[] { alignment });
+  
+    /*
+     * feature on CC(3-4) should now be on CC(1-2)
+     */
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
+    assertEquals(1, sf.getBegin());
+    assertEquals(2, sf.getEnd());
+
+    // TODO add further cases including Undo - see JAL-2541
+  }
 }
index 1cfa771..4b5d096 100644 (file)
@@ -1300,4 +1300,25 @@ public class AlignmentTest
     AlignmentI alignment = new Alignment(new SequenceI[] { seq });
     alignment.setDataset(alignment);
   }
+
+  @Test(groups = "Functional")
+  public void testAppend()
+  {
+    SequenceI seq = new Sequence("seq1", "FRMLPSRT-A--L-");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq });
+    alignment.setGapCharacter('-');
+    SequenceI seq2 = new Sequence("seq1", "KP..L.FQII.");
+    AlignmentI alignment2 = new Alignment(new SequenceI[] { seq2 });
+    alignment2.setGapCharacter('.');
+
+    alignment.append(alignment2);
+
+    assertEquals('-', alignment.getGapCharacter());
+    assertSame(seq, alignment.getSequenceAt(0));
+    assertEquals("KP--L-FQII-", alignment.getSequenceAt(1)
+            .getSequenceAsString());
+
+    // todo test coverage for annotations, mappings, groups,
+    // hidden sequences, properties
+  }
 }
index ab25aa6..89169d6 100644 (file)
@@ -121,7 +121,7 @@ public class SeqCigarTest
     /*
      * TODO: can we add assertions to the sysouts that follow?
      */
-    System.out.println("Original sequence align:\n" + sub_gapped_s
+    System.out.println("\nOriginal sequence align:\n" + sub_gapped_s
             + "\nReconstructed window from 8 to 48\n" + "XXXXXXXX"
             + sub_se_gp.getSequenceString('-') + "..." + "\nCigar String:"
             + sub_se_gp.getCigarstring() + "\n");
@@ -193,7 +193,8 @@ public class SeqCigarTest
     SequenceI gen_sgapped_s = gen_sgapped.getSeq('-');
     // assertEquals("Couldn't reconstruct sequence", s_gapped.getSequence(),
     // gen_sgapped_s);
-    if (!gen_sgapped_s.getSequence().equals(s_gapped.getSequence()))
+    if (!gen_sgapped_s.getSequenceAsString().equals(
+            s_gapped.getSequenceAsString()))
     {
       // TODO: investigate errors reported here, to allow full conversion to
       // passing JUnit assertion form
index 2da8918..fbeb365 100644 (file)
@@ -42,7 +42,7 @@ public class SequenceFeatureTest
   }
 
   @Test(groups = { "Functional" })
-  public void testCopyConstructor()
+  public void testCopyConstructors()
   {
     SequenceFeature sf1 = new SequenceFeature("type", "desc", 22, 33,
             12.5f, "group");
@@ -56,10 +56,41 @@ public class SequenceFeatureTest
     assertEquals("desc", sf2.getDescription());
     assertEquals(22, sf2.getBegin());
     assertEquals(33, sf2.getEnd());
+    assertEquals(12.5f, sf2.getScore());
     assertEquals("+", sf2.getValue("STRAND"));
     assertEquals("Testing", sf2.getValue("Note"));
     // shallow clone of otherDetails map - contains the same object values!
     assertSame(count, sf2.getValue("Count"));
+
+    /*
+     * copy constructor modifying begin/end/group/score
+     */
+    SequenceFeature sf3 = new SequenceFeature(sf1, 11, 14, "group2", 17.4f);
+    assertEquals("type", sf3.getType());
+    assertEquals("desc", sf3.getDescription());
+    assertEquals(11, sf3.getBegin());
+    assertEquals(14, sf3.getEnd());
+    assertEquals(17.4f, sf3.getScore());
+    assertEquals("+", sf3.getValue("STRAND"));
+    assertEquals("Testing", sf3.getValue("Note"));
+    // shallow clone of otherDetails map - contains the same object values!
+    assertSame(count, sf3.getValue("Count"));
+
+    /*
+     * copy constructor modifying type/begin/end/group/score
+     */
+    SequenceFeature sf4 = new SequenceFeature(sf1, "Disulfide bond", 12,
+            15, "group3", -9.1f);
+    assertEquals("Disulfide bond", sf4.getType());
+    assertTrue(sf4.isContactFeature());
+    assertEquals("desc", sf4.getDescription());
+    assertEquals(12, sf4.getBegin());
+    assertEquals(15, sf4.getEnd());
+    assertEquals(-9.1f, sf4.getScore());
+    assertEquals("+", sf4.getValue("STRAND"));
+    assertEquals("Testing", sf4.getValue("Note"));
+    // shallow clone of otherDetails map - contains the same object values!
+    assertSame(count, sf4.getValue("Count"));
   }
 
   /**
@@ -123,51 +154,61 @@ public class SequenceFeatureTest
     assertEquals(sf1.hashCode(), sf2.hashCode());
 
     // changing type breaks equals:
-    String restores = sf2.getType();
-    sf2.setType("Type");
-    assertFalse(sf1.equals(sf2));
-    sf2.setType(restores);
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 22, 33,
+            12.5f, "group");
+    SequenceFeature sf4 = new SequenceFeature("Type", "desc", 22, 33,
+            12.5f, "group");
+    assertFalse(sf3.equals(sf4));
 
     // changing description breaks equals:
-    restores = sf2.getDescription();
+    String restores = sf2.getDescription();
     sf2.setDescription("Desc");
     assertFalse(sf1.equals(sf2));
     sf2.setDescription(restores);
 
     // changing score breaks equals:
     float restoref = sf2.getScore();
-    sf2.setScore(12.4f);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), 10f);
     assertFalse(sf1.equals(sf2));
-    sf2.setScore(restoref);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), restoref);
 
     // NaN doesn't match a number
     restoref = sf2.getScore();
-    sf2.setScore(Float.NaN);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), Float.NaN);
     assertFalse(sf1.equals(sf2));
 
     // NaN matches NaN
-    sf1.setScore(Float.NaN);
+    sf1 = new SequenceFeature(sf1, sf1.getBegin(), sf1.getEnd(),
+            sf1.getFeatureGroup(), Float.NaN);
     assertTrue(sf1.equals(sf2));
-    sf1.setScore(restoref);
-    sf2.setScore(restoref);
+    sf1 = new SequenceFeature(sf1, sf1.getBegin(), sf1.getEnd(),
+            sf1.getFeatureGroup(), restoref);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), restoref);
 
     // changing start position breaks equals:
     int restorei = sf2.getBegin();
-    sf2.setBegin(21);
+    sf2 = new SequenceFeature(sf2, 21, sf2.getEnd(), sf2.getFeatureGroup(), sf2.getScore());
     assertFalse(sf1.equals(sf2));
-    sf2.setBegin(restorei);
+    sf2 = new SequenceFeature(sf2, restorei, sf2.getEnd(),
+            sf2.getFeatureGroup(), sf2.getScore());
 
     // changing end position breaks equals:
     restorei = sf2.getEnd();
-    sf2.setEnd(32);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), 32,
+            sf2.getFeatureGroup(), sf2.getScore());
     assertFalse(sf1.equals(sf2));
-    sf2.setEnd(restorei);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), restorei,
+            sf2.getFeatureGroup(), sf2.getScore());
 
     // changing feature group breaks equals:
     restores = sf2.getFeatureGroup();
-    sf2.setFeatureGroup("Group");
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), "Group", sf2.getScore());
     assertFalse(sf1.equals(sf2));
-    sf2.setFeatureGroup(restores);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), restores, sf2.getScore());
 
     // changing ID breaks equals:
     restores = (String) sf2.getValue("ID");
@@ -215,17 +256,21 @@ public class SequenceFeatureTest
     SequenceFeature sf = new SequenceFeature("type", "desc", 22, 33, 12.5f,
             "group");
     assertFalse(sf.isContactFeature());
-    sf.setType("");
+    sf = new SequenceFeature("", "desc", 22, 33, 12.5f, "group");
     assertFalse(sf.isContactFeature());
-    sf.setType(null);
+    sf = new SequenceFeature(null, "desc", 22, 33, 12.5f, "group");
     assertFalse(sf.isContactFeature());
-    sf.setType("Disulfide Bond");
+    sf = new SequenceFeature("Disulfide Bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
-    sf.setType("disulfide bond");
+    sf = new SequenceFeature("disulfide bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
-    sf.setType("Disulphide Bond");
+    sf = new SequenceFeature("Disulphide Bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
-    sf.setType("disulphide bond");
+    sf = new SequenceFeature("disulphide bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
   }
 }
index a52f0a2..6844072 100644 (file)
@@ -23,11 +23,13 @@ package jalview.datamodel;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertNotSame;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
-import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import jalview.commands.EditCommand;
+import jalview.commands.EditCommand.Action;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.gui.JvOptionPane;
 import jalview.util.MapList;
@@ -39,6 +41,8 @@ import java.util.BitSet;
 import java.util.List;
 import java.util.Vector;
 
+import junit.extensions.PA;
+
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
@@ -102,15 +106,6 @@ public class SequenceTest
     // change sequence, should trigger an update of cached result
     sq.setSequence("ASDFASDFADSF");
     assertTrue(sq.isProtein());
-    /*
-     * in situ change of sequence doesn't change hashcode :-O
-     * (sequence should not expose internal implementation)
-     */
-    for (int i = 0; i < sq.getSequence().length; i++)
-    {
-      sq.getSequence()[i] = "acgtu".charAt(i % 5);
-    }
-    assertTrue(sq.isProtein()); // but it isn't
   }
 
   @Test(groups = { "Functional" })
@@ -235,82 +230,353 @@ public class SequenceTest
   @Test(groups = { "Functional" })
   public void testFindIndex()
   {
+    /* 
+     * call sequenceChanged() after each test to invalidate any cursor,
+     * forcing the 1-arg findIndex to be executed
+     */
     SequenceI sq = new Sequence("test", "ABCDEF");
     assertEquals(0, sq.findIndex(0));
+    sq.sequenceChanged();
     assertEquals(1, sq.findIndex(1));
+    sq.sequenceChanged();
     assertEquals(5, sq.findIndex(5));
+    sq.sequenceChanged();
     assertEquals(6, sq.findIndex(6));
+    sq.sequenceChanged();
     assertEquals(6, sq.findIndex(9));
 
-    sq = new Sequence("test", "-A--B-C-D-E-F--");
-    assertEquals(2, sq.findIndex(1));
-    assertEquals(5, sq.findIndex(2));
-    assertEquals(7, sq.findIndex(3));
+    sq = new Sequence("test/8-13", "-A--B-C-D-E-F--");
+    assertEquals(2, sq.findIndex(8));
+    sq.sequenceChanged();
+    assertEquals(5, sq.findIndex(9));
+    sq.sequenceChanged();
+    assertEquals(7, sq.findIndex(10));
 
     // before start returns 0
+    sq.sequenceChanged();
     assertEquals(0, sq.findIndex(0));
+    sq.sequenceChanged();
     assertEquals(0, sq.findIndex(-1));
 
     // beyond end returns last residue column
+    sq.sequenceChanged();
     assertEquals(13, sq.findIndex(99));
-
   }
 
   /**
-   * Tests for the method that returns a dataset sequence position (base 1) for
+   * Tests for the method that returns a dataset sequence position (start..) for
    * an aligned column position (base 0).
    */
   @Test(groups = { "Functional" })
   public void testFindPosition()
   {
-    SequenceI sq = new Sequence("test", "ABCDEF");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(6, sq.findPosition(5));
+    /* 
+     * call sequenceChanged() after each test to invalidate any cursor,
+     * forcing the 1-arg findPosition to be executed
+     */
+    SequenceI sq = new Sequence("test/8-13", "ABCDEF");
+    assertEquals(8, sq.findPosition(0));
+    // Sequence should now hold a cursor at [8, 0]
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    int token = (int) PA.getValue(sq, "changeCount");
+    assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+
+    sq.sequenceChanged();
+
+    /*
+     * find F13 at column offset 5, cursor should update to [13, 6]
+     * endColumn is found and saved in cursor
+     */
+    assertEquals(13, sq.findPosition(5));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(++token, (int) PA.getValue(sq, "changeCount"));
+    assertEquals(new SequenceCursor(sq, 13, 6, token), cursor);
+    assertEquals("test:Pos13:Col6:startCol1:endCol6:tok1",
+            PA.getValue(sq, "cursor").toString());
+
     // assertEquals(-1, seq.findPosition(6)); // fails
 
-    sq = new Sequence("test", "AB-C-D--");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(2, sq.findPosition(1));
+    sq = new Sequence("test/8-11", "AB-C-D--");
+    token = (int) PA.getValue(sq, "changeCount"); // 0
+    assertEquals(8, sq.findPosition(0));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(9, sq.findPosition(1));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
     // gap position 'finds' residue to the right (not the left as per javadoc)
-    assertEquals(3, sq.findPosition(2));
-    assertEquals(3, sq.findPosition(3));
-    assertEquals(4, sq.findPosition(4));
-    assertEquals(4, sq.findPosition(5));
+    // cursor is set to the last residue position found [B 2]
+    assertEquals(10, sq.findPosition(2));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(3));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    // column[4] is the gap after C - returns D11
+    // cursor is set to [C 4]
+    assertEquals(11, sq.findPosition(4));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(5)); // D
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+    // lastCol has been found and saved in the cursor
+    assertEquals("test:Pos11:Col6:startCol1:endCol6:tok5",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
     // returns 1 more than sequence length if off the end ?!?
-    assertEquals(5, sq.findPosition(6));
-    assertEquals(5, sq.findPosition(7));
+    assertEquals(12, sq.findPosition(6));
 
-    sq = new Sequence("test", "--AB-C-DEF--");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(1, sq.findPosition(1));
-    assertEquals(1, sq.findPosition(2));
-    assertEquals(2, sq.findPosition(3));
-    assertEquals(3, sq.findPosition(4));
-    assertEquals(3, sq.findPosition(5));
-    assertEquals(4, sq.findPosition(6));
-    assertEquals(4, sq.findPosition(7));
-    assertEquals(5, sq.findPosition(8));
-    assertEquals(6, sq.findPosition(9));
-    assertEquals(7, sq.findPosition(10));
-    assertEquals(7, sq.findPosition(11));
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(7));
+
+    /*
+     * first findPosition should also set firstResCol in cursor
+     */
+    sq = new Sequence("test/8-13", "--AB-C-DEF--");
+    assertEquals(8, sq.findPosition(0));
+    assertNull(PA.getValue(sq, "cursor"));
+
+    sq.sequenceChanged();
+    assertEquals(8, sq.findPosition(1));
+    assertNull(PA.getValue(sq, "cursor"));
+
+    sq.sequenceChanged();
+    assertEquals(8, sq.findPosition(2));
+    assertEquals("test:Pos8:Col3:startCol3:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(9, sq.findPosition(3));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    // column[4] is a gap, returns next residue pos (C10)
+    // cursor is set to last residue found [B]
+    assertEquals(10, sq.findPosition(4));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(5));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok5",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    // column[6] is a gap, returns next residue pos (D11)
+    // cursor is set to last residue found [C]
+    assertEquals(11, sq.findPosition(6));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok6",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(7));
+    assertEquals("test:Pos11:Col8:startCol3:endCol0:tok7",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8));
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok8",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * when the last residue column is found, it is set in the cursor
+     */
+    sq.sequenceChanged();
+    assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok9",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(10));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok10",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * findPosition for column beyond sequence length
+     * returns 1 more than last residue position
+     */
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(11));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok11",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(99));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok12",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * gapped sequence ending in non-gap
+     */
+    sq = new Sequence("test/8-13", "--AB-C-DEF");
+    assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    // sequenceChanged() invalidates cursor.lastResidueColumn
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok1",
+            cursor.toString());
+    // findPosition with cursor accepts base 1 column values
+    assertEquals(13, ((Sequence) sq).findPosition(10, cursor));
+    assertEquals(13, sq.findPosition(9)); // F13
+    // lastResidueColumn has now been found and saved in cursor
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
   }
 
   @Test(groups = { "Functional" })
   public void testDeleteChars()
   {
+    /*
+     * internal delete
+     */
+    SequenceI sq = new Sequence("test", "ABCDEF");
+    assertNull(PA.getValue(sq, "datasetSequence"));
+    assertEquals(1, sq.getStart());
+    assertEquals(6, sq.getEnd());
+    sq.deleteChars(2, 3);
+    assertEquals("ABDEF", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(5, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+
+    /*
+     * delete at start
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.deleteChars(0, 2);
+    assertEquals("CDEF", sq.getSequenceAsString());
+    assertEquals(3, sq.getStart());
+    assertEquals(6, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+
+    /*
+     * delete at end
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.deleteChars(4, 6);
+    assertEquals("ABCD", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(4, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDeleteChars_withDbRefsAndFeatures()
+  {
+    /*
+     * internal delete - new dataset sequence created
+     * gets a copy of any dbrefs
+     */
     SequenceI sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    DBRefEntry dbr1 = new DBRefEntry("Uniprot", "0", "a123");
+    sq.addDBRef(dbr1);
+    Object ds = PA.getValue(sq, "datasetSequence");
+    assertNotNull(ds);
     assertEquals(1, sq.getStart());
     assertEquals(6, sq.getEnd());
     sq.deleteChars(2, 3);
     assertEquals("ABDEF", sq.getSequenceAsString());
     assertEquals(1, sq.getStart());
     assertEquals(5, sq.getEnd());
+    Object newDs = PA.getValue(sq, "datasetSequence");
+    assertNotNull(newDs);
+    assertNotSame(ds, newDs);
+    assertNotNull(sq.getDBRefs());
+    assertEquals(1, sq.getDBRefs().length);
+    assertNotSame(dbr1, sq.getDBRefs()[0]);
+    assertEquals(dbr1, sq.getDBRefs()[0]);
 
+    /*
+     * internal delete with sequence features
+     * (failure case for JAL-2541)
+     */
     sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            "CathGroup");
+    sq.addSequenceFeature(sf1);
+    ds = PA.getValue(sq, "datasetSequence");
+    assertNotNull(ds);
+    assertEquals(1, sq.getStart());
+    assertEquals(6, sq.getEnd());
+    sq.deleteChars(2, 4);
+    assertEquals("ABEF", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(4, sq.getEnd());
+    newDs = PA.getValue(sq, "datasetSequence");
+    assertNotNull(newDs);
+    assertNotSame(ds, newDs);
+    List<SequenceFeature> sfs = sq.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    assertNotSame(sf1, sfs.get(0));
+    assertEquals(sf1, sfs.get(0));
+
+    /*
+     * delete at start - no new dataset sequence created
+     * any sequence features remain as before
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    ds = PA.getValue(sq, "datasetSequence");
+    sf1 = new SequenceFeature("Cath", "desc", 2, 4, 2f, "CathGroup");
+    sq.addSequenceFeature(sf1);
     sq.deleteChars(0, 2);
     assertEquals("CDEF", sq.getSequenceAsString());
     assertEquals(3, sq.getStart());
     assertEquals(6, sq.getEnd());
+    assertSame(ds, PA.getValue(sq, "datasetSequence"));
+    sfs = sq.getSequenceFeatures();
+    assertNotNull(sfs);
+    assertEquals(1, sfs.size());
+    assertSame(sf1, sfs.get(0));
+
+    /*
+     * delete at end - no new dataset sequence created
+     * any dbrefs remain as before
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    ds = PA.getValue(sq, "datasetSequence");
+    dbr1 = new DBRefEntry("Uniprot", "0", "a123");
+    sq.addDBRef(dbr1);
+    sq.deleteChars(4, 6);
+    assertEquals("ABCD", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(4, sq.getEnd());
+    assertSame(ds, PA.getValue(sq, "datasetSequence"));
+    assertNotNull(sq.getDBRefs());
+    assertEquals(1, sq.getDBRefs().length);
+    assertSame(dbr1, sq.getDBRefs()[0]);
   }
 
   @Test(groups = { "Functional" })
@@ -348,16 +614,16 @@ public class SequenceTest
     SequenceI sq = new Sequence("test", "GATCAT");
     sq.createDatasetSequence();
 
-    assertNull(sq.getSequenceFeatures());
+    assertTrue(sq.getSequenceFeatures().isEmpty());
 
     /*
      * SequenceFeature on sequence
      */
-    SequenceFeature sf = new SequenceFeature();
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 4, 2f, null);
     sq.addSequenceFeature(sf);
-    SequenceFeature[] sfs = sq.getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    assertSame(sf, sfs[0]);
+    List<SequenceFeature> sfs = sq.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    assertSame(sf, sfs.get(0));
 
     /*
      * SequenceFeature on sequence and dataset sequence; returns that on
@@ -366,18 +632,19 @@ public class SequenceTest
      * Note JAL-2046: spurious: we have no use case for this at the moment.
      * This test also buggy - as sf2.equals(sf), no new feature is added
      */
-    SequenceFeature sf2 = new SequenceFeature();
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            null);
     sq.getDatasetSequence().addSequenceFeature(sf2);
     sfs = sq.getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    assertSame(sf, sfs[0]);
+    assertEquals(1, sfs.size());
+    assertSame(sf, sfs.get(0));
 
     /*
      * SequenceFeature on dataset sequence only
      * Note JAL-2046: spurious: we have no use case for setting a non-dataset sequence's feature array to null at the moment.
      */
     sq.setSequenceFeatures(null);
-    assertNull(sq.getDatasetSequence().getSequenceFeatures());
+    assertTrue(sq.getDatasetSequence().getSequenceFeatures().isEmpty());
 
     /*
      * Corrupt case - no SequenceFeature, dataset's dataset is the original
@@ -398,7 +665,7 @@ public class SequenceTest
       assertTrue(e.getMessage().toLowerCase()
               .contains("implementation error"));
     }
-    assertNull(sq.getSequenceFeatures());
+    assertTrue(sq.getSequenceFeatures().isEmpty());
   }
 
   /**
@@ -448,11 +715,23 @@ public class SequenceTest
   public void testCreateDatasetSequence()
   {
     SequenceI sq = new Sequence("my", "ASDASD");
+    sq.addSequenceFeature(new SequenceFeature("type", "desc", 1, 10, 1f,
+            "group"));
+    sq.addDBRef(new DBRefEntry("source", "version", "accession"));
     assertNull(sq.getDatasetSequence());
+    assertNotNull(PA.getValue(sq, "sequenceFeatureStore"));
+    assertNotNull(PA.getValue(sq, "dbrefs"));
+
     SequenceI rds = sq.createDatasetSequence();
     assertNotNull(rds);
     assertNull(rds.getDatasetSequence());
-    assertEquals(sq.getDatasetSequence(), rds);
+    assertSame(sq.getDatasetSequence(), rds);
+
+    // sequence features and dbrefs transferred to dataset sequence
+    assertNull(PA.getValue(sq, "sequenceFeatureStore"));
+    assertNull(PA.getValue(sq, "dbrefs"));
+    assertNotNull(PA.getValue(rds, "sequenceFeatureStore"));
+    assertNotNull(PA.getValue(rds, "dbrefs"));
   }
 
   /**
@@ -559,12 +838,9 @@ public class SequenceTest
     assertEquals("CD", derived.getSequenceAsString());
     assertSame(sq.getDatasetSequence(), derived.getDatasetSequence());
 
-    assertNull(sq.sequenceFeatures);
-    assertNull(derived.sequenceFeatures);
     // derived sequence should access dataset sequence features
     assertNotNull(sq.getSequenceFeatures());
-    assertArrayEquals(sq.getSequenceFeatures(),
-            derived.getSequenceFeatures());
+    assertEquals(sq.getSequenceFeatures(), derived.getSequenceFeatures());
 
     /*
      *  verify we have primary db refs *just* for PDB IDs with associated
@@ -694,18 +970,18 @@ public class SequenceTest
     assertEquals(anns[0].score, seq1.getAnnotation()[0].score);
 
     // copy has a copy of the sequence feature:
-    SequenceFeature[] sfs = copy.getSequenceFeatures();
-    assertEquals(1, sfs.length);
+    List<SequenceFeature> sfs = copy.getSequenceFeatures();
+    assertEquals(1, sfs.size());
     if (seq1.getDatasetSequence() != null
             && copy.getDatasetSequence() == seq1.getDatasetSequence())
     {
-      assertTrue(sfs[0] == seq1.getSequenceFeatures()[0]);
+      assertSame(sfs.get(0), seq1.getSequenceFeatures().get(0));
     }
     else
     {
-      assertFalse(sfs[0] == seq1.getSequenceFeatures()[0]);
+      assertNotSame(sfs.get(0), seq1.getSequenceFeatures().get(0));
     }
-    assertTrue(sfs[0].equals(seq1.getSequenceFeatures()[0]));
+    assertEquals(sfs.get(0), seq1.getSequenceFeatures().get(0));
 
     // copy has a copy of the PDB entry
     Vector<PDBEntry> pdbs = copy.getAllPDBEntries();
@@ -724,6 +1000,36 @@ public class SequenceTest
     assertEquals(' ', sq.getCharAt(-1));
   }
 
+  @Test(groups = { "Functional" })
+  public void testAddSequenceFeatures()
+  {
+    SequenceI sq = new Sequence("", "abcde");
+    // type may not be null
+    assertFalse(sq.addSequenceFeature(new SequenceFeature(null, "desc", 4,
+            8, 0f, null)));
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, 0f, null)));
+    // can't add a duplicate feature
+    assertFalse(sq.addSequenceFeature(new SequenceFeature("Cath", "desc",
+            4, 8, 0f, null)));
+    // can add a different feature
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Scop", "desc", 4,
+            8, 0f, null))); // different type
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath",
+            "description", 4, 8, 0f, null)));// different description
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 3,
+            8, 0f, null))); // different start position
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            9, 0f, null))); // different end position
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, 1f, null))); // different score
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, Float.NaN, null))); // score NaN
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, 0f, "Metal"))); // different group
+    assertEquals(8, sq.getFeatures().getAllFeatures().size());
+  }
+
   /**
    * Tests for adding (or updating) dbrefs
    * 
@@ -1023,4 +1329,347 @@ public class SequenceTest
     seq2.createDatasetSequence();
     seq.setDatasetSequence(seq2);
   }
+
+  @Test(groups = { "Functional" })
+  public void testFindFeatures()
+  {
+    SequenceI sq = new Sequence("test/8-16", "-ABC--DEF--GHI--");
+    sq.createDatasetSequence();
+
+    assertTrue(sq.findFeatures(1, 99).isEmpty());
+
+    // add non-positional feature
+    SequenceFeature sf0 = new SequenceFeature("Cath", "desc", 0, 0, 2f,
+            null);
+    sq.addSequenceFeature(sf0);
+    // add feature on BCD
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 9, 11, 2f,
+            null);
+    sq.addSequenceFeature(sfBCD);
+    // add feature on DE
+    SequenceFeature sfDE = new SequenceFeature("Cath", "desc", 11, 12, 2f,
+            null);
+    sq.addSequenceFeature(sfDE);
+    // add contact feature at [B, H]
+    SequenceFeature sfContactBH = new SequenceFeature("Disulphide bond",
+            "desc", 9, 15, 2f, null);
+    sq.addSequenceFeature(sfContactBH);
+    // add contact feature at [F, G]
+    SequenceFeature sfContactFG = new SequenceFeature("Disulfide Bond",
+            "desc", 13, 14, 2f, null);
+    sq.addSequenceFeature(sfContactFG);
+    // add single position feature at [I]
+    SequenceFeature sfI = new SequenceFeature("Disulfide Bond",
+            "desc", 16, 16, null);
+    sq.addSequenceFeature(sfI);
+
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+
+    // columns 1-6 (-ABC--) includes BCD and B/H feature but not DE
+    found = sq.findFeatures(1, 6);
+    assertEquals(2, found.size());
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfContactBH));
+
+    // columns 5-6 (--) includes (enclosing) BCD but not (contact) B/H feature
+    found = sq.findFeatures(5, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfBCD));
+
+    // columns 7-10 (DEF-) includes BCD, DE, F/G but not B/H feature
+    found = sq.findFeatures(7, 10);
+    assertEquals(3, found.size());
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfDE));
+    assertTrue(found.contains(sfContactFG));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+
+    // columns 14-14 (I) should find variant feature
+    found = sq.findFeatures(14, 14);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfI));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindIndex_withCursor()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+
+    // find F given A
+    assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0)));
+
+    // find A given F
+    assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0)));
+
+    // find C given C
+    assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0)));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPosition_withCursor()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+  
+    // find F pos given A - lastCol gets set in cursor
+    assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // find A pos given F - first residue column is saved in cursor
+    assertEquals(8, sq.findPosition(2, new SequenceCursor(sq, 13, 10, 0)));
+    assertEquals("test:Pos8:Col2:startCol2:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+  
+    // find C pos given C (neither startCol nor endCol is set)
+    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 10, 6, 0)));
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // now the grey area - what residue position for a gapped column? JAL-2562
+
+    // find 'residue' for column 3 given cursor for D (so working left)
+    // returns B9; cursor is updated to [B 5]
+    assertEquals(9, sq.findPosition(3, new SequenceCursor(sq, 11, 7, 0)));
+    assertEquals("test:Pos9:Col5:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // find 'residue' for column 8 given cursor for D (so working right)
+    // returns E12; cursor is updated to [D 7]
+    assertEquals(12, sq.findPosition(8, new SequenceCursor(sq, 11, 7, 0)));
+    assertEquals("test:Pos11:Col7:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // find 'residue' for column 12 given cursor for B
+    // returns 1 more than last residue position; cursor is updated to [F 10]
+    // lastCol position is saved in cursor
+    assertEquals(14, sq.findPosition(12, new SequenceCursor(sq, 9, 5, 0)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * findPosition for column beyond length of sequence
+     * returns 1 more than the last residue position
+     * cursor is set to last real residue position [F 10]
+     */
+    assertEquals(14, sq.findPosition(99, new SequenceCursor(sq, 8, 2, 0)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * and the case without a trailing gap
+     */
+    sq = new Sequence("test/8-13", "-A--BCD-EF");
+    // first find C from A
+    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 8, 2, 0)));
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0",
+            cursor.toString());
+    // now 'find' 99 from C
+    // cursor is set to [F 10] and saved lastCol
+    assertEquals(14, sq.findPosition(99, cursor));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+  }
+
+  @Test
+  public void testIsValidCursor()
+  {
+    Sequence sq = new Sequence("Seq", "ABC--DE-F", 8, 13);
+    assertFalse(sq.isValidCursor(null));
+
+    /*
+     * cursor is valid if it has valid sequence ref and changeCount token
+     * and positions within the range of the sequence
+     */
+    int changeCount = (int) PA.getValue(sq, "changeCount");
+    SequenceCursor cursor = new SequenceCursor(sq, 13, 1, changeCount);
+    assertTrue(sq.isValidCursor(cursor));
+
+    /*
+     * column position outside [0 - length] is rejected
+     */
+    cursor = new SequenceCursor(sq, 13, -1, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 13, 10, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 7, 8, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 14, 2, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+
+    /*
+     * wrong sequence is rejected
+     */
+    cursor = new SequenceCursor(null, 13, 1, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(new Sequence("Seq", "abc"), 13, 1,
+            changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+
+    /*
+     * wrong token value is rejected
+     */
+    cursor = new SequenceCursor(sq, 13, 1, changeCount + 1);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 13, 1, changeCount - 1);
+    assertFalse(sq.isValidCursor(cursor));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPosition_withCursorAndEdits()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+  
+    // find F pos given A
+    assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
+    int token = (int) PA.getValue(sq, "changeCount"); // 0
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+
+    /*
+     * setSequence should invalidate the cursor cached by the sequence
+     */
+    sq.setSequence("-A-BCD-EF---"); // one gap removed
+    assertEquals(8, sq.getStart()); // sanity check
+    assertEquals(11, sq.findPosition(5)); // D11
+    // cursor should now be at [D 6]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+
+    /*
+     * deleteChars should invalidate the cached cursor
+     */
+    sq.deleteChars(2, 5); // delete -BC
+    assertEquals("-AD-EF---", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart()); // sanity check
+    assertEquals(10, sq.findPosition(4)); // E10
+    // cursor should now be at [E 5]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 5, ++token), cursor);
+
+    /*
+     * Edit to insert gaps should invalidate the cached cursor
+     * insert 2 gaps at column[3] to make -AD---EF---
+     */
+    SequenceI[] seqs = new SequenceI[] { sq };
+    AlignmentI al = new Alignment(seqs);
+    new EditCommand().appendEdit(Action.INSERT_GAP, seqs, 3, 2, al, true);
+    assertEquals("-AD---EF---", sq.getSequenceAsString());
+    assertEquals(10, sq.findPosition(4)); // E10
+    // cursor should now be at [D 3]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 3, ++token), cursor);
+
+    /*
+     * insertCharAt should invalidate the cached cursor
+     * insert CC at column[4] to make -AD-CC--EF---
+     */
+    sq.insertCharAt(4, 2, 'C');
+    assertEquals("-AD-CC--EF---", sq.getSequenceAsString());
+    assertEquals(13, sq.findPosition(9)); // F13
+    // cursor should now be at [F 10]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetSequence()
+  {
+    String seqstring = "-A--BCD-EF--";
+    Sequence sq = new Sequence("test/8-13", seqstring);
+    sq.createDatasetSequence();
+    assertTrue(Arrays.equals(sq.getSequence(), seqstring.toCharArray()));
+    assertTrue(Arrays.equals(sq.getDatasetSequence().getSequence(),
+            "ABCDEF".toCharArray()));
+
+    // verify a copy of the sequence array is returned
+    char[] theSeq = (char[]) PA.getValue(sq, "sequence");
+    assertNotSame(theSeq, sq.getSequence());
+    theSeq = (char[]) PA.getValue(sq.getDatasetSequence(), "sequence");
+    assertNotSame(theSeq, sq.getDatasetSequence().getSequence());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testReplace()
+  {
+    String seqstring = "-A--BCD-EF--";
+    SequenceI sq = new Sequence("test/8-13", seqstring);
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('A', 'A')); // same char
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('X', 'Y')); // not there
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(1, sq.replace('A', 'K'));
+    assertEquals("-K--BCD-EF--", sq.getSequenceAsString());
+    assertEquals(1, PA.getValue(sq, "changeCount"));
+
+    assertEquals(6, sq.replace('-', '.'));
+    assertEquals(".K..BCD.EF..", sq.getSequenceAsString());
+    assertEquals(2, PA.getValue(sq, "changeCount"));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPositions()
+  {
+    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
+
+    /*
+     * invalid inputs
+     */
+    assertNull(sq.findPositions(6, 5));
+    assertNull(sq.findPositions(0, 5));
+    assertNull(sq.findPositions(-1, 5));
+
+    /*
+     * all gapped ranges
+     */
+    assertNull(sq.findPositions(1, 1)); // 1-based columns
+    assertNull(sq.findPositions(5, 5));
+    assertNull(sq.findPositions(5, 6));
+    assertNull(sq.findPositions(5, 7));
+
+    /*
+     * all ungapped ranges
+     */
+    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
+    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
+    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
+    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
+
+    /*
+     * gap to ungapped range
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
+
+    /*
+     * ungapped to gapped range
+     */
+    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
+    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
+
+    /*
+     * ungapped to ungapped enclosing gaps
+     */
+    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
+    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
+
+    /*
+     * gapped to gapped enclosing ungapped
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
+    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
+    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
+  }
 }
diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java
new file mode 100644 (file)
index 0000000..db21c2f
--- /dev/null
@@ -0,0 +1,911 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.testng.annotations.Test;
+
+public class FeatureStoreTest
+{
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_nonNested()
+  {
+    FeatureStore fs = new FeatureStore();
+    fs.addFeature(new SequenceFeature("", "", 10, 20, Float.NaN,
+            null));
+    // same range different description
+    fs.addFeature(new SequenceFeature("", "desc", 10, 20, Float.NaN, null));
+    fs.addFeature(new SequenceFeature("", "", 15, 25, Float.NaN, null));
+    fs.addFeature(new SequenceFeature("", "", 20, 35, Float.NaN, null));
+
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
+    assertTrue(overlaps.isEmpty());
+
+    overlaps = fs.findOverlappingFeatures(8, 10);
+    assertEquals(overlaps.size(), 2);
+    assertEquals(overlaps.get(0).getEnd(), 20);
+    assertEquals(overlaps.get(1).getEnd(), 20);
+
+    overlaps = fs.findOverlappingFeatures(12, 16);
+    assertEquals(overlaps.size(), 3);
+    assertEquals(overlaps.get(0).getEnd(), 20);
+    assertEquals(overlaps.get(1).getEnd(), 20);
+    assertEquals(overlaps.get(2).getEnd(), 25);
+
+    overlaps = fs.findOverlappingFeatures(33, 33);
+    assertEquals(overlaps.size(), 1);
+    assertEquals(overlaps.get(0).getEnd(), 35);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_nested()
+  {
+    FeatureStore fs = new FeatureStore();
+    SequenceFeature sf1 = addFeature(fs, 10, 50);
+    SequenceFeature sf2 = addFeature(fs, 10, 40);
+    SequenceFeature sf3 = addFeature(fs, 20, 30);
+    // fudge feature at same location but different group (so is added)
+    SequenceFeature sf4 = new SequenceFeature("", "", 20, 30, Float.NaN,
+            "different group");
+    fs.addFeature(sf4);
+    SequenceFeature sf5 = addFeature(fs, 35, 36);
+
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
+    assertTrue(overlaps.isEmpty());
+
+    overlaps = fs.findOverlappingFeatures(10, 15);
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+
+    overlaps = fs.findOverlappingFeatures(45, 60);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf1));
+
+    overlaps = fs.findOverlappingFeatures(32, 38);
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+    assertTrue(overlaps.contains(sf5));
+
+    overlaps = fs.findOverlappingFeatures(15, 25);
+    assertEquals(overlaps.size(), 4);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+    assertTrue(overlaps.contains(sf3));
+    assertTrue(overlaps.contains(sf4));
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_mixed()
+  {
+    FeatureStore fs = new FeatureStore();
+    SequenceFeature sf1 = addFeature(fs, 10, 50);
+    SequenceFeature sf2 = addFeature(fs, 1, 15);
+    SequenceFeature sf3 = addFeature(fs, 20, 30);
+    SequenceFeature sf4 = addFeature(fs, 40, 100);
+    SequenceFeature sf5 = addFeature(fs, 60, 100);
+    SequenceFeature sf6 = addFeature(fs, 70, 70);
+
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(200, 200);
+    assertTrue(overlaps.isEmpty());
+
+    overlaps = fs.findOverlappingFeatures(1, 9);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf2));
+
+    overlaps = fs.findOverlappingFeatures(5, 18);
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+
+    overlaps = fs.findOverlappingFeatures(30, 40);
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf3));
+    assertTrue(overlaps.contains(sf4));
+
+    overlaps = fs.findOverlappingFeatures(80, 90);
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+
+    overlaps = fs.findOverlappingFeatures(68, 70);
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+    assertTrue(overlaps.contains(sf6));
+  }
+
+  /**
+   * Helper method to add a feature of no particular type
+   * 
+   * @param fs
+   * @param from
+   * @param to
+   * @return
+   */
+  SequenceFeature addFeature(FeatureStore fs, int from, int to)
+  {
+    SequenceFeature sf1 = new SequenceFeature("", "", from, to, Float.NaN,
+            null);
+    fs.addFeature(sf1);
+    return sf1;
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_contactFeatures()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    SequenceFeature sf = new SequenceFeature("disulphide bond", "bond", 10,
+            20, Float.NaN, null);
+    fs.addFeature(sf);
+
+    /*
+     * neither contact point in range
+     */
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
+    assertTrue(overlaps.isEmpty());
+
+    /*
+     * neither contact point in range
+     */
+    overlaps = fs.findOverlappingFeatures(11, 19);
+    assertTrue(overlaps.isEmpty());
+
+    /*
+     * first contact point in range
+     */
+    overlaps = fs.findOverlappingFeatures(5, 15);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf));
+
+    /*
+     * second contact point in range
+     */
+    overlaps = fs.findOverlappingFeatures(15, 25);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf));
+
+    /*
+     * both contact points in range
+     */
+    overlaps = fs.findOverlappingFeatures(5, 25);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf));
+  }
+
+  /**
+   * Tests for the method that returns false for an attempt to add a feature
+   * that would enclose, or be enclosed by, another feature
+   */
+  @Test(groups = "Functional")
+  public void testAddNonNestedFeature()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    String type = "Domain";
+    SequenceFeature sf1 = new SequenceFeature(type, type, 10, 20,
+            Float.NaN, null);
+    assertTrue(fs.addNonNestedFeature(sf1));
+
+    // co-located feature is ok
+    SequenceFeature sf2 = new SequenceFeature(type, type, 10, 20,
+            Float.NaN, null);
+    assertTrue(fs.addNonNestedFeature(sf2));
+
+    // overlap left is ok
+    SequenceFeature sf3 = new SequenceFeature(type, type, 5, 15, Float.NaN,
+            null);
+    assertTrue(fs.addNonNestedFeature(sf3));
+
+    // overlap right is ok
+    SequenceFeature sf4 = new SequenceFeature(type, type, 15, 25,
+            Float.NaN, null);
+    assertTrue(fs.addNonNestedFeature(sf4));
+
+    // add enclosing feature is not ok
+    SequenceFeature sf5 = new SequenceFeature(type, type, 10, 21,
+            Float.NaN, null);
+    assertFalse(fs.addNonNestedFeature(sf5));
+    SequenceFeature sf6 = new SequenceFeature(type, type, 4, 15, Float.NaN,
+            null);
+    assertFalse(fs.addNonNestedFeature(sf6));
+    SequenceFeature sf7 = new SequenceFeature(type, type, 1, 50, Float.NaN,
+            null);
+    assertFalse(fs.addNonNestedFeature(sf7));
+
+    // add enclosed feature is not ok
+    SequenceFeature sf8 = new SequenceFeature(type, type, 10, 19,
+            Float.NaN, null);
+    assertFalse(fs.addNonNestedFeature(sf8));
+    SequenceFeature sf9 = new SequenceFeature(type, type, 16, 25,
+            Float.NaN, null);
+    assertFalse(fs.addNonNestedFeature(sf9));
+    SequenceFeature sf10 = new SequenceFeature(type, type, 7, 7, Float.NaN,
+            null);
+    assertFalse(fs.addNonNestedFeature(sf10));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPositionalFeatures()
+  {
+    FeatureStore store = new FeatureStore();
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.addFeature(sf1);
+    // same range, different description
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20,
+            Float.NaN, null);
+    store.addFeature(sf2);
+    // discontiguous range
+    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40,
+            Float.NaN, null);
+    store.addFeature(sf3);
+    // overlapping range
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35,
+            Float.NaN, null);
+    store.addFeature(sf4);
+    // enclosing range
+    SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50,
+            Float.NaN, null);
+    store.addFeature(sf5);
+    // non-positional feature
+    SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.addFeature(sf6);
+    // contact feature
+    SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.addFeature(sf7);
+
+    List<SequenceFeature> features = store.getPositionalFeatures();
+    assertEquals(features.size(), 6);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertFalse(features.contains(sf6));
+    assertTrue(features.contains(sf7));
+
+    features = store.getNonPositionalFeatures();
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete()
+  {
+    FeatureStore store = new FeatureStore();
+    SequenceFeature sf1 = addFeature(store, 10, 20);
+    assertTrue(store.getPositionalFeatures().contains(sf1));
+
+    /*
+     * simple deletion
+     */
+    assertTrue(store.delete(sf1));
+    assertTrue(store.getPositionalFeatures().isEmpty());
+
+    /*
+     * non-positional feature deletion
+     */
+    SequenceFeature sf2 = addFeature(store, 0, 0);
+    assertFalse(store.getPositionalFeatures().contains(sf2));
+    assertTrue(store.getNonPositionalFeatures().contains(sf2));
+    assertTrue(store.delete(sf2));
+    assertTrue(store.getNonPositionalFeatures().isEmpty());
+
+    /*
+     * contact feature deletion
+     */
+    SequenceFeature sf3 = new SequenceFeature("", "Disulphide Bond", 11,
+            23, Float.NaN, null);
+    store.addFeature(sf3);
+    assertEquals(store.getPositionalFeatures().size(), 1);
+    assertTrue(store.getPositionalFeatures().contains(sf3));
+    assertTrue(store.delete(sf3));
+    assertTrue(store.getPositionalFeatures().isEmpty());
+
+    /*
+     * nested feature deletion
+     */
+    SequenceFeature sf4 = addFeature(store, 20, 30);
+    SequenceFeature sf5 = addFeature(store, 22, 26); // to NCList
+    SequenceFeature sf6 = addFeature(store, 23, 24); // child of sf5
+    SequenceFeature sf7 = addFeature(store, 25, 25); // sibling of sf6
+    SequenceFeature sf8 = addFeature(store, 24, 24); // child of sf6
+    SequenceFeature sf9 = addFeature(store, 23, 23); // child of sf6
+    assertEquals(store.getPositionalFeatures().size(), 6);
+
+    // delete a node with children - they take its place
+    assertTrue(store.delete(sf6)); // sf8, sf9 should become children of sf5
+    assertEquals(store.getPositionalFeatures().size(), 5);
+    assertFalse(store.getPositionalFeatures().contains(sf6));
+
+    // delete a node with no children
+    assertTrue(store.delete(sf7));
+    assertEquals(store.getPositionalFeatures().size(), 4);
+    assertFalse(store.getPositionalFeatures().contains(sf7));
+
+    // delete root of NCList
+    assertTrue(store.delete(sf5));
+    assertEquals(store.getPositionalFeatures().size(), 3);
+    assertFalse(store.getPositionalFeatures().contains(sf5));
+
+    // continue the killing fields
+    assertTrue(store.delete(sf4));
+    assertEquals(store.getPositionalFeatures().size(), 2);
+    assertFalse(store.getPositionalFeatures().contains(sf4));
+
+    assertTrue(store.delete(sf9));
+    assertEquals(store.getPositionalFeatures().size(), 1);
+    assertFalse(store.getPositionalFeatures().contains(sf9));
+
+    assertTrue(store.delete(sf8));
+    assertTrue(store.getPositionalFeatures().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testAddFeature()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, null);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, null);
+
+    assertTrue(fs.addFeature(sf1));
+    assertEquals(fs.getFeatureCount(true), 1); // positional
+    assertEquals(fs.getFeatureCount(false), 0); // non-positional
+
+    /*
+     * re-adding the same or an identical feature should fail
+     */
+    assertFalse(fs.addFeature(sf1));
+    assertEquals(fs.getFeatureCount(true), 1);
+    assertFalse(fs.addFeature(sf2));
+    assertEquals(fs.getFeatureCount(true), 1);
+
+    /*
+     * add non-positional
+     */
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
+            null);
+    assertTrue(fs.addFeature(sf3));
+    assertEquals(fs.getFeatureCount(true), 1); // positional
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
+            null);
+    assertFalse(fs.addFeature(sf4)); // already stored
+    assertEquals(fs.getFeatureCount(true), 1); // positional
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+
+    /*
+     * add contact
+     */
+    SequenceFeature sf5 = new SequenceFeature("Disulfide bond", "", 10, 20,
+            Float.NaN, null);
+    assertTrue(fs.addFeature(sf5));
+    assertEquals(fs.getFeatureCount(true), 2); // positional - add 1 for contact
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "", 10, 20,
+            Float.NaN, null);
+    assertFalse(fs.addFeature(sf6)); // already stored
+    assertEquals(fs.getFeatureCount(true), 2); // no change
+    assertEquals(fs.getFeatureCount(false), 1); // no change
+  }
+
+  @Test(groups = "Functional")
+  public void testIsEmpty()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 0);
+
+    /*
+     * non-nested feature
+     */
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, null);
+    fs.addFeature(sf1);
+    assertFalse(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 1);
+    fs.delete(sf1);
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 0);
+
+    /*
+     * non-positional feature
+     */
+    sf1 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, null);
+    fs.addFeature(sf1);
+    assertFalse(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+    assertEquals(fs.getFeatureCount(true), 0); // positional
+    fs.delete(sf1);
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(false), 0);
+
+    /*
+     * contact feature
+     */
+    sf1 = new SequenceFeature("Disulfide bond", "", 19, 49, Float.NaN, null);
+    fs.addFeature(sf1);
+    assertFalse(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 1);
+    fs.delete(sf1);
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 0);
+
+    /*
+     * sf2, sf3 added as nested features
+     */
+    sf1 = new SequenceFeature("Cath", "", 19, 49, Float.NaN, null);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 20, 40,
+            Float.NaN, null);
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 25, 35,
+            Float.NaN, null);
+    fs.addFeature(sf1);
+    fs.addFeature(sf2);
+    fs.addFeature(sf3);
+    assertEquals(fs.getFeatureCount(true), 3);
+    assertTrue(fs.delete(sf1));
+    assertEquals(fs.getFeatureCount(true), 2);
+    // FeatureStore should now only contain features in the NCList
+    assertTrue(fs.nonNestedFeatures.isEmpty());
+    assertEquals(fs.nestedFeatures.size(), 2);
+    assertFalse(fs.isEmpty());
+    assertTrue(fs.delete(sf2));
+    assertEquals(fs.getFeatureCount(true), 1);
+    assertFalse(fs.isEmpty());
+    assertTrue(fs.delete(sf3));
+    assertEquals(fs.getFeatureCount(true), 0);
+    assertTrue(fs.isEmpty()); // all gone
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureGroups()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertTrue(fs.getFeatureGroups(true).isEmpty());
+    assertTrue(fs.getFeatureGroups(false).isEmpty());
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f, "group1");
+    fs.addFeature(sf1);
+    Set<String> groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("group1"));
+
+    /*
+     * add another feature of the same group, delete one, delete both
+     */
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "group1");
+    fs.addFeature(sf2);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("group1"));
+    fs.delete(sf2);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("group1"));
+    fs.delete(sf1);
+    groups = fs.getFeatureGroups(true);
+    assertTrue(fs.getFeatureGroups(true).isEmpty());
+
+    SequenceFeature sf3 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "group2");
+    fs.addFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "Group2");
+    fs.addFeature(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 20, 30, 1f, null);
+    fs.addFeature(sf5);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 3);
+    assertTrue(groups.contains("group2"));
+    assertTrue(groups.contains("Group2")); // case sensitive
+    assertTrue(groups.contains(null)); // null allowed
+    assertTrue(fs.getFeatureGroups(false).isEmpty()); // non-positional
+
+    fs.delete(sf3);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 2);
+    assertFalse(groups.contains("group2"));
+    fs.delete(sf4);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertFalse(groups.contains("Group2"));
+    fs.delete(sf5);
+    groups = fs.getFeatureGroups(true);
+    assertTrue(groups.isEmpty());
+
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
+            "CathGroup");
+    fs.addFeature(sf6);
+    groups = fs.getFeatureGroups(false);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("CathGroup"));
+    assertTrue(fs.delete(sf6));
+    assertTrue(fs.getFeatureGroups(false).isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetTotalFeatureLength()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertEquals(fs.getTotalFeatureLength(), 0);
+
+    addFeature(fs, 10, 20); // 11
+    assertEquals(fs.getTotalFeatureLength(), 11);
+    addFeature(fs, 17, 37); // 21
+    SequenceFeature sf1 = addFeature(fs, 14, 74); // 61
+    assertEquals(fs.getTotalFeatureLength(), 93);
+
+    // non-positional features don't count
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
+            "group1");
+    fs.addFeature(sf2);
+    assertEquals(fs.getTotalFeatureLength(), 93);
+
+    // contact features count 1
+    SequenceFeature sf3 = new SequenceFeature("disulphide bond", "desc",
+            15, 35, 1f, "group1");
+    fs.addFeature(sf3);
+    assertEquals(fs.getTotalFeatureLength(), 94);
+
+    assertTrue(fs.delete(sf1));
+    assertEquals(fs.getTotalFeatureLength(), 33);
+    assertFalse(fs.delete(sf1));
+    assertEquals(fs.getTotalFeatureLength(), 33);
+    assertTrue(fs.delete(sf2));
+    assertEquals(fs.getTotalFeatureLength(), 33);
+    assertTrue(fs.delete(sf3));
+    assertEquals(fs.getTotalFeatureLength(), 32);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureLength()
+  {
+    /*
+     * positional feature
+     */
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f, "group1");
+    assertEquals(FeatureStore.getFeatureLength(sf1), 11);
+  
+    /*
+     * non-positional feature
+     */
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
+            "CathGroup");
+    assertEquals(FeatureStore.getFeatureLength(sf2), 0);
+
+    /*
+     * contact feature counts 1
+     */
+    SequenceFeature sf3 = new SequenceFeature("Disulphide Bond", "desc",
+            14, 28, 1f, "AGroup");
+    assertEquals(FeatureStore.getFeatureLength(sf3), 1);
+  }
+
+  @Test(groups = "Functional")
+  public void testMin()
+  {
+    assertEquals(FeatureStore.min(Float.NaN, Float.NaN), Float.NaN);
+    assertEquals(FeatureStore.min(Float.NaN, 2f), 2f);
+    assertEquals(FeatureStore.min(-2f, Float.NaN), -2f);
+    assertEquals(FeatureStore.min(2f, -3f), -3f);
+  }
+
+  @Test(groups = "Functional")
+  public void testMax()
+  {
+    assertEquals(FeatureStore.max(Float.NaN, Float.NaN), Float.NaN);
+    assertEquals(FeatureStore.max(Float.NaN, 2f), 2f);
+    assertEquals(FeatureStore.max(-2f, Float.NaN), -2f);
+    assertEquals(FeatureStore.max(2f, -3f), 2f);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinimumScore_getMaximumScore()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertEquals(fs.getMinimumScore(true), Float.NaN); // positional
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN); // non-positional
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // add features with no score
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 0, 0,
+            Float.NaN, "group");
+    fs.addFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 10, 20,
+            Float.NaN, "group");
+    fs.addFeature(sf2);
+    assertEquals(fs.getMinimumScore(true), Float.NaN);
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // add positional features with score
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 10, 20, 1f,
+            "group");
+    fs.addFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("type", "desc", 12, 16, 4f,
+            "group");
+    fs.addFeature(sf4);
+    assertEquals(fs.getMinimumScore(true), 1f);
+    assertEquals(fs.getMaximumScore(true), 4f);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // add non-positional features with score
+    SequenceFeature sf5 = new SequenceFeature("type", "desc", 0, 0, 11f,
+            "group");
+    fs.addFeature(sf5);
+    SequenceFeature sf6 = new SequenceFeature("type", "desc", 0, 0, -7f,
+            "group");
+    fs.addFeature(sf6);
+    assertEquals(fs.getMinimumScore(true), 1f);
+    assertEquals(fs.getMaximumScore(true), 4f);
+    assertEquals(fs.getMinimumScore(false), -7f);
+    assertEquals(fs.getMaximumScore(false), 11f);
+
+    // delete one positional and one non-positional
+    // min-max should be recomputed
+    assertTrue(fs.delete(sf6));
+    assertTrue(fs.delete(sf3));
+    assertEquals(fs.getMinimumScore(true), 4f);
+    assertEquals(fs.getMaximumScore(true), 4f);
+    assertEquals(fs.getMinimumScore(false), 11f);
+    assertEquals(fs.getMaximumScore(false), 11f);
+
+    // delete remaining features with score
+    assertTrue(fs.delete(sf4));
+    assertTrue(fs.delete(sf5));
+    assertEquals(fs.getMinimumScore(true), Float.NaN);
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // delete all features
+    assertTrue(fs.delete(sf1));
+    assertTrue(fs.delete(sf2));
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getMinimumScore(true), Float.NaN);
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+  }
+
+  @Test(groups = "Functional")
+  public void testListContains()
+  {
+    assertFalse(FeatureStore.listContains(null, null));
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+    assertFalse(FeatureStore.listContains(features, null));
+
+    SequenceFeature sf1 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
+            "group1");
+    assertFalse(FeatureStore.listContains(null, sf1));
+    assertFalse(FeatureStore.listContains(features, sf1));
+
+    features.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
+            "group1");
+    SequenceFeature sf3 = new SequenceFeature("type1", "desc1", 20, 40, 3f,
+            "group1");
+
+    // sf2.equals(sf1) so contains should return true
+    assertTrue(FeatureStore.listContains(features, sf2));
+    assertFalse(FeatureStore.listContains(features, sf3));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeaturesForGroup()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    /*
+     * with no features
+     */
+    assertTrue(fs.getFeaturesForGroup(true, null).isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, null).isEmpty());
+    assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty());
+
+    /*
+     * sf1: positional feature in the null group
+     */
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            null);
+    fs.addFeature(sf1);
+    assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, null).isEmpty());
+    List<SequenceFeature> features = fs.getFeaturesForGroup(true, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+
+    /*
+     * sf2: non-positional feature in the null group
+     * sf3: positional feature in a non-null group
+     * sf4: non-positional feature in a non-null group
+     */
+    SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
+            null);
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            "Uniprot");
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
+            "Rfam");
+    fs.addFeature(sf2);
+    fs.addFeature(sf3);
+    fs.addFeature(sf4);
+
+    features = fs.getFeaturesForGroup(true, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+
+    features = fs.getFeaturesForGroup(false, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+
+    features = fs.getFeaturesForGroup(true, "Uniprot");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+
+    features = fs.getFeaturesForGroup(false, "Rfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf4));
+  }
+
+  @Test(groups = "Functional")
+  public void testShiftFeatures()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertFalse(fs.shiftFeatures(1));
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
+    fs.addFeature(sf1);
+    // nested feature:
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 8, 14, 0f, null);
+    fs.addFeature(sf2);
+    // contact feature:
+    SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32,
+            0f, null);
+    fs.addFeature(sf3);
+    // non-positional feature:
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f, null);
+    fs.addFeature(sf4);
+
+    /*
+     * shift features right by 5
+     */
+    assertTrue(fs.shiftFeatures(5));
+
+    // non-positional features untouched:
+    List<SequenceFeature> nonPos = fs.getNonPositionalFeatures();
+    assertEquals(nonPos.size(), 1);
+    assertTrue(nonPos.contains(sf4));
+
+    // positional features are replaced
+    List<SequenceFeature> pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 3);
+    assertFalse(pos.contains(sf1));
+    assertFalse(pos.contains(sf2));
+    assertFalse(pos.contains(sf3));
+    SequenceFeatures.sortFeatures(pos, true); // ascending start pos
+    assertEquals(pos.get(0).getBegin(), 7);
+    assertEquals(pos.get(0).getEnd(), 10);
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 19);
+    assertEquals(pos.get(2).getBegin(), 28);
+    assertEquals(pos.get(2).getEnd(), 37);
+
+    /*
+     * now shift left by 15
+     * feature at [7-10] should be removed
+     * feature at [13-19] should become [1-4] 
+     */
+    assertTrue(fs.shiftFeatures(-15));
+    pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 22);
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete_readd()
+  {
+    /*
+     * add a feature and a nested feature
+     */
+    FeatureStore store = new FeatureStore();
+    SequenceFeature sf1 = addFeature(store, 10, 20);
+    // sf2 is nested in sf1 so will be stored in nestedFeatures
+    SequenceFeature sf2 = addFeature(store, 12, 14);
+    List<SequenceFeature> features = store.getPositionalFeatures();
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(store.nonNestedFeatures.contains(sf1));
+    assertTrue(store.nestedFeatures.contains(sf2));
+  
+    /*
+     * delete the first feature
+     */
+    assertTrue(store.delete(sf1));
+    features = store.getPositionalFeatures();
+    assertFalse(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+
+    /*
+     * re-add the 'nested' feature; is it now duplicated?
+     */
+    store.addFeature(sf2);
+    features = store.getPositionalFeatures();
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+  }
+
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    FeatureStore fs = new FeatureStore();
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, "group1");
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, "group2");
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
+            "group1");
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f,
+            "group1");
+    SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "", 5, 15,
+            Float.NaN, "group1");
+    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "", 5, 15,
+            Float.NaN, "group2");
+
+    fs.addFeature(sf1);
+    fs.addFeature(sf3);
+    fs.addFeature(sf5);
+    assertTrue(fs.contains(sf1)); // positional feature
+    assertTrue(fs.contains(new SequenceFeature(sf1))); // identical feature
+    assertFalse(fs.contains(sf2)); // different group
+    assertTrue(fs.contains(sf3)); // non-positional
+    assertTrue(fs.contains(new SequenceFeature(sf3)));
+    assertFalse(fs.contains(sf4)); // different score
+    assertTrue(fs.contains(sf5)); // contact feature
+    assertTrue(fs.contains(new SequenceFeature(sf5)));
+    assertFalse(fs.contains(sf6)); // different group
+
+    /*
+     * add a nested feature
+     */
+    SequenceFeature sf7 = new SequenceFeature("Cath", "", 12, 16,
+            Float.NaN, "group1");
+    fs.addFeature(sf7);
+    assertTrue(fs.contains(sf7));
+    assertTrue(fs.contains(new SequenceFeature(sf7)));
+
+    /*
+     * delete the outer (enclosing, non-nested) feature
+     */
+    fs.delete(sf1);
+    assertFalse(fs.contains(sf1));
+    assertTrue(fs.contains(sf7));
+  }
+}
diff --git a/test/jalview/datamodel/features/NCListTest.java b/test/jalview/datamodel/features/NCListTest.java
new file mode 100644 (file)
index 0000000..2c7f752
--- /dev/null
@@ -0,0 +1,682 @@
+package jalview.datamodel.features;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class NCListTest
+{
+
+  private Random random = new Random(107);
+
+  private Comparator<ContiguousI> sorter = new RangeComparator(true);
+
+  /**
+   * A basic sanity test of the constructor
+   */
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 20));
+    ranges.add(new Range(10, 20));
+    ranges.add(new Range(15, 30));
+    ranges.add(new Range(10, 30));
+    ranges.add(new Range(11, 19));
+    ranges.add(new Range(10, 20));
+    ranges.add(new Range(1, 100));
+
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    String expected = "[1-100 [10-30 [10-20 [10-20 [11-19]]]], 15-30 [20-20]]";
+    assertEquals(ncl.toString(), expected);
+    assertTrue(ncl.isValid());
+
+    Collections.reverse(ranges);
+    ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), expected);
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testFindOverlaps()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    ranges.add(new Range(30, 70));
+    ranges.add(new Range(1, 100));
+    ranges.add(new Range(70, 120));
+  
+    NCList<Range> ncl = new NCList<Range>(ranges);
+
+    List<Range> overlaps = ncl.findOverlaps(121, 122);
+    assertEquals(overlaps.size(), 0);
+
+    overlaps = ncl.findOverlaps(21, 22);
+    assertEquals(overlaps.size(), 2);
+    assertEquals(((ContiguousI) overlaps.get(0)).getBegin(), 1);
+    assertEquals(((ContiguousI) overlaps.get(0)).getEnd(), 100);
+    assertEquals(((ContiguousI) overlaps.get(1)).getBegin(), 20);
+    assertEquals(((ContiguousI) overlaps.get(1)).getEnd(), 50);
+
+    overlaps = ncl.findOverlaps(110, 110);
+    assertEquals(overlaps.size(), 1);
+    assertEquals(((ContiguousI) overlaps.get(0)).getBegin(), 70);
+    assertEquals(((ContiguousI) overlaps.get(0)).getEnd(), 120);
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_onTheEnd()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(60, 70));
+    assertEquals(ncl.toString(), "[20-50, 60-70]");
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_inside()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(30, 40));
+    assertEquals(ncl.toString(), "[20-50 [30-40]]");
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_onTheFront()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(5, 15));
+    assertEquals(ncl.toString(), "[5-15, 20-50]");
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_enclosing()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    ranges.add(new Range(30, 60));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50, 30-60]");
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.getStart(), 20);
+
+    ncl.add(new Range(10, 70));
+    assertEquals(ncl.toString(), "[10-70 [20-50, 30-60]]");
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_spanning()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 40));
+    ranges.add(new Range(60, 70));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-40, 60-70]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(30, 50));
+    assertEquals(ncl.toString(), "[20-40, 30-50, 60-70]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(40, 65));
+    assertEquals(ncl.toString(), "[20-40, 30-50, 40-65, 60-70]");
+    assertTrue(ncl.isValid());
+  }
+
+  /**
+   * Provides the scales for pseudo-random NCLists i.e. the range of the maximal
+   * [0-scale] interval to be stored
+   * 
+   * @return
+   */
+  @DataProvider(name = "scalesOfLife")
+  public Object[][] getScales()
+  {
+    return new Object[][] { new Integer[] { 10 }, new Integer[] { 100 } };
+  }
+
+  /**
+   * Do a number of pseudo-random (reproducible) builds of an NCList, to
+   * exercise as many methods of the class as possible while generating the
+   * range of possible structure topologies
+   * <ul>
+   * <li>verify that add adds an entry and increments size</li>
+   * <li>...except where the entry is already contained (by equals test)</li>
+   * <li>verify that the structure is valid at all stages of construction</li>
+   * <li>generate, run and verify a range of overlap queries</li>
+   * <li>tear down the structure by deleting entries, verifying correctness at
+   * each stage</li>
+   * </ul>
+   */
+  @Test(groups = "Functional", dataProvider = "scalesOfLife")
+  public void test_pseudoRandom(Integer scale)
+  {
+    NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>(scale);
+    
+    testAdd_pseudoRandom(scale, ncl, features);
+
+    /*
+     * sort the list of added ranges - this doesn't affect the test,
+     * just makes it easier to inspect the data in the debugger
+     */
+    Collections.sort(features, sorter);
+
+    testFindOverlaps_pseudoRandom(ncl, scale, features);
+
+    testDelete_pseudoRandom(ncl, features);
+  }
+
+  /**
+   * Pick randomly selected entries to delete in turn, checking the NCList size
+   * and validity at each stage, until it is empty
+   * 
+   * @param ncl
+   * @param features
+   */
+  protected void testDelete_pseudoRandom(NCList<SequenceFeature> ncl,
+          List<SequenceFeature> features)
+  {
+    int deleted = 0;
+
+    while (!features.isEmpty())
+    {
+      assertEquals(ncl.size(), features.size());
+      int toDelete = random.nextInt(features.size());
+      SequenceFeature entry = features.get(toDelete);
+      assertTrue(ncl.contains(entry), String.format(
+              "NCList doesn't contain entry [%d] '%s'!", deleted,
+              entry.toString()));
+
+      ncl.delete(entry);
+      assertFalse(ncl.contains(entry), String.format(
+              "NCList still contains deleted entry [%d] '%s'!", deleted,
+              entry.toString()));
+      features.remove(toDelete);
+      deleted++;
+
+      assertTrue(ncl.isValid(), String.format(
+              "NCList invalid after %d deletions, last deleted was '%s'",
+              deleted, entry.toString()));
+
+      /*
+       * brute force check that deleting one entry didn't delete any others
+       */
+      for (int i = 0; i < features.size(); i++)
+      {
+        SequenceFeature sf = features.get(i);
+        assertTrue(ncl.contains(sf), String.format(
+                        "NCList doesn't contain entry [%d] %s after deleting '%s'!",
+                        i, sf.toString(), entry.toString()));
+      }
+    }
+    assertEquals(ncl.size(), 0); // all gone
+  }
+
+  /**
+   * Randomly generate entries and add them to the NCList, checking its validity
+   * and size at each stage. A few entries should be duplicates (by equals test)
+   * so not get added.
+   * 
+   * @param scale
+   * @param ncl
+   * @param features
+   */
+  protected void testAdd_pseudoRandom(Integer scale,
+          NCList<SequenceFeature> ncl,
+          List<SequenceFeature> features)
+  {
+    int count = 0;
+    final int size = 50;
+
+    for (int i = 0; i < size; i++)
+    {
+      int r1 = random.nextInt(scale + 1);
+      int r2 = random.nextInt(scale + 1);
+      int from = Math.min(r1, r2);
+      int to = Math.max(r1, r2);
+
+      /*
+       * choice of two feature values means that occasionally an identical
+       * feature may be generated, in which case it should not be added 
+       */
+      float value = (float) i % 2;
+      SequenceFeature feature = new SequenceFeature("Pfam", "", from, to,
+              value, "group");
+
+      /*
+       * add to NCList - with duplicate entries (by equals) disallowed
+       */
+      ncl.add(feature, false);
+      if (features.contains(feature))
+      {
+        System.out.println("Duplicate feature generated "
+                + feature.toString());
+      }
+      else
+      {
+        features.add(feature);
+        count++;
+      }
+    
+      /*
+       * check list format is valid at each stage of its construction
+       */
+      assertTrue(ncl.isValid(),
+              String.format("Failed for scale = %d, i=%d", scale, i));
+      assertEquals(ncl.size(), count);
+    }
+    // System.out.println(ncl.prettyPrint());
+  }
+
+  /**
+   * A helper method that generates pseudo-random range queries and veries that
+   * findOverlaps returns the correct matches
+   * 
+   * @param ncl
+   *          the NCList to query
+   * @param scale
+   *          ncl maximal range is [0, scale]
+   * @param features
+   *          a list of the ranges stored in ncl
+   */
+  protected void testFindOverlaps_pseudoRandom(NCList<SequenceFeature> ncl,
+          int scale,
+          List<SequenceFeature> features)
+  {
+    int halfScale = scale / 2;
+    int minIterations = 20;
+
+    /*
+     * generates ranges in [-halfScale, scale+halfScale]
+     * - some should be internal to [0, scale] P = 1/4
+     * - some should lie before 0 P = 1/16
+     * - some should lie after scale P = 1/16
+     * - some should overlap left P = 1/4
+     * - some should overlap right P = 1/4
+     * - some should enclose P = 1/8
+     * 
+     * 50 iterations give a 96% probability of including the
+     * unlikeliest case; keep going until we have done all!
+     */
+    boolean inside = false;
+    boolean enclosing = false;
+    boolean before = false;
+    boolean after = false;
+    boolean overlapLeft = false;
+    boolean overlapRight = false;
+    boolean allCasesCovered = false;
+
+    int i = 0;
+    while (i < minIterations || !allCasesCovered)
+    {
+      i++;
+      int r1 = random.nextInt((scale + 1) * 2);
+      int r2 = random.nextInt((scale + 1) * 2);
+      int from = Math.min(r1, r2) - halfScale;
+      int to = Math.max(r1, r2) - halfScale;
+
+      /*
+       * ensure all cases of interest get covered
+       */
+      inside |= from >= 0 && to <= scale;
+      enclosing |= from <= 0 && to >= scale;
+      before |= to < 0;
+      after |= from > scale;
+      overlapLeft |= from < 0 && to >= 0 && to <= scale;
+      overlapRight |= from >= 0 && from <= scale && to > scale;
+      if (!allCasesCovered)
+      {
+        allCasesCovered |= inside && enclosing && before && after
+              && overlapLeft && overlapRight;
+        if (allCasesCovered)
+        {
+          System.out
+                  .println(String
+                          .format("Covered all findOverlaps cases after %d iterations for scale %d",
+                                  i, scale));
+        }
+      }
+
+      verifyFindOverlaps(ncl, from, to, features);
+    }
+  }
+
+  /**
+   * A helper method that verifies that overlaps found by interrogating an
+   * NCList correctly match those found by brute force search
+   * 
+   * @param ncl
+   * @param from
+   * @param to
+   * @param features
+   */
+  protected void verifyFindOverlaps(NCList<SequenceFeature> ncl, int from,
+          int to, List<SequenceFeature> features)
+  {
+    List<SequenceFeature> overlaps = ncl.findOverlaps(from, to);
+
+    /*
+     * check returned entries do indeed overlap from-to range
+     */
+    for (ContiguousI sf : overlaps)
+    {
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
+      assertTrue(begin <= to && end >= from, String.format(
+              "[%d, %d] does not overlap query range [%d, %d]", begin, end,
+              from, to));
+    }
+
+    /*
+     * check overlapping ranges are included in the results
+     * (the test above already shows non-overlapping ranges are not)
+     */
+    for (ContiguousI sf : features)
+    {
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
+      if (begin <= to && end >= from)
+      {
+        boolean found = overlaps.contains(sf);
+        assertTrue(found, String.format(
+                "[%d, %d] missing in query range [%d, %d]", begin, end,
+                from, to));
+      }
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testGetEntries()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(20, 20);
+    Range r2 = new Range(10, 20);
+    Range r3 = new Range(15, 30);
+    Range r4 = new Range(10, 30);
+    Range r5 = new Range(11, 19);
+    Range r6 = new Range(10, 20);
+    ranges.add(r1);
+    ranges.add(r2);
+    ranges.add(r3);
+    ranges.add(r4);
+    ranges.add(r5);
+    ranges.add(r6);
+  
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    Range r7 = new Range(1, 100);
+    ncl.add(r7);
+
+    List<Range> contents = ncl.getEntries();
+    assertEquals(contents.size(), 7);
+    assertTrue(contents.contains(r1));
+    assertTrue(contents.contains(r2));
+    assertTrue(contents.contains(r3));
+    assertTrue(contents.contains(r4));
+    assertTrue(contents.contains(r5));
+    assertTrue(contents.contains(r6));
+    assertTrue(contents.contains(r7));
+
+    ncl = new NCList<Range>();
+    assertTrue(ncl.getEntries().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(20, 30);
+    ranges.add(r1);
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.getEntries().contains(r1));
+
+    Range r2 = new Range(20, 30);
+    assertFalse(ncl.delete(null)); // null argument
+    assertFalse(ncl.delete(r2)); // never added
+    assertTrue(ncl.delete(r1)); // success
+    assertTrue(ncl.getEntries().isEmpty());
+
+    /*
+     * tests where object.equals() == true
+     */
+    NCList<SequenceFeature> features = new NCList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    features.add(sf1);
+    assertEquals(sf1, sf2); // sf1.equals(sf2)
+    assertFalse(features.delete(sf2)); // equality is not enough for deletion
+    assertTrue(features.getEntries().contains(sf1)); // still there!
+    assertTrue(features.delete(sf1));
+    assertTrue(features.getEntries().isEmpty()); // gone now
+
+    /*
+     * test with duplicate objects in NCList
+     */
+    features.add(sf1);
+    features.add(sf1);
+    assertEquals(features.getEntries().size(), 2);
+    assertSame(features.getEntries().get(0), sf1);
+    assertSame(features.getEntries().get(1), sf1);
+    assertTrue(features.delete(sf1)); // first match only is deleted
+    assertTrue(features.contains(sf1));
+    assertEquals(features.size(), 1);
+    assertTrue(features.delete(sf1));
+    assertTrue(features.getEntries().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_overlapping()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(40, 50));
+    ranges.add(new Range(20, 30));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-30, 40-50]");
+    assertTrue(ncl.isValid());
+  
+    /*
+     * add range overlapping internally
+     */
+    ncl.add(new Range(25, 35));
+    assertEquals(ncl.toString(), "[20-30, 25-35, 40-50]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * add range overlapping last range
+     */
+    ncl.add(new Range(45, 55));
+    assertEquals(ncl.toString(), "[20-30, 25-35, 40-50, 45-55]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * add range overlapping first range
+     */
+    ncl.add(new Range(15, 25));
+    assertEquals(ncl.toString(), "[15-25, 20-30, 25-35, 40-50, 45-55]");
+    assertTrue(ncl.isValid());
+  }
+
+  /**
+   * Test the contains method (which uses object equals test)
+   */
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "anothergroup");
+    ncl.add(sf1);
+
+    assertTrue(ncl.contains(sf1));
+    assertTrue(ncl.contains(sf2)); // sf1.equals(sf2)
+    assertFalse(ncl.contains(sf3)); // !sf1.equals(sf3)
+
+    /*
+     * make some deeper structure in the NCList
+     */
+    SequenceFeature sf4 = new SequenceFeature("type", "desc", 2, 9, 2f,
+            "group");
+    ncl.add(sf4);
+    assertTrue(ncl.contains(sf4));
+    SequenceFeature sf5 = new SequenceFeature("type", "desc", 4, 5, 2f,
+            "group");
+    SequenceFeature sf6 = new SequenceFeature("type", "desc", 6, 8, 2f,
+            "group");
+    ncl.add(sf5);
+    ncl.add(sf6);
+    assertTrue(ncl.contains(sf5));
+    assertTrue(ncl.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testIsValid()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(40, 50);
+    ranges.add(r1);
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.isValid());
+
+    Range r2 = new Range(42, 44);
+    ncl.add(r2);
+    assertTrue(ncl.isValid());
+    Range r3 = new Range(46, 48);
+    ncl.add(r3);
+    assertTrue(ncl.isValid());
+    Range r4 = new Range(43, 43);
+    ncl.add(r4);
+    assertTrue(ncl.isValid());
+
+    assertEquals(ncl.toString(), "[40-50 [42-44 [43-43], 46-48]]");
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r1, "start", 43);
+    assertFalse(ncl.isValid()); // r2 not inside r1
+    PA.setValue(r1, "start", 40);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r3, "start", 41);
+    assertFalse(ncl.isValid()); // r3 should precede r2
+    PA.setValue(r3, "start", 46);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r4, "start", 41);
+    assertFalse(ncl.isValid()); // r4 not inside r2
+    PA.setValue(r4, "start", 43);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r4, "start", 44);
+    assertFalse(ncl.isValid()); // r4 has reverse range
+  }
+
+  @Test(groups = "Functional")
+  public void testPrettyPrint()
+  {
+    /*
+     * construct NCList from a list of ranges
+     * they are sorted then assembled into NCList subregions
+     * notice that 42-42 end up inside 41-46
+     */
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(40, 50));
+    ranges.add(new Range(45, 55));
+    ranges.add(new Range(40, 45));
+    ranges.add(new Range(41, 46));
+    ranges.add(new Range(42, 42));
+    ranges.add(new Range(42, 42));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.toString(),
+            "[40-50 [40-45], 41-46 [42-42 [42-42]], 45-55]");
+    String expected = "40-50\n  40-45\n41-46\n  42-42\n    42-42\n45-55\n";
+    assertEquals(ncl.prettyPrint(), expected);
+
+    /*
+     * repeat but now add ranges one at a time
+     * notice that 42-42 end up inside 40-50 so we get
+     * a different but equal valid NCList structure
+     */
+    ranges.clear();
+    ncl = new NCList<Range>(ranges);
+    ncl.add(new Range(40, 50));
+    ncl.add(new Range(45, 55));
+    ncl.add(new Range(40, 45));
+    ncl.add(new Range(41, 46));
+    ncl.add(new Range(42, 42));
+    ncl.add(new Range(42, 42));
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.toString(),
+            "[40-50 [40-45 [42-42 [42-42]], 41-46], 45-55]");
+    expected = "40-50\n  40-45\n    42-42\n      42-42\n  41-46\n45-55\n";
+    assertEquals(ncl.prettyPrint(), expected);
+  }
+
+  /**
+   * A test that shows different valid trees can be constructed from the same
+   * set of ranges, depending on the order of construction
+   */
+  @Test(groups = "Functional")
+  public void testConstructor_alternativeTrees()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(10, 60));
+    ranges.add(new Range(20, 30));
+    ranges.add(new Range(40, 50));
+  
+    /*
+     * constructor with greedy traversal of sorted ranges to build nested
+     * containment lists results in 20-30 inside 10-60, 40-50 a sibling
+     */
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[10-60 [20-30], 40-50]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * adding ranges one at a time results in 40-50 
+     * a sibling of 20-30 inside 10-60
+     */
+    ncl = new NCList<Range>(new Range(10, 60));
+    ncl.add(new Range(20, 30));
+    ncl.add(new Range(40, 50));
+    assertEquals(ncl.toString(), "[10-60 [20-30, 40-50]]");
+    assertTrue(ncl.isValid());
+  }
+}
diff --git a/test/jalview/datamodel/features/NCNodeTest.java b/test/jalview/datamodel/features/NCNodeTest.java
new file mode 100644 (file)
index 0000000..4713084
--- /dev/null
@@ -0,0 +1,136 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.Range;
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class NCNodeTest
+{
+  @Test(groups = "Functional")
+  public void testAdd()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    assertEquals(node.getBegin(), 10);
+    Range r2 = new Range(10, 15);
+    node.add(r2);
+
+    List<Range> contents = new ArrayList<Range>();
+    node.getEntries(contents);
+    assertEquals(contents.size(), 2);
+    assertTrue(contents.contains(r1));
+    assertTrue(contents.contains(r2));
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testAdd_invalidRangeStart()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    assertEquals(node.getBegin(), 10);
+    Range r2 = new Range(9, 15);
+    node.add(r2);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testAdd_invalidRangeEnd()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    assertEquals(node.getBegin(), 10);
+    Range r2 = new Range(12, 21);
+    node.add(r2);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetEntries()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    List<Range> entries = new ArrayList<Range>();
+
+    node.getEntries(entries);
+    assertEquals(entries.size(), 1);
+    assertTrue(entries.contains(r1));
+
+    // clearing the returned list does not affect the NCNode
+    entries.clear();
+    node.getEntries(entries);
+    assertEquals(entries.size(), 1);
+    assertTrue(entries.contains(r1));
+
+    Range r2 = new Range(15, 18);
+    node.add(r2);
+    entries.clear();
+    node.getEntries(entries);
+    assertEquals(entries.size(), 2);
+    assertTrue(entries.contains(r1));
+    assertTrue(entries.contains(r2));
+  }
+
+  /**
+   * Tests for the contains method (uses entry.equals() test)
+   */
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "anothergroup");
+    NCNode<SequenceFeature> node = new NCNode<SequenceFeature>(sf1);
+
+    assertFalse(node.contains(null));
+    assertTrue(node.contains(sf1));
+    assertTrue(node.contains(sf2)); // sf1.equals(sf2)
+    assertFalse(node.contains(sf3)); // !sf1.equals(sf3)
+  }
+
+  /**
+   * Test method that checks for valid structure. Valid means that all
+   * subregions (if any) lie within the root range, and that all subregions have
+   * valid structure.
+   */
+  @Test(groups = "Functional")
+  public void testIsValid()
+  {
+    Range r1 = new Range(10, 20);
+    Range r2 = new Range(14, 15);
+    Range r3 = new Range(16, 17);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    node.add(r2);
+    node.add(r3);
+
+    /*
+     * node has root range [10-20] and contains an
+     * NCList of [14-15, 16-17]
+     */
+    assertTrue(node.isValid());
+    PA.setValue(r1, "start", 15);
+    assertFalse(node.isValid()); // r2 not within r1
+    PA.setValue(r1, "start", 10);
+    assertTrue(node.isValid());
+    PA.setValue(r1, "end", 16);
+    assertFalse(node.isValid()); // r3 not within r1
+    PA.setValue(r1, "end", 20);
+    assertTrue(node.isValid());
+    PA.setValue(r3, "start", 12);
+    assertFalse(node.isValid()); // r3 should precede r2
+  }
+}
diff --git a/test/jalview/datamodel/features/RangeComparatorTest.java b/test/jalview/datamodel/features/RangeComparatorTest.java
new file mode 100644 (file)
index 0000000..4849b38
--- /dev/null
@@ -0,0 +1,65 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+
+import java.util.Comparator;
+
+import org.testng.annotations.Test;
+
+public class RangeComparatorTest
+{
+
+  @Test(groups = "Functional")
+  public void testCompare()
+  {
+    RangeComparator comp = new RangeComparator(true);
+
+    // same position, same length
+    assertEquals(comp.compare(10, 10, 20, 20), 0);
+    // same position, len1 > len2
+    assertEquals(comp.compare(10, 10, 20, 19), -1);
+    // same position, len1 < len2
+    assertEquals(comp.compare(10, 10, 20, 21), 1);
+    // pos1 > pos2
+    assertEquals(comp.compare(11, 10, 20, 20), 1);
+    // pos1 < pos2
+    assertEquals(comp.compare(10, 11, 20, 10), -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testCompare_byStart()
+  {
+    Comparator<ContiguousI> comp = RangeComparator.BY_START_POSITION;
+
+    // same start position, same length
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 20)), 0);
+    // same start position, len1 > len2
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 19)), -1);
+    // same start position, len1 < len2
+    assertEquals(comp.compare(new Range(10, 18), new Range(10, 20)), 1);
+    // pos1 > pos2
+    assertEquals(comp.compare(new Range(11, 20), new Range(10, 20)), 1);
+    // pos1 < pos2
+    assertEquals(comp.compare(new Range(10, 20), new Range(11, 20)), -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testCompare_byEnd()
+  {
+    Comparator<ContiguousI> comp = RangeComparator.BY_END_POSITION;
+
+    // same end position, same length
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 20)), 0);
+    // same end position, len1 > len2
+    assertEquals(comp.compare(new Range(10, 20), new Range(11, 20)), -1);
+    // same end position, len1 < len2
+    assertEquals(comp.compare(new Range(11, 20), new Range(10, 20)), 1);
+    // end1 > end2
+    assertEquals(comp.compare(new Range(10, 21), new Range(10, 20)), 1);
+    // end1 < end2
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 21)), -1);
+  }
+}
diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java
new file mode 100644 (file)
index 0000000..a144f03
--- /dev/null
@@ -0,0 +1,1221 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class SequenceFeaturesTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertFalse(store.hasFeatures());
+
+    store = new SequenceFeatures((List<SequenceFeature>) null);
+    assertFalse(store.hasFeatures());
+
+    List<SequenceFeature> features = new ArrayList<>();
+    store = new SequenceFeatures(features);
+    assertFalse(store.hasFeatures());
+
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    features.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 15, 18,
+            Float.NaN, null);
+    features.add(sf2); // nested
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc2", 0, 0,
+            Float.NaN, null); // non-positional
+    features.add(sf3);
+    store = new SequenceFeatures(features);
+    assertTrue(store.hasFeatures());
+    assertEquals(2, store.getFeatureCount(true)); // positional
+    assertEquals(1, store.getFeatureCount(false)); // non-positional
+    assertFalse(store.add(sf1)); // already contained
+    assertFalse(store.add(sf2)); // already contained
+    assertFalse(store.add(sf3)); // already contained
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPositionalFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // same range, different description
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20,
+            Float.NaN, null);
+    store.add(sf2);
+    // discontiguous range
+    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(sf3);
+    // overlapping range
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35,
+            Float.NaN, null);
+    store.add(sf4);
+    // enclosing range
+    SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50,
+            Float.NaN, null);
+    store.add(sf5);
+    // non-positional feature
+    SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf6);
+    // contact feature
+    SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.add(sf7);
+    // different feature type
+    SequenceFeature sf8 = new SequenceFeature("Pfam", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(sf8);
+    SequenceFeature sf9 = new SequenceFeature("Pfam", "desc", 15, 35,
+            Float.NaN, null);
+    store.add(sf9);
+
+    /*
+     * get all positional features
+     */
+    List<SequenceFeature> features = store.getPositionalFeatures();
+    assertEquals(features.size(), 8);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertFalse(features.contains(sf6)); // non-positional
+    assertTrue(features.contains(sf7));
+    assertTrue(features.contains(sf8));
+    assertTrue(features.contains(sf9));
+
+    /*
+     * get features by type
+     */
+    assertTrue(store.getPositionalFeatures((String) null).isEmpty());
+    assertTrue(store.getPositionalFeatures("Cath").isEmpty());
+    assertTrue(store.getPositionalFeatures("METAL").isEmpty());
+
+    features = store.getPositionalFeatures("Metal");
+    assertEquals(features.size(), 5);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertFalse(features.contains(sf6));
+
+    features = store.getPositionalFeatures("Disulphide bond");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf7));
+
+    features = store.getPositionalFeatures("Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf8));
+    assertTrue(features.contains(sf9));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetContactFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    // non-contact
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // non-positional
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf2);
+    // contact feature
+    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.add(sf3);
+    // repeat for different feature type
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18,
+            45, Float.NaN, null);
+    store.add(sf6);
+  
+    /*
+     * get all contact features
+     */
+    List<SequenceFeature> features = store.getContactFeatures();
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf6));
+  
+    /*
+     * get contact features by type
+     */
+    assertTrue(store.getContactFeatures((String) null).isEmpty());
+    assertTrue(store.getContactFeatures("Cath").isEmpty());
+    assertTrue(store.getContactFeatures("Pfam").isEmpty());
+    assertTrue(store.getContactFeatures("DISULPHIDE BOND").isEmpty());
+  
+    features = store.getContactFeatures("Disulphide bond");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+  
+    features = store.getContactFeatures("Disulfide bond");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetNonPositionalFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    // positional
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // non-positional
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf2);
+    // contact feature
+    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.add(sf3);
+    // repeat for different feature type
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18,
+            45, Float.NaN, null);
+    store.add(sf6);
+    // one more non-positional, different description
+    SequenceFeature sf7 = new SequenceFeature("Pfam", "desc2", 0, 0,
+            Float.NaN, null);
+    store.add(sf7);
+  
+    /*
+     * get all non-positional features
+     */
+    List<SequenceFeature> features = store.getNonPositionalFeatures();
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf7));
+  
+    /*
+     * get non-positional features by type
+     */
+    assertTrue(store.getNonPositionalFeatures((String) null).isEmpty());
+    assertTrue(store.getNonPositionalFeatures("Cath").isEmpty());
+    assertTrue(store.getNonPositionalFeatures("PFAM").isEmpty());
+  
+    features = store.getNonPositionalFeatures("Metal");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+  
+    features = store.getNonPositionalFeatures("Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf7));
+  }
+
+  /**
+   * Helper method to add a feature of no particular type
+   * 
+   * @param sf
+   * @param type
+   * @param from
+   * @param to
+   * @return
+   */
+  SequenceFeature addFeature(SequenceFeaturesI sf, String type, int from,
+          int to)
+  {
+    SequenceFeature sf1 = new SequenceFeature(type, "", from, to,
+            Float.NaN,
+            null);
+    sf.add(sf1);
+    return sf1;
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
+    SequenceFeature sf2 = addFeature(sf, "Pfam", 1, 15);
+    SequenceFeature sf3 = addFeature(sf, "Pfam", 20, 30);
+    SequenceFeature sf4 = addFeature(sf, "Pfam", 40, 100);
+    SequenceFeature sf5 = addFeature(sf, "Pfam", 60, 100);
+    SequenceFeature sf6 = addFeature(sf, "Pfam", 70, 70);
+    SequenceFeature sf7 = addFeature(sf, "Cath", 10, 50);
+    SequenceFeature sf8 = addFeature(sf, "Cath", 1, 15);
+    SequenceFeature sf9 = addFeature(sf, "Cath", 20, 30);
+    SequenceFeature sf10 = addFeature(sf, "Cath", 40, 100);
+    SequenceFeature sf11 = addFeature(sf, "Cath", 60, 100);
+    SequenceFeature sf12 = addFeature(sf, "Cath", 70, 70);
+  
+    List<SequenceFeature> overlaps = sf.findFeatures(200, 200, "Pfam");
+    assertTrue(overlaps.isEmpty());
+  
+    overlaps = sf.findFeatures( 1, 9, "Pfam");
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf2));
+  
+    overlaps = sf.findFeatures( 5, 18, "Pfam");
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+  
+    overlaps = sf.findFeatures(30, 40, "Pfam");
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf3));
+    assertTrue(overlaps.contains(sf4));
+  
+    overlaps = sf.findFeatures( 80, 90, "Pfam");
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+  
+    overlaps = sf.findFeatures( 68, 70, "Pfam");
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+    assertTrue(overlaps.contains(sf6));
+
+    overlaps = sf.findFeatures(16, 69, "Cath");
+    assertEquals(overlaps.size(), 4);
+    assertTrue(overlaps.contains(sf7));
+    assertFalse(overlaps.contains(sf8));
+    assertTrue(overlaps.contains(sf9));
+    assertTrue(overlaps.contains(sf10));
+    assertTrue(overlaps.contains(sf11));
+    assertFalse(overlaps.contains(sf12));
+
+    assertTrue(sf.findFeatures(0, 1000, "Metal").isEmpty());
+
+    overlaps = sf.findFeatures(7, 7, (String) null);
+    assertTrue(overlaps.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
+    assertTrue(sf.getPositionalFeatures().contains(sf1));
+
+    assertFalse(sf.delete(null));
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 15, 0f, null);
+    assertFalse(sf.delete(sf2)); // not added, can't delete it
+    assertTrue(sf.delete(sf1));
+    assertTrue(sf.getPositionalFeatures().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testHasFeatures()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    assertFalse(sf.hasFeatures());
+
+    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
+    assertTrue(sf.hasFeatures());
+
+    sf.delete(sf1);
+    assertFalse(sf.hasFeatures());
+  }
+
+  /**
+   * Tests for the method that gets feature groups for positional or
+   * non-positional features
+   */
+  @Test(groups = "Functional")
+  public void testGetFeatureGroups()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    assertTrue(sf.getFeatureGroups(true).isEmpty());
+    assertTrue(sf.getFeatureGroups(false).isEmpty());
+
+    /*
+     * add a non-positional feature (begin/end = 0/0)
+     */
+    SequenceFeature sfx = new SequenceFeature("AType", "Desc", 0, 0, 0f,
+            "AGroup");
+    sf.add(sfx);
+    Set<String> groups = sf.getFeatureGroups(true); // for positional
+    assertTrue(groups.isEmpty());
+    groups = sf.getFeatureGroups(false); // for non-positional
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+    groups = sf.getFeatureGroups(false, "AType");
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+    groups = sf.getFeatureGroups(true, "AnotherType");
+    assertTrue(groups.isEmpty());
+
+    /*
+     * add, then delete, more non-positional features of different types
+     */
+    SequenceFeature sfy = new SequenceFeature("AnotherType", "Desc", 0, 0,
+            0f,
+            "AnotherGroup");
+    sf.add(sfy);
+    SequenceFeature sfz = new SequenceFeature("AThirdType", "Desc", 0, 0,
+            0f,
+            null);
+    sf.add(sfz);
+    groups = sf.getFeatureGroups(false);
+    assertEquals(groups.size(), 3);
+    assertTrue(groups.contains("AGroup"));
+    assertTrue(groups.contains("AnotherGroup"));
+    assertTrue(groups.contains(null)); // null is a possible group
+    sf.delete(sfz);
+    sf.delete(sfy);
+    groups = sf.getFeatureGroups(false);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+
+    /*
+     * add positional features
+     */
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "Desc", 10, 50, 0f,
+            "PfamGroup");
+    sf.add(sf1);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("PfamGroup"));
+    groups = sf.getFeatureGroups(false); // non-positional unchanged
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+
+    SequenceFeature sf2 = new SequenceFeature("Cath", "Desc", 10, 50, 0f,
+            null);
+    sf.add(sf2);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("PfamGroup"));
+    assertTrue(groups.contains(null));
+
+    sf.delete(sf1);
+    sf.delete(sf2);
+    assertTrue(sf.getFeatureGroups(true).isEmpty());
+
+    SequenceFeature sf3 = new SequenceFeature("CDS", "", 10, 50, 0f,
+            "Ensembl");
+    sf.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("exon", "", 10, 50, 0f,
+            "Ensembl");
+    sf.add(sf4);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("Ensembl"));
+
+    /*
+     * delete last Ensembl group feature from CDS features
+     * but still have one in exon features
+     */
+    sf.delete(sf3);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("Ensembl"));
+
+    /*
+     * delete the last non-positional feature
+     */
+    sf.delete(sfx);
+    groups = sf.getFeatureGroups(false);
+    assertTrue(groups.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureTypesForGroups()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty());
+  
+    /*
+     * add feature with group = "Uniprot", type = "helix"
+     */
+    String groupUniprot = "Uniprot";
+    SequenceFeature sf1 = new SequenceFeature("helix", "Desc", 10, 50, 0f,
+            groupUniprot);
+    sf.add(sf1);
+    Set<String> groups = sf.getFeatureTypesForGroups(true, groupUniprot);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("helix"));
+    assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty());
+  
+    /*
+     * add feature with group = "Uniprot", type = "strand"
+     */
+    SequenceFeature sf2 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
+            groupUniprot);
+    sf.add(sf2);
+    groups = sf.getFeatureTypesForGroups(true, groupUniprot);
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("helix"));
+    assertTrue(groups.contains("strand"));
+
+    /*
+     * delete the "strand" Uniprot feature - still have "helix"
+     */
+    sf.delete(sf2);
+    groups = sf.getFeatureTypesForGroups(true, groupUniprot);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("helix"));
+
+    /*
+     * delete the "helix" Uniprot feature - none left
+     */
+    sf.delete(sf1);
+    assertTrue(sf.getFeatureTypesForGroups(true, groupUniprot).isEmpty());
+
+    /*
+     * add some null group features
+     */
+    SequenceFeature sf3 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
+            null);
+    sf.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("turn", "Desc", 10, 50, 0f,
+            null);
+    sf.add(sf4);
+    groups = sf.getFeatureTypesForGroups(true, (String) null);
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("strand"));
+    assertTrue(groups.contains("turn"));
+
+    /*
+     * add strand/Cath  and turn/Scop and query for one or both groups
+     * (find feature types for groups selected in Feature Settings)
+     */
+    SequenceFeature sf5 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
+            "Cath");
+    sf.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("turn", "Desc", 10, 50, 0f,
+            "Scop");
+    sf.add(sf6);
+    groups = sf.getFeatureTypesForGroups(true, "Cath");
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("strand"));
+    groups = sf.getFeatureTypesForGroups(true, "Scop");
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("turn"));
+    groups = sf.getFeatureTypesForGroups(true, "Cath", "Scop");
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("turn"));
+    assertTrue(groups.contains("strand"));
+    // alternative vararg syntax
+    groups = sf.getFeatureTypesForGroups(true, new String[] { "Cath",
+        "Scop" });
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("turn"));
+    assertTrue(groups.contains("strand"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureTypes()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    Set<String> types = store.getFeatureTypes();
+    assertTrue(types.isEmpty());
+
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 1);
+    assertTrue(types.contains("Metal"));
+
+    // null type is rejected...
+    SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20,
+            Float.NaN, null);
+    assertFalse(store.add(sf2));
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 1);
+    assertFalse(types.contains(null));
+    assertTrue(types.contains("Metal"));
+
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 2);
+    assertTrue(types.contains("Pfam"));
+
+    /*
+     * add contact feature
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 3);
+    assertTrue(types.contains("Disulphide Bond"));
+
+    /*
+     * add another Pfam
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 3); // unchanged
+
+    /*
+     * delete first Pfam - still have one
+     */
+    assertTrue(store.delete(sf3));
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 3);
+    assertTrue(types.contains("Pfam"));
+
+    /*
+     * delete second Pfam - no longer have one
+     */
+    assertTrue(store.delete(sf5));
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 2);
+    assertFalse(types.contains("Pfam"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureCount()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertEquals(store.getFeatureCount(true), 0);
+    assertEquals(store.getFeatureCount(false), 0);
+  
+    /*
+     * add positional
+     */
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    assertEquals(store.getFeatureCount(true), 1);
+    assertEquals(store.getFeatureCount(false), 0);
+
+    /*
+     * null feature type is rejected
+     */
+    SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20,
+            Float.NaN, null);
+    assertFalse(store.add(sf2));
+    assertEquals(store.getFeatureCount(true), 1);
+    assertEquals(store.getFeatureCount(false), 0);
+  
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    assertEquals(store.getFeatureCount(true), 1);
+    assertEquals(store.getFeatureCount(false), 1);
+  
+    /*
+     * add contact feature (counts as 1)
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    assertEquals(store.getFeatureCount(true), 2);
+    assertEquals(store.getFeatureCount(false), 1);
+  
+    /*
+     * add another Pfam but this time as a positional feature
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    assertEquals(store.getFeatureCount(true), 3); // sf1, sf4, sf5
+    assertEquals(store.getFeatureCount(false), 1); // sf3
+    assertEquals(store.getFeatureCount(true, "Pfam"), 1); // positional
+    assertEquals(store.getFeatureCount(false, "Pfam"), 1); // non-positional
+    // search for type==null
+    assertEquals(store.getFeatureCount(true, (String) null), 0);
+    // search with no type specified
+    assertEquals(store.getFeatureCount(true, (String[]) null), 3);
+    assertEquals(store.getFeatureCount(true, "Metal", "Cath"), 1);
+    assertEquals(store.getFeatureCount(true, "Disulphide Bond"), 1);
+    assertEquals(store.getFeatureCount(true, "Metal", "Pfam", null), 2);
+
+    /*
+     * delete first Pfam (non-positional)
+     */
+    assertTrue(store.delete(sf3));
+    assertEquals(store.getFeatureCount(true), 3);
+    assertEquals(store.getFeatureCount(false), 0);
+  
+    /*
+     * delete second Pfam (positional)
+     */
+    assertTrue(store.delete(sf5));
+    assertEquals(store.getFeatureCount(true), 2);
+    assertEquals(store.getFeatureCount(false), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAllFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    List<SequenceFeature> features = store.getAllFeatures();
+    assertTrue(features.isEmpty());
+  
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+  
+    SequenceFeature sf2 = new SequenceFeature("Metallic", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf2);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf2));
+  
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf3));
+  
+    /*
+     * add contact feature
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 4);
+    assertTrue(features.contains(sf4));
+  
+    /*
+     * add another Pfam
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 5);
+    assertTrue(features.contains(sf5));
+
+    /*
+     * select by type does not apply to non-positional features
+     */
+    features = store.getAllFeatures("Cath");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+
+    features = store.getAllFeatures("Pfam", "Cath", "Metal");
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+  
+    /*
+     * delete first Pfam
+     */
+    assertTrue(store.delete(sf3));
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 4);
+    assertFalse(features.contains(sf3));
+  
+    /*
+     * delete second Pfam
+     */
+    assertTrue(store.delete(sf5));
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 3);
+    assertFalse(features.contains(sf3));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetTotalFeatureLength()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertEquals(store.getTotalFeatureLength(), 0);
+
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    assertTrue(store.add(sf1));
+    assertEquals(store.getTotalFeatureLength(), 11);
+    assertEquals(store.getTotalFeatureLength("Metal"), 11);
+    assertEquals(store.getTotalFeatureLength("Plastic"), 0);
+
+    // re-add does nothing!
+    assertFalse(store.add(sf1));
+    assertEquals(store.getTotalFeatureLength(), 11);
+
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    assertEquals(store.getTotalFeatureLength(), 11);
+
+    /*
+     * add contact feature - counts 1 to feature length
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    assertEquals(store.getTotalFeatureLength(), 12);
+
+    /*
+     * add another Pfam
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    assertEquals(store.getTotalFeatureLength(), 23);
+
+    /*
+     * delete features
+     */
+    assertTrue(store.delete(sf3)); // non-positional
+    assertEquals(store.getTotalFeatureLength(), 23); // no change
+
+    assertTrue(store.delete(sf5));
+    assertEquals(store.getTotalFeatureLength(), 12);
+
+    assertTrue(store.delete(sf4)); // contact
+    assertEquals(store.getTotalFeatureLength(), 11);
+
+    assertTrue(store.delete(sf1));
+    assertEquals(store.getTotalFeatureLength(), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinimumScore_getMaximumScore()
+  {
+    SequenceFeatures sf = new SequenceFeatures();
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, "group"); // non-positional, no score
+    sf.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 10, 20,
+            Float.NaN, "group"); // positional, no score
+    sf.add(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 10, 20, 1f,
+            "group");
+    sf.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 12, 16, 4f,
+            "group");
+    sf.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 0, 0, 11f,
+            "group");
+    sf.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, -7f,
+            "group");
+    sf.add(sf6);
+
+    assertEquals(sf.getMinimumScore("nosuchtype", true), Float.NaN);
+    assertEquals(sf.getMinimumScore("nosuchtype", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("nosuchtype", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("nosuchtype", false), Float.NaN);
+
+    // positional features min-max:
+    assertEquals(sf.getMinimumScore("Metal", true), 1f);
+    assertEquals(sf.getMaximumScore("Metal", true), 4f);
+    assertEquals(sf.getMinimumScore("Cath", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("Cath", true), Float.NaN);
+
+    // non-positional features min-max:
+    assertEquals(sf.getMinimumScore("Cath", false), -7f);
+    assertEquals(sf.getMaximumScore("Cath", false), 11f);
+    assertEquals(sf.getMinimumScore("Metal", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("Metal", false), Float.NaN);
+
+    // delete features; min-max should get recomputed
+    sf.delete(sf6);
+    assertEquals(sf.getMinimumScore("Cath", false), 11f);
+    assertEquals(sf.getMaximumScore("Cath", false), 11f);
+    sf.delete(sf4);
+    assertEquals(sf.getMinimumScore("Metal", true), 1f);
+    assertEquals(sf.getMaximumScore("Metal", true), 1f);
+    sf.delete(sf5);
+    assertEquals(sf.getMinimumScore("Cath", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("Cath", false), Float.NaN);
+    sf.delete(sf3);
+    assertEquals(sf.getMinimumScore("Metal", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("Metal", true), Float.NaN);
+    sf.delete(sf1);
+    sf.delete(sf2);
+    assertFalse(sf.hasFeatures());
+    assertEquals(sf.getMinimumScore("Cath", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("Cath", false), Float.NaN);
+    assertEquals(sf.getMinimumScore("Metal", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("Metal", true), Float.NaN);
+  }
+
+  @Test(groups = "Functional")
+  public void testVarargsToTypes()
+  {
+    SequenceFeatures sf = new SequenceFeatures();
+    sf.add(new SequenceFeature("Metal", "desc", 0, 0, Float.NaN, "group"));
+    sf.add(new SequenceFeature("Cath", "desc", 10, 20, Float.NaN, "group"));
+
+    /*
+     * no type specified - get all types stored
+     * they are returned in keyset (alphabetical) order
+     */
+    Map<String, FeatureStore> featureStores = (Map<String, FeatureStore>) PA
+            .getValue(sf, "featureStore");
+
+    Iterable<FeatureStore> types = sf.varargToTypes();
+    Iterator<FeatureStore> iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * empty array is the same as no vararg parameter supplied
+     * so treated as all stored types
+     */
+    types = sf.varargToTypes(new String[] {});
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * null type specified; this is passed as vararg
+     * String[1] {null}
+     */
+    types = sf.varargToTypes((String) null);
+    assertFalse(types.iterator().hasNext());
+
+    /*
+     * null types array specified; this is passed as vararg null
+     */
+    types = sf.varargToTypes((String[]) null);
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one type specified
+     */
+    types = sf.varargToTypes("Metal");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two types specified - get sorted alphabetically
+     */
+    types = sf.varargToTypes("Metal", "Cath");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * null type included - should be ignored
+     */
+    types = sf.varargToTypes("Metal", null, "Helix");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureTypes_byOntology()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+  
+    SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // mRNA isA mature_transcript isA transcript
+    SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf2);
+    // just to prove non-positional feature types are included
+    SequenceFeature sf3 = new SequenceFeature("mRNA", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("CDS", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf4);
+
+    Set<String> types = store.getFeatureTypes("transcript");
+    assertEquals(types.size(), 2);
+    assertTrue(types.contains("transcript"));
+    assertTrue(types.contains("mRNA"));
+
+    // matches include arguments whether SO terms or not
+    types = store.getFeatureTypes("transcript", "CDS");
+    assertEquals(types.size(), 3);
+    assertTrue(types.contains("transcript"));
+    assertTrue(types.contains("mRNA"));
+    assertTrue(types.contains("CDS"));
+
+    types = store.getFeatureTypes("exon");
+    assertTrue(types.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeaturesByOntology()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    List<SequenceFeature> features = store.getFeaturesByOntology();
+    assertTrue(features.isEmpty());
+    assertTrue(store.getFeaturesByOntology(new String[] {}).isEmpty());
+    assertTrue(store.getFeaturesByOntology((String[]) null).isEmpty());
+  
+    SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+
+    // mRNA isA transcript; added here 'as if' non-positional
+    // just to show that non-positional features are included in results
+    SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf2);
+
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(sf3);
+
+    features = store.getFeaturesByOntology("transcript");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+
+    features = store.getFeaturesByOntology("mRNA");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+
+    features = store.getFeaturesByOntology("mRNA", "Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+  }
+
+  @Test(groups = "Functional")
+  public void testSortFeatures()
+  {
+    List<SequenceFeature> sfs = new ArrayList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 80,
+            Float.NaN, null);
+    sfs.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Rfam", "desc", 40, 50,
+            Float.NaN, null);
+    sfs.add(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Rfam", "desc", 50, 60,
+            Float.NaN, null);
+    sfs.add(sf3);
+
+    // sort by end position descending
+    SequenceFeatures.sortFeatures(sfs, false);
+    assertSame(sfs.get(0), sf1);
+    assertSame(sfs.get(1), sf3);
+    assertSame(sfs.get(2), sf2);
+
+    // sort by start position ascending
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertSame(sfs.get(0), sf1);
+    assertSame(sfs.get(1), sf2);
+    assertSame(sfs.get(2), sf3);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeaturesForGroup()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+
+    List<SequenceFeature> features = store.getFeaturesForGroup(true, null);
+    assertTrue(features.isEmpty());
+    assertTrue(store.getFeaturesForGroup(false, null).isEmpty());
+    assertTrue(store.getFeaturesForGroup(true, "Uniprot").isEmpty());
+    assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty());
+
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            null);
+    SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
+            null);
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            "Uniprot");
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 0, 0, 0f,
+            "Rfam");
+    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 5, 15, 0f,
+            null);
+    store.add(sf1);
+    store.add(sf2);
+    store.add(sf3);
+    store.add(sf4);
+    store.add(sf5);
+
+    // positional features for null group, any type
+    features = store.getFeaturesForGroup(true, null);
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf5));
+
+    // positional features for null group, specified type
+    features = store.getFeaturesForGroup(true, null, new String[] { "Pfam",
+        "Xfam" });
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+    features = store.getFeaturesForGroup(true, null, new String[] { "Pfam",
+        "Xfam", "Cath" });
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf5));
+
+    // positional features for non-null group, any type
+    features = store.getFeaturesForGroup(true, "Uniprot");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+    assertTrue(store.getFeaturesForGroup(true, "Rfam").isEmpty());
+
+    // positional features for non-null group, specified type
+    features = store.getFeaturesForGroup(true, "Uniprot", "Pfam", "Xfam",
+            "Rfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+    assertTrue(store.getFeaturesForGroup(true, "Uniprot", "Cath").isEmpty());
+
+    // non-positional features for null group, any type
+    features = store.getFeaturesForGroup(false, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+
+    // non-positional features for null group, specified type
+    features = store.getFeaturesForGroup(false, null, "Pfam", "Xfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+    assertTrue(store.getFeaturesForGroup(false, null, "Cath").isEmpty());
+
+    // non-positional features for non-null group, any type
+    features = store.getFeaturesForGroup(false, "Rfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf4));
+    assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty());
+
+    // non-positional features for non-null group, specified type
+    features = store.getFeaturesForGroup(false, "Rfam", "Pfam", "Metal");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf4));
+    assertTrue(store.getFeaturesForGroup(false, "Rfam", "Cath", "Pfam")
+            .isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testShiftFeatures()
+  {
+    SequenceFeatures store = new SequenceFeatures();
+    assertFalse(store.shiftFeatures(1));
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
+    store.add(sf1);
+    // nested feature:
+    SequenceFeature sf2 = new SequenceFeature("Metal", "", 8, 14, 0f, null);
+    store.add(sf2);
+    // contact feature:
+    SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32,
+            0f, null);
+    store.add(sf3);
+    // non-positional feature:
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "", 0, 0, 0f, null);
+    store.add(sf4);
+  
+    /*
+     * shift features right by 5
+     */
+    assertTrue(store.shiftFeatures(5));
+  
+    // non-positional features untouched:
+    List<SequenceFeature> nonPos = store.getNonPositionalFeatures();
+    assertEquals(nonPos.size(), 1);
+    assertTrue(nonPos.contains(sf4));
+  
+    // positional features are replaced
+    List<SequenceFeature> pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 3);
+    assertFalse(pos.contains(sf1));
+    assertFalse(pos.contains(sf2));
+    assertFalse(pos.contains(sf3));
+    SequenceFeatures.sortFeatures(pos, true); // ascending start pos
+    assertEquals(pos.get(0).getBegin(), 7);
+    assertEquals(pos.get(0).getEnd(), 10);
+    assertEquals(pos.get(0).getType(), "Cath");
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 19);
+    assertEquals(pos.get(1).getType(), "Metal");
+    assertEquals(pos.get(2).getBegin(), 28);
+    assertEquals(pos.get(2).getEnd(), 37);
+    assertEquals(pos.get(2).getType(), "Disulfide bond");
+  
+    /*
+     * now shift left by 15
+     * feature at [7-10] should be removed
+     * feature at [13-19] should become [1-4] 
+     */
+    assertTrue(store.shiftFeatures(-15));
+    pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(0).getType(), "Metal");
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 22);
+    assertEquals(pos.get(1).getType(), "Disulfide bond");
+  }
+
+  @Test(groups = "Functional")
+  public void testIsOntologyTerm()
+  {
+    SequenceFeatures store = new SequenceFeatures();
+    assertTrue(store.isOntologyTerm("gobbledygook"));
+    assertTrue(store.isOntologyTerm("transcript", "transcript"));
+    assertTrue(store.isOntologyTerm("mRNA", "transcript"));
+    assertFalse(store.isOntologyTerm("transcript", "mRNA"));
+    assertTrue(store.isOntologyTerm("junk", "transcript", "junk"));
+    assertTrue(store.isOntologyTerm("junk", new String[] {}));
+    assertTrue(store.isOntologyTerm("junk", (String[]) null));
+  }
+}
index fb0204b..6611e05 100644 (file)
@@ -212,14 +212,16 @@ public class EnsemblCdnaTest
             20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("aberrant_processed_transcript");
+    sf = new SequenceFeature("aberrant_processed_transcript", "", 20000,
+            20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500,
+            0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
     // other feature with no parent is retained
-    sf.setType("sequence_variant");
+    sf = new SequenceFeature("sequence_variant", "", 20000, 20500, 0f, null);
     assertTrue(testee.retainFeature(sf, accId));
 
     // other feature with desired parent is retained
@@ -254,15 +256,18 @@ public class EnsemblCdnaTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // exon sub-type with right parent is valid
-    sf.setType("coding_exon");
+    sf = new SequenceFeature("coding_exon", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript not valid:
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // CDS not valid:
-    sf.setType("CDS");
+    sf = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index b7f9f8d..8482c90 100644 (file)
@@ -130,11 +130,12 @@ public class EnsemblCdsTest
             null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("CDS_predicted");
+    sf = new SequenceFeature("CDS_predicted", "", 20000, 20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
     // other feature with no parent is retained
-    sf.setType("sequence_variant");
+    sf = new SequenceFeature("CDS_psequence_variantredicted", "", 20000,
+            20500, 0f, null);
     assertTrue(testee.retainFeature(sf, accId));
 
     // other feature with desired parent is retained
@@ -169,15 +170,18 @@ public class EnsemblCdsTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // cds sub-type with right parent is valid
-    sf.setType("CDS_predicted");
+    sf = new SequenceFeature("CDS_predicted", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript not valid:
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // exon not valid:
-    sf.setType("exon");
+    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index 6cfd85b..a8c491c 100644 (file)
@@ -22,7 +22,6 @@ package jalview.ext.ensembl;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.api.FeatureSettingsModelI;
@@ -76,7 +75,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // gene at (start+10000) length 501
+    // gene at (start+20000) length 501
+    // should be ignored - the first 'gene' found defines the whole range
+    // (note features are found in position order, not addition order)
     SequenceFeature sf = new SequenceFeature("gene", "", 20000, 20500, 0f,
             null);
     sf.setValue("ID", "gene:" + geneId);
@@ -84,7 +85,6 @@ public class EnsemblGeneTest
     genomic.addSequenceFeature(sf);
 
     // gene at (start + 10500) length 101
-    // should be ignored - the first 'gene' found defines the whole range
     sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
@@ -94,13 +94,13 @@ public class EnsemblGeneTest
             23);
     List<int[]> fromRanges = ranges.getFromRanges();
     assertEquals(1, fromRanges.size());
-    assertEquals(20000, fromRanges.get(0)[0]);
-    assertEquals(20500, fromRanges.get(0)[1]);
+    assertEquals(10500, fromRanges.get(0)[0]);
+    assertEquals(10600, fromRanges.get(0)[1]);
     // to range should start from given start numbering
     List<int[]> toRanges = ranges.getToRanges();
     assertEquals(1, toRanges.size());
     assertEquals(23, toRanges.get(0)[0]);
-    assertEquals(523, toRanges.get(0)[1]);
+    assertEquals(123, toRanges.get(0)[1]);
   }
 
   /**
@@ -115,7 +115,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // gene at (start+10000) length 501
+    // gene at (start+20000) length 501
+    // should be ignored - the first 'gene' found defines the whole range
+    // (real data would only have one such feature)
     SequenceFeature sf = new SequenceFeature("ncRNA_gene", "", 20000,
             20500, 0f, null);
     sf.setValue("ID", "gene:" + geneId);
@@ -123,8 +125,6 @@ public class EnsemblGeneTest
     genomic.addSequenceFeature(sf);
 
     // gene at (start + 10500) length 101
-    // should be ignored - the first 'gene' found defines the whole range
-    // (real data would only have one such feature)
     sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
@@ -135,13 +135,13 @@ public class EnsemblGeneTest
     List<int[]> fromRanges = ranges.getFromRanges();
     assertEquals(1, fromRanges.size());
     // from range on reverse strand:
-    assertEquals(20500, fromRanges.get(0)[0]);
-    assertEquals(20000, fromRanges.get(0)[1]);
+    assertEquals(10500, fromRanges.get(0)[0]);
+    assertEquals(10600, fromRanges.get(0)[1]);
     // to range should start from given start numbering
     List<int[]> toRanges = ranges.getToRanges();
     assertEquals(1, toRanges.size());
     assertEquals(23, toRanges.get(0)[0]);
-    assertEquals(523, toRanges.get(0)[1]);
+    assertEquals(123, toRanges.get(0)[1]);
   }
 
   /**
@@ -164,7 +164,7 @@ public class EnsemblGeneTest
     genomic.addSequenceFeature(sf1);
 
     // transcript sub-type feature
-    SequenceFeature sf2 = new SequenceFeature("snRNA", "", 20000, 20500,
+    SequenceFeature sf2 = new SequenceFeature("snRNA", "", 21000, 21500,
             0f, null);
     sf2.setValue("Parent", "gene:" + geneId);
     sf2.setValue("transcript_id", "transcript2");
@@ -172,13 +172,13 @@ public class EnsemblGeneTest
 
     // NMD_transcript_variant treated like transcript in Ensembl
     SequenceFeature sf3 = new SequenceFeature("NMD_transcript_variant", "",
-            20000, 20500, 0f, null);
+            22000, 22500, 0f, null);
     sf3.setValue("Parent", "gene:" + geneId);
     sf3.setValue("transcript_id", "transcript3");
     genomic.addSequenceFeature(sf3);
 
     // transcript for a different gene - ignored
-    SequenceFeature sf4 = new SequenceFeature("snRNA", "", 20000, 20500,
+    SequenceFeature sf4 = new SequenceFeature("snRNA", "", 23000, 23500,
             0f, null);
     sf4.setValue("Parent", "gene:XYZ");
     sf4.setValue("transcript_id", "transcript4");
@@ -192,9 +192,9 @@ public class EnsemblGeneTest
     List<SequenceFeature> features = testee.getTranscriptFeatures(geneId,
             genomic);
     assertEquals(3, features.size());
-    assertSame(sf1, features.get(0));
-    assertSame(sf2, features.get(1));
-    assertSame(sf3, features.get(2));
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
   }
 
   /**
@@ -211,22 +211,24 @@ public class EnsemblGeneTest
     sf.setValue("ID", "gene:" + geneId);
     assertFalse(testee.retainFeature(sf, geneId));
 
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 20000, 20500, 0f, null);
     sf.setValue("Parent", "gene:" + geneId);
     assertTrue(testee.retainFeature(sf, geneId));
 
-    sf.setType("mature_transcript");
+    sf = new SequenceFeature("mature_transcript", "", 20000, 20500, 0f,
+            null);
     sf.setValue("Parent", "gene:" + geneId);
     assertTrue(testee.retainFeature(sf, geneId));
 
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500,
+            0f, null);
     sf.setValue("Parent", "gene:" + geneId);
     assertTrue(testee.retainFeature(sf, geneId));
 
     sf.setValue("Parent", "gene:XYZ");
     assertFalse(testee.retainFeature(sf, geneId));
 
-    sf.setType("anything");
+    sf = new SequenceFeature("anything", "", 20000, 20500, 0f, null);
     assertTrue(testee.retainFeature(sf, geneId));
   }
 
@@ -253,15 +255,18 @@ public class EnsemblGeneTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // gene sub-type with right ID is valid
-    sf.setType("snRNA_gene");
+    sf = new SequenceFeature("snRNA_gene", "", 1, 2, 0f, null);
+    sf.setValue("ID", "gene:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript not valid:
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf.setValue("ID", "gene:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // exon not valid:
-    sf.setType("exon");
+    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf.setValue("ID", "gene:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index 654797c..8687da9 100644 (file)
@@ -136,14 +136,16 @@ public class EnsemblGenomeTest
             20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("mature_transcript");
+    sf = new SequenceFeature("mature_transcript", "", 20000, 20500, 0f,
+            null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500,
+            0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
     // other feature with no parent is kept
-    sf.setType("anything");
+    sf = new SequenceFeature("anything", "", 20000, 20500, 0f, null);
     assertTrue(testee.retainFeature(sf, accId));
 
     // other feature with correct parent is kept
@@ -179,19 +181,23 @@ public class EnsemblGenomeTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript sub-type with right ID is valid
-    sf.setType("ncRNA");
+    sf = new SequenceFeature("ncRNA", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // Ensembl treats NMD_transcript_variant as if a transcript
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // gene not valid:
-    sf.setType("gene");
+    sf = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // exon not valid:
-    sf.setType("exon");
+    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index e977233..aa2c315 100644 (file)
@@ -22,12 +22,13 @@ package jalview.ext.ensembl;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
-import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
 import jalview.datamodel.Alignment;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FastaFile;
@@ -37,6 +38,7 @@ import jalview.io.gff.SequenceOntologyLite;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.List;
 
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
@@ -269,15 +271,22 @@ public class EnsemblSeqProxyTest
     SequenceFeature sf2 = new SequenceFeature("", "", 8, 12, 0f, null);
     SequenceFeature sf3 = new SequenceFeature("", "", 8, 13, 0f, null);
     SequenceFeature sf4 = new SequenceFeature("", "", 11, 11, 0f, null);
-    SequenceFeature[] sfs = new SequenceFeature[] { sf1, sf2, sf3, sf4 };
+    List<SequenceFeature> sfs = Arrays.asList(new SequenceFeature[] { sf1,
+        sf2, sf3, sf4 });
 
     // sort by start position ascending (forward strand)
     // sf2 and sf3 tie and should not be reordered by sorting
-    EnsemblSeqProxy.sortFeatures(sfs, true);
-    assertArrayEquals(new SequenceFeature[] { sf2, sf3, sf1, sf4 }, sfs);
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertSame(sfs.get(0), sf2);
+    assertSame(sfs.get(1), sf3);
+    assertSame(sfs.get(2), sf1);
+    assertSame(sfs.get(3), sf4);
 
     // sort by end position descending (reverse strand)
-    EnsemblSeqProxy.sortFeatures(sfs, false);
-    assertArrayEquals(new SequenceFeature[] { sf1, sf3, sf2, sf4 }, sfs);
+    SequenceFeatures.sortFeatures(sfs, false);
+    assertSame(sfs.get(0), sf1);
+    assertSame(sfs.get(1), sf3);
+    assertSame(sfs.get(2), sf2);
+    assertSame(sfs.get(3), sf4);
   }
 }
index 36e9b20..f5e637c 100644 (file)
@@ -277,10 +277,9 @@ public class JmolParserTest
     /*
      * the ID is also the group for features derived from structure data 
      */
-    assertNotNull(structureData.getSeqs().get(0).getSequenceFeatures()[0].featureGroup);
-    assertEquals(
-            structureData.getSeqs().get(0).getSequenceFeatures()[0].featureGroup,
-            "localstruct");
-
+    String featureGroup = structureData.getSeqs().get(0)
+            .getSequenceFeatures().get(0).featureGroup;
+    assertNotNull(featureGroup);
+    assertEquals(featureGroup, "localstruct");
   }
 }
index 85fc039..c6c1a29 100644 (file)
@@ -152,10 +152,10 @@ public class TestAnnotate3D
         {
           {
             SequenceI struseq = null;
-            String sq_ = new String(sq.getSequence()).toLowerCase();
+            String sq_ = sq.getSequenceAsString().toLowerCase();
             for (SequenceI _struseq : pdbf.getSeqsAsArray())
             {
-              final String lowerCase = new String(_struseq.getSequence())
+              final String lowerCase = _struseq.getSequenceAsString()
                       .toLowerCase();
               if (lowerCase.equals(sq_))
               {
index 29fd092..734f7eb 100644 (file)
@@ -39,6 +39,7 @@ import jalview.gui.JvOptionPane;
 import jalview.gui.Preferences;
 import jalview.gui.StructureViewer;
 import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -50,7 +51,6 @@ import java.io.File;
 import java.io.IOException;
 import java.util.List;
 import java.util.Vector;
-import jalview.io.DataSourceType;
 
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.AfterMethod;
@@ -440,15 +440,18 @@ public class JalviewChimeraView
     binding.copyStructureAttributesToFeatures("phi", af.getViewport()
             .getAlignPanel());
     fr.setVisible("phi");
-    List<SequenceFeature> fs = fr.findFeaturesAtRes(fer2Arath, 54);
+    List<SequenceFeature> fs = fer2Arath.getFeatures().findFeatures(54, 54);
     assertEquals(fs.size(), 3);
-    assertEquals(fs.get(0).getType(), "RESNUM");
-    assertEquals(fs.get(1).getType(), "phi");
-    assertEquals(fs.get(2).getType(), "phi");
-    assertEquals(fs.get(1).getDescription(), "A"); // chain
-    assertEquals(fs.get(2).getDescription(), "B");
-    assertEquals(fs.get(1).getScore(), -131.0713f, 0.001f);
-    assertEquals(fs.get(2).getScore(), -127.39512, 0.001f);
+    /*
+     * order of returned features is not guaranteed
+     */
+    assertTrue("RESNUM".equals(fs.get(0).getType())
+            || "RESNUM".equals(fs.get(1).getType())
+            || "RESNUM".equals(fs.get(2).getType()));
+    assertTrue(fs.contains(new SequenceFeature("phi", "A", 54, 54,
+            -131.0713f, "Chimera")));
+    assertTrue(fs.contains(new SequenceFeature("phi", "B", 54, 54,
+            -127.39512f, "Chimera")));
 
     /*
      * tear down - also in AfterMethod
@@ -470,7 +473,8 @@ public class JalviewChimeraView
           int res, String featureType)
   {
     String where = "at position " + res;
-    List<SequenceFeature> fs = fr.findFeaturesAtRes(seq, res);
+    List<SequenceFeature> fs = seq.getFeatures().findFeatures(res, res);
+
     assertEquals(fs.size(), 2, where);
     assertEquals(fs.get(0).getType(), "RESNUM", where);
     SequenceFeature sf = fs.get(1);
index b2286e0..812fd8f 100644 (file)
@@ -75,6 +75,13 @@ public class AlignViewportTest
   {
     Jalview.main(new String[] { "-nonews", "-props",
         "test/jalview/testProps.jvprops" });
+
+    /*
+     * remove any sequence mappings left lying around by other tests
+     */
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+    ssm.resetAll();
   }
 
   @BeforeMethod(alwaysRun = true)
@@ -173,18 +180,19 @@ public class AlignViewportTest
      */
     StructureSelectionManager ssm = StructureSelectionManager
             .getStructureSelectionManager(Desktop.instance);
-    assertEquals(2, ssm.getSequenceMappings().size());
-    assertTrue(ssm.getSequenceMappings().contains(acf1));
-    assertTrue(ssm.getSequenceMappings().contains(acf2));
+    List<AlignedCodonFrame> sequenceMappings = ssm.getSequenceMappings();
+    assertEquals(2, sequenceMappings.size());
+    assertTrue(sequenceMappings.contains(acf1));
+    assertTrue(sequenceMappings.contains(acf2));
 
     /*
      * Close the second view. Verify that mappings are not removed as the first
      * view still holds a reference to them.
      */
     af1.closeMenuItem_actionPerformed(false);
-    assertEquals(2, ssm.getSequenceMappings().size());
-    assertTrue(ssm.getSequenceMappings().contains(acf1));
-    assertTrue(ssm.getSequenceMappings().contains(acf2));
+    assertEquals(2, sequenceMappings.size());
+    assertTrue(sequenceMappings.contains(acf1));
+    assertTrue(sequenceMappings.contains(acf2));
   }
 
   /**
index d8ae999..e14a478 100644 (file)
@@ -30,12 +30,14 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.structure.StructureImportSettings;
 import jalview.structure.StructureImportSettings.StructureParser;
 
 import java.io.File;
+import java.util.List;
 
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
@@ -127,32 +129,35 @@ public class AnnotatedPDBFileInputTest
     /*
      * 1GAQ/A
      */
-    SequenceFeature[] sf = al.getSequenceAt(0).getSequenceFeatures();
-    assertEquals(296, sf.length);
-    assertEquals("RESNUM", sf[0].getType());
-    assertEquals("GLU:  19  1gaqA", sf[0].getDescription());
-    assertEquals("RESNUM", sf[295].getType());
-    assertEquals("TYR: 314  1gaqA", sf[295].getDescription());
+    List<SequenceFeature> sf = al.getSequenceAt(0).getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sf, true);
+    assertEquals(296, sf.size());
+    assertEquals("RESNUM", sf.get(0).getType());
+    assertEquals("GLU:  19  1gaqA", sf.get(0).getDescription());
+    assertEquals("RESNUM", sf.get(295).getType());
+    assertEquals("TYR: 314  1gaqA", sf.get(295).getDescription());
 
     /*
      * 1GAQ/B
      */
     sf = al.getSequenceAt(1).getSequenceFeatures();
-    assertEquals(98, sf.length);
-    assertEquals("RESNUM", sf[0].getType());
-    assertEquals("ALA:   1  1gaqB", sf[0].getDescription());
-    assertEquals("RESNUM", sf[97].getType());
-    assertEquals("ALA:  98  1gaqB", sf[97].getDescription());
+    SequenceFeatures.sortFeatures(sf, true);
+    assertEquals(98, sf.size());
+    assertEquals("RESNUM", sf.get(0).getType());
+    assertEquals("ALA:   1  1gaqB", sf.get(0).getDescription());
+    assertEquals("RESNUM", sf.get(97).getType());
+    assertEquals("ALA:  98  1gaqB", sf.get(97).getDescription());
 
     /*
      * 1GAQ/C
      */
     sf = al.getSequenceAt(2).getSequenceFeatures();
-    assertEquals(296, sf.length);
-    assertEquals("RESNUM", sf[0].getType());
-    assertEquals("GLU:  19  1gaqC", sf[0].getDescription());
-    assertEquals("RESNUM", sf[295].getType());
-    assertEquals("TYR: 314  1gaqC", sf[295].getDescription());
+    SequenceFeatures.sortFeatures(sf, true);
+    assertEquals(296, sf.size());
+    assertEquals("RESNUM", sf.get(0).getType());
+    assertEquals("GLU:  19  1gaqC", sf.get(0).getDescription());
+    assertEquals("RESNUM", sf.get(295).getType());
+    assertEquals("TYR: 314  1gaqC", sf.get(295).getDescription());
   }
 
   @Test(groups = { "Functional" })
index cc7dca0..152ab84 100644 (file)
@@ -23,7 +23,6 @@ package jalview.io;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNotNull;
-import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.api.FeatureColourI;
@@ -33,19 +32,39 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
+import jalview.structure.StructureSelectionManager;
 
 import java.awt.Color;
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 public class FeaturesFileTest
 {
+  private static String simpleGffFile = "examples/testdata/simpleGff3.gff";
+
+  @AfterClass(alwaysRun = true)
+  public void tearDownAfterClass()
+  {
+    /*
+     * remove any sequence mappings created so they don't pollute other tests
+     */
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+    ssm.resetAll();
+  }
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -54,8 +73,6 @@ public class FeaturesFileTest
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  private static String simpleGffFile = "examples/testdata/simpleGff3.gff";
-
   @Test(groups = { "Functional" })
   public void testParse() throws Exception
   {
@@ -86,10 +103,15 @@ public class FeaturesFileTest
     /*
      * verify (some) features on sequences
      */
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures(); // FER_CAPAA
-    assertEquals(8, sfs.length);
-    SequenceFeature sf = sfs[0];
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertEquals(8, sfs.size());
+
+    /*
+     * verify (in ascending start position order)
+     */
+    SequenceFeature sf = sfs.get(0);
     assertEquals("Pfam family%LINK%", sf.description);
     assertEquals(0, sf.begin);
     assertEquals(0, sf.end);
@@ -99,46 +121,52 @@ public class FeaturesFileTest
     assertEquals("Pfam family|http://pfam.xfam.org/family/PF00111",
             sf.links.get(0));
 
-    sf = sfs[1];
+    sf = sfs.get(1);
+    assertEquals("Ferredoxin_fold Status: True Positive ", sf.description);
+    assertEquals(3, sf.begin);
+    assertEquals(93, sf.end);
+    assertEquals("uniprot", sf.featureGroup);
+    assertEquals("Cath", sf.type);
+
+    sf = sfs.get(2);
+    assertEquals("Fer2 Status: True Positive Pfam 8_8%LINK%",
+            sf.description);
+    assertEquals("Pfam 8_8|http://pfam.xfam.org/family/PF00111",
+            sf.links.get(0));
+    assertEquals(8, sf.begin);
+    assertEquals(83, sf.end);
+    assertEquals("uniprot", sf.featureGroup);
+    assertEquals("Pfam", sf.type);
+
+    sf = sfs.get(3);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(39, sf.begin);
     assertEquals(39, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[2];
+
+    sf = sfs.get(4);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(44, sf.begin);
     assertEquals(44, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[3];
+
+    sf = sfs.get(5);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(47, sf.begin);
     assertEquals(47, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[4];
+
+    sf = sfs.get(6);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(77, sf.begin);
     assertEquals(77, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[5];
-    assertEquals("Fer2 Status: True Positive Pfam 8_8%LINK%",
-            sf.description);
-    assertEquals("Pfam 8_8|http://pfam.xfam.org/family/PF00111",
-            sf.links.get(0));
-    assertEquals(8, sf.begin);
-    assertEquals(83, sf.end);
-    assertEquals("uniprot", sf.featureGroup);
-    assertEquals("Pfam", sf.type);
-    sf = sfs[6];
-    assertEquals("Ferredoxin_fold Status: True Positive ", sf.description);
-    assertEquals(3, sf.begin);
-    assertEquals(93, sf.end);
-    assertEquals("uniprot", sf.featureGroup);
-    assertEquals("Cath", sf.type);
-    sf = sfs[7];
+
+    sf = sfs.get(7);
     assertEquals(
             "High confidence server. Only hits with scores over 0.8 are reported. PHOSPHORYLATION (T) 89_8%LINK%",
             sf.description);
@@ -181,10 +209,10 @@ public class FeaturesFileTest
     assertEquals(colours.get("METAL").getColour(), new Color(0xcc9900));
 
     // verify feature on FER_CAPAA
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    SequenceFeature sf = sfs[0];
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
     assertEquals("Iron-sulfur,2Fe-2S", sf.description);
     assertEquals(44, sf.begin);
     assertEquals(45, sf.end);
@@ -194,8 +222,8 @@ public class FeaturesFileTest
 
     // verify feature on FER1_SOLLC
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    sf = sfs[0];
+    assertEquals(1, sfs.size());
+    sf = sfs.get(0);
     assertEquals("uniprot", sf.description);
     assertEquals(55, sf.begin);
     assertEquals(130, sf.end);
@@ -242,10 +270,10 @@ public class FeaturesFileTest
             featuresFile.parse(al.getDataset(), colours, true));
 
     // verify feature on FER_CAPAA
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    SequenceFeature sf = sfs[0];
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
     // description parsed from Note attribute
     assertEquals("Iron-sulfur (2Fe-2S),another note", sf.description);
     assertEquals(39, sf.begin);
@@ -258,8 +286,8 @@ public class FeaturesFileTest
 
     // verify feature on FER1_SOLLC1
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    sf = sfs[0];
+    assertEquals(1, sfs.size());
+    sf = sfs.get(0);
     // ID used for description if available
     assertEquals("$23", sf.description);
     assertEquals(55, sf.begin);
@@ -295,10 +323,10 @@ public class FeaturesFileTest
             featuresFile.parse(al.getDataset(), colours, true));
 
     // verify FER_CAPAA feature
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    SequenceFeature sf = sfs[0];
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(39, sf.begin);
     assertEquals(39, sf.end);
@@ -306,8 +334,8 @@ public class FeaturesFileTest
 
     // verify FER1_SOLLC feature
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    sf = sfs[0];
+    assertEquals(1, sfs.size());
+    sf = sfs.get(0);
     assertEquals("Iron-phosphorus (2Fe-P)", sf.description);
     assertEquals(86, sf.begin);
     assertEquals(87, sf.end);
@@ -337,14 +365,14 @@ public class FeaturesFileTest
     assertFalse("dummy replacement buggy for seq2",
             placeholderseq.equals(seq2.getSequenceAsString()));
     assertNotNull("No features added to seq1", seq1.getSequenceFeatures());
-    assertEquals("Wrong number of features", 3,
-            seq1.getSequenceFeatures().length);
-    assertNull(seq2.getSequenceFeatures());
+    assertEquals("Wrong number of features", 3, seq1.getSequenceFeatures()
+            .size());
+    assertTrue(seq2.getSequenceFeatures().isEmpty());
     assertEquals(
             "Wrong number of features",
             0,
             seq2.getSequenceFeatures() == null ? 0 : seq2
-                    .getSequenceFeatures().length);
+                    .getSequenceFeatures().size());
     assertTrue(
             "Expected at least one CDNA/Protein mapping for seq1",
             dataset.getCodonFrame(seq1) != null
@@ -410,6 +438,7 @@ public class FeaturesFileTest
             + "GAMMA-TURN\tred|0,255,255|20.0|95.0|below|66.0\n"
             + "Pfam\tred\n"
             + "STARTGROUP\tuniprot\n"
+            + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\n" // non-positional feature
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\n"
             + "<html>Pfam domain<a href=\"http://pfam.xfam.org/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\n"
@@ -419,28 +448,57 @@ public class FeaturesFileTest
     featuresFile.parse(al.getDataset(), colours, false);
 
     /*
-     * first with no features displayed
+     * add positional and non-positional features with null and
+     * empty feature group to check handled correctly
+     */
+    SequenceI seq = al.getSequenceAt(1); // FER_CAPAN
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc1", 0, 0, 1.3f,
+            null));
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc2", 4, 9,
+            Float.NaN, null));
+    seq = al.getSequenceAt(2); // FER1_SOLLC
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc3", 0, 0,
+            Float.NaN, ""));
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc4", 5, 8,
+            -2.6f, ""));
+
+    /*
+     * first with no features displayed, exclude non-positional features
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     Map<String, FeatureColourI> visible = fr.getDisplayedFeatureCols();
+    List<String> visibleGroups = new ArrayList<String>(
+            Arrays.asList(new String[] {}));
     String exported = featuresFile.printJalviewFormat(
-            al.getSequencesArray(), visible);
+            al.getSequencesArray(), visible, visibleGroups, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
     /*
+     * include non-positional features
+     */
+    visibleGroups.add("uniprot");
+    exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
+            visible, visibleGroups, true);
+    expected = "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n"
+            + "desc1\tFER_CAPAN\t-1\t0\t0\tPfam\t1.3\n"
+            + "desc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n" // NaN is not output
+            + "\nSTARTGROUP\tuniprot\nENDGROUP\tuniprot\n";
+    assertEquals(expected, exported);
+
+    /*
      * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
      */
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible);
+            visible, visibleGroups, false);
     expected = "METAL\tcc9900\n"
             + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
-            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
+            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "ENDGROUP\tuniprot\n";
     assertEquals(expected, exported);
 
@@ -450,19 +508,119 @@ public class FeaturesFileTest
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible);
+            visible, visibleGroups, false);
     /*
-     * note the order of feature types is uncontrolled - derives from
-     * FeaturesDisplayed.featuresDisplayed which is a HashSet
+     * features are output within group, ordered by sequence and by type
      */
     expected = "METAL\tcc9900\n"
             + "Pfam\tff0000\n"
             + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
-            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
+            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "<html>Pfam domain<a href=\"http://pfam.xfam.org/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n"
-            + "ENDGROUP\tuniprot\n";
+            + "ENDGROUP\tuniprot\n"
+            // null / empty group features output after features in named
+            // groups:
+            + "desc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
+            + "desc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n";
+    assertEquals(expected, exported);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testPrintGffFormat() throws Exception
+  {
+    File f = new File("examples/uniref50.fa");
+    AlignmentI al = readAlignmentFile(f);
+    AlignFrame af = new AlignFrame(al, 500, 500);
+
+    /*
+     * no features
+     */
+    FeaturesFile featuresFile = new FeaturesFile();
+    FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
+    Map<String, FeatureColourI> visible = new HashMap<String, FeatureColourI>();
+    List<String> visibleGroups = new ArrayList<String>(
+            Arrays.asList(new String[] {}));
+    String exported = featuresFile.printGffFormat(al.getSequencesArray(),
+            visible, visibleGroups, false);
+    String gffHeader = "##gff-version 2\n";
+    assertEquals(gffHeader, exported);
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, true);
+    assertEquals(gffHeader, exported);
+
+    /*
+     * add some features
+     */
+    al.getSequenceAt(0).addSequenceFeature(
+            new SequenceFeature("Domain", "Cath", 0, 0, 0f, "Uniprot"));
+    al.getSequenceAt(0).addSequenceFeature(
+            new SequenceFeature("METAL", "Cath", 39, 39, 1.2f, null));
+    al.getSequenceAt(1)
+            .addSequenceFeature(
+                    new SequenceFeature("GAMMA-TURN", "Turn", 36, 38, 2.1f,
+                            "s3dm"));
+    SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f,
+            "Uniprot");
+    sf.setAttributes("x=y;black=white");
+    sf.setStrand("+");
+    sf.setPhase("2");
+    al.getSequenceAt(1).addSequenceFeature(sf);
+
+    /*
+     * with no features displayed, exclude non-positional features
+     */
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    assertEquals(gffHeader, exported);
+
+    /*
+     * include non-positional features
+     */
+    visibleGroups.add("Uniprot");
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, true);
+    String expected = gffHeader
+            + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n";
+    assertEquals(expected, exported);
+
+    /*
+     * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
+     * only Uniprot group visible here...
+     */
+    fr.setVisible("METAL");
+    fr.setVisible("GAMMA-TURN");
+    visible = fr.getDisplayedFeatureCols();
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    // METAL feature has null group: description used for column 2
+    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n";
+    assertEquals(expected, exported);
+
+    /*
+     * set s3dm group visible
+     */
+    visibleGroups.add("s3dm");
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    // METAL feature has null group: description used for column 2
+    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
+            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n";
+    assertEquals(expected, exported);
+
+    /*
+     * now set Pfam visible
+     */
+    fr.setVisible("Pfam");
+    visible = fr.getDisplayedFeatureCols();
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    // Pfam feature columns include strand(+), phase(2), attributes
+    expected = gffHeader
+            + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
+            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"
+            + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n";
     assertEquals(expected, exported);
   }
 }
index e046d94..158c901 100644 (file)
@@ -32,6 +32,7 @@ import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.json.binding.biojson.v1.ColourSchemeMapper;
@@ -42,6 +43,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.testng.Assert;
 import org.testng.AssertJUnit;
@@ -96,6 +98,10 @@ public class JSONFileTest
   @BeforeTest(alwaysRun = true)
   public void setup() throws Exception
   {
+    /*
+     * construct expected values
+     * nb this have to match the data in examples/example.json
+     */
     // create and add sequences
     Sequence[] seqs = new Sequence[5];
     seqs[0] = new Sequence("FER_CAPAN",
@@ -115,14 +121,18 @@ public class JSONFileTest
 
     // create and add sequence features
     SequenceFeature seqFeature2 = new SequenceFeature("feature_x",
-            "desciption", "status", 6, 15, "Jalview");
+            "theDesc", 6, 15, "Jalview");
     SequenceFeature seqFeature3 = new SequenceFeature("feature_x",
-            "desciption", "status", 9, 18, "Jalview");
+            "theDesc", 9, 18, "Jalview");
     SequenceFeature seqFeature4 = new SequenceFeature("feature_x",
-            "desciption", "status", 9, 18, "Jalview");
+            "theDesc", 9, 18, "Jalview");
+    // non-positional feature:
+    SequenceFeature seqFeature5 = new SequenceFeature("Domain",
+            "My description", 0, 0, "Pfam");
     seqs[2].addSequenceFeature(seqFeature2);
     seqs[3].addSequenceFeature(seqFeature3);
     seqs[4].addSequenceFeature(seqFeature4);
+    seqs[2].addSequenceFeature(seqFeature5);
 
     for (Sequence seq : seqs)
     {
@@ -456,7 +466,7 @@ public class JSONFileTest
     return true;
   }
 
-  public boolean isSeqMatched(SequenceI expectedSeq, SequenceI actualSeq)
+  boolean isSeqMatched(SequenceI expectedSeq, SequenceI actualSeq)
   {
     System.out.println("Testing >>> " + actualSeq.getName());
 
@@ -490,14 +500,19 @@ public class JSONFileTest
             + actualGrp.getStartRes());
     System.out.println(expectedGrp.getEndRes() + " | "
             + actualGrp.getEndRes());
-    System.out.println(expectedGrp.cs + " | " + actualGrp.cs);
+    System.out.println(expectedGrp.cs.getColourScheme() + " | "
+            + actualGrp.cs.getColourScheme());
 
+    boolean colourSchemeMatches = (expectedGrp.cs.getColourScheme() == null && actualGrp.cs
+            .getColourScheme() == null)
+            || expectedGrp.cs.getColourScheme().getClass()
+                    .equals(actualGrp.cs.getColourScheme().getClass());
     if (expectedGrp.getName().equals(actualGrp.getName())
             && expectedGrp.getColourText() == actualGrp.getColourText()
             && expectedGrp.getDisplayBoxes() == actualGrp.getDisplayBoxes()
             && expectedGrp.getIgnoreGapsConsensus() == actualGrp
                     .getIgnoreGapsConsensus()
-            && (expectedGrp.cs.getClass().equals(actualGrp.cs.getClass()))
+            && colourSchemeMatches
             && expectedGrp.getSequences().size() == actualGrp
                     .getSequences().size()
             && expectedGrp.getStartRes() == actualGrp.getStartRes()
@@ -510,7 +525,6 @@ public class JSONFileTest
 
   private boolean featuresMatched(SequenceI seq1, SequenceI seq2)
   {
-    boolean matched = false;
     try
     {
       if (seq1 == null && seq2 == null)
@@ -518,52 +532,95 @@ public class JSONFileTest
         return true;
       }
 
-      SequenceFeature[] inFeature = seq1.getSequenceFeatures();
-      SequenceFeature[] outFeature = seq2.getSequenceFeatures();
+      List<SequenceFeature> inFeature = seq1.getFeatures().getAllFeatures();
+      List<SequenceFeature> outFeature = seq2.getFeatures()
+              .getAllFeatures();
 
-      if (inFeature == null && outFeature == null)
-      {
-        return true;
-      }
-      else if ((inFeature == null && outFeature != null)
-              || (inFeature != null && outFeature == null))
+      if (inFeature.size() != outFeature.size())
       {
+        System.err.println("Feature count in: " + inFeature.size()
+                + ", out: " + outFeature.size());
         return false;
       }
 
-      int testSize = inFeature.length;
-      int matchedCount = 0;
+      SequenceFeatures.sortFeatures(inFeature, true);
+      SequenceFeatures.sortFeatures(outFeature, true);
+      int i = 0;
       for (SequenceFeature in : inFeature)
       {
-        for (SequenceFeature out : outFeature)
+        SequenceFeature out = outFeature.get(i);
+        /*
+        System.out.println(out.getType() + " | " + in.getType());
+        System.out.println(out.getBegin() + " | " + in.getBegin());
+        System.out.println(out.getEnd() + " | " + in.getEnd());
+        */
+        if (!in.equals(out))
         {
-          System.out.println(out.getType() + " | " + in.getType());
-          System.out.println(out.getBegin() + " | " + in.getBegin());
-          System.out.println(out.getEnd() + " | " + in.getEnd());
-
-          if (inFeature.length == outFeature.length
-                  && in.getBegin() == out.getBegin()
-                  && in.getEnd() == out.getEnd()
-                  && in.getScore() == out.getScore()
-                  && in.getFeatureGroup().equals(out.getFeatureGroup())
-                  && in.getType().equals(out.getType()))
-          {
-
-            ++matchedCount;
-          }
+          System.err.println("Mismatch of " + in.toString() + " "
+                  + out.toString());
+          return false;
         }
-      }
-      System.out.println("matched count >>>>>> " + matchedCount);
-      if (testSize == matchedCount)
-      {
-        matched = true;
+        /*
+                if (in.getBegin() == out.getBegin() && in.getEnd() == out.getEnd()
+                        && in.getScore() == out.getScore()
+                        && in.getFeatureGroup().equals(out.getFeatureGroup())
+                        && in.getType().equals(out.getType())
+                        && mapsMatch(in.otherDetails, out.otherDetails))
+                {
+                }
+                else
+                {
+                  System.err.println("Feature[" + i + "] mismatch, in: "
+                          + in.toString() + ", out: "
+                          + outFeature.get(i).toString());
+                  return false;
+                }
+                */
+        i++;
       }
     } catch (Exception e)
     {
       e.printStackTrace();
     }
     // System.out.println(">>>>>>>>>>>>>> features matched : " + matched);
-    return matched;
+    return true;
+  }
+
+  boolean mapsMatch(Map<String, Object> m1, Map<String, Object> m2)
+  {
+    if (m1 == null || m2 == null)
+    {
+      if (m1 != null || m2 != null)
+      {
+        System.err
+                .println("only one SequenceFeature.otherDetails is not null");
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    if (m1.size() != m2.size())
+    {
+      System.err.println("otherDetails map different sizes");
+      return false;
+    }
+    for (String key : m1.keySet())
+    {
+      if (!m2.containsKey(key))
+      {
+        System.err.println(key + " in only one otherDetails");
+        return false;
+      }
+      if (m1.get(key) == null && m2.get(key) != null || m1.get(key) != null
+              && m2.get(key) == null || !m1.get(key).equals(m2.get(key)))
+      {
+        System.err.println(key + " values in otherDetails don't match");
+        return false;
+      }
+    }
+    return true;
   }
 
   /**
@@ -599,7 +656,7 @@ public class JSONFileTest
     Assert.assertNotNull(newAlignment.getGroups());
     for (SequenceGroup seqGrp : newAlignment.getGroups())
     {
-      SequenceGroup expectedGrp = expectedGrps.get(seqGrp.getName());
+      SequenceGroup expectedGrp = copySg;
       AssertJUnit.assertTrue(
               "Failed SequenceGroup Test for >>> " + seqGrp.getName(),
               isGroupMatched(expectedGrp, seqGrp));
index 2895874..9e61bec 100644 (file)
 package jalview.io;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
+import jalview.io.gff.GffConstants;
 
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.Map;
 
+import junit.extensions.PA;
+
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -192,4 +200,134 @@ public class SequenceAnnotationReportTest
     // if no <html> tag, html-encodes > and < (only):
     assertEquals("METAL 1 3; &lt;br&gt;&kHD&gt;6", sb.toString());
   }
+
+  @Test(groups = "Functional")
+  public void testCreateSequenceAnnotationReport()
+  {
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    StringBuilder sb = new StringBuilder();
+
+    SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL");
+    seq.setDescription("SeqDesc");
+
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null);
+
+    /*
+     * positional features are ignored
+     */
+    seq.addSequenceFeature(new SequenceFeature("Domain", "Ferredoxin", 5,
+            10, 1f, null));
+    assertEquals("<i><br>SeqDesc</i>", sb.toString());
+
+    /*
+     * non-positional feature
+     */
+    seq.addSequenceFeature(new SequenceFeature("Type1", "Nonpos", 0, 0, 1f,
+            null));
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null);
+    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos</i>";
+    assertEquals(expected, sb.toString());
+
+    /*
+     * non-positional features not wanted
+     */
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, null);
+    assertEquals("<i><br>SeqDesc</i>", sb.toString());
+
+    /*
+     * add non-pos feature with score inside min-max range for feature type
+     * minmax holds { [positionalMin, positionalMax], [nonPosMin, nonPosMax] }
+     * score is only appended for positional features so ignored here!
+     * minMax are not recorded for non-positional features
+     */
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f,
+            null));
+    Map<String, float[][]> minmax = new HashMap<String, float[][]>();
+    minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
+    assertEquals(expected, sb.toString());
+    
+    /*
+     * 'linkonly' features are ignored; this is obsolete, as linkonly
+     * is only set by DasSequenceFetcher, and DAS is history
+     */
+    SequenceFeature sf = new SequenceFeature("Metal", "Desc", 0, 0, 5f,
+            null);
+    sf.setValue("linkonly", Boolean.TRUE);
+    seq.addSequenceFeature(sf);
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    assertEquals(expected, sb.toString()); // unchanged!
+
+    /*
+     * 'clinical_significance' currently being specially included
+     */
+    SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0,
+            5f, null);
+    sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign");
+    seq.addSequenceFeature(sf2);
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    assertEquals(expected, sb.toString());
+
+    /*
+     * add dbrefs
+     */
+    seq.addDBRef(new DBRefEntry("PDB", "0", "3iu1"));
+    seq.addDBRef(new DBRefEntry("Uniprot", "1", "P30419"));
+    // with showDbRefs = false
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, false, true, minmax);
+    assertEquals(expected, sb.toString()); // unchanged
+    // with showDbRefs = true
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    assertEquals(expected, sb.toString());
+    // with showNonPositionalFeatures = false
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
+    assertEquals(expected, sb.toString());
+
+    // see other tests for treatment of status and html
+  }
+
+  /**
+   * Test that exercises an abbreviated sequence details report, with ellipsis
+   * where there are more than 40 different sources, or more than 4 dbrefs for a
+   * single source
+   */
+  @Test(groups = "Functional")
+  public void testCreateSequenceAnnotationReport_withEllipsis()
+  {
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    StringBuilder sb = new StringBuilder();
+  
+    SequenceI seq = new Sequence("s1", "ABC");
+
+    int maxSources = (int) PA.getValue(sar, "MAX_SOURCES");
+    for (int i = 0; i <= maxSources; i++)
+    {
+      seq.addDBRef(new DBRefEntry("PDB" + i, "0", "3iu1"));
+    }
+    
+    int maxRefs = (int) PA.getValue(sar, "MAX_REFS_PER_SOURCE");
+    for (int i = 0; i <= maxRefs; i++)
+    {
+      seq.addDBRef(new DBRefEntry("Uniprot", "0", "P3041" + i));
+    }
+  
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null, true);
+    String report = sb.toString();
+    assertTrue(report
+            .startsWith("<i><br>UNIPROT P30410, P30411, P30412, P30413,...<br>PDB0 3iu1"));
+    assertTrue(report
+            .endsWith("<br>PDB7 3iu1<br>PDB8,...<br>(Output Sequence Details to list all database references)</i>"));
+  }
 }
index 228c935..4273e6c 100644 (file)
@@ -287,7 +287,8 @@ public class StockholmFileTest
     seq_original = al.getSequencesArray();
     SequenceI[] seq_new = new SequenceI[al_input.getSequencesArray().length];
     seq_new = al_input.getSequencesArray();
-    SequenceFeature[] sequenceFeatures_original, sequenceFeatures_new;
+    List<SequenceFeature> sequenceFeatures_original;
+    List<SequenceFeature> sequenceFeatures_new;
     AlignmentAnnotation annot_original, annot_new;
     //
     for (int i = 0; i < al.getSequencesArray().length; i++)
@@ -323,23 +324,20 @@ public class StockholmFileTest
                   && seq_new[in].getSequenceFeatures() != null)
           {
             System.out.println("There are feature!!!");
-            sequenceFeatures_original = new SequenceFeature[seq_original[i]
-                    .getSequenceFeatures().length];
             sequenceFeatures_original = seq_original[i]
                     .getSequenceFeatures();
-            sequenceFeatures_new = new SequenceFeature[seq_new[in]
-                    .getSequenceFeatures().length];
             sequenceFeatures_new = seq_new[in].getSequenceFeatures();
 
-            assertEquals("different number of features",
-                    seq_original[i].getSequenceFeatures().length,
-                    seq_new[in].getSequenceFeatures().length);
+            assertEquals("different number of features", seq_original[i]
+                    .getSequenceFeatures().size(), seq_new[in]
+                    .getSequenceFeatures().size());
 
-            for (int feat = 0; feat < seq_original[i].getSequenceFeatures().length; feat++)
+            for (int feat = 0; feat < seq_original[i].getSequenceFeatures()
+                    .size(); feat++)
             {
               assertEquals("Different features",
-                      sequenceFeatures_original[feat],
-                      sequenceFeatures_new[feat]);
+                      sequenceFeatures_original.get(feat),
+                      sequenceFeatures_new.get(feat));
             }
           }
           // compare alignment annotation
index bf038ac..cd5a0d8 100644 (file)
@@ -37,7 +37,9 @@ import jalview.gui.JvOptionPane;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -231,4 +233,48 @@ public class Gff3HelperTest
             .getToRanges().get(0));
   }
 
+  @Test(groups = "Functional")
+  public void testGetDescription()
+  {
+    Gff3Helper testee = new Gff3Helper();
+    SequenceFeature sf = new SequenceFeature("type", "desc", 10, 20, 3f,
+            "group");
+    Map<String, List<String>> attributes = new HashMap<String, List<String>>();
+    assertNull(testee.getDescription(sf, attributes));
+
+    // ID if any is a fall-back for description
+    sf.setValue("ID", "Patrick");
+    assertEquals("Patrick", testee.getDescription(sf, attributes));
+
+    // Target is set by Exonerate
+    sf.setValue("Target", "Destination Moon");
+    assertEquals("Destination", testee.getDescription(sf, attributes));
+
+    // Ensembl variant feature - extract "alleles" value
+    // may be sequence_variant or a sub-type in the sequence ontology
+    sf = new SequenceFeature("feature_variant", "desc", 10, 20, 3f, "group");
+    List<String> atts = new ArrayList<String>();
+    atts.add("A");
+    atts.add("C");
+    atts.add("T");
+    attributes.put("alleles", atts);
+    assertEquals("A,C,T", testee.getDescription(sf, attributes));
+
+    // Ensembl transcript or exon feature - extract Name
+    List<String> atts2 = new ArrayList<String>();
+    atts2.add("ENSE00001871077");
+    attributes.put("Name", atts2);
+    sf = new SequenceFeature("transcript", "desc", 10, 20, 3f, "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+    // transcript sub-type in SO
+    sf = new SequenceFeature("mRNA", "desc", 10, 20, 3f, "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+    // special usage of feature by Ensembl
+    sf = new SequenceFeature("NMD_transcript_variant", "desc", 10, 20, 3f,
+            "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+    // exon feature
+    sf = new SequenceFeature("exon", "desc", 10, 20, 3f, "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+  }
 }
index bcccf35..dde83a3 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.io.gff;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
@@ -30,6 +31,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
@@ -76,6 +78,16 @@ public class InterProScanHelperTest
     assertEquals(1, newseqs.size());
     assertTrue(newseqs.get(0) instanceof SequenceDummy);
     assertEquals("match$17_5_30", newseqs.get(0).getName());
+
+    assertNotNull(newseqs.get(0).getSequenceFeatures());
+    assertEquals(1, newseqs.get(0).getSequenceFeatures().size());
+    SequenceFeature sf = newseqs.get(0).getSequenceFeatures().get(0);
+    assertEquals(1, sf.getBegin());
+    assertEquals(26, sf.getEnd());
+    assertEquals("Pfam", sf.getType());
+    assertEquals("4Fe-4S dicluster domain", sf.getDescription());
+    assertEquals("InterProScan", sf.getFeatureGroup());
+
     assertEquals(1, align.getCodonFrames().size());
     AlignedCodonFrame mapping = align.getCodonFrames().iterator().next();
 
index 2016087..1687516 100644 (file)
@@ -30,7 +30,6 @@ import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignViewport;
 import jalview.gui.JvOptionPane;
-import jalview.gui.Preferences;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.UserColourScheme;
 import jalview.schemes.ZappoColourScheme;
@@ -244,7 +243,9 @@ public class OverviewResColourFinderTest
 
     // gaps gap colour
     c = rcf.getBoxColour(shader, seq, 3);
-    assertEquals(Preferences.OVERVIEW_DEFAULT_GAP, c);
+    assertEquals(
+            jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_GAP,
+            c);
 
     // legacy colouring set explicitly via constructor
     rcf = new OverviewResColourFinder(true, Color.blue, Color.red);
@@ -268,7 +269,9 @@ public class OverviewResColourFinderTest
 
     // gaps gap colour
     c = rcf.getBoxColour(shader, seq, 3);
-    assertEquals(Preferences.OVERVIEW_DEFAULT_GAP, c);
+    assertEquals(
+            jalview.renderer.OverviewResColourFinder.OVERVIEW_DEFAULT_GAP,
+            c);
 
     // non legacy colouring with colour scheme
     rcf = new OverviewResColourFinder(false, Color.blue, Color.red);
index 76fd9b4..eba5f59 100644 (file)
@@ -8,9 +8,15 @@ import jalview.analysis.Conservation;
 import jalview.datamodel.Profile;
 import jalview.datamodel.ProfileI;
 import jalview.datamodel.Profiles;
+import jalview.datamodel.ProfilesI;
+import jalview.datamodel.ResidueCount;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
+import jalview.schemes.ColourSchemeI;
 import jalview.schemes.PIDColourScheme;
+import jalview.schemes.ResidueProperties;
+import jalview.schemes.UserColourScheme;
+import jalview.schemes.ZappoColourScheme;
 
 import java.awt.Color;
 import java.util.Collections;
@@ -163,4 +169,165 @@ public class ResidueShaderTest
     assertEquals(Color.WHITE, ccs.applyConservation(colour, 12));
   }
 
+  @Test(groups = "Functional")
+  public void testFindColour_gapColour()
+  {
+    /*
+     * normally, a gap is coloured white
+     */
+    ResidueShader rs = new ResidueShader(new ZappoColourScheme());
+    assertEquals(Color.white, rs.findColour(' ', 7, null));
+
+    /*
+     * a User Colour Scheme may specify a bespoke gap colour
+     */
+    Color[] colours = new Color[ResidueProperties.maxProteinIndex + 1];
+    colours[5] = Color.blue; // Q colour
+    colours[23] = Color.red; // gap colour
+    ColourSchemeI cs = new UserColourScheme(colours);
+    rs = new ResidueShader(cs);
+  
+    assertEquals(Color.red, rs.findColour(' ', 7, null));
+    assertEquals(Color.blue, rs.findColour('Q', 7, null));
+  
+    /*
+     * stub Conservation to return a given consensus string
+     */
+    final String consSequence = "0123456789+*-";
+    Conservation cons = new Conservation(null,
+            Collections.<SequenceI> emptyList(), 0, 0)
+    {
+      @Override
+      public SequenceI getConsSequence()
+      {
+        return new Sequence("seq", consSequence);
+      }
+    };
+    rs.setConservation(cons);
+  
+    /*
+     * with 0% threshold, there should be no fading
+     */
+    rs.setConservationInc(0);
+    assertEquals(Color.red, rs.findColour(' ', 7, null));
+    assertEquals(Color.blue, rs.findColour('Q', 7, null));
+  
+    /*
+     * with 40% threshold, 'fade factor' is 
+     * (11-score)/10 * 40/20 = (11-score)/5
+     * so position 7, score 7 fades 80% of the way to white (255, 255, 255)
+     */
+    rs.setConservationInc(40);
+
+    /*
+     * gap colour is unchanged for Conservation
+     */
+    assertEquals(Color.red, rs.findColour(' ', 7, null));
+    assertEquals(Color.red, rs.findColour('-', 7, null));
+    assertEquals(Color.red, rs.findColour('.', 7, null));
+
+    /*
+     * residue colour is faded 80% of the way from
+     * blue(0, 0, 255) to white(255, 255, 255)
+     * making (204, 204, 255)
+     */
+    assertEquals(new Color(204, 204, 255), rs.findColour('Q', 7, null));
+
+    /*
+     * turn off By Conservation, apply Above Identity Threshold
+     * providing a stub Consensus that has modal residue "Q" with pid 60%
+     */
+    rs.setConservationApplied(false);
+    ProfilesI consensus = getStubConsensus("Q", 60f);
+    rs.setConsensus(consensus);
+
+    // with consensus pid (60) above threshold (50), colours are unchanged
+    rs.setThreshold(50, false);
+    assertEquals(Color.blue, rs.findColour('Q', 7, null));
+    assertEquals(Color.red, rs.findColour('-', 7, null));
+
+    // with consensus pid (60) below threshold (70),
+    // residue colour becomes white, gap colour is unchanged
+    rs.setThreshold(70, false);
+    assertEquals(Color.white, rs.findColour('Q', 7, null));
+    assertEquals(Color.red, rs.findColour('-', 7, null));
+  }
+
+  /**
+   * @param modalResidue
+   * @param pid
+   * @return
+   */
+  protected ProfilesI getStubConsensus(final String modalResidue,
+          final float pid)
+  {
+    ProfilesI consensus = new ProfilesI() {
+
+      @Override
+      public ProfileI get(int i)
+      {
+        return new ProfileI() {
+          @Override
+          public void setCounts(ResidueCount residueCounts)
+          {
+          }
+
+          @Override
+          public float getPercentageIdentity(boolean ignoreGaps)
+          {
+            return pid;
+          }
+
+          @Override
+          public ResidueCount getCounts()
+          {
+            return null;
+          }
+
+          @Override
+          public int getHeight()
+          {
+            return 0;
+          }
+
+          @Override
+          public int getGapped()
+          {
+            return 0;
+          }
+
+          @Override
+          public int getMaxCount()
+          {
+            return 0;
+          }
+
+          @Override
+          public String getModalResidue()
+          {
+            return modalResidue;
+          }
+
+          @Override
+          public int getNonGapped()
+          {
+            return 0;
+         }};
+      }
+
+      @Override
+      public int getStartColumn()
+      {
+        return 0;
+      }
+
+      @Override
+      public int getEndColumn()
+      {
+        return 0;
+      }
+      
+    };
+    return consensus;
+  }
 }
index 4fc079e..7fd7abc 100644 (file)
@@ -16,6 +16,7 @@ import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
 
 import java.awt.Color;
+import java.util.List;
 
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.BeforeTest;
@@ -70,13 +71,10 @@ public class FeatureColourFinderTest
   @BeforeMethod(alwaysRun = true)
   public void setUpBeforeTest()
   {
-    SequenceFeature[] sfs = seq.getSequenceFeatures();
-    if (sfs != null)
+    List<SequenceFeature> sfs = seq.getSequenceFeatures();
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
-      {
-        seq.deleteFeature(sf);
-      }
+      seq.deleteFeature(sf);
     }
     fr.findAllFeatures(true);
 
@@ -454,15 +452,19 @@ public class FeatureColourFinderTest
   @Test(groups = "Functional")
   public void testFindFeatureColour_graduatedWithThreshold()
   {
-    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 2,
+    String kdFeature = "kd";
+    String metalFeature = "Metal";
+    seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 2,
             2, 0f, "KdGroup"));
-    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 4,
+    seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 4,
             4, 5f, "KdGroup"));
-    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 7,
+    seq.addSequenceFeature(new SequenceFeature(metalFeature, "Fe", 4, 4,
+            5f, "MetalGroup"));
+    seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 7,
             7, 10f, "KdGroup"));
   
     /*
-     * graduated colour from 0 to 10
+     * kd feature has graduated colour from 0 to 10
      * above threshold value of 5
      */
     Color min = new Color(100, 50, 150);
@@ -470,8 +472,19 @@ public class FeatureColourFinderTest
     FeatureColourI fc = new FeatureColour(min, max, 0, 10);
     fc.setAboveThreshold(true);
     fc.setThreshold(5f);
-    fr.setColour("kd", fc);
+    fr.setColour(kdFeature, fc);
+    FeatureColour green = new FeatureColour(Color.green);
+    fr.setColour(metalFeature, green);
     fr.featuresAdded();
+
+    /*
+     * render order is kd above Metal
+     */
+    Object[][] data = new Object[2][];
+    data[0] = new Object[] { kdFeature, fc, true };
+    data[1] = new Object[] { metalFeature, green, true };
+    fr.setFeaturePriority(data);
+
     av.setShowSequenceFeatures(true);
   
     /*
@@ -481,10 +494,11 @@ public class FeatureColourFinderTest
     assertEquals(c, Color.blue);
 
     /*
-     * position 4, column 3, score 5 - at threshold - default colour
+     * position 4, column 3, score 5 - at threshold
+     * should return Green (colour of Metal feature)
      */
     c = finder.findFeatureColour(Color.blue, seq, 3);
-    assertEquals(c, Color.blue);
+    assertEquals(c, Color.green);
   
     /*
      * position 7, column 9, score 10 - maximum colour in range
@@ -504,10 +518,11 @@ public class FeatureColourFinderTest
     assertEquals(c, min);
 
     /*
-     * position 4, column 3, score 5 - at threshold - default colour
+     * position 4, column 3, score 5 - at threshold
+     * should return Green (colour of Metal feature)
      */
     c = finder.findFeatureColour(Color.blue, seq, 3);
-    assertEquals(c, Color.blue);
+    assertEquals(c, Color.green);
 
     /*
      * position 7, column 9, score 10 - above threshold - default colour
diff --git a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java
new file mode 100644 (file)
index 0000000..d3cddf9
--- /dev/null
@@ -0,0 +1,363 @@
+package jalview.renderer.seqfeatures;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class FeatureRendererTest
+{
+
+  @Test(groups = "Functional")
+  public void testFindAllFeatures()
+  {
+    String seqData = ">s1\nabcdef\n>s2\nabcdef\n>s3\nabcdef\n>s4\nabcdef\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
+
+    /*
+     * with no features
+     */
+    fr.findAllFeatures(true);
+    assertTrue(fr.getRenderOrder().isEmpty());
+    assertTrue(fr.getFeatureGroups().isEmpty());
+
+    List<SequenceI> seqs = av.getAlignment().getSequences();
+
+    // add a non-positional feature - should be ignored by FeatureRenderer
+    SequenceFeature sf1 = new SequenceFeature("Type", "Desc", 0, 0, 1f,
+            "Group");
+    seqs.get(0).addSequenceFeature(sf1);
+    fr.findAllFeatures(true);
+    // ? bug - types and groups added for non-positional features
+    List<String> types = fr.getRenderOrder();
+    List<String> groups = fr.getFeatureGroups();
+    assertEquals(types.size(), 0);
+    assertFalse(types.contains("Type"));
+    assertEquals(groups.size(), 0);
+    assertFalse(groups.contains("Group"));
+
+    // add some positional features
+    seqs.get(1).addSequenceFeature(
+            new SequenceFeature("Pfam", "Desc", 5, 9, 1f, "PfamGroup"));
+    seqs.get(2).addSequenceFeature(
+            new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup"));
+    // bug in findAllFeatures - group not checked for a known feature type
+    seqs.get(2).addSequenceFeature(
+            new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN,
+                    "RfamGroup"));
+    // existing feature type with null group
+    seqs.get(3).addSequenceFeature(
+            new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
+    // new feature type with null group
+    seqs.get(3).addSequenceFeature(
+            new SequenceFeature("Scop", "Desc", 5, 9, Float.NaN, null));
+    // null value for type produces NullPointerException
+    fr.findAllFeatures(true);
+    types = fr.getRenderOrder();
+    groups = fr.getFeatureGroups();
+    assertEquals(types.size(), 3);
+    assertFalse(types.contains("Type"));
+    assertTrue(types.contains("Pfam"));
+    assertTrue(types.contains("Rfam"));
+    assertTrue(types.contains("Scop"));
+    assertEquals(groups.size(), 2);
+    assertFalse(groups.contains("Group"));
+    assertTrue(groups.contains("PfamGroup"));
+    assertTrue(groups.contains("RfamGroup"));
+    assertFalse(groups.contains(null)); // null group is ignored
+
+    /*
+     * check min-max values
+     */
+    Map<String, float[][]> minMax = fr.getMinMax();
+    assertEquals(minMax.size(), 1); // non-positional and NaN not stored
+    assertEquals(minMax.get("Pfam")[0][0], 1f); // positional min
+    assertEquals(minMax.get("Pfam")[0][1], 2f); // positional max
+
+    // increase max for Pfam, add scores for Rfam
+    seqs.get(0).addSequenceFeature(
+            new SequenceFeature("Pfam", "Desc", 14, 22, 8f, "RfamGroup"));
+    seqs.get(1).addSequenceFeature(
+            new SequenceFeature("Rfam", "Desc", 5, 9, 6f, "RfamGroup"));
+    fr.findAllFeatures(true);
+    // note minMax is not a defensive copy, shouldn't expose this
+    assertEquals(minMax.size(), 2);
+    assertEquals(minMax.get("Pfam")[0][0], 1f);
+    assertEquals(minMax.get("Pfam")[0][1], 8f);
+    assertEquals(minMax.get("Rfam")[0][0], 6f);
+    assertEquals(minMax.get("Rfam")[0][1], 6f);
+
+    /*
+     * check render order (last is on top)
+     */
+    List<String> renderOrder = fr.getRenderOrder();
+    assertEquals(renderOrder, Arrays.asList("Scop", "Rfam", "Pfam"));
+
+    /*
+     * change render order (todo: an easier way)
+     * nb here last comes first in the data array
+     */
+    Object[][] data = new Object[3][];
+    FeatureColourI colour = new FeatureColour(Color.RED);
+    data[0] = new Object[] { "Rfam", colour, true };
+    data[1] = new Object[] { "Pfam", colour, false };
+    data[2] = new Object[] { "Scop", colour, false };
+    fr.setFeaturePriority(data);
+    assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam"));
+    assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
+
+    /*
+     * add a new feature type: should go on top of render order as visible,
+     * other feature ordering and visibility should be unchanged
+     */
+    seqs.get(2).addSequenceFeature(
+            new SequenceFeature("Metal", "Desc", 14, 22, 8f, "MetalGroup"));
+    fr.findAllFeatures(true);
+    assertEquals(fr.getRenderOrder(),
+            Arrays.asList("Scop", "Pfam", "Rfam", "Metal"));
+    assertEquals(fr.getDisplayedFeatureTypes(),
+            Arrays.asList("Rfam", "Metal"));
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeaturesAtColumn()
+  {
+    String seqData = ">s1/4-29\n-ab--cdefghijklmnopqrstuvwxyz\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
+    SequenceI seq = av.getAlignment().getSequenceAt(0);
+
+    /*
+     * with no features
+     */
+    List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, 3);
+    assertTrue(features.isEmpty());
+
+    /*
+     * add features
+     */
+    SequenceFeature sf1 = new SequenceFeature("Type1", "Desc", 0, 0, 1f,
+            "Group"); // non-positional
+    seq.addSequenceFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 8, 18, 1f,
+            "Group1");
+    seq.addSequenceFeature(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
+            "Group2");
+    seq.addSequenceFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
+            null); // null group is always treated as visible
+    seq.addSequenceFeature(sf4);
+
+    /*
+     * add contact features
+     */
+    SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, "Group1");
+    seq.addSequenceFeature(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, "Group2");
+    seq.addSequenceFeature(sf6);
+    SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, null);
+    seq.addSequenceFeature(sf7);
+
+    // feature spanning B--C
+    SequenceFeature sf8 = new SequenceFeature("Type1", "Desc", 5, 6, 1f,
+            "Group");
+    seq.addSequenceFeature(sf8);
+    // contact feature B/C
+    SequenceFeature sf9 = new SequenceFeature("Disulphide Bond", "Desc", 5,
+            6, 1f, "Group");
+    seq.addSequenceFeature(sf9);
+
+    /*
+     * let feature renderer discover features (and make visible)
+     */
+    fr.findAllFeatures(true);
+    features = fr.findFeaturesAtColumn(seq, 15); // all positional
+    assertEquals(features.size(), 6);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf6));
+    assertTrue(features.contains(sf7));
+
+    /*
+     * at a non-contact position
+     */
+    features = fr.findFeaturesAtColumn(seq, 14);
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+
+    /*
+     * make "Type2" not displayed
+     */
+    Object[][] data = new Object[4][];
+    FeatureColourI colour = new FeatureColour(Color.RED);
+    data[0] = new Object[] { "Type1", colour, true };
+    data[1] = new Object[] { "Type2", colour, false };
+    data[2] = new Object[] { "Type3", colour, true };
+    data[3] = new Object[] { "Disulphide Bond", colour, true };
+    fr.setFeaturePriority(data);
+
+    features = fr.findFeaturesAtColumn(seq, 15);
+    assertEquals(features.size(), 5); // no sf2
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf6));
+    assertTrue(features.contains(sf7));
+
+    /*
+     * make "Group2" not displayed
+     */
+    fr.setGroupVisibility("Group2", false);
+
+    features = fr.findFeaturesAtColumn(seq, 15);
+    assertEquals(features.size(), 3); // no sf2, sf3, sf6
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf7));
+
+    // features 'at' a gap between b and c
+    // - returns enclosing feature BC but not contact feature B/C
+    features = fr.findFeaturesAtColumn(seq, 4);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf8));
+    features = fr.findFeaturesAtColumn(seq, 5);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf8));
+  }
+
+  @Test(groups = "Functional")
+  public void testFilterFeaturesForDisplay()
+  {
+    String seqData = ">s1\nabcdef\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
+
+    List<SequenceFeature> features = new ArrayList<>();
+    fr.filterFeaturesForDisplay(features, null); // empty list, does nothing
+
+    SequenceI seq = av.getAlignment().getSequenceAt(0);
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
+            "group1");
+    seq.addSequenceFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 5, 11, 2f,
+            "group2");
+    seq.addSequenceFeature(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 5, 11, 3f,
+            "group3");
+    seq.addSequenceFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 6, 8, 4f,
+            "group4");
+    seq.addSequenceFeature(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "", 6, 9, 5f,
+            "group4");
+    seq.addSequenceFeature(sf5);
+
+    fr.findAllFeatures(true);
+
+    features = seq.getSequenceFeatures();
+    assertEquals(features.size(), 5);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * filter out duplicate (co-located) features
+     * note: which gets removed is not guaranteed
+     */
+    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertTrue(features.contains(sf2) || features.contains(sf3));
+    assertFalse(features.contains(sf2) && features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * hide group 3 - sf3 is removed, sf2 is retained
+     */
+    fr.setGroupVisibility("group3", false);
+    features = seq.getSequenceFeatures();
+    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertTrue(features.contains(sf2));
+    assertFalse(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * hide group 2, show group 3 - sf2 is removed, sf3 is retained
+     */
+    fr.setGroupVisibility("group2", false);
+    fr.setGroupVisibility("group3", true);
+    features = seq.getSequenceFeatures();
+    fr.filterFeaturesForDisplay(features, null);
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertFalse(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * no filtering of co-located features with graduated colour scheme
+     * filterFeaturesForDisplay does _not_ check colour threshold
+     * sf2 is removed as its group is hidden
+     */
+    features = seq.getSequenceFeatures();
+    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
+            Color.white, 0f, 1f));
+    assertEquals(features.size(), 4);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * co-located features with colour by label
+     * should not get filtered
+     */
+    features = seq.getSequenceFeatures();
+    FeatureColour fc = new FeatureColour(Color.black);
+    fc.setColourByLabel(true);
+    fr.filterFeaturesForDisplay(features, fc);
+    assertEquals(features.size(), 4);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+  }
+}
index 1c93856..b7a5164 100644 (file)
@@ -49,7 +49,7 @@ public class AnnotationColourGradientTest
       anns[col] = new Annotation("a", "a", 'a', col, colour);
     }
 
-    seq = new Sequence("", "");
+    seq = new Sequence("Seq", "");
     al = new Alignment(new SequenceI[]{ seq});
     
     /*
index c16d541..7a72c15 100644 (file)
@@ -22,6 +22,7 @@ package jalview.schemes;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
 
@@ -84,60 +85,11 @@ public class FeatureColourTest
   }
 
   @Test(groups = { "Functional" })
-  public void testIsColored_simpleColour()
-  {
-    FeatureColour fc = new FeatureColour(Color.RED);
-    assertTrue(fc.isColored(new SequenceFeature()));
-  }
-
-  @Test(groups = { "Functional" })
-  public void testIsColored_colourByLabel()
-  {
-    FeatureColour fc = new FeatureColour();
-    fc.setColourByLabel(true);
-    assertTrue(fc.isColored(new SequenceFeature()));
-  }
-
-  @Test(groups = { "Functional" })
-  public void testIsColored_aboveThreshold()
-  {
-    // graduated colour range from score 20 to 100
-    FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 20f,
-            100f);
-
-    // score 0 is adjusted to bottom of range
-    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 0f,
-            null);
-    assertTrue(fc.isColored(sf));
-    assertEquals(Color.WHITE, fc.getColor(sf));
-
-    // score 120 is adjusted to top of range
-    sf.setScore(120f);
-    assertEquals(Color.BLACK, fc.getColor(sf));
-
-    // value below threshold is still rendered
-    // setting threshold has no effect yet...
-    fc.setThreshold(60f);
-    sf.setScore(36f);
-    assertTrue(fc.isColored(sf));
-    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
-
-    // now apply threshold:
-    fc.setAboveThreshold(true);
-    assertFalse(fc.isColored(sf));
-    // colour is still returned though ?!?
-    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
-
-    sf.setScore(84); // above threshold now
-    assertTrue(fc.isColored(sf));
-    assertEquals(new Color(51, 51, 51), fc.getColor(sf));
-  }
-
-  @Test(groups = { "Functional" })
   public void testGetColor_simpleColour()
   {
     FeatureColour fc = new FeatureColour(Color.RED);
-    assertEquals(Color.RED, fc.getColor(new SequenceFeature()));
+    assertEquals(Color.RED,
+            fc.getColor(new SequenceFeature("Cath", "", 1, 2, 0f, null)));
   }
 
   @Test(groups = { "Functional" })
@@ -169,20 +121,35 @@ public class FeatureColourTest
   }
 
   @Test(groups = { "Functional" })
-  public void testGetColor_belowThreshold()
+  public void testGetColor_aboveBelowThreshold()
   {
     // gradient from [50, 150] from WHITE(255, 255, 255) to BLACK(0, 0, 0)
     FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 50f,
             150f);
     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 70f,
             null);
+
+    /*
+     * feature with score of Float.NaN is always assigned minimum colour
+     */
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 0, 20,
+            Float.NaN, null);
+
     fc.setThreshold(100f); // ignore for now
-    assertTrue(fc.isColored(sf));
     assertEquals(new Color(204, 204, 204), fc.getColor(sf));
+    assertEquals(Color.white, fc.getColor(sf2));
 
     fc.setAboveThreshold(true); // feature lies below threshold
-    assertFalse(fc.isColored(sf));
-    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
+    assertNull(fc.getColor(sf));
+    assertEquals(Color.white, fc.getColor(sf2));
+
+    fc.setBelowThreshold(true);
+    fc.setThreshold(70f);
+    assertNull(fc.getColor(sf)); // feature score == threshold - hidden
+    assertEquals(Color.white, fc.getColor(sf2));
+    fc.setThreshold(69f);
+    assertNull(fc.getColor(sf)); // feature score > threshold - hidden
+    assertEquals(Color.white, fc.getColor(sf2));
   }
 
   /**
index a7e52ff..a59fbde 100644 (file)
@@ -145,7 +145,7 @@ public class StructureSelectionManagerTest
     /*
      * Verify a RESNUM sequence feature in the PDBfile sequence
      */
-    SequenceFeature sf = pmap.getSeqs().get(0).getSequenceFeatures()[0];
+    SequenceFeature sf = pmap.getSeqs().get(0).getSequenceFeatures().get(0);
     assertEquals("RESNUM", sf.getType());
     assertEquals("1gaq", sf.getFeatureGroup());
     assertEquals("GLU:  19  1gaqA", sf.getDescription());
@@ -155,7 +155,7 @@ public class StructureSelectionManagerTest
      * sequence
      */
     StructureMapping map = sm.getMapping("examples/1gaq.txt")[0];
-    sf = map.sequence.getSequenceFeatures()[0];
+    sf = map.sequence.getSequenceFeatures().get(0);
     assertEquals("RESNUM", sf.getType());
     assertEquals("1gaq", sf.getFeatureGroup());
     assertEquals("ALA:   1  1gaqB", sf.getDescription());
index 70a3687..851b1b7 100644 (file)
@@ -1,6 +1,7 @@
 package jalview.viewmodel;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
 import jalview.analysis.AlignmentGenerator;
@@ -12,6 +13,7 @@ import jalview.datamodel.HiddenSequences;
 import java.beans.PropertyChangeEvent;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.testng.annotations.BeforeClass;
@@ -346,17 +348,41 @@ public class ViewportRangesTest {
   @Test(groups = { "Functional" })
   public void testScrollToWrappedVisible()
   {
-    ViewportRanges vr = new ViewportRanges(al);
+    AlignmentI al2 = gen.generate(60, 30, 1, 5, 5);
+
+    ViewportRanges vr = new ViewportRanges(al2);
+
+    // start with viewport on 5-14
     vr.setViewportStartAndWidth(5, 10);
+    assertEquals(vr.getStartRes(), 5);
+    assertEquals(vr.getEndRes(), 14);
 
-    vr.scrollToWrappedVisible(0);
+    // scroll to 12 - no change
+    assertFalse(vr.scrollToWrappedVisible(12));
+    assertEquals(vr.getStartRes(), 5);
+
+    // scroll to 2 - back to 0-9
+    assertTrue(vr.scrollToWrappedVisible(2));
     assertEquals(vr.getStartRes(), 0);
+    assertEquals(vr.getEndRes(), 9);
 
-    vr.scrollToWrappedVisible(10);
-    assertEquals(vr.getStartRes(), 10);
+    // scroll to 9 - no change
+    assertFalse(vr.scrollToWrappedVisible(9));
+    assertEquals(vr.getStartRes(), 0);
 
-    vr.scrollToWrappedVisible(15);
+    // scroll to 12 - moves to 10-19
+    assertTrue(vr.scrollToWrappedVisible(12));
     assertEquals(vr.getStartRes(), 10);
+    assertEquals(vr.getEndRes(), 19);
+
+    vr.setStartRes(13);
+    assertEquals(vr.getStartRes(), 13);
+    assertEquals(vr.getEndRes(), 22);
+
+    // scroll to 45 - jumps to 43-52
+    assertTrue(vr.scrollToWrappedVisible(45));
+    assertEquals(vr.getStartRes(), 43);
+    assertEquals(vr.getEndRes(), 52);
   }
 
   // leave until JAL-2388 is merged and we can do without viewport
@@ -509,9 +535,16 @@ public class ViewportRangesTest {
             Arrays.asList("startseq", "startseq", "startseq", "startseq")));
     l.reset();
 
-    vr.scrollToWrappedVisible(5);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    /*
+     * scrollToWrappedVisible does nothing if the target position is
+     * within the current startRes-endRes range
+     */
+    assertFalse(vr.scrollToWrappedVisible(5));
+    assertTrue(l.verify(0, Collections.<String> emptyList()));
     l.reset();
+
+    vr.scrollToWrappedVisible(25);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
   }
 
   @Test(groups = { "Functional" })
index 2f548d0..2d4be71 100644 (file)
@@ -26,9 +26,9 @@ import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertNull;
 
 import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.UniprotEntry;
+import jalview.datamodel.xdb.uniprot.UniprotEntry;
+import jalview.datamodel.xdb.uniprot.UniprotFeature;
 import jalview.gui.JvOptionPane;
 
 import java.io.Reader;
@@ -97,13 +97,12 @@ public class UniprotTest
     /*
      * Check sequence features
      */
-    Vector<SequenceFeature> features = entry.getFeature();
+    Vector<UniprotFeature> features = entry.getFeature();
     assertEquals(3, features.size());
-    SequenceFeature sf = features.get(0);
+    UniprotFeature sf = features.get(0);
     assertEquals("signal peptide", sf.getType());
     assertNull(sf.getDescription());
     assertNull(sf.getStatus());
-    assertEquals(1, sf.getPosition());
     assertEquals(1, sf.getBegin());
     assertEquals(18, sf.getEnd());
     sf = features.get(1);
@@ -139,10 +138,8 @@ public class UniprotTest
     xref = xrefs.get(2);
     assertEquals("AE007869", xref.getId());
     assertEquals("EMBL", xref.getType());
-    assertEquals("AAK85932.1",
- xref.getProperty("protein sequence ID"));
-    assertEquals("Genomic_DNA",
- xref.getProperty("molecule type"));
+    assertEquals("AAK85932.1", xref.getProperty("protein sequence ID"));
+    assertEquals("Genomic_DNA", xref.getProperty("molecule type"));
   }
 
   @Test(groups = { "Functional" })
index e35f83e..de91af3 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.ws.seqfetcher;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertTrue;
 
@@ -173,13 +174,13 @@ public class DbRefFetcherTest
     SequenceI seq = alsq.getSequenceAt(0);
     assertEquals("Wrong sequence name", embl.getDbSource() + "|"
             + retrievalId, seq.getName());
-    SequenceFeature[] sfs = seq.getSequenceFeatures();
-    assertNotNull("Sequence features missing", sfs);
+    List<SequenceFeature> sfs = seq.getSequenceFeatures();
+    assertFalse("Sequence features missing", sfs.isEmpty());
     assertTrue(
             "Feature not CDS",
             FeatureProperties.isCodingFeature(embl.getDbSource(),
-                    sfs[0].getType()));
-    assertEquals(embl.getDbSource(), sfs[0].getFeatureGroup());
+ sfs.get(0).getType()));
+    assertEquals(embl.getDbSource(), sfs.get(0).getFeatureGroup());
     DBRefEntry[] dr = DBRefUtils.selectRefs(seq.getDBRefs(),
             new String[] { DBRefSource.UNIPROT });
     assertNotNull(dr);
index 557700a..abe9d4b 100755 (executable)
@@ -1471,7 +1471,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>true</boolean>
                                                        </property>
                                                        <property name="destinationName">
-                                                               <string><![CDATA[min-jabaws-client-2.1.0.jar]]></string>
+                                                               <string><![CDATA[min-jabaws-client-2.2.0.jar]]></string>
                                                        </property>
                                                        <property name="fileSize">
                                                                <long>601804</long>