Merge branch 'develop' into releases/Release_2_11_Branch
authorJim Procter <jprocter@issues.jalview.org>
Tue, 11 Sep 2018 12:29:37 +0000 (13:29 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Tue, 11 Sep 2018 12:29:37 +0000 (13:29 +0100)
JAL-3091 bring 2.11 onto release 2.10.5 to simplify merge

217 files changed:
.classpath
RELEASE
THIRDPARTYLIBS
build.xml
examples/exampleFeatures.txt
help/help.jhm
help/helpTOC.xml
help/html/editing/index.html
help/html/features/clarguments.html
help/html/features/dasfeatures.html [deleted file]
help/html/features/dassettings.html [deleted file]
help/html/features/preferences.html
help/html/features/search.html
help/html/features/searchclearhist.png
help/html/features/seqfeatures.html
help/html/features/seqfetch.html
help/html/menus/alignmentMenu.html
help/html/menus/alwview.html
help/html/menus/wsmenu.html
help/html/releases.html
help/html/webServices/dbreffetcher.html
help/html/webServices/index.html
lib/htsjdk-2.12.0.jar [new file with mode: 0644]
lib/jdas-1.0.4.jar [deleted file]
nbbuild.xml
resources/lang/Messages.properties
resources/lang/Messages_es.properties
schemas/JalviewUserColours.xsd
schemas/jalview.xsd
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Dna.java
src/jalview/api/FeatureColourI.java
src/jalview/api/FeatureRenderer.java
src/jalview/appletgui/APopupMenu.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/FeatureColourChooser.java
src/jalview/appletgui/FeatureSettings.java
src/jalview/appletgui/OverviewCanvas.java
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/binding/Colour.java
src/jalview/binding/CompoundMatcher.java [new file with mode: 0644]
src/jalview/binding/FeatureMatcher.java [new file with mode: 0644]
src/jalview/binding/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/binding/Filter.java [new file with mode: 0644]
src/jalview/binding/JalviewUserColours.java
src/jalview/binding/MatchCondition.java [new file with mode: 0644]
src/jalview/binding/MatcherSet.java [new file with mode: 0644]
src/jalview/binding/types/ColourThreshTypeType.java [new file with mode: 0644]
src/jalview/binding/types/FeatureMatcherByType.java [new file with mode: 0644]
src/jalview/binding/types/NoValueColour.java [new file with mode: 0644]
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/DBRefEntry.java
src/jalview/datamodel/GeneLociI.java [new file with mode: 0644]
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/SequenceI.java
src/jalview/datamodel/features/FeatureAttributeType.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureAttributes.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcher.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherSetI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSource.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSourceI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSources.java [new file with mode: 0644]
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/datamodel/features/SequenceFeaturesI.java
src/jalview/ext/ensembl/EnsemblCdna.java
src/jalview/ext/ensembl/EnsemblCds.java
src/jalview/ext/ensembl/EnsemblData.java [new file with mode: 0644]
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblGenome.java
src/jalview/ext/ensembl/EnsemblInfo.java
src/jalview/ext/ensembl/EnsemblLookup.java
src/jalview/ext/ensembl/EnsemblMap.java
src/jalview/ext/ensembl/EnsemblProtein.java
src/jalview/ext/ensembl/EnsemblRestClient.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/ensembl/EnsemblSequenceFetcher.java
src/jalview/ext/ensembl/EnsemblSymbol.java
src/jalview/ext/htsjdk/HtsContigDb.java
src/jalview/ext/htsjdk/VCFReader.java [new file with mode: 0644]
src/jalview/gui/AlignFrame.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AquaInternalFrameManager.java
src/jalview/gui/CalculationChooser.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/CutAndPasteHtmlTransfer.java
src/jalview/gui/DasSourceBrowser.java [deleted file]
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureColourChooser.java [deleted file]
src/jalview/gui/FeatureRenderer.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java [new file with mode: 0644]
src/jalview/gui/FontChooser.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JDatabaseTree.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/JalviewDialog.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OverviewCanvas.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/gui/TreeCanvas.java
src/jalview/io/FeaturesFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/cache/JvCacheableInputBox.java
src/jalview/io/gff/Gff3Helper.java
src/jalview/io/gff/SequenceOntologyI.java
src/jalview/io/gff/SequenceOntologyLite.java
src/jalview/io/vcf/VCFLoader.java [new file with mode: 0644]
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GCutAndPasteHtmlTransfer.java
src/jalview/jbgui/GPreferences.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/schemabinding/version2/.castor.cdr
src/jalview/schemabinding/version2/Colour.java
src/jalview/schemabinding/version2/CompoundMatcher.java [new file with mode: 0644]
src/jalview/schemabinding/version2/FeatureMatcher.java [new file with mode: 0644]
src/jalview/schemabinding/version2/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/schemabinding/version2/Filter.java [new file with mode: 0644]
src/jalview/schemabinding/version2/JalviewUserColours.java
src/jalview/schemabinding/version2/MatchCondition.java [new file with mode: 0644]
src/jalview/schemabinding/version2/MatcherSet.java [new file with mode: 0644]
src/jalview/schemabinding/version2/OtherData.java
src/jalview/schemabinding/version2/Setting.java
src/jalview/schemabinding/version2/descriptors/ColourDescriptor.java
src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/JalviewUserColoursDescriptor.java
src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/OtherDataDescriptor.java
src/jalview/schemabinding/version2/descriptors/SettingDescriptor.java
src/jalview/schemabinding/version2/types/.castor.cdr [new file with mode: 0644]
src/jalview/schemabinding/version2/types/ColourThreshTypeType.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/FeatureMatcherByType.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/NoValueColour.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java [new file with mode: 0644]
src/jalview/schemes/FeatureColour.java
src/jalview/schemes/ResidueProperties.java
src/jalview/util/ColorUtils.java
src/jalview/util/MapList.java
src/jalview/util/MappingUtils.java
src/jalview/util/MathUtils.java [new file with mode: 0644]
src/jalview/util/StringUtils.java
src/jalview/util/matcher/Condition.java [new file with mode: 0644]
src/jalview/util/matcher/Matcher.java [new file with mode: 0644]
src/jalview/util/matcher/MatcherI.java [new file with mode: 0644]
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/DasSequenceFeatureFetcher.java [deleted file]
src/jalview/ws/SequenceFetcher.java
src/jalview/ws/dbsources/das/api/DasSourceRegistryI.java [deleted file]
src/jalview/ws/dbsources/das/api/jalviewSourceI.java [deleted file]
src/jalview/ws/dbsources/das/datamodel/DasSequenceSource.java [deleted file]
src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java [deleted file]
src/jalview/ws/dbsources/das/datamodel/JalviewSource.java [deleted file]
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/analysis/CrossRefTest.java
test/jalview/controller/AlignViewControllerTest.java
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/datamodel/features/FeatureAttributesTest.java [new file with mode: 0644]
test/jalview/datamodel/features/FeatureMatcherSetTest.java [new file with mode: 0644]
test/jalview/datamodel/features/FeatureMatcherTest.java [new file with mode: 0644]
test/jalview/datamodel/features/SequenceFeaturesTest.java
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/EnsemblSeqProxyAdapter.java
test/jalview/ext/ensembl/EnsemblSeqProxyTest.java
test/jalview/ext/htsjdk/TestHtsContigDb.java
test/jalview/ext/htsjdk/VCFReaderTest.java [new file with mode: 0644]
test/jalview/ext/so/SequenceOntologyTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/FeatureSettingsTest.java [new file with mode: 0644]
test/jalview/gui/PopupMenuTest.java
test/jalview/gui/SeqCanvasTest.java
test/jalview/io/CrossRef2xmlTests.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/Jalview2xmlTests.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/gff/SequenceOntologyLiteTest.java [new file with mode: 0644]
test/jalview/io/vcf/VCFLoaderTest.java [new file with mode: 0644]
test/jalview/io/vcf/contigs.fasta [new file with mode: 0644]
test/jalview/io/vcf/contigs.fasta.fai [new file with mode: 0644]
test/jalview/io/vcf/testVcf.dat [new file with mode: 0644]
test/jalview/io/vcf/testVcf.vcf [new file with mode: 0644]
test/jalview/io/vcf/testVcf2.vcf [new file with mode: 0644]
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java
test/jalview/renderer/seqfeatures/FeatureRendererTest.java
test/jalview/schemes/Blosum62ColourSchemeTest.java
test/jalview/schemes/FeatureColourTest.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java
test/jalview/util/MathUtilsTest.java [new file with mode: 0644]
test/jalview/util/StringUtilsTest.java
test/jalview/util/matcher/ConditionTest.java [new file with mode: 0644]
test/jalview/util/matcher/MatcherTest.java [new file with mode: 0644]
test/jalview/ws/PDBSequenceFetcherTest.java
test/jalview/ws/SequenceFetcherTest.java
test/jalview/ws/dbsources/RemoteFormatTest.java
test/jalview/ws/seqfetcher/DasSequenceFetcher.java [deleted file]
utils/MessageBundleChecker.java

index b19436c..ad1fa86 100644 (file)
@@ -36,7 +36,6 @@
        <classpathentry kind="lib" path="lib/miglayout-4.0-swing.jar"/>
        <classpathentry kind="lib" path="lib/jswingreader-0.3.jar" sourcepath="/jswingreader"/>
        <classpathentry kind="lib" path="lib/commons-codec-1.3.jar"/>
-       <classpathentry kind="lib" path="lib/jdas-1.0.4.jar"/>
        <classpathentry kind="lib" path="lib/spring-core-3.0.5.RELEASE.jar"/>
        <classpathentry kind="lib" path="lib/spring-web-3.0.5.RELEASE.jar"/>
        <classpathentry kind="lib" path="lib/jabaws-min-client-2.2.0.jar" sourcepath="/clustengine"/>
@@ -68,6 +67,7 @@
        <classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
        <classpathentry kind="lib" path="lib/biojava-core-4.1.0.jar"/>
        <classpathentry kind="lib" path="lib/biojava-ontology-4.1.0.jar"/>
+       <classpathentry kind="lib" path="lib/htsjdk-2.12.0.jar"/>
        <classpathentry kind="lib" path="lib/groovy-all-2.4.12-indy.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
        <classpathentry kind="output" path="classes"/>
diff --git a/RELEASE b/RELEASE
index 54b71f0..1960368 100644 (file)
--- a/RELEASE
+++ b/RELEASE
@@ -1,2 +1,2 @@
-jalview.release=releases/Release_2_10_5_Branch
-jalview.version=2.10.5
+jalview.release=releases/Release_2_11_Branch
+jalview.version=2.11.0
index 7d4ffc7..40d0f6c 100644 (file)
@@ -30,7 +30,6 @@ httpclient-4.0.3.jar
 httpcore-4.0.1.jar
 httpmime-4.0.3.jar
 jaxrpc.jar
-jdas-1.0.4.jar : Apache License - built from http://code.google.com/p/jdas/ (29th Feb 2012)
 jhall.jar
 jswingreader-0.3.jar : Apache license - built from http://jswingreader.sourceforge.net/ svn/trunk v12
 log4j-1.2.8.jar
@@ -39,8 +38,8 @@ miglayout-4.0-swing.jar       BSD http://www.migcalendar.com/miglayout/versions/4.0/li
 min-jaba-client.jar
 regex.jar
 saaj.jar
-spring-core-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven
-spring-web-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven
+spring-core-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven - TODO: JAL-3035 remove if no longer needed ?
+spring-web-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven - TODO: JAL-3035 remove if no longer needed ?
 vamsas-client.jar
 wsdl4j.jar
 xercesImpl.jar
index 957684b..5ca105a 100755 (executable)
--- a/build.xml
+++ b/build.xml
@@ -43,6 +43,7 @@
     <echo message="See docs/building.html and the comments in build file for other targets." />
     <echo message="note: compile and makeApplet optionally compile/obfuscate applet against a different Java version by specifying -Djava118.home=PathtoJDK/lib which is the lib directory in the JDK install that contains rt.jar " />
     <echo message="Useful -D flags: -Ddonotobfuscate will prevent applet obfuscation" />
+    <echo message="Useful -D flags: -Dclover.jar to specify path to openclover for testng coverage report" />
   </target>
 
 
     <!-- add the add-modules j2se attribute for java 9 -->
     <replace file="${jnlpFile}" value="j2se version=&quot;1.8+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee --illegal-access=warn&quot;">
           <replacetoken>j2se version="1.8+"</replacetoken>
-           
-        </replace>
+    </replace>
   </target>
 
   <target name="-dofakejnlpfileassoc" depends="-generatejnlpf" if="nojnlpfileassocs">
index 9e65534..99af214 100755 (executable)
@@ -26,6 +26,11 @@ BETA-TURN-IIL        8b5b50
 ST-MOTIF       ac25a1
 kdHydrophobicity       ccffcc|333300|-3.9|4.5|above|-2.0
 
+STARTFILTERS
+GAMMA-TURN-INVERSE     Label Contains PDB
+kdHydrophobicity       (Score LT 1.5) OR (Score GE 2.8)
+ENDFILTERS
+
 STARTGROUP     uniprot
 <html><a href="http://pfam.xfam.org/family/PF00111">Pfam family</a></html>     FER_CAPAA       -1      0       0       Pfam
 Iron-sulfur (2Fe-2S)   FER_CAPAA       -1      39      39      METAL
index e4bf12c..78f86b6 100755 (executable)
@@ -70,8 +70,6 @@
    <mapID target="annotations.fileformat" url="html/features/annotationsFormat.html"/>
    <mapID target="features.fileformat" url="html/features/featuresFormat.html"/>
    <mapID target="features.featureschemes" url="html/features/featureschemes.html"/>
-   <mapID target="das.settings" url="html/features/dassettings.html"/>
-   <mapID target="das.viewing" url="html/features/dasfeatures.html"/>
    <mapID target="edit" url="html/editing/index.html"/>
    <mapID target="jalarchive" url="html/features/jalarchive.html"/>
    <mapID target="multipleviews" url="html/features/multipleViews.html"/>
index b218b88..77ddd88 100755 (executable)
@@ -60,8 +60,6 @@
                        <tocitem text="Feature Colourschemes" target="features.featureschemes" />
                        <tocitem text="User Defined Sequence Features" target="seqfeatcreat" />
                        <tocitem text="Editing Sequence Features" target="seqfeatedit" />
-                       <tocitem text="DAS Feature Retrieval" target="das.viewing" />
-                       <tocitem text="DAS Feature Settings" target="das.settings" />
                        <tocitem text="HTML annotation report" target="io.seqreport" />
                </tocitem>
                
@@ -78,8 +76,7 @@
                        <tocitem text="Alignment Conservation Analysis" target="aacon" />
                        <tocitem text="Multi-Harmony Alignment Analysis" target="shmrws" />
                        <tocitem text="Sequence Retrieval" target="seqfetch" />
-                       <tocitem text="Database Reference Retrieval" target="dbreffetcher" />
-                       <tocitem text="DAS Feature Retrieval" target="das.viewing" />
+                       <tocitem text="Database Reference Retrieval" target="dbreffetcher" />                   
                </tocitem>
                
                <tocitem text="Colour Schemes" target="colours" expand="false">
index fd8c5a3..b3600c9 100755 (executable)
@@ -61,7 +61,7 @@
     <em>Undo / redo</em> - editing of sequences (insertion/removal of
     gaps, removal of sequences, trimming sequences etc) may be undone or
     redone at any time using the appropriate menu items from the edit
-    menu. The undo history list only allows a maximum of 10 actions.
+    menu.
   <p>
     <em>Trimming alignment</em> - First select a column by clicking the
     scale indicator (above the sequences) The alignment may then be
index e065494..fa273a5 100644 (file)
     </tr>
     <tr>
       <td>
-        <div align="center">-dasserver nickname=URL</div>
-      <td>
-        <div align="left">
-          Add and enable a <a href="dassettings.html">DAS server</a>
-          with given nickname (alphanumeric or underscores only) for
-          retrieval of features for all alignments<br> Sources that
-          also support the sequence command may be specified by
-          prepending the URL with 'sequence:'<br> <em>e.g.</em>
-          sequence:http://localdas.somewhere.org/das/source
-        </div>
-      </td>
-    </tr>
-    <tr>
-      <td>
-        <div align="center">-fetchfrom nickname</div>
-      <td>
-        <div align="left">
-          Query a <a href="dassettings.html">DAS source</a> called
-          nickname for features for the alignments and display them
-        </div>
-      </td>
-    </tr>
-    <tr>
-      <td>
         <div align="center">-groovy FILE/URL</div>
       <td>
         <div align="left">Execute groovy script in FILE (where
diff --git a/help/html/features/dasfeatures.html b/help/html/features/dasfeatures.html
deleted file mode 100644 (file)
index 1965e70..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<html>
-<!--
- * 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.
- -->
-<head>
-<title>DAS Features</title>
-</head>
-
-<body>
-  <p>
-    <strong>DAS Sequence Feature Retrieval</strong>
-  </p>
-  <p>Jalview includes a client for retrieving sequences and their
-    features via the Distributed Annotation System.</p>
-  <ol>
-    <li>Open the Feature Settings panel by selecting &quot;View
-      -&gt; Feature Settings...&quot;</li>
-    <li>Click on the &quot;<a href="dassettings.html">DAS
-        Settings</a>&quot; tabbed pane.
-    </li>
-    <li>Select the sources to use for DAS feature retrieval, then
-      click the &quot;Fetch DAS Features&quot; button.
-      <ul>
-        <li>Cancelling Feature Retrieval<br> Press the <strong>Cancel
-            Fetch</strong> button to immediately stop feature retrieval. This
-          will not remove any features already added to the alignment,
-          but will halt any outstanding DAS requests.<em>The cancel
-            fetch button is of particular use when one or more DAS
-            annotation servers are not responding!</em>
-      </ul>
-    </li>
-  </ol>
-  <p>
-    If your DAS source selection contains sources which use UniProt
-    accession ids, you will be asked whether Jalview should find UniProt
-    Accession ids for the given sequence names. It is important to
-    realise that many DAS sources only use UniProt accession ids, rather
-    than Swissprot/UniProt sequence names.<br> The <a
-      href="../webServices/dbreffetcher.html">database
-      reference fetcher</a> documentation describes how Jalview discovers
-    what database references are appropriate for the sequences in the
-    alignment.
-  <ul>
-    <li><em>Note</em><br> Please remember to save your
-      alignment if either the start/end numbering, or the sequence IDs
-      were updated during the ID retrieval process.</li>
-  </ul>
-  <p>&nbsp;
-  <p>
-    <em>DAS support was introduced in Jalview Version 2.1.</em>
-  </p>
-  <br />
-  <p>
-    <em>The DAS registry at http://www.dasregistry.org was
-      decommissioned early in 2015. An unmaintained mirror is currently
-      hosted at http://www.ebi.ac.uk/das-srv/registry/.</em>
-  </p>
-  <p>&nbsp;
-</body>
-</html>
diff --git a/help/html/features/dassettings.html b/help/html/features/dassettings.html
deleted file mode 100644 (file)
index 9600070..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<html>
-<!--
- * 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.
- -->
-<head>
-<title>DAS Settings</title>
-</head>
-
-<body>
-  <p>
-    <strong>DAS Settings</strong>
-  </p>
-  <p>
-    Jalview can retrieve sequences and features from many <a
-      href="http://biodas.org/">DAS</a> sources. The DAS sources
-    that it uses are discovered and selected <em>via</em> the DAS
-    settings panel, opened either from the <a
-      href="featuresettings.html">View&#8594;Feature Settings</a> dialog
-    box from the alignment window's menu bar, or the <a
-      href="featuresettings.html">Tools&#8594;Preferences</a>
-    dialog box opened from the Desktop menu bar.
-  </p>
-  <p>
-    <img src="das.gif">
-  <p>The available sources are listed in the table using each
-    source's Nickname as its identifier. Clicking on a source's entry in
-    the table reveals more information about that service in the panel
-    to the right. Select the tickbox in the &quot;Use Source&quot;
-    column for a source to add it to the set Jalview queries for
-    alignment and sequence features.</p>
-  <p>You can filter the visible DAS sources by authority, type and
-    &quot;label&quot;. You should read the DAS documentation to
-    understand more about these values.
-  <p>
-    <strong>Updating the list of sources</strong>
-  </p>
-  <p>
-    When the DAS Settings panel is first opened, and when the <strong>'Refresh
-      source'</strong> buton is pressed, a list of DAS sources is retrieved from
-    the DAS registry URL. Note that the registry hosted at
-    http://www.dasregistry.org/das/ was retired at the start of 2015. An
-    alternative service is currently hosted at
-    http://www.ebi.ac.uk/das-srv/registry/das/. To connect to this
-    service, ensure your .jalview_properties file includes the following
-    line:<br> <b>DAS_REGISTRY_URL=http\://www.ebi.ac.uk/das-srv/registry/das/</b>
-  </p>
-  <p>
-    <strong>Adding your own DAS Sources</strong>
-  </p>
-  <p>You can add your own DAS source to the list by clicking the
-    &quot;Add Local Source&quot; button. Enter the URL and nickname of
-    your additional service. It should be noted that Jalview 2.1 will
-    not query additional sources for more information, but this will be
-    implemented in future editions.
-  <p>&nbsp;
-</body>
-</html>
index 50f864b..52e88db 100755 (executable)
         Preferences</a> tab contains settings affecting the export of
       sequence alignments and EPS files.
     </li>
-    <li>The <a href="dassettings.html"><strong>&quot;DAS
-          Settings&quot;</strong> Preferences</a> tab allows you to select which DAS
-      sources to use when fetching DAS Features.
-    </li>
     <li>The <a href="../webServices/webServicesPrefs.html"><strong>&quot;Web
           Service&quot;</strong> Preferences</a> tab allows you to configure the <a
       href="http://www.compbio.dundee.ac.uk/jabaws">JABAWS</a>
index 72e5bdf..eec68ee 100755 (executable)
@@ -159,7 +159,7 @@ td {
   <p>The search history keeps up to 99 queries by default. To clear
     the history, or modify the size of the history, right-click the text
     box.</p>
-  <img src="searchclearhist.png" width="402" height="121" align="left" />
+  <img src="searchclearhist.png" width="402" height="127" align="left" />
   <p>
     <strong>Other dialogs that provide a query history</strong>
   </p>
index 200c4d3..29cc34f 100644 (file)
Binary files a/help/html/features/searchclearhist.png and b/help/html/features/searchclearhist.png differ
index 3c9d2b8..eeb63f6 100755 (executable)
       href="featuresettings.html">Sequence Feature Settings</a>
     dialog box. Feature colour schemes and display parameters are unique
     to a particular alignment, so it is possible to colour the same
-    sequence features differently in different alignment views.<br>
-    Since Jalview 2.1, it is possible to add <a href="dassettings.html">DAS
-      features</a> to an alignment via the DAS tabbed pane of the feature
-    settings window.
+    sequence features differently in different alignment views.
   </p>
   <p>
     <strong>View&#8594;Sequence ID Tooltip&#8594;Show
index 44aa1c2..e726c49 100755 (executable)
   <p>
     <strong>Sequence Fetcher</strong>
   </p>
-  <p>
-    Jalview can retrieve sequences from certain databases using either
-    the DBFetch service provided by the EMBL European Bioinformatics
-    Institute, or, since Jalview 2.4, DAS servers capable of the <em>sequence</em>
-    command (configured in <a href="dassettings.html">DAS settings</a>).
-  </p>
-  <p>The Sequence Fetcher can be opened via the &quot;File&quot;
+       <p>Jalview can retrieve sequences from a range of sequence, 3D
+               structure, genomic and domain family databases provided by EMBL-EBI.</p>
+       <p>The Sequence Fetcher can be opened via the &quot;File&quot;
     menu on the main desktop in order to retrieve sequences as a new
     alignment, or opened via the &quot;File&quot; menu of an existing
     alignment to import additional sequences. There may be a short delay
-    when the sequence fetcher is first opened, whilst Jalview compiles
-    the list of available sequence datasources from the currently
-    defined DAS server registry.</p>
+    when the sequence fetcher is first opened, whilst Jalview contacts each database's web API.</p>
   <p>
     Every time a new fetcher is opened, you will need to <strong>select
       the database you want to retrieve sequences</strong> from the database
     tooltips are shown if you mouse over some sources, explaining what
     the database will retrieve. You can select one by using the up/down
     arrow keys and hitting return, or by double clicking with the mouse.
-    <br />
-    <em>If you have DAS sources enabled, then you may have several
-      sources for the same type of sequence identifier, and these will
-      be grouped together in a sub-branch branch labeled with the
-      identifier.</em>
   </p>
   <p>Once you have selected a sequence database, its fetcher dialog
     will open. Jalview provides two types of dialog:</p>
       currently selected database into the retrieval box. Finally, press
       &quot;OK&quot; to initiate the retrieval.</li>
   </ol>
-  <p>
-    <strong>Only retrieving part of a sequence</strong>
-  </p>
-  <p>
-    When using DAS sources (indicated by a &quot;<em>(DAS)</em>&quot;),
-    you can append a range in addition to a sequence ID. For example, to
-    retrieve 50 residues starting at position 35 in UNIPROT sequence
-    P73137 using the UNIPROT DAS server, you would enter
-    &quot;'P73137:35,84'.<br /> <em>Full support for DAS range
-      queries was introduced in Jalview 2.8</em>
-  </p>
 
   <p>If you use the WSDBFetch sequence fetcher services (EMBL,
     UniProt, PFAM, and RFAM) in work for publication, please cite:</p>
index 00a2ec4..f3ab75d 100755 (executable)
             href="../features/featuresettings.html">Sequence
               Feature Settings...</a> </strong><br> <em>Opens the
               Sequence Feature Settings dialog box to control the colour
-              and display of sequence features on the alignment, and
-              configure and retrieve features from DAS annotation
-              servers.</em></li>
+              and display of sequence features on the alignment.</em></li>
         <li><strong>Sequence ID Tooltip</strong><em>
             (application only) <br>This submenu's options allow the
             inclusion or exclusion of non-positional sequence features
       is dynamic, and may contain user-defined web service entries in
       addition to any of the following ones:</em>
     <ul>
-      <li><strong>Fetch DB References</strong><br> <em>This
-          submenu contains options for accessing any of the database
-          services that Jalview is aware of (e.g. DAS sequence servers
-          and the WSDBFetch service provided by the EBI) to verify
-          sequence start/end positions and retrieve all database cross
-          references and PDB ids associated with all or just the
-          selected sequences in the alignment.
-          <ul>
-            <li>'Trim Retrieved Sequences' - when checked, Jalview
-              will discard any additional sequence data for accessions
-              associated with sequences in the alignment. <br> <strong>Note:
-                Disabling this could cause out of memory errors when
-                working with genomic sequence records !</strong><br> <strong>Added
-                in Jalview 2.8.1</strong>
-            </li>
-            <li>'Standard Databases' will check sequences against
-              the EBI databases plus any active DAS sequence sources</li>
-          </ul> Other sub-menus allow you to pick a specific source to query
-          - sources are listed alphabetically according to their
-          nickname.
-      </em><br></li>
-    </ul>
+                               <li><strong>Fetch DB References</strong><br> <em>This
+                                               submenu contains options for accessing any of the database
+                                               services that Jalview is aware of (e.g. those provided by
+                                               EMBL-EBI) to verify sequence start/end positions and retrieve all
+                                               database cross references and PDB ids associated with all or just
+                                               the selected sequences in the alignment.
+                                               <ul>
+                                                       <li>'Trim Retrieved Sequences' - when checked, Jalview will
+                                                               discard any additional sequence data for accessions associated
+                                                               with sequences in the alignment. <br> <strong>Note:
+                                                                       Disabling this could cause out of memory errors when working
+                                                                       with genomic sequence records !</strong><br> <strong>Added
+                                                                       in Jalview 2.8.1</strong>
+                                                       </li>
+                                                       <li>'Standard Databases' will check sequences against the
+                                                               EBI databases.</li>
+                                               </ul> Other sub-menus allow you to pick a specific source to query -
+                                               sources are listed alphabetically according to their nickname.
+                               </em><br></li>
+                       </ul>
     <p>Selecting items from the following submenus will start a
       remote service on compute facilities at the University of Dundee,
       or elsewhere. You need a continuous network connection in order to
index 2028304..33ec77f 100755 (executable)
@@ -53,8 +53,7 @@
     <li><strong><a href="../features/featuresettings.html">Sequence
           Feature Settings...</a></strong><em><br> Opens the Sequence
         Feature Settings dialog box to control the colour and display of
-        sequence features on the alignment, and configure and retrieve
-        features from DAS annotation servers.</em></li>
+        sequence features on the alignment.</em></li>
     <li><strong>Sequence ID Tooltip</strong><em> (application
         only) <br>This submenu's options allow the inclusion or
         exclusion of non-positional sequence features or database cross
index b71bc1a..b8b357e 100755 (executable)
       dynamic, and may contain user-defined web service entries in
       addition to any of the following ones:</em>
   <ul>
-    <li><strong>Fetch DB References</strong><br> <em>This
-        submenu contains options for accessing any of the database
-        services that Jalview is aware of (e.g. DAS sequence servers and
-        the WSDBFetch service provided by the EBI) to verify sequence
-        start/end positions and retrieve all database cross references
-        and PDB ids associated with all or just the selected sequences
-        in the alignment.
-        <ul>
+    <li><strong>Fetch DB References</strong><br> <em>This submenu
+                               contains options for accessing any of the database services that
+                               Jalview is aware of (e.g services provided by the EBI) to verify
+                               sequence start/end positions and retrieve all database cross
+                               references and PDB ids associated with all or just the selected
+                               sequences in the alignment.
+                               <ul>
           <li>'Retrieve full Sequence' - when checked, Jalview will
             retrieve the full sequence for any accessions associated
             with sequences in the alignment. <br> <strong>Note:
@@ -45,7 +44,7 @@
               in Jalview 2.8.1</strong>
           </li>
           <li>'Standard Databases' will check sequences against the
-            EBI databases plus any active DAS sequence sources<</li>
+            EBI databases</li>
         </ul> Other submenus allow you to pick a specific source to query -
         sources are listed alphabetically according to their nickname.
     </em></li>
index df1ddcd..3eaf234 100755 (executable)
@@ -67,6 +67,30 @@ li:before {
         </div>
       </td>
     </tr>
+    
+      <td width="60" nowrap>
+        <div align="center">
+          <strong><a name="Jalview.2.11.0">2.11.0</a><br />
+            <em>8/09/2018</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <em></em>
+          <ul>
+            <li>
+              <!--  -->
+            </li>
+          </ul>
+        </div></td>
+      <td><div align="left">
+          <em></em>
+          <ul>
+            <li>
+              <!-- JAL-3035 -->DAS sequence retrieval and annotation capabilities removed from the Jalview Desktop
+            </li>
+          </ul>
+        </div></td>
+    </tr>
     <tr>
     <td width="60" nowrap>
       <div align="center">
index 83c80ba..1f61dea 100644 (file)
@@ -30,8 +30,7 @@
   ID, and can be viewed in full via the
   <a href="../io/exportseqreport.html">Sequence Details</a> window. .
   Jalview also uses references for the retrieval of
-  <a href="../features/viewingpdbs.html">PDB structures</a> and <a
-    href="../features/dasfeatures.html">DAS features</a>, and for
+  <a href="../features/viewingpdbs.html">PDB structures</a>, and for
   retrieving sequence cross-references such as the protein products of a
   DNA sequence.
 </p>
   application provides three ways to access the retrieval function.
   Either:
 <ul>
-  <li>select the <strong>Discover PDB IDs</strong> option from the
-    structure submenu of the sequence's popup menu
-  </li>
-  <li>Choose one of the options from the 'Fetch DB Refs' submenu in
+       <li>select the <strong>Structure Chooser...</strong> option from
+               the Sequence ID popup menu.
+       </li>
+       <li>Choose one of the options from the 'Fetch DB Refs' submenu in
     the alignment window's <strong>Web Services</strong> menu:
     <ul>
-      <li><em>Standard Databases</em> will fetch references from
-        the EBI databases plus currently selected DAS sources</li>
-      <li>The other entries submenus leading to lists of individual
+                       <li><em>Standard Databases</em> will fetch references from EBI
+                               databases appropriate for the sequence type (Nucleotide or Protein)</li>
+                       <li>The other entries submenus leading to lists of individual
         database sources that Jalview can access.</li>
     </ul>
   </li>
-  <li>Answer 'Yes' when asked if you wish to retrieve database
-    references for your sequences after initiating a DAS Sequence
-    Feature fetch.</li>
 </ul>
 <p>Jalview discovers references for a sequence by generating a set
   of ID queries from the ID string of each sequence in the alignment. It
index d0b497c..8f36013 100755 (executable)
       Institute (EBI) and Distributed Annotation System servers that are
       capable of serving sequences.
     </li>
-    <li>The <a href="../features/dasfeatures.html">DAS Feature
-        Fetcher</a> enables the retrieval and visualization of features from
-      DAS annotation sources
-    </li>
-    <li>The <a href="dbreffetcher.html">Database Reference
-        Fetcher</a> transfers database references from records available
-      from DAS or the public sequence databases.
-    </li>
-    <li>The <strong>Web Services</strong> menu in each alignment
+               <li>The <a href="dbreffetcher.html">Database Reference
+                               Fetcher</a> transfers database references and annotation from the public
+                       sequence databases.
+               </li>
+               <li>The <strong>Web Services</strong> menu in each alignment
       window also provides access to the following:
       <ul>
         <li>Programs for <a href="msaclient.html">multiple
diff --git a/lib/htsjdk-2.12.0.jar b/lib/htsjdk-2.12.0.jar
new file mode 100644 (file)
index 0000000..1df12b2
Binary files /dev/null and b/lib/htsjdk-2.12.0.jar differ
diff --git a/lib/jdas-1.0.4.jar b/lib/jdas-1.0.4.jar
deleted file mode 100644 (file)
index fb5d128..0000000
Binary files a/lib/jdas-1.0.4.jar and /dev/null differ
index d6e4cf3..ca8a275 100644 (file)
@@ -17,7 +17,7 @@
  * 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.
 -->
-<!--
+
 <!-- You may freely edit this file. See commented blocks below for -->
 <!-- some examples of how to customize the build. -->
 <!-- (If you delete it and reopen the project it will be recreated.) -->
index 4d87b80..ae5b0e7 100644 (file)
@@ -242,7 +242,6 @@ label.documentation = Documentation
 label.about = About...
 label.show_sequence_limits = Show Sequence Limits
 action.feature_settings = Feature Settings...
-label.feature_settings = Feature Settings
 label.all_columns = All Columns
 label.all_sequences = All Sequences
 label.selected_columns = Selected Columns 
@@ -275,6 +274,7 @@ label.chimera_missing = Chimera structure viewer not found.<br/>Please enter the
 label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure
 label.min_colour = Minimum Colour
 label.max_colour = Maximum Colour
+label.no_colour = No Colour
 label.use_original_colours = Use Original Colours
 label.threshold_minmax = Threshold is min/max
 label.represent_group_with = Represent Group with {0}
@@ -282,9 +282,9 @@ label.selection = Selection
 label.group_colour = Group Colour
 label.sequence = Sequence
 label.view_pdb_structure = View PDB Structure
-label.min = Min:
-label.max = Max:
-label.colour_by_label = Colour by label
+label.min_value = Min value
+label.max_value = Max value
+label.no_value = No value
 label.new_feature = New Feature
 label.match_case = Match Case
 label.view_alignment_editor = View in alignment editor
@@ -369,6 +369,8 @@ label.optimise_order = Optimise Order
 label.seq_sort_by_score = Sequence sort by Score
 label.load_colours = Load Colours
 label.save_colours = Save Colours
+label.load_colours_tooltip = Load feature colours and filters from file
+label.save_colours_tooltip = Save feature colours and filters to file
 label.fetch_das_features = Fetch DAS Features
 label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} 
 label.database_param = Database: {0}
@@ -487,6 +489,10 @@ label.settings_for_type = Settings for {0}
 label.view_full_application = View in Full Application
 label.load_associated_tree = Load Associated Tree...
 label.load_features_annotations = Load Features/Annotations...
+label.load_vcf = Load SNP variants from plain text or indexed VCF data
+label.load_vcf_file = Load VCF File
+label.searching_vcf = Loading VCF variants...
+label.added_vcf = Added {0} VCF variants to {1} sequence(s)
 label.export_features = Export Features...
 label.export_annotations = Export Annotations...
 label.to_upper_case = To Upper Case
@@ -525,7 +531,6 @@ label.threshold_feature_above_threshold = Above Threshold
 label.threshold_feature_below_threshold = Below Threshold
 label.adjust_threshold = Adjust threshold
 label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
-label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
 label.select_colour_minimum_value = Select Colour for Minimum Value
 label.select_colour_maximum_value = Select Colour for Maximum Value
 label.open_url_param = Open URL {0}
@@ -777,7 +782,7 @@ label.pairwise_aligned_sequences = Pairwise Aligned Sequences
 label.original_data_for_params = Original Data for {0}
 label.points_for_params = Points for {0}
 label.transformed_points_for_params = Transformed points for {0}
-label.graduated_color_for_params = Graduated Feature Colour for {0}
+label.variable_color_for = Variable Feature Colour for {0}
 label.select_background_colour = Select Background Colour
 label.invalid_font = Invalid Font
 label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
@@ -864,7 +869,7 @@ label.msa_service_is_unknown = The Multiple Sequence Alignment Service named {0}
 label.service_called_is_not_seq_search_service = The Service called \n{0}\nis not a \nSequence Search Service\!
 label.seq_search_service_is_unknown = The Sequence Search Service named {0} is unknown
 label.feature_type = Feature Type
-label.display = Display
+label.show = Show
 label.service_url = Service URL
 label.copied_sequences = Copied sequences
 label.cut_sequences = Cut Sequences
@@ -1316,9 +1321,41 @@ label.select_hidden_colour = Select hidden colour
 label.overview = Overview
 label.reset_to_defaults = Reset to defaults
 label.oview_calc = Recalculating overview...
+label.feature_details = Feature details
+label.matchCondition_contains = Contains
+label.matchCondition_notcontains = Does not contain
+label.matchCondition_matches = Matches
+label.matchCondition_notmatches = Does not match
+label.matchCondition_present = Is present
+label.matchCondition_notpresent = Is not present
+label.matchCondition_eq = =
+label.matchCondition_ne = not =
+label.matchCondition_lt = <
+label.matchCondition_le = <=
+label.matchCondition_gt = >
+label.matchCondition_ge = >=
+label.numeric_required = The value should be numeric
+label.filter = Filter
+label.filters = Filters
+label.join_conditions = Join conditions with
+label.score = Score
+label.colour_by_label = Colour by label
+label.variable_colour = Variable colour...
+label.select_colour = Select colour
 option.enable_disable_autosearch = When ticked, search is performed automatically
 option.autosearch = Autosearch
 label.retrieve_ids = Retrieve IDs
+label.display_settings_for = Display settings for {0} features
+label.simple = Simple
+label.simple_colour = Simple Colour
+label.colour_by_text = Colour by text
+label.graduated_colour = Graduated Colour
+label.by_text_of = By text of
+label.by_range_of = By range of
+label.filters_tooltip = Click to set or amend filters
+label.or = Or
+label.and = And
+label.sequence_feature_colours = Sequence Feature Colours
 label.best_quality = Best Quality
 label.best_resolution = Best Resolution
 label.most_protein_chain = Most Protein Chain
index d7c040c..555977d 100644 (file)
@@ -226,7 +226,6 @@ label.automatic_scrolling = Desplazamiento autom
 label.documentation = Documentación
 label.about = Acerca de...
 label.show_sequence_limits = Mostrar los límites de la secuencia
-label.feature_settings = Ajustar funciones...
 label.all_columns = Todas las columnas
 label.all_sequences = Todas las secuencias
 label.selected_columns = Columnas seleccionadas
@@ -243,6 +242,7 @@ label.apply_all_groups = Aplicar a todos los grupos
 label.autocalculated_annotation = Anotación autocalculada
 label.min_colour = Color mínimo
 label.max_colour = Color máximo
+label.no_colour = Sin color
 label.use_original_colours = Usar colores originales
 label.threshold_minmax = El umbral es mín/máx
 label.represent_group_with = Representar al grupo con
@@ -250,8 +250,9 @@ label.selection = Seleccionar
 label.group_colour = Color del grupo
 label.sequence = Secuencia
 label.view_pdb_structure = Ver estructura PDB
-label.min = Mín:
-label.max = Máx:
+label.max_value = Valor máximo
+label.min_value = Valor mínimo
+label.no_value = Sin valor
 label.colour_by_label = Color por etiquetas
 label.new_feature = Nueva función
 label.match_case = Hacer corresponder mayúsculas y minúsculas
@@ -336,6 +337,8 @@ label.optimise_order = Optimizar orden
 label.seq_sort_by_score = Ordenar las secuencias por puntuación
 label.load_colours = Cargar colores
 label.save_colours = Guardar colores
+label.load_colours_tooltip = Cargar colores y filtros desde fichero
+label.save_colours_tooltip = Guardar colores y filtros en fichero
 label.fetch_das_features = Recuperar funciones DAS
 label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para buscar de {2} 
 label.database_param = Base de datos: {0}
@@ -452,6 +455,10 @@ label.settings_for_type = Ajustes para {0}
 label.view_full_application = Ver en la aplicación completa 
 label.load_associated_tree = Cargar Ã¡rbol asociado ...
 label.load_features_annotations = Cargar características/anotaciones ...
+label.load_vcf = Cargar variantes SNP desde fichero VCF texto o tab-indexado
+label.load_vcf_file = Cargar fichero VCF
+label.searching_vcf = Cargando variantes VCF...
+label.added_vcf= {0} variantes VCF añadidas a {1} secuencia(s)
 label.export_features = Exportar características...
 label.export_annotations = Exportar anotaciones ...
 label.to_upper_case = Pasar a mayúsculas
@@ -485,7 +492,6 @@ label.threshold_feature_above_threshold = Por encima del umbral
 label.threshold_feature_below_threshold = Por debajo del umbral
 label.adjust_threshold = Ajustar umbral
 label.toggle_absolute_relative_display_threshold = Cambiar entre mostrar el umbral absoluto y el relativo.
-label.display_features_same_type_different_label_using_different_colour = Mostrar las características del mismo tipo con una etiqueta diferente y empleando un color distinto (p.e. características del dominio)
 label.select_colour_minimum_value = Seleccionar el color para el valor mínimo
 label.select_colour_maximum_value = Seleccionar el color para el valor máximo
 label.open_url_param = Abrir URL {0}
@@ -705,7 +711,7 @@ label.pairwise_aligned_sequences = Secuencias alineadas a pares
 label.original_data_for_params = Datos originales de {0}
 label.points_for_params = Puntos de {0}
 label.transformed_points_for_params = Puntos transformados de {0}
-label.graduated_color_for_params = Color graduado para la característica de {0}
+label.variable_color_for = Color variable para la característica de {0}
 label.select_background_colour = Seleccionar color de fondo
 label.invalid_font = Fuente no válida
 label.separate_multiple_accession_ids = Separar los accession id con un punto y coma ";"
@@ -788,7 +794,7 @@ label.msa_service_is_unknown = El Servicio de Alineamiento M
 label.service_called_is_not_seq_search_service = El Servicio llamando \n{0}\nno es un \nServicio de B\u00FAsqueda de Secuencias\!
 label.seq_search_service_is_unknown = El Servicio de Búsqueda de Sencuencias llamado {0} es desconocido
 label.feature_type = Tipo de característisca
-label.display = Representación
+label.show = Mostrar
 label.service_url = URL del servicio
 label.copied_sequences = Secuencias copiadas
 label.cut_sequences = Cortar secuencias
@@ -1316,9 +1322,41 @@ label.select_hidden_colour = Seleccionar color de las regiones ocultas
 label.overview = Resumen
 label.reset_to_defaults = Restablecen a los predeterminados
 label.oview_calc = Recalculando resumen
+label.feature_details = Detalles de característica 
+label.matchCondition_contains = Contiene
+label.matchCondition_notcontains = No contiene
+label.matchCondition_matches = Es igual a
+label.matchCondition_notmatches = No es igual a
+label.matchCondition_present = Está presente
+label.matchCondition_notpresent = No está presente
+label.matchCondition_eq = =
+label.matchCondition_ne = not =
+label.matchCondition_lt = <
+label.matchCondition_le = <=
+label.matchCondition_gt = >
+label.matchCondition_ge = >=
+label.numeric_required = Valor numérico requerido
+label.filter = Filtro
+label.filters = Filtros
+label.join_conditions = Combinar condiciones con
+label.score = Puntuación
+label.colour_by_label = Colorear por texto
+label.variable_colour = Color variable...
+label.select_colour = Seleccionar color
 option.enable_disable_autosearch = Marcar para buscar automáticamente
 option.autosearch = Auto búsqueda
 label.retrieve_ids = Recuperar IDs
+label.display_settings_for = Visualización de características {0}
+label.simple = Simple
+label.simple_colour = Color simple
+label.colour_by_text = Colorear por texto
+label.graduated_colour = Color graduado
+label.by_text_of = Por texto de
+label.by_range_of = Por rango de
+label.filters_tooltip = Haga clic para configurar o modificar los filtros
+label.or = O
+label.and = Y
+label.sequence_feature_colours = Colores de características de las secuencias
 label.best_quality = Mejor Calidad
 label.best_resolution = Mejor Resolución
 label.most_protein_chain = Más Cadena de Proteína
index bd43e9d..3934d66 100755 (executable)
@@ -16,8 +16,7 @@
   
   You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<!-- edited with XMLSpy v2005 rel. 3 U (http://www.altova.com) by lj (jl) -->
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="www.jalview.org/colours">
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jalview="www.jalview.org/colours" targetNamespace="www.jalview.org/colours">
        <xs:complexType name="JalviewUserColours">
                <xs:sequence>
                        <xs:element name="Version" maxOccurs="1" minOccurs="0" type="xs:string">
                        </xs:element>
                        <xs:element name="colour" maxOccurs="unbounded" minOccurs="0">
                                <xs:complexType>
-                                       <xs:attribute name="Name" type="xs:string"/>
+                                   <xs:sequence>
+                                           <xs:element name="attributeName" type="xs:string" minOccurs="0" maxOccurs="2">
+                                                       <xs:annotation>
+                                                               <xs:documentation>name of feature attribute to colour by, or attribute and sub-attribute</xs:documentation>
+                                                       </xs:annotation>
+                                           </xs:element> 
+                                       </xs:sequence>
+                                       <xs:attribute name="Name" type="xs:string">
+                                               <xs:annotation>
+                                                       <xs:documentation>Single letter residue code for an alignment colour scheme, or feature type for a feature colour scheme</xs:documentation>
+                                               </xs:annotation>
+                                       </xs:attribute>
                                        <xs:attribute name="RGB" type="xs:string" use="required"/>
                                        <xs:attribute name="minRGB" type="xs:string" use="optional"/>
-                                       <xs:attribute name="threshType" type="xs:string" use="optional">
-                                       <xs:annotation>
-                                       <xs:documentation>loosely specified enumeration: NONE,ABOVE, or BELOW</xs:documentation>
-                                       </xs:annotation>
+                                       <xs:attribute name="noValueColour" use="optional" type="jalview:NoValueColour" default="Min" />
+                                       <xs:attribute name="threshType" use="optional">
+                                               <xs:simpleType> 
+                                                       <xs:restriction base="xs:string">
+                                                     <xs:enumeration value="NONE" />
+                                                     <xs:enumeration value="ABOVE" />
+                                                     <xs:enumeration value="BELOW" />
+                                                   </xs:restriction>   
+                                           </xs:simpleType> 
                                        </xs:attribute>
                                        <xs:attribute name="threshold" type="xs:float" use="optional"/>
                                        <xs:attribute name="max" type="xs:float" use="optional"/>
                                        <xs:attribute name="autoScale" type="xs:boolean" use="optional"/>
                                </xs:complexType>
                        </xs:element>
+                       <xs:element name="filter" maxOccurs="unbounded" minOccurs="0" >
+                           <xs:complexType>
+                                       <xs:sequence> 
+                                               <xs:element name="matcherSet" type="jalview:FeatureMatcherSet" />
+                                       </xs:sequence> 
+                                       <xs:attribute name="featureType" type="xs:string" use="required"/>
+                           </xs:complexType>
+                       </xs:element>
                </xs:sequence>
                <xs:attribute name="schemeName" type="xs:string" use="optional"/>
        </xs:complexType>
+
+       <xs:complexType name="FeatureMatcherSet"> 
+               <xs:annotation>
+                       <xs:documentation>A feature match condition, which may be simple or compound</xs:documentation>
+               </xs:annotation>
+         <xs:choice> 
+           <xs:element name="matchCondition" type="jalview:FeatureMatcher" /> 
+           <xs:element name="compoundMatcher">
+                   <xs:complexType>
+                         <xs:sequence> 
+                             <xs:element name="matcherSet" minOccurs="2" maxOccurs="2" type="jalview:FeatureMatcherSet" /> 
+                         </xs:sequence> 
+                     <xs:attribute name="and" type="xs:boolean" use="required">
+                                 <xs:annotation>
+                                       <xs:documentation>If true, matchers are AND-ed, if false they are OR-ed</xs:documentation>
+                                 </xs:annotation>
+                     </xs:attribute>
+                   </xs:complexType>
+           </xs:element>
+         </xs:choice> 
+       </xs:complexType> 
+       
+       <xs:complexType name="FeatureMatcher"> 
+         <xs:sequence> 
+           <xs:element name="attributeName" type="xs:string" minOccurs="0" maxOccurs="2">
+                       <xs:annotation>
+                               <xs:documentation>name of feature attribute to filter on, or attribute and sub-attribute</xs:documentation>
+                       </xs:annotation>
+           </xs:element> 
+           <xs:element name="condition" type="xs:string" /> 
+           <xs:element name="value" type="xs:string" /> 
+         </xs:sequence>
+         <xs:attribute name="by">
+               <xs:simpleType> 
+                       <xs:restriction base="xs:string">
+                     <xs:enumeration value="byLabel" />
+                     <xs:enumeration value="byScore" />
+                     <xs:enumeration value="byAttribute" />
+                   </xs:restriction>   
+           </xs:simpleType> 
+         </xs:attribute> 
+       </xs:complexType> 
+       
+       <xs:simpleType name="NoValueColour">
+               <xs:annotation>
+                       <xs:documentation>Graduated feature colour if no score (or attribute) value</xs:documentation>
+               </xs:annotation>
+               <xs:restriction base="xs:string">
+             <xs:enumeration value="None" />
+             <xs:enumeration value="Min" />
+             <xs:enumeration value="Max" />
+           </xs:restriction>   
+       </xs:simpleType>
 </xs:schema>
index f0bd638..48824e7 100755 (executable)
                                                <xs:sequence>
                                                        <xs:element name="setting" minOccurs="0" maxOccurs="unbounded">
                                                                <xs:complexType>
+                                                                       <xs:sequence>
+                                                                           <xs:element name="attributeName" type="xs:string" minOccurs="0" maxOccurs="2">
+                                                                                       <xs:annotation>
+                                                                                               <xs:documentation>name of feature attribute to colour by, or attribute and sub-attribute</xs:documentation>
+                                                                                       </xs:annotation>
+                                                                           </xs:element> 
+                                                                               <xs:element name="matcherSet" minOccurs="0" type="jalview:FeatureMatcherSet">
+                                                                                       <xs:annotation>
+                                                                                               <xs:documentation>optional filter(s) applied to the feature type</xs:documentation>
+                                                                                       </xs:annotation>
+                                                                               </xs:element>
+                                                                       </xs:sequence>
                                                                        <xs:attribute name="type" type="xs:string" use="required" />
                                                                        <xs:attribute name="colour" type="xs:int" use="required" />
                                                                        <xs:attribute name="display" type="xs:boolean"
                                                                                        </xs:documentation>
                                                                                </xs:annotation>
                                                                        </xs:attribute>
+                                                                       <xs:attribute name="noValueColour" use="optional" type="jalview:NoValueColour" default="Min" />
                                                                        <xs:attribute name="threshold" type="xs:float"
                                                                                use="optional">
                                                                                <xs:annotation>
                        <xs:element name="otherData" minOccurs="0" maxOccurs="unbounded">
                                <xs:complexType>
                                        <xs:attribute name="key" type="xs:string" use="required" />
+                                       <xs:attribute name="key2" type="xs:string" use="optional">
+                                               <xs:annotation>
+                                                       <xs:documentation>key2 may be used for a sub-attribute of key</xs:documentation>
+                                               </xs:annotation>
+                                       </xs:attribute>
                                        <xs:attribute name="value" type="xs:string" use="required" />
                                </xs:complexType>
                        </xs:element>
index 85ae718..a910a5a 100644 (file)
@@ -40,6 +40,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -83,7 +84,7 @@ public class ChimeraManager
     this.structureManager = structureManager;
     chimera = null;
     chimeraListenerThread = null;
-    currentModelsMap = new HashMap<Integer, ChimeraModel>();
+    currentModelsMap = new HashMap<>();
 
   }
 
@@ -98,7 +99,7 @@ public class ChimeraManager
   public List<ChimeraModel> getChimeraModels(String modelName,
           ModelType modelType)
   {
-    List<ChimeraModel> models = new ArrayList<ChimeraModel>();
+    List<ChimeraModel> models = new ArrayList<>();
     for (ChimeraModel model : currentModelsMap.values())
     {
       if (modelName.equals(model.getModelName())
@@ -112,7 +113,7 @@ public class ChimeraManager
 
   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
   {
-    Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
+    Map<String, List<ChimeraModel>> models = new HashMap<>();
     for (ChimeraModel model : currentModelsMap.values())
     {
       String modelName = model.getModelName();
@@ -393,7 +394,7 @@ public class ChimeraManager
 
   public Map<Integer, ChimeraModel> getSelectedModels()
   {
-    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
+    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
     List<String> chimeraReply = sendChimeraCommand(
             "list selection level molecule", true);
     if (chimeraReply != null)
@@ -418,7 +419,7 @@ public class ChimeraManager
    */
   public List<String> getSelectedResidueSpecs()
   {
-    List<String> selectedResidues = new ArrayList<String>();
+    List<String> selectedResidues = new ArrayList<>();
     List<String> chimeraReply = sendChimeraCommand(
             "list selection level residue", true);
     if (chimeraReply != null)
@@ -471,7 +472,7 @@ public class ChimeraManager
   // TODO: [Optional] Handle smiles names in a better way in Chimera?
   public List<ChimeraModel> getModelList()
   {
-    List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
+    List<ChimeraModel> modelList = new ArrayList<>();
     List<String> list = sendChimeraCommand("list models type molecule",
             true);
     if (list != null)
@@ -494,7 +495,7 @@ public class ChimeraManager
    */
   public List<String> getPresets()
   {
-    ArrayList<String> presetList = new ArrayList<String>();
+    ArrayList<String> presetList = new ArrayList<>();
     List<String> output = sendChimeraCommand("preset list", true);
     if (output != null)
     {
@@ -550,17 +551,19 @@ public class ChimeraManager
     // iterate over possible paths for starting Chimera
     for (String chimeraPath : chimeraPaths)
     {
-      File path = new File(chimeraPath);
-      // uncomment the next line to simulate Chimera not installed
-      // path = new File(chimeraPath + "x");
-      if (!path.canExecute())
-      {
-        error += "File '" + path + "' does not exist.\n";
-        continue;
-      }
       try
       {
-        List<String> args = new ArrayList<String>();
+        // ensure symbolic links are resolved
+        chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
+        File path = new File(chimeraPath);
+        // uncomment the next line to simulate Chimera not installed
+        // path = new File(chimeraPath + "x");
+        if (!path.canExecute())
+        {
+          error += "File '" + path + "' does not exist.\n";
+          continue;
+        }
+        List<String> args = new ArrayList<>();
         args.add(chimeraPath);
         // shows Chimera output window but suppresses REST responses:
         // args.add("--debug");
@@ -573,7 +576,7 @@ public class ChimeraManager
         break;
       } catch (Exception e)
       {
-        // Chimera could not be started
+        // Chimera could not be started using this path
         error += e.getMessage();
       }
     }
@@ -699,7 +702,7 @@ public class ChimeraManager
 
   public List<String> getAttrList()
   {
-    List<String> attributes = new ArrayList<String>();
+    List<String> attributes = new ArrayList<>();
     final List<String> reply = sendChimeraCommand("list resattr", true);
     if (reply != null)
     {
@@ -718,7 +721,7 @@ public class ChimeraManager
   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
           ChimeraModel model)
   {
-    Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
+    Map<ChimeraResidue, Object> values = new HashMap<>();
     final List<String> reply = sendChimeraCommand("list residue spec "
             + model.toSpec() + " attribute " + aCommand, true);
     if (reply != null)
@@ -818,10 +821,10 @@ public class ChimeraManager
   protected List<String> sendRestCommand(String command)
   {
     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
-    List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
+    List<NameValuePair> commands = new ArrayList<>(1);
     commands.add(new BasicNameValuePair("command", command));
 
-    List<String> reply = new ArrayList<String>();
+    List<String> reply = new ArrayList<>();
     BufferedReader response = null;
     try
     {
index 343ebc7..d1217bf 100644 (file)
@@ -29,6 +29,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.IncompleteCodonException;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.Sequence;
@@ -36,6 +37,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
@@ -105,6 +107,15 @@ public class AlignmentUtils
     {
       return variant == null ? null : variant.getFeatureGroup();
     }
+
+    /**
+     * toString for aid in the debugger only
+     */
+    @Override
+    public String toString()
+    {
+      return base + ":" + (variant == null ? "" : variant.getDescription());
+    }
   }
 
   /**
@@ -384,7 +395,7 @@ public class AlignmentUtils
    * Answers true if the mappings include one between the given (dataset)
    * sequences.
    */
-  public static boolean mappingExists(List<AlignedCodonFrame> mappings,
+  protected static boolean mappingExists(List<AlignedCodonFrame> mappings,
           SequenceI aaSeq, SequenceI cdnaSeq)
   {
     if (mappings != null)
@@ -454,7 +465,7 @@ public class AlignmentUtils
     {
       String lastCodon = String.valueOf(cdnaSeqChars,
               cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase();
-      for (String stop : ResidueProperties.STOP)
+      for (String stop : ResidueProperties.STOP_CODONS)
       {
         if (lastCodon.equals(stop))
         {
@@ -525,7 +536,8 @@ public class AlignmentUtils
        * allow * in protein to match untranslatable in dna
        */
       final char aaRes = aaSeqChars[aaPos];
-      if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
+      if ((translated == null || ResidueProperties.STOP.equals(translated))
+              && aaRes == '*')
       {
         continue;
       }
@@ -557,7 +569,8 @@ public class AlignmentUtils
     if (dnaPos == cdnaSeqChars.length - CODON_LENGTH)
     {
       String codon = String.valueOf(cdnaSeqChars, dnaPos, CODON_LENGTH);
-      if ("STOP".equals(ResidueProperties.codonTranslate(codon)))
+      if (ResidueProperties.STOP
+              .equals(ResidueProperties.codonTranslate(codon)))
       {
         return true;
       }
@@ -1636,8 +1649,8 @@ public class AlignmentUtils
       productSeqs = new HashSet<>();
       for (SequenceI seq : products)
       {
-        productSeqs.add(seq.getDatasetSequence() == null ? seq
-                : seq.getDatasetSequence());
+        productSeqs.add(seq.getDatasetSequence() == null ? seq : seq
+                .getDatasetSequence());
       }
     }
 
@@ -1730,9 +1743,8 @@ public class AlignmentUtils
           /*
            * add a mapping from CDS to the (unchanged) mapped to range
            */
-          List<int[]> cdsRange = Collections
-                  .singletonList(new int[]
-                  { 1, cdsSeq.getLength() });
+          List<int[]> cdsRange = Collections.singletonList(new int[] { 1,
+              cdsSeq.getLength() });
           MapList cdsToProteinMap = new MapList(cdsRange,
                   mapList.getToRanges(), mapList.getFromRatio(),
                   mapList.getToRatio());
@@ -1754,7 +1766,7 @@ public class AlignmentUtils
            * add another mapping from original 'from' range to CDS
            */
           AlignedCodonFrame dnaToCdsMapping = new AlignedCodonFrame();
-          MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
+          final MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
                   cdsRange, 1, 1);
           dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeqDss,
                   dnaToCdsMap);
@@ -1764,6 +1776,13 @@ public class AlignmentUtils
           }
 
           /*
+           * transfer dna chromosomal loci (if known) to the CDS
+           * sequence (via the mapping)
+           */
+          final MapList cdsToDnaMap = dnaToCdsMap.getInverse();
+          transferGeneLoci(dnaSeq, cdsToDnaMap, cdsSeq);
+
+          /*
            * add DBRef with mapping from protein to CDS
            * (this enables Get Cross-References from protein alignment)
            * This is tricky because we can't have two DBRefs with the
@@ -1782,26 +1801,30 @@ public class AlignmentUtils
 
           for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
           {
-            // creates a complementary cross-reference to the source sequence's
-            // primary reference.
-
-            DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(),
-                    primRef.getSource() + ":" + primRef.getVersion(),
-                    primRef.getAccessionId());
-            cdsCrossRef
-                    .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap)));
+            /*
+             * create a cross-reference from CDS to the source sequence's
+             * primary reference and vice versa
+             */
+            String source = primRef.getSource();
+            String version = primRef.getVersion();
+            DBRefEntry cdsCrossRef = new DBRefEntry(source, source + ":"
+                    + version, primRef.getAccessionId());
+            cdsCrossRef.setMap(new Mapping(dnaDss, new MapList(cdsToDnaMap)));
             cdsSeqDss.addDBRef(cdsCrossRef);
 
+            dnaSeq.addDBRef(new DBRefEntry(source, version, cdsSeq
+                    .getName(), new Mapping(cdsSeqDss, dnaToCdsMap)));
+
             // problem here is that the cross-reference is synthesized -
             // cdsSeq.getName() may be like 'CDS|dnaaccession' or
             // 'CDS|emblcdsacc'
             // assuming cds version same as dna ?!?
 
-            DBRefEntry proteinToCdsRef = new DBRefEntry(primRef.getSource(),
-                    primRef.getVersion(), cdsSeq.getName());
+            DBRefEntry proteinToCdsRef = new DBRefEntry(source, version,
+                    cdsSeq.getName());
             //
-            proteinToCdsRef.setMap(
-                    new Mapping(cdsSeqDss, cdsToProteinMap.getInverse()));
+            proteinToCdsRef.setMap(new Mapping(cdsSeqDss, cdsToProteinMap
+                    .getInverse()));
             proteinProduct.addDBRef(proteinToCdsRef);
           }
 
@@ -1814,14 +1837,46 @@ public class AlignmentUtils
       }
     }
 
-    AlignmentI cds = new Alignment(
-            cdsSeqs.toArray(new SequenceI[cdsSeqs.size()]));
+    AlignmentI cds = new Alignment(cdsSeqs.toArray(new SequenceI[cdsSeqs
+            .size()]));
     cds.setDataset(dataset);
 
     return cds;
   }
 
   /**
+   * Tries to transfer gene loci (dbref to chromosome positions) from fromSeq to
+   * toSeq, mediated by the given mapping between the sequences
+   * 
+   * @param fromSeq
+   * @param targetToFrom
+   *          Map
+   * @param targetSeq
+   */
+  protected static void transferGeneLoci(SequenceI fromSeq,
+          MapList targetToFrom, SequenceI targetSeq)
+  {
+    if (targetSeq.getGeneLoci() != null)
+    {
+      // already have - don't override
+      return;
+    }
+    GeneLociI fromLoci = fromSeq.getGeneLoci();
+    if (fromLoci == null)
+    {
+      return;
+    }
+
+    MapList newMap = targetToFrom.traverse(fromLoci.getMap());
+
+    if (newMap != null)
+    {
+      targetSeq.setGeneLoci(fromLoci.getSpeciesId(),
+              fromLoci.getAssemblyId(), fromLoci.getChromosomeId(), newMap);
+    }
+  }
+
+  /**
    * A helper method that finds a CDS sequence in the alignment dataset that is
    * mapped to the given protein sequence, and either is, or has a mapping from,
    * the given dna sequence.
@@ -1999,21 +2054,23 @@ public class AlignmentUtils
   }
 
   /**
-   * add any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
+   * Adds any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
    * the given mapping.
    * 
    * @param cdsSeq
    * @param contig
+   * @param proteinProduct
    * @param mapping
-   * @return list of DBRefEntrys added.
+   * @return list of DBRefEntrys added
    */
-  public static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
+  protected static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
           SequenceI contig, SequenceI proteinProduct, Mapping mapping)
   {
 
-    // gather direct refs from contig congrent with mapping
+    // gather direct refs from contig congruent with mapping
     List<DBRefEntry> direct = new ArrayList<>();
     HashSet<String> directSources = new HashSet<>();
+
     if (contig.getDBRefs() != null)
     {
       for (DBRefEntry dbr : contig.getDBRefs())
@@ -2091,7 +2148,7 @@ public class AlignmentUtils
    *          subtypes in the Sequence Ontology)
    * @param omitting
    */
-  public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
+  protected static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
           MapList mapping, String select, String... omitting)
   {
     SequenceI copyTo = toSeq;
@@ -2246,7 +2303,7 @@ public class AlignmentUtils
    * @param dnaSeq
    * @return
    */
-  public static List<int[]> findCdsPositions(SequenceI dnaSeq)
+  protected static List<int[]> findCdsPositions(SequenceI dnaSeq)
   {
     List<int[]> result = new ArrayList<>();
 
@@ -2381,15 +2438,22 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
           {
-            String codon = base + base2 + base3;
-            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
+            if (!base1.equalsIgnoreCase(base))
             {
-              count++;
+              String codon = base.toUpperCase() + base2.toLowerCase()
+                      + base3.toLowerCase();
+              String canonical = base1.toUpperCase() + base2.toLowerCase()
+                      + base3.toLowerCase();
+              if (addPeptideVariant(peptide, peptidePos, residue, var,
+                      codon, canonical))
+              {
+                count++;
+              }
             }
           }
         }
@@ -2403,15 +2467,22 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
           {
-            String codon = base1 + base + base3;
-            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
+            if (!base2.equalsIgnoreCase(base))
             {
-              count++;
+              String codon = base1.toLowerCase() + base.toUpperCase()
+                      + base3.toLowerCase();
+              String canonical = base1.toLowerCase() + base2.toUpperCase()
+                      + base3.toLowerCase();
+              if (addPeptideVariant(peptide, peptidePos, residue, var,
+                      codon, canonical))
+              {
+                count++;
+              }
             }
           }
         }
@@ -2425,15 +2496,22 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
           {
-            String codon = base1 + base2 + base;
-            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
+            if (!base3.equalsIgnoreCase(base))
             {
-              count++;
+              String codon = base1.toLowerCase() + base2.toLowerCase()
+                      + base.toUpperCase();
+              String canonical = base1.toLowerCase() + base2.toLowerCase()
+                      + base3.toUpperCase();
+              if (addPeptideVariant(peptide, peptidePos, residue, var,
+                      codon, canonical))
+              {
+                count++;
+              }
             }
           }
         }
@@ -2444,20 +2522,22 @@ public class AlignmentUtils
   }
 
   /**
-   * Helper method that adds a peptide variant feature, provided the given codon
-   * translates to a value different to the current residue (is a non-synonymous
-   * variant). ID and clinical_significance attributes of the dna variant (if
-   * present) are copied to the new feature.
+   * Helper method that adds a peptide variant feature. ID and
+   * clinical_significance attributes of the dna variant (if present) are copied
+   * to the new feature.
    * 
    * @param peptide
    * @param peptidePos
    * @param residue
    * @param var
    * @param codon
+   *          the variant codon e.g. aCg
+   * @param canonical
+   *          the 'normal' codon e.g. aTg
    * @return true if a feature was added, else false
    */
   static boolean addPeptideVariant(SequenceI peptide, int peptidePos,
-          String residue, DnaVariant var, String codon)
+          String residue, DnaVariant var, String codon, String canonical)
   {
     /*
      * get peptide translation of codon e.g. GAT -> D
@@ -2465,62 +2545,79 @@ public class AlignmentUtils
      * e.g. multibase variants or HGMD_MUTATION etc
      * are currently ignored here
      */
-    String trans = codon.contains("-") ? "-"
+    String trans = codon.contains("-") ? null
             : (codon.length() > CODON_LENGTH ? null
                     : ResidueProperties.codonTranslate(codon));
-    if (trans != null && !trans.equals(residue))
+    if (trans == null)
+    {
+      return false;
+    }
+    String desc = canonical + "/" + codon;
+    String featureType = "";
+    if (trans.equals(residue))
+    {
+      featureType = SequenceOntologyI.SYNONYMOUS_VARIANT;
+    }
+    else if (ResidueProperties.STOP.equals(trans))
+    {
+      featureType = SequenceOntologyI.STOP_GAINED;
+    }
+    else
     {
       String residue3Char = StringUtils
               .toSentenceCase(ResidueProperties.aa2Triplet.get(residue));
       String trans3Char = StringUtils
               .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
-      String desc = "p." + residue3Char + peptidePos + trans3Char;
-      SequenceFeature sf = new SequenceFeature(
-              SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
-              peptidePos, var.getSource());
-      StringBuilder attributes = new StringBuilder(32);
-      String id = (String) var.variant.getValue(ID);
-      if (id != null)
-      {
-        if (id.startsWith(SEQUENCE_VARIANT))
-        {
-          id = id.substring(SEQUENCE_VARIANT.length());
-        }
-        sf.setValue(ID, id);
-        attributes.append(ID).append("=").append(id);
-        // TODO handle other species variants JAL-2064
-        StringBuilder link = new StringBuilder(32);
-        try
-        {
-          link.append(desc).append(" ").append(id).append(
-                  "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
-                  .append(URLEncoder.encode(id, "UTF-8"));
-          sf.addLink(link.toString());
-        } catch (UnsupportedEncodingException e)
-        {
-          // as if
-        }
-      }
-      String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
-      if (clinSig != null)
+      desc = "p." + residue3Char + peptidePos + trans3Char;
+      featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT;
+    }
+    SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos,
+            peptidePos, var.getSource());
+
+    StringBuilder attributes = new StringBuilder(32);
+    String id = (String) var.variant.getValue(ID);
+    if (id != null)
+    {
+      if (id.startsWith(SEQUENCE_VARIANT))
       {
-        sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
-        attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
-                .append(clinSig);
+        id = id.substring(SEQUENCE_VARIANT.length());
       }
-      peptide.addSequenceFeature(sf);
-      if (attributes.length() > 0)
+      sf.setValue(ID, id);
+      attributes.append(ID).append("=").append(id);
+      // TODO handle other species variants JAL-2064
+      StringBuilder link = new StringBuilder(32);
+      try
+      {
+        link.append(desc).append(" ").append(id).append(
+                "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
+                .append(URLEncoder.encode(id, "UTF-8"));
+        sf.addLink(link.toString());
+      } catch (UnsupportedEncodingException e)
       {
-        sf.setAttributes(attributes.toString());
+        // as if
       }
-      return true;
     }
-    return false;
+    String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
+    if (clinSig != null)
+    {
+      sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
+      attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
+              .append(clinSig);
+    }
+    peptide.addSequenceFeature(sf);
+    if (attributes.length() > 0)
+    {
+      sf.setAttributes(attributes.toString());
+    }
+    return true;
   }
 
   /**
    * Builds a map whose key is position in the protein sequence, and value is a
-   * list of the base and all variants for each corresponding codon position
+   * list of the base and all variants for each corresponding codon position.
+   * <p>
+   * This depends on dna variants being held as a comma-separated list as
+   * property "alleles" on variant features.
    * 
    * @param dnaSeq
    * @param dnaToProtein
@@ -2558,6 +2655,30 @@ public class AlignmentUtils
         // not handling multi-locus variant features
         continue;
       }
+
+      /*
+       * ignore variant if not a SNP
+       */
+      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
+      if (alls == null)
+      {
+        continue; // non-SNP VCF variant perhaps - can't process this
+      }
+
+      String[] alleles = alls.toUpperCase().split(",");
+      boolean isSnp = true;
+      for (String allele : alleles)
+      {
+        if (allele.trim().length() > 1)
+        {
+          isSnp = false;
+        }
+      }
+      if (!isSnp)
+      {
+        continue;
+      }
+
       int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
       if (mapsTo == null)
       {
@@ -2576,21 +2697,6 @@ public class AlignmentUtils
       }
 
       /*
-       * 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
index d534c8f..2ad8487 100644 (file)
@@ -437,6 +437,7 @@ public class Dna
   {
     List<int[]> skip = new ArrayList<>();
     int[] skipint = null;
+
     int npos = 0;
     int vc = 0;
 
@@ -574,7 +575,7 @@ public class Dna
             skip.add(skipint);
             skipint = null;
           }
-          if (aa.equals("STOP"))
+          if (aa.equals(ResidueProperties.STOP))
           {
             aa = STOP_ASTERIX;
           }
@@ -881,6 +882,23 @@ public class Dna
   }
 
   /**
+   * Answers the reverse complement of the input string
+   * 
+   * @see #getComplement(char)
+   * @param s
+   * @return
+   */
+  public static String reverseComplement(String s)
+  {
+    StringBuilder sb = new StringBuilder(s.length());
+    for (int i = s.length() - 1; i >= 0; i--)
+    {
+      sb.append(Dna.getComplement(s.charAt(i)));
+    }
+    return sb.toString();
+  }
+
+  /**
    * Returns dna complement (preserving case) for aAcCgGtTuU. Ambiguity codes
    * are treated as on http://reverse-complement.com/. Anything else is left
    * unchanged.
index 0ded079..4dbb1bb 100644 (file)
@@ -56,6 +56,14 @@ public interface FeatureColourI
   Color getMaxColour();
 
   /**
+   * Returns the 'no value' colour (used when a feature lacks score, or the
+   * attribute, being used for colouring)
+   * 
+   * @return
+   */
+  Color getNoColour();
+
+  /**
    * Answers true if the feature has a single colour, i.e. if isColourByLabel()
    * and isGraduatedColour() both answer false
    * 
@@ -64,7 +72,8 @@ public interface FeatureColourI
   boolean isSimpleColour();
 
   /**
-   * Answers true if the feature is coloured by label (description)
+   * Answers true if the feature is coloured by label (description) or by text
+   * value of an attribute
    * 
    * @return
    */
@@ -93,18 +102,6 @@ public interface FeatureColourI
   void setAboveThreshold(boolean b);
 
   /**
-   * Answers true if the threshold is the minimum value (when
-   * isAboveThreshold()) or maximum value (when isBelowThreshold()) of the
-   * colour range; only applicable when isGraduatedColour and either
-   * isAboveThreshold() or isBelowThreshold() answers true
-   * 
-   * @return
-   */
-  boolean isThresholdMinMax();
-
-  void setThresholdMinMax(boolean b);
-
-  /**
    * Returns the threshold value (if any), else zero
    * 
    * @return
@@ -156,7 +153,10 @@ public interface FeatureColourI
   Color getColor(SequenceFeature feature);
 
   /**
-   * Update the min-max range for a graduated colour scheme
+   * Update the min-max range for a graduated colour scheme. Note that the
+   * colour scheme may be configured to colour by feature score, or a
+   * (numeric-valued) attribute - the caller should ensure that the correct
+   * range is being set.
    * 
    * @param min
    * @param max
@@ -169,4 +169,27 @@ public interface FeatureColourI
    * @return
    */
   String toJalviewFormat(String featureType);
+
+  /**
+   * Answers true if colour is by attribute text or numerical value
+   * 
+   * @return
+   */
+  boolean isColourByAttribute();
+
+  /**
+   * Answers the name of the attribute (and optional sub-attribute...) used for
+   * colouring if any, or null
+   * 
+   * @return
+   */
+  String[] getAttributeName();
+
+  /**
+   * Sets the name of the attribute (and optional sub-attribute...) used for
+   * colouring if any, or null to remove this property
+   * 
+   * @return
+   */
+  void setAttributeName(String... name);
 }
index 9d2d7f4..cf3c8da 100644 (file)
@@ -22,6 +22,7 @@ package jalview.api;
 
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 
 import java.awt.Color;
 import java.awt.Graphics;
@@ -132,7 +133,7 @@ public interface FeatureRenderer
   List<String> getGroups(boolean visible);
 
   /**
-   * change visibility for a range of groups
+   * Set visibility for a list of groups
    * 
    * @param toset
    * @param visible
@@ -140,7 +141,7 @@ public interface FeatureRenderer
   void setGroupVisibility(List<String> toset, boolean visible);
 
   /**
-   * change visibiilty of given group
+   * Set visibility of the given feature group
    * 
    * @param group
    * @param visible
@@ -148,9 +149,9 @@ public interface FeatureRenderer
   void setGroupVisibility(String group, boolean visible);
 
   /**
-   * 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).
+   * Returns visible 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
@@ -215,4 +216,53 @@ public interface FeatureRenderer
    */
   float getTransparency();
 
+  /**
+   * Answers the filters applied to the given feature type, or null if none is
+   * set
+   * 
+   * @param featureType
+   * @return
+   */
+  FeatureMatcherSetI getFeatureFilter(String featureType);
+
+  /**
+   * Answers the feature filters map
+   * 
+   * @return
+   */
+  public Map<String, FeatureMatcherSetI> getFeatureFilters();
+
+  /**
+   * Sets the filters for the feature type, or removes them if a null or empty
+   * filter is passed
+   * 
+   * @param featureType
+   * @param filter
+   */
+  void setFeatureFilter(String featureType, FeatureMatcherSetI filter);
+
+  /**
+   * Replaces all feature filters with the given map
+   * 
+   * @param filters
+   */
+  void setFeatureFilters(Map<String, FeatureMatcherSetI> filters);
+
+  /**
+   * Returns the colour for a particular feature instance. This includes
+   * calculation of 'colour by label', or of a graduated score colour, if
+   * applicable.
+   * <p>
+   * Returns null if
+   * <ul>
+   * <li>feature type is not visible, or</li>
+   * <li>feature group is not visible, or</li>
+   * <li>feature values lie outside any colour threshold, or</li>
+   * <li>feature is excluded by filter conditions</li>
+   * </ul>
+   * 
+   * @param feature
+   * @return
+   */
+  Color getColour(SequenceFeature feature);
 }
index 46bd4fd..76f2705 100644 (file)
@@ -901,10 +901,7 @@ public class APopupMenu extends java.awt.PopupMenu
               .formatMessage("label.annotation_for_displayid", new Object[]
               { seq.getDisplayId(true) }));
       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
-              contents, seq, true, true,
-              (ap.seqPanel.seqCanvas.fr != null)
-                      ? ap.seqPanel.seqCanvas.fr.getMinMax()
-                      : null);
+              contents, seq, true, true, ap.seqPanel.seqCanvas.fr);
       contents.append("</p>");
     }
     Frame frame = new Frame();
index 63f2745..5ad212e 100644 (file)
@@ -1445,9 +1445,10 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     FeaturesFile formatter = new FeaturesFile();
     if (format.equalsIgnoreCase("Jalview"))
     {
-      features = formatter.printJalviewFormat(viewport.getAlignment()
-              .getSequencesArray(), getDisplayedFeatureCols(),
-              getDisplayedFeatureGroups(), true);
+      features = formatter.printJalviewFormat(
+              viewport.getAlignment().getSequencesArray(),
+              getDisplayedFeatureCols(), null, getDisplayedFeatureGroups(),
+              true);
     }
     else
     {
index 5a073c6..d9eae11 100644 (file)
@@ -58,6 +58,8 @@ public class FeatureColourChooser extends Panel implements ActionListener,
    */
   private static final int SCALE_FACTOR_1K = 1000;
 
+  private static final String COLON = ":";
+
   private JVDialog frame;
 
   private Frame owner;
@@ -167,9 +169,9 @@ public class FeatureColourChooser extends Panel implements ActionListener,
     slider.addAdjustmentListener(this);
     slider.addMouseListener(this);
     owner = (af != null) ? af : fs.frame;
-    frame = new JVDialog(owner, MessageManager
-            .formatMessage("label.graduated_color_for_params", new String[]
-            { type }), true, 480, 248);
+    frame = new JVDialog(owner, MessageManager.formatMessage(
+            "label.variable_color_for", new String[] { type }), true, 480,
+            248);
     frame.setMainPanel(this);
     validate();
     frame.setVisible(true);
@@ -198,8 +200,10 @@ public class FeatureColourChooser extends Panel implements ActionListener,
 
   private void jbInit() throws Exception
   {
-    Label minLabel = new Label(MessageManager.getString("label.min")),
-            maxLabel = new Label(MessageManager.getString("label.max"));
+    Label minLabel = new Label(
+            MessageManager.getString("label.min_value") + COLON);
+    Label maxLabel = new Label(
+            MessageManager.getString("label.max_value") + COLON);
     minLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
     maxLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
     // minColour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
index cd85ab7..a60aacd 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.api.FeatureSettingsControllerI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.BorderLayout;
 import java.awt.Button;
@@ -591,22 +592,19 @@ public class FeatureSettings extends Panel
   {
     Component[] comps = featurePanel.getComponents();
     int cSize = comps.length;
-
-    Object[][] tmp = new Object[cSize][3];
-    int tmpSize = 0;
-    for (int i = 0; i < cSize; i++)
-    {
-      MyCheckbox check = (MyCheckbox) comps[i];
-      tmp[tmpSize][0] = check.type;
-      tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
-      tmp[tmpSize][2] = new Boolean(check.getState());
-      tmpSize++;
+    FeatureSettingsBean[] rowData = new FeatureSettingsBean[cSize];
+    int i = 0;
+    for (Component comp : comps)
+    {
+      MyCheckbox check = (MyCheckbox) comp;
+      // feature filter set to null as not (yet) offered in applet
+      FeatureColourI colour = fr.getFeatureStyle(check.type);
+      rowData[i] = new FeatureSettingsBean(check.type, colour, null,
+              check.getState());
+      i++;
     }
 
-    Object[][] data = new Object[tmpSize][3];
-    System.arraycopy(tmp, 0, data, 0, tmpSize);
-
-    fr.setFeaturePriority(data);
+    fr.setFeaturePriority(rowData);
 
     ap.paintAlignment(updateOverview, updateOverview);
   }
index a556bdb..e99c021 100644 (file)
@@ -131,7 +131,6 @@ public class OverviewCanvas extends Component
               od.getGraphHeight(), od.getColumns(av.getAlignment()));
       mg.translate(0, -od.getSequencesHeight());
     }
-    System.gc();
 
     if (restart)
     {
index dcd6546..83bc810 100755 (executable)
@@ -28,8 +28,6 @@ import jalview.schemes.UserColourScheme;
 import jalview.structure.StructureImportSettings;
 import jalview.urls.IdOrgSettings;
 import jalview.util.ColorUtils;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.datamodel.DasSourceRegistry;
 import jalview.ws.sifts.SiftsSettings;
 
 import java.awt.Color;
@@ -982,22 +980,6 @@ public class Cache
     return null;
   }
 
-  private static DasSourceRegistryI sourceRegistry = null;
-
-  /**
-   * initialise and ..
-   * 
-   * @return instance of the das source registry
-   */
-  public static DasSourceRegistryI getDasSourceRegistry()
-  {
-    if (sourceRegistry == null)
-    {
-      sourceRegistry = new DasSourceRegistry();
-    }
-    return sourceRegistry;
-  }
-
   /**
    * Set the specified value, or remove it if null or empty. Does not save the
    * properties file.
index adf3773..3270144 100755 (executable)
@@ -156,7 +156,6 @@ public class Jalview
           af.setProgressBar(MessageManager
                   .getString("status.das_features_being_retrived"), id);
           af.featureSettings_actionPerformed(null);
-          af.featureSettings.fetchDasFeatures(dasSources, true);
           af.setProgressBar(null, id);
           synchronized (us)
           {
index 25cf9bf..f51e9af 100644 (file)
@@ -27,7 +27,8 @@ public class Colour implements java.io.Serializable
   // --------------------------/
 
   /**
-   * Field _name.
+   * Single letter residue code for an alignment colour scheme, or feature type
+   * for a feature colour scheme
    */
   private java.lang.String _name;
 
@@ -42,9 +43,15 @@ public class Colour implements java.io.Serializable
   private java.lang.String _minRGB;
 
   /**
-   * loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Field _noValueColour.
    */
-  private java.lang.String _threshType;
+  private jalview.binding.types.NoValueColour _noValueColour = jalview.binding.types.NoValueColour
+          .valueOf("Min");
+
+  /**
+   * Field _threshType.
+   */
+  private jalview.binding.types.ColourThreshTypeType _threshType;
 
   /**
    * Field _threshold.
@@ -96,6 +103,11 @@ public class Colour implements java.io.Serializable
    */
   private boolean _has_autoScale;
 
+  /**
+   * name of feature attribute to colour by, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -103,6 +115,8 @@ public class Colour implements java.io.Serializable
   public Colour()
   {
     super();
+    setNoValueColour(jalview.binding.types.NoValueColour.valueOf("Min"));
+    this._attributeNameList = new java.util.Vector();
   }
 
   // -----------/
@@ -110,41 +124,140 @@ public class Colour implements java.io.Serializable
   // -----------/
 
   /**
-     */
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   */
   public void deleteAutoScale()
   {
     this._has_autoScale = false;
   }
 
   /**
-     */
+   */
   public void deleteColourByLabel()
   {
     this._has_colourByLabel = false;
   }
 
   /**
-     */
+   */
   public void deleteMax()
   {
     this._has_max = false;
   }
 
   /**
-     */
+   */
   public void deleteMin()
   {
     this._has_min = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshold()
   {
     this._has_threshold = false;
   }
 
   /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
    * Returns the value of field 'autoScale'.
    * 
    * @return the value of field 'AutoScale'.
@@ -195,7 +308,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'name'.
+   * Returns the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @return the value of field 'Name'.
    */
@@ -205,6 +320,16 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'noValueColour'.
+   * 
+   * @return the value of field 'NoValueColour'.
+   */
+  public jalview.binding.types.NoValueColour getNoValueColour()
+  {
+    return this._noValueColour;
+  }
+
+  /**
    * Returns the value of field 'RGB'.
    * 
    * @return the value of field 'RGB'.
@@ -215,12 +340,11 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Returns the value of field 'threshType'.
    * 
    * @return the value of field 'ThreshType'.
    */
-  public java.lang.String getThreshType()
+  public jalview.binding.types.ColourThreshTypeType getThreshType()
   {
     return this._threshType;
   }
@@ -360,6 +484,76 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'autoScale'.
    * 
    * @param autoScale
@@ -419,7 +613,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'name'.
+   * Sets the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @param name
    *          the value of field 'name'.
@@ -430,6 +626,18 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'noValueColour'.
+   * 
+   * @param noValueColour
+   *          the value of field 'noValueColour'.
+   */
+  public void setNoValueColour(
+          final jalview.binding.types.NoValueColour noValueColour)
+  {
+    this._noValueColour = noValueColour;
+  }
+
+  /**
    * Sets the value of field 'RGB'.
    * 
    * @param RGB
@@ -441,13 +649,13 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Sets the value of field 'threshType'.
    * 
    * @param threshType
    *          the value of field 'threshType'.
    */
-  public void setThreshType(final java.lang.String threshType)
+  public void setThreshType(
+          final jalview.binding.types.ColourThreshTypeType threshType)
   {
     this._threshType = threshType;
   }
diff --git a/src/jalview/binding/CompoundMatcher.java b/src/jalview/binding/CompoundMatcher.java
new file mode 100644 (file)
index 0000000..a2d1048
--- /dev/null
@@ -0,0 +1,368 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class CompoundMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class CompoundMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * If true, matchers are AND-ed, if false they are OR-ed
+   */
+  private boolean _and;
+
+  /**
+   * keeps track of state for field: _and
+   */
+  private boolean _has_and;
+
+  /**
+   * Field _matcherSetList.
+   */
+  private java.util.Vector _matcherSetList;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public CompoundMatcher()
+  {
+    super();
+    this._matcherSetList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(final jalview.binding.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.addElement(vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(final int index,
+          final jalview.binding.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.add(index, vMatcherSet);
+  }
+
+  /**
+   */
+  public void deleteAnd()
+  {
+    this._has_and = false;
+  }
+
+  /**
+   * Method enumerateMatcherSet.
+   * 
+   * @return an Enumeration over all jalview.binding.MatcherSet elements
+   */
+  public java.util.Enumeration enumerateMatcherSet()
+  {
+    return this._matcherSetList.elements();
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean getAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method getMatcherSet.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the jalview.binding.MatcherSet at the given index
+   */
+  public jalview.binding.MatcherSet getMatcherSet(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    return (jalview.binding.MatcherSet) _matcherSetList.get(index);
+  }
+
+  /**
+   * Method getMatcherSet.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public jalview.binding.MatcherSet[] getMatcherSet()
+  {
+    jalview.binding.MatcherSet[] array = new jalview.binding.MatcherSet[0];
+    return (jalview.binding.MatcherSet[]) this._matcherSetList
+            .toArray(array);
+  }
+
+  /**
+   * Method getMatcherSetCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getMatcherSetCount()
+  {
+    return this._matcherSetList.size();
+  }
+
+  /**
+   * Method hasAnd.
+   * 
+   * @return true if at least one And has been added
+   */
+  public boolean hasAnd()
+  {
+    return this._has_and;
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean isAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllMatcherSet()
+  {
+    this._matcherSetList.clear();
+  }
+
+  /**
+   * Method removeMatcherSet.
+   * 
+   * @param vMatcherSet
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeMatcherSet(
+          final jalview.binding.MatcherSet vMatcherSet)
+  {
+    boolean removed = _matcherSetList.remove(vMatcherSet);
+    return removed;
+  }
+
+  /**
+   * Method removeMatcherSetAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public jalview.binding.MatcherSet removeMatcherSetAt(final int index)
+  {
+    java.lang.Object obj = this._matcherSetList.remove(index);
+    return (jalview.binding.MatcherSet) obj;
+  }
+
+  /**
+   * Sets the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @param and
+   *          the value of field 'and'.
+   */
+  public void setAnd(final boolean and)
+  {
+    this._and = and;
+    this._has_and = true;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setMatcherSet(final int index,
+          final jalview.binding.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    this._matcherSetList.set(index, vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param vMatcherSetArray
+   */
+  public void setMatcherSet(
+          final jalview.binding.MatcherSet[] vMatcherSetArray)
+  {
+    // -- copy array
+    _matcherSetList.clear();
+
+    for (int i = 0; i < vMatcherSetArray.length; i++)
+    {
+      this._matcherSetList.add(vMatcherSetArray[i]);
+    }
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.CompoundMatcher
+   */
+  public static jalview.binding.CompoundMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.CompoundMatcher) Unmarshaller
+            .unmarshal(jalview.binding.CompoundMatcher.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/FeatureMatcher.java b/src/jalview/binding/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..e4e52fb
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class FeatureMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _by.
+   */
+  private jalview.binding.types.FeatureMatcherByType _by;
+
+  /**
+   * name of feature attribute to filter on, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
+  /**
+   * Field _condition.
+   */
+  private java.lang.String _condition;
+
+  /**
+   * Field _value.
+   */
+  private java.lang.String _value;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcher()
+  {
+    super();
+    this._attributeNameList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
+   * Returns the value of field 'by'.
+   * 
+   * @return the value of field 'By'.
+   */
+  public jalview.binding.types.FeatureMatcherByType getBy()
+  {
+    return this._by;
+  }
+
+  /**
+   * Returns the value of field 'condition'.
+   * 
+   * @return the value of field 'Condition'.
+   */
+  public java.lang.String getCondition()
+  {
+    return this._condition;
+  }
+
+  /**
+   * Returns the value of field 'value'.
+   * 
+   * @return the value of field 'Value'.
+   */
+  public java.lang.String getValue()
+  {
+    return this._value;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
+   * Sets the value of field 'by'.
+   * 
+   * @param by
+   *          the value of field 'by'.
+   */
+  public void setBy(final jalview.binding.types.FeatureMatcherByType by)
+  {
+    this._by = by;
+  }
+
+  /**
+   * Sets the value of field 'condition'.
+   * 
+   * @param condition
+   *          the value of field 'condition'.
+   */
+  public void setCondition(final java.lang.String condition)
+  {
+    this._condition = condition;
+  }
+
+  /**
+   * Sets the value of field 'value'.
+   * 
+   * @param value
+   *          the value of field 'value'.
+   */
+  public void setValue(final java.lang.String value)
+  {
+    this._value = value;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcher
+   */
+  public static jalview.binding.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.binding.FeatureMatcher.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/FeatureMatcherSet.java b/src/jalview/binding/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..7ba5f0e
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * A feature match condition, which may be simple or compound
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherSet implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Internal choice value storage
+   */
+  private java.lang.Object _choiceValue;
+
+  /**
+   * Field _matchCondition.
+   */
+  private jalview.binding.MatchCondition _matchCondition;
+
+  /**
+   * Field _compoundMatcher.
+   */
+  private jalview.binding.CompoundMatcher _compoundMatcher;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'choiceValue'. The field 'choiceValue' has the
+   * following description: Internal choice value storage
+   * 
+   * @return the value of field 'ChoiceValue'.
+   */
+  public java.lang.Object getChoiceValue()
+  {
+    return this._choiceValue;
+  }
+
+  /**
+   * Returns the value of field 'compoundMatcher'.
+   * 
+   * @return the value of field 'CompoundMatcher'.
+   */
+  public jalview.binding.CompoundMatcher getCompoundMatcher()
+  {
+    return this._compoundMatcher;
+  }
+
+  /**
+   * Returns the value of field 'matchCondition'.
+   * 
+   * @return the value of field 'MatchCondition'.
+   */
+  public jalview.binding.MatchCondition getMatchCondition()
+  {
+    return this._matchCondition;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'compoundMatcher'.
+   * 
+   * @param compoundMatcher
+   *          the value of field 'compoundMatcher'.
+   */
+  public void setCompoundMatcher(
+          final jalview.binding.CompoundMatcher compoundMatcher)
+  {
+    this._compoundMatcher = compoundMatcher;
+    this._choiceValue = compoundMatcher;
+  }
+
+  /**
+   * Sets the value of field 'matchCondition'.
+   * 
+   * @param matchCondition
+   *          the value of field 'matchCondition'.
+   */
+  public void setMatchCondition(
+          final jalview.binding.MatchCondition matchCondition)
+  {
+    this._matchCondition = matchCondition;
+    this._choiceValue = matchCondition;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcherSet
+   */
+  public static jalview.binding.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcherSet) Unmarshaller
+            .unmarshal(jalview.binding.FeatureMatcherSet.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/Filter.java b/src/jalview/binding/Filter.java
new file mode 100644 (file)
index 0000000..687ae91
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class Filter.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class Filter implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _featureType.
+   */
+  private java.lang.String _featureType;
+
+  /**
+   * Field _matcherSet.
+   */
+  private jalview.binding.MatcherSet _matcherSet;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public Filter()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'featureType'.
+   * 
+   * @return the value of field 'FeatureType'.
+   */
+  public java.lang.String getFeatureType()
+  {
+    return this._featureType;
+  }
+
+  /**
+   * Returns the value of field 'matcherSet'.
+   * 
+   * @return the value of field 'MatcherSet'.
+   */
+  public jalview.binding.MatcherSet getMatcherSet()
+  {
+    return this._matcherSet;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'featureType'.
+   * 
+   * @param featureType
+   *          the value of field 'featureType'.
+   */
+  public void setFeatureType(final java.lang.String featureType)
+  {
+    this._featureType = featureType;
+  }
+
+  /**
+   * Sets the value of field 'matcherSet'.
+   * 
+   * @param matcherSet
+   *          the value of field 'matcherSet'.
+   */
+  public void setMatcherSet(final jalview.binding.MatcherSet matcherSet)
+  {
+    this._matcherSet = matcherSet;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.Filter
+   */
+  public static jalview.binding.Filter unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.Filter) Unmarshaller
+            .unmarshal(jalview.binding.Filter.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
index 6709487..67ee5a2 100644 (file)
@@ -42,6 +42,11 @@ public class JalviewUserColours implements java.io.Serializable
    */
   private java.util.Vector _colourList;
 
+  /**
+   * Field _filterList.
+   */
+  private java.util.Vector _filterList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -50,6 +55,7 @@ public class JalviewUserColours implements java.io.Serializable
   {
     super();
     this._colourList = new java.util.Vector();
+    this._filterList = new java.util.Vector();
   }
 
   // -----------/
@@ -84,6 +90,33 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.addElement(vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.add(index, vFilter);
+  }
+
+  /**
    * Method enumerateColour.
    * 
    * @return an Enumeration over all Colour elements
@@ -94,6 +127,16 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method enumerateFilter.
+   * 
+   * @return an Enumeration over all Filter elements
+   */
+  public java.util.Enumeration enumerateFilter()
+  {
+    return this._filterList.elements();
+  }
+
+  /**
    * Method getColour.
    * 
    * @param index
@@ -141,6 +184,53 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method getFilter.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the Filter at the given index
+   */
+  public Filter getFilter(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    return (Filter) _filterList.get(index);
+  }
+
+  /**
+   * Method getFilter.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public Filter[] getFilter()
+  {
+    Filter[] array = new Filter[0];
+    return (Filter[]) this._filterList.toArray(array);
+  }
+
+  /**
+   * Method getFilterCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getFilterCount()
+  {
+    return this._filterList.size();
+  }
+
+  /**
    * Returns the value of field 'schemeName'.
    * 
    * @return the value of field 'SchemeName'.
@@ -217,13 +307,20 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
-     */
+   */
   public void removeAllColour()
   {
     this._colourList.clear();
   }
 
   /**
+   */
+  public void removeAllFilter()
+  {
+    this._filterList.clear();
+  }
+
+  /**
    * Method removeColour.
    * 
    * @param vColour
@@ -248,6 +345,30 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method removeFilter.
+   * 
+   * @param vFilter
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeFilter(final Filter vFilter)
+  {
+    boolean removed = _filterList.remove(vFilter);
+    return removed;
+  }
+
+  /**
+   * Method removeFilterAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public Filter removeFilterAt(final int index)
+  {
+    java.lang.Object obj = this._filterList.remove(index);
+    return (Filter) obj;
+  }
+
+  /**
    * 
    * 
    * @param index
@@ -286,6 +407,44 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    this._filterList.set(index, vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param vFilterArray
+   */
+  public void setFilter(final Filter[] vFilterArray)
+  {
+    // -- copy array
+    _filterList.clear();
+
+    for (int i = 0; i < vFilterArray.length; i++)
+    {
+      this._filterList.add(vFilterArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'schemeName'.
    * 
    * @param schemeName
diff --git a/src/jalview/binding/MatchCondition.java b/src/jalview/binding/MatchCondition.java
new file mode 100644 (file)
index 0000000..44a3d3e
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class MatchCondition.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatchCondition extends FeatureMatcher
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatchCondition()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcher
+   */
+  public static jalview.binding.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.binding.MatchCondition.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/MatcherSet.java b/src/jalview/binding/MatcherSet.java
new file mode 100644 (file)
index 0000000..756d93a
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class MatcherSet.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatcherSet extends FeatureMatcherSet
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcherSet
+   */
+  public static jalview.binding.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcherSet) Unmarshaller
+            .unmarshal(jalview.binding.MatcherSet.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/types/ColourThreshTypeType.java b/src/jalview/binding/types/ColourThreshTypeType.java
new file mode 100644 (file)
index 0000000..024f2c0
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class ColourThreshTypeType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class ColourThreshTypeType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The NONE type
+     */
+    public static final int NONE_TYPE = 0;
+
+    /**
+     * The instance of the NONE type
+     */
+    public static final ColourThreshTypeType NONE = new ColourThreshTypeType(NONE_TYPE, "NONE");
+
+    /**
+     * The ABOVE type
+     */
+    public static final int ABOVE_TYPE = 1;
+
+    /**
+     * The instance of the ABOVE type
+     */
+    public static final ColourThreshTypeType ABOVE = new ColourThreshTypeType(ABOVE_TYPE, "ABOVE");
+
+    /**
+     * The BELOW type
+     */
+    public static final int BELOW_TYPE = 2;
+
+    /**
+     * The instance of the BELOW type
+     */
+    public static final ColourThreshTypeType BELOW = new ColourThreshTypeType(BELOW_TYPE, "BELOW");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private ColourThreshTypeType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of ColourThreshTypeType
+     * 
+     * @return an Enumeration over all possible instances of
+     * ColourThreshTypeType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this ColourThreshTypeType
+     * 
+     * @return the type of this ColourThreshTypeType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("NONE", NONE);
+        members.put("ABOVE", ABOVE);
+        members.put("BELOW", BELOW);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * ColourThreshTypeType
+     * 
+     * @return the String representation of this ColourThreshTypeTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new ColourThreshTypeType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the ColourThreshTypeType value of parameter 'string'
+     */
+    public static jalview.binding.types.ColourThreshTypeType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid ColourThreshTypeType";
+            throw new IllegalArgumentException(err);
+        }
+        return (ColourThreshTypeType) obj;
+    }
+
+}
diff --git a/src/jalview/binding/types/FeatureMatcherByType.java b/src/jalview/binding/types/FeatureMatcherByType.java
new file mode 100644 (file)
index 0000000..2185bba
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class FeatureMatcherByType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherByType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The byLabel type
+     */
+    public static final int BYLABEL_TYPE = 0;
+
+    /**
+     * The instance of the byLabel type
+     */
+    public static final FeatureMatcherByType BYLABEL = new FeatureMatcherByType(BYLABEL_TYPE, "byLabel");
+
+    /**
+     * The byScore type
+     */
+    public static final int BYSCORE_TYPE = 1;
+
+    /**
+     * The instance of the byScore type
+     */
+    public static final FeatureMatcherByType BYSCORE = new FeatureMatcherByType(BYSCORE_TYPE, "byScore");
+
+    /**
+     * The byAttribute type
+     */
+    public static final int BYATTRIBUTE_TYPE = 2;
+
+    /**
+     * The instance of the byAttribute type
+     */
+    public static final FeatureMatcherByType BYATTRIBUTE = new FeatureMatcherByType(BYATTRIBUTE_TYPE, "byAttribute");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private FeatureMatcherByType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of FeatureMatcherByType
+     * 
+     * @return an Enumeration over all possible instances of
+     * FeatureMatcherByType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this FeatureMatcherByType
+     * 
+     * @return the type of this FeatureMatcherByType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("byLabel", BYLABEL);
+        members.put("byScore", BYSCORE);
+        members.put("byAttribute", BYATTRIBUTE);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * FeatureMatcherByType
+     * 
+     * @return the String representation of this FeatureMatcherByTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new FeatureMatcherByType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the FeatureMatcherByType value of parameter 'string'
+     */
+    public static jalview.binding.types.FeatureMatcherByType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid FeatureMatcherByType";
+            throw new IllegalArgumentException(err);
+        }
+        return (FeatureMatcherByType) obj;
+    }
+
+}
diff --git a/src/jalview/binding/types/NoValueColour.java b/src/jalview/binding/types/NoValueColour.java
new file mode 100644 (file)
index 0000000..c1540f6
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding.types;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Graduated feature colour if no score (or attribute) value
+ * 
+ * @version $Revision$ $Date$
+ */
+public class NoValueColour implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * The None type
+   */
+  public static final int NONE_TYPE = 0;
+
+  /**
+   * The instance of the None type
+   */
+  public static final NoValueColour NONE = new NoValueColour(NONE_TYPE,
+          "None");
+
+  /**
+   * The Min type
+   */
+  public static final int MIN_TYPE = 1;
+
+  /**
+   * The instance of the Min type
+   */
+  public static final NoValueColour MIN = new NoValueColour(MIN_TYPE,
+          "Min");
+
+  /**
+   * The Max type
+   */
+  public static final int MAX_TYPE = 2;
+
+  /**
+   * The instance of the Max type
+   */
+  public static final NoValueColour MAX = new NoValueColour(MAX_TYPE,
+          "Max");
+
+  /**
+   * Field _memberTable.
+   */
+  private static java.util.Hashtable _memberTable = init();
+
+  /**
+   * Field type.
+   */
+  private int type = -1;
+
+  /**
+   * Field stringValue.
+   */
+  private java.lang.String stringValue = null;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  private NoValueColour(final int type, final java.lang.String value)
+  {
+    super();
+    this.type = type;
+    this.stringValue = value;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method enumerate.Returns an enumeration of all possible instances of
+   * NoValueColour
+   * 
+   * @return an Enumeration over all possible instances of NoValueColour
+   */
+  public static java.util.Enumeration enumerate()
+  {
+    return _memberTable.elements();
+  }
+
+  /**
+   * Method getType.Returns the type of this NoValueColour
+   * 
+   * @return the type of this NoValueColour
+   */
+  public int getType()
+  {
+    return this.type;
+  }
+
+  /**
+   * Method init.
+   * 
+   * @return the initialized Hashtable for the member table
+   */
+  private static java.util.Hashtable init()
+  {
+    Hashtable members = new Hashtable();
+    members.put("None", NONE);
+    members.put("Min", MIN);
+    members.put("Max", MAX);
+    return members;
+  }
+
+  /**
+   * Method readResolve. will be called during deserialization to replace the
+   * deserialized object with the correct constant instance.
+   * 
+   * @return this deserialized object
+   */
+  private java.lang.Object readResolve()
+  {
+    return valueOf(this.stringValue);
+  }
+
+  /**
+   * Method toString.Returns the String representation of this NoValueColour
+   * 
+   * @return the String representation of this NoValueColour
+   */
+  public java.lang.String toString()
+  {
+    return this.stringValue;
+  }
+
+  /**
+   * Method valueOf.Returns a new NoValueColour based on the given String value.
+   * 
+   * @param string
+   * @return the NoValueColour value of parameter 'string'
+   */
+  public static jalview.binding.types.NoValueColour valueOf(
+          final java.lang.String string)
+  {
+    java.lang.Object obj = null;
+    if (string != null)
+    {
+      obj = _memberTable.get(string);
+    }
+    if (obj == null)
+    {
+      String err = "" + string + " is not a valid NoValueColour";
+      throw new IllegalArgumentException(err);
+    }
+    return (NoValueColour) obj;
+  }
+
+}
index 460c2b3..d992e4e 100644 (file)
@@ -53,20 +53,19 @@ public class AlignViewController implements AlignViewControllerI
   private AlignViewControllerGuiI avcg;
 
   public AlignViewController(AlignViewControllerGuiI alignFrame,
-          AlignViewportI viewport, AlignmentViewPanel alignPanel)
+          AlignViewportI vp, AlignmentViewPanel ap)
   {
     this.avcg = alignFrame;
-    this.viewport = viewport;
-    this.alignPanel = alignPanel;
+    this.viewport = vp;
+    this.alignPanel = ap;
   }
 
   @Override
-  public void setViewportAndAlignmentPanel(AlignViewportI viewport,
-          AlignmentViewPanel alignPanel)
+  public void setViewportAndAlignmentPanel(AlignViewportI vp,
+          AlignmentViewPanel ap)
   {
-    this.alignPanel = alignPanel;
-    this.viewport = viewport;
-
+    this.alignPanel = ap;
+    this.viewport = vp;
   }
 
   @Override
@@ -215,17 +214,21 @@ public class AlignViewController implements AlignViewControllerI
 
   /**
    * Sets a bit in the BitSet for each column (base 0) in the sequence
-   * collection which includes the specified feature type. Returns the number of
-   * sequences which have the feature in the selected range.
+   * collection which includes a visible feature of the specified feature type.
+   * Returns the number of sequences which have the feature visible in the
+   * selected range.
    * 
    * @param featureType
    * @param sqcol
    * @param bs
    * @return
    */
-  static int findColumnsWithFeature(String featureType,
+  int findColumnsWithFeature(String featureType,
           SequenceCollectionI sqcol, BitSet bs)
   {
+    FeatureRenderer fr = alignPanel == null ? null : alignPanel
+            .getFeatureRenderer();
+
     final int startColumn = sqcol.getStartRes() + 1; // converted to base 1
     final int endColumn = sqcol.getEndRes() + 1;
     List<SequenceI> seqs = sqcol.getSequences();
@@ -238,13 +241,19 @@ public class AlignViewController implements AlignViewControllerI
         List<SequenceFeature> sfs = sq.findFeatures(startColumn,
                 endColumn, featureType);
 
-        if (!sfs.isEmpty())
-        {
-          nseq++;
-        }
-
+        boolean found = false;
         for (SequenceFeature sf : sfs)
         {
+          if (fr.getColour(sf) == null)
+          {
+            continue;
+          }
+          if (!found)
+          {
+            nseq++;
+          }
+          found = true;
+
           int sfStartCol = sq.findIndex(sf.getBegin());
           int sfEndCol = sq.findIndex(sf.getEnd());
 
@@ -343,25 +352,25 @@ public class AlignViewController implements AlignViewControllerI
   public boolean parseFeaturesFile(String file, DataSourceType protocol,
           boolean relaxedIdMatching)
   {
-    boolean featuresFile = false;
+    boolean featuresAdded = false;
+    FeatureRenderer fr = alignPanel.getFeatureRenderer();
     try
     {
-      featuresFile = new FeaturesFile(false, file, protocol).parse(
-              viewport.getAlignment().getDataset(),
-              alignPanel.getFeatureRenderer().getFeatureColours(), false,
-              relaxedIdMatching);
+      featuresAdded = new FeaturesFile(false, file, protocol).parse(
+              viewport.getAlignment().getDataset(), fr.getFeatureColours(),
+              fr.getFeatureFilters(), false, relaxedIdMatching);
     } catch (Exception ex)
     {
       ex.printStackTrace();
     }
 
-    if (featuresFile)
+    if (featuresAdded)
     {
       avcg.refreshFeatureUI(true);
-      if (alignPanel.getFeatureRenderer() != null)
+      if (fr != null)
       {
         // update the min/max ranges where necessary
-        alignPanel.getFeatureRenderer().findAllFeatures(true);
+        fr.findAllFeatures(true);
       }
       if (avcg.getFeatureSettingsUI() != null)
       {
@@ -370,7 +379,7 @@ public class AlignViewController implements AlignViewControllerI
       alignPanel.paintAlignment(true, true);
     }
 
-    return featuresFile;
+    return featuresAdded;
 
   }
 
index f7837f7..98868ce 100755 (executable)
@@ -27,7 +27,20 @@ import java.util.List;
 
 public class DBRefEntry implements DBRefEntryI
 {
-  String source = "", version = "", accessionId = "";
+  /*
+   * the mapping to chromosome (genome) is held as an instance with
+   * source = speciesId
+   * version = assemblyId
+   * accessionId = "chromosome:" + chromosomeId
+   * map = mapping from sequence to reference assembly
+   */
+  public static final String CHROMOSOME = "chromosome";
+
+  String source = "";
+
+  String version = "";
+
+  String accessionId = "";
 
   /**
    * maps from associated sequence to the database sequence's coordinate system
@@ -331,4 +344,14 @@ public class DBRefEntry implements DBRefEntryI
     }
     return true;
   }
+
+  /**
+   * Mappings to chromosome are held with accessionId as "chromosome:id"
+   * 
+   * @return
+   */
+  public boolean isChromosome()
+  {
+    return accessionId != null && accessionId.startsWith(CHROMOSOME + ":");
+  }
 }
diff --git a/src/jalview/datamodel/GeneLociI.java b/src/jalview/datamodel/GeneLociI.java
new file mode 100644 (file)
index 0000000..f8c7ec5
--- /dev/null
@@ -0,0 +1,38 @@
+package jalview.datamodel;
+
+import jalview.util.MapList;
+
+/**
+ * An interface to model one or more contiguous regions on one chromosome
+ */
+public interface GeneLociI
+{
+  /**
+   * Answers the species identifier
+   * 
+   * @return
+   */
+  String getSpeciesId();
+
+  /**
+   * Answers the reference assembly identifier
+   * 
+   * @return
+   */
+  String getAssemblyId();
+
+  /**
+   * Answers the chromosome identifier e.g. "2", "Y", "II"
+   * 
+   * @return
+   */
+  String getChromosomeId();
+
+  /**
+   * Answers the mapping from sequence to chromosome loci. For a reverse strand
+   * mapping, the chromosomal ranges will have start > end.
+   * 
+   * @return
+   */
+  MapList getMap();
+}
index abf334c..33de452 100755 (executable)
@@ -445,7 +445,7 @@ public class Sequence extends ASequence implements SequenceI
   @Override
   public Vector<PDBEntry> getAllPDBEntries()
   {
-    return pdbIds == null ? new Vector<PDBEntry>() : pdbIds;
+    return pdbIds == null ? new Vector<>() : pdbIds;
   }
 
   /**
@@ -658,10 +658,10 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
-   * DOCUMENT ME!
+   * Sets the sequence description, and also parses out any special formats of
+   * interest
    * 
    * @param desc
-   *          DOCUMENT ME!
    */
   @Override
   public void setDescription(String desc)
@@ -669,10 +669,67 @@ public class Sequence extends ASequence implements SequenceI
     this.description = desc;
   }
 
+  @Override
+  public void setGeneLoci(String speciesId, String assemblyId,
+          String chromosomeId, MapList map)
+  {
+    addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME
+            + ":" + chromosomeId, new Mapping(map)));
+  }
+
   /**
-   * DOCUMENT ME!
+   * Returns the gene loci mapping for the sequence (may be null)
    * 
-   * @return DOCUMENT ME!
+   * @return
+   */
+  @Override
+  public GeneLociI getGeneLoci()
+  {
+    DBRefEntry[] refs = getDBRefs();
+    if (refs != null)
+    {
+      for (final DBRefEntry ref : refs)
+      {
+        if (ref.isChromosome())
+        {
+          return new GeneLociI()
+          {
+            @Override
+            public String getSpeciesId()
+            {
+              return ref.getSource();
+            }
+
+            @Override
+            public String getAssemblyId()
+            {
+              return ref.getVersion();
+            }
+
+            @Override
+            public String getChromosomeId()
+            {
+              // strip off "chromosome:" prefix to chrId
+              return ref.getAccessionId().substring(
+                      DBRefEntry.CHROMOSOME.length() + 1);
+            }
+
+            @Override
+            public MapList getMap()
+            {
+              return ref.getMap().getMap();
+            }
+          };
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Answers the description
+   * 
+   * @return
    */
   @Override
   public String getDescription()
@@ -758,7 +815,7 @@ public class Sequence extends ASequence implements SequenceI
    * @param curs
    * @return
    */
-  protected int findIndex(int pos, SequenceCursor curs)
+  protected int findIndex(final int pos, SequenceCursor curs)
   {
     if (!isValidCursor(curs))
     {
@@ -784,8 +841,13 @@ public class Sequence extends ASequence implements SequenceI
     while (newPos != pos)
     {
       col += delta; // shift one column left or right
-      if (col < 0 || col == sequence.length)
+      if (col < 0)
+      {
+        break;
+      }
+      if (col == sequence.length)
       {
+        col--; // return last column if we failed to reach pos
         break;
       }
       if (!Comparison.isGap(sequence[col]))
@@ -795,7 +857,14 @@ public class Sequence extends ASequence implements SequenceI
     }
 
     col++; // convert back to base 1
-    updateCursor(pos, col, curs.firstColumnPosition);
+
+    /*
+     * only update cursor if we found the target position
+     */
+    if (newPos == pos)
+    {
+      updateCursor(pos, col, curs.firstColumnPosition);
+    }
 
     return col;
   }
index f17bd33..7052f34 100755 (executable)
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.datamodel.features.FeatureLocationI;
+import jalview.datamodel.features.FeatureSourceI;
+import jalview.datamodel.features.FeatureSources;
+import jalview.util.StringUtils;
 
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.Vector;
 
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * A class that models a single contiguous feature on a sequence. If flag
+ * 'contactFeature' is true, the start and end positions are interpreted instead
+ * as two contact points.
  */
 public class SequenceFeature implements FeatureLocationI
 {
@@ -52,6 +58,8 @@ public class SequenceFeature implements FeatureLocationI
   // private key for ENA location designed not to conflict with real GFF data
   private static final String LOCATION = "!Location";
 
+  private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
+
   /*
    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
    * name1=value1;name2=value2,value3;...etc
@@ -85,6 +93,12 @@ public class SequenceFeature implements FeatureLocationI
 
   public Vector<String> links;
 
+  /*
+   * the identifier (if known) for the FeatureSource held in FeatureSources,
+   * as a provider of metadata about feature attributes 
+   */
+  private String source;
+
   /**
    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
    * otherDetails map, so the new and original SequenceFeature may reference the
@@ -156,6 +170,8 @@ public class SequenceFeature implements FeatureLocationI
     this(newType, sf.getDescription(), newBegin, newEnd, newScore,
             newGroup);
 
+    this.source = sf.source;
+
     if (sf.otherDetails != null)
     {
       otherDetails = new HashMap<>();
@@ -367,6 +383,30 @@ public class SequenceFeature implements FeatureLocationI
   }
 
   /**
+   * Answers the value of the specified attribute as string, or null if no such
+   * value. If more than one attribute name is provided, tries to resolve as keys
+   * to nested maps. For example, if attribute "CSQ" holds a map of key-value
+   * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
+   * in that map.
+   * 
+   * @param key
+   * @return
+   */
+  public String getValueAsString(String... key)
+  {
+    if (otherDetails == null)
+    {
+      return null;
+    }
+    Object value = otherDetails.get(key[0]);
+    if (key.length > 1 && value instanceof Map<?, ?>)
+    {
+      value = ((Map) value).get(key[1]);
+    }
+    return value == null ? null : value.toString();
+  }
+
+  /**
    * Returns a property value for the given key if known, else the specified
    * default value
    * 
@@ -399,9 +439,31 @@ public class SequenceFeature implements FeatureLocationI
       }
 
       otherDetails.put(key, value);
+      recordAttribute(key, value);
     }
   }
 
+  /**
+   * Notifies the addition of a feature attribute. This lets us keep track of
+   * which attributes are present on each feature type, and also the range of
+   * numerical-valued attributes.
+   * 
+   * @param key
+   * @param value
+   */
+  protected void recordAttribute(String key, Object value)
+  {
+    String attDesc = null;
+    if (source != null)
+    {
+      attDesc = FeatureSources.getInstance().getSource(source)
+              .getAttributeName(key);
+    }
+
+    FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
+            key);
+  }
+
   /*
    * The following methods are added to maintain the castor Uniprot mapping file
    * for the moment.
@@ -536,6 +598,144 @@ public class SequenceFeature implements FeatureLocationI
   {
     return begin == 0 && end == 0;
   }
+
+  /**
+   * Answers an html-formatted report of feature details
+   * 
+   * @return
+   */
+  public String getDetailsReport()
+  {
+    FeatureSourceI metadata = FeatureSources.getInstance()
+            .getSource(source);
+
+    StringBuilder sb = new StringBuilder(128);
+    sb.append("<br>");
+    sb.append("<table>");
+    sb.append(String.format(ROW_DATA, "Type", type, ""));
+    sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
+            : begin + (isContactFeature() ? ":" : "-") + end, ""));
+    String desc = StringUtils.stripHtmlTags(description);
+    sb.append(String.format(ROW_DATA, "Description", desc, ""));
+    if (!Float.isNaN(score) && score != 0f)
+    {
+      sb.append(String.format(ROW_DATA, "Score", score, ""));
+    }
+    if (featureGroup != null)
+    {
+      sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
+    }
+
+    if (otherDetails != null)
+    {
+      TreeMap<String, Object> ordered = new TreeMap<>(
+              String.CASE_INSENSITIVE_ORDER);
+      ordered.putAll(otherDetails);
+
+      for (Entry<String, Object> entry : ordered.entrySet())
+      {
+        String key = entry.getKey();
+        if (ATTRIBUTES.equals(key))
+        {
+          continue; // to avoid double reporting
+        }
+
+        Object value = entry.getValue();
+        if (value instanceof Map<?, ?>)
+        {
+          /*
+           * expand values in a Map attribute across separate lines
+           * copy to a TreeMap for alphabetical ordering
+           */
+          Map<String, Object> values = (Map<String, Object>) value;
+          SortedMap<String, Object> sm = new TreeMap<>(
+                  String.CASE_INSENSITIVE_ORDER);
+          sm.putAll(values);
+          for (Entry<?, ?> e : sm.entrySet())
+          {
+            sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
+                    .getValue().toString()));
+          }
+        }
+        else
+        {
+          // tried <td title="key"> but it failed to provide a tooltip :-(
+          String attDesc = null;
+          if (metadata != null)
+          {
+            attDesc = metadata.getAttributeName(key);
+          }
+          String s = entry.getValue().toString();
+          if (isValueInteresting(key, s, metadata))
+          {
+            sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
+                    : attDesc, s));
+          }
+        }
+      }
+    }
+    sb.append("</table>");
+
+    String text = sb.toString();
+    return text;
+  }
+
+  /**
+   * Answers true if we judge the value is worth displaying, by some heuristic
+   * rules, else false
+   * 
+   * @param key
+   * @param value
+   * @param metadata
+   * @return
+   */
+  boolean isValueInteresting(String key, String value,
+          FeatureSourceI metadata)
+  {
+    /*
+     * currently suppressing zero values as well as null or empty
+     */
+    if (value == null || "".equals(value) || ".".equals(value)
+            || "0".equals(value))
+    {
+      return false;
+    }
+
+    if (metadata == null)
+    {
+      return true;
+    }
+
+    FeatureAttributeType attType = metadata.getAttributeType(key);
+    if (attType != null
+            && (attType == FeatureAttributeType.Float || attType
+                    .equals(FeatureAttributeType.Integer)))
+    {
+      try
+      {
+        float fval = Float.valueOf(value);
+        if (fval == 0f)
+        {
+          return false;
+        }
+      } catch (NumberFormatException e)
+      {
+        // ignore
+      }
+    }
+
+    return true; // default to interesting
+  }
+
+  /**
+   * Sets the feature source identifier
+   * 
+   * @param theSource
+   */
+  public void setSource(String theSource)
+  {
+    source = theSource;
+  }
 }
 
 class SFSortByEnd implements Comparator<SequenceFeature>
index b22e48f..8dce31e 100755 (executable)
@@ -21,6 +21,7 @@
 package jalview.datamodel;
 
 import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.MapList;
 
 import java.util.BitSet;
 import java.util.Iterator;
@@ -534,6 +535,25 @@ public interface SequenceI extends ASequenceI
   public int replace(char c1, char c2);
 
   /**
+   * Answers the GeneLociI, or null if not known
+   * 
+   * @return
+   */
+  GeneLociI getGeneLoci();
+
+  /**
+   * Sets the mapping to gene loci for the sequence
+   * 
+   * @param speciesId
+   * @param assemblyId
+   * @param chromosomeId
+   * @param map
+   */
+  void setGeneLoci(String speciesId, String assemblyId,
+          String chromosomeId, MapList map);
+
+
+  /**
    * Returns the sequence string constructed from the substrings of a sequence
    * defined by the int[] ranges provided by an iterator. E.g. the iterator
    * could iterate over all visible regions of the alignment
diff --git a/src/jalview/datamodel/features/FeatureAttributeType.java b/src/jalview/datamodel/features/FeatureAttributeType.java
new file mode 100644 (file)
index 0000000..fd3069d
--- /dev/null
@@ -0,0 +1,12 @@
+package jalview.datamodel.features;
+
+/**
+ * A class to model the datatype of feature attributes.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public enum FeatureAttributeType
+{
+  String, Integer, Float, Character, Flag;
+}
diff --git a/src/jalview/datamodel/features/FeatureAttributes.java b/src/jalview/datamodel/features/FeatureAttributes.java
new file mode 100644 (file)
index 0000000..10249f3
--- /dev/null
@@ -0,0 +1,374 @@
+package jalview.datamodel.features;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * A singleton class to hold the set of attributes known for each feature type
+ */
+public class FeatureAttributes
+{
+  public enum Datatype
+  {
+    Character, Number, Mixed
+  }
+
+  private static FeatureAttributes instance = new FeatureAttributes();
+
+  /*
+   * map, by feature type, of a map, by attribute name, of
+   * attribute description and min-max range (if known)
+   */
+  private Map<String, Map<String[], AttributeData>> attributes;
+
+  /*
+   * a case-insensitive comparator so that attributes are ordered e.g.
+   * AC
+   * af
+   * CSQ:AFR_MAF
+   * CSQ:Allele
+   */
+  private Comparator<String[]> comparator = new Comparator<String[]>()
+  {
+    @Override
+    public int compare(String[] o1, String[] o2)
+    {
+      int i = 0;
+      while (i < o1.length || i < o2.length)
+      {
+        if (o2.length <= i)
+        {
+          return o1.length <= i ? 0 : 1;
+        }
+        if (o1.length <= i)
+        {
+          return -1;
+        }
+        int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
+        if (comp != 0)
+        {
+          return comp;
+        }
+        i++;
+      }
+      return 0; // same length and all matched
+    }
+  };
+
+  private class AttributeData
+  {
+    /*
+     * description(s) for this attribute, if known
+     * (different feature source might have differing descriptions)
+     */
+    List<String> description;
+
+    /*
+     * minimum value (of any numeric values recorded)
+     */
+    float min = 0f;
+
+    /*
+     * maximum value (of any numeric values recorded)
+     */
+    float max = 0f;
+
+    /*
+     * flag is set true if any numeric value is detected for this attribute
+     */
+    boolean hasValue = false;
+
+    Datatype type;
+
+    /**
+     * Note one instance of this attribute, recording unique, non-null
+     * descriptions, and the min/max of any numerical values
+     * 
+     * @param desc
+     * @param value
+     */
+    void addInstance(String desc, String value)
+    {
+      addDescription(desc);
+
+      if (value != null)
+      {
+        value = value.trim();
+
+        /*
+         * Parse numeric value unless we have previously
+         * seen text data for this attribute type
+         */
+        if (type == null || type == Datatype.Number)
+        {
+          try
+          {
+            float f = Float.valueOf(value);
+            min = hasValue ? Float.min(min, f) : f;
+            max = hasValue ? Float.max(max, f) : f;
+            hasValue = true;
+            type = (type == null || type == Datatype.Number)
+                    ? Datatype.Number
+                    : Datatype.Mixed;
+          } catch (NumberFormatException e)
+          {
+            /*
+             * non-numeric data: treat attribute as Character (or Mixed)
+             */
+            type = (type == null || type == Datatype.Character)
+                    ? Datatype.Character
+                    : Datatype.Mixed;
+            min = 0f;
+            max = 0f;
+            hasValue = false;
+          }
+        }
+      }
+    }
+
+    /**
+     * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
+     * @return
+     */
+    public String getDescription()
+    {
+      if (description != null && description.size() == 1)
+      {
+        return description.get(0);
+      }
+      return null;
+    }
+
+    public Datatype getType()
+    {
+      return type;
+    }
+
+    /**
+     * Adds the given description to the list of known descriptions (without
+     * duplication)
+     * 
+     * @param desc
+     */
+    public void addDescription(String desc)
+    {
+      if (desc != null)
+      {
+        if (description == null)
+        {
+          description = new ArrayList<>();
+        }
+        if (!description.contains(desc))
+        {
+          description.add(desc);
+        }
+      }
+    }
+  }
+
+  /**
+   * Answers the singleton instance of this class
+   * 
+   * @return
+   */
+  public static FeatureAttributes getInstance()
+  {
+    return instance;
+  }
+
+  private FeatureAttributes()
+  {
+    attributes = new HashMap<>();
+  }
+
+  /**
+   * Answers the attribute names known for the given feature type, in
+   * alphabetical order (not case sensitive), or an empty set if no attributes
+   * are known. An attribute name is typically 'simple' e.g. "AC", but may be
+   * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
+   * 
+   * @param featureType
+   * @return
+   */
+  public List<String[]> getAttributes(String featureType)
+  {
+    if (!attributes.containsKey(featureType))
+    {
+      return Collections.<String[]> emptyList();
+    }
+
+    return new ArrayList<>(attributes.get(featureType).keySet());
+  }
+
+  /**
+   * Answers true if at least one attribute is known for the given feature type,
+   * else false
+   * 
+   * @param featureType
+   * @return
+   */
+  public boolean hasAttributes(String featureType)
+  {
+    if (attributes.containsKey(featureType))
+    {
+      if (!attributes.get(featureType).isEmpty())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Records the given attribute name and description for the given feature
+   * type, and updates the min-max for any numeric value
+   * 
+   * @param featureType
+   * @param description
+   * @param value
+   * @param attName
+   */
+  public void addAttribute(String featureType, String description,
+          Object value, String... attName)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+
+    /*
+     * if attribute value is a map, drill down one more level to
+     * record its sub-fields
+     */
+    if (value instanceof Map<?, ?>)
+    {
+      for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+      {
+        String[] attNames = new String[attName.length + 1];
+        System.arraycopy(attName, 0, attNames, 0, attName.length);
+        attNames[attName.length] = entry.getKey().toString();
+        addAttribute(featureType, description, entry.getValue(), attNames);
+      }
+      return;
+    }
+
+    String valueAsString = value.toString();
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<>(comparator);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
+    {
+      attData = new AttributeData();
+      atts.put(attName, attData);
+    }
+    attData.addInstance(description, valueAsString);
+  }
+
+  /**
+   * Answers the description of the given attribute for the given feature type,
+   * if known and unique, else null
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public String getDescription(String featureType, String... attName)
+  {
+    String desc = null;
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null)
+      {
+        desc = attData.getDescription();
+      }
+    }
+    return desc;
+  }
+
+  /**
+   * Answers the [min, max] value range of the given attribute for the given
+   * feature type, if known, else null. Attributes with a mixture of text and
+   * numeric values are considered text (do not return a min-max range).
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public float[] getMinMax(String featureType, String... attName)
+  {
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null && attData.hasValue)
+      {
+        return new float[] { attData.min, attData.max };
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Records the given attribute description for the given feature type
+   * 
+   * @param featureType
+   * @param attName
+   * @param description
+   */
+  public void addDescription(String featureType, String description,
+          String... attName)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+  
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<>(comparator);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
+    {
+      attData = new AttributeData();
+      atts.put(attName, attData);
+    }
+    attData.addDescription(description);
+  }
+
+  /**
+   * Answers the datatype of the feature, which is one of Character, Number or
+   * Mixed (or null if not known), as discovered from values recorded.
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public Datatype getDatatype(String featureType, String... attName)
+  {
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null)
+      {
+        return attData.getType();
+      }
+    }
+    return null;
+  }
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcher.java b/src/jalview/datamodel/features/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..f844141
--- /dev/null
@@ -0,0 +1,417 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.Matcher;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An immutable class that models one or more match conditions, each of which is
+ * applied to the value obtained by lookup given the match key.
+ * <p>
+ * For example, the value provider could be a SequenceFeature's attributes map,
+ * and the conditions might be
+ * <ul>
+ * <li>CSQ contains "pathological"</li>
+ * <li>AND</li>
+ * <li>AF <= 1.0e-5</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureMatcher implements FeatureMatcherI
+{
+  private static final String SCORE = "Score";
+
+  private static final String LABEL = "Label";
+
+  private static final String SPACE = " ";
+
+  private static final String QUOTE = "'";
+
+  /*
+   * a dummy matcher that comes in useful for the 'add a filter' gui row
+   */
+  public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
+          .byLabel(Condition.values()[0], "");
+
+  private static final String COLON = ":";
+
+  /*
+   * if true, match is against feature description
+   */
+  final private boolean byLabel;
+
+  /*
+   * if true, match is against feature score
+   */
+  final private boolean byScore;
+
+  /*
+   * if not null, match is against feature attribute [sub-attribute]
+   */
+  final private String[] key;
+
+  final private MatcherI matcher;
+
+  /**
+   * A helper method that converts a 'compound' attribute name from its display
+   * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
+   * 
+   * @param attribute
+   * @return
+   */
+  public static String[] fromAttributeDisplayName(String attribute)
+  {
+    return attribute == null ? null : attribute.split(COLON);
+  }
+
+  /**
+   * A helper method that converts a 'compound' attribute name to its display
+   * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
+   * 
+   * @param attName
+   * @return
+   */
+  public static String toAttributeDisplayName(String[] attName)
+  {
+    return attName == null ? "" : String.join(COLON, attName);
+  }
+
+  /**
+   * A factory constructor that converts a stringified object (as output by
+   * toStableString) to an object instance. Returns null if parsing fails.
+   * <p>
+   * Leniency in parsing (for manually created feature files):
+   * <ul>
+   * <li>keywords Score and Label, and the condition, are not
+   * case-sensitive</li>
+   * <li>quotes around value and pattern are optional if string does not include
+   * a space</li>
+   * </ul>
+   * 
+   * @param descriptor
+   * @return
+   */
+  public static FeatureMatcher fromString(final String descriptor)
+  {
+    String invalidFormat = "Invalid matcher format: " + descriptor;
+
+    /*
+     * expect 
+     * value condition pattern
+     * where value is Label or Space or attributeName or attName1:attName2
+     * and pattern is a float value as string, or a text string
+     * attribute names or patterns may be quoted (must be if include space)
+     */
+    String attName = null;
+    boolean byScore = false;
+    boolean byLabel = false;
+    Condition cond = null;
+    String pattern = null;
+
+    /*
+     * parse first field (Label / Score / attribute)
+     * optionally in quotes (required if attName includes space)
+     */
+    String leftToParse = descriptor;
+    String firstField = null;
+
+    if (descriptor.startsWith(QUOTE))
+    {
+      // 'Label' / 'Score' / 'attName'
+      int nextQuotePos = descriptor.indexOf(QUOTE, 1);
+      if (nextQuotePos == -1)
+      {
+        System.err.println(invalidFormat);
+        return null;
+      }
+      firstField = descriptor.substring(1, nextQuotePos);
+      leftToParse = descriptor.substring(nextQuotePos + 1).trim();
+    }
+    else
+    {
+      // Label / Score / attName (unquoted)
+      int nextSpacePos = descriptor.indexOf(SPACE);
+      if (nextSpacePos == -1)
+      {
+        System.err.println(invalidFormat);
+        return null;
+      }
+      firstField = descriptor.substring(0, nextSpacePos);
+      leftToParse = descriptor.substring(nextSpacePos + 1).trim();
+    }
+    String lower = firstField.toLowerCase();
+    if (lower.startsWith(LABEL.toLowerCase()))
+    {
+      byLabel = true;
+    }
+    else if (lower.startsWith(SCORE.toLowerCase()))
+    {
+      byScore = true;
+    }
+    else
+    {
+      attName = firstField;
+    }
+
+    /*
+     * next field is the comparison condition
+     * most conditions require a following pattern (optionally quoted)
+     * although some conditions e.g. Present do not
+     */
+    int nextSpacePos = leftToParse.indexOf(SPACE);
+    if (nextSpacePos == -1)
+    {
+      /*
+       * no value following condition - only valid for some conditions
+       */
+      cond = Condition.fromString(leftToParse);
+      if (cond == null || cond.needsAPattern())
+      {
+        System.err.println(invalidFormat);
+        return null;
+      }
+    }
+    else
+    {
+      /*
+       * condition and pattern
+       */
+      cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
+      leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
+      if (leftToParse.startsWith(QUOTE))
+      {
+        // pattern in quotes
+        if (leftToParse.endsWith(QUOTE))
+        {
+          pattern = leftToParse.substring(1, leftToParse.length() - 1);
+        }
+        else
+        {
+          // unbalanced quote
+          System.err.println(invalidFormat);
+          return null;
+        }
+      }
+      else
+      {
+        // unquoted pattern
+        pattern = leftToParse;
+      }
+    }
+
+    /*
+     * we have parsed out value, condition and pattern
+     * so can now make the FeatureMatcher
+     */
+    try
+    {
+      if (byLabel)
+      {
+        return FeatureMatcher.byLabel(cond, pattern);
+      }
+      else if (byScore)
+      {
+        return FeatureMatcher.byScore(cond, pattern);
+      }
+      else
+      {
+        String[] attNames = FeatureMatcher
+                .fromAttributeDisplayName(attName);
+        return FeatureMatcher.byAttribute(cond, pattern, attNames);
+      }
+    } catch (NumberFormatException e)
+    {
+      // numeric condition with non-numeric pattern
+      return null;
+    }
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the feature label (description)
+   * 
+   * @param cond
+   * @param pattern
+   * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
+   */
+  public static FeatureMatcher byLabel(Condition cond, String pattern)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), true, false,
+            null);
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the feature score
+   * 
+   * @param cond
+   * @param pattern
+   * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
+   */
+  public static FeatureMatcher byScore(Condition cond, String pattern)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), false, true,
+            null);
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the named feature attribute [and optional sub-attribute]
+   * 
+   * @param cond
+   * @param pattern
+   * @param attName
+   * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
+   */
+  public static FeatureMatcher byAttribute(Condition cond, String pattern,
+          String... attName)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), false, false,
+            attName);
+  }
+
+  private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
+          String[] theKey)
+  {
+    key = theKey;
+    matcher = m;
+    byLabel = forLabel;
+    byScore = forScore;
+  }
+  @Override
+  public boolean matches(SequenceFeature feature)
+  {
+    String value = byLabel ? feature.getDescription()
+            : (byScore ? String.valueOf(feature.getScore())
+                    : feature.getValueAsString(key));
+    return matcher.matches(value);
+  }
+
+  @Override
+  public String[] getAttribute()
+  {
+    return key;
+  }
+
+  @Override
+  public MatcherI getMatcher()
+  {
+    return matcher;
+  }
+
+  /**
+   * Answers a string description of this matcher, suitable for display, debugging
+   * or logging. The format may change in future.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    if (byLabel)
+    {
+      sb.append(MessageManager.getString("label.label"));
+    }
+    else if (byScore)
+    {
+      sb.append(MessageManager.getString("label.score"));
+    }
+    else
+    {
+      sb.append(String.join(COLON, key));
+    }
+
+    Condition condition = matcher.getCondition();
+    sb.append(SPACE).append(condition.toString().toLowerCase());
+    if (condition.isNumeric())
+    {
+      sb.append(SPACE).append(matcher.getPattern());
+    }
+    else if (condition.needsAPattern())
+    {
+      sb.append(" '").append(matcher.getPattern()).append(QUOTE);
+    }
+
+    return sb.toString();
+  }
+
+  @Override
+  public boolean isByLabel()
+  {
+    return byLabel;
+  }
+
+  @Override
+  public boolean isByScore()
+  {
+    return byScore;
+  }
+
+  @Override
+  public boolean isByAttribute()
+  {
+    return getAttribute() != null;
+  }
+
+  /**
+   * {@inheritDoc} The output of this method should be parseable by method
+   * <code>fromString<code> to restore the original object.
+   */
+  @Override
+  public String toStableString()
+  {
+    StringBuilder sb = new StringBuilder();
+    if (byLabel)
+    {
+      sb.append(LABEL); // no i18n here unlike toString() !
+    }
+    else if (byScore)
+    {
+      sb.append(SCORE);
+    }
+    else
+    {
+      /*
+       * enclose attribute name in quotes if it includes space
+       */
+      String displayName = toAttributeDisplayName(key);
+      if (displayName.contains(SPACE))
+      {
+        sb.append(QUOTE).append(displayName).append(QUOTE);
+      }
+      else
+      {
+        sb.append(displayName);
+      }
+    }
+  
+    Condition condition = matcher.getCondition();
+    sb.append(SPACE).append(condition.getStableName());
+    String pattern = matcher.getPattern();
+    if (condition.needsAPattern())
+    {
+      /*
+       * enclose pattern in quotes if it includes space
+       */
+      if (pattern.contains(SPACE))
+      {
+        sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
+      }
+      else
+      {
+        sb.append(SPACE).append(pattern);
+      }
+    }
+  
+    return sb.toString();
+  }
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherI.java b/src/jalview/datamodel/features/FeatureMatcherI.java
new file mode 100644 (file)
index 0000000..f1f8585
--- /dev/null
@@ -0,0 +1,65 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An interface for an object that can apply a match condition to a
+ * SequenceFeature object
+ * 
+ * @author gmcarstairs
+ */
+public interface FeatureMatcherI
+{
+  /**
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
+   * 
+   * @param feature
+   * @return
+   */
+  boolean matches(SequenceFeature feature);
+
+  /**
+   * Answers the attribute key this matcher operates on (or null if match is by
+   * Label or Score)
+   * 
+   * @return
+   */
+  String[] getAttribute();
+
+  /**
+   * Answers true if match is against feature label (description), else false
+   * 
+   * @return
+   */
+  boolean isByLabel();
+
+  /**
+   * Answers true if match is against feature score, else false
+   * 
+   * @return
+   */
+  boolean isByScore();
+
+  /**
+   * Answers true if match is against a feature attribute (text or range)
+   * 
+   * @return
+   */
+  boolean isByAttribute();
+
+  /**
+   * Answers the match condition that is applied
+   * 
+   * @return
+   */
+  MatcherI getMatcher();
+
+  /**
+   * Answers a string representation of this object suitable for use when
+   * persisting data, in a format that can be reliably read back. Any changes to
+   * the format should be backwards compatible.
+   */
+  String toStableString();
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherSet.java b/src/jalview/datamodel/features/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..b51f2f0
--- /dev/null
@@ -0,0 +1,294 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that models one or more match conditions, which may be combined with
+ * AND or OR (but not a mixture)
+ * 
+ * @author gmcarstairs
+ */
+public class FeatureMatcherSet implements FeatureMatcherSetI
+{
+  private static final String OR = "OR";
+
+  private static final String AND = "AND";
+
+  private static final String SPACE = " ";
+
+  private static final String CLOSE_BRACKET = ")";
+
+  private static final String OPEN_BRACKET = "(";
+
+  private static final String OR_I18N = MessageManager
+          .getString("label.or");
+
+  private static final String AND_18N = MessageManager
+          .getString("label.and");
+
+  List<FeatureMatcherI> matchConditions;
+
+  boolean andConditions;
+
+  /**
+   * A factory constructor that converts a stringified object (as output by
+   * toStableString) to an object instance.
+   * 
+   * Format:
+   * <ul>
+   * <li>(condition1) AND (condition2) AND (condition3)</li>
+   * <li>or</li>
+   * <li>(condition1) OR (condition2) OR (condition3)</li>
+   * </ul>
+   * where OR and AND are not case-sensitive, and may not be mixed. Brackets are
+   * optional if there is only one condition.
+   * 
+   * @param descriptor
+   * @return
+   * @see FeatureMatcher#fromString(String)
+   */
+  public static FeatureMatcherSet fromString(final String descriptor)
+  {
+    String invalid = "Invalid descriptor: " + descriptor;
+    boolean firstCondition = true;
+    FeatureMatcherSet result = new FeatureMatcherSet();
+
+    String leftToParse = descriptor.trim();
+
+    while (leftToParse.length() > 0)
+    {
+      /*
+       * inspect AND or OR condition, check not mixed
+       */
+      boolean and = true;
+      if (!firstCondition)
+      {
+        int spacePos = leftToParse.indexOf(SPACE);
+        if (spacePos == -1)
+        {
+          // trailing junk after a match condition
+          System.err.println(invalid);
+          return null;
+        }
+        String conjunction = leftToParse.substring(0, spacePos);
+        leftToParse = leftToParse.substring(spacePos + 1).trim();
+        if (conjunction.equalsIgnoreCase(AND))
+        {
+          and = true;
+        }
+        else if (conjunction.equalsIgnoreCase(OR))
+        {
+          and = false;
+        }
+        else
+        {
+          // not an AND or an OR - invalid
+          System.err.println(invalid);
+          return null;
+        }
+      }
+
+      /*
+       * now extract the next condition and AND or OR it
+       */
+      String nextCondition = leftToParse;
+      if (leftToParse.startsWith(OPEN_BRACKET))
+      {
+        int closePos = leftToParse.indexOf(CLOSE_BRACKET);
+        if (closePos == -1)
+        {
+          System.err.println(invalid);
+          return null;
+        }
+        nextCondition = leftToParse.substring(1, closePos);
+        leftToParse = leftToParse.substring(closePos + 1).trim();
+      }
+      else
+      {
+        leftToParse = "";
+      }
+
+      FeatureMatcher fm = FeatureMatcher.fromString(nextCondition);
+      if (fm == null)
+      {
+        System.err.println(invalid);
+        return null;
+      }
+      try
+      {
+        if (and)
+        {
+          result.and(fm);
+        }
+        else
+        {
+          result.or(fm);
+        }
+        firstCondition = false;
+      } catch (IllegalStateException e)
+      {
+        // thrown if OR and AND are mixed
+        System.err.println(invalid);
+        return null;
+      }
+
+    }
+    return result;
+  }
+
+  /**
+   * Constructor
+   */
+  public FeatureMatcherSet()
+  {
+    matchConditions = new ArrayList<>();
+  }
+
+  @Override
+  public boolean matches(SequenceFeature feature)
+  {
+    /*
+     * no conditions matches anything
+     */
+    if (matchConditions.isEmpty())
+    {
+      return true;
+    }
+
+    /*
+     * AND until failure
+     */
+    if (andConditions)
+    {
+      for (FeatureMatcherI m : matchConditions)
+      {
+        if (!m.matches(feature))
+        {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    /*
+     * OR until match
+     */
+    for (FeatureMatcherI m : matchConditions)
+    {
+      if (m.matches(feature))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void and(FeatureMatcherI m)
+  {
+    if (!andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an AND to OR conditions");
+    }
+    matchConditions.add(m);
+    andConditions = true;
+  }
+
+  @Override
+  public void or(FeatureMatcherI m)
+  {
+    if (andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an OR to AND conditions");
+    }
+    matchConditions.add(m);
+    andConditions = false;
+  }
+
+  @Override
+  public boolean isAnded()
+  {
+    return andConditions;
+  }
+
+  @Override
+  public Iterable<FeatureMatcherI> getMatchers()
+  {
+    return matchConditions;
+  }
+
+  /**
+   * Answers a string representation of this object suitable for display, and
+   * possibly internationalized. The format is not guaranteed stable and may
+   * change in future.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    boolean multiple = matchConditions.size() > 1;
+    for (FeatureMatcherI matcher : matchConditions)
+    {
+      if (!first)
+      {
+        String joiner = andConditions ? AND_18N : OR_I18N;
+        sb.append(SPACE).append(joiner.toLowerCase()).append(SPACE);
+      }
+      first = false;
+      if (multiple)
+      {
+        sb.append(OPEN_BRACKET).append(matcher.toString())
+                .append(CLOSE_BRACKET);
+      }
+      else
+      {
+        sb.append(matcher.toString());
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public boolean isEmpty()
+  {
+    return matchConditions == null || matchConditions.isEmpty();
+  }
+
+  /**
+   * {@inheritDoc} The output of this method should be parseable by method
+   * <code>fromString<code> to restore the original object.
+   */
+  @Override
+  public String toStableString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean moreThanOne = matchConditions.size() > 1;
+    boolean first = true;
+
+    for (FeatureMatcherI matcher : matchConditions)
+    {
+      if (!first)
+      {
+        String joiner = andConditions ? AND : OR;
+        sb.append(SPACE).append(joiner).append(SPACE);
+      }
+      first = false;
+      if (moreThanOne)
+      {
+        sb.append(OPEN_BRACKET).append(matcher.toStableString())
+                .append(CLOSE_BRACKET);
+      }
+      else
+      {
+        sb.append(matcher.toStableString());
+      }
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherSetI.java b/src/jalview/datamodel/features/FeatureMatcherSetI.java
new file mode 100644 (file)
index 0000000..90c2986
--- /dev/null
@@ -0,0 +1,68 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+/**
+ * An interface to describe a set of one or more feature matchers, where all
+ * matchers are combined with either AND or OR
+ * 
+ * @author gmcarstairs
+ *
+ */
+public interface FeatureMatcherSetI
+{
+  /**
+   * Answers true if the feature provided passes this matcher's match condition
+   * 
+   * @param feature
+   * @return
+   */
+  boolean matches(SequenceFeature feature);
+
+  /**
+   * Adds (ANDs) match condition m to this object's matcher set
+   * 
+   * @param m
+   * @throws IllegalStateException
+   *           if an attempt is made to AND to existing OR-ed conditions
+   */
+  void and(FeatureMatcherI m);
+
+  /**
+   * Answers true if any second condition is AND-ed with this one, false if it
+   * is OR-ed
+   * 
+   * @return
+   */
+  boolean isAnded();
+
+  /**
+   * Adds (ORs) the given condition to this object's match conditions
+   * 
+   * @param m
+   * @throws IllegalStateException
+   *           if an attempt is made to OR to existing AND-ed conditions
+   */
+  void or(FeatureMatcherI m);
+
+  /**
+   * Answers an iterator over the combined match conditions
+   * 
+   * @return
+   */
+  Iterable<FeatureMatcherI> getMatchers();
+
+  /**
+   * Answers true if this object contains no conditions
+   * 
+   * @return
+   */
+  boolean isEmpty();
+
+  /**
+   * Answers a string representation of this object suitable for use when
+   * persisting data, in a format that can be reliably read back. Any changes to
+   * the format should be backwards compatible.
+   */
+  String toStableString();
+}
diff --git a/src/jalview/datamodel/features/FeatureSource.java b/src/jalview/datamodel/features/FeatureSource.java
new file mode 100644 (file)
index 0000000..a1be1dc
--- /dev/null
@@ -0,0 +1,78 @@
+package jalview.datamodel.features;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class to model one source of feature data, including metadata about
+ * attributes of features
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureSource implements FeatureSourceI
+{
+  private String name;
+
+  private Map<String, String> attributeNames;
+  
+  private Map<String, FeatureAttributeType> attributeTypes;
+  
+  /**
+   * Constructor
+   * 
+   * @param theName
+   */
+  public FeatureSource(String theName)
+  {
+    this.name = theName;
+    attributeNames = new HashMap<>();
+    attributeTypes = new HashMap<>();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getName()
+  {
+    return name;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getAttributeName(String attributeId)
+  {
+    return attributeNames.get(attributeId);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public FeatureAttributeType getAttributeType(String attributeId)
+  {
+    return attributeTypes.get(attributeId);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setAttributeName(String id, String attName)
+  {
+    attributeNames.put(id, attName);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setAttributeType(String id, FeatureAttributeType type)
+  {
+    attributeTypes.put(id, type);
+  }
+
+}
diff --git a/src/jalview/datamodel/features/FeatureSourceI.java b/src/jalview/datamodel/features/FeatureSourceI.java
new file mode 100644 (file)
index 0000000..c873593
--- /dev/null
@@ -0,0 +1,45 @@
+package jalview.datamodel.features;
+
+public interface FeatureSourceI
+{
+  /**
+   * Answers a name for the feature source (not necessarily unique)
+   * 
+   * @return
+   */
+  String getName();
+
+  /**
+   * Answers the 'long name' of an attribute given its id (short name or
+   * abbreviation), or null if not known
+   * 
+   * @param attributeId
+   * @return
+   */
+  String getAttributeName(String attributeId);
+
+  /**
+   * Sets the 'long name' of an attribute given its id (short name or
+   * abbreviation).
+   * 
+   * @param id
+   * @param name
+   */
+  void setAttributeName(String id, String name);
+
+  /**
+   * Answers the datatype of the attribute with given id, or null if not known
+   * 
+   * @param attributeId
+   * @return
+   */
+  FeatureAttributeType getAttributeType(String attributeId);
+
+  /**
+   * Sets the datatype of the attribute with given id
+   * 
+   * @param id
+   * @param type
+   */
+  void setAttributeType(String id, FeatureAttributeType type);
+}
diff --git a/src/jalview/datamodel/features/FeatureSources.java b/src/jalview/datamodel/features/FeatureSources.java
new file mode 100644 (file)
index 0000000..1be1b82
--- /dev/null
@@ -0,0 +1,58 @@
+package jalview.datamodel.features;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A singleton to hold metadata about feature attributes, keyed by a unique
+ * feature source identifier
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureSources
+{
+  private static FeatureSources instance = new FeatureSources();
+
+  private Map<String, FeatureSourceI> sources;
+
+  /**
+   * Answers the singleton instance of this class
+   * 
+   * @return
+   */
+  public static FeatureSources getInstance()
+  {
+    return instance;
+  }
+
+  private FeatureSources()
+  {
+    sources = new HashMap<>();
+  }
+
+  /**
+   * Answers the FeatureSource with the given unique identifier, or null if not
+   * known
+   * 
+   * @param sourceId
+   * @return
+   */
+  public FeatureSourceI getSource(String sourceId)
+  {
+    return sources.get(sourceId);
+  }
+
+  /**
+   * Adds the given source under the given key. This will replace any existing
+   * source with the same id, it is the caller's responsibility to ensure keys
+   * are unique if necessary.
+   * 
+   * @param sourceId
+   * @param source
+   */
+  public void addSource(String sourceId, FeatureSource source)
+  {
+    sources.put(sourceId, source);
+  }
+}
index fcf1b53..727d3ef 100644 (file)
@@ -87,7 +87,7 @@ public class SequenceFeatures implements SequenceFeaturesI
      */
     // featureStore = Collections
     // .synchronizedSortedMap(new TreeMap<String, FeatureStore>());
-    featureStore = new TreeMap<String, FeatureStore>();
+    featureStore = new TreeMap<>();
   }
 
   /**
@@ -382,9 +382,10 @@ public class SequenceFeatures implements SequenceFeaturesI
   }
 
   /**
-   * 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.
+   * Answers true if the given type matches one of the specified terms (or is a
+   * sub-type of one in the Sequence Ontology), 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
@@ -399,7 +400,7 @@ public class SequenceFeatures implements SequenceFeaturesI
     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
     for (String term : soTerm)
     {
-      if (so.isA(type, term))
+      if (type.equals(term) || so.isA(type, term))
       {
         return true;
       }
index 80c4f9a..31712b9 100644 (file)
@@ -82,9 +82,9 @@ public interface SequenceFeaturesI
           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.
+   * Answers a list of all features stored, whose type either matches, or is a
+   * specialisation (in the Sequence Ontology) of, one of the given terms.
+   * Results are returned in no particular order.
    * 
    * @param ontologyTerm
    * @return
index 952f01e..7384327 100644 (file)
 package jalview.ext.ensembl;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import com.stevesoft.pat.Regex;
 
 /**
@@ -109,23 +113,27 @@ public class EnsemblCdna extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true if the sequence feature type is 'exon' (or a subtype of exon
-   * in the Sequence Ontology), and the Parent of the feature is the transcript
-   * we are retrieving
+   * Answers a list of sequence features (if any) whose type is 'exon' (or a
+   * subtype of exon in the Sequence Ontology), and whose Parent is the
+   * transcript we are retrieving
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
-            SequenceOntologyI.EXON))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.EXON);
+    for (SequenceFeature sf : sfs)
     {
       String parentFeature = (String) sf.getValue(PARENT);
       if (("transcript:" + accId).equals(parentFeature))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+
+    return result;
   }
 
   /**
index 8b2550d..8a71b64 100644 (file)
@@ -102,23 +102,26 @@ public class EnsemblCds extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true if the sequence feature type is 'CDS' (or a subtype of CDS in
-   * the Sequence Ontology), and the Parent of the feature is the transcript we
-   * are retrieving
+   * Answers a list of sequence features (if any) whose type is 'CDS' (or a
+   * subtype of CDS in the Sequence Ontology), and whose Parent is the
+   * transcript we are retrieving
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
-            SequenceOntologyI.CDS))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.CDS);
+    for (SequenceFeature sf : sfs)
     {
       String parentFeature = (String) sf.getValue(PARENT);
       if (("transcript:" + accId).equals(parentFeature))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+    return result;
   }
 
   /**
@@ -130,7 +133,7 @@ public class EnsemblCds extends EnsemblSeqProxy
   protected List<int[]> getCdsRanges(SequenceI dnaSeq)
   {
     int len = dnaSeq.getLength();
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     ranges.add(new int[] { 1, len });
     return ranges;
   }
diff --git a/src/jalview/ext/ensembl/EnsemblData.java b/src/jalview/ext/ensembl/EnsemblData.java
new file mode 100644 (file)
index 0000000..47fe0fc
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * 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.ext.ensembl;
+
+/**
+ * A data class to model the data and rest version of one Ensembl domain,
+ * currently for rest.ensembl.org and rest.ensemblgenomes.org
+ * 
+ * @author gmcarstairs
+ */
+class EnsemblData
+{
+  /*
+   * The http domain this object is holding data values for
+   */
+  String domain;
+
+  /*
+   * The latest version Jalview has tested for, e.g. "4.5"; a minor version change should be
+   * ok, a major version change may break stuff 
+   */
+  String expectedRestVersion;
+
+  /*
+   * Major / minor / point version e.g. "4.5.1"
+   * @see http://rest.ensembl.org/info/rest/?content-type=application/json
+   */
+  String restVersion;
+
+  /*
+   * data version
+   * @see http://rest.ensembl.org/info/data/?content-type=application/json
+   */
+  String dataVersion;
+
+  /*
+   * true when http://rest.ensembl.org/info/ping/?content-type=application/json
+   * returns response code 200 and not {"error":"Database is unavailable"}
+   */
+  boolean restAvailable;
+
+  /*
+   * absolute time when availability was last checked
+   */
+  long lastAvailableCheckTime;
+
+  /*
+   * absolute time when version numbers were last checked
+   */
+  long lastVersionCheckTime;
+
+  // flag set to true if REST major version is not the one expected
+  boolean restMajorVersionMismatch;
+
+  /*
+   * absolute time to wait till if we overloaded the REST service
+   */
+  long retryAfter;
+
+  /**
+   * Constructor given expected REST version number e.g 4.5 or 3.4.3
+   * 
+   * @param restExpected
+   */
+  EnsemblData(String theDomain, String restExpected)
+  {
+    domain = theDomain;
+    expectedRestVersion = restExpected;
+    lastAvailableCheckTime = -1;
+    lastVersionCheckTime = -1;
+  }
+
+}
index 0d5fc26..36b19e2 100644 (file)
@@ -23,6 +23,8 @@ package jalview.ext.ensembl;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsModelI;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -150,10 +152,14 @@ public class EnsemblGene extends EnsemblSeqProxy
       {
         continue;
       }
+      
       if (geneAlignment.getHeight() == 1)
       {
         // ensure id has 'correct' case for the Ensembl identifier
         geneId = geneAlignment.getSequenceAt(0).getName();
+
+        findGeneLoci(geneAlignment.getSequenceAt(0), geneId);
+
         getTranscripts(geneAlignment, geneId);
       }
       if (al == null)
@@ -169,6 +175,67 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
+   * Calls the /lookup/id REST service, parses the response for gene
+   * coordinates, and if successful, adds these to the sequence. If this fails,
+   * fall back on trying to parse the sequence description in case it is in
+   * Ensembl-gene format e.g. chromosome:GRCh38:17:45051610:45109016:1.
+   * 
+   * @param seq
+   * @param geneId
+   */
+  void findGeneLoci(SequenceI seq, String geneId)
+  {
+    GeneLociI geneLoci = new EnsemblLookup(getDomain()).getGeneLoci(geneId);
+    if (geneLoci != null)
+    {
+      seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+              geneLoci.getChromosomeId(), geneLoci.getMap());
+    }
+    else
+    {
+      parseChromosomeLocations(seq);
+    }
+  }
+
+  /**
+   * Parses and saves fields of an Ensembl-style description e.g.
+   * chromosome:GRCh38:17:45051610:45109016:1
+   * 
+   * @param seq
+   */
+  boolean parseChromosomeLocations(SequenceI seq)
+  {
+    String description = seq.getDescription();
+    if (description == null)
+    {
+      return false;
+    }
+    String[] tokens = description.split(":");
+    if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME))
+    {
+      String ref = tokens[1];
+      String chrom = tokens[2];
+      try
+      {
+        int chStart = Integer.parseInt(tokens[3]);
+        int chEnd = Integer.parseInt(tokens[4]);
+        boolean forwardStrand = "1".equals(tokens[5]);
+        String species = ""; // not known here
+        int[] from = new int[] { seq.getStart(), seq.getEnd() };
+        int[] to = new int[] { forwardStrand ? chStart : chEnd,
+            forwardStrand ? chEnd : chStart };
+        MapList map = new MapList(from, to, 1, 1);
+        seq.setGeneLoci(species, ref, chrom, map);
+        return true;
+      } catch (NumberFormatException e)
+      {
+        System.err.println("Bad integers in description " + description);
+      }
+    }
+    return false;
+  }
+
+  /**
    * Converts a query, which may contain one or more gene, transcript, or
    * external (to Ensembl) identifiers, into a non-redundant list of gene
    * identifiers.
@@ -362,6 +429,8 @@ public class EnsemblGene extends EnsemblSeqProxy
     cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(),
             transcript.getDatasetSequence(), mapping, parentId);
 
+    mapTranscriptToChromosome(transcript, gene, mapping);
+
     /*
      * fetch and save cross-references
      */
@@ -376,6 +445,42 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
+   * If the gene has a mapping to chromosome coordinates, derive the transcript
+   * chromosome regions and save on the transcript sequence
+   * 
+   * @param transcript
+   * @param gene
+   * @param mapping
+   *          the mapping from gene to transcript positions
+   */
+  protected void mapTranscriptToChromosome(SequenceI transcript,
+          SequenceI gene, MapList mapping)
+  {
+    GeneLociI loci = gene.getGeneLoci();
+    if (loci == null)
+    {
+      return;
+    }
+
+    MapList geneMapping = loci.getMap();
+
+    List<int[]> exons = mapping.getFromRanges();
+    List<int[]> transcriptLoci = new ArrayList<>();
+
+    for (int[] exon : exons)
+    {
+      transcriptLoci.add(geneMapping.locateInTo(exon[0], exon[1]));
+    }
+
+    List<int[]> transcriptRange = Arrays.asList(new int[] {
+        transcript.getStart(), transcript.getEnd() });
+    MapList mapList = new MapList(transcriptRange, transcriptLoci, 1, 1);
+
+    transcript.setGeneLoci(loci.getSpeciesId(), loci.getAssemblyId(),
+            loci.getChromosomeId(), mapList);
+  }
+
+  /**
    * Returns the 'transcript_id' property of the sequence feature (or null)
    * 
    * @param feature
@@ -443,23 +548,27 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true for a feature of type 'gene' (or a sub-type of gene in the
-   * Sequence Ontology), whose ID is the accession we are retrieving
+   * Answers a list of sequence features (if any) whose type is 'gene' (or a
+   * subtype of gene in the Sequence Ontology), and whose ID is the accession we
+   * are retrieving
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
-            SequenceOntologyI.GENE))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.GENE);
+    for (SequenceFeature sf : sfs)
     {
       // NB features as gff use 'ID'; rest services return as 'id'
       String id = (String) sf.getValue("ID");
       if ((GENE_PREFIX + accId).equalsIgnoreCase(id))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+    return result;
   }
 
   /**
@@ -490,17 +599,6 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
-   * Answers false. This allows an optimisation - a single 'gene' feature is all
-   * that is needed to identify the positions of the gene on the genomic
-   * sequence.
-   */
-  @Override
-  protected boolean isSpliceable()
-  {
-    return false;
-  }
-
-  /**
    * Override to do nothing as Ensembl doesn't return a protein sequence for a
    * gene identifier
    */
index bde3c0f..6684e20 100644 (file)
 package jalview.ext.ensembl;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.io.gff.SequenceOntologyI;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A client to fetch genomic sequence from Ensembl
@@ -94,22 +99,32 @@ public class EnsemblGenome extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true if the sequence feature type is 'transcript' (or a subtype of
-   * transcript in the Sequence Ontology), and the ID of the feature is the
-   * transcript we are retrieving
+   * Answers a list of sequence features (if any) whose type is 'transcript' (or
+   * a subtype of transcript in the Sequence Ontology), and whose ID is the
+   * accession we are retrieving.
+   * <p>
+   * Note we also include features of type "NMD_transcript_variant", although
+   * not strictly 'transcript' in the SO, as they used in Ensembl as if they
+   * were.
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (isTranscript(sf.getType()))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures().getFeaturesByOntology(
+            SequenceOntologyI.TRANSCRIPT,
+            SequenceOntologyI.NMD_TRANSCRIPT_VARIANT);
+    for (SequenceFeature sf : sfs)
     {
+      // NB features as gff use 'ID'; rest services return as 'id'
       String id = (String) sf.getValue("ID");
       if (("transcript:" + accId).equals(id))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+    return result;
   }
 
 }
index 7668941..fa24f1e 100644 (file)
-/*
- * 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.ext.ensembl;
 
-/**
- * A data class to model the data and rest version of one Ensembl domain,
- * currently for rest.ensembl.org and rest.ensemblgenomes.org
- * 
- * @author gmcarstairs
- */
-class EnsemblInfo
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefSource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.json.simple.JSONArray;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public class EnsemblInfo extends EnsemblRestClient
 {
-  /*
-   * The http domain this object is holding data values for
-   */
-  String domain;
 
   /*
-   * The latest version Jalview has tested for, e.g. "4.5"; a minor version change should be
-   * ok, a major version change may break stuff 
+   * cached results of REST /info/divisions service, currently
+   * <pre>
+   * { 
+   *  { "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBL", "http://rest.ensembl.org" }
+   *  }
+   * </pre>
+   * The values for EnsemblGenomes are retrieved by a REST call, that for
+   * Ensembl is added programmatically for convenience of lookup
    */
-  String expectedRestVersion;
+  private static Map<String, String> divisions;
 
-  /*
-   * Major / minor / point version e.g. "4.5.1"
-   * @see http://rest.ensembl.org/info/rest/?content-type=application/json
-   */
-  String restVersion;
+  @Override
+  public String getDbName()
+  {
+    return "ENSEMBL";
+  }
 
-  /*
-   * data version
-   * @see http://rest.ensembl.org/info/data/?content-type=application/json
-   */
-  String dataVersion;
+  @Override
+  public AlignmentI getSequenceRecords(String queries) throws Exception
+  {
+    return null;
+  }
 
-  /*
-   * true when http://rest.ensembl.org/info/ping/?content-type=application/json
-   * returns response code 200 and not {"error":"Database is unavailable"}
+  @Override
+  protected URL getUrl(List<String> ids) throws MalformedURLException
+  {
+    return null;
+  }
+
+  @Override
+  protected boolean useGetRequest()
+  {
+    return true;
+  }
+
+  /**
+   * Answers the domain (http://rest.ensembl.org or
+   * http://rest.ensemblgenomes.org) for the given division, or null if not
+   * recognised by Ensembl.
+   * 
+   * @param division
+   * @return
    */
-  boolean restAvailable;
+  public String getDomain(String division)
+  {
+    if (divisions == null)
+    {
+      fetchDivisions();
+    }
+    return divisions.get(division.toUpperCase());
+  }
 
-  /*
-   * absolute time when availability was last checked
+  /**
+   * On first request only, populate the lookup map by fetching the list of
+   * divisions known to EnsemblGenomes.
    */
-  long lastAvailableCheckTime;
+  void fetchDivisions()
+  {
+    divisions = new HashMap<>();
 
-  /*
-   * absolute time when version numbers were last checked
+    /*
+     * for convenience, pre-fill ensembl.org as the domain for "ENSEMBL"
+     */
+    divisions.put(DBRefSource.ENSEMBL.toUpperCase(), ensemblDomain);
+
+    BufferedReader br = null;
+    try
+    {
+      URL url = getDivisionsUrl(ensemblGenomesDomain);
+      if (url != null)
+      {
+        br = getHttpResponse(url, null);
+      }
+      parseResponse(br, ensemblGenomesDomain);
+    } catch (IOException e)
+    {
+      // ignore
+    } finally
+    {
+      if (br != null)
+      {
+        try
+        {
+          br.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+    }
+  }
+
+  /**
+   * Parses the JSON response to /info/divisions, and add each to the lookup map
+   * 
+   * @param br
+   * @param domain
    */
-  long lastVersionCheckTime;
+  void parseResponse(BufferedReader br, String domain)
+  {
+    JSONParser jp = new JSONParser();
 
-  // flag set to true if REST major version is not the one expected
-  boolean restMajorVersionMismatch;
+    try
+    {
+      JSONArray parsed = (JSONArray) jp.parse(br);
+
+      Iterator rvals = parsed.iterator();
+      while (rvals.hasNext())
+      {
+        String division = rvals.next().toString();
+        divisions.put(division.toUpperCase(), domain);
+      }
+    } catch (IOException | ParseException | NumberFormatException e)
+    {
+      // ignore
+    }
+  }
 
   /**
-   * Constructor given expected REST version number e.g 4.5 or 3.4.3
+   * Constructs the URL for the EnsemblGenomes /info/divisions REST service
+   * @param domain TODO
    * 
-   * @param restExpected
+   * @return
+   * @throws MalformedURLException
    */
-  EnsemblInfo(String theDomain, String restExpected)
+  URL getDivisionsUrl(String domain) throws MalformedURLException
   {
-    domain = theDomain;
-    expectedRestVersion = restExpected;
-    lastAvailableCheckTime = -1;
-    lastVersionCheckTime = -1;
+    return new URL(domain
+            + "/info/divisions?content-type=application/json");
   }
 
+  /**
+   * Returns the set of 'divisions' recognised by Ensembl or EnsemblGenomes
+   * 
+   * @return
+   */
+  public Set<String> getDivisions() {
+    if (divisions == null)
+    {
+      fetchDivisions();
+    }
+
+    return divisions.keySet();
+  }
 }
index d7f1b07..c6b794a 100644 (file)
  */
 package jalview.ext.ensembl;
 
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.GeneLociI;
+import jalview.util.MapList;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.json.simple.JSONObject;
@@ -34,13 +38,16 @@ import org.json.simple.parser.JSONParser;
 import org.json.simple.parser.ParseException;
 
 /**
- * A client for the Ensembl lookup REST endpoint, used to find the gene
- * identifier given a gene, transcript or protein identifier.
+ * A client for the Ensembl /lookup REST endpoint, used to find the gene
+ * identifier given a gene, transcript or protein identifier, or to extract the
+ * species or chromosomal coordinates from the same service response
  * 
  * @author gmcarstairs
  */
 public class EnsemblLookup extends EnsemblRestClient
 {
+  private static final String SPECIES = "species";
+
   /**
    * Default constructor (to use rest.ensembl.org)
    */
@@ -111,8 +118,8 @@ public class EnsemblLookup extends EnsemblRestClient
   }
 
   /**
-   * Returns the gene id related to the given identifier, which may be for a
-   * gene, transcript or protein
+   * Returns the gene id related to the given identifier (which may be for a
+   * gene, transcript or protein), or null if none is found
    * 
    * @param identifier
    * @return
@@ -124,7 +131,7 @@ public class EnsemblLookup extends EnsemblRestClient
 
   /**
    * Returns the gene id related to the given identifier (which may be for a
-   * gene, transcript or protein)
+   * gene, transcript or protein), or null if none is found
    * 
    * @param identifier
    * @param objectType
@@ -132,20 +139,95 @@ public class EnsemblLookup extends EnsemblRestClient
    */
   public String getGeneId(String identifier, String objectType)
   {
+    return parseGeneId(getResult(identifier, objectType));
+  }
+
+  /**
+   * Parses the JSON response and returns the gene identifier, or null if not
+   * found. If the returned object_type is Gene, returns the id, if Transcript
+   * returns the Parent. If it is Translation (peptide identifier), then the
+   * Parent is the transcript identifier, so we redo the search with this value.
+   * 
+   * @param br
+   * @return
+   */
+  protected String parseGeneId(JSONObject val)
+  {
+    if (val == null)
+    {
+      return null;
+    }
+    String geneId = null;
+    String type = val.get(OBJECT_TYPE).toString();
+    if (OBJECT_TYPE_GENE.equalsIgnoreCase(type))
+    {
+      // got the gene - just returns its id
+      geneId = val.get(JSON_ID).toString();
+    }
+    else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type))
+    {
+      // got the transcript - return its (Gene) Parent
+      geneId = val.get(PARENT).toString();
+    }
+    else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type))
+    {
+      // got the protein - get its Parent, restricted to type Transcript
+      String transcriptId = val.get(PARENT).toString();
+      geneId = getGeneId(transcriptId, OBJECT_TYPE_TRANSCRIPT);
+    }
+
+    return geneId;
+  }
+
+  /**
+   * Calls the Ensembl lookup REST endpoint and retrieves the 'species' for the
+   * given identifier, or null if not found
+   * 
+   * @param identifier
+   * @return
+   */
+  public String getSpecies(String identifier)
+  {
+    String species = null;
+    JSONObject json = getResult(identifier, null);
+    if (json != null)
+    {
+      Object o = json.get(SPECIES);
+      if (o != null)
+      {
+        species = o.toString();
+      }
+    }
+    return species;
+  }
+
+  /**
+   * Calls the /lookup/id rest service and returns the response as a JSONObject,
+   * or null if any error
+   * 
+   * @param identifier
+   * @param objectType
+   *          (optional)
+   * @return
+   */
+  protected JSONObject getResult(String identifier, String objectType)
+  {
     List<String> ids = Arrays.asList(new String[] { identifier });
 
     BufferedReader br = null;
     try
     {
       URL url = getUrl(identifier, objectType);
+
       if (url != null)
       {
         br = getHttpResponse(url, ids);
       }
-      return br == null ? null : parseResponse(br);
-    } catch (IOException e)
+      return br == null ? null : (JSONObject) (new JSONParser().parse(br));
+    } catch (IOException | ParseException e)
     {
-      // ignore
+      System.err.println("Error parsing " + identifier + " lookup response "
+              + e.getMessage());
       return null;
     } finally
     {
@@ -163,44 +245,82 @@ public class EnsemblLookup extends EnsemblRestClient
   }
 
   /**
-   * Parses the JSON response and returns the gene identifier, or null if not
-   * found. If the returned object_type is Gene, returns the id, if Transcript
-   * returns the Parent. If it is Translation (peptide identifier), then the
-   * Parent is the transcript identifier, so we redo the search with this value.
+   * Calls the /lookup/id rest service for the given id, and if successful,
+   * parses and returns the gene's chromosomal coordinates
    * 
-   * @param br
+   * @param geneId
    * @return
-   * @throws IOException
    */
-  protected String parseResponse(BufferedReader br) throws IOException
+  public GeneLociI getGeneLoci(String geneId)
   {
-    String geneId = null;
-    JSONParser jp = new JSONParser();
+    return parseGeneLoci(getResult(geneId, OBJECT_TYPE_GENE));
+  }
+
+  /**
+   * Parses the /lookup/id response for species, asssembly_name,
+   * seq_region_name, start, end and returns an object that wraps them, or null
+   * if unsuccessful
+   * 
+   * @param json
+   * @return
+   */
+  GeneLociI parseGeneLoci(JSONObject json)
+  {
+    if (json == null)
+    {
+      return null;
+    }
+
     try
     {
-      JSONObject val = (JSONObject) jp.parse(br);
-      String type = val.get(OBJECT_TYPE).toString();
-      if (OBJECT_TYPE_GENE.equalsIgnoreCase(type))
-      {
-        // got the gene - just returns its id
-        geneId = val.get(ID).toString();
-      }
-      else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type))
-      {
-        // got the transcript - return its (Gene) Parent
-        geneId = val.get(PARENT).toString();
-      }
-      else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type))
+      final String species = json.get("species").toString();
+      final String assembly = json.get("assembly_name").toString();
+      final String chromosome = json.get("seq_region_name").toString();
+      String strand = json.get("strand").toString();
+      int start = Integer.parseInt(json.get("start").toString());
+      int end = Integer.parseInt(json.get("end").toString());
+      int fromEnd = end - start + 1;
+      boolean reverseStrand = "-1".equals(strand);
+      int toStart = reverseStrand ? end : start;
+      int toEnd = reverseStrand ? start : end;
+      List<int[]> fromRange = Collections.singletonList(new int[] { 1,
+          fromEnd });
+      List<int[]> toRange = Collections.singletonList(new int[] { toStart,
+          toEnd });
+      final MapList map = new MapList(fromRange, toRange, 1, 1);
+      return new GeneLociI()
       {
-        // got the protein - get its Parent, restricted to type Transcript
-        String transcriptId = val.get(PARENT).toString();
-        geneId = getGeneId(transcriptId, OBJECT_TYPE_TRANSCRIPT);
-      }
-    } catch (ParseException e)
+
+        @Override
+        public String getSpeciesId()
+        {
+          return species == null ? "" : species;
+        }
+
+        @Override
+        public String getAssemblyId()
+        {
+          return assembly;
+        }
+
+        @Override
+        public String getChromosomeId()
+        {
+          return chromosome;
+        }
+
+        @Override
+        public MapList getMap()
+        {
+          return map;
+        }
+      };
+    } catch (NullPointerException | NumberFormatException e)
     {
-      // ignore
+      Cache.log.error("Error looking up gene loci: " + e.getMessage());
+      e.printStackTrace();
     }
-    return geneId;
+    return null;
   }
 
 }
index c94528e..f01bd4f 100644 (file)
@@ -2,11 +2,15 @@ package jalview.ext.ensembl;
 
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
+import jalview.util.MapList;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
@@ -21,6 +25,10 @@ public class EnsemblMap extends EnsemblRestClient
 
   private static final String MAPPINGS = "mappings";
 
+  private static final String CDS = "cds";
+
+  private static final String CDNA = "cdna";
+
   /**
    * Default constructor (to use rest.ensembl.org)
    */
@@ -189,6 +197,80 @@ public class EnsemblMap extends EnsemblRestClient
   }
 
   /**
+   * Calls the REST /map/cds/id service, and returns a DBRefEntry holding the
+   * returned chromosomal coordinates, or returns null if the call fails
+   * 
+   * @param division
+   *          e.g. Ensembl, EnsemblMetazoa
+   * @param accession
+   *          e.g. ENST00000592782, Y55B1AR.1.1
+   * @param start
+   * @param end
+   * @return
+   */
+  public GeneLociI getCdsMapping(String division, String accession,
+          int start, int end)
+  {
+    return getIdMapping(division, accession, start, end, CDS);
+  }
+
+  /**
+   * Calls the REST /map/cdna/id service, and returns a DBRefEntry holding the
+   * returned chromosomal coordinates, or returns null if the call fails
+   * 
+   * @param division
+   *          e.g. Ensembl, EnsemblMetazoa
+   * @param accession
+   *          e.g. ENST00000592782, Y55B1AR.1.1
+   * @param start
+   * @param end
+   * @return
+   */
+  public GeneLociI getCdnaMapping(String division, String accession,
+          int start, int end)
+  {
+    return getIdMapping(division, accession, start, end, CDNA);
+  }
+
+  GeneLociI getIdMapping(String division, String accession, int start,
+          int end, String cdsOrCdna)
+  {
+    URL url = null;
+    BufferedReader br = null;
+
+    try
+    {
+      String domain = new EnsemblInfo().getDomain(division);
+      if (domain != null)
+      {
+        url = getIdMapUrl(domain, accession, start, end, cdsOrCdna);
+        br = getHttpResponse(url, null);
+        if (br != null)
+        {
+          return (parseIdMappingResponse(br, accession, domain));
+        }
+      }
+      return null;
+    } catch (Throwable t)
+    {
+      System.out.println("Error calling " + url + ": " + t.getMessage());
+      return null;
+    } finally
+    {
+      if (br != null)
+      {
+        try
+        {
+          br.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+    }
+  }
+
+  /**
    * Constructs a URL to the /map/cds/<id> or /map/cdna/<id> REST service. The
    * REST call is to either ensembl or ensemblgenomes, as determined from the
    * division, e.g. Ensembl or EnsemblProtists.
@@ -210,4 +292,122 @@ public class EnsemblMap extends EnsemblRestClient
     return new URL(url);
   }
 
+  /**
+   * Parses the JSON response from the /map/cds/ or /map/cdna REST service. The
+   * format is
+   * 
+   * <pre>
+   * {"mappings":
+   *   [
+   *    {"assembly_name":"TAIR10","end":2501311,"seq_region_name":"1","gap":0,
+   *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2501114},
+   *    {"assembly_name":"TAIR10","end":2500815,"seq_region_name":"1","gap":0,
+   *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2500714}
+   *   ]
+   * }
+   * </pre>
+   * 
+   * @param br
+   * @param accession
+   * @param domain
+   * @return
+   */
+  GeneLociI parseIdMappingResponse(BufferedReader br, String accession,
+          String domain)
+  {
+    JSONParser jp = new JSONParser();
+
+    try
+    {
+      JSONObject parsed = (JSONObject) jp.parse(br);
+      JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
+
+      Iterator rvals = mappings.iterator();
+      String assembly = null;
+      String chromosome = null;
+      int fromEnd = 0;
+      List<int[]> regions = new ArrayList<>();
+
+      while (rvals.hasNext())
+      {
+        JSONObject val = (JSONObject) rvals.next();
+        JSONObject original = (JSONObject) val.get("original");
+        fromEnd = Integer.parseInt(original.get("end").toString());
+
+        JSONObject mapped = (JSONObject) val.get(MAPPED);
+        int start = Integer.parseInt(mapped.get("start").toString());
+        int end = Integer.parseInt(mapped.get("end").toString());
+        String ass = mapped.get("assembly_name").toString();
+        if (assembly != null && !assembly.equals(ass))
+        {
+          System.err
+                  .println("EnsemblMap found multiple assemblies - can't resolve");
+          return null;
+        }
+        assembly = ass;
+        String chr = mapped.get("seq_region_name").toString();
+        if (chromosome != null && !chromosome.equals(chr))
+        {
+          System.err
+                  .println("EnsemblMap found multiple chromosomes - can't resolve");
+          return null;
+        }
+        chromosome = chr;
+        String strand = mapped.get("strand").toString();
+        if ("-1".equals(strand))
+        {
+          regions.add(new int[] { end, start });
+        }
+        else
+        {
+          regions.add(new int[] { start, end });
+        }
+      }
+
+      /*
+       * processed all mapped regions on chromosome, assemble the result,
+       * having first fetched the species id for the accession
+       */
+      final String species = new EnsemblLookup(domain)
+              .getSpecies(accession);
+      final String as = assembly;
+      final String chr = chromosome;
+      List<int[]> fromRange = Collections.singletonList(new int[] { 1,
+          fromEnd });
+      final MapList map = new MapList(fromRange, regions, 1, 1);
+      return new GeneLociI()
+      {
+
+        @Override
+        public String getSpeciesId()
+        {
+          return species == null ? "" : species;
+        }
+
+        @Override
+        public String getAssemblyId()
+        {
+          return as;
+        }
+
+        @Override
+        public String getChromosomeId()
+        {
+          return chr;
+        }
+
+        @Override
+        public MapList getMap()
+        {
+          return map;
+        }
+      };
+    } catch (IOException | ParseException | NumberFormatException e)
+    {
+      // ignore
+    }
+
+    return null;
+  }
+
 }
index 99006aa..0280f16 100644 (file)
@@ -22,6 +22,10 @@ package jalview.ext.ensembl;
 
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+
+import java.util.ArrayList;
+import java.util.List;
 
 import com.stevesoft.pat.Regex;
 
@@ -106,10 +110,10 @@ public class EnsemblProtein extends EnsemblSeqProxy
   }
 
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    // not applicable - protein sequence is not a 'subset' of genomic sequence
-    return false;
+    return new ArrayList<>();
   }
 
   @Override
index e6b1264..9e01cc4 100644 (file)
@@ -64,13 +64,13 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    * @see https://github.com/Ensembl/ensembl-rest/wiki/Change-log
    * @see http://rest.ensembl.org/info/rest?content-type=application/json
    */
-  private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "6.0";
+  private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "7.0";
 
-  private static final String LATEST_ENSEMBL_REST_VERSION = "6.1";
+  private static final String LATEST_ENSEMBL_REST_VERSION = "7.0";
 
   private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
 
-  private static Map<String, EnsemblInfo> domainData = new HashMap<>();
+  private static Map<String, EnsemblData> domainData;
 
   private final static long AVAILABILITY_RETEST_INTERVAL = 10000L; // 10 seconds
 
@@ -80,10 +80,10 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   static
   {
+    domainData = new HashMap<>();
     domainData.put(DEFAULT_ENSEMBL_BASEURL,
-            new EnsemblInfo(DEFAULT_ENSEMBL_BASEURL, LATEST_ENSEMBL_REST_VERSION));
-    domainData.put(DEFAULT_ENSEMBL_GENOMES_BASEURL,
-            new EnsemblInfo(
+            new EnsemblData(DEFAULT_ENSEMBL_BASEURL, LATEST_ENSEMBL_REST_VERSION));
+    domainData.put(DEFAULT_ENSEMBL_GENOMES_BASEURL, new EnsemblData(
             DEFAULT_ENSEMBL_GENOMES_BASEURL, LATEST_ENSEMBLGENOMES_REST_VERSION));
   }
 
@@ -102,11 +102,11 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
     if (!domainData.containsKey(ensemblDomain))
     {
       domainData.put(ensemblDomain,
-              new EnsemblInfo(ensemblDomain, LATEST_ENSEMBL_REST_VERSION));
+              new EnsemblData(ensemblDomain, LATEST_ENSEMBL_REST_VERSION));
     }
     if (!domainData.containsKey(ensemblGenomesDomain))
     {
-      domainData.put(ensemblGenomesDomain, new EnsemblInfo(
+      domainData.put(ensemblGenomesDomain, new EnsemblData(
               ensemblGenomesDomain, LATEST_ENSEMBLGENOMES_REST_VERSION));
     }
   }
@@ -258,7 +258,8 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
   }
 
   /**
-   * Sends the HTTP request and gets the response as a reader
+   * Sends the HTTP request and gets the response as a reader. Returns null if
+   * the HTTP response code was not 200.
    * 
    * @param url
    * @param ids
@@ -267,7 +268,6 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    *          in milliseconds
    * @return
    * @throws IOException
-   *           if response code was not 200, or other I/O error
    */
   protected BufferedReader getHttpResponse(URL url, List<String> ids,
           int readTimeout) throws IOException
@@ -391,7 +391,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    */
   protected boolean isEnsemblAvailable()
   {
-    EnsemblInfo info = domainData.get(getDomain());
+    EnsemblData info = domainData.get(getDomain());
 
     long now = System.currentTimeMillis();
 
@@ -465,7 +465,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    */
   private void checkEnsemblRestVersion()
   {
-    EnsemblInfo info = domainData.get(getDomain());
+    EnsemblData info = domainData.get(getDomain());
 
     JSONParser jp = new JSONParser();
     URL url = null;
index c903de3..7b448fd 100644 (file)
@@ -33,6 +33,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.exceptions.JalviewException;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.util.Comparison;
@@ -61,8 +62,6 @@ import org.json.simple.parser.ParseException;
  */
 public abstract class EnsemblSeqProxy extends EnsemblRestClient
 {
-  private static final String ALLELES = "alleles";
-
   protected static final String NAME = "Name";
 
   protected static final String DESCRIPTION = "description";
@@ -443,10 +442,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
-   * Parses a JSON response into a list of sequences
+   * Parses a JSON response for a single sequence ID query
    * 
    * @param br
-   * @return
+   * @return a single jalview.datamodel.Sequence
    * @see http://rest.ensembl.org/documentation/info/sequence_id
    */
   protected List<SequenceI> parseSequenceJson(BufferedReader br)
@@ -582,8 +581,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
           String accId, int start)
   {
-    List<SequenceFeature> sfs = sourceSequence.getFeatures()
-            .getPositionalFeatures();
+    List<SequenceFeature> sfs = getIdentifyingFeatures(sourceSequence,
+            accId);
     if (sfs.isEmpty())
     {
       return null;
@@ -600,47 +599,31 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
     for (SequenceFeature sf : sfs)
     {
+      int strand = sf.getStrand();
+      strand = strand == 0 ? 1 : strand; // treat unknown as forward
+
+      if (directionSet && strand != direction)
+      {
+        // abort - mix of forward and backward
+        System.err
+                .println("Error: forward and backward strand for " + accId);
+        return null;
+      }
+      direction = strand;
+      directionSet = true;
+
       /*
-       * accept the target feature type or a specialisation of it
-       * (e.g. coding_exon for exon)
+       * add to CDS ranges, semi-sorted forwards/backwards
        */
-      if (identifiesSequence(sf, accId))
+      if (strand < 0)
       {
-        int strand = sf.getStrand();
-        strand = strand == 0 ? 1 : strand; // treat unknown as forward
-
-        if (directionSet && strand != direction)
-        {
-          // abort - mix of forward and backward
-          System.err.println(
-                  "Error: forward and backward strand for " + accId);
-          return null;
-        }
-        direction = strand;
-        directionSet = true;
-
-        /*
-         * add to CDS ranges, semi-sorted forwards/backwards
-         */
-        if (strand < 0)
-        {
-          regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
-        }
-        else
-        {
-          regions.add(new int[] { sf.getBegin(), sf.getEnd() });
-        }
-        mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
-
-        if (!isSpliceable())
-        {
-          /*
-           * 'gene' sequence is contiguous so we can stop as soon as its
-           * identifying feature has been found
-           */
-          break;
-        }
+        regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
+      }
+      else
+      {
+        regions.add(new int[] { sf.getBegin(), sf.getEnd() });
       }
+      mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
     }
 
     if (regions.isEmpty())
@@ -665,26 +648,18 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
-   * Answers true if the sequence being retrieved may occupy discontiguous
-   * regions on the genomic sequence.
-   */
-  protected boolean isSpliceable()
-  {
-    return true;
-  }
-
-  /**
-   * Returns true if the sequence feature marks positions of the genomic
+   * Answers a list of sequence features that mark positions of the genomic
    * sequence feature which are within the sequence being retrieved. For
    * example, an 'exon' feature whose parent is the target transcript marks the
-   * cdna positions of the transcript.
+   * cdna positions of the transcript. For a gene sequence, this is trivially
+   * just the 'gene' feature with matching gene id.
    * 
-   * @param sf
+   * @param seq
    * @param accId
    * @return
    */
-  protected abstract boolean identifiesSequence(SequenceFeature sf,
-          String accId);
+  protected abstract List<SequenceFeature> getIdentifyingFeatures(
+          SequenceI seq, String accId);
 
   /**
    * Transfers the sequence feature to the target sequence, locating its start
@@ -738,7 +713,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
    */
   static void reverseComplementAlleles(SequenceFeature sf)
   {
-    final String alleles = (String) sf.getValue(ALLELES);
+    final String alleles = (String) sf.getValue(Gff3Helper.ALLELES);
     if (alleles == null)
     {
       return;
@@ -749,7 +724,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       reverseComplementAllele(complement, allele);
     }
     String comp = complement.toString();
-    sf.setValue(ALLELES, comp);
+    sf.setValue(Gff3Helper.ALLELES, comp);
     sf.setDescription(comp);
 
     /*
@@ -759,7 +734,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     String atts = sf.getAttributes();
     if (atts != null)
     {
-      atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp);
+      atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles,
+              Gff3Helper.ALLELES + "=" + comp);
       sf.setAttributes(atts);
     }
   }
index 0aaaf93..9e3fef4 100644 (file)
@@ -64,7 +64,7 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
 
   protected static final String PARENT = "Parent";
 
-  protected static final String ID = "id";
+  protected static final String JSON_ID = "id";
 
   protected static final String OBJECT_TYPE = "object_type";
 
index e3b6c93..40d6cad 100644 (file)
@@ -75,7 +75,7 @@ public class EnsemblSymbol extends EnsemblXref
       while (rvals.hasNext())
       {
         JSONObject val = (JSONObject) rvals.next();
-        String id = val.get(ID).toString();
+        String id = val.get(JSON_ID).toString();
         String type = val.get(TYPE).toString();
         if (id != null && GENE.equals(type))
         {
@@ -150,7 +150,6 @@ public class EnsemblSymbol extends EnsemblXref
             if (br != null)
             {
               String geneId = parseSymbolResponse(br);
-              System.out.println(url + " returned " + geneId);
               if (geneId != null && !result.contains(geneId))
               {
                 result.add(geneId);
index 37ce625..73d1674 100644 (file)
  */
 package jalview.ext.htsjdk;
 
-import htsjdk.samtools.SAMSequenceDictionary;
-import htsjdk.samtools.SAMSequenceRecord;
-import htsjdk.samtools.reference.ReferenceSequence;
-import htsjdk.samtools.reference.ReferenceSequenceFile;
-import htsjdk.samtools.reference.ReferenceSequenceFileFactory;
-import htsjdk.samtools.util.StringUtil;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 
 import java.io.File;
+import java.io.IOException;
 import java.math.BigInteger;
+import java.nio.file.Path;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
@@ -38,6 +34,15 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import htsjdk.samtools.SAMException;
+import htsjdk.samtools.SAMSequenceDictionary;
+import htsjdk.samtools.SAMSequenceRecord;
+import htsjdk.samtools.reference.FastaSequenceIndexCreator;
+import htsjdk.samtools.reference.ReferenceSequence;
+import htsjdk.samtools.reference.ReferenceSequenceFile;
+import htsjdk.samtools.reference.ReferenceSequenceFileFactory;
+import htsjdk.samtools.util.StringUtil;
+
 /**
  * a source of sequence data accessed via the HTSJDK
  * 
@@ -46,14 +51,25 @@ import java.util.Set;
  */
 public class HtsContigDb
 {
-
   private String name;
 
   private File dbLocation;
 
   private htsjdk.samtools.reference.ReferenceSequenceFile refFile = null;
 
-  public HtsContigDb(String name, File descriptor) throws Exception
+  public static void createFastaSequenceIndex(Path path, boolean overwrite)
+          throws IOException
+  {
+    try
+    {
+      FastaSequenceIndexCreator.create(path, overwrite);
+    } catch (SAMException e)
+    {
+      throw new IOException(e.getMessage());
+    }
+  }
+
+  public HtsContigDb(String name, File descriptor)
   {
     if (descriptor.isFile())
     {
@@ -63,7 +79,21 @@ public class HtsContigDb
     initSource();
   }
 
-  private void initSource() throws Exception
+  public void close()
+  {
+    if (refFile != null)
+    {
+      try
+      {
+        refFile.close();
+      } catch (IOException e)
+      {
+        // ignore
+      }
+    }
+  }
+
+  private void initSource()
   {
     if (refFile != null)
     {
@@ -142,8 +172,8 @@ public class HtsContigDb
     final ReferenceSequenceFile refSeqFile = ReferenceSequenceFileFactory
             .getReferenceSequenceFile(f, truncate);
     ReferenceSequence refSeq;
-    List<SAMSequenceRecord> ret = new ArrayList<SAMSequenceRecord>();
-    Set<String> sequenceNames = new HashSet<String>();
+    List<SAMSequenceRecord> ret = new ArrayList<>();
+    Set<String> sequenceNames = new HashSet<>();
     for (int numSequences = 0; (refSeq = refSeqFile
             .nextSequence()) != null; ++numSequences)
     {
@@ -220,14 +250,29 @@ public class HtsContigDb
 
   // ///// end of hts bits.
 
-  SequenceI getSequenceProxy(String id)
+  /**
+   * Reads the contig with the given id and returns as a Jalview SequenceI object.
+   * Note the database must be indexed for this operation to succeed.
+   * 
+   * @param id
+   * @return
+   */
+  public SequenceI getSequenceProxy(String id)
   {
-    if (!isValid())
+    if (!isValid() || !refFile.isIndexed())
     {
+      System.err.println(
+              "Cannot read contig as file is invalid or not indexed");
       return null;
     }
 
     ReferenceSequence sseq = refFile.getSequence(id);
     return new Sequence(sseq.getName(), new String(sseq.getBases()));
   }
+
+  public boolean isIndexed()
+  {
+    return refFile != null && refFile.isIndexed();
+  }
+
 }
diff --git a/src/jalview/ext/htsjdk/VCFReader.java b/src/jalview/ext/htsjdk/VCFReader.java
new file mode 100644 (file)
index 0000000..14c057f
--- /dev/null
@@ -0,0 +1,214 @@
+package jalview.ext.htsjdk;
+
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFFileReader;
+import htsjdk.variant.vcf.VCFHeader;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A thin wrapper for htsjdk classes to read either plain, or compressed, or
+ * compressed and indexed VCF files
+ */
+public class VCFReader implements Closeable, Iterable<VariantContext>
+{
+  private static final String GZ = "gz";
+
+  private static final String TBI_EXTENSION = ".tbi";
+
+  private boolean indexed;
+
+  private VCFFileReader reader;
+
+  /**
+   * Constructor given a raw or compressed VCF file or a (tabix) index file
+   * <p>
+   * For now, file type is inferred from its suffix: .gz or .bgz for compressed
+   * data, .tbi for an index file, anything else is assumed to be plain text
+   * VCF.
+   * 
+   * @param f
+   * @throws IOException
+   */
+  public VCFReader(String filePath) throws IOException
+  {
+    if (filePath.endsWith(GZ))
+    {
+      if (new File(filePath + TBI_EXTENSION).exists())
+      {
+        indexed = true;
+      }
+    }
+    else if (filePath.endsWith(TBI_EXTENSION))
+    {
+      indexed = true;
+      filePath = filePath.substring(0, filePath.length() - 4);
+    }
+
+    reader = new VCFFileReader(new File(filePath), indexed);
+  }
+
+  @Override
+  public void close() throws IOException
+  {
+    if (reader != null)
+    {
+      reader.close();
+    }
+  }
+
+  /**
+   * Returns an iterator over VCF variants in the file. The client should call
+   * close() on the iterator when finished with it.
+   */
+  @Override
+  public CloseableIterator<VariantContext> iterator()
+  {
+    return reader == null ? null : reader.iterator();
+  }
+
+  /**
+   * Queries for records overlapping the region specified. Note that this method
+   * is performant if the VCF file is indexed, and may be very slow if it is
+   * not.
+   * <p>
+   * Client code should call close() on the iterator when finished with it.
+   * 
+   * @param chrom
+   *          the chromosome to query
+   * @param start
+   *          query interval start
+   * @param end
+   *          query interval end
+   * @return
+   */
+  public CloseableIterator<VariantContext> query(final String chrom,
+          final int start, final int end)
+  {
+   if (reader == null) {
+     return null;
+   }
+    if (indexed)
+    {
+      return reader.query(chrom, start, end);
+    }
+    else
+    {
+      return queryUnindexed(chrom, start, end);
+    }
+  }
+
+  /**
+   * Returns an iterator over variant records read from a flat file which
+   * overlap the specified chromosomal positions. Call close() on the iterator
+   * when finished with it!
+   * 
+   * @param chrom
+   * @param start
+   * @param end
+   * @return
+   */
+  protected CloseableIterator<VariantContext> queryUnindexed(
+          final String chrom, final int start, final int end)
+  {
+    final CloseableIterator<VariantContext> it = reader.iterator();
+    
+    return new CloseableIterator<VariantContext>()
+    {
+      boolean atEnd = false;
+
+      // prime look-ahead buffer with next matching record
+      private VariantContext next = findNext();
+
+      private VariantContext findNext()
+      {
+        if (atEnd)
+        {
+          return null;
+        }
+        VariantContext variant = null;
+        while (it.hasNext())
+        {
+          variant = it.next();
+          int vstart = variant.getStart();
+
+          if (vstart > end)
+          {
+            atEnd = true;
+            close();
+            return null;
+          }
+
+          int vend = variant.getEnd();
+          // todo what is the undeprecated way to get
+          // the chromosome for the variant?
+          if (chrom.equals(variant.getChr()) && (vstart <= end)
+                  && (vend >= start))
+          {
+            return variant;
+          }
+        }
+        return null;
+      }
+
+      @Override
+      public boolean hasNext()
+      {
+        boolean hasNext = !atEnd && (next != null);
+        if (!hasNext)
+        {
+          close();
+        }
+        return hasNext;
+      }
+
+      @Override
+      public VariantContext next()
+      {
+        /*
+         * return the next match, and then re-prime
+         * it with the following one (if any)
+         */
+        VariantContext temp = next;
+        next = findNext();
+        return temp;
+      }
+
+      @Override
+      public void remove()
+      {
+        // not implemented
+      }
+
+      @Override
+      public void close()
+      {
+        it.close();
+      }
+    };
+  }
+
+  /**
+   * Returns an object that models the VCF file headers
+   * 
+   * @return
+   */
+  public VCFHeader getFileHeader()
+  {
+    return reader == null ? null : reader.getFileHeader();
+  }
+
+  /**
+   * Answers true if we are processing a tab-indexed VCF file, false if it is a
+   * plain text (uncompressed) file.
+   * 
+   * @return
+   */
+  public boolean isIndex()
+  {
+    return indexed;
+  }
+}
index c5a2bf0..94b38ed 100644 (file)
@@ -81,6 +81,7 @@ import jalview.io.JnetAnnotationMaker;
 import jalview.io.NewickFile;
 import jalview.io.ScoreMatrixFile;
 import jalview.io.TCoffeeScoreFile;
+import jalview.io.vcf.VCFLoader;
 import jalview.jbgui.GAlignFrame;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
@@ -839,6 +840,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     AlignmentI al = getViewport().getAlignment();
     boolean nucleotide = al.isNucleotide();
 
+    loadVcf.setVisible(nucleotide);
     showTranslation.setVisible(nucleotide);
     showReverse.setVisible(nucleotide);
     showReverseComplement.setVisible(nucleotide);
@@ -1390,13 +1392,13 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   @Override
   public void exportFeatures_actionPerformed(ActionEvent e)
   {
-    new AnnotationExporter().exportFeatures(alignPanel);
+    new AnnotationExporter(alignPanel).exportFeatures();
   }
 
   @Override
   public void exportAnnotations_actionPerformed(ActionEvent e)
   {
-    new AnnotationExporter().exportAnnotations(alignPanel);
+    new AnnotationExporter(alignPanel).exportAnnotations();
   }
 
   @Override
@@ -1826,7 +1828,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   @Override
   protected void copy_actionPerformed(ActionEvent e)
   {
-    System.gc();
     if (viewport.getSelectionGroup() == null)
     {
       return;
@@ -4250,7 +4251,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
           final String source)
   {
-    new Thread(CrossRefAction.showProductsFor(sel, _odna, source, this))
+    new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this))
             .start();
   }
 
@@ -5592,6 +5593,27 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       new CalculationChooser(AlignFrame.this);
     }
   }
+
+  @Override
+  protected void loadVcf_actionPerformed()
+  {
+    JalviewFileChooser chooser = new JalviewFileChooser(
+            Cache.getProperty("LAST_DIRECTORY"));
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
+    chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
+
+    int value = chooser.showOpenDialog(null);
+
+    if (value == JalviewFileChooser.APPROVE_OPTION)
+    {
+      String choice = chooser.getSelectedFile().getPath();
+      Cache.setProperty("LAST_DIRECTORY", choice);
+      SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
+      new VCFLoader(choice).loadVCF(seqs, this);
+    }
+
+  }
 }
 
 class PrintThread extends Thread
index a619997..6fefbd0 100644 (file)
 package jalview.gui;
 
 import jalview.api.FeatureColourI;
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.io.AnnotationFile;
 import jalview.io.FeaturesFile;
 import jalview.io.JalviewFileChooser;
@@ -34,6 +36,8 @@ import java.awt.Color;
 import java.awt.FlowLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.io.FileWriter;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Map;
 
@@ -57,18 +61,22 @@ import javax.swing.SwingConstants;
  */
 public class AnnotationExporter extends JPanel
 {
-  JInternalFrame frame;
+  private JInternalFrame frame;
 
-  AlignmentPanel ap;
+  private AlignmentPanel ap;
 
-  boolean features = true;
+  /*
+   * true if exporting features, false if exporting annotations
+   */
+  private boolean exportFeatures = true;
 
   private AlignmentAnnotation[] annotations;
 
   private boolean wholeView;
 
-  public AnnotationExporter()
+  public AnnotationExporter(AlignmentPanel panel)
   {
+    this.ap = panel;
     try
     {
       jbInit();
@@ -84,47 +92,54 @@ public class AnnotationExporter extends JPanel
             frame.getPreferredSize().height);
   }
 
-  public void exportFeatures(AlignmentPanel ap)
+  /**
+   * Configures the diglog for options to export visible features
+   */
+  public void exportFeatures()
   {
-    this.ap = ap;
-    features = true;
+    exportFeatures = true;
     CSVFormat.setVisible(false);
     frame.setTitle(MessageManager.getString("label.export_features"));
   }
 
-  public void exportAnnotations(AlignmentPanel ap)
+  /**
+   * Configures the dialog for options to export all visible annotations
+   */
+  public void exportAnnotations()
   {
-    this.ap = ap;
-    annotations = ap.av.isShowAnnotation() ? null
-            : ap.av.getAlignment().getAlignmentAnnotation();
-    wholeView = true;
-    startExportAnnotation();
+    boolean showAnnotation = ap.av.isShowAnnotation();
+    exportAnnotation(showAnnotation ? null
+            : ap.av.getAlignment().getAlignmentAnnotation(), true);
   }
 
-  public void exportAnnotations(AlignmentPanel alp,
-          AlignmentAnnotation[] toExport)
+  /**
+   * Configures the dialog for options to export the given annotation row
+   * 
+   * @param toExport
+   */
+  public void exportAnnotation(AlignmentAnnotation toExport)
   {
-    ap = alp;
-    annotations = toExport;
-    wholeView = false;
-    startExportAnnotation();
+    exportAnnotation(new AlignmentAnnotation[] { toExport }, false);
   }
 
-  private void startExportAnnotation()
+  private void exportAnnotation(AlignmentAnnotation[] toExport,
+          boolean forWholeView)
   {
-    features = false;
+    wholeView = forWholeView;
+    annotations = toExport;
+    exportFeatures = false;
     GFFFormat.setVisible(false);
     CSVFormat.setVisible(true);
     frame.setTitle(MessageManager.getString("label.export_annotations"));
   }
 
-  public void toFile_actionPerformed(ActionEvent e)
+  private void toFile_actionPerformed()
   {
     JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
+            Cache.getProperty("LAST_DIRECTORY"));
 
     chooser.setFileView(new JalviewFileView());
-    chooser.setDialogTitle(features
+    chooser.setDialogTitle(exportFeatures
             ? MessageManager.getString("label.save_features_to_file")
             : MessageManager.getString("label.save_annotation_to_file"));
     chooser.setToolTipText(MessageManager.getString("action.save"));
@@ -133,13 +148,12 @@ public class AnnotationExporter extends JPanel
 
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
-      String text = getFileContents();
+      String text = getText();
 
       try
       {
-        java.io.PrintWriter out = new java.io.PrintWriter(
-                new java.io.FileWriter(chooser.getSelectedFile()));
-
+        PrintWriter out = new PrintWriter(
+                new FileWriter(chooser.getSelectedFile()));
         out.print(text);
         out.close();
       } catch (Exception ex)
@@ -148,64 +162,89 @@ public class AnnotationExporter extends JPanel
       }
     }
 
-    close_actionPerformed(null);
+    close_actionPerformed();
+  }
+
+  /**
+   * Answers the text to output for either Features (in GFF or Jalview format) or
+   * Annotations (in CSV or Jalview format)
+   * 
+   * @return
+   */
+  private String getText()
+  {
+    return exportFeatures ? getFeaturesText() : getAnnotationsText();
   }
 
-  private String getFileContents()
+  /**
+   * Returns the text contents for output of annotations in either CSV or Jalview
+   * format
+   * 
+   * @return
+   */
+  private String getAnnotationsText()
   {
-    String text = MessageManager
-            .getString("label.no_features_on_alignment");
-    if (features)
+    String text;
+    if (CSVFormat.isSelected())
     {
-      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 = formatter.printGffFormat(sequences, featureColours,
-                featureGroups, includeNonPositional);
-      }
-      else
-      {
-        text = formatter.printJalviewFormat(sequences, featureColours,
-                featureGroups, includeNonPositional);
-      }
+      text = new AnnotationFile().printCSVAnnotations(annotations);
     }
     else
     {
-      if (CSVFormat.isSelected())
+      if (wholeView)
       {
-        text = new AnnotationFile().printCSVAnnotations(annotations);
+        text = new AnnotationFile().printAnnotationsForView(ap.av);
       }
       else
       {
-        if (wholeView)
-        {
-          text = new AnnotationFile().printAnnotationsForView(ap.av);
-        }
-        else
-        {
-          text = new AnnotationFile().printAnnotations(annotations, null,
-                  null);
-        }
+        text = new AnnotationFile().printAnnotations(annotations, null,
+                null);
       }
     }
     return text;
   }
 
-  public void toTextbox_actionPerformed(ActionEvent e)
+  /**
+   * Returns the text contents for output of features in either GFF or Jalview
+   * format
+   * 
+   * @return
+   */
+  private String getFeaturesText()
+  {
+    String text;
+    SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
+    Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
+            .getDisplayedFeatureCols();
+    Map<String, FeatureMatcherSetI> featureFilters = ap.getFeatureRenderer()
+            .getFeatureFilters();
+    List<String> featureGroups = ap.getFeatureRenderer()
+            .getDisplayedFeatureGroups();
+    boolean includeNonPositional = ap.av.isShowNPFeats();
+
+    FeaturesFile formatter = new FeaturesFile();
+    if (GFFFormat.isSelected())
+    {
+      text = formatter.printGffFormat(sequences, featureColours,
+              featureGroups, includeNonPositional);
+    }
+    else
+    {
+      text = formatter.printJalviewFormat(sequences, featureColours,
+              featureFilters, featureGroups, includeNonPositional);
+    }
+    return text;
+  }
+
+  private void toTextbox_actionPerformed()
   {
     CutAndPasteTransfer cap = new CutAndPasteTransfer();
 
     try
     {
-      String text = getFileContents();
+      String text = getText();
       cap.setText(text);
-      Desktop.addInternalFrame(cap, (features ? MessageManager
+      Desktop.addInternalFrame(cap, (exportFeatures ? MessageManager
               .formatMessage("label.features_for_params", new String[]
               { ap.alignFrame.getTitle() })
               : MessageManager.formatMessage("label.annotations_for_params",
@@ -214,7 +253,7 @@ public class AnnotationExporter extends JPanel
               600, 500);
     } catch (OutOfMemoryError oom)
     {
-      new OOMWarning((features ? MessageManager.formatMessage(
+      new OOMWarning((exportFeatures ? MessageManager.formatMessage(
               "label.generating_features_for_params", new String[]
               { ap.alignFrame.getTitle() })
               : MessageManager.formatMessage(
@@ -225,10 +264,10 @@ public class AnnotationExporter extends JPanel
       cap.dispose();
     }
 
-    close_actionPerformed(null);
+    close_actionPerformed();
   }
 
-  public void close_actionPerformed(ActionEvent e)
+  private void close_actionPerformed()
   {
     try
     {
@@ -248,7 +287,7 @@ public class AnnotationExporter extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        toFile_actionPerformed(e);
+        toFile_actionPerformed();
       }
     });
     toTextbox.setText(MessageManager.getString("label.to_textbox"));
@@ -257,7 +296,7 @@ public class AnnotationExporter extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        toTextbox_actionPerformed(e);
+        toTextbox_actionPerformed();
       }
     });
     close.setText(MessageManager.getString("action.close"));
@@ -266,7 +305,7 @@ public class AnnotationExporter extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        close_actionPerformed(e);
+        close_actionPerformed();
       }
     });
     jalviewFormat.setOpaque(false);
index b58269d..6f8b225 100755 (executable)
@@ -257,9 +257,7 @@ public class AnnotationLabels extends JPanel
     }
     else if (evt.getActionCommand().equals(OUTPUT_TEXT))
     {
-      new AnnotationExporter().exportAnnotations(ap,
-              new AlignmentAnnotation[]
-              { aa[selectedRow] });
+      new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]);
     }
     else if (evt.getActionCommand().equals(COPYCONS_SEQ))
     {
index 7cfac99..8ef204c 100644 (file)
@@ -255,4 +255,4 @@ public class AquaInternalFrameManager extends DefaultDesktopManager
   {
     switchFrame(false);
   }
-}
\ No newline at end of file
+}
index e403dba..f674c7e 100644 (file)
@@ -169,8 +169,8 @@ public class CalculationChooser extends JPanel
     JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
     treePanel.setOpaque(false);
 
-    treePanel.setBorder(BorderFactory
-            .createTitledBorder(MessageManager.getString("label.tree")));
+    JvSwingUtils.createTitledBorder(treePanel,
+            MessageManager.getString("label.tree"), true);
 
     // then copy the inset dimensions for the border-less PCA panel
     JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
index 2d1dfd4..85f2498 100644 (file)
@@ -27,17 +27,25 @@ import jalview.api.FeatureSettingsModelI;
 import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.SequenceI;
+import jalview.ext.ensembl.EnsemblInfo;
+import jalview.ext.ensembl.EnsemblMap;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.DBRefUtils;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
 import jalview.ws.SequenceFetcher;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-
-import javax.swing.JOptionPane;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Factory constructor and runnable for discovering and displaying
@@ -52,13 +60,13 @@ public class CrossRefAction implements Runnable
 
   private SequenceI[] sel;
 
-  private boolean _odna;
+  private final boolean _odna;
 
   private String source;
 
-  List<AlignmentViewPanel> xrefViews = new ArrayList<AlignmentViewPanel>();
+  List<AlignmentViewPanel> xrefViews = new ArrayList<>();
 
-  public List<jalview.api.AlignmentViewPanel> getXrefViews()
+  List<AlignmentViewPanel> getXrefViews()
   {
     return xrefViews;
   }
@@ -90,6 +98,13 @@ public class CrossRefAction implements Runnable
       {
         return;
       }
+
+      /*
+       * try to look up chromosomal coordinates for nucleotide
+       * sequences (if not already retrieved)
+       */
+      findGeneLoci(xrefs.getSequences());
+
       /*
        * get display scheme (if any) to apply to features
        */
@@ -113,75 +128,14 @@ public class CrossRefAction implements Runnable
 
       if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
       {
-        boolean copyAlignmentIsAligned = false;
-        if (dna)
-        {
-          copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
-                  xrefsAlignment.getSequencesArray());
-          if (copyAlignment.getHeight() == 0)
-          {
-            JvOptionPane.showMessageDialog(alignFrame,
-                    MessageManager.getString("label.cant_map_cds"),
-                    MessageManager.getString("label.operation_failed"),
-                    JvOptionPane.OK_OPTION);
-            System.err.println("Failed to make CDS alignment");
-          }
-
-          /*
-           * pending getting Embl transcripts to 'align', 
-           * we are only doing this for Ensembl
-           */
-          // TODO proper criteria for 'can align as cdna'
-          if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
-                  || AlignmentUtils.looksLikeEnsembl(alignment))
-          {
-            copyAlignment.alignAs(alignment);
-            copyAlignmentIsAligned = true;
-          }
-        }
-        else
+        copyAlignment = copyAlignmentForSplitFrame(alignment, dataset, dna,
+                xrefs, xrefsAlignment);
+        if (copyAlignment == null)
         {
-          copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
-                  xrefs.getSequencesArray(), dataset);
-        }
-        copyAlignment
-                .setGapCharacter(alignFrame.viewport.getGapCharacter());
-
-        StructureSelectionManager ssm = StructureSelectionManager
-                .getStructureSelectionManager(Desktop.instance);
-
-        /*
-         * register any new mappings for sequence mouseover etc
-         * (will not duplicate any previously registered mappings)
-         */
-        ssm.registerMappings(dataset.getCodonFrames());
-
-        if (copyAlignment.getHeight() <= 0)
-        {
-          System.err.println(
-                  "No Sequences generated for xRef type " + source);
-          return;
-        }
-        /*
-         * align protein to dna
-         */
-        if (dna && copyAlignmentIsAligned)
-        {
-          xrefsAlignment.alignAs(copyAlignment);
-        }
-        else
-        {
-          /*
-           * align cdna to protein - currently only if 
-           * fetching and aligning Ensembl transcripts!
-           */
-          // TODO: generalise for other sources of locus/transcript/cds data
-          if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
-          {
-            copyAlignment.alignAs(xrefsAlignment);
-          }
+          return; // failed
         }
       }
+
       /*
        * build AlignFrame(s) according to available alignment data
        */
@@ -207,6 +161,7 @@ public class CrossRefAction implements Runnable
         xrefViews.add(newFrame.alignPanel);
         return; // via finally clause
       }
+
       AlignFrame copyThis = new AlignFrame(copyAlignment,
               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
       copyThis.setTitle(alignFrame.getTitle());
@@ -221,10 +176,14 @@ public class CrossRefAction implements Runnable
       /*
        * copy feature rendering settings to split frame
        */
-      newFrame.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
-              .transferSettings(myFeatureStyling);
-      copyThis.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
-              .transferSettings(myFeatureStyling);
+      FeatureRenderer fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas
+              .getFeatureRenderer();
+      fr1.transferSettings(myFeatureStyling);
+      fr1.findAllFeatures(true);
+      FeatureRenderer fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas
+              .getFeatureRenderer();
+      fr2.transferSettings(myFeatureStyling);
+      fr2.findAllFeatures(true);
 
       /*
        * apply 'database source' feature configuration
@@ -263,6 +222,260 @@ public class CrossRefAction implements Runnable
   }
 
   /**
+   * Tries to add chromosomal coordinates to any nucleotide sequence which does
+   * not already have them. Coordinates are retrieved from Ensembl given an
+   * Ensembl identifier, either on the sequence itself or on a peptide sequence
+   * it has a reference to.
+   * 
+   * <pre>
+   * Example (human):
+   * - fetch EMBLCDS cross-references for Uniprot entry P30419
+   * - the EMBL sequences do not have xrefs to Ensembl
+   * - the Uniprot entry has xrefs to 
+   *    ENSP00000258960, ENSP00000468424, ENST00000258960, ENST00000592782
+   * - either of the transcript ids can be used to retrieve gene loci e.g.
+   *    http://rest.ensembl.org/map/cds/ENST00000592782/1..100000
+   * Example (invertebrate):
+   * - fetch EMBLCDS cross-references for Uniprot entry Q43517 (FER1_SOLLC)
+   * - the Uniprot entry has an xref to ENSEMBLPLANTS Solyc10g044520.1.1
+   * - can retrieve gene loci with
+   *    http://rest.ensemblgenomes.org/map/cds/Solyc10g044520.1.1/1..100000
+   * </pre>
+   * 
+   * @param sequences
+   */
+  public static void findGeneLoci(List<SequenceI> sequences)
+  {
+    Map<DBRefEntry, GeneLociI> retrievedLoci = new HashMap<>();
+    for (SequenceI seq : sequences)
+    {
+      findGeneLoci(seq, retrievedLoci);
+    }
+  }
+
+  /**
+   * Tres to find chromosomal coordinates for the sequence, by searching its
+   * direct and indirect cross-references for Ensembl. If the loci have already
+   * been retrieved, just reads them out of the map of retrievedLoci; this is
+   * the case of an alternative transcript for the same protein. Otherwise calls
+   * a REST service to retrieve the loci, and if successful, adds them to the
+   * sequence and to the retrievedLoci.
+   * 
+   * @param seq
+   * @param retrievedLoci
+   */
+  static void findGeneLoci(SequenceI seq,
+          Map<DBRefEntry, GeneLociI> retrievedLoci)
+  {
+    /*
+     * don't replace any existing chromosomal coordinates
+     */
+    if (seq == null || seq.isProtein() || seq.getGeneLoci() != null
+            || seq.getDBRefs() == null)
+    {
+      return;
+    }
+    
+    Set<String> ensemblDivisions = new EnsemblInfo().getDivisions();
+    
+    /*
+     * first look for direct dbrefs from sequence to Ensembl
+     */
+    String[] divisionsArray = ensemblDivisions
+            .toArray(new String[ensemblDivisions.size()]);
+    DBRefEntry[] seqRefs = seq.getDBRefs();
+    DBRefEntry[] directEnsemblRefs = DBRefUtils.selectRefs(seqRefs,
+            divisionsArray);
+    if (directEnsemblRefs != null)
+    {
+      for (DBRefEntry ensemblRef : directEnsemblRefs)
+      {
+        if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
+        {
+          return;
+        }
+      }
+    }
+
+    /*
+     * else look for indirect dbrefs from sequence to Ensembl
+     */
+    for (DBRefEntry dbref : seq.getDBRefs())
+    {
+      if (dbref.getMap() != null && dbref.getMap().getTo() != null)
+      {
+        DBRefEntry[] dbrefs = dbref.getMap().getTo().getDBRefs();
+        DBRefEntry[] indirectEnsemblRefs = DBRefUtils.selectRefs(dbrefs,
+                divisionsArray);
+        if (indirectEnsemblRefs != null)
+        {
+          for (DBRefEntry ensemblRef : indirectEnsemblRefs)
+          {
+            if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
+            {
+              return;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Retrieves chromosomal coordinates for the Ensembl (or EnsemblGenomes)
+   * identifier in dbref. If successful, and the sequence length matches gene
+   * loci length, then add it to the sequence, and to the retrievedLoci map.
+   * Answers true if successful, else false.
+   * 
+   * @param seq
+   * @param dbref
+   * @param retrievedLoci
+   * @return
+   */
+  static boolean fetchGeneLoci(SequenceI seq, DBRefEntry dbref,
+          Map<DBRefEntry, GeneLociI> retrievedLoci)
+  {
+    String accession = dbref.getAccessionId();
+    String division = dbref.getSource();
+
+    /*
+     * hack: ignore cross-references to Ensembl protein ids
+     * (or use map/translation perhaps?)
+     * todo: is there an equivalent in EnsemblGenomes?
+     */
+    if (accession.startsWith("ENSP"))
+    {
+      return false;
+    }
+    EnsemblMap mapper = new EnsemblMap();
+
+    /*
+     * try CDS mapping first
+     */
+    GeneLociI geneLoci = mapper.getCdsMapping(division, accession, 1,
+            seq.getLength());
+    if (geneLoci != null)
+    {
+      MapList map = geneLoci.getMap();
+      int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
+      if (mappedFromLength == seq.getLength())
+      {
+        seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+                geneLoci.getChromosomeId(), geneLoci.getMap());
+        retrievedLoci.put(dbref, geneLoci);
+        return true;
+      }
+    }
+
+    /*
+     * else try CDNA mapping
+     */
+    geneLoci = mapper.getCdnaMapping(division, accession, 1,
+            seq.getLength());
+    if (geneLoci != null)
+    {
+      MapList map = geneLoci.getMap();
+      int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
+      if (mappedFromLength == seq.getLength())
+      {
+        seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+                geneLoci.getChromosomeId(), geneLoci.getMap());
+        retrievedLoci.put(dbref, geneLoci);
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * @param alignment
+   * @param dataset
+   * @param dna
+   * @param xrefs
+   * @param xrefsAlignment
+   * @return
+   */
+  protected AlignmentI copyAlignmentForSplitFrame(AlignmentI alignment,
+          AlignmentI dataset, boolean dna, AlignmentI xrefs,
+          AlignmentI xrefsAlignment)
+  {
+    AlignmentI copyAlignment;
+    boolean copyAlignmentIsAligned = false;
+    if (dna)
+    {
+      copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
+              xrefsAlignment.getSequencesArray());
+      if (copyAlignment.getHeight() == 0)
+      {
+        JvOptionPane.showMessageDialog(alignFrame,
+                MessageManager.getString("label.cant_map_cds"),
+                MessageManager.getString("label.operation_failed"),
+                JvOptionPane.OK_OPTION);
+        System.err.println("Failed to make CDS alignment");
+        return null;
+      }
+
+      /*
+       * pending getting Embl transcripts to 'align', 
+       * we are only doing this for Ensembl
+       */
+      // TODO proper criteria for 'can align as cdna'
+      if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
+              || AlignmentUtils.looksLikeEnsembl(alignment))
+      {
+        copyAlignment.alignAs(alignment);
+        copyAlignmentIsAligned = true;
+      }
+    }
+    else
+    {
+      copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
+              xrefs.getSequencesArray(), dataset);
+    }
+    copyAlignment
+            .setGapCharacter(alignFrame.viewport.getGapCharacter());
+
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+
+    /*
+     * register any new mappings for sequence mouseover etc
+     * (will not duplicate any previously registered mappings)
+     */
+    ssm.registerMappings(dataset.getCodonFrames());
+
+    if (copyAlignment.getHeight() <= 0)
+    {
+      System.err.println(
+              "No Sequences generated for xRef type " + source);
+      return null;
+    }
+
+    /*
+     * align protein to dna
+     */
+    if (dna && copyAlignmentIsAligned)
+    {
+      xrefsAlignment.alignAs(copyAlignment);
+    }
+    else
+    {
+      /*
+       * align cdna to protein - currently only if 
+       * fetching and aligning Ensembl transcripts!
+       */
+      // TODO: generalise for other sources of locus/transcript/cds data
+      if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
+      {
+        copyAlignment.alignAs(xrefsAlignment);
+      }
+    }
+
+    return copyAlignment;
+  }
+
+  /**
    * Makes an alignment containing the given sequences, and adds them to the
    * given dataset, which is also set as the dataset for the new alignment
    * 
@@ -291,20 +504,28 @@ public class CrossRefAction implements Runnable
     return al;
   }
 
-  public CrossRefAction(AlignFrame alignFrame, SequenceI[] sel,
-          boolean _odna, String source)
+  /**
+   * Constructor
+   * 
+   * @param af
+   * @param seqs
+   * @param fromDna
+   * @param dbSource
+   */
+  CrossRefAction(AlignFrame af, SequenceI[] seqs, boolean fromDna,
+          String dbSource)
   {
-    this.alignFrame = alignFrame;
-    this.sel = sel;
-    this._odna = _odna;
-    this.source = source;
+    this.alignFrame = af;
+    this.sel = seqs;
+    this._odna = fromDna;
+    this.source = dbSource;
   }
 
-  public static CrossRefAction showProductsFor(final SequenceI[] sel,
-          final boolean _odna, final String source,
+  public static CrossRefAction getHandlerFor(final SequenceI[] sel,
+          final boolean fromDna, final String source,
           final AlignFrame alignFrame)
   {
-    return new CrossRefAction(alignFrame, sel, _odna, source);
+    return new CrossRefAction(alignFrame, sel, fromDna, source);
   }
 
 }
index 71a1520..2e51bce 100644 (file)
@@ -141,6 +141,7 @@ public class CutAndPasteHtmlTransfer extends GCutAndPasteHtmlTransfer
    */
   public void setText(String text)
   {
+    textarea.setDocument(textarea.getEditorKit().createDefaultDocument());
     textarea.setText(text);
   }
 
diff --git a/src/jalview/gui/DasSourceBrowser.java b/src/jalview/gui/DasSourceBrowser.java
deleted file mode 100644 (file)
index 8570ac3..0000000
+++ /dev/null
@@ -1,864 +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.gui;
-
-import jalview.jbgui.GDasSourceBrowser;
-import jalview.util.MessageManager;
-import jalview.util.TableSorter;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-
-import java.awt.BorderLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Properties;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
-import javax.swing.JCheckBox;
-import javax.swing.JLabel;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JTextField;
-import javax.swing.ListSelectionModel;
-import javax.swing.SwingUtilities;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-import javax.swing.table.AbstractTableModel;
-
-import org.biodas.jdas.schema.sources.CAPABILITY;
-import org.biodas.jdas.schema.sources.COORDINATES;
-import org.biodas.jdas.schema.sources.PROP;
-import org.biodas.jdas.schema.sources.VERSION;
-
-public class DasSourceBrowser extends GDasSourceBrowser
-        implements Runnable, ListSelectionListener
-{
-  DasSourceRegistryI sourceRegistry = null;
-
-  Vector<String> selectedSources;
-
-  public DasSourceBrowser(FeatureSettings featureSettings)
-  {
-    fs = featureSettings;
-    // TODO DasSourceRegistryProvider API
-    sourceRegistry = jalview.bin.Cache.getDasSourceRegistry();
-    String registry = sourceRegistry.getDasRegistryURL();
-
-    registryURL.setText(registry);
-
-    setSelectedFromProperties();
-
-    displayFullDetails(null);
-    table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-
-    filter1.addListSelectionListener(this);
-    filter2.addListSelectionListener(this);
-    filter3.addListSelectionListener(this);
-
-    // Ask to be notified of selection changes.
-    ListSelectionModel rowSM = table.getSelectionModel();
-    rowSM.addListSelectionListener(new ListSelectionListener()
-    {
-      @Override
-      public void valueChanged(ListSelectionEvent e)
-      {
-        ListSelectionModel lsm = (ListSelectionModel) e.getSource();
-        if (!lsm.isSelectionEmpty())
-        {
-          int selectedRow = lsm.getMinSelectionIndex();
-          displayFullDetails(table.getValueAt(selectedRow, 0).toString());
-        }
-      }
-    });
-
-    table.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mouseClicked(MouseEvent evt)
-      {
-        if (evt.getClickCount() == 2 || evt.isPopupTrigger())
-        {
-          editRemoveLocalSource(evt);
-        }
-      }
-    });
-
-    if (sourceRegistry.getSources() != null)
-    {
-      init();
-    }
-  }
-
-  FeatureSettings fs = null;
-
-  private boolean loadingDasSources;
-
-  public DasSourceBrowser()
-  {
-    this(null);
-  }
-
-  @Override
-  public void paintComponent(java.awt.Graphics g)
-  {
-    if (sourceRegistry == null)
-    {
-      Thread worker = new Thread(this);
-      worker.start();
-    }
-  }
-
-  void init()
-  {
-    List<jalviewSourceI> sources = sourceRegistry.getSources();
-    int dSize = sources.size();
-    Object[][] data = new Object[dSize][2];
-    for (int i = 0; i < dSize; i++)
-    {
-      data[i][0] = sources.get(i).getTitle(); // what's equivalent of nickname
-      data[i][1] = new Boolean(
-              selectedSources.contains(sources.get(i).getTitle()));
-    }
-
-    refreshTableData(data);
-    setCapabilities(sourceRegistry);
-
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        TableSorter sorter = (TableSorter) table.getModel();
-        sorter.setSortingStatus(1, TableSorter.DESCENDING);
-        sorter.setSortingStatus(1, TableSorter.NOT_SORTED);
-      }
-    });
-
-    progressBar.setIndeterminate(false);
-    progressBar.setVisible(false);
-    addLocal.setVisible(true);
-    refresh.setVisible(true);
-  }
-
-  public void refreshTableData(Object[][] data)
-  {
-    TableSorter sorter = new TableSorter(new DASTableModel(data));
-    sorter.setTableHeader(table.getTableHeader());
-    table.setModel(sorter);
-  }
-
-  void displayFullDetails(String nickName)
-  {
-
-    StringBuffer text = new StringBuffer(
-            "<HTML><font size=\"2\" face=\"Verdana, Arial, Helvetica, sans-serif\">");
-
-    if (nickName == null)
-    {
-      fullDetails.setText(text + MessageManager
-              .getString("label.select_das_service_from_table"));
-      return;
-    }
-
-    int dSize = sourceRegistry.getSources().size();
-    for (jalviewSourceI ds : sourceRegistry.getSources())
-    {
-      if (!ds.getTitle().equals(nickName))
-      {
-        continue;
-      }
-
-      VERSION latest = ds.getVersion();
-      text.append(
-              "<font color=\"#0000FF\">Id:</font> " + ds.getUri() + "<br>");
-      text.append("<font color=\"#0000FF\">Nickname:</font> "
-              + ds.getTitle() + "<br>");
-
-      text.append("<font color=\"#0000FF\">URL:</font> <a href=\""
-              + ds.getSourceURL() + "\">" + ds.getSourceURL() + "</a>"
-              + "<br>");
-      if (!ds.isLocal())
-      {
-        if (ds.getDocHref() != null && ds.getDocHref().length() > 0)
-        {
-          text.append("<font color=\"#0000FF\">Site:</font> <a href=\""
-                  + ds.getDocHref() + "\">" + ds.getDocHref() + "</a>"
-                  + "<br>");
-        }
-
-        text.append("<font color=\"#0000FF\">Description:</font> "
-                + ds.getDescription() + "<br>");
-
-        text.append(
-                "<font color=\"#0000FF\">Admin Email:</font> <a href=\"mailto:"
-                        + ds.getEmail() + "\">" + ds.getEmail() + "</a>"
-                        + "<br>");
-
-        text.append("<font color=\"#0000FF\">Registered at:</font> "
-                + latest.getCreated() + "<br>");
-
-        // TODO: Identify last successful test date
-        // text.append("<font color=\"#0000FF\">Last successful test:</font> "
-        // + latest.dasSources[i].getLeaseDate() + "<br>");
-      }
-      else
-      {
-        text.append("Source was added manually.<br/>");
-      }
-      text.append("<font color=\"#0000FF\">Labels:</font> ");
-      boolean b = false;
-      for (PROP labl : latest.getPROP())
-      {
-        if (labl.getName().equalsIgnoreCase("LABEL"))
-        {
-          if (b)
-          {
-            text.append(",");
-          }
-          text.append(" ");
-
-          text.append(labl.getValue());
-          b = true;
-        }
-        ;
-      }
-      text.append("<br>");
-
-      text.append("<font color=\"#0000FF\">Capabilities:</font> ");
-      CAPABILITY[] scap = latest.getCAPABILITY().toArray(new CAPABILITY[0]);
-      for (int j = 0; j < scap.length; j++)
-      {
-        text.append(scap[j].getType());
-        if (j < scap.length - 1)
-        {
-          text.append(", ");
-        }
-      }
-      text.append("<br>");
-
-      text.append("<font color=\"#0000FF\">Coordinates:</font>");
-      int i = 1;
-      for (COORDINATES dcs : latest.getCOORDINATES())
-      {
-        text.append("<br/>" + i++ + ". ");
-        text.append(dcs.getAuthority() + " : " + dcs.getSource());
-        if (dcs.getTaxid() != null && dcs.getTaxid().trim().length() > 0)
-        {
-          text.append(" [TaxId:" + dcs.getTaxid() + "]");
-        }
-        if (dcs.getVersion() != null
-                && dcs.getVersion().trim().length() > 0)
-        {
-          {
-            text.append(" {v. " + dcs.getVersion() + "}");
-          }
-        }
-        text.append(" (<a href=\"" + dcs.getUri() + "\">" + dcs.getUri()
-                + "</a>)");
-      }
-      text.append("</font></html>");
-
-      break;
-    }
-
-    fullDetails.setText(text.toString());
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        fullDetailsScrollpane.getVerticalScrollBar().setValue(0);
-      }
-    });
-  }
-
-  @Override
-  public void run()
-  {
-    loadingDasSources = true;
-
-    addLocal.setVisible(false);
-    refresh.setVisible(false);
-    progressBar.setVisible(true);
-    progressBar.setIndeterminate(true);
-    setParentGuiEnabled(false);
-    // Refresh the source list.
-    sourceRegistry.refreshSources();
-
-    init();
-
-    setParentGuiEnabled(true);
-    loadingDasSources = false;
-
-  }
-
-  private void setParentGuiEnabled(boolean b)
-  {
-    if (fs != null)
-    {
-      fs.fetchDAS.setEnabled(b);
-      fs.saveDAS.setEnabled(b);
-    }
-  }
-
-  public Vector<jalviewSourceI> getSelectedSources()
-  {
-    // wait around if we're still loading.
-    while (sourceRegistry == null)
-    {
-      if (!loadingDasSources)
-      {
-        new Thread(this).start();
-        try
-        {
-          Thread.sleep(5);
-        } catch (Exception e)
-        {
-        }
-        ;
-        while (loadingDasSources)
-        {
-          try
-          {
-            Thread.sleep(5);
-          } catch (Exception e)
-          {
-          }
-          ;
-        }
-        ;
-      }
-    }
-
-    Vector<jalviewSourceI> selected = new Vector<jalviewSourceI>();
-    for (String source : selectedSources)
-    {
-      jalviewSourceI srce = sourceRegistry.getSource(source);
-      if (srce != null)
-      {
-        selected.addElement(srce);
-      }
-    }
-    return selected;
-  }
-
-  @Override
-  public void refresh_actionPerformed(ActionEvent e)
-  {
-    saveProperties(jalview.bin.Cache.applicationProperties);
-
-    Thread worker = new Thread(this);
-    worker.start();
-  }
-
-  private void setCapabilities(DasSourceRegistryI sourceRegistry2)
-  {
-    Vector<String> authority = new Vector<String>();
-    Vector<String> type = new Vector<String>();
-    Vector<String> label = new Vector<String>();
-    Vector<String> taxIds = new Vector<String>();
-    authority.add("Any");
-    type.add("Any");
-    label.add("Any");
-
-    for (jalviewSourceI ds : sourceRegistry2.getSources())
-    {
-      VERSION latest = ds.getVersion();
-
-      for (COORDINATES cs : latest.getCOORDINATES())
-      {
-        if (!type.contains(cs.getSource()))
-        {
-          type.add(cs.getSource()); // source==category
-        }
-
-        if (!authority.contains(cs.getAuthority()))
-        {
-          authority.add(cs.getAuthority());
-        }
-      }
-
-      for (PROP slabel : latest.getPROP())
-      {
-        if (slabel.getName().equalsIgnoreCase("LABEL")
-                && !label.contains(slabel.getValue()))
-        {
-          label.add(slabel.getValue());
-        }
-      }
-
-    }
-
-    filter1.setListData(authority);
-    filter2.setListData(type);
-    filter3.setListData(label);
-    // filter4 taxIds
-
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        filter1.setSelectedIndex(0);
-        filter2.setSelectedIndex(0);
-        filter3.setSelectedIndex(0);
-      }
-    });
-  }
-
-  @Override
-  public void amendLocal(boolean newSource)
-  {
-    String url = "http://localhost:8080/", nickname = "";
-    boolean seqsrc = false;
-    if (!newSource)
-    {
-      int selectedRow = table.getSelectionModel().getMinSelectionIndex();
-      nickname = table.getValueAt(selectedRow, 0).toString();
-      jalviewSourceI source = sourceRegistry.getSource(nickname);
-      url = source.getUri();
-      seqsrc = source.isSequenceSource();
-    }
-
-    JTextField nametf = new JTextField(nickname, 40);
-    JTextField urltf = new JTextField(url, 40);
-    JCheckBox seqs = new JCheckBox(
-            MessageManager.getString("label.sequence_source"));
-    seqs.setSelected(seqsrc);
-    JPanel panel = new JPanel(new BorderLayout());
-    JPanel pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.name:")),
-            BorderLayout.CENTER);
-    pane12.add(nametf, BorderLayout.EAST);
-    panel.add(pane12, BorderLayout.NORTH);
-    pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.url:")),
-            BorderLayout.NORTH);
-    pane12.add(seqs, BorderLayout.SOUTH);
-    pane12.add(urltf, BorderLayout.EAST);
-    panel.add(pane12, BorderLayout.SOUTH);
-
-    int reply = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-            panel, MessageManager.getString("label.enter_local_das_source"),
-            JvOptionPane.OK_CANCEL_OPTION);
-
-    if (reply != JvOptionPane.OK_OPTION)
-    {
-      return;
-    }
-
-    if (!urltf.getText().endsWith("/"))
-    {
-      urltf.setText(urltf.getText() + "/");
-    }
-
-    jalviewSourceI local = sourceRegistry.createLocalSource(urltf.getText(),
-            nametf.getText(), seqs.isSelected(), true);
-    List sources = sourceRegistry.getSources();
-    int osize = sources.size();
-    int size = osize + (newSource ? 1 : 0);
-
-    Object[][] data = new Object[size][2];
-    DASTableModel dtm = (table != null)
-            ? (DASTableModel) ((TableSorter) table.getModel())
-                    .getTableModel()
-            : null;
-    for (int i = 0; i < osize; i++)
-    {
-      String osrc = (dtm == null || i >= osize) ? null
-              : (String) dtm.getValueAt(i, 0);
-      if (!newSource && osrc != null
-              && dtm.getValueAt(i, 0).equals(nickname))
-      {
-        data[i][0] = local.getTitle();
-        data[i][1] = new Boolean(true);
-      }
-      else
-      {
-        data[i][0] = osrc;
-        data[i][1] = new Boolean(selectedSources.contains(osrc));
-      }
-    }
-    // Always add a new source at the end
-    if (newSource)
-    {
-      data[osize][0] = local.getTitle();
-      data[osize][1] = new Boolean(true);
-      selectedSources.add(local.getTitle());
-    }
-
-    refreshTableData(data);
-
-    SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        scrollPane.getVerticalScrollBar()
-                .setValue(scrollPane.getVerticalScrollBar().getMaximum());
-      }
-    });
-
-    displayFullDetails(local.getTitle());
-  }
-
-  public void editRemoveLocalSource(MouseEvent evt)
-  {
-    int selectedRow = table.getSelectionModel().getMinSelectionIndex();
-    if (selectedRow == -1)
-    {
-      return;
-    }
-
-    String nickname = table.getValueAt(selectedRow, 0).toString();
-
-    if (!sourceRegistry.getSource(nickname).isLocal())
-    {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString(
-                      "label.you_can_only_edit_or_remove_local_das_sources"),
-              MessageManager.getString("label.public_das_source"),
-              JvOptionPane.WARNING_MESSAGE);
-      return;
-    }
-
-    Object[] options = { "Edit", "Remove", "Cancel" };
-    int choice = JvOptionPane.showInternalOptionDialog(Desktop.desktop,
-            "Do you want to edit or remove " + nickname + "?",
-            "Edit / Remove Local DAS Source",
-            JvOptionPane.YES_NO_CANCEL_OPTION,
-            JvOptionPane.QUESTION_MESSAGE, null, options, options[2]);
-
-    switch (choice)
-    {
-    case 0:
-      amendLocal(false);
-      break;
-    case 1:
-      sourceRegistry.removeLocalSource(sourceRegistry.getSource(nickname));
-      selectedSources.remove(nickname);
-      Object[][] data = new Object[sourceRegistry.getSources().size()][2];
-      int index = 0, l = table.getRowCount();
-
-      for (int i = 0; i < l; i++)
-      {
-        String nm;
-        if ((nm = (String) table.getValueAt(i, 0)).equals(nickname))
-        {
-          continue;
-        }
-        else
-        {
-          data[index][0] = nm;
-          data[index][1] = new Boolean(selectedSources.contains(nm));
-          index++;
-        }
-      }
-      refreshTableData(data);
-      SwingUtilities.invokeLater(new Runnable()
-      {
-        @Override
-        public void run()
-        {
-          scrollPane.getVerticalScrollBar()
-                  .setValue(scrollPane.getVerticalScrollBar().getMaximum());
-        }
-      });
-
-      break;
-    }
-  }
-
-  @Override
-  public void valueChanged(ListSelectionEvent evt)
-  {
-    // Called when the MainTable selection changes
-    if (evt.getValueIsAdjusting())
-    {
-      return;
-    }
-
-    displayFullDetails(null);
-
-    // Filter the displayed data sources
-
-    ArrayList names = new ArrayList();
-    ArrayList selected = new ArrayList();
-
-    // The features filter is not visible, but we must still
-    // filter the das source list here.
-    // July 2006 - only 6 sources fo not serve features
-    Object[] dummyFeatureList = new Object[] { "features" };
-    List<jalviewSourceI> srcs = sourceRegistry.getSources();
-    for (jalviewSourceI ds : srcs)
-    {
-
-      VERSION v = ds.getVersion();
-      List<COORDINATES> coords = v.getCOORDINATES();
-      if (ds.isLocal() || ((coords == null || coords.size() == 0)
-              && filter1.getSelectedIndex() == 0
-              && filter2.getSelectedIndex() == 0
-              && filter3.getSelectedIndex() == 0))
-      {
-        // THIS IS A FIX FOR LOCAL SOURCES WHICH DO NOT
-        // HAVE COORDINATE SYSTEMS, INFO WHICH AT PRESENT
-        // IS ADDED FROM THE REGISTRY
-        names.add(ds.getTitle());
-        selected.add(new Boolean(selectedSources.contains(ds.getTitle())));
-        continue;
-      }
-
-      if (!selectedInList(dummyFeatureList, ds.getCapabilityList(v))
-              || !selectedInList(filter3.getSelectedValues(),
-                      ds.getLabelsFor(v)))
-      {
-        continue;
-      }
-
-      for (int j = 0; j < coords.size(); j++)
-      {
-        if (selectedInList(filter1.getSelectedValues(),
-                new String[]
-                { coords.get(j).getAuthority() })
-                && selectedInList(filter2.getSelectedValues(), new String[]
-                { coords.get(j).getSource() }))
-        {
-          names.add(ds.getTitle());
-          selected.add(
-                  new Boolean(selectedSources.contains(ds.getTitle())));
-          break;
-        }
-      }
-    }
-
-    int dSize = names.size();
-    Object[][] data = new Object[dSize][2];
-    for (int d = 0; d < dSize; d++)
-    {
-      data[d][0] = names.get(d);
-      data[d][1] = selected.get(d);
-    }
-
-    refreshTableData(data);
-  }
-
-  private boolean selectedInList(Object[] selection, String[] items)
-  {
-    for (int i = 0; i < selection.length; i++)
-    {
-      if (selection[i].equals("Any"))
-      {
-        return true;
-      }
-      if (items == null || items.length == 0)
-      {
-        return false;
-      }
-      String sel = (items[0].startsWith("das1:") ? "das1:" : "")
-              + selection[i];
-      for (int j = 0; j < items.length; j++)
-      {
-        if (sel.equals(items[j]))
-        {
-          return true;
-        }
-      }
-    }
-
-    return false;
-  }
-
-  void setSelectedFromProperties()
-  {
-    String active = jalview.bin.Cache.getDefault("DAS_ACTIVE_SOURCE",
-            "uniprot");
-    StringTokenizer st = new StringTokenizer(active, "\t");
-    selectedSources = new Vector();
-    while (st.hasMoreTokens())
-    {
-      selectedSources.addElement(st.nextToken());
-    }
-  }
-
-  @Override
-  public void reset_actionPerformed(ActionEvent e)
-  {
-    registryURL.setText(sourceRegistry.getDasRegistryURL());
-  }
-
-  /**
-   * set the DAS source settings in the given jalview properties.
-   * 
-   * @param properties
-   */
-  public void saveProperties(Properties properties)
-  {
-    if (registryURL.getText() == null || registryURL.getText().length() < 1)
-    {
-      properties.remove(jalview.bin.Cache.DAS_REGISTRY_URL);
-    }
-    else
-    {
-      properties.setProperty(jalview.bin.Cache.DAS_REGISTRY_URL,
-              registryURL.getText());
-    }
-
-    StringBuffer sb = new StringBuffer();
-    for (int r = 0; r < table.getModel().getRowCount(); r++)
-    {
-      if (((Boolean) table.getValueAt(r, 1)).booleanValue())
-      {
-        sb.append(table.getValueAt(r, 0) + "\t");
-      }
-    }
-
-    properties.setProperty(jalview.bin.Cache.DAS_ACTIVE_SOURCE,
-            sb.toString());
-
-    String sourceprop = sourceRegistry.getLocalSourceString();
-    properties.setProperty(jalview.bin.Cache.DAS_LOCAL_SOURCE, sourceprop);
-  }
-
-  class DASTableModel extends AbstractTableModel
-  {
-
-    public DASTableModel(Object[][] data)
-    {
-      this.data = data;
-    }
-
-    private String[] columnNames = new String[] {
-        MessageManager.getString("label.nickname"),
-        MessageManager.getString("label.use_source") };
-
-    private Object[][] data;
-
-    @Override
-    public int getColumnCount()
-    {
-      return columnNames.length;
-    }
-
-    @Override
-    public int getRowCount()
-    {
-      return data.length;
-    }
-
-    @Override
-    public String getColumnName(int col)
-    {
-      return columnNames[col];
-    }
-
-    @Override
-    public Object getValueAt(int row, int col)
-    {
-      return data[row][col];
-    }
-
-    /*
-     * JTable uses this method to determine the default renderer/ editor for
-     * each cell. If we didn't implement this method, then the last column would
-     * contain text ("true"/"false"), rather than a check box.
-     */
-    @Override
-    public Class getColumnClass(int c)
-    {
-      return getValueAt(0, c).getClass();
-    }
-
-    /*
-     * Don't need to implement this method unless your table's editable.
-     */
-    @Override
-    public boolean isCellEditable(int row, int col)
-    {
-      // Note that the data/cell address is constant,
-      // no matter where the cell appears onscreen.
-      return col == 1;
-
-    }
-
-    /*
-     * Don't need to implement this method unless your table's data can change.
-     */
-    @Override
-    public void setValueAt(Object value, int row, int col)
-    {
-      data[row][col] = value;
-      fireTableCellUpdated(row, col);
-
-      String name = getValueAt(row, 0).toString();
-      boolean selected = ((Boolean) value).booleanValue();
-
-      if (selectedSources.contains(name) && !selected)
-      {
-        selectedSources.remove(name);
-      }
-
-      if (!selectedSources.contains(name) && selected)
-      {
-        selectedSources.add(name);
-      }
-    }
-  }
-
-  public void initDasSources()
-  {
-
-    Thread thr = new Thread(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        // this actually initialises the das source list
-        paintComponent(null); // yuk
-      }
-    });
-    thr.start();
-    while (loadingDasSources || sourceRegistry == null)
-    {
-      try
-      {
-        Thread.sleep(10);
-      } catch (Exception e)
-      {
-      }
-      ;
-    }
-  }
-
-  /**
-   * disable or enable the buttons on the source browser
-   * 
-   * @param b
-   */
-  public void setGuiEnabled(boolean b)
-  {
-    refresh.setEnabled(b);
-    addLocal.setEnabled(b);
-  }
-}
index 259e693..8d9e366 100644 (file)
@@ -69,6 +69,7 @@ import java.awt.dnd.DropTargetEvent;
 import java.awt.dnd.DropTargetListener;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
@@ -92,10 +93,13 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.Semaphore;
 
 import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ActionMap;
 import javax.swing.Box;
 import javax.swing.BoxLayout;
 import javax.swing.DefaultDesktopManager;
 import javax.swing.DesktopManager;
+import javax.swing.InputMap;
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
@@ -904,8 +908,6 @@ public class Desktop extends jalview.jbgui.GDesktop
           menuItem.removeActionListener(menuItem.getActionListeners()[0]);
         }
         windowMenu.remove(menuItem);
-
-        System.gc();
       };
     });
 
@@ -925,6 +927,8 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     });
 
+    setKeyBindings(frame);
+
     desktop.add(frame);
 
     windowMenu.add(menuItem);
@@ -944,6 +948,42 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
   }
 
+  /**
+   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close
+   * the window
+   * 
+   * @param frame
+   */
+  private static void setKeyBindings(JInternalFrame frame)
+  {
+    @SuppressWarnings("serial")
+    final Action closeAction = new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        frame.dispose();
+      }
+    };
+
+    /*
+     * set up key bindings for Ctrl-W and Cmd-W, with the same (Close) action
+     */
+    KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
+            InputEvent.CTRL_DOWN_MASK);
+    KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
+            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
+
+    InputMap inputMap = frame
+            .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
+    String ctrlW = ctrlWKey.toString();
+    inputMap.put(ctrlWKey, ctrlW);
+    inputMap.put(cmdWKey, ctrlW);
+
+    ActionMap actionMap = frame.getActionMap();
+    actionMap.put(ctrlW, closeAction);
+  }
+
   @Override
   public void lostOwnership(Clipboard clipboard, Transferable contents)
   {
@@ -1393,7 +1433,6 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       ssm.resetAll();
     }
-    System.gc();
   }
 
   @Override
diff --git a/src/jalview/gui/FeatureColourChooser.java b/src/jalview/gui/FeatureColourChooser.java
deleted file mode 100644 (file)
index d8db546..0000000
+++ /dev/null
@@ -1,632 +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.gui;
-
-import jalview.api.FeatureColourI;
-import jalview.datamodel.GraphLine;
-import jalview.schemes.FeatureColour;
-import jalview.util.MessageManager;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-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;
-
-import javax.swing.BorderFactory;
-import javax.swing.JCheckBox;
-import javax.swing.JColorChooser;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSlider;
-import javax.swing.JTextField;
-import javax.swing.border.LineBorder;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-
-public class FeatureColourChooser extends JalviewDialog
-{
-  // FeatureSettings fs;
-  private FeatureRenderer fr;
-
-  private FeatureColourI cs;
-
-  private FeatureColourI oldcs;
-
-  private AlignmentPanel ap;
-
-  private boolean adjusting = false;
-
-  final private float min;
-
-  final private float max;
-
-  final private float scaleFactor;
-
-  private String type = null;
-
-  private JPanel minColour = new JPanel();
-
-  private JPanel maxColour = new JPanel();
-
-  private JComboBox<String> threshold = new JComboBox<>();
-
-  private JSlider slider = new JSlider();
-
-  private JTextField thresholdValue = new JTextField(20);
-
-  // TODO implement GUI for tolower flag
-  // JCheckBox toLower = new JCheckBox();
-
-  private JCheckBox thresholdIsMin = new JCheckBox();
-
-  private JCheckBox colourByLabel = new JCheckBox();
-
-  private GraphLine threshline;
-
-  private Color oldmaxColour;
-
-  private Color oldminColour;
-
-  private ActionListener colourEditor = null;
-
-  /**
-   * Constructor
-   * 
-   * @param frender
-   * @param theType
-   */
-  public FeatureColourChooser(FeatureRenderer frender, String theType)
-  {
-    this(frender, false, theType);
-  }
-
-  /**
-   * Constructor, with option to make a blocking dialog (has to complete in the
-   * AWT event queue thread). Currently this option is always set to false.
-   * 
-   * @param frender
-   * @param blocking
-   * @param theType
-   */
-  FeatureColourChooser(FeatureRenderer frender, boolean blocking,
-          String theType)
-  {
-    this.fr = frender;
-    this.type = theType;
-    ap = fr.ap;
-    String title = MessageManager
-            .formatMessage("label.graduated_color_for_params", new String[]
-            { theType });
-    initDialogFrame(this, true, blocking, title, 480, 185);
-
-    slider.addChangeListener(new ChangeListener()
-    {
-      @Override
-      public void stateChanged(ChangeEvent evt)
-      {
-        if (!adjusting)
-        {
-          thresholdValue.setText((slider.getValue() / scaleFactor) + "");
-          sliderValueChanged();
-        }
-      }
-    });
-    slider.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mouseReleased(MouseEvent evt)
-      {
-        /*
-         * only update Overview and/or structure colouring
-         * when threshold slider drag ends (mouse up)
-         */
-        if (ap != null)
-        {
-          ap.paintAlignment(true, true);
-        }
-      }
-    });
-
-    float mm[] = fr.getMinMax().get(theType)[0];
-    min = mm[0];
-    max = mm[1];
-
-    /*
-     * ensure scale factor allows a scaled range with
-     * 10 integer divisions ('ticks'); if we have got here,
-     * we should expect that max != min
-     */
-    scaleFactor = (max == min) ? 1f : 100f / (max - min);
-
-    oldcs = fr.getFeatureColours().get(theType);
-    if (!oldcs.isSimpleColour())
-    {
-      if (oldcs.isAutoScaled())
-      {
-        // update the scale
-        cs = new FeatureColour((FeatureColour) oldcs, min, max);
-      }
-      else
-      {
-        cs = new FeatureColour((FeatureColour) oldcs);
-      }
-    }
-    else
-    {
-      // promote original color to a graduated color
-      Color bl = oldcs.getColour();
-      if (bl == null)
-      {
-        bl = Color.BLACK;
-      }
-      // original colour becomes the maximum colour
-      cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
-      cs.setColourByLabel(false);
-    }
-    minColour.setBackground(oldminColour = cs.getMinColour());
-    maxColour.setBackground(oldmaxColour = cs.getMaxColour());
-    adjusting = true;
-
-    try
-    {
-      jbInit();
-    } catch (Exception ex)
-    {
-    }
-    // update the gui from threshold state
-    thresholdIsMin.setSelected(!cs.isAutoScaled());
-    colourByLabel.setSelected(cs.isColourByLabel());
-    if (cs.hasThreshold())
-    {
-      // initialise threshold slider and selector
-      threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
-      slider.setEnabled(true);
-      slider.setValue((int) (cs.getThreshold() * scaleFactor));
-      thresholdValue.setEnabled(true);
-      threshline = new GraphLine((max - min) / 2f, "Threshold",
-              Color.black);
-      threshline.value = cs.getThreshold();
-    }
-
-    adjusting = false;
-
-    changeColour(false);
-    waitForInput();
-  }
-
-  private void jbInit() throws Exception
-  {
-
-    minColour.setFont(JvSwingUtils.getLabelFont());
-    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    minColour.setPreferredSize(new Dimension(40, 20));
-    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
-    minColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (minColour.isEnabled())
-        {
-          minColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setFont(JvSwingUtils.getLabelFont());
-    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    maxColour.setPreferredSize(new Dimension(40, 20));
-    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
-    maxColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (maxColour.isEnabled())
-        {
-          maxColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setBorder(new LineBorder(Color.black));
-    JLabel minText = new JLabel(MessageManager.getString("label.min"));
-    minText.setFont(JvSwingUtils.getLabelFont());
-    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
-    maxText.setFont(JvSwingUtils.getLabelFont());
-    this.setLayout(new BorderLayout());
-    JPanel jPanel1 = new JPanel();
-    jPanel1.setBackground(Color.white);
-    JPanel jPanel2 = new JPanel();
-    jPanel2.setLayout(new FlowLayout());
-    jPanel2.setBackground(Color.white);
-    threshold.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        threshold_actionPerformed();
-      }
-    });
-    threshold.setToolTipText(MessageManager
-            .getString("label.threshold_feature_display_by_score"));
-    threshold.addItem(MessageManager
-            .getString("label.threshold_feature_no_threshold")); // index 0
-    threshold.addItem(MessageManager
-            .getString("label.threshold_feature_above_threshold")); // index 1
-    threshold.addItem(MessageManager
-            .getString("label.threshold_feature_below_threshold")); // index 2
-
-    JPanel jPanel3 = new JPanel();
-    jPanel3.setLayout(new FlowLayout());
-    thresholdValue.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        thresholdValue_actionPerformed();
-      }
-    });
-    thresholdValue.addFocusListener(new FocusAdapter()
-    {
-      @Override
-      public void focusLost(FocusEvent e)
-      {
-        thresholdValue_actionPerformed();
-      }
-    });
-    slider.setPaintLabels(false);
-    slider.setPaintTicks(true);
-    slider.setBackground(Color.white);
-    slider.setEnabled(false);
-    slider.setOpaque(false);
-    slider.setPreferredSize(new Dimension(100, 32));
-    slider.setToolTipText(
-            MessageManager.getString("label.adjust_threshold"));
-    thresholdValue.setEnabled(false);
-    thresholdValue.setColumns(7);
-    jPanel3.setBackground(Color.white);
-    thresholdIsMin.setBackground(Color.white);
-    thresholdIsMin
-            .setText(MessageManager.getString("label.threshold_minmax"));
-    thresholdIsMin.setToolTipText(MessageManager
-            .getString("label.toggle_absolute_relative_display_threshold"));
-    thresholdIsMin.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        thresholdIsMin_actionPerformed();
-      }
-    });
-    colourByLabel.setBackground(Color.white);
-    colourByLabel
-            .setText(MessageManager.getString("label.colour_by_label"));
-    colourByLabel.setToolTipText(MessageManager.getString(
-            "label.display_features_same_type_different_label_using_different_colour"));
-    colourByLabel.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        colourByLabel_actionPerformed();
-      }
-    });
-
-    JPanel colourPanel = new JPanel();
-    colourPanel.setBackground(Color.white);
-    jPanel1.add(ok);
-    jPanel1.add(cancel);
-    jPanel2.add(colourByLabel, BorderLayout.WEST);
-    jPanel2.add(colourPanel, BorderLayout.EAST);
-    colourPanel.add(minText);
-    colourPanel.add(minColour);
-    colourPanel.add(maxText);
-    colourPanel.add(maxColour);
-    this.add(jPanel3, BorderLayout.CENTER);
-    jPanel3.add(threshold);
-    jPanel3.add(slider);
-    jPanel3.add(thresholdValue);
-    jPanel3.add(thresholdIsMin);
-    this.add(jPanel1, BorderLayout.SOUTH);
-    this.add(jPanel2, BorderLayout.NORTH);
-  }
-
-  /**
-   * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
-   * set the selected colour (if the user does not cancel out of the dialog)
-   */
-  protected void minColour_actionPerformed()
-  {
-    Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_colour_minimum_value"),
-            minColour.getBackground());
-    if (col != null)
-    {
-      minColour.setBackground(col);
-      minColour.setForeground(col);
-    }
-    minColour.repaint();
-    changeColour(true);
-  }
-
-  /**
-   * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
-   * set the selected colour (if the user does not cancel out of the dialog)
-   */
-  protected void maxColour_actionPerformed()
-  {
-    Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_colour_maximum_value"),
-            maxColour.getBackground());
-    if (col != null)
-    {
-      maxColour.setBackground(col);
-      maxColour.setForeground(col);
-    }
-    maxColour.repaint();
-    changeColour(true);
-  }
-
-  /**
-   * Constructs and sets the selected colour options as the colour for the
-   * feature type, and repaints the alignment, and optionally the Overview
-   * and/or structure viewer if open
-   * 
-   * @param updateStructsAndOverview
-   */
-  void changeColour(boolean updateStructsAndOverview)
-  {
-    // Check if combobox is still adjusting
-    if (adjusting)
-    {
-      return;
-    }
-
-    boolean aboveThreshold = false;
-    boolean belowThreshold = false;
-    if (threshold.getSelectedIndex() == 1)
-    {
-      aboveThreshold = true;
-    }
-    else if (threshold.getSelectedIndex() == 2)
-    {
-      belowThreshold = true;
-    }
-    boolean hasThreshold = aboveThreshold || belowThreshold;
-
-    slider.setEnabled(true);
-    thresholdValue.setEnabled(true);
-
-    FeatureColourI acg;
-    if (cs.isColourByLabel())
-    {
-      acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
-    }
-    else
-    {
-      acg = new FeatureColour(oldminColour = minColour.getBackground(),
-              oldmaxColour = maxColour.getBackground(), min, max);
-    }
-
-    if (!hasThreshold)
-    {
-      slider.setEnabled(false);
-      thresholdValue.setEnabled(false);
-      thresholdValue.setText("");
-      thresholdIsMin.setEnabled(false);
-    }
-    else if (threshline == null)
-    {
-      /*
-       * todo not yet implemented: visual indication of feature threshold
-       */
-      threshline = new GraphLine((max - min) / 2f, "Threshold",
-              Color.black);
-    }
-
-    if (hasThreshold)
-    {
-      adjusting = true;
-      acg.setThreshold(threshline.value);
-
-      float range = (max - min) * scaleFactor;
-
-      slider.setMinimum((int) (min * scaleFactor));
-      slider.setMaximum((int) (max * scaleFactor));
-      // slider.setValue((int) (threshline.value * scaleFactor));
-      slider.setValue(Math.round(threshline.value * scaleFactor));
-      thresholdValue.setText(threshline.value + "");
-      slider.setMajorTickSpacing((int) (range / 10f));
-      slider.setEnabled(true);
-      thresholdValue.setEnabled(true);
-      thresholdIsMin.setEnabled(!colourByLabel.isSelected());
-      adjusting = false;
-    }
-
-    acg.setAboveThreshold(aboveThreshold);
-    acg.setBelowThreshold(belowThreshold);
-    if (thresholdIsMin.isSelected() && hasThreshold)
-    {
-      acg.setAutoScaled(false);
-      if (aboveThreshold)
-      {
-        acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
-      }
-      else
-      {
-        acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
-      }
-    }
-    else
-    {
-      acg.setAutoScaled(true);
-    }
-    acg.setColourByLabel(colourByLabel.isSelected());
-    if (acg.isColourByLabel())
-    {
-      maxColour.setEnabled(false);
-      minColour.setEnabled(false);
-      maxColour.setBackground(this.getBackground());
-      maxColour.setForeground(this.getBackground());
-      minColour.setBackground(this.getBackground());
-      minColour.setForeground(this.getBackground());
-
-    }
-    else
-    {
-      maxColour.setEnabled(true);
-      minColour.setEnabled(true);
-      maxColour.setBackground(oldmaxColour);
-      minColour.setBackground(oldminColour);
-      maxColour.setForeground(oldmaxColour);
-      minColour.setForeground(oldminColour);
-    }
-    fr.setColour(type, acg);
-    cs = acg;
-    ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
-  }
-
-  @Override
-  protected void raiseClosed()
-  {
-    if (this.colourEditor != null)
-    {
-      colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
-    }
-  }
-
-  @Override
-  public void okPressed()
-  {
-    changeColour(false);
-  }
-
-  @Override
-  public void cancelPressed()
-  {
-    reset();
-  }
-
-  /**
-   * Action when the user cancels the dialog. All previous settings should be
-   * restored and rendered on the alignment, and any linked Overview window or
-   * structure.
-   */
-  void reset()
-  {
-    fr.setColour(type, oldcs);
-    ap.paintAlignment(true, true);
-    cs = null;
-  }
-
-  /**
-   * Action on change of choice of No / Above / Below Threshold
-   */
-  protected void threshold_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  /**
-   * Action on text entry of a threshold value
-   */
-  protected void thresholdValue_actionPerformed()
-  {
-    try
-    {
-      float f = Float.parseFloat(thresholdValue.getText());
-      slider.setValue((int) (f * scaleFactor));
-      threshline.value = f;
-
-      /*
-       * force repaint of any Overview window or structure
-       */
-      ap.paintAlignment(true, true);
-    } catch (NumberFormatException ex)
-    {
-    }
-  }
-
-  /**
-   * Action on change of threshold slider value. This may be done interactively
-   * (by moving the slider), or programmatically (to update the slider after
-   * manual input of a threshold value).
-   */
-  protected void sliderValueChanged()
-  {
-    /*
-     * squash rounding errors by forcing min/max of slider to 
-     * actual min/max of feature score range
-     */
-    int value = slider.getValue();
-    threshline.value = value == slider.getMaximum() ? max
-            : (value == slider.getMinimum() ? min : value / scaleFactor);
-    cs.setThreshold(threshline.value);
-
-    /*
-     * repaint alignment, but not Overview or structure,
-     * to avoid overload while dragging the slider
-     */
-    changeColour(false);
-  }
-
-  protected void thresholdIsMin_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  protected void colourByLabel_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  void addActionListener(ActionListener graduatedColorEditor)
-  {
-    if (colourEditor != null)
-    {
-      System.err.println(
-              "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
-    }
-    colourEditor = graduatedColorEditor;
-  }
-
-  /**
-   * Answers the last colour setting selected by user - either oldcs (which may
-   * be a java.awt.Color) or the new GraduatedColor
-   * 
-   * @return
-   */
-  FeatureColourI getLastColour()
-  {
-    if (cs == null)
-    {
-      return oldcs;
-    }
-    return cs;
-  }
-
-}
index 9c4b009..46f574e 100644 (file)
@@ -180,15 +180,15 @@ public class FeatureRenderer
     final JSpinner end = new JSpinner();
     start.setPreferredSize(new Dimension(80, 20));
     end.setPreferredSize(new Dimension(80, 20));
-    final FeatureRenderer me = this;
     final JLabel colour = new JLabel();
     colour.setOpaque(true);
     // colour.setBorder(BorderFactory.createEtchedBorder());
     colour.setMaximumSize(new Dimension(30, 16));
     colour.addMouseListener(new MouseAdapter()
     {
-      FeatureColourChooser fcc = null;
-
+      /*
+       * open colour chooser on click in colour panel
+       */
       @Override
       public void mousePressed(MouseEvent evt)
       {
@@ -205,28 +205,26 @@ public class FeatureRenderer
         }
         else
         {
-          if (fcc == null)
+          /*
+           * variable colour dialog - on OK, refetch the updated
+           * feature colour and update this display
+           */
+          final String ft = features.get(featureIndex).getType();
+          final String type = ft == null ? lastFeatureAdded : ft;
+          FeatureTypeSettings fcc = new FeatureTypeSettings(
+                  FeatureRenderer.this, type);
+          fcc.setRequestFocusEnabled(true);
+          fcc.requestFocus();
+          fcc.addActionListener(new ActionListener()
           {
-            final String ft = features.get(featureIndex).getType();
-            final String type = ft == null ? lastFeatureAdded : ft;
-            fcc = new FeatureColourChooser(me, type);
-            fcc.setRequestFocusEnabled(true);
-            fcc.requestFocus();
-
-            fcc.addActionListener(new ActionListener()
+            @Override
+            public void actionPerformed(ActionEvent e)
             {
-
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                fcol = fcc.getLastColour();
-                fcc = null;
-                setColour(type, fcol);
-                updateColourButton(mainPanel, colour, fcol);
-              }
-            });
-
-          }
+              fcol = FeatureRenderer.this.getFeatureStyle(ft);
+              setColour(type, fcol);
+              updateColourButton(mainPanel, colour, fcol);
+            }
+          });
         }
       }
     });
index 12f9db9..78c1cac 100644 (file)
@@ -22,20 +22,21 @@ package jalview.gui;
 
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
-import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.schemabinding.version2.Filter;
 import jalview.schemabinding.version2.JalviewUserColours;
+import jalview.schemabinding.version2.MatcherSet;
 import jalview.schemes.FeatureColour;
-import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.util.QuickSort;
-import jalview.viewmodel.AlignmentViewport;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -44,6 +45,7 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
+import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -61,13 +63,14 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Vector;
 
 import javax.help.HelpSetException;
 import javax.swing.AbstractCellEditor;
@@ -86,36 +89,52 @@ import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.JScrollPane;
 import javax.swing.JSlider;
-import javax.swing.JTabbedPane;
 import javax.swing.JTable;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingConstants;
-import javax.swing.SwingUtilities;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import javax.swing.table.AbstractTableModel;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
 
 public class FeatureSettings extends JPanel
         implements FeatureSettingsControllerI
 {
-  DasSourceBrowser dassourceBrowser;
+  private static final String SEQUENCE_FEATURE_COLOURS = MessageManager
+          .getString("label.sequence_feature_colours");
 
-  jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher;
+  /*
+   * column indices of fields in Feature Settings table
+   */
+  static final int TYPE_COLUMN = 0;
+
+  static final int COLOUR_COLUMN = 1;
+
+  static final int FILTER_COLUMN = 2;
+
+  static final int SHOW_COLUMN = 3;
 
-  JPanel settingsPane = new JPanel();
+  private static final int COLUMN_COUNT = 4;
 
-  JPanel dasSettingsPane = new JPanel();
+  private static final int MIN_WIDTH = 400;
+
+  private static final int MIN_HEIGHT = 400;
 
   final FeatureRenderer fr;
 
   public final AlignFrame af;
 
+  /*
+   * 'original' fields hold settings to restore on Cancel
+   */
   Object[][] originalData;
 
   private float originalTransparency;
 
+  private Map<String, FeatureMatcherSetI> originalFilters;
+
   final JInternalFrame frame;
 
   JScrollPane scrollPane = new JScrollPane();
@@ -126,29 +145,47 @@ public class FeatureSettings extends JPanel
 
   JSlider transparency = new JSlider();
 
-  JPanel transPanel = new JPanel(new GridLayout(1, 2));
-
-  private static final int MIN_WIDTH = 400;
-
-  private static final int MIN_HEIGHT = 400;
-  
-  /**
+  /*
    * when true, constructor is still executing - so ignore UI events
    */
   protected volatile boolean inConstruction = true;
 
+  int selectedRow = -1;
+
+  JButton fetchDAS = new JButton();
+
+  JButton saveDAS = new JButton();
+
+  JButton cancelDAS = new JButton();
+
+  boolean resettingTable = false;
+
+  /*
+   * true when Feature Settings are updating from feature renderer
+   */
+  private boolean handlingUpdate = false;
+
+  /*
+   * holds {featureCount, totalExtent} for each feature type
+   */
+  Map<String, float[]> typeWidth = null;
+
   /**
    * Constructor
    * 
    * @param af
    */
-  public FeatureSettings(AlignFrame af)
+  public FeatureSettings(AlignFrame alignFrame)
   {
-    this.af = af;
+    this.af = alignFrame;
     fr = af.getFeatureRenderer();
-    // allow transparency to be recovered
-    transparency.setMaximum(100
-            - (int) ((originalTransparency = fr.getTransparency()) * 100));
+
+    // save transparency for restore on Cancel
+    originalTransparency = fr.getTransparency();
+    int originalTransparencyAsPercent = (int) (originalTransparency * 100);
+    transparency.setMaximum(100 - originalTransparencyAsPercent);
+
+    originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
 
     try
     {
@@ -163,25 +200,48 @@ public class FeatureSettings extends JPanel
       @Override
       public String getToolTipText(MouseEvent e)
       {
-        if (table.columnAtPoint(e.getPoint()) == 0)
+        String tip = null;
+        int column = table.columnAtPoint(e.getPoint());
+        switch (column)
         {
-          /*
-           * Tooltip for feature name only
-           */
-          return JvSwingUtils.wrapTooltip(true, MessageManager
+        case TYPE_COLUMN:
+          tip = JvSwingUtils.wrapTooltip(true, MessageManager
                   .getString("label.feature_settings_click_drag"));
+          break;
+        case FILTER_COLUMN:
+          int row = table.rowAtPoint(e.getPoint());
+          FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row,
+                  column);
+          tip = o.isEmpty()
+                  ? MessageManager.getString("label.filters_tooltip")
+                  : o.toString();
+          break;
+        default:
+          break;
         }
-        return null;
+        return tip;
       }
     };
     table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12));
     table.setFont(new Font("Verdana", Font.PLAIN, 12));
-    table.setDefaultRenderer(Color.class, new ColorRenderer());
-
-    table.setDefaultEditor(Color.class, new ColorEditor(this));
 
+    // table.setDefaultRenderer(Color.class, new ColorRenderer());
+    // table.setDefaultEditor(Color.class, new ColorEditor(this));
+    //
     table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
+
+    table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this));
+    table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer());
+
+    TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
+            new ColorRenderer(), new ColorEditor(this));
+    table.addColumn(colourColumn);
+
+    TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75,
+            new FilterRenderer(), new FilterEditor(this));
+    table.addColumn(filterColumn);
+
     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
 
     table.addMouseListener(new MouseAdapter()
@@ -190,11 +250,12 @@ public class FeatureSettings extends JPanel
       public void mousePressed(MouseEvent evt)
       {
         selectedRow = table.rowAtPoint(evt.getPoint());
+        String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
         if (evt.isPopupTrigger())
         {
-          popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
-                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
-                  evt.getX(), evt.getY());
+          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
+          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
+                  evt.getY());
         }
         else if (evt.getClickCount() == 2)
         {
@@ -202,8 +263,7 @@ public class FeatureSettings extends JPanel
           boolean toggleSelection = Platform.isControlDown(evt);
           boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
-                  invertSelection, extendSelection, toggleSelection,
-                  (String) table.getValueAt(selectedRow, 0));
+                  invertSelection, extendSelection, toggleSelection, type);
         }
       }
 
@@ -214,9 +274,10 @@ public class FeatureSettings extends JPanel
         selectedRow = table.rowAtPoint(evt.getPoint());
         if (evt.isPopupTrigger())
         {
-          popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
-                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
-                  evt.getX(), evt.getY());
+          String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
+          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
+          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
+                  evt.getY());
         }
       }
     });
@@ -253,9 +314,6 @@ public class FeatureSettings extends JPanel
     // MessageManager.getString("label.feature_settings_click_drag")));
     scrollPane.setViewportView(table);
 
-    dassourceBrowser = new DasSourceBrowser(this);
-    dasSettingsPane.add(dassourceBrowser, BorderLayout.CENTER);
-
     if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
     {
       fr.findAllFeatures(true); // display everything!
@@ -272,8 +330,8 @@ public class FeatureSettings extends JPanel
         if (!fs.resettingTable && !fs.handlingUpdate)
         {
           fs.handlingUpdate = true;
-          fs.resetTable(null); // new groups may be added with new seuqence
-          // feature types only
+          fs.resetTable(null);
+          // new groups may be added with new sequence feature types only
           fs.handlingUpdate = false;
         }
       }
@@ -286,13 +344,13 @@ public class FeatureSettings extends JPanel
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              475, 480);
+              600, 480);
     }
     else
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              400, 450);
+              600, 450);
     }
     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
@@ -304,14 +362,13 @@ public class FeatureSettings extends JPanel
                       javax.swing.event.InternalFrameEvent evt)
               {
                 fr.removePropertyChangeListener(change);
-                dassourceBrowser.fs = null;
               };
             });
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
     inConstruction = false;
   }
 
-  protected void popupSort(final int selectedRow, final String type,
+  protected void popupSort(final int rowSelected, final String type,
           final Object typeCol, final Map<String, float[][]> minmax, int x,
           int y)
   {
@@ -351,84 +408,70 @@ public class FeatureSettings extends JPanel
 
     });
     men.add(dens);
-    if (minmax != null)
+
+    /*
+     * variable colour options include colour by label, by score,
+     * by selected attribute text, or attribute value
+     */
+    final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
+            MessageManager.getString("label.variable_colour"));
+    mxcol.setSelected(!featureColour.isSimpleColour());
+    men.add(mxcol);
+    mxcol.addActionListener(new ActionListener()
     {
-      final float[][] typeMinMax = minmax.get(type);
-      /*
-       * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); //
-       * this is broken at the moment and isn't that useful anyway!
-       * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new
-       * ActionListener() {
-       * 
-       * public void actionPerformed(ActionEvent e) {
-       * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
-       * null); } else { minmax.put(type, typeMinMax); } }
-       * 
-       * });
-       * 
-       * men.add(chb);
-       */
-      if (typeMinMax != null && typeMinMax[0] != null)
-      {
-        // if (table.getValueAt(row, column));
-        // graduated colourschemes for those where minmax exists for the
-        // positional features
-        final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
-                "Graduated Colour");
-        mxcol.setSelected(!featureColour.isSimpleColour());
-        men.add(mxcol);
-        mxcol.addActionListener(new ActionListener()
-        {
-          JColorChooser colorChooser;
+      JColorChooser colorChooser;
 
-          @Override
-          public void actionPerformed(ActionEvent e)
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (e.getSource() == mxcol)
+        {
+          if (featureColour.isSimpleColour())
           {
-            if (e.getSource() == mxcol)
-            {
-              if (featureColour.isSimpleColour())
-              {
-                FeatureColourChooser fc = new FeatureColourChooser(me.fr,
-                        type);
-                fc.addActionListener(this);
-              }
-              else
-              {
-                // bring up simple color chooser
-                colorChooser = new JColorChooser();
-                JDialog dialog = JColorChooser.createDialog(me,
-                        "Select new Colour", true, // modal
-                        colorChooser, this, // OK button handler
-                        null); // no CANCEL button handler
-                colorChooser.setColor(featureColour.getMaxColour());
-                dialog.setVisible(true);
-              }
-            }
-            else
-            {
-              if (e.getSource() instanceof FeatureColourChooser)
-              {
-                FeatureColourChooser fc = (FeatureColourChooser) e
-                        .getSource();
-                table.setValueAt(fc.getLastColour(), selectedRow, 1);
-                table.validate();
-              }
-              else
-              {
-                // probably the color chooser!
-                table.setValueAt(new FeatureColour(colorChooser.getColor()),
-                        selectedRow, 1);
-                table.validate();
-                me.updateFeatureRenderer(
-                        ((FeatureTableModel) table.getModel()).getData(),
-                        false);
-              }
-            }
+            FeatureTypeSettings fc = new FeatureTypeSettings(me.fr, type);
+            fc.addActionListener(this);
           }
-
-        });
+          else
+          {
+            // bring up simple color chooser
+            colorChooser = new JColorChooser();
+            String title = MessageManager
+                    .getString("label.select_colour");
+            JDialog dialog = JColorChooser.createDialog(me,
+                    title, true, // modal
+                    colorChooser, this, // OK button handler
+                    null); // no CANCEL button handler
+            colorChooser.setColor(featureColour.getMaxColour());
+            dialog.setVisible(true);
+          }
+        }
+        else
+        {
+          if (e.getSource() instanceof FeatureTypeSettings)
+          {
+            /*
+             * update after OK in feature colour dialog; the updated
+             * colour will have already been set in the FeatureRenderer
+             */
+            FeatureColourI fci = fr.getFeatureColours().get(type);
+            table.setValueAt(fci, rowSelected, 1);
+            table.validate();
+          }
+          else
+          {
+            // probably the color chooser!
+            table.setValueAt(new FeatureColour(colorChooser.getColor()),
+                    rowSelected, 1);
+            table.validate();
+            me.updateFeatureRenderer(
+                    ((FeatureTableModel) table.getModel()).getData(),
+                    false);
+          }
+        }
       }
-    }
+
+    });
+
     JMenuItem selCols = new JMenuItem(
             MessageManager.getString("label.select_columns_containing"));
     selCols.addActionListener(new ActionListener()
@@ -478,16 +521,6 @@ public class FeatureSettings extends JPanel
     men.show(table, x, y);
   }
 
-  /**
-   * true when Feature Settings are updating from feature renderer
-   */
-  private boolean handlingUpdate = false;
-
-  /**
-   * holds {featureCount, totalExtent} for each feature type
-   */
-  Map<String, float[]> typeWidth = null;
-
   @Override
   synchronized public void discoverAllFeatureData()
   {
@@ -549,8 +582,6 @@ public class FeatureSettings extends JPanel
     return visible;
   }
 
-  boolean resettingTable = false;
-
   synchronized void resetTable(String[] groupChanged)
   {
     if (resettingTable)
@@ -613,7 +644,7 @@ public class FeatureSettings extends JPanel
       }
     }
 
-    Object[][] data = new Object[displayableTypes.size()][3];
+    Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
     int dataIndex = 0;
 
     if (fr.hasRenderOrder())
@@ -636,9 +667,13 @@ public class FeatureSettings extends JPanel
           continue;
         }
 
-        data[dataIndex][0] = type;
-        data[dataIndex][1] = fr.getFeatureStyle(type);
-        data[dataIndex][2] = new Boolean(
+        data[dataIndex][TYPE_COLUMN] = type;
+        data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
+        FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
+        data[dataIndex][FILTER_COLUMN] = featureFilter == null
+                ? new FeatureMatcherSet()
+                : featureFilter;
+        data[dataIndex][SHOW_COLUMN] = new Boolean(
                 af.getViewport().getFeaturesDisplayed().isVisible(type));
         dataIndex++;
         displayableTypes.remove(type);
@@ -652,27 +687,30 @@ public class FeatureSettings extends JPanel
     while (!displayableTypes.isEmpty())
     {
       String type = displayableTypes.iterator().next();
-      data[dataIndex][0] = type;
+      data[dataIndex][TYPE_COLUMN] = type;
 
-      data[dataIndex][1] = fr.getFeatureStyle(type);
-      if (data[dataIndex][1] == null)
+      data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
+      if (data[dataIndex][COLOUR_COLUMN] == null)
       {
         // "Colour has been updated in another view!!"
         fr.clearRenderOrder();
         return;
       }
-
-      data[dataIndex][2] = new Boolean(true);
+      FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
+      data[dataIndex][FILTER_COLUMN] = featureFilter == null
+              ? new FeatureMatcherSet()
+              : featureFilter;
+      data[dataIndex][SHOW_COLUMN] = new Boolean(true);
       dataIndex++;
       displayableTypes.remove(type);
     }
 
     if (originalData == null)
     {
-      originalData = new Object[data.length][3];
+      originalData = new Object[data.length][COLUMN_COUNT];
       for (int i = 0; i < data.length; i++)
       {
-        System.arraycopy(data[i], 0, originalData[i], 0, 3);
+        System.arraycopy(data[i], 0, originalData[i], 0, COLUMN_COUNT);
       }
     }
     else
@@ -693,8 +731,8 @@ public class FeatureSettings extends JPanel
   }
 
   /**
-   * Updates 'originalData' (used for restore on Cancel) if we detect that
-   * changes have been made outwith this dialog
+   * Updates 'originalData' (used for restore on Cancel) if we detect that changes
+   * have been made outwith this dialog
    * <ul>
    * <li>a new feature type added (and made visible)</li>
    * <li>a feature colour changed (in the Amend Features dialog)</li>
@@ -710,27 +748,27 @@ public class FeatureSettings extends JPanel
             .getData();
     for (Object[] row : foundData)
     {
-      String type = (String) row[0];
+      String type = (String) row[TYPE_COLUMN];
       boolean found = false;
       for (Object[] current : currentData)
       {
-        if (type.equals(current[0]))
+        if (type.equals(current[TYPE_COLUMN]))
         {
           found = true;
           /*
            * currently dependent on object equality here;
            * really need an equals method on FeatureColour
            */
-          if (!row[1].equals(current[1]))
+          if (!row[COLOUR_COLUMN].equals(current[COLOUR_COLUMN]))
           {
             /*
              * feature colour has changed externally - update originalData
              */
             for (Object[] original : originalData)
             {
-              if (type.equals(original[0]))
+              if (type.equals(original[TYPE_COLUMN]))
               {
-                original[1] = row[1];
+                original[COLOUR_COLUMN] = row[COLOUR_COLUMN];
                 break;
               }
             }
@@ -743,10 +781,12 @@ public class FeatureSettings extends JPanel
         /*
          * new feature detected - add to original data (on top)
          */
-        Object[][] newData = new Object[originalData.length + 1][3];
+        Object[][] newData = new Object[originalData.length
+                + 1][COLUMN_COUNT];
         for (int i = 0; i < originalData.length; i++)
         {
-          System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
+          System.arraycopy(originalData[i], 0, newData[i + 1], 0,
+                  COLUMN_COUNT);
         }
         newData[0] = row;
         originalData = newData;
@@ -756,8 +796,8 @@ public class FeatureSettings extends JPanel
 
   /**
    * Remove from the groups panel any checkboxes for groups that are not in the
-   * foundGroups set. This enables removing a group from the display when the
-   * last feature in that group is deleted.
+   * foundGroups set. This enables removing a group from the display when the last
+   * feature in that group is deleted.
    * 
    * @param foundGroups
    */
@@ -800,10 +840,14 @@ public class FeatureSettings extends JPanel
     }
   }
 
+  /**
+   * Offers a file chooser dialog, and then loads the feature colours and
+   * filters from file in XML format and unmarshals to Jalview feature settings
+   */
   void load()
   {
     JalviewFileChooser chooser = new JalviewFileChooser("fc",
-            "Sequence Feature Colours");
+            SEQUENCE_FEATURE_COLOURS);
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(
             MessageManager.getString("label.load_feature_colours"));
@@ -814,88 +858,78 @@ public class FeatureSettings extends JPanel
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
       File file = chooser.getSelectedFile();
+      load(file);
+    }
+  }
 
-      try
-      {
-        InputStreamReader in = new InputStreamReader(
-                new FileInputStream(file), "UTF-8");
+  /**
+   * Loads feature colours and filters from XML stored in the given file
+   * 
+   * @param file
+   */
+  void load(File file)
+  {
+    try
+    {
+      InputStreamReader in = new InputStreamReader(
+              new FileInputStream(file), "UTF-8");
 
-        JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
+      JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
 
-        for (int i = jucs.getColourCount() - 1; i >= 0; i--)
-        {
-          String name;
-          jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
-          if (newcol.hasMax())
-          {
-            Color mincol = null, maxcol = null;
-            try
-            {
-              mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16));
-              maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16));
+      /*
+       * load feature colours
+       */
+      for (int i = jucs.getColourCount() - 1; i >= 0; i--)
+      {
+        jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
+        FeatureColourI colour = Jalview2XML.unmarshalColour(newcol);
+        fr.setColour(newcol.getName(), colour);
+        fr.setOrder(newcol.getName(), i / (float) jucs.getColourCount());
+      }
 
-            } catch (Exception e)
-            {
-              Cache.log.warn("Couldn't parse out graduated feature color.",
-                      e);
-            }
-            FeatureColourI gcol = new FeatureColour(mincol, maxcol,
-                    newcol.getMin(), newcol.getMax());
-            if (newcol.hasAutoScale())
-            {
-              gcol.setAutoScaled(newcol.getAutoScale());
-            }
-            if (newcol.hasColourByLabel())
-            {
-              gcol.setColourByLabel(newcol.getColourByLabel());
-            }
-            if (newcol.hasThreshold())
-            {
-              gcol.setThreshold(newcol.getThreshold());
-            }
-            if (newcol.getThreshType().length() > 0)
-            {
-              String ttyp = newcol.getThreshType();
-              if (ttyp.equalsIgnoreCase("ABOVE"))
-              {
-                gcol.setAboveThreshold(true);
-              }
-              if (ttyp.equalsIgnoreCase("BELOW"))
-              {
-                gcol.setBelowThreshold(true);
-              }
-            }
-            fr.setColour(name = newcol.getName(), gcol);
-          }
-          else
-          {
-            Color color = new Color(
-                    Integer.parseInt(jucs.getColour(i).getRGB(), 16));
-            fr.setColour(name = jucs.getColour(i).getName(),
-                    new FeatureColour(color));
-          }
-          fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
-        }
-        if (table != null)
+      /*
+       * load feature filters; loaded filters will replace any that are
+       * currently defined, other defined filters are left unchanged 
+       */
+      for (int i = 0; i < jucs.getFilterCount(); i++)
+      {
+        jalview.schemabinding.version2.Filter filterModel = jucs
+                .getFilter(i);
+        String featureType = filterModel.getFeatureType();
+        FeatureMatcherSetI filter = Jalview2XML.unmarshalFilter(featureType,
+                filterModel.getMatcherSet());
+        if (!filter.isEmpty())
         {
-          resetTable(null);
-          Object[][] data = ((FeatureTableModel) table.getModel())
-                  .getData();
-          ensureOrder(data);
-          updateFeatureRenderer(data, false);
-          table.repaint();
+          fr.setFeatureFilter(featureType, filter);
         }
-      } catch (Exception ex)
-      {
-        System.out.println("Error loading User Colour File\n" + ex);
       }
+
+      /*
+       * update feature settings table
+       */
+      if (table != null)
+      {
+        resetTable(null);
+        Object[][] data = ((FeatureTableModel) table.getModel())
+                .getData();
+        ensureOrder(data);
+        updateFeatureRenderer(data, false);
+        table.repaint();
+      }
+    } catch (Exception ex)
+    {
+      System.out.println("Error loading User Colour File\n" + ex);
     }
   }
 
+  /**
+   * Offers a file chooser dialog, and then saves the current feature colours
+   * and any filters to the selected file in XML format
+   */
   void save()
   {
     JalviewFileChooser chooser = new JalviewFileChooser("fc",
-            "Sequence Feature Colours");
+            SEQUENCE_FEATURE_COLOURS);
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(
             MessageManager.getString("label.save_feature_colours"));
@@ -905,57 +939,75 @@ public class FeatureSettings extends JPanel
 
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
-      String choice = chooser.getSelectedFile().getPath();
-      jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
-      ucs.setSchemeName("Sequence Features");
-      try
-      {
-        PrintWriter out = new PrintWriter(new OutputStreamWriter(
-                new FileOutputStream(choice), "UTF-8"));
+      save(chooser.getSelectedFile());
+    }
+  }
 
-        Set<String> fr_colours = fr.getAllFeatureColours();
-        Iterator<String> e = fr_colours.iterator();
-        float[] sortOrder = new float[fr_colours.size()];
-        String[] sortTypes = new String[fr_colours.size()];
-        int i = 0;
-        while (e.hasNext())
+  /**
+   * Saves feature colours and filters to the given file
+   * 
+   * @param file
+   */
+  void save(File file)
+  {
+    JalviewUserColours ucs = new JalviewUserColours();
+    ucs.setSchemeName("Sequence Features");
+    try
+    {
+      PrintWriter out = new PrintWriter(new OutputStreamWriter(
+              new FileOutputStream(file), "UTF-8"));
+
+      /*
+       * sort feature types by colour order, from 0 (highest)
+       * to 1 (lowest)
+       */
+      Set<String> fr_colours = fr.getAllFeatureColours();
+      String[] sortedTypes = fr_colours
+              .toArray(new String[fr_colours.size()]);
+      Arrays.sort(sortedTypes, new Comparator<String>()
+      {
+        @Override
+        public int compare(String type1, String type2)
         {
-          sortTypes[i] = e.next();
-          sortOrder[i] = fr.getOrder(sortTypes[i]);
-          i++;
+          return Float.compare(fr.getOrder(type1), fr.getOrder(type2));
         }
-        QuickSort.sort(sortOrder, sortTypes);
-        sortOrder = null;
-        for (i = 0; i < sortTypes.length; i++)
+      });
+
+      /*
+       * save feature colours
+       */
+      for (String featureType : sortedTypes)
+      {
+        FeatureColourI fcol = fr.getFeatureStyle(featureType);
+        jalview.schemabinding.version2.Colour col = Jalview2XML.marshalColour(
+                featureType, fcol);
+        ucs.addColour(col);
+      }
+
+      /*
+       * save any feature filters
+       */
+      for (String featureType : sortedTypes)
+      {
+        FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
+        if (filter != null && !filter.isEmpty())
         {
-          jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
-          col.setName(sortTypes[i]);
-          FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
-          if (fcol.isSimpleColour())
-          {
-            col.setRGB(Format.getHexString(fcol.getColour()));
-          }
-          else
-          {
-            col.setRGB(Format.getHexString(fcol.getMaxColour()));
-            col.setMin(fcol.getMin());
-            col.setMax(fcol.getMax());
-            col.setMinRGB(
-                    jalview.util.Format.getHexString(fcol.getMinColour()));
-            col.setAutoScale(fcol.isAutoScaled());
-            col.setThreshold(fcol.getThreshold());
-            col.setColourByLabel(fcol.isColourByLabel());
-            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
-                    : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
-          }
-          ucs.addColour(col);
+          Iterator<FeatureMatcherI> iterator = filter.getMatchers().iterator();
+          FeatureMatcherI firstMatcher = iterator.next();
+          MatcherSet ms = Jalview2XML.marshalFilter(firstMatcher, iterator,
+                  filter.isAnded());
+          Filter filterModel = new Filter();
+          filterModel.setFeatureType(featureType);
+          filterModel.setMatcherSet(ms);
+          ucs.addFilter(filterModel);
         }
-        ucs.marshal(out);
-        out.close();
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
       }
+
+      ucs.marshal(out);
+      out.close();
+    } catch (Exception ex)
+    {
+      ex.printStackTrace();
     }
   }
 
@@ -964,7 +1016,7 @@ public class FeatureSettings extends JPanel
     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
     for (int i = 0; i < data.length; i++)
     {
-      data[i][2] = !(Boolean) data[i][2];
+      data[i][SHOW_COLUMN] = !(Boolean) data[i][SHOW_COLUMN];
     }
     updateFeatureRenderer(data, true);
     table.repaint();
@@ -980,17 +1032,16 @@ public class FeatureSettings extends JPanel
     float[] width = new float[data.length];
     float[] awidth;
     float max = 0;
-    int num = 0;
+
     for (int i = 0; i < data.length; i++)
     {
-      awidth = typeWidth.get(data[i][0]);
+      awidth = typeWidth.get(data[i][TYPE_COLUMN]);
       if (awidth[0] > 0)
       {
         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
         // weight - but have to make per
         // sequence, too (awidth[2])
         // if (width[i]==1) // hack to distinguish single width sequences.
-        num++;
       }
       else
       {
@@ -1007,16 +1058,17 @@ public class FeatureSettings extends JPanel
       // awidth = (float[]) typeWidth.get(data[i][0]);
       if (width[i] == 0)
       {
-        width[i] = fr.getOrder(data[i][0].toString());
+        width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString());
         if (width[i] < 0)
         {
-          width[i] = fr.setOrder(data[i][0].toString(), i / data.length);
+          width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(),
+                  i / data.length);
         }
       }
       else
       {
         width[i] /= max; // normalize
-        fr.setOrder(data[i][0].toString(), width[i]); // store for later
+        fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for later
       }
       if (i > 0)
       {
@@ -1050,76 +1102,56 @@ public class FeatureSettings extends JPanel
   }
 
   /**
-   * Update the priority order of features; only repaint if this changed the
-   * order of visible features
+   * Update the priority order of features; only repaint if this changed the order
+   * of visible features
    * 
    * @param data
    * @param visibleNew
    */
   private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
   {
-    if (fr.setFeaturePriority(data, visibleNew))
+    FeatureSettingsBean[] rowData = getTableAsBeans(data);
+
+    if (fr.setFeaturePriority(rowData, visibleNew))
     {
       af.alignPanel.paintAlignment(true, true);
     }
   }
 
-  int selectedRow = -1;
-
-  JTabbedPane tabbedPane = new JTabbedPane();
-
-  BorderLayout borderLayout1 = new BorderLayout();
-
-  BorderLayout borderLayout2 = new BorderLayout();
-
-  BorderLayout borderLayout3 = new BorderLayout();
-
-  JPanel bigPanel = new JPanel();
-
-  BorderLayout borderLayout4 = new BorderLayout();
-
-  JButton invert = new JButton();
-
-  JPanel buttonPanel = new JPanel();
-
-  JButton cancel = new JButton();
-
-  JButton ok = new JButton();
-
-  JButton loadColours = new JButton();
-
-  JButton saveColours = new JButton();
-
-  JPanel dasButtonPanel = new JPanel();
-
-  JButton fetchDAS = new JButton();
-
-  JButton saveDAS = new JButton();
-
-  JButton cancelDAS = new JButton();
-
-  JButton optimizeOrder = new JButton();
-
-  JButton sortByScore = new JButton();
-
-  JButton sortByDens = new JButton();
-
-  JButton help = new JButton();
-
-  JPanel transbuttons = new JPanel(new GridLayout(5, 1));
+  /**
+   * Converts table data into an array of data beans
+   */
+  private FeatureSettingsBean[] getTableAsBeans(Object[][] data)
+  {
+    FeatureSettingsBean[] rowData = new FeatureSettingsBean[data.length];
+    for (int i = 0; i < data.length; i++)
+    {
+      String type = (String) data[i][TYPE_COLUMN];
+      FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN];
+      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN];
+      Boolean isShown = (Boolean) data[i][SHOW_COLUMN];
+      rowData[i] = new FeatureSettingsBean(type, colour, theFilter,
+              isShown);
+    }
+    return rowData;
+  }
 
   private void jbInit() throws Exception
   {
-    this.setLayout(borderLayout1);
-    settingsPane.setLayout(borderLayout2);
-    dasSettingsPane.setLayout(borderLayout3);
-    bigPanel.setLayout(borderLayout4);
+    this.setLayout(new BorderLayout());
+
+    JPanel settingsPane = new JPanel();
+    settingsPane.setLayout(new BorderLayout());
+
+    JPanel bigPanel = new JPanel();
+    bigPanel.setLayout(new BorderLayout());
 
     groupPanel = new JPanel();
     bigPanel.add(groupPanel, BorderLayout.NORTH);
 
+    JButton invert = new JButton(
+            MessageManager.getString("label.invert_selection"));
     invert.setFont(JvSwingUtils.getLabelFont());
-    invert.setText(MessageManager.getString("label.invert_selection"));
     invert.addActionListener(new ActionListener()
     {
       @Override
@@ -1128,8 +1160,10 @@ public class FeatureSettings extends JPanel
         invertSelection();
       }
     });
+
+    JButton optimizeOrder = new JButton(
+            MessageManager.getString("label.optimise_order"));
     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
-    optimizeOrder.setText(MessageManager.getString("label.optimise_order"));
     optimizeOrder.addActionListener(new ActionListener()
     {
       @Override
@@ -1138,9 +1172,10 @@ public class FeatureSettings extends JPanel
         orderByAvWidth();
       }
     });
+
+    JButton sortByScore = new JButton(
+            MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.setFont(JvSwingUtils.getLabelFont());
-    sortByScore
-            .setText(MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.addActionListener(new ActionListener()
     {
       @Override
@@ -1149,9 +1184,9 @@ public class FeatureSettings extends JPanel
         af.avc.sortAlignmentByFeatureScore(null);
       }
     });
-    sortByDens.setFont(JvSwingUtils.getLabelFont());
-    sortByDens.setText(
+    JButton sortByDens = new JButton(
             MessageManager.getString("label.sequence_sort_by_density"));
+    sortByDens.setFont(JvSwingUtils.getLabelFont());
     sortByDens.addActionListener(new ActionListener()
     {
       @Override
@@ -1160,8 +1195,9 @@ public class FeatureSettings extends JPanel
         af.avc.sortAlignmentByFeatureDensity(null);
       }
     });
+
+    JButton help = new JButton(MessageManager.getString("action.help"));
     help.setFont(JvSwingUtils.getLabelFont());
-    help.setText(MessageManager.getString("action.help"));
     help.addActionListener(new ActionListener()
     {
       @Override
@@ -1192,20 +1228,23 @@ public class FeatureSettings extends JPanel
         }
       }
     });
+
+    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
     cancel.setFont(JvSwingUtils.getLabelFont());
-    cancel.setText(MessageManager.getString("action.cancel"));
     cancel.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
         fr.setTransparency(originalTransparency);
+        fr.setFeatureFilters(originalFilters);
         updateFeatureRenderer(originalData);
         close();
       }
     });
+
+    JButton ok = new JButton(MessageManager.getString("action.ok"));
     ok.setFont(JvSwingUtils.getLabelFont());
-    ok.setText(MessageManager.getString("action.ok"));
     ok.addActionListener(new ActionListener()
     {
       @Override
@@ -1214,8 +1253,12 @@ public class FeatureSettings extends JPanel
         close();
       }
     });
+
+    JButton loadColours = new JButton(
+            MessageManager.getString("label.load_colours"));
     loadColours.setFont(JvSwingUtils.getLabelFont());
-    loadColours.setText(MessageManager.getString("label.load_colours"));
+    loadColours.setToolTipText(
+            MessageManager.getString("label.load_colours_tooltip"));
     loadColours.addActionListener(new ActionListener()
     {
       @Override
@@ -1224,8 +1267,12 @@ public class FeatureSettings extends JPanel
         load();
       }
     });
+
+    JButton saveColours = new JButton(
+            MessageManager.getString("label.save_colours"));
     saveColours.setFont(JvSwingUtils.getLabelFont());
-    saveColours.setText(MessageManager.getString("label.save_colours"));
+    saveColours.setToolTipText(
+            MessageManager.getString("label.save_colours_tooltip"));
     saveColours.addActionListener(new ActionListener()
     {
       @Override
@@ -1242,7 +1289,7 @@ public class FeatureSettings extends JPanel
         if (!inConstruction)
         {
           fr.setTransparency((100 - transparency.getValue()) / 100f);
-          af.alignPanel.paintAlignment(true,true);
+          af.alignPanel.paintAlignment(true, true);
         }
       }
     });
@@ -1250,238 +1297,48 @@ public class FeatureSettings extends JPanel
     transparency.setMaximum(70);
     transparency.setToolTipText(
             MessageManager.getString("label.transparency_tip"));
-    fetchDAS.setText(MessageManager.getString("label.fetch_das_features"));
-    fetchDAS.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        fetchDAS_actionPerformed(e);
-      }
-    });
-    saveDAS.setText(MessageManager.getString("action.save_as_default"));
-    saveDAS.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        saveDAS_actionPerformed(e);
-      }
-    });
-    dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
-    dasSettingsPane.setBorder(null);
-    cancelDAS.setEnabled(false);
-    cancelDAS.setText(MessageManager.getString("action.cancel_fetch"));
-    cancelDAS.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        cancelDAS_actionPerformed(e);
-      }
-    });
-    this.add(tabbedPane, java.awt.BorderLayout.CENTER);
-    tabbedPane.addTab(MessageManager.getString("label.feature_settings"),
-            settingsPane);
-    tabbedPane.addTab(MessageManager.getString("label.das_settings"),
-            dasSettingsPane);
-    bigPanel.add(transPanel, java.awt.BorderLayout.SOUTH);
+
+    JPanel transPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(transPanel, BorderLayout.SOUTH);
+
+    JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
     transbuttons.add(invert);
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
-    JPanel sliderPanel = new JPanel();
-    sliderPanel.add(transparency);
     transPanel.add(transparency);
     transPanel.add(transbuttons);
+
+    JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
     buttonPanel.add(cancel);
     buttonPanel.add(loadColours);
     buttonPanel.add(saveColours);
-    bigPanel.add(scrollPane, java.awt.BorderLayout.CENTER);
-    dasSettingsPane.add(dasButtonPanel, java.awt.BorderLayout.SOUTH);
-    dasButtonPanel.add(fetchDAS);
-    dasButtonPanel.add(cancelDAS);
-    dasButtonPanel.add(saveDAS);
-    settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER);
-    settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH);
+    bigPanel.add(scrollPane, BorderLayout.CENTER);
+    settingsPane.add(bigPanel, BorderLayout.CENTER);
+    settingsPane.add(buttonPanel, BorderLayout.SOUTH);
+    this.add(settingsPane);
   }
 
-  public void fetchDAS_actionPerformed(ActionEvent e)
+  // ///////////////////////////////////////////////////////////////////////
+  // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
+  // ///////////////////////////////////////////////////////////////////////
+  class FeatureTableModel extends AbstractTableModel
   {
-    fetchDAS.setEnabled(false);
-    cancelDAS.setEnabled(true);
-    dassourceBrowser.setGuiEnabled(false);
-    Vector<jalviewSourceI> selectedSources = dassourceBrowser
-            .getSelectedSources();
-    doDasFeatureFetch(selectedSources, true, true);
-  }
-
-  /**
-   * get the features from selectedSources for all or the current selection
-   * 
-   * @param selectedSources
-   * @param checkDbRefs
-   * @param promptFetchDbRefs
-   */
-  private void doDasFeatureFetch(List<jalviewSourceI> selectedSources,
-          boolean checkDbRefs, boolean promptFetchDbRefs)
-  {
-    SequenceI[] dataset, seqs;
-    int iSize;
-    AlignmentViewport vp = af.getViewport();
-    if (vp.getSelectionGroup() != null
-            && vp.getSelectionGroup().getSize() > 0)
-    {
-      iSize = vp.getSelectionGroup().getSize();
-      dataset = new SequenceI[iSize];
-      seqs = vp.getSelectionGroup().getSequencesInOrder(vp.getAlignment());
-    }
-    else
-    {
-      iSize = vp.getAlignment().getHeight();
-      seqs = vp.getAlignment().getSequencesArray();
-    }
-
-    dataset = new SequenceI[iSize];
-    for (int i = 0; i < iSize; i++)
-    {
-      dataset[i] = seqs[i].getDatasetSequence();
-    }
-
-    cancelDAS.setEnabled(true);
-    dasFeatureFetcher = new jalview.ws.DasSequenceFeatureFetcher(dataset,
-            this, selectedSources, checkDbRefs, promptFetchDbRefs);
-    af.getViewport().setShowSequenceFeatures(true);
-    af.showSeqFeatures.setSelected(true);
-  }
-
-  /**
-   * blocking call to initialise the das source browser
-   */
-  public void initDasSources()
-  {
-    dassourceBrowser.initDasSources();
-  }
-
-  /**
-   * examine the current list of das sources and return any matching the given
-   * nicknames in sources
-   * 
-   * @param sources
-   *          Vector of Strings to resolve to DAS source nicknames.
-   * @return sources that are present in source list.
-   */
-  public List<jalviewSourceI> resolveSourceNicknames(Vector<String> sources)
-  {
-    return dassourceBrowser.sourceRegistry.resolveSourceNicknames(sources);
-  }
-
-  /**
-   * get currently selected das sources. ensure you have called initDasSources
-   * before calling this.
-   * 
-   * @return vector of selected das source nicknames
-   */
-  public Vector<jalviewSourceI> getSelectedSources()
-  {
-    return dassourceBrowser.getSelectedSources();
-  }
-
-  /**
-   * properly initialise DAS fetcher and then initiate a new thread to fetch
-   * features from the named sources (rather than any turned on by default)
-   * 
-   * @param sources
-   * @param block
-   *          if true then runs in same thread, otherwise passes to the Swing
-   *          executor
-   */
-  public void fetchDasFeatures(Vector<String> sources, boolean block)
-  {
-    initDasSources();
-    List<jalviewSourceI> resolved = dassourceBrowser.sourceRegistry
-            .resolveSourceNicknames(sources);
-    if (resolved.size() == 0)
-    {
-      resolved = dassourceBrowser.getSelectedSources();
-    }
-    if (resolved.size() > 0)
-    {
-      final List<jalviewSourceI> dassources = resolved;
-      fetchDAS.setEnabled(false);
-      // cancelDAS.setEnabled(true); doDasFetch does this.
-      Runnable fetcher = new Runnable()
-      {
-
-        @Override
-        public void run()
-        {
-          doDasFeatureFetch(dassources, true, false);
-
-        }
-      };
-      if (block)
-      {
-        fetcher.run();
-      }
-      else
-      {
-        SwingUtilities.invokeLater(fetcher);
-      }
-    }
-  }
-
-  public void saveDAS_actionPerformed(ActionEvent e)
-  {
-    dassourceBrowser
-            .saveProperties(jalview.bin.Cache.applicationProperties);
-  }
-
-  public void complete()
-  {
-    fetchDAS.setEnabled(true);
-    cancelDAS.setEnabled(false);
-    dassourceBrowser.setGuiEnabled(true);
-
-  }
-
-  public void cancelDAS_actionPerformed(ActionEvent e)
-  {
-    if (dasFeatureFetcher != null)
-    {
-      dasFeatureFetcher.cancel();
-    }
-    complete();
-  }
+    private String[] columnNames = {
+        MessageManager.getString("label.feature_type"),
+        MessageManager.getString("action.colour"),
+        MessageManager.getString("label.filter"),
+        MessageManager.getString("label.show") };
 
-  public void noDasSourceActive()
-  {
-    complete();
-    JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-            MessageManager.getString("label.no_das_sources_selected_warn"),
-            MessageManager.getString("label.no_das_sources_selected_title"),
-            JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
-  }
+    private Object[][] data;
 
-  // ///////////////////////////////////////////////////////////////////////
-  // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
-  // ///////////////////////////////////////////////////////////////////////
-  class FeatureTableModel extends AbstractTableModel
-  {
     FeatureTableModel(Object[][] data)
     {
       this.data = data;
     }
 
-    private String[] columnNames = {
-        MessageManager.getString("label.feature_type"),
-        MessageManager.getString("action.colour"),
-        MessageManager.getString("label.display") };
-
-    private Object[][] data;
-
     public Object[][] getData()
     {
       return data;
@@ -1521,10 +1378,14 @@ public class FeatureSettings extends JPanel
       return data[row][col];
     }
 
+    /**
+     * Answers the class of the object in column c of the first row of the table
+     */
     @Override
-    public Class getColumnClass(int c)
+    public Class<?> getColumnClass(int c)
     {
-      return getValueAt(0, c).getClass();
+      Object v = getValueAt(0, c);
+      return v == null ? null : v.getClass();
     }
 
     @Override
@@ -1563,12 +1424,7 @@ public class FeatureSettings extends JPanel
             boolean isSelected, boolean hasFocus, int row, int column)
     {
       FeatureColourI cellColour = (FeatureColourI) color;
-      // JLabel comp = new JLabel();
-      // comp.
       setOpaque(true);
-      // comp.
-      // setBounds(getBounds());
-      Color newColor;
       setToolTipText(baseTT);
       setBackground(tbl.getBackground());
       if (!cellColour.isSimpleColour())
@@ -1576,15 +1432,61 @@ public class FeatureSettings extends JPanel
         Rectangle cr = tbl.getCellRect(row, column, false);
         FeatureSettings.renderGraduatedColor(this, cellColour,
                 (int) cr.getWidth(), (int) cr.getHeight());
-
       }
       else
       {
         this.setText("");
         this.setIcon(null);
-        newColor = cellColour.getColour();
-        setBackground(newColor);
+        setBackground(cellColour.getColour());
+      }
+      if (isSelected)
+      {
+        if (selectedBorder == null)
+        {
+          selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
+                  tbl.getSelectionBackground());
+        }
+        setBorder(selectedBorder);
       }
+      else
+      {
+        if (unselectedBorder == null)
+        {
+          unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
+                  tbl.getBackground());
+        }
+        setBorder(unselectedBorder);
+      }
+
+      return this;
+    }
+  }
+
+  class FilterRenderer extends JLabel implements TableCellRenderer
+  {
+    javax.swing.border.Border unselectedBorder = null;
+
+    javax.swing.border.Border selectedBorder = null;
+
+    public FilterRenderer()
+    {
+      setOpaque(true); // MUST do this for background to show up.
+      setHorizontalTextPosition(SwingConstants.CENTER);
+      setVerticalTextPosition(SwingConstants.CENTER);
+    }
+
+    @Override
+    public Component getTableCellRendererComponent(JTable tbl,
+            Object filter, boolean isSelected, boolean hasFocus, int row,
+            int column)
+    {
+      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter;
+      setOpaque(true);
+      String asText = theFilter.toString();
+      setBackground(tbl.getBackground());
+      this.setText(asText);
+      this.setIcon(null);
+
       if (isSelected)
       {
         if (selectedBorder == null)
@@ -1634,28 +1536,43 @@ public class FeatureSettings extends JPanel
           int w, int h)
   {
     boolean thr = false;
-    String tt = "";
-    String tx = "";
+    StringBuilder tt = new StringBuilder();
+    StringBuilder tx = new StringBuilder();
+
+    if (gcol.isColourByAttribute())
+    {
+      tx.append(String.join(":", gcol.getAttributeName()));
+    }
+    else if (!gcol.isColourByLabel())
+    {
+      tx.append(MessageManager.getString("label.score"));
+    }
+    tx.append(" ");
     if (gcol.isAboveThreshold())
     {
       thr = true;
-      tx += ">";
-      tt += "Thresholded (Above " + gcol.getThreshold() + ") ";
+      tx.append(">");
+      tt.append("Thresholded (Above ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isBelowThreshold())
     {
       thr = true;
-      tx += "<";
-      tt += "Thresholded (Below " + gcol.getThreshold() + ") ";
+      tx.append("<");
+      tt.append("Thresholded (Below ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isColourByLabel())
     {
-      tt = "Coloured by label text. " + tt;
+      tt.append("Coloured by label text. ").append(tt);
       if (thr)
       {
-        tx += " ";
+        tx.append(" ");
+      }
+      if (!gcol.isColourByAttribute())
+      {
+        tx.append("Label");
       }
-      tx += "Label";
       comp.setIcon(null);
     }
     else
@@ -1671,18 +1588,258 @@ public class FeatureSettings extends JPanel
       // + ", " + minCol.getBlue() + ")");
     }
     comp.setHorizontalAlignment(SwingConstants.CENTER);
-    comp.setText(tx);
+    comp.setText(tx.toString());
     if (tt.length() > 0)
     {
       if (comp.getToolTipText() == null)
       {
-        comp.setToolTipText(tt);
+        comp.setToolTipText(tt.toString());
+      }
+      else
+      {
+        comp.setToolTipText(
+                tt.append(" ").append(comp.getToolTipText()).toString());
+      }
+    }
+  }
+
+  class ColorEditor extends AbstractCellEditor
+          implements TableCellEditor, ActionListener
+  {
+    FeatureSettings me;
+
+    FeatureColourI currentColor;
+
+    FeatureTypeSettings chooser;
+
+    String type;
+
+    JButton button;
+
+    JColorChooser colorChooser;
+
+    JDialog dialog;
+
+    protected static final String EDIT = "edit";
+
+    int rowSelected = 0;
+
+    public ColorEditor(FeatureSettings me)
+    {
+      this.me = me;
+      // Set up the editor (from the table's point of view),
+      // which is a button.
+      // This button brings up the color chooser dialog,
+      // which is the editor from the user's point of view.
+      button = new JButton();
+      button.setActionCommand(EDIT);
+      button.addActionListener(this);
+      button.setBorderPainted(false);
+      // Set up the dialog that the button brings up.
+      colorChooser = new JColorChooser();
+      dialog = JColorChooser.createDialog(button,
+              MessageManager.getString("label.select_colour"), true, // modal
+              colorChooser, this, // OK button handler
+              null); // no CANCEL button handler
+    }
+
+    /**
+     * Handles events from the editor button and from the dialog's OK button.
+     */
+    @Override
+    public void actionPerformed(ActionEvent e)
+    {
+      // todo test e.getSource() instead here
+      if (EDIT.equals(e.getActionCommand()))
+      {
+        // The user has clicked the cell, so
+        // bring up the dialog.
+        if (currentColor.isSimpleColour())
+        {
+          // bring up simple color chooser
+          button.setBackground(currentColor.getColour());
+          colorChooser.setColor(currentColor.getColour());
+          dialog.setVisible(true);
+        }
+        else
+        {
+          // bring up graduated chooser.
+          chooser = new FeatureTypeSettings(me.fr, type);
+          chooser.setRequestFocusEnabled(true);
+          chooser.requestFocus();
+          chooser.addActionListener(this);
+          chooser.showTab(true);
+        }
+        // Make the renderer reappear.
+        fireEditingStopped();
+
+      }
+      else
+      {
+        if (currentColor.isSimpleColour())
+        {
+          /*
+           * read off colour picked in colour chooser after OK pressed
+           */
+          currentColor = new FeatureColour(colorChooser.getColor());
+          me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
+        }
+        else
+        {
+          /*
+           * after OK in variable colour dialog, any changes to colour 
+           * (or filters!) are already set in FeatureRenderer, so just
+           * update table data without triggering updateFeatureRenderer
+           */
+          currentColor = fr.getFeatureColours().get(type);
+          FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
+          if (currentFilter == null)
+          {
+            currentFilter = new FeatureMatcherSet();
+          }
+          Object[] data = ((FeatureTableModel) table.getModel())
+                  .getData()[rowSelected];
+          data[COLOUR_COLUMN] = currentColor;
+          data[FILTER_COLUMN] = currentFilter;
+        }
+        fireEditingStopped();
+        me.table.validate();
+      }
+    }
+
+    // Implement the one CellEditor method that AbstractCellEditor doesn't.
+    @Override
+    public Object getCellEditorValue()
+    {
+      return currentColor;
+    }
+
+    // Implement the one method defined by TableCellEditor.
+    @Override
+    public Component getTableCellEditorComponent(JTable theTable, Object value,
+            boolean isSelected, int row, int column)
+    {
+      currentColor = (FeatureColourI) value;
+      this.rowSelected = row;
+      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
+      button.setOpaque(true);
+      button.setBackground(me.getBackground());
+      if (!currentColor.isSimpleColour())
+      {
+        JLabel btn = new JLabel();
+        btn.setSize(button.getSize());
+        FeatureSettings.renderGraduatedColor(btn, currentColor);
+        button.setBackground(btn.getBackground());
+        button.setIcon(btn.getIcon());
+        button.setText(btn.getText());
       }
       else
       {
-        comp.setToolTipText(tt + " " + comp.getToolTipText());
+        button.setText("");
+        button.setIcon(null);
+        button.setBackground(currentColor.getColour());
+      }
+      return button;
+    }
+  }
+
+  /**
+   * The cell editor for the Filter column. It displays the text of any filters
+   * for the feature type in that row (in full as a tooltip, possible abbreviated
+   * as display text). On click in the cell, opens the Feature Display Settings
+   * dialog at the Filters tab.
+   */
+  class FilterEditor extends AbstractCellEditor
+          implements TableCellEditor, ActionListener
+  {
+    FeatureSettings me;
+
+    FeatureMatcherSetI currentFilter;
+
+    Point lastLocation;
+
+    String type;
+
+    JButton button;
+
+    protected static final String EDIT = "edit";
+
+    int rowSelected = 0;
+
+    public FilterEditor(FeatureSettings me)
+    {
+      this.me = me;
+      button = new JButton();
+      button.setActionCommand(EDIT);
+      button.addActionListener(this);
+      button.setBorderPainted(false);
+    }
+
+    /**
+     * Handles events from the editor button
+     */
+    @Override
+    public void actionPerformed(ActionEvent e)
+    {
+      if (button == e.getSource())
+      {
+        FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
+        chooser.addActionListener(this);
+        chooser.setRequestFocusEnabled(true);
+        chooser.requestFocus();
+        if (lastLocation != null)
+        {
+          // todo open at its last position on screen
+          chooser.setBounds(lastLocation.x, lastLocation.y,
+                  chooser.getWidth(), chooser.getHeight());
+          chooser.validate();
+        }
+        chooser.showTab(false);
+        fireEditingStopped();
+      }
+      else if (e.getSource() instanceof Component)
+      {
+
+        /*
+         * after OK in variable colour dialog, any changes to filter
+         * (or colours!) are already set in FeatureRenderer, so just
+         * update table data without triggering updateFeatureRenderer
+         */
+        FeatureColourI currentColor = fr.getFeatureColours().get(type);
+        currentFilter = me.fr.getFeatureFilter(type);
+        if (currentFilter == null)
+        {
+          currentFilter = new FeatureMatcherSet();
+        }
+        Object[] data = ((FeatureTableModel) table.getModel())
+                .getData()[rowSelected];
+        data[COLOUR_COLUMN] = currentColor;
+        data[FILTER_COLUMN] = currentFilter;
+        fireEditingStopped();
+        me.table.validate();
       }
     }
+
+    @Override
+    public Object getCellEditorValue()
+    {
+      return currentFilter;
+    }
+
+    @Override
+    public Component getTableCellEditorComponent(JTable theTable, Object value,
+            boolean isSelected, int row, int column)
+    {
+      currentFilter = (FeatureMatcherSetI) value;
+      this.rowSelected = row;
+      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
+      button.setOpaque(true);
+      button.setBackground(me.getBackground());
+      button.setText(currentFilter.toString());
+      button.setToolTipText(currentFilter.toString());
+      button.setIcon(null);
+      return button;
+    }
   }
 }
 
@@ -1767,124 +1924,3 @@ class FeatureIcon implements Icon
     }
   }
 }
-
-class ColorEditor extends AbstractCellEditor
-        implements TableCellEditor, ActionListener
-{
-  FeatureSettings me;
-
-  FeatureColourI currentColor;
-
-  FeatureColourChooser chooser;
-
-  String type;
-
-  JButton button;
-
-  JColorChooser colorChooser;
-
-  JDialog dialog;
-
-  protected static final String EDIT = "edit";
-
-  int selectedRow = 0;
-
-  public ColorEditor(FeatureSettings me)
-  {
-    this.me = me;
-    // Set up the editor (from the table's point of view),
-    // which is a button.
-    // This button brings up the color chooser dialog,
-    // which is the editor from the user's point of view.
-    button = new JButton();
-    button.setActionCommand(EDIT);
-    button.addActionListener(this);
-    button.setBorderPainted(false);
-    // Set up the dialog that the button brings up.
-    colorChooser = new JColorChooser();
-    dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal
-            colorChooser, this, // OK button handler
-            null); // no CANCEL button handler
-  }
-
-  /**
-   * Handles events from the editor button and from the dialog's OK button.
-   */
-  @Override
-  public void actionPerformed(ActionEvent e)
-  {
-
-    if (EDIT.equals(e.getActionCommand()))
-    {
-      // The user has clicked the cell, so
-      // bring up the dialog.
-      if (currentColor.isSimpleColour())
-      {
-        // bring up simple color chooser
-        button.setBackground(currentColor.getColour());
-        colorChooser.setColor(currentColor.getColour());
-        dialog.setVisible(true);
-      }
-      else
-      {
-        // bring up graduated chooser.
-        chooser = new FeatureColourChooser(me.fr, type);
-        chooser.setRequestFocusEnabled(true);
-        chooser.requestFocus();
-        chooser.addActionListener(this);
-      }
-      // Make the renderer reappear.
-      fireEditingStopped();
-
-    }
-    else
-    { // User pressed dialog's "OK" button.
-      if (currentColor.isSimpleColour())
-      {
-        currentColor = new FeatureColour(colorChooser.getColor());
-      }
-      else
-      {
-        currentColor = chooser.getLastColour();
-      }
-      me.table.setValueAt(getCellEditorValue(), selectedRow, 1);
-      fireEditingStopped();
-      me.table.validate();
-    }
-  }
-
-  // Implement the one CellEditor method that AbstractCellEditor doesn't.
-  @Override
-  public Object getCellEditorValue()
-  {
-    return currentColor;
-  }
-
-  // Implement the one method defined by TableCellEditor.
-  @Override
-  public Component getTableCellEditorComponent(JTable table, Object value,
-          boolean isSelected, int row, int column)
-  {
-    currentColor = (FeatureColourI) value;
-    this.selectedRow = row;
-    type = me.table.getValueAt(row, 0).toString();
-    button.setOpaque(true);
-    button.setBackground(me.getBackground());
-    if (!currentColor.isSimpleColour())
-    {
-      JLabel btn = new JLabel();
-      btn.setSize(button.getSize());
-      FeatureSettings.renderGraduatedColor(btn, currentColor);
-      button.setBackground(btn.getBackground());
-      button.setIcon(btn.getIcon());
-      button.setText(btn.getText());
-    }
-    else
-    {
-      button.setText("");
-      button.setIcon(null);
-      button.setBackground(currentColor.getColour());
-    }
-    return button;
-  }
-}
diff --git a/src/jalview/gui/FeatureTypeSettings.java b/src/jalview/gui/FeatureTypeSettings.java
new file mode 100644 (file)
index 0000000..e13f6ee
--- /dev/null
@@ -0,0 +1,1763 @@
+/*
+ * 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.gui;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.GraphLine;
+import jalview.datamodel.features.FeatureAttributes;
+import jalview.datamodel.features.FeatureAttributes.Datatype;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.schemes.FeatureColour;
+import jalview.util.ColorUtils;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSlider;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.border.LineBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.plaf.basic.BasicArrowButton;
+
+/**
+ * A dialog where the user can configure colour scheme, and any filters, for one
+ * feature type
+ * <p>
+ * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
+ * filter options)
+ */
+public class FeatureTypeSettings extends JalviewDialog
+{
+  private final static String LABEL_18N = MessageManager
+          .getString("label.label");
+
+  private final static String SCORE_18N = MessageManager
+          .getString("label.score");
+
+  private static final int RADIO_WIDTH = 130;
+
+  private static final String COLON = ":";
+
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
+  private static final int NO_COLOUR_OPTION = 0;
+
+  private static final int MIN_COLOUR_OPTION = 1;
+
+  private static final int MAX_COLOUR_OPTION = 2;
+
+  private static final int ABOVE_THRESHOLD_OPTION = 1;
+
+  private static final int BELOW_THRESHOLD_OPTION = 2;
+
+  private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
+          "##.##");
+
+  /*
+   * FeatureRenderer holds colour scheme and filters for feature types
+   */
+  private final FeatureRenderer fr; // todo refactor to allow interface type here
+
+  /*
+   * the view panel to update when settings change
+   */
+  private final AlignmentViewPanel ap;
+
+  private final String featureType;
+
+  /*
+   * the colour and filters to reset to on Cancel
+   */
+  private final FeatureColourI originalColour;
+
+  private final FeatureMatcherSetI originalFilter;
+
+  /*
+   * set flag to true when setting values programmatically,
+   * to avoid invocation of action handlers
+   */
+  private boolean adjusting = false;
+
+  /*
+   * minimum of the value range for graduated colour
+   * (may be for feature score or for a numeric attribute)
+   */
+  private float min;
+
+  /*
+   * maximum of the value range for graduated colour
+   */
+  private float max;
+
+  /*
+   * scale factor for conversion between absolute min-max and slider
+   */
+  private float scaleFactor;
+
+  /*
+   * radio button group, to select what to colour by:
+   * simple colour, by category (text), or graduated
+   */
+  private JRadioButton simpleColour = new JRadioButton();
+
+  private JRadioButton byCategory = new JRadioButton();
+
+  private JRadioButton graduatedColour = new JRadioButton();
+
+  private JPanel singleColour = new JPanel();
+
+  private JPanel minColour = new JPanel();
+
+  private JPanel maxColour = new JPanel();
+
+  private JComboBox<String> threshold = new JComboBox<>();
+
+  private JSlider slider = new JSlider();
+
+  private JTextField thresholdValue = new JTextField(20);
+
+  private JCheckBox thresholdIsMin = new JCheckBox();
+
+  private GraphLine threshline;
+
+  private ActionListener featureSettings = null;
+
+  private ActionListener changeColourAction;
+
+  /*
+   * choice of option for 'colour for no value'
+   */
+  private JComboBox<String> noValueCombo;
+
+  /*
+   * choice of what to colour by text (Label or attribute)
+   */
+  private JComboBox<String> colourByTextCombo;
+
+  /*
+   * choice of what to colour by range (Score or attribute)
+   */
+  private JComboBox<String> colourByRangeCombo;
+
+  private JRadioButton andFilters;
+
+  private JRadioButton orFilters;
+
+  /*
+   * filters for the currently selected feature type
+   */
+  private List<FeatureMatcherI> filters;
+
+  // set white normally, black to debug layout
+  private Color debugBorderColour = Color.white;
+
+  private JPanel chooseFiltersPanel;
+
+  private JTabbedPane tabbedPane;
+
+  /**
+   * Constructor
+   * 
+   * @param frender
+   * @param theType
+   */
+  public FeatureTypeSettings(FeatureRenderer frender, String theType)
+  {
+    this(frender, false, theType);
+  }
+
+  /**
+   * Constructor, with option to make a blocking dialog (has to complete in the
+   * AWT event queue thread). Currently this option is always set to false.
+   * 
+   * @param frender
+   * @param blocking
+   * @param theType
+   */
+  FeatureTypeSettings(FeatureRenderer frender, boolean blocking,
+          String theType)
+  {
+    this.fr = frender;
+    this.featureType = theType;
+    ap = fr.ap;
+    originalFilter = fr.getFeatureFilter(theType);
+    originalColour = fr.getFeatureColours().get(theType);
+
+    adjusting = true;
+
+    try
+    {
+      initialise();
+    } catch (Exception ex)
+    {
+      ex.printStackTrace();
+      return;
+    }
+
+    updateColoursTab();
+
+    updateFiltersTab();
+
+    adjusting = false;
+
+    colourChanged(false);
+
+    String title = MessageManager
+            .formatMessage("label.display_settings_for", new String[]
+            { theType });
+    initDialogFrame(this, true, blocking, title, 600, 360);
+
+    waitForInput();
+  }
+
+  /**
+   * Configures the widgets on the Colours tab according to the current feature
+   * colour scheme
+   */
+  private void updateColoursTab()
+  {
+    FeatureColourI fc = fr.getFeatureColours().get(featureType);
+
+    /*
+     * suppress action handling while updating values programmatically
+     */
+    adjusting = true;
+    try
+    {
+      /*
+       * single colour
+       */
+      if (fc.isSimpleColour())
+      {
+        simpleColour.setSelected(true);
+        singleColour.setBackground(fc.getColour());
+        singleColour.setForeground(fc.getColour());
+      }
+
+      /*
+       * colour by text (Label or attribute text)
+       */
+      if (fc.isColourByLabel())
+      {
+        byCategory.setSelected(true);
+        colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
+        if (fc.isColourByAttribute())
+        {
+          String[] attributeName = fc.getAttributeName();
+          colourByTextCombo.setSelectedItem(
+                  FeatureMatcher.toAttributeDisplayName(attributeName));
+        }
+        else
+        {
+          colourByTextCombo.setSelectedItem(LABEL_18N);
+        }
+      }
+      else
+      {
+        colourByTextCombo.setEnabled(false);
+      }
+
+      if (!fc.isGraduatedColour())
+      {
+        colourByRangeCombo.setEnabled(false);
+        minColour.setEnabled(false);
+        maxColour.setEnabled(false);
+        noValueCombo.setEnabled(false);
+        threshold.setEnabled(false);
+        slider.setEnabled(false);
+        thresholdValue.setEnabled(false);
+        thresholdIsMin.setEnabled(false);
+        return;
+      }
+
+      /*
+       * Graduated colour, by score or attribute value range
+       */
+      graduatedColour.setSelected(true);
+      updateColourMinMax(); // ensure min, max are set
+      colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
+      minColour.setEnabled(true);
+      maxColour.setEnabled(true);
+      noValueCombo.setEnabled(true);
+      threshold.setEnabled(true);
+      minColour.setBackground(fc.getMinColour());
+      maxColour.setBackground(fc.getMaxColour());
+
+      if (fc.isColourByAttribute())
+      {
+        String[] attributeName = fc.getAttributeName();
+        colourByRangeCombo.setSelectedItem(
+                FeatureMatcher.toAttributeDisplayName(attributeName));
+      }
+      else
+      {
+        colourByRangeCombo.setSelectedItem(SCORE_18N);
+      }
+      Color noColour = fc.getNoColour();
+      if (noColour == null)
+      {
+        noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
+      }
+      else if (noColour.equals(fc.getMinColour()))
+      {
+        noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
+      }
+      else if (noColour.equals(fc.getMaxColour()))
+      {
+        noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
+      }
+
+      /*
+       * update min-max scaling if there is a range to work with,
+       * else disable the widgets (this shouldn't happen if only 
+       * valid options are offered in the combo box)
+       */
+      scaleFactor = (max == min) ? 1f : 100f / (max - min);
+      float range = (max - min) * scaleFactor;
+      slider.setMinimum((int) (min * scaleFactor));
+      slider.setMaximum((int) (max * scaleFactor));
+      slider.setMajorTickSpacing((int) (range / 10f));
+
+      threshline = new GraphLine((max - min) / 2f, "Threshold",
+              Color.black);
+      threshline.value = fc.getThreshold();
+
+      if (fc.hasThreshold())
+      {
+        threshold.setSelectedIndex(
+                fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
+                        : BELOW_THRESHOLD_OPTION);
+        slider.setEnabled(true);
+        slider.setValue((int) (fc.getThreshold() * scaleFactor));
+        thresholdValue.setText(String.valueOf(getRoundedSliderValue()));
+        thresholdValue.setEnabled(true);
+        thresholdIsMin.setEnabled(true);
+      }
+      else
+      {
+        slider.setEnabled(false);
+        thresholdValue.setEnabled(false);
+        thresholdIsMin.setEnabled(false);
+      }
+      thresholdIsMin.setSelected(!fc.isAutoScaled());
+    } finally
+    {
+      adjusting = false;
+    }
+  }
+
+  /**
+   * Configures the initial layout
+   */
+  private void initialise()
+  {
+    this.setLayout(new BorderLayout());
+    tabbedPane = new JTabbedPane();
+    this.add(tabbedPane, BorderLayout.CENTER);
+
+    /*
+     * an ActionListener that applies colour changes
+     */
+    changeColourAction = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        colourChanged(true);
+      }
+    };
+
+    /*
+     * first tab: colour options
+     */
+    JPanel coloursPanel = initialiseColoursPanel();
+    tabbedPane.addTab(MessageManager.getString("action.colour"),
+            coloursPanel);
+
+    /*
+     * second tab: filter options
+     */
+    JPanel filtersPanel = initialiseFiltersPanel();
+    tabbedPane.addTab(MessageManager.getString("label.filters"),
+            filtersPanel);
+
+    JPanel okCancelPanel = initialiseOkCancelPanel();
+
+    this.add(okCancelPanel, BorderLayout.SOUTH);
+  }
+
+  /**
+   * Updates the min-max range if Colour By selected item is Score, or an
+   * attribute, with a min-max range
+   */
+  protected void updateColourMinMax()
+  {
+    if (!graduatedColour.isSelected())
+    {
+      return;
+    }
+
+    String colourBy = (String) colourByRangeCombo.getSelectedItem();
+    float[] minMax = getMinMax(colourBy);
+
+    if (minMax != null)
+    {
+      min = minMax[0];
+      max = minMax[1];
+    }
+  }
+
+  /**
+   * Retrieves the min-max range:
+   * <ul>
+   * <li>of feature score, if colour or filter is by Score</li>
+   * <li>else of the selected attribute</li>
+   * </ul>
+   * 
+   * @param attName
+   * @return
+   */
+  private float[] getMinMax(String attName)
+  {
+    float[] minMax = null;
+    if (SCORE_18N.equals(attName))
+    {
+      minMax = fr.getMinMax().get(featureType)[0];
+    }
+    else
+    {
+      // colour by attribute range
+      minMax = FeatureAttributes.getInstance().getMinMax(featureType,
+              FeatureMatcher.fromAttributeDisplayName(attName));
+    }
+    return minMax;
+  }
+
+  /**
+   * Lay out fields for graduated colour (by score or attribute value)
+   * 
+   * @return
+   */
+  private JPanel initialiseGraduatedColourPanel()
+  {
+    JPanel graduatedColourPanel = new JPanel();
+    graduatedColourPanel.setLayout(
+            new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
+    JvSwingUtils.createTitledBorder(graduatedColourPanel,
+            MessageManager.getString("label.graduated_colour"), true);
+    graduatedColourPanel.setBackground(Color.white);
+
+    /*
+     * first row: graduated colour radio button, score/attribute drop-down
+     */
+    JPanel graduatedChoicePanel = new JPanel(
+            new FlowLayout(FlowLayout.LEFT));
+    graduatedChoicePanel.setBackground(Color.white);
+    graduatedColour = new JRadioButton(
+            MessageManager.getString("label.by_range_of") + COLON);
+    graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+    graduatedColour.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        if (graduatedColour.isSelected())
+        {
+          colourChanged(true);
+        }
+      }
+    });
+    graduatedChoicePanel.add(graduatedColour);
+
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(featureType);
+    colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
+    colourByRangeCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        colourChanged(true);
+      }
+    });
+
+    /*
+     * disable graduated colour option if no range found
+     */
+    graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
+
+    graduatedChoicePanel.add(colourByRangeCombo);
+    graduatedColourPanel.add(graduatedChoicePanel);
+
+    /*
+     * second row - min/max/no colours
+     */
+    JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    colourRangePanel.setBackground(Color.white);
+    graduatedColourPanel.add(colourRangePanel);
+
+    minColour.setFont(JvSwingUtils.getLabelFont());
+    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    minColour.setPreferredSize(new Dimension(40, 20));
+    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
+    minColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (minColour.isEnabled())
+        {
+          showColourChooser(minColour, "label.select_colour_minimum_value");
+        }
+      }
+    });
+
+    maxColour.setFont(JvSwingUtils.getLabelFont());
+    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    maxColour.setPreferredSize(new Dimension(40, 20));
+    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
+    maxColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (maxColour.isEnabled())
+        {
+          showColourChooser(maxColour, "label.select_colour_maximum_value");
+        }
+      }
+    });
+    maxColour.setBorder(new LineBorder(Color.black));
+
+    /*
+     * default max colour to current colour (if a plain colour),
+     * or to Black if colour by label;  make min colour a pale
+     * version of max colour
+     */
+    FeatureColourI fc = fr.getFeatureColours().get(featureType);
+    Color bg = fc.isSimpleColour() ? fc.getColour() : Color.BLACK;
+    maxColour.setBackground(bg);
+    minColour.setBackground(ColorUtils.bleachColour(bg, 0.9f));
+
+    noValueCombo = new JComboBox<>();
+    noValueCombo.addItem(MessageManager.getString("label.no_colour"));
+    noValueCombo.addItem(MessageManager.getString("label.min_colour"));
+    noValueCombo.addItem(MessageManager.getString("label.max_colour"));
+    noValueCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        colourChanged(true);
+      }
+    });
+
+    JLabel minText = new JLabel(
+            MessageManager.getString("label.min_value") + COLON);
+    minText.setFont(JvSwingUtils.getLabelFont());
+    JLabel maxText = new JLabel(
+            MessageManager.getString("label.max_value") + COLON);
+    maxText.setFont(JvSwingUtils.getLabelFont());
+    JLabel noText = new JLabel(
+            MessageManager.getString("label.no_value") + COLON);
+    noText.setFont(JvSwingUtils.getLabelFont());
+
+    colourRangePanel.add(minText);
+    colourRangePanel.add(minColour);
+    colourRangePanel.add(maxText);
+    colourRangePanel.add(maxColour);
+    colourRangePanel.add(noText);
+    colourRangePanel.add(noValueCombo);
+
+    /*
+     * third row - threshold options and value
+     */
+    JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    thresholdPanel.setBackground(Color.white);
+    graduatedColourPanel.add(thresholdPanel);
+
+    threshold.addActionListener(changeColourAction);
+    threshold.setToolTipText(MessageManager
+            .getString("label.threshold_feature_display_by_score"));
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_no_threshold")); // index 0
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_above_threshold")); // index 1
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_below_threshold")); // index 2
+
+    thresholdValue.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
+    thresholdValue.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
+    slider.setPaintLabels(false);
+    slider.setPaintTicks(true);
+    slider.setBackground(Color.white);
+    slider.setEnabled(false);
+    slider.setOpaque(false);
+    slider.setPreferredSize(new Dimension(100, 32));
+    slider.setToolTipText(
+            MessageManager.getString("label.adjust_threshold"));
+
+    slider.addChangeListener(new ChangeListener()
+    {
+      @Override
+      public void stateChanged(ChangeEvent evt)
+      {
+        if (!adjusting)
+        {
+          thresholdValue
+                  .setText(String.valueOf(slider.getValue() / scaleFactor));
+          sliderValueChanged();
+        }
+      }
+    });
+    slider.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mouseReleased(MouseEvent evt)
+      {
+        /*
+         * only update Overview and/or structure colouring
+         * when threshold slider drag ends (mouse up)
+         */
+        if (ap != null)
+        {
+          ap.paintAlignment(true, true);
+        }
+      }
+    });
+
+    thresholdValue.setEnabled(false);
+    thresholdValue.setColumns(7);
+
+    thresholdPanel.add(threshold);
+    thresholdPanel.add(slider);
+    thresholdPanel.add(thresholdValue);
+
+    thresholdIsMin.setBackground(Color.white);
+    thresholdIsMin
+            .setText(MessageManager.getString("label.threshold_minmax"));
+    thresholdIsMin.setToolTipText(MessageManager
+            .getString("label.toggle_absolute_relative_display_threshold"));
+    thresholdIsMin.addActionListener(changeColourAction);
+    thresholdPanel.add(thresholdIsMin);
+
+    return graduatedColourPanel;
+  }
+
+  /**
+   * Lay out OK and Cancel buttons
+   * 
+   * @return
+   */
+  private JPanel initialiseOkCancelPanel()
+  {
+    JPanel okCancelPanel = new JPanel();
+    // okCancelPanel.setBackground(Color.white);
+    okCancelPanel.add(ok);
+    okCancelPanel.add(cancel);
+    return okCancelPanel;
+  }
+
+  /**
+   * Lay out Colour options panel, containing
+   * <ul>
+   * <li>plain colour, with colour picker</li>
+   * <li>colour by text, with choice of Label or other attribute</li>
+   * <li>colour by range, of score or other attribute, when available</li>
+   * </ul>
+   * 
+   * @return
+   */
+  private JPanel initialiseColoursPanel()
+  {
+    JPanel colourByPanel = new JPanel();
+    colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
+
+    /*
+     * simple colour radio button and colour picker
+     */
+    JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    simpleColourPanel.setBackground(Color.white);
+    JvSwingUtils.createTitledBorder(simpleColourPanel,
+            MessageManager.getString("label.simple"), true);
+    colourByPanel.add(simpleColourPanel);
+
+    simpleColour = new JRadioButton(
+            MessageManager.getString("label.simple_colour"));
+    simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+    simpleColour.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        if (simpleColour.isSelected() && !adjusting)
+        {
+          showColourChooser(singleColour, "label.select_colour");
+        }
+      }
+
+    });
+    
+    singleColour.setFont(JvSwingUtils.getLabelFont());
+    singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    singleColour.setPreferredSize(new Dimension(40, 20));
+    singleColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (simpleColour.isSelected())
+        {
+          showColourChooser(singleColour, "label.select_colour");
+        }
+      }
+    });
+    simpleColourPanel.add(simpleColour); // radio button
+    simpleColourPanel.add(singleColour); // colour picker button
+
+    /*
+     * colour by text (category) radio button and drop-down choice list
+     */
+    JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byTextPanel.setBackground(Color.white);
+    JvSwingUtils.createTitledBorder(byTextPanel,
+            MessageManager.getString("label.colour_by_text"), true);
+    colourByPanel.add(byTextPanel);
+    byCategory = new JRadioButton(
+            MessageManager.getString("label.by_text_of") + COLON);
+    byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+    byCategory.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        if (byCategory.isSelected())
+        {
+          colourChanged(true);
+        }
+      }
+    });
+    byTextPanel.add(byCategory);
+
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(featureType);
+    colourByTextCombo = populateAttributesDropdown(attNames, false, true);
+    colourByTextCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        colourChanged(true);
+      }
+    });
+    byTextPanel.add(colourByTextCombo);
+
+    /*
+     * graduated colour panel
+     */
+    JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
+    colourByPanel.add(graduatedColourPanel);
+
+    /*
+     * 3 radio buttons select between simple colour, 
+     * by category (text), or graduated
+     */
+    ButtonGroup bg = new ButtonGroup();
+    bg.add(simpleColour);
+    bg.add(byCategory);
+    bg.add(graduatedColour);
+
+    return colourByPanel;
+  }
+
+  private void showColourChooser(JPanel colourPanel, String key)
+  {
+    Color col = JColorChooser.showDialog(this,
+            MessageManager.getString(key), colourPanel.getBackground());
+    if (col != null)
+    {
+      colourPanel.setBackground(col);
+      colourPanel.setForeground(col);
+    }
+    colourPanel.repaint();
+    colourChanged(true);
+  }
+
+  /**
+   * Constructs and sets the selected colour options as the colour for the feature
+   * type, and repaints the alignment, and optionally the Overview and/or
+   * structure viewer if open
+   * 
+   * @param updateStructsAndOverview
+   */
+  void colourChanged(boolean updateStructsAndOverview)
+  {
+    if (adjusting)
+    {
+      /*
+       * ignore action handlers while setting values programmatically
+       */
+      return;
+    }
+
+    /*
+     * ensure min-max range is for the latest choice of 
+     * 'graduated colour by'
+     */
+    updateColourMinMax();
+
+    FeatureColourI acg = makeColourFromInputs();
+
+    /*
+     * save the colour, and repaint stuff
+     */
+    fr.setColour(featureType, acg);
+    ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
+
+    updateColoursTab();
+  }
+
+  /**
+   * Converts the input values into an instance of FeatureColour
+   * 
+   * @return
+   */
+  private FeatureColourI makeColourFromInputs()
+  {
+    /*
+     * easiest case - a single colour
+     */
+    if (simpleColour.isSelected())
+    {
+      return new FeatureColour(singleColour.getBackground());
+    }
+
+    /*
+     * next easiest case - colour by Label, or attribute text
+     */
+    if (byCategory.isSelected())
+    {
+      Color c = this.getBackground();
+      FeatureColourI fc = new FeatureColour(c, c, null, 0f, 0f);
+      fc.setColourByLabel(true);
+      String byWhat = (String) colourByTextCombo.getSelectedItem();
+      if (!LABEL_18N.equals(byWhat))
+      {
+        fc.setAttributeName(
+                FeatureMatcher.fromAttributeDisplayName(byWhat));
+      }
+      return fc;
+    }
+
+    /*
+     * remaining case - graduated colour by score, or attribute value
+     */
+    Color noColour = null;
+    if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
+    {
+      noColour = minColour.getBackground();
+    }
+    else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
+    {
+      noColour = maxColour.getBackground();
+    }
+
+    float thresh = 0f;
+    try
+    {
+      thresh = Float.valueOf(thresholdValue.getText());
+    } catch (NumberFormatException e)
+    {
+      // invalid inputs are already handled on entry
+    }
+
+    /*
+     * min-max range is to (or from) threshold value if 
+     * 'threshold is min/max' is selected 
+     */
+    float minValue = min;
+    float maxValue = max;
+    final int thresholdOption = threshold.getSelectedIndex();
+    if (thresholdIsMin.isSelected()
+            && thresholdOption == ABOVE_THRESHOLD_OPTION)
+    {
+      minValue = thresh;
+    }
+    if (thresholdIsMin.isSelected()
+            && thresholdOption == BELOW_THRESHOLD_OPTION)
+    {
+      maxValue = thresh;
+    }
+
+    /*
+     * make the graduated colour
+     */
+    FeatureColourI fc = new FeatureColour(minColour.getBackground(),
+            maxColour.getBackground(), noColour, minValue, maxValue);
+
+    /*
+     * set attribute to colour by if selected
+     */
+    String byWhat = (String) colourByRangeCombo.getSelectedItem();
+    if (!SCORE_18N.equals(byWhat))
+    {
+      fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
+    }
+
+    /*
+     * set threshold options and 'autoscaled' which is
+     * false if 'threshold is min/max' is selected
+     * else true (colour range is on actual range of values)
+     */
+    fc.setThreshold(thresh);
+    fc.setAutoScaled(!thresholdIsMin.isSelected());
+    fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
+    fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
+
+    if (threshline == null)
+    {
+      /*
+       * todo not yet implemented: visual indication of feature threshold
+       */
+      threshline = new GraphLine((max - min) / 2f, "Threshold",
+              Color.black);
+    }
+
+    return fc;
+  }
+
+  @Override
+  protected void raiseClosed()
+  {
+    if (this.featureSettings != null)
+    {
+      featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
+    }
+  }
+
+  /**
+   * Action on OK is just to dismiss the dialog - any changes have already been
+   * applied
+   */
+  @Override
+  public void okPressed()
+  {
+  }
+
+  /**
+   * Action on Cancel is to restore colour scheme and filters as they were when
+   * the dialog was opened
+   */
+  @Override
+  public void cancelPressed()
+  {
+    fr.setColour(featureType, originalColour);
+    fr.setFeatureFilter(featureType, originalFilter);
+    ap.paintAlignment(true, true);
+  }
+
+  /**
+   * Action on text entry of a threshold value
+   */
+  protected void thresholdValue_actionPerformed()
+  {
+    try
+    {
+      adjusting = true;
+      float f = Float.parseFloat(thresholdValue.getText());
+      slider.setValue((int) (f * scaleFactor));
+      threshline.value = f;
+      thresholdValue.setBackground(Color.white); // ok
+
+      /*
+       * force repaint of any Overview window or structure
+       */
+      ap.paintAlignment(true, true);
+    } catch (NumberFormatException ex)
+    {
+      thresholdValue.setBackground(Color.red); // not ok
+    } finally
+    {
+      adjusting = false;
+    }
+  }
+
+  /**
+   * Action on change of threshold slider value. This may be done interactively
+   * (by moving the slider), or programmatically (to update the slider after
+   * manual input of a threshold value).
+   */
+  protected void sliderValueChanged()
+  {
+    threshline.value = getRoundedSliderValue();
+
+    /*
+     * repaint alignment, but not Overview or structure,
+     * to avoid overload while dragging the slider
+     */
+    colourChanged(false);
+  }
+
+  /**
+   * Converts the slider value to its absolute value by dividing by the
+   * scaleFactor. Rounding errors are squashed by forcing min/max of slider range
+   * to the actual min/max of feature score range
+   * 
+   * @return
+   */
+  private float getRoundedSliderValue()
+  {
+    int value = slider.getValue();
+    float f = value == slider.getMaximum() ? max
+            : (value == slider.getMinimum() ? min : value / scaleFactor);
+    return f;
+  }
+
+  void addActionListener(ActionListener listener)
+  {
+    if (featureSettings != null)
+    {
+      System.err.println(
+              "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
+    }
+    featureSettings = listener;
+  }
+
+  /**
+   * A helper method to build the drop-down choice of attributes for a feature. If
+   * 'withRange' is true, then Score, and any attributes with a min-max range, are
+   * added. If 'withText' is true, Label and any known attributes are added. This
+   * allows 'categorical numerical' attributes e.g. codon position to be coloured
+   * by text.
+   * <p>
+   * Where metadata is available with a description for an attribute, that is
+   * added as a tooltip.
+   * <p>
+   * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
+   * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
+   * <p>
+   * This method does not add any ActionListener to the JComboBox.
+   * 
+   * @param attNames
+   * @param withRange
+   * @param withText
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          List<String[]> attNames, boolean withRange, boolean withText)
+  {
+    List<String> displayAtts = new ArrayList<>();
+    List<String> tooltips = new ArrayList<>();
+
+    if (withText)
+    {
+      displayAtts.add(LABEL_18N);
+      tooltips.add(MessageManager.getString("label.description"));
+    }
+    if (withRange)
+    {
+      float[][] minMax = fr.getMinMax().get(featureType);
+      if (minMax != null && minMax[0][0] != minMax[0][1])
+      {
+        displayAtts.add(SCORE_18N);
+        tooltips.add(SCORE_18N);
+      }
+    }
+
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    for (String[] attName : attNames)
+    {
+      float[] minMax = fa.getMinMax(featureType, attName);
+      boolean hasRange = minMax != null && minMax[0] != minMax[1];
+      if (!withText && !hasRange)
+      {
+        continue;
+      }
+      displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
+      String desc = fa.getDescription(featureType, attName);
+      if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+      {
+        desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+      }
+      tooltips.add(desc == null ? "" : desc);
+    }
+
+    JComboBox<String> attCombo = JvSwingUtils
+            .buildComboWithTooltips(displayAtts, tooltips);
+
+    return attCombo;
+  }
+
+  /**
+   * Populates initial layout of the feature attribute filters panel
+   */
+  private JPanel initialiseFiltersPanel()
+  {
+    filters = new ArrayList<>();
+
+    JPanel filtersPanel = new JPanel();
+    filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
+    filtersPanel.setBackground(Color.white);
+    JvSwingUtils.createTitledBorder(filtersPanel,
+            MessageManager.getString("label.filters"), true);
+
+    JPanel andOrPanel = initialiseAndOrPanel();
+    filtersPanel.add(andOrPanel);
+
+    /*
+     * panel with filters - populated by refreshFiltersDisplay, 
+     * which also sets the layout manager
+     */
+    chooseFiltersPanel = new JPanel();
+    chooseFiltersPanel.setBackground(Color.white);
+    filtersPanel.add(chooseFiltersPanel);
+
+    return filtersPanel;
+  }
+
+  /**
+   * Lays out the panel with radio buttons to AND or OR filter conditions
+   * 
+   * @return
+   */
+  private JPanel initialiseAndOrPanel()
+  {
+    JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    andOrPanel.setBackground(Color.white);
+    andOrPanel.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+    andFilters = new JRadioButton(MessageManager.getString("label.and"));
+    orFilters = new JRadioButton(MessageManager.getString("label.or"));
+    ActionListener actionListener = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filtersChanged();
+      }
+    };
+    andFilters.addActionListener(actionListener);
+    orFilters.addActionListener(actionListener);
+    ButtonGroup andOr = new ButtonGroup();
+    andOr.add(andFilters);
+    andOr.add(orFilters);
+    andFilters.setSelected(true);
+    andOrPanel.add(
+            new JLabel(MessageManager.getString("label.join_conditions")));
+    andOrPanel.add(andFilters);
+    andOrPanel.add(orFilters);
+    return andOrPanel;
+  }
+
+  /**
+   * Refreshes the display to show any filters currently configured for the
+   * selected feature type (editable, with 'remove' option), plus one extra row
+   * for adding a condition. This should be called after a filter has been
+   * removed, added or amended.
+   */
+  private void updateFiltersTab()
+  {
+    /*
+     * clear the panel and list of filter conditions
+     */
+    chooseFiltersPanel.removeAll();
+    filters.clear();
+
+    /*
+     * look up attributes known for feature type
+     */
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(featureType);
+
+    /*
+     * if this feature type has filters set, load them first
+     */
+    FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
+    if (featureFilters != null)
+    {
+      if (!featureFilters.isAnded())
+      {
+        orFilters.setSelected(true);
+      }
+      featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
+    }
+
+    /*
+     * and an empty filter for the user to populate (add)
+     */
+    filters.add(FeatureMatcher.NULL_MATCHER);
+
+    /*
+     * use GridLayout to 'justify' rows to the top of the panel, until
+     * there are too many to fit in, then fall back on BoxLayout
+     */
+    if (filters.size() <= 5)
+    {
+      chooseFiltersPanel.setLayout(new GridLayout(5, 1));
+    }
+    else
+    {
+      chooseFiltersPanel.setLayout(
+              new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
+    }
+
+    /*
+     * render the conditions in rows, each in its own JPanel
+     */
+    int filterIndex = 0;
+    for (FeatureMatcherI filter : filters)
+    {
+      JPanel row = addFilter(filter, attNames, filterIndex);
+      row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+      chooseFiltersPanel.add(row);
+      filterIndex++;
+    }
+
+    this.validate();
+    this.repaint();
+  }
+
+  /**
+   * A helper method that constructs a row (panel) with one filter condition:
+   * <ul>
+   * <li>a drop-down list of Label, Score and attribute names to choose from</li>
+   * <li>a drop-down list of conditions to choose from</li>
+   * <li>a text field for input of a match pattern</li>
+   * <li>optionally, a 'remove' button</li>
+   * </ul>
+   * The filter values are set as defaults for the input fields. The 'remove'
+   * button is added unless the pattern is empty (incomplete filter condition).
+   * <p>
+   * Action handlers on these fields provide for
+   * <ul>
+   * <li>validate pattern field - should be numeric if condition is numeric</li>
+   * <li>save filters and refresh display on any (valid) change</li>
+   * <li>remove filter and refresh on 'Remove'</li>
+   * <li>update conditions list on change of Label/Score/Attribute</li>
+   * <li>refresh value field tooltip with min-max range on change of
+   * attribute</li>
+   * </ul>
+   * 
+   * @param filter
+   * @param attNames
+   * @param filterIndex
+   * @return
+   */
+  protected JPanel addFilter(FeatureMatcherI filter,
+          List<String[]> attNames, int filterIndex)
+  {
+    String[] attName = filter.getAttribute();
+    Condition cond = filter.getMatcher().getCondition();
+    String pattern = filter.getMatcher().getPattern();
+
+    JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    filterRow.setBackground(Color.white);
+
+    /*
+     * drop-down choice of attribute, with description as a tooltip 
+     * if we can obtain it
+     */
+    final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
+            true, true);
+    String filterBy = setSelectedAttribute(attCombo, filter);
+
+    JComboBox<Condition> condCombo = new JComboBox<>();
+
+    JTextField patternField = new JTextField(8);
+    patternField.setText(pattern);
+
+    /*
+     * action handlers that validate and (if valid) apply changes
+     */
+    ActionListener actionListener = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (validateFilter(patternField, condCombo))
+        {
+          if (updateFilter(attCombo, condCombo, patternField, filterIndex))
+          {
+            filtersChanged();
+          }
+        }
+      }
+    };
+    ItemListener itemListener = new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    };
+
+    if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
+    {
+      attCombo.setSelectedIndex(0);
+    }
+    else
+    {
+      attCombo.setSelectedItem(
+              FeatureMatcher.toAttributeDisplayName(attName));
+    }
+    attCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        /*
+         * on change of attribute, refresh the conditions list to
+         * ensure it is appropriate for the attribute datatype
+         */
+        populateConditions((String) attCombo.getSelectedItem(),
+                (Condition) condCombo.getSelectedItem(), condCombo,
+                patternField);
+        actionListener.actionPerformed(null);
+      }
+    });
+
+    filterRow.add(attCombo);
+
+    /*
+     * drop-down choice of test condition
+     */
+    populateConditions(filterBy, cond, condCombo, patternField);
+    condCombo.setPreferredSize(new Dimension(150, 20));
+    condCombo.addItemListener(itemListener);
+    filterRow.add(condCombo);
+
+    /*
+     * pattern to match against
+     */
+    patternField.addActionListener(actionListener);
+    patternField.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    });
+    filterRow.add(patternField);
+
+    /*
+     * disable pattern field for condition 'Present / NotPresent'
+     */
+    Condition selectedCondition = (Condition) condCombo.getSelectedItem();
+    patternField.setEnabled(selectedCondition.needsAPattern());
+
+    /*
+     * if a numeric condition is selected, show the value range
+     * as a tooltip on the value input field
+     */
+    setNumericHints(filterBy, selectedCondition, patternField);
+
+    /*
+     * add remove button if filter is populated (non-empty pattern)
+     */
+    if (!patternField.isEnabled()
+            || (pattern != null && pattern.trim().length() > 0))
+    {
+      // todo: gif for button drawing '-' or 'x'
+      JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
+      removeCondition
+              .setToolTipText(MessageManager.getString("label.delete_row"));
+      removeCondition.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          filters.remove(filterIndex);
+          filtersChanged();
+        }
+      });
+      filterRow.add(removeCondition);
+    }
+
+    return filterRow;
+  }
+
+  /**
+   * Sets the selected item in the Label/Score/Attribute drop-down to match the
+   * filter
+   * 
+   * @param attCombo
+   * @param filter
+   */
+  private String setSelectedAttribute(JComboBox<String> attCombo,
+          FeatureMatcherI filter)
+  {
+    String item = null;
+    if (filter.isByScore())
+    {
+      item = SCORE_18N;
+    }
+    else if (filter.isByLabel())
+    {
+      item = LABEL_18N;
+    }
+    else
+    {
+      item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
+    }
+    attCombo.setSelectedItem(item);
+    return item;
+  }
+
+  /**
+   * If a numeric comparison condition is selected, retrieves the min-max range
+   * for the value (score or attribute), and sets it as a tooltip on the value
+   * field. If the field is currently empty, then pre-populates it with
+   * <ul>
+   * <li>the minimum value, if condition is > or >=</li>
+   * <li>the maximum value, if condition is < or <=</li>
+   * </ul>
+   * 
+   * @param attName
+   * @param selectedCondition
+   * @param patternField
+   */
+  private void setNumericHints(String attName,
+          Condition selectedCondition, JTextField patternField)
+  {
+    patternField.setToolTipText("");
+
+    if (selectedCondition.isNumeric())
+    {
+      float[] minMax = getMinMax(attName);
+      if (minMax != null)
+      {
+        String minFormatted = DECFMT_2_2.format(minMax[0]);
+        String maxFormatted = DECFMT_2_2.format(minMax[1]);
+        String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
+        patternField.setToolTipText(tip);
+        if (patternField.getText().isEmpty())
+        {
+          if (selectedCondition == Condition.GE
+                  || selectedCondition == Condition.GT)
+          {
+            patternField.setText(minFormatted);
+          }
+          else
+          {
+            if (selectedCondition == Condition.LE
+                    || selectedCondition == Condition.LT)
+            {
+              patternField.setText(maxFormatted);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Populates the drop-down list of comparison conditions for the given attribute
+   * name. The conditions added depend on the datatype of the attribute values.
+   * The supplied condition is set as the selected item in the list, provided it
+   * is in the list. If the pattern is now invalid (non-numeric pattern for a
+   * numeric condition), it is cleared.
+   * 
+   * @param attName
+   * @param cond
+   * @param condCombo
+   * @param patternField
+   */
+  private void populateConditions(String attName, Condition cond,
+          JComboBox<Condition> condCombo, JTextField patternField)
+  {
+    Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
+            FeatureMatcher.fromAttributeDisplayName(attName));
+    if (LABEL_18N.equals(attName))
+    {
+      type = Datatype.Character;
+    }
+    else if (SCORE_18N.equals(attName))
+    {
+      type = Datatype.Number;
+    }
+
+    /*
+     * remove itemListener before starting
+     */
+    ItemListener listener = condCombo.getItemListeners()[0];
+    condCombo.removeItemListener(listener);
+    boolean condIsValid = false;
+
+    condCombo.removeAllItems();
+    for (Condition c : Condition.values())
+    {
+      if ((c.isNumeric() && type == Datatype.Number)
+              || (!c.isNumeric() && type != Datatype.Number))
+      {
+        condCombo.addItem(c);
+        if (c == cond)
+        {
+          condIsValid = true;
+        }
+      }
+    }
+
+    /*
+     * set the selected condition (does nothing if not in the list)
+     */
+    if (condIsValid)
+    {
+      condCombo.setSelectedItem(cond);
+    }
+    else
+    {
+      condCombo.setSelectedIndex(0);
+    }
+
+    /*
+     * clear pattern if it is now invalid for condition
+     */
+    if (((Condition) condCombo.getSelectedItem()).isNumeric())
+    {
+      try
+      {
+        String pattern = patternField.getText().trim();
+        if (pattern.length() > 0)
+        {
+          Float.valueOf(pattern);
+        }
+      } catch (NumberFormatException e)
+      {
+        patternField.setText("");
+      }
+    }
+
+    /*
+     * restore the listener
+     */
+    condCombo.addItemListener(listener);
+  }
+
+  /**
+   * Answers true unless a numeric condition has been selected with a non-numeric
+   * value. Sets the value field to RED with a tooltip if in error.
+   * <p>
+   * If the pattern is expected but is empty, this method returns false, but does
+   * not mark the field as invalid. This supports selecting an attribute for a new
+   * condition before a match pattern has been entered.
+   * 
+   * @param value
+   * @param condCombo
+   */
+  protected boolean validateFilter(JTextField value,
+          JComboBox<Condition> condCombo)
+  {
+    if (value == null || condCombo == null)
+    {
+      return true; // fields not populated
+    }
+
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    if (!cond.needsAPattern())
+    {
+      return true;
+    }
+
+    value.setBackground(Color.white);
+    value.setToolTipText("");
+    String v1 = value.getText().trim();
+    if (v1.length() == 0)
+    {
+      // return false;
+    }
+
+    if (cond.isNumeric() && v1.length() > 0)
+    {
+      try
+      {
+        Float.valueOf(v1);
+      } catch (NumberFormatException e)
+      {
+        value.setBackground(Color.red);
+        value.setToolTipText(
+                MessageManager.getString("label.numeric_required"));
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Constructs a filter condition from the given input fields, and replaces the
+   * condition at filterIndex with the new one. Does nothing if the pattern field
+   * is blank (unless the match condition is one that doesn't require a pattern,
+   * e.g. 'Is present'). Answers true if the filter was updated, else false.
+   * <p>
+   * This method may update the tooltip on the filter value field to show the
+   * value range, if a numeric condition is selected. This ensures the tooltip is
+   * updated when a numeric valued attribute is chosen on the last 'add a filter'
+   * row.
+   * 
+   * @param attCombo
+   * @param condCombo
+   * @param valueField
+   * @param filterIndex
+   */
+  protected boolean updateFilter(JComboBox<String> attCombo,
+          JComboBox<Condition> condCombo, JTextField valueField,
+          int filterIndex)
+  {
+    String attName = (String) attCombo.getSelectedItem();
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    String pattern = valueField.getText().trim();
+
+    setNumericHints(attName, cond, valueField);
+
+    if (pattern.length() == 0 && cond.needsAPattern())
+    {
+      valueField.setEnabled(true); // ensure pattern field is enabled!
+      return false;
+    }
+
+    /*
+     * Construct a matcher that operates on Label, Score, 
+     * or named attribute
+     */
+    FeatureMatcherI km = null;
+    if (LABEL_18N.equals(attName))
+    {
+      km = FeatureMatcher.byLabel(cond, pattern);
+    }
+    else if (SCORE_18N.equals(attName))
+    {
+      km = FeatureMatcher.byScore(cond, pattern);
+    }
+    else
+    {
+      km = FeatureMatcher.byAttribute(cond, pattern,
+              FeatureMatcher.fromAttributeDisplayName(attName));
+    }
+
+    filters.set(filterIndex, km);
+
+    return true;
+  }
+
+  /**
+   * Makes the dialog visible, at the Feature Colour tab or at the Filters tab
+   * 
+   * @param coloursTab
+   */
+  public void showTab(boolean coloursTab)
+  {
+    setVisible(true);
+    tabbedPane.setSelectedIndex(coloursTab ? 0 : 1);
+  }
+
+  /**
+   * Action on any change to feature filtering, namely
+   * <ul>
+   * <li>change of selected attribute</li>
+   * <li>change of selected condition</li>
+   * <li>change of match pattern</li>
+   * <li>removal of a condition</li>
+   * </ul>
+   * The inputs are parsed into a combined filter and this is set for the feature
+   * type, and the alignment redrawn.
+   */
+  protected void filtersChanged()
+  {
+    /*
+     * update the filter conditions for the feature type
+     */
+    boolean anded = andFilters.isSelected();
+    FeatureMatcherSetI combined = new FeatureMatcherSet();
+
+    for (FeatureMatcherI filter : filters)
+    {
+      String pattern = filter.getMatcher().getPattern();
+      Condition condition = filter.getMatcher().getCondition();
+      if (pattern.trim().length() > 0 || !condition.needsAPattern())
+      {
+        if (anded)
+        {
+          combined.and(filter);
+        }
+        else
+        {
+          combined.or(filter);
+        }
+      }
+    }
+
+    /*
+     * save the filter conditions in the FeatureRenderer
+     * (note this might now be an empty filter with no conditions)
+     */
+    fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
+    ap.paintAlignment(true, true);
+
+    updateFiltersTab();
+  }
+}
index f3c8e8f..c66f304 100755 (executable)
@@ -134,7 +134,7 @@ public class FontChooser extends GFontChooser
       fontAsCdna.setSelected(ap.av.isProteinFontAsCdna());
     }
 
-    if (tp != null)
+    if (isTreeFont())
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("action.change_font_tree_panel"),
@@ -229,7 +229,11 @@ public class FontChooser extends GFontChooser
   @Override
   protected void cancel_actionPerformed()
   {
-    if (ap != null)
+    if (isTreeFont())
+    {
+      tp.setTreeFont(oldFont);
+    }
+    else if (ap != null)
     {
       ap.av.setFont(oldFont, true);
       ap.av.setScaleProteinAsCdna(oldProteinScale);
@@ -250,10 +254,6 @@ public class FontChooser extends GFontChooser
         splitFrame.repaint();
       }
     }
-    else if (tp != null)
-    {
-      tp.setTreeFont(oldFont);
-    }
 
     try
     {
@@ -263,6 +263,11 @@ public class FontChooser extends GFontChooser
     }
   }
 
+  private boolean isTreeFont()
+  {
+    return tp != null;
+  }
+
   /**
    * DOCUMENT ME!
    */
@@ -317,7 +322,7 @@ public class FontChooser extends GFontChooser
       }
       return;
     }
-    if (tp != null)
+    if (isTreeFont())
     {
       tp.setTreeFont(newFont);
     }
index f2761ab..a183144 100755 (executable)
@@ -108,8 +108,7 @@ public class IdPanel extends JPanel
       SequenceI sequence = av.getAlignment().getSequenceAt(seq);
       StringBuilder tip = new StringBuilder(64);
       seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
-              av.isShowDBRefs(), av.isShowNPFeats(),
-              sp.seqCanvas.fr.getMinMax());
+              av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
       setToolTipText(JvSwingUtils.wrapTooltip(true,
               sequence.getDisplayId(true) + " " + tip.toString()));
     }
@@ -332,7 +331,8 @@ public class IdPanel extends JPanel
      *  and any non-positional features
      */
     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-    for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
+    List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
+    for (SequenceFeature sf : features)
     {
       if (sf.links != null)
       {
@@ -343,7 +343,7 @@ public class IdPanel extends JPanel
       }
     }
 
-    PopupMenu pop = new PopupMenu(alignPanel, sq, nlinks,
+    PopupMenu pop = new PopupMenu(alignPanel, sq, features,
             Preferences.getGroupURLLinks());
     pop.show(this, e.getX(), e.getY());
   }
index 1018d6e..8d62433 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.gui;
 
-import jalview.bin.Cache;
 import jalview.util.MessageManager;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
@@ -42,7 +41,6 @@ import java.util.List;
 import java.util.Vector;
 
 import javax.swing.JButton;
-import javax.swing.JFrame;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
@@ -518,54 +516,6 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
     lstners.remove(actionListener);
   }
 
-  public static void main(String args[])
-  {
-    Cache.getDasSourceRegistry();
-    JDatabaseTree jdt = new JDatabaseTree(new jalview.ws.SequenceFetcher());
-    JFrame foo = new JFrame();
-    foo.setLayout(new BorderLayout());
-    foo.add(jdt.getDatabaseSelectorButton(), BorderLayout.CENTER);
-    foo.pack();
-    foo.setVisible(true);
-    int nultimes = 5;
-    final Thread us = Thread.currentThread();
-    jdt.addActionListener(new ActionListener()
-    {
-
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        us.interrupt();
-      }
-    });
-    do
-    {
-      try
-      {
-        Thread.sleep(50);
-      } catch (InterruptedException x)
-      {
-        nultimes--;
-        if (!jdt.hasSelection())
-        {
-          System.out.println("No Selection");
-        }
-        else
-        {
-          System.out.println("Selection: " + jdt.getSelectedItem());
-          int s = 1;
-          for (DbSourceProxy pr : jdt.getSelectedSources())
-          {
-            System.out.println("Source " + s++ + ": " + pr.getDbName()
-                    + " (" + pr.getDbSource() + ") Version "
-                    + pr.getDbVersion() + ". Test:\t" + pr.getTestQuery());
-          }
-          System.out.println("Test queries: " + jdt.getExampleQueries());
-        }
-      }
-    } while (nultimes > 0 && foo.isVisible());
-    foo.setVisible(false);
-  }
 
   @Override
   public void keyPressed(KeyEvent arg0)
index c7ec757..9285754 100644 (file)
@@ -37,6 +37,10 @@ import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.StructureViewerModel;
 import jalview.datamodel.StructureViewerModel.StructureData;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.ext.varna.RnaModel;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
@@ -48,6 +52,8 @@ import jalview.schemabinding.version2.Annotation;
 import jalview.schemabinding.version2.AnnotationColours;
 import jalview.schemabinding.version2.AnnotationElement;
 import jalview.schemabinding.version2.CalcIdParam;
+import jalview.schemabinding.version2.Colour;
+import jalview.schemabinding.version2.CompoundMatcher;
 import jalview.schemabinding.version2.DBRef;
 import jalview.schemabinding.version2.Features;
 import jalview.schemabinding.version2.Group;
@@ -60,6 +66,8 @@ import jalview.schemabinding.version2.MapListFrom;
 import jalview.schemabinding.version2.MapListTo;
 import jalview.schemabinding.version2.Mapping;
 import jalview.schemabinding.version2.MappingChoice;
+import jalview.schemabinding.version2.MatchCondition;
+import jalview.schemabinding.version2.MatcherSet;
 import jalview.schemabinding.version2.OtherData;
 import jalview.schemabinding.version2.PdbentryItem;
 import jalview.schemabinding.version2.Pdbids;
@@ -75,6 +83,9 @@ import jalview.schemabinding.version2.ThresholdLine;
 import jalview.schemabinding.version2.Tree;
 import jalview.schemabinding.version2.UserColours;
 import jalview.schemabinding.version2.Viewport;
+import jalview.schemabinding.version2.types.ColourThreshTypeType;
+import jalview.schemabinding.version2.types.FeatureMatcherByType;
+import jalview.schemabinding.version2.types.NoValueColour;
 import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
@@ -83,10 +94,12 @@ import jalview.schemes.ResidueProperties;
 import jalview.schemes.UserColourScheme;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.util.StringUtils;
 import jalview.util.jarInputStreamProvider;
+import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.ViewportRanges;
 import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
@@ -115,6 +128,7 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -879,15 +893,33 @@ public class Jalview2XML
         }
         if (sf.otherDetails != null)
         {
-          String key;
-          Iterator<String> keys = sf.otherDetails.keySet().iterator();
-          while (keys.hasNext())
+          /*
+           * save feature attributes, which may be simple strings or
+           * map valued (have sub-attributes)
+           */
+          for (Entry<String, Object> entry : sf.otherDetails.entrySet())
           {
-            key = keys.next();
-            OtherData keyValue = new OtherData();
-            keyValue.setKey(key);
-            keyValue.setValue(sf.otherDetails.get(key).toString());
-            features.addOtherData(keyValue);
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (value instanceof Map<?, ?>)
+            {
+              for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
+                      .entrySet())
+              {
+                OtherData otherData = new OtherData();
+                otherData.setKey(key);
+                otherData.setKey2(subAttribute.getKey());
+                otherData.setValue(subAttribute.getValue().toString());
+                features.addOtherData(otherData);
+              }
+            }
+            else
+            {
+              OtherData otherData = new OtherData();
+              otherData.setKey(key);
+              otherData.setValue(value.toString());
+              features.addOtherData(otherData);
+            }
           }
         }
 
@@ -1313,19 +1345,33 @@ public class Jalview2XML
       {
         jalview.schemabinding.version2.FeatureSettings fs = new jalview.schemabinding.version2.FeatureSettings();
 
-        String[] renderOrder = ap.getSeqPanel().seqCanvas
-                .getFeatureRenderer().getRenderOrder()
-                .toArray(new String[0]);
+        FeatureRenderer fr = ap.getSeqPanel().seqCanvas
+                .getFeatureRenderer();
+        String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
 
         Vector<String> settingsAdded = new Vector<>();
         if (renderOrder != null)
         {
           for (String featureType : renderOrder)
           {
-            FeatureColourI fcol = ap.getSeqPanel().seqCanvas
-                    .getFeatureRenderer().getFeatureStyle(featureType);
             Setting setting = new Setting();
             setting.setType(featureType);
+
+            /*
+             * save any filter for the feature type
+             */
+            FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
+            if (filter != null)  {
+              Iterator<FeatureMatcherI> filters = filter.getMatchers().iterator();
+              FeatureMatcherI firstFilter = filters.next();
+              setting.setMatcherSet(Jalview2XML.marshalFilter(
+                      firstFilter, filters, filter.isAnded()));
+            }
+
+            /*
+             * save colour scheme for the feature type
+             */
+            FeatureColourI fcol = fr.getFeatureStyle(featureType);
             if (!fcol.isSimpleColour())
             {
               setting.setColour(fcol.getMaxColour().getRGB());
@@ -1333,8 +1379,25 @@ public class Jalview2XML
               setting.setMin(fcol.getMin());
               setting.setMax(fcol.getMax());
               setting.setColourByLabel(fcol.isColourByLabel());
+              if (fcol.isColourByAttribute())
+              {
+                setting.setAttributeName(fcol.getAttributeName());
+              }
               setting.setAutoScale(fcol.isAutoScaled());
               setting.setThreshold(fcol.getThreshold());
+              Color noColour = fcol.getNoColour();
+              if (noColour == null)
+              {
+                setting.setNoValueColour(NoValueColour.NONE);
+              }
+              else if (noColour.equals(fcol.getMaxColour()))
+              {
+                setting.setNoValueColour(NoValueColour.MAX);
+              }
+              else
+              {
+                setting.setNoValueColour(NoValueColour.MIN);
+              }
               // -1 = No threshold, 0 = Below, 1 = Above
               setting.setThreshstate(fcol.isAboveThreshold() ? 1
                       : (fcol.isBelowThreshold() ? 0 : -1));
@@ -1346,7 +1409,7 @@ public class Jalview2XML
 
             setting.setDisplay(
                     av.getFeaturesDisplayed().isVisible(featureType));
-            float rorder = ap.getSeqPanel().seqCanvas.getFeatureRenderer()
+            float rorder = fr
                     .getOrder(featureType);
             if (rorder > -1)
             {
@@ -1358,8 +1421,7 @@ public class Jalview2XML
         }
 
         // is groups actually supposed to be a map here ?
-        Iterator<String> en = ap.getSeqPanel().seqCanvas
-                .getFeatureRenderer().getFeatureGroups().iterator();
+        Iterator<String> en = fr.getFeatureGroups().iterator();
         Vector<String> groupsAdded = new Vector<>();
         while (en.hasNext())
         {
@@ -1370,8 +1432,7 @@ public class Jalview2XML
           }
           Group g = new Group();
           g.setName(grp);
-          g.setDisplay(((Boolean) ap.getSeqPanel().seqCanvas
-                  .getFeatureRenderer().checkGroupVisibility(grp, false))
+          g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
                           .booleanValue());
           fs.addGroup(g);
           groupsAdded.addElement(grp);
@@ -2963,19 +3024,46 @@ public class Jalview2XML
                     features[f].getEnd(), features[f].getScore(),
                     features[f].getFeatureGroup());
             sf.setStatus(features[f].getStatus());
+
+            /*
+             * load any feature attributes - include map-valued attributes
+             */
+            Map<String, Map<String, String>> mapAttributes = new HashMap<>();
             for (int od = 0; od < features[f].getOtherDataCount(); od++)
             {
               OtherData keyValue = features[f].getOtherData(od);
-              if (keyValue.getKey().startsWith("LINK"))
+              String attributeName = keyValue.getKey();
+              String attributeValue = keyValue.getValue();
+              if (attributeName.startsWith("LINK"))
               {
-                sf.addLink(keyValue.getValue());
+                sf.addLink(attributeValue);
               }
               else
               {
-                sf.setValue(keyValue.getKey(), keyValue.getValue());
+                String subAttribute = keyValue.getKey2();
+                if (subAttribute == null)
+                {
+                  // simple string-valued attribute
+                  sf.setValue(attributeName, attributeValue);
+                }
+                else
+                {
+                  // attribute 'key' has sub-attribute 'key2'
+                  if (!mapAttributes.containsKey(attributeName))
+                  {
+                    mapAttributes.put(attributeName, new HashMap<>());
+                  }
+                  mapAttributes.get(attributeName).put(subAttribute,
+                          attributeValue);
+                }
               }
-
             }
+            for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
+                    .entrySet())
+            {
+              sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
+            }
+
             // adds feature to datasequence's feature set (since Jalview 2.10)
             al.getSequenceAt(i).addSequenceFeature(sf);
           }
@@ -4551,9 +4639,11 @@ public class Jalview2XML
       af.viewport.setShowGroupConservation(false);
     }
 
-    // recover featre settings
+    // recover feature settings
     if (jms.getFeatureSettings() != null)
     {
+      FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas
+              .getFeatureRenderer();
       FeaturesDisplayed fdi;
       af.viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
       String[] renderOrder = new String[jms.getFeatureSettings()
@@ -4565,14 +4655,51 @@ public class Jalview2XML
               .getSettingCount(); fs++)
       {
         Setting setting = jms.getFeatureSettings().getSetting(fs);
+        String featureType = setting.getType();
+
+        /*
+         * restore feature filters (if any)
+         */
+        MatcherSet filters = setting.getMatcherSet();
+        if (filters != null)
+        {
+          FeatureMatcherSetI filter = Jalview2XML
+                  .unmarshalFilter(featureType, filters);
+          if (!filter.isEmpty())
+          {
+            fr.setFeatureFilter(featureType, filter);
+          }
+        }
+
+        /*
+         * restore feature colour scheme
+         */
+        Color maxColour = new Color(setting.getColour());
         if (setting.hasMincolour())
         {
-          FeatureColourI gc = setting.hasMin()
-                  ? new FeatureColour(new Color(setting.getMincolour()),
-                          new Color(setting.getColour()), setting.getMin(),
-                          setting.getMax())
-                  : new FeatureColour(new Color(setting.getMincolour()),
-                          new Color(setting.getColour()), 0, 1);
+          /*
+           * minColour is always set unless a simple colour
+           * (including for colour by label though it doesn't use it)
+           */
+          Color minColour = new Color(setting.getMincolour());
+          Color noValueColour = minColour;
+          NoValueColour noColour = setting.getNoValueColour();
+          if (noColour == NoValueColour.NONE)
+          {
+            noValueColour = null;
+          }
+          else if (noColour == NoValueColour.MAX)
+          {
+            noValueColour = maxColour;
+          }
+          float min = setting.hasMin() ? setting.getMin() : 0f;
+          float max = setting.hasMin() ? setting.getMax() : 1f;
+          FeatureColourI gc = new FeatureColour(minColour, maxColour,
+                  noValueColour, min, max);
+          if (setting.getAttributeNameCount() > 0)
+          {
+            gc.setAttributeName(setting.getAttributeName());
+          }
           if (setting.hasThreshold())
           {
             gc.setThreshold(setting.getThreshold());
@@ -4597,26 +4724,26 @@ public class Jalview2XML
             gc.setColourByLabel(setting.getColourByLabel());
           }
           // and put in the feature colour table.
-          featureColours.put(setting.getType(), gc);
+          featureColours.put(featureType, gc);
         }
         else
         {
-          featureColours.put(setting.getType(),
-                  new FeatureColour(new Color(setting.getColour())));
+          featureColours.put(featureType,
+                  new FeatureColour(maxColour));
         }
-        renderOrder[fs] = setting.getType();
+        renderOrder[fs] = featureType;
         if (setting.hasOrder())
         {
-          featureOrder.put(setting.getType(), setting.getOrder());
+          featureOrder.put(featureType, setting.getOrder());
         }
         else
         {
-          featureOrder.put(setting.getType(), new Float(
+          featureOrder.put(featureType, new Float(
                   fs / jms.getFeatureSettings().getSettingCount()));
         }
         if (setting.getDisplay())
         {
-          fdi.setVisible(setting.getType());
+          fdi.setVisible(featureType);
         }
       }
       Map<String, Boolean> fgtable = new Hashtable<>();
@@ -4630,9 +4757,7 @@ public class Jalview2XML
       // jms.getFeatureSettings().getTransparency() : 0.0, featureOrder);
       FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
               fgtable, featureColours, 1.0f, featureOrder);
-      af.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
-              .transferSettings(frs);
-
+      fr.transferSettings(frs);
     }
 
     if (view.getHiddenColumnsCount() > 0)
@@ -5334,7 +5459,7 @@ public class Jalview2XML
 
     if (this.frefedSequence == null)
     {
-      frefedSequence = new Vector<SeqFref>();
+      frefedSequence = new Vector<>();
     }
 
     viewportsAdded.clear();
@@ -5586,4 +5711,289 @@ public class Jalview2XML
   {
     return counter++;
   }
+
+  /**
+   * Populates an XML model of the feature colour scheme for one feature type
+   * 
+   * @param featureType
+   * @param fcol
+   * @return
+   */
+  protected static jalview.schemabinding.version2.Colour marshalColour(
+          String featureType, FeatureColourI fcol)
+  {
+    jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
+    if (fcol.isSimpleColour())
+    {
+      col.setRGB(Format.getHexString(fcol.getColour()));
+    }
+    else
+    {
+      col.setRGB(Format.getHexString(fcol.getMaxColour()));
+      col.setMin(fcol.getMin());
+      col.setMax(fcol.getMax());
+      col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour()));
+      col.setAutoScale(fcol.isAutoScaled());
+      col.setThreshold(fcol.getThreshold());
+      col.setColourByLabel(fcol.isColourByLabel());
+      col.setThreshType(fcol.isAboveThreshold() ? ColourThreshTypeType.ABOVE
+              : (fcol.isBelowThreshold() ? ColourThreshTypeType.BELOW
+                      : ColourThreshTypeType.NONE));
+      if (fcol.isColourByAttribute())
+      {
+        col.setAttributeName(fcol.getAttributeName());
+      }
+      Color noColour = fcol.getNoColour();
+      if (noColour == null)
+      {
+        col.setNoValueColour(NoValueColour.NONE);
+      }
+      else if (noColour == fcol.getMaxColour())
+      {
+        col.setNoValueColour(NoValueColour.MAX);
+      }
+      else
+      {
+        col.setNoValueColour(NoValueColour.MIN);
+      }
+    }
+    col.setName(featureType);
+    return col;
+  }
+
+  /**
+   * Populates an XML model of the feature filter(s) for one feature type
+   * 
+   * @param firstMatcher
+   *          the first (or only) match condition)
+   * @param filter
+   *          remaining match conditions (if any)
+   * @param and
+   *          if true, conditions are and-ed, else or-ed
+   */
+  protected static MatcherSet marshalFilter(FeatureMatcherI firstMatcher,
+          Iterator<FeatureMatcherI> filters, boolean and)
+  {
+    MatcherSet result = new MatcherSet();
+  
+    if (filters.hasNext())
+    {
+      /*
+       * compound matcher
+       */
+      CompoundMatcher compound = new CompoundMatcher();
+      compound.setAnd(and);
+      MatcherSet matcher1 = marshalFilter(firstMatcher,
+              Collections.emptyIterator(), and);
+      compound.addMatcherSet(matcher1);
+      FeatureMatcherI nextMatcher = filters.next();
+      MatcherSet matcher2 = marshalFilter(nextMatcher, filters, and);
+      compound.addMatcherSet(matcher2);
+      result.setCompoundMatcher(compound);
+    }
+    else
+    {
+      /*
+       * single condition matcher
+       */
+      MatchCondition matcherModel = new MatchCondition();
+      matcherModel.setCondition(
+              firstMatcher.getMatcher().getCondition().getStableName());
+      matcherModel.setValue(firstMatcher.getMatcher().getPattern());
+      if (firstMatcher.isByAttribute())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYATTRIBUTE);
+        matcherModel.setAttributeName(firstMatcher.getAttribute());
+      }
+      else if (firstMatcher.isByLabel())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYLABEL);
+      }
+      else if (firstMatcher.isByScore())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYSCORE);
+      }
+      result.setMatchCondition(matcherModel);
+    }
+  
+    return result;
+  }
+
+  /**
+   * Loads one XML model of a feature filter to a Jalview object
+   * 
+   * @param featureType
+   * @param matcherSetModel
+   * @return
+   */
+  protected static FeatureMatcherSetI unmarshalFilter(
+          String featureType, MatcherSet matcherSetModel)
+  {
+    FeatureMatcherSetI result = new FeatureMatcherSet();
+    try
+    {
+      unmarshalFilterConditions(result, matcherSetModel, true);
+    } catch (IllegalStateException e)
+    {
+      // mixing AND and OR conditions perhaps
+      System.err.println(
+              String.format("Error reading filter conditions for '%s': %s",
+                      featureType, e.getMessage()));
+      // return as much as was parsed up to the error
+    }
+  
+    return result;
+  }
+
+  /**
+   * Adds feature match conditions to matcherSet as unmarshalled from XML
+   * (possibly recursively for compound conditions)
+   * 
+   * @param matcherSet
+   * @param matcherSetModel
+   * @param and
+   *          if true, multiple conditions are AND-ed, else they are OR-ed
+   * @throws IllegalStateException
+   *           if AND and OR conditions are mixed
+   */
+  protected static void unmarshalFilterConditions(
+          FeatureMatcherSetI matcherSet, MatcherSet matcherSetModel,
+          boolean and)
+  {
+    MatchCondition mc = matcherSetModel.getMatchCondition();
+    if (mc != null)
+    {
+      /*
+       * single condition
+       */
+      FeatureMatcherByType filterBy = mc.getBy();
+      Condition cond = Condition.fromString(mc.getCondition());
+      String pattern = mc.getValue();
+      FeatureMatcherI matchCondition = null;
+      if (filterBy == FeatureMatcherByType.BYLABEL)
+      {
+        matchCondition = FeatureMatcher.byLabel(cond, pattern);
+      }
+      else if (filterBy == FeatureMatcherByType.BYSCORE)
+      {
+        matchCondition = FeatureMatcher.byScore(cond, pattern);
+  
+      }
+      else if (filterBy == FeatureMatcherByType.BYATTRIBUTE)
+      {
+        String[] attNames = mc.getAttributeName();
+        matchCondition = FeatureMatcher.byAttribute(cond, pattern,
+                attNames);
+      }
+  
+      /*
+       * note this throws IllegalStateException if AND-ing to a 
+       * previously OR-ed compound condition, or vice versa
+       */
+      if (and)
+      {
+        matcherSet.and(matchCondition);
+      }
+      else
+      {
+        matcherSet.or(matchCondition);
+      }
+    }
+    else
+    {
+      /*
+       * compound condition
+       */
+      MatcherSet[] matchers = matcherSetModel.getCompoundMatcher()
+              .getMatcherSet();
+      boolean anded = matcherSetModel.getCompoundMatcher().getAnd();
+      if (matchers.length == 2)
+      {
+        unmarshalFilterConditions(matcherSet, matchers[0], anded);
+        unmarshalFilterConditions(matcherSet, matchers[1], anded);
+      }
+      else
+      {
+        System.err.println("Malformed compound filter condition");
+      }
+    }
+  }
+
+  /**
+   * Loads one XML model of a feature colour to a Jalview object
+   * 
+   * @param colourModel
+   * @return
+   */
+  protected static FeatureColourI unmarshalColour(
+          jalview.schemabinding.version2.Colour colourModel)
+  {
+    FeatureColourI colour = null;
+  
+    if (colourModel.hasMax())
+    {
+      Color mincol = null;
+      Color maxcol = null;
+      Color noValueColour = null;
+  
+      try
+      {
+        mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
+        maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
+      } catch (Exception e)
+      {
+        Cache.log.warn("Couldn't parse out graduated feature color.", e);
+      }
+  
+      NoValueColour noCol = colourModel.getNoValueColour();
+      if (noCol == NoValueColour.MIN)
+      {
+        noValueColour = mincol;
+      }
+      else if (noCol == NoValueColour.MAX)
+      {
+        noValueColour = maxcol;
+      }
+  
+      colour = new FeatureColour(mincol, maxcol, noValueColour,
+              colourModel.getMin(),
+              colourModel.getMax());
+      String[] attributes = colourModel.getAttributeName();
+      if (attributes != null && attributes.length > 0)
+      {
+        colour.setAttributeName(attributes);
+      }
+      if (colourModel.hasAutoScale())
+      {
+        colour.setAutoScaled(colourModel.getAutoScale());
+      }
+      if (colourModel.hasColourByLabel())
+      {
+        colour.setColourByLabel(colourModel.getColourByLabel());
+      }
+      if (colourModel.hasThreshold())
+      {
+        colour.setThreshold(colourModel.getThreshold());
+      }
+      ColourThreshTypeType ttyp = colourModel.getThreshType();
+      if (ttyp != null)
+      {
+        if (ttyp == ColourThreshTypeType.ABOVE)
+        {
+          colour.setAboveThreshold(true);
+        }
+        else if (ttyp == ColourThreshTypeType.BELOW)
+        {
+          colour.setBelowThreshold(true);
+        }
+      }
+    }
+    else
+    {
+      Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
+      colour = new FeatureColour(color);
+    }
+  
+    return colour;
+  }
 }
index 05f5ffc..1d7bf3d 100644 (file)
@@ -27,8 +27,8 @@ import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
-import java.awt.event.WindowListener;
 
 import javax.swing.JButton;
 import javax.swing.JDialog;
@@ -118,55 +118,14 @@ public abstract class JalviewDialog extends JPanel
         closeDialog();
       }
     });
-    frame.addWindowListener(new WindowListener()
+    frame.addWindowListener(new WindowAdapter()
     {
-
-      @Override
-      public void windowOpened(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowIconified(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowDeiconified(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowDeactivated(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
       @Override
       public void windowClosing(WindowEvent e)
       {
         // user has cancelled the dialog
         closeDialog();
       }
-
-      @Override
-      public void windowClosed(WindowEvent e)
-      {
-      }
-
-      @Override
-      public void windowActivated(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
     });
   }
 
@@ -177,8 +136,8 @@ public abstract class JalviewDialog extends JPanel
   {
     try
     {
-      frame.dispose();
       raiseClosed();
+      frame.dispose();
     } catch (Exception ex)
     {
     }
index 0a765cb..4658668 100644 (file)
@@ -24,14 +24,20 @@ import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Font;
 import java.awt.GridLayout;
 import java.awt.Rectangle;
 import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.List;
 import java.util.Objects;
 
 import javax.swing.AbstractButton;
+import javax.swing.BorderFactory;
 import javax.swing.JButton;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JMenu;
@@ -39,6 +45,8 @@ import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JScrollBar;
 import javax.swing.SwingConstants;
+import javax.swing.border.Border;
+import javax.swing.border.TitledBorder;
 
 /**
  * useful functions for building Swing GUIs
@@ -304,4 +312,71 @@ public final class JvSwingUtils
     comp.setFont(JvSwingUtils.getLabelFont());
   }
 
+  /**
+   * A helper method to build a drop-down choice of values, with tooltips for
+   * the entries
+   * 
+   * @param entries
+   * @param tooltips
+   */
+  public static JComboBox<String> buildComboWithTooltips(
+          List<String> entries, List<String> tooltips)
+  {
+    JComboBox<String> combo = new JComboBox<>();
+    final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
+    combo.setRenderer(renderer);
+    for (String attName : entries)
+    {
+      combo.addItem(attName);
+    }
+    renderer.setTooltips(tooltips);
+    final MouseAdapter mouseListener = new MouseAdapter()
+    {
+      @Override
+      public void mouseEntered(MouseEvent e)
+      {
+        int j = combo.getSelectedIndex();
+        if (j > -1)
+        {
+          combo.setToolTipText(tooltips.get(j));
+        }
+      }
+      @Override
+      public void mouseExited(MouseEvent e)
+      {
+        combo.setToolTipText(null);
+      }
+    };
+    for (Component c : combo.getComponents())
+    {
+      c.addMouseListener(mouseListener);
+    }
+    return combo;
+  }
+
+  /**
+   * Adds a titled border to the component in the default font and position (top
+   * left), optionally witht italic text
+   * 
+   * @param comp
+   * @param title
+   * @param italic
+   */
+  public static TitledBorder createTitledBorder(JComponent comp,
+          String title, boolean italic)
+  {
+    Font font = comp.getFont();
+    if (italic)
+    {
+      font = new Font(font.getName(), Font.ITALIC, font.getSize());
+    }
+    Border border = BorderFactory.createTitledBorder("");
+    TitledBorder titledBorder = BorderFactory.createTitledBorder(border,
+            title, TitledBorder.LEADING, TitledBorder.DEFAULT_POSITION,
+            font);
+    comp.setBorder(titledBorder);
+
+    return titledBorder;
+  }
+
 }
index 1f55ea6..cc361a5 100644 (file)
@@ -160,7 +160,6 @@ public class OverviewCanvas extends JComponent
               od.getGraphHeight(), od.getColumns(av.getAlignment()));
       mg.translate(0, -od.getSequencesHeight());
     }
-    System.gc();
 
     or.removePropertyChangeListener(progressPanel);
     or = null;
index 9f52d26..7ceceee 100644 (file)
@@ -464,7 +464,16 @@ public class PCAPanel extends GPCAPanel
     public void run()
     {
       PrinterJob printJob = PrinterJob.getPrinterJob();
-      PageFormat pf = printJob.pageDialog(printJob.defaultPage());
+      PageFormat defaultPage = printJob.defaultPage();
+      PageFormat pf = printJob.pageDialog(defaultPage);
+
+      if (defaultPage == pf)
+      {
+        /*
+         * user cancelled
+         */
+        return;
+      }
 
       printJob.setPrintable(this, pf);
 
index 759c63b..ed3d29a 100644 (file)
@@ -34,7 +34,6 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
-import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -50,6 +49,7 @@ import jalview.schemes.PIDColourScheme;
 import jalview.util.GroupUrlLink;
 import jalview.util.GroupUrlLink.UrlStringTooLongException;
 import jalview.util.MessageManager;
+import jalview.util.StringUtils;
 import jalview.util.UrlLink;
 
 import java.awt.Color;
@@ -176,25 +176,31 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * Creates a new PopupMenu object.
    * 
    * @param ap
-   *          DOCUMENT ME!
    * @param seq
-   *          DOCUMENT ME!
+   * @param features
+   *          non-positional features (for seq not null), or positional features
+   *          at residue (for seq equal to null)
    */
-  public PopupMenu(final AlignmentPanel ap, Sequence seq,
-          List<String> links)
+  public PopupMenu(final AlignmentPanel ap, SequenceI seq,
+          List<SequenceFeature> features)
   {
-    this(ap, seq, links, null);
+    this(ap, seq, features, null);
   }
 
   /**
+   * Constructor
    * 
-   * @param ap
+   * @param alignPanel
    * @param seq
-   * @param links
+   *          the sequence under the cursor if in the Id panel, null if in the
+   *          sequence panel
+   * @param features
+   *          non-positional features if in the Id panel, features at the
+   *          clicked residue if in the sequence panel
    * @param groupLinks
    */
-  public PopupMenu(final AlignmentPanel ap, final SequenceI seq,
-          List<String> links, List<String> groupLinks)
+  public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
+          List<SequenceFeature> features, List<String> groupLinks)
   {
     // /////////////////////////////////////////////////////////
     // If this is activated from the sequence panel, the user may want to
@@ -202,7 +208,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     //
     // If from the IDPanel, we must display the sequence menu
     // ////////////////////////////////////////////////////////
-    this.ap = ap;
+    this.ap = alignPanel;
     sequence = seq;
 
     for (String ff : FileFormats.getInstance().getWritableFormats(true))
@@ -237,9 +243,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     /*
      * And repeat for the current selection group (if there is one):
      */
-    final List<SequenceI> selectedGroup = (ap.av.getSelectionGroup() == null
+    final List<SequenceI> selectedGroup = (alignPanel.av.getSelectionGroup() == null
             ? Collections.<SequenceI> emptyList()
-            : ap.av.getSelectionGroup().getSequences());
+            : alignPanel.av.getSelectionGroup().getSequences());
     buildAnnotationTypesMenus(groupShowAnnotationsMenu,
             groupHideAnnotationsMenu, selectedGroup);
     configureReferenceAnnotationsMenu(groupAddReferenceAnnotations,
@@ -257,7 +263,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     if (seq != null)
     {
       sequenceMenu.setText(sequence.getName());
-      if (seq == ap.av.getAlignment().getSeqrep())
+      if (seq == alignPanel.av.getAlignment().getSeqrep())
       {
         makeReferenceSeq.setText(
                 MessageManager.getString("action.unmark_as_reference"));
@@ -268,7 +274,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 MessageManager.getString("action.set_as_reference"));
       }
 
-      if (!ap.av.getAlignment().isNucleotide())
+      if (!alignPanel.av.getAlignment().isNucleotide())
       {
         remove(rnaStructureMenu);
       }
@@ -279,7 +285,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
          * add menu items to 2D-render any alignment or sequence secondary
          * structure annotation
          */
-        AlignmentAnnotation[] aas = ap.av.getAlignment()
+        AlignmentAnnotation[] aas = alignPanel.av.getAlignment()
                 .getAlignmentAnnotation();
         if (aas != null)
         {
@@ -299,7 +305,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 @Override
                 public void actionPerformed(ActionEvent e)
                 {
-                  new AppVarna(seq, aa, ap);
+                  new AppVarna(seq, aa, alignPanel);
                 }
               });
               rnaStructureMenu.add(menuItem);
@@ -328,7 +334,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 public void actionPerformed(ActionEvent e)
                 {
                   // TODO: VARNA does'nt print gaps in the sequence
-                  new AppVarna(seq, aa, ap);
+                  new AppVarna(seq, aa, alignPanel);
                 }
               });
               rnaStructureMenu.add(menuItem);
@@ -353,8 +359,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       });
       add(menuItem);
 
-      if (ap.av.getSelectionGroup() != null
-              && ap.av.getSelectionGroup().getSize() > 1)
+      if (alignPanel.av.getSelectionGroup() != null
+              && alignPanel.av.getSelectionGroup().getSize() > 1)
       {
         menuItem = new JMenuItem(MessageManager
                 .formatMessage("label.represent_group_with", new Object[]
@@ -370,12 +376,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceMenu.add(menuItem);
       }
 
-      if (ap.av.hasHiddenRows())
+      if (alignPanel.av.hasHiddenRows())
       {
-        final int index = ap.av.getAlignment().findIndex(seq);
+        final int index = alignPanel.av.getAlignment().findIndex(seq);
 
-        if (ap.av.adjustForHiddenSeqs(index)
-                - ap.av.adjustForHiddenSeqs(index - 1) > 1)
+        if (alignPanel.av.adjustForHiddenSeqs(index)
+                - alignPanel.av.adjustForHiddenSeqs(index - 1) > 1)
         {
           menuItem = new JMenuItem(
                   MessageManager.getString("action.reveal_sequences"));
@@ -384,10 +390,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             @Override
             public void actionPerformed(ActionEvent e)
             {
-              ap.av.showSequence(index);
-              if (ap.overviewPanel != null)
+              alignPanel.av.showSequence(index);
+              if (alignPanel.overviewPanel != null)
               {
-                ap.overviewPanel.updateOverviewImage();
+                alignPanel.overviewPanel.updateOverviewImage();
               }
             }
           });
@@ -396,7 +402,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     }
     // for the case when no sequences are even visible
-    if (ap.av.hasHiddenRows())
+    if (alignPanel.av.hasHiddenRows())
     {
       {
         menuItem = new JMenuItem(
@@ -406,10 +412,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
           @Override
           public void actionPerformed(ActionEvent e)
           {
-            ap.av.showAllHiddenSeqs();
-            if (ap.overviewPanel != null)
+            alignPanel.av.showAllHiddenSeqs();
+            if (alignPanel.overviewPanel != null)
             {
-              ap.overviewPanel.updateOverviewImage();
+              alignPanel.overviewPanel.updateOverviewImage();
             }
           }
         });
@@ -418,9 +424,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     }
 
-    SequenceGroup sg = ap.av.getSelectionGroup();
+    SequenceGroup sg = alignPanel.av.getSelectionGroup();
     boolean isDefinedGroup = (sg != null)
-            ? ap.av.getAlignment().getGroups().contains(sg)
+            ? alignPanel.av.getAlignment().getGroups().contains(sg)
             : false;
 
     if (sg != null && sg.getSize() > 0)
@@ -458,7 +464,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       Hashtable<String, PDBEntry> pdbe = new Hashtable<>(), reppdb = new Hashtable<>();
 
       SequenceI sqass = null;
-      for (SequenceI sq : ap.av.getSequenceSelection())
+      for (SequenceI sq : alignPanel.av.getSequenceSelection())
       {
         Vector<PDBEntry> pes = sq.getDatasetSequence().getAllPDBEntries();
         if (pes != null && pes.size() > 0)
@@ -508,24 +514,133 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       rnaStructureMenu.setVisible(false);
     }
 
-    if (links != null && links.size() > 0)
+    addLinks(seq, features);
+
+    if (seq == null)
+    {
+      addFeatureDetails(features);
+    }
+  }
+
+  /**
+   * Add a link to show feature details for each sequence feature
+   * 
+   * @param features
+   */
+  protected void addFeatureDetails(List<SequenceFeature> features)
+  {
+    if (features == null || features.isEmpty())
+    {
+      return;
+    }
+    JMenu details = new JMenu(
+            MessageManager.getString("label.feature_details"));
+    add(details);
+
+    for (final SequenceFeature sf : features)
     {
-      addFeatureLinks(seq, links);
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+      String desc = null;
+      if (start == end)
+      {
+        desc = String.format("%s %d", sf.getType(), start);
+      }
+      else
+      {
+        desc = String.format("%s %d-%d", sf.getType(), start, end);
+      }
+      String tooltip = desc;
+      String description = sf.getDescription();
+      if (description != null)
+      {
+        description = StringUtils.stripHtmlTags(description);
+        if (description.length() > 12)
+        {
+          desc = desc + " " + description.substring(0, 12) + "..";
+        }
+        else
+        {
+          desc = desc + " " + description;
+        }
+        tooltip = tooltip + " " + description;
+      }
+      if (sf.getFeatureGroup() != null)
+      {
+        tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
+      }
+      JMenuItem item = new JMenuItem(desc);
+      item.setToolTipText(tooltip);
+      item.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          showFeatureDetails(sf);
+        }
+      });
+      details.add(item);
     }
   }
 
   /**
+   * Opens a panel showing a text report of feature dteails
+   * 
+   * @param sf
+   */
+  protected void showFeatureDetails(SequenceFeature sf)
+  {
+    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
+    // it appears Java's CSS does not support border-collaps :-(
+    cap.addStylesheetRule("table { border-collapse: collapse;}");
+    cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
+    cap.setText(sf.getDetailsReport());
+
+    Desktop.addInternalFrame(cap,
+            MessageManager.getString("label.feature_details"), 500, 500);
+  }
+
+  /**
    * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided.
+   * When seq is not null, these are links for the sequence id, which may be to
+   * external web sites for the sequence accession, and/or links embedded in
+   * non-positional features. When seq is null, only links embedded in the
+   * provided features are added.
    * 
    * @param seq
-   * @param links
+   * @param features
    */
-  void addFeatureLinks(final SequenceI seq, List<String> links)
+  void addLinks(final SequenceI seq, List<SequenceFeature> features)
   {
     JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
+
+    List<String> nlinks = null;
+    if (seq != null)
+    {
+      nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
+    }
+    else
+    {
+      nlinks = new ArrayList<>();
+    }
+
+    if (features != null)
+    {
+      for (SequenceFeature sf : features)
+      {
+        if (sf.links != null)
+        {
+          for (String link : sf.links)
+          {
+            nlinks.add(link);
+          }
+        }
+      }
+    }
+
     Map<String, List<String>> linkset = new LinkedHashMap<>();
 
-    for (String link : links)
+    for (String link : nlinks)
     {
       UrlLink urlLink = null;
       try
@@ -548,25 +663,18 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
     addshowLinks(linkMenu, linkset.values());
 
-    // disable link menu if there are no valid entries
+    // only add link menu if it has entries
     if (linkMenu.getItemCount() > 0)
     {
-      linkMenu.setEnabled(true);
-    }
-    else
-    {
-      linkMenu.setEnabled(false);
-    }
-
-    if (sequence != null)
-    {
-      sequenceMenu.add(linkMenu);
-    }
-    else
-    {
-      add(linkMenu);
+      if (sequence != null)
+      {
+        sequenceMenu.add(linkMenu);
+      }
+      else
+      {
+        add(linkMenu);
+      }
     }
-
   }
 
   /**
@@ -1509,10 +1617,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
               new Object[]
               { seq.getDisplayId(true) }) + "</h2></p><p>");
       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
-              contents, seq, true, true,
-              (ap.getSeqPanel().seqCanvas.fr != null)
-                      ? ap.getSeqPanel().seqCanvas.fr.getMinMax()
-                      : null);
+              contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
       contents.append("</p>");
     }
     cap.setText("<html>" + contents.toString() + "</html>");
index 5aab26d..7d02fac 100755 (executable)
@@ -168,8 +168,6 @@ public class Preferences extends GPreferences
 
   JInternalFrame frame;
 
-  DasSourceBrowser dasSource;
-
   private WsPreferences wsPrefs;
 
   private OptionsParam promptEachTimeOpt = new OptionsParam(
@@ -190,8 +188,6 @@ public class Preferences extends GPreferences
     super();
     frame = new JInternalFrame();
     frame.setContentPane(this);
-    dasSource = new DasSourceBrowser();
-    dasTab.add(dasSource, BorderLayout.CENTER);
     wsPrefs = new WsPreferences();
     wsTab.add(wsPrefs, BorderLayout.CENTER);
     int width = 500, height = 450;
@@ -797,7 +793,6 @@ public class Preferences extends GPreferences
     Cache.applicationProperties.setProperty("PAD_GAPS",
             Boolean.toString(padGaps.isSelected()));
 
-    dasSource.saveProperties(Cache.applicationProperties);
     wsPrefs.updateAndRefreshWsMenuConfig(false);
     Cache.saveProperties();
     Desktop.instance.doConfigureStructurePrefs();
index e36fa53..8b2e7bc 100644 (file)
@@ -59,7 +59,6 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -76,12 +75,11 @@ import javax.swing.ToolTipManager;
 public class SeqPanel extends JPanel
         implements MouseListener, MouseMotionListener, MouseWheelListener,
         SequenceListener, SelectionListener
-
 {
-  /** DOCUMENT ME!! */
+  private static final int MAX_TOOLTIP_LENGTH = 300;
+
   public SeqCanvas seqCanvas;
 
-  /** DOCUMENT ME!! */
   public AlignmentPanel ap;
 
   /*
@@ -148,35 +146,33 @@ public class SeqPanel extends JPanel
   SearchResultsI lastSearchResults;
 
   /**
-   * Creates a new SeqPanel object.
+   * Creates a new SeqPanel object
    * 
-   * @param avp
-   *          DOCUMENT ME!
-   * @param p
-   *          DOCUMENT ME!
+   * @param viewport
+   * @param alignPanel
    */
-  public SeqPanel(AlignViewport av, AlignmentPanel ap)
+  public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
   {
     linkImageURL = getClass().getResource("/images/link.gif");
     seqARep = new SequenceAnnotationReport(linkImageURL.toString());
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
-    this.av = av;
+    this.av = viewport;
     setBackground(Color.white);
 
-    seqCanvas = new SeqCanvas(ap);
+    seqCanvas = new SeqCanvas(alignPanel);
     setLayout(new BorderLayout());
     add(seqCanvas, BorderLayout.CENTER);
 
-    this.ap = ap;
+    this.ap = alignPanel;
 
-    if (!av.isDataset())
+    if (!viewport.isDataset())
     {
       addMouseMotionListener(this);
       addMouseListener(this);
       addMouseWheelListener(this);
-      ssm = av.getStructureSelectionManager();
+      ssm = viewport.getStructureSelectionManager();
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
@@ -850,7 +846,7 @@ public class SeqPanel extends JPanel
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
       seqARep.appendFeatures(tooltipText, pos, features,
-              this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
+              this.ap.getSeqPanel().seqCanvas.fr);
     }
     if (tooltipText.length() == 6) // <html>
     {
@@ -859,6 +855,11 @@ public class SeqPanel extends JPanel
     }
     else
     {
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+      {
+        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+        tooltipText.append("...");
+      }
       String textString = tooltipText.toString();
       if (lastTooltip == null || !lastTooltip.equals(textString))
       {
@@ -1839,21 +1840,10 @@ public class SeqPanel extends JPanel
     final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
+    List<SequenceFeature> features = ap.getFeatureRenderer()
             .findFeaturesAtColumn(sequence, column + 1);
-    List<String> links = new ArrayList<>();
-    for (SequenceFeature sf : allFeatures)
-    {
-      if (sf.links != null)
-      {
-        for (String link : sf.links)
-        {
-          links.add(link);
-        }
-      }
-    }
 
-    PopupMenu pop = new PopupMenu(ap, null, links);
+    PopupMenu pop = new PopupMenu(ap, null, features);
     pop.show(this, evt.getX(), evt.getY());
   }
 
index f545e70..8754fbb 100755 (executable)
@@ -34,7 +34,6 @@ import jalview.io.gff.SequenceOntologyI;
 import jalview.util.DBRefUtils;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
 import java.awt.BorderLayout;
@@ -111,10 +110,6 @@ public class SequenceFetcher extends JPanel implements Runnable
 
   private static jalview.ws.SequenceFetcher sfetch = null;
 
-  private static long lastDasSourceRegistry = -3;
-
-  private static DasSourceRegistryI dasRegistry = null;
-
   private static boolean _initingFetcher = false;
 
   private static Thread initingThread = null;
@@ -165,11 +160,7 @@ public class SequenceFetcher extends JPanel implements Runnable
                 Thread.currentThread().hashCode());
       }
     }
-    if (sfetch == null || dasRegistry != Cache.getDasSourceRegistry()
-            || lastDasSourceRegistry != (Cache.getDasSourceRegistry()
-                    .getDasRegistryURL()
-                    + Cache.getDasSourceRegistry().getLocalSourceString())
-                            .hashCode())
+    if (sfetch == null)
     {
       _initingFetcher = true;
       initingThread = Thread.currentThread();
@@ -183,16 +174,12 @@ public class SequenceFetcher extends JPanel implements Runnable
                         "status.init_sequence_database_fetchers"),
                 Thread.currentThread().hashCode());
       }
-      dasRegistry = Cache.getDasSourceRegistry();
-      dasRegistry.refreshSources();
 
       jalview.ws.SequenceFetcher sf = new jalview.ws.SequenceFetcher();
       if (guiWindow != null)
       {
         guiWindow.setProgressBar(null, Thread.currentThread().hashCode());
       }
-      lastDasSourceRegistry = (dasRegistry.getDasRegistryURL()
-              + dasRegistry.getLocalSourceString()).hashCode();
       sfetch = sf;
       _initingFetcher = false;
       initingThread = null;
@@ -564,15 +551,10 @@ public class SequenceFetcher extends JPanel implements Runnable
       dbeg.setText(MessageManager.formatMessage("label.example_query_param",
               new String[]
               { eq }));
+      // TODO this should be a property of the SequenceFetcher whether commas are and
+      // colons are allowed in the IDs...
+
       boolean enablePunct = !(eq != null && eq.indexOf(",") > -1);
-      for (DbSourceProxy dbs : database.getSelectedSources())
-      {
-        if (dbs instanceof jalview.ws.dbsources.das.datamodel.DasSequenceSource)
-        {
-          enablePunct = false;
-          break;
-        }
-      }
       replacePunctuation.setEnabled(enablePunct);
 
     } catch (Exception ex)
index 7dc1a99..fa30e13 100755 (executable)
@@ -962,6 +962,15 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
           }
         }
         colourGroups(groups);
+
+        /*
+         * clear partition (don't show vertical line) if
+         * it is to the right of all nodes
+         */
+        if (groups.isEmpty())
+        {
+          threshold = 0f;
+        }
       }
 
       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
@@ -981,7 +990,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
 
       Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
 
-      Vector<SequenceI> sequences = new Vector<SequenceI>();
+      Vector<SequenceI> sequences = new Vector<>();
 
       for (int j = 0; j < l.size(); j++)
       {
index e0722c0..169da5a 100755 (executable)
@@ -31,6 +31,8 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.io.gff.GffHelperBase;
 import jalview.io.gff.GffHelperFactory;
 import jalview.io.gff.GffHelperI;
@@ -68,6 +70,16 @@ import java.util.Map.Entry;
  */
 public class FeaturesFile extends AlignFile implements FeaturesSourceI
 {
+  private static final String TAB_REGEX = "\\t";
+
+  private static final String STARTGROUP = "STARTGROUP";
+
+  private static final String ENDGROUP = "ENDGROUP";
+
+  private static final String STARTFILTERS = "STARTFILTERS";
+
+  private static final String ENDFILTERS = "ENDFILTERS";
+
   private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED";
 
   private static final String NOTE = "Note";
@@ -167,7 +179,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * @param align
    *          - alignment/dataset containing sequences that are to be annotated
    * @param colours
-   *          - hashtable to store feature colour definitions
+   *          - map to store feature colour definitions
    * @param removeHTML
    *          - process html strings into plain text
    * @param relaxedIdmatching
@@ -178,6 +190,29 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           Map<String, FeatureColourI> colours, boolean removeHTML,
           boolean relaxedIdmatching)
   {
+    return parse(align, colours, null, removeHTML, relaxedIdmatching);
+  }
+
+  /**
+   * Parse GFF or Jalview format sequence features file
+   * 
+   * @param align
+   *          - alignment/dataset containing sequences that are to be annotated
+   * @param colours
+   *          - map to store feature colour definitions
+   * @param filters
+   *          - map to store feature filter definitions
+   * @param removeHTML
+   *          - process html strings into plain text
+   * @param relaxedIdmatching
+   *          - when true, ID matches to compound sequence IDs are allowed
+   * @return true if features were added
+   */
+  public boolean parse(AlignmentI align,
+          Map<String, FeatureColourI> colours,
+          Map<String, FeatureMatcherSetI> filters, boolean removeHTML,
+          boolean relaxedIdmatching)
+  {
     Map<String, String> gffProps = new HashMap<>();
     /*
      * keep track of any sequences we try to create from the data
@@ -202,7 +237,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           continue;
         }
 
-        gffColumns = line.split("\\t"); // tab as regex
+        gffColumns = line.split(TAB_REGEX);
         if (gffColumns.length == 1)
         {
           if (line.trim().equalsIgnoreCase("GFF"))
@@ -216,18 +251,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           }
         }
 
-        if (gffColumns.length > 1 && gffColumns.length < 4)
+        if (gffColumns.length > 0 && gffColumns.length < 4)
         {
           /*
            * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
            * a feature type colour specification
            */
           String ft = gffColumns[0];
-          if (ft.equalsIgnoreCase("startgroup"))
+          if (ft.equalsIgnoreCase(STARTFILTERS))
+          {
+            parseFilters(filters);
+            continue;
+          }
+          if (ft.equalsIgnoreCase(STARTGROUP))
           {
             featureGroup = gffColumns[1];
           }
-          else if (ft.equalsIgnoreCase("endgroup"))
+          else if (ft.equalsIgnoreCase(ENDGROUP))
           {
             // We should check whether this is the current group,
             // but at present there's no way of showing more than 1 group
@@ -288,6 +328,43 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type
+   * filter to the map for each line parsed. After exit from this method,
+   * nextLine() should return the line after ENDFILTERS (or we are already at
+   * end of file if ENDFILTERS was missing).
+   * 
+   * @param filters
+   * @throws IOException
+   */
+  protected void parseFilters(Map<String, FeatureMatcherSetI> filters)
+          throws IOException
+  {
+    String line;
+    while ((line = nextLine()) != null)
+    {
+      if (line.toUpperCase().startsWith(ENDFILTERS))
+      {
+        return;
+      }
+      String[] tokens = line.split(TAB_REGEX);
+      if (tokens.length != 2)
+      {
+        System.err.println(String.format("Invalid token count %d for %d",
+                tokens.length, line));
+      }
+      else
+      {
+        String featureType = tokens[0];
+        FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]);
+        if (fm != null && filters != null)
+        {
+          filters.put(featureType, fm);
+        }
+      }
+    }
+  }
+
+  /**
    * Try to parse a Jalview format feature specification and add it as a
    * sequence feature to any matching sequences in the alignment. Returns true
    * if successful (a feature was added), or false if not.
@@ -485,15 +562,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * 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).
+   * 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
    *          map of colour for each visible feature type
+   * @param featureFilters
    * @param visibleFeatureGroups
    * @param includeNonPositional
    *          if true, include non-positional features (regardless of group or
@@ -502,6 +580,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    */
   public String printJalviewFormat(SequenceI[] sequences,
           Map<String, FeatureColourI> visible,
+          Map<String, FeatureMatcherSetI> featureFilters,
           List<String> visibleFeatureGroups, boolean includeNonPositional)
   {
     if (!includeNonPositional && (visible == null || visible.isEmpty()))
@@ -529,6 +608,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
             .toArray(new String[visible.keySet().size()]);
 
     /*
+     * feature filters if any
+     */
+    outputFeatureFilters(out, visible, featureFilters);
+
+    /*
      * sort groups alphabetically, and ensure that features with a
      * null or empty group are output after those in named groups
      */
@@ -558,13 +642,76 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       }
     }
 
-    for (String group : sortedGroups)
+    /*
+     * positional features within groups
+     */
+    foundSome |= outputFeaturesByGroup(out, sortedGroups, types, sequences);
+
+    return foundSome ? out.toString() : "No Features Visible";
+  }
+
+  /**
+   * Outputs any feature filters defined for visible feature types, sandwiched by
+   * STARTFILTERS and ENDFILTERS lines
+   * 
+   * @param out
+   * @param visible
+   * @param featureFilters
+   */
+  void outputFeatureFilters(StringBuilder out,
+          Map<String, FeatureColourI> visible,
+          Map<String, FeatureMatcherSetI> featureFilters)
+  {
+    if (visible == null || featureFilters == null
+            || featureFilters.isEmpty())
+    {
+      return;
+    }
+
+    boolean first = true;
+    for (String featureType : visible.keySet())
+    {
+      FeatureMatcherSetI filter = featureFilters.get(featureType);
+      if (filter != null)
+      {
+        if (first)
+        {
+          first = false;
+          out.append(newline).append(STARTFILTERS).append(newline);
+        }
+        out.append(featureType).append(TAB).append(filter.toStableString())
+                .append(newline);
+      }
+    }
+    if (!first)
+    {
+      out.append(ENDFILTERS).append(newline).append(newline);
+    }
+
+  }
+
+  /**
+   * Appends output of sequence features within feature groups to the output
+   * buffer. Groups other than the null or empty group are sandwiched by
+   * STARTGROUP and ENDGROUP lines.
+   * 
+   * @param out
+   * @param groups
+   * @param featureTypes
+   * @param sequences
+   * @return
+   */
+  private boolean outputFeaturesByGroup(StringBuilder out,
+          List<String> groups, String[] featureTypes, SequenceI[] sequences)
+  {
+    boolean foundSome = false;
+    for (String group : groups)
     {
       boolean isNamedGroup = (group != null && !"".equals(group));
       if (isNamedGroup)
       {
         out.append(newline);
-        out.append("STARTGROUP").append(TAB);
+        out.append(STARTGROUP).append(TAB);
         out.append(group);
         out.append(newline);
       }
@@ -576,10 +723,10 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       {
         String sequenceName = sequences[i].getName();
         List<SequenceFeature> features = new ArrayList<>();
-        if (types.length > 0)
+        if (featureTypes.length > 0)
         {
           features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
-                  true, group, types));
+                  true, group, featureTypes));
         }
 
         for (SequenceFeature sequenceFeature : features)
@@ -591,13 +738,12 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       if (isNamedGroup)
       {
-        out.append("ENDGROUP").append(TAB);
+        out.append(ENDGROUP).append(TAB);
         out.append(group);
         out.append(newline);
       }
     }
-
-    return foundSome ? out.toString() : "No Features Visible";
+    return foundSome;
   }
 
   /**
index f1ebcac..6b82671 100644 (file)
  */
 package jalview.io;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.io.gff.GffConstants;
 import jalview.util.MessageManager;
+import jalview.util.StringUtils;
 import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -58,7 +60,7 @@ public class SequenceAnnotationReport
 
   /*
    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
-   * with 'Primary' sources placed before others
+   * with 'Primary' sources placed before others, and 'chromosome' first of all
    */
   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
   {
@@ -66,6 +68,14 @@ public class SequenceAnnotationReport
     @Override
     public int compare(DBRefEntry ref1, DBRefEntry ref2)
     {
+      if (ref1.isChromosome())
+      {
+        return -1;
+      }
+      if (ref2.isChromosome())
+      {
+        return 1;
+      }
       String s1 = ref1.getSource();
       String s2 = ref2.getSource();
       boolean s1Primary = isPrimarySource(s1);
@@ -78,14 +88,14 @@ public class SequenceAnnotationReport
       {
         return 1;
       }
-      int comp = s1 == null ? -1
-              : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
+      int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
+              .compareToIgnoreCase(s2));
       if (comp == 0)
       {
         String a1 = ref1.getAccessionId();
         String a2 = ref2.getAccessionId();
-        comp = a1 == null ? -1
-                : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
+        comp = a1 == null ? -1 : (a2 == null ? 1 : a1
+                .compareToIgnoreCase(a2));
       }
       return comp;
     }
@@ -106,9 +116,9 @@ public class SequenceAnnotationReport
     }
   };
 
-  public SequenceAnnotationReport(String linkImageURL)
+  public SequenceAnnotationReport(String linkURL)
   {
-    this.linkImageURL = linkImageURL;
+    this.linkImageURL = linkURL;
   }
 
   /**
@@ -120,13 +130,13 @@ public class SequenceAnnotationReport
    * @param minmax
    */
   public void appendFeatures(final StringBuilder sb, int rpos,
-          List<SequenceFeature> features, Map<String, float[][]> minmax)
+          List<SequenceFeature> features, FeatureRendererModel fr)
   {
     if (features != null)
     {
       for (SequenceFeature feature : features)
       {
-        appendFeature(sb, rpos, minmax, feature);
+        appendFeature(sb, rpos, fr, feature);
       }
     }
   }
@@ -140,7 +150,7 @@ public class SequenceAnnotationReport
    * @param feature
    */
   void appendFeature(final StringBuilder sb, int rpos,
-          Map<String, float[][]> minmax, SequenceFeature feature)
+          FeatureRendererModel fr, SequenceFeature feature)
   {
     if (feature.isContactFeature())
     {
@@ -153,99 +163,92 @@ public class SequenceAnnotationReport
         sb.append(feature.getType()).append(" ").append(feature.getBegin())
                 .append(":").append(feature.getEnd());
       }
+      return;
     }
-    else
+
+    if (sb.length() > 6)
     {
-      if (sb.length() > 6)
+      sb.append("<br>");
+    }
+    // TODO: remove this hack to display link only features
+    boolean linkOnly = feature.getValue("linkonly") != null;
+    if (!linkOnly)
+    {
+      sb.append(feature.getType()).append(" ");
+      if (rpos != 0)
       {
-        sb.append("<br>");
+        // we are marking a positional feature
+        sb.append(feature.begin);
       }
-      // TODO: remove this hack to display link only features
-      boolean linkOnly = feature.getValue("linkonly") != null;
-      if (!linkOnly)
+      if (feature.begin != feature.end)
       {
-        sb.append(feature.getType()).append(" ");
-        if (rpos != 0)
-        {
-          // we are marking a positional feature
-          sb.append(feature.begin);
-        }
-        if (feature.begin != feature.end)
-        {
-          sb.append(" ").append(feature.end);
-        }
+        sb.append(" ").append(feature.end);
+      }
 
-        if (feature.getDescription() != null
-                && !feature.description.equals(feature.getType()))
-        {
-          String tmpString = feature.getDescription();
-          String tmp2up = tmpString.toUpperCase();
-          int startTag = tmp2up.indexOf("<HTML>");
-          if (startTag > -1)
-          {
-            tmpString = tmpString.substring(startTag + 6);
-            tmp2up = tmp2up.substring(startTag + 6);
-          }
-          int endTag = tmp2up.indexOf("</BODY>");
-          if (endTag > -1)
-          {
-            tmpString = tmpString.substring(0, endTag);
-            tmp2up = tmp2up.substring(0, endTag);
-          }
-          endTag = tmp2up.indexOf("</HTML>");
-          if (endTag > -1)
-          {
-            tmpString = tmpString.substring(0, endTag);
-          }
+      String description = feature.getDescription();
+      if (description != null && !description.equals(feature.getType()))
+      {
+        description = StringUtils.stripHtmlTags(description);
+        sb.append("; ").append(description);
+      }
 
-          if (startTag > -1)
-          {
-            sb.append("; ").append(tmpString);
-          }
-          else
-          {
-            if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
-            {
-              // The description does not specify html is to
-              // be used, so we must remove < > symbols
-              tmpString = tmpString.replaceAll("<", "&lt;");
-              tmpString = tmpString.replaceAll(">", "&gt;");
+      if (showScore(feature, fr))
+      {
+        sb.append(" Score=").append(String.valueOf(feature.getScore()));
+      }
+      String status = (String) feature.getValue("status");
+      if (status != null && status.length() > 0)
+      {
+        sb.append("; (").append(status).append(")");
+      }
 
-              sb.append("; ");
-              sb.append(tmpString);
-            }
-            else
-            {
-              sb.append("; ").append(tmpString);
-            }
-          }
-        }
-        // check score should be shown
-        if (!Float.isNaN(feature.getScore()))
+      /*
+       * add attribute value if coloured by attribute
+       */
+      if (fr != null)
+      {
+        FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
+        if (fc != null && fc.isColourByAttribute())
         {
-          float[][] rng = (minmax == null) ? null
-                  : minmax.get(feature.getType());
-          if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
+          String[] attName = fc.getAttributeName();
+          String attVal = feature.getValueAsString(attName);
+          if (attVal != null)
           {
-            sb.append(" Score=").append(String.valueOf(feature.getScore()));
+            sb.append("; ").append(String.join(":", attName)).append("=")
+                    .append(attVal);
           }
         }
-        String status = (String) feature.getValue("status");
-        if (status != null && status.length() > 0)
-        {
-          sb.append("; (").append(status).append(")");
-        }
-        String clinSig = (String) feature
-                .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
-        if (clinSig != null)
-        {
-          sb.append("; ").append(clinSig);
-        }
       }
     }
   }
 
   /**
+   * Answers true if score should be shown, else false. Score is shown if it is
+   * not NaN, and the feature type has a non-trivial min-max score range
+   */
+  boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
+  {
+    if (Float.isNaN(feature.getScore()))
+    {
+      return false;
+    }
+    if (fr == null)
+    {
+      return true;
+    }
+    float[][] minMax = fr.getMinMax().get(feature.getType());
+
+    /*
+     * minMax[0] is the [min, max] score range for positional features
+     */
+    if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
    * Format and appends any hyperlinks for the sequence feature to the string
    * buffer
    * 
@@ -268,19 +271,20 @@ public class SequenceAnnotationReport
           {
             for (List<String> urllink : createLinksFrom(null, urlstring))
             {
-              sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
-                      + urllink.get(0) + "\">"
+              sb.append("<br/> <a href=\""
+                      + urllink.get(3)
+                      + "\" target=\""
+                      + urllink.get(0)
+                      + "\">"
                       + (urllink.get(0).toLowerCase()
-                              .equals(urllink.get(1).toLowerCase())
-                                      ? urllink.get(0)
-                                      : (urllink.get(0) + ":"
-                                              + urllink.get(1)))
-                      + "</a></br>");
+                              .equals(urllink.get(1).toLowerCase()) ? urllink
+                              .get(0) : (urllink.get(0) + ":" + urllink
+                              .get(1))) + "</a></br>");
             }
           } catch (Exception x)
           {
-            System.err.println(
-                    "problem when creating links from " + urlstring);
+            System.err.println("problem when creating links from "
+                    + urlstring);
             x.printStackTrace();
           }
         }
@@ -298,7 +302,7 @@ public class SequenceAnnotationReport
    */
   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
   {
-    Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
+    Map<String, List<String>> urlSets = new LinkedHashMap<>();
     UrlLink urlLink = new UrlLink(link);
     if (!urlLink.isValid())
     {
@@ -313,10 +317,10 @@ public class SequenceAnnotationReport
 
   public void createSequenceAnnotationReport(final StringBuilder tip,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax)
+          FeatureRendererModel fr)
   {
     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
-            minmax, false);
+            fr, false);
   }
 
   /**
@@ -331,13 +335,13 @@ public class SequenceAnnotationReport
    *          whether to include database references for the sequence
    * @param showNpFeats
    *          whether to include non-positional sequence features
-   * @param minmax
+   * @param fr
    * @param summary
    * @return
    */
   int createSequenceAnnotationReport(final StringBuilder sb,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax, boolean summary)
+          FeatureRendererModel fr, boolean summary)
   {
     String tmp;
     sb.append("<i>");
@@ -354,7 +358,7 @@ public class SequenceAnnotationReport
     {
       ds = ds.getDatasetSequence();
     }
-    
+
     if (showDbRefs)
     {
       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
@@ -369,7 +373,7 @@ public class SequenceAnnotationReport
               .getNonPositionalFeatures())
       {
         int sz = -sb.length();
-        appendFeature(sb, 0, minmax, sf);
+        appendFeature(sb, 0, fr, sf);
         sz += sb.length();
         maxWidth = Math.max(maxWidth, sz);
       }
@@ -458,8 +462,7 @@ public class SequenceAnnotationReport
     }
     if (moreSources)
     {
-      sb.append("<br>").append(source)
-              .append(COMMA).append(ELLIPSIS);
+      sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
     }
     if (ellipsis)
     {
@@ -473,10 +476,10 @@ public class SequenceAnnotationReport
 
   public void createTooltipAnnotationReport(final StringBuilder tip,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax)
+          FeatureRendererModel fr)
   {
-    int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
-            showNpFeats, minmax, true);
+    int maxWidth = createSequenceAnnotationReport(tip, sequence,
+            showDbRefs, showNpFeats, fr, true);
 
     if (maxWidth > 60)
     {
index a837512..beef3e7 100644 (file)
@@ -23,8 +23,6 @@ package jalview.io.cache;
 import jalview.bin.Cache;
 import jalview.util.MessageManager;
 
-import java.awt.Color;
-import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
@@ -36,37 +34,22 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
-import javax.swing.BorderFactory;
 import javax.swing.JComboBox;
-import javax.swing.JLabel;
 import javax.swing.JMenuItem;
-import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
-import javax.swing.JTextField;
 import javax.swing.SwingUtilities;
-import javax.swing.text.AttributeSet;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.PlainDocument;
 
 public class JvCacheableInputBox<E> extends JComboBox<String>
 {
 
   private static final long serialVersionUID = 5774610435079326695L;
 
-  private static final int INPUT_LIMIT = 2;
-
   private static final int LEFT_BOARDER_WIDTH = 16;
 
   private String cacheKey;
 
   private AppCache appCache;
 
-  private JPanel pnlDefaultCache = new JPanel();
-
-  private JLabel lblDefaultCacheSize = new JLabel();
-
-  private JTextField txtDefaultCacheSize = new JTextField();
-
   private JPopupMenu popup = new JPopupMenu();
 
   private JMenuItem menuItemClearCache = new JMenuItem();
@@ -123,8 +106,8 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
   }
 
   /**
-   * Method for initialising cache items for a given cache key and populating
-   * the in-memory cache with persisted cache items
+   * Method for initialising cache items for a given cache key and populating the
+   * in-memory cache with persisted cache items
    * 
    * @param cacheKey
    */
@@ -144,7 +127,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
             .getAllCachedItemsFor(cacheKey);
     if (foundCacheItems == null)
     {
-      foundCacheItems = new LinkedHashSet<String>();
+      foundCacheItems = new LinkedHashSet<>();
     }
     // populate memory cache
     for (String cacheItem : persistedCacheItems)
@@ -159,48 +142,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
    */
   private void initCachePopupMenu()
   {
-    pnlDefaultCache.setBackground(Color.WHITE);
-    // pad panel so as to align with other menu items
-    pnlDefaultCache.setBorder(
-            BorderFactory.createEmptyBorder(0, LEFT_BOARDER_WIDTH, 0, 0));
-    txtDefaultCacheSize.setPreferredSize(new Dimension(45, 20));
-    txtDefaultCacheSize.setFont(new java.awt.Font("Verdana", 0, 12));
-    lblDefaultCacheSize
-            .setText(MessageManager.getString("label.default_cache_size"));
-    lblDefaultCacheSize.setFont(new java.awt.Font("Verdana", 0, 12));
-    // Force input to accept only Integer entries up to length - INPUT_LIMIT
-    txtDefaultCacheSize.setDocument(new PlainDocument()
-    {
-      private static final long serialVersionUID = 1L;
-
-      @Override
-      public void insertString(int offs, String str, AttributeSet a)
-              throws BadLocationException
-      {
-        if (getLength() + str.length() <= INPUT_LIMIT && isInteger(str))
-        {
-          super.insertString(offs, str, a);
-        }
-      }
-    });
-    txtDefaultCacheSize.addKeyListener(new java.awt.event.KeyAdapter()
-    {
-      @Override
-      public void keyPressed(KeyEvent e)
-      {
-        if (e.getKeyCode() == KeyEvent.VK_ENTER)
-        {
-          e.consume();
-          updateCache();
-          closePopup();
-        }
-      }
-    });
-
-    txtDefaultCacheSize.setText(appCache.getCacheLimit(cacheKey));
-    pnlDefaultCache.add(lblDefaultCacheSize);
     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
-    pnlDefaultCache.add(txtDefaultCacheSize);
     menuItemClearCache
             .setText(MessageManager.getString("action.clear_cached_items"));
     menuItemClearCache.addActionListener(new ActionListener()
@@ -215,18 +157,11 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
       }
     });
 
-    popup.insert(pnlDefaultCache, 0);
     popup.add(menuItemClearCache);
     setComponentPopupMenu(popup);
     add(popup);
   }
 
-  private void closePopup()
-  {
-    popup.setVisible(false);
-    popup.transferFocus();
-  }
-
   /**
    * Answers true if input text is an integer
    * 
@@ -255,10 +190,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
       @Override
       public void run()
       {
-        int userLimit = txtDefaultCacheSize.getText().trim().isEmpty()
-                ? Integer.valueOf(AppCache.DEFAULT_LIMIT)
-                : Integer.valueOf(txtDefaultCacheSize.getText());
-        int cacheLimit = appCache.updateCacheLimit(cacheKey, userLimit);
+        int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
         String userInput = getUserInput();
         if (userInput != null && !userInput.isEmpty())
         {
@@ -277,7 +209,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
           removeAllItems();
         }
         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
-        List<String> reversedCacheItems = new ArrayList<String>();
+        List<String> reversedCacheItems = new ArrayList<>();
         reversedCacheItems.addAll(cacheItems);
         cacheItems = null;
         Collections.reverse(reversedCacheItems);
@@ -326,10 +258,6 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
   public void persistCache()
   {
     appCache.persistCache(cacheKey);
-    int userLimit = txtDefaultCacheSize.getText().trim().isEmpty()
-            ? Integer.valueOf(AppCache.DEFAULT_LIMIT)
-            : Integer.valueOf(txtDefaultCacheSize.getText());
-    appCache.updateCacheLimit(cacheKey, userLimit);
   }
 
   /**
index c7e1d7a..a25a014 100644 (file)
@@ -39,6 +39,8 @@ import java.util.Map;
  */
 public class Gff3Helper extends GffHelperBase
 {
+  public static final String ALLELES = "alleles";
+
   protected static final String TARGET = "Target";
 
   protected static final String ID = "ID";
@@ -399,7 +401,7 @@ public class Gff3Helper extends GffHelperBase
       /*
        * Ensembl returns dna variants as 'alleles'
        */
-      desc = StringUtils.listToDelimitedString(attributes.get("alleles"),
+      desc = StringUtils.listToDelimitedString(attributes.get(ALLELES),
               ",");
     }
 
index c0570e0..307e1d1 100644 (file)
@@ -42,6 +42,15 @@ public interface SequenceOntologyI
   // SO:0001060
   public static final String SEQUENCE_VARIANT = "sequence_variant";
 
+  // SO:0001819
+  public static final String SYNONYMOUS_VARIANT = "synonymous_variant";
+
+  // SO:0001992
+  public static final String NONSYNONYMOUS_VARIANT = "nonsynonymous_variant";
+
+  // SO:0001587
+  public static final String STOP_GAINED = "stop_gained";
+
   // SO:0000147
   public static final String EXON = "exon";
 
index f989f7b..72e906c 100644 (file)
@@ -44,7 +44,7 @@ public class SequenceOntologyLite implements SequenceOntologyI
    * initial selection of types of interest when processing Ensembl features
    * NB unlike the full SequenceOntology we don't traverse indirect
    * child-parent relationships here so e.g. need to list every sub-type
-   * of gene (direct or indirect) that is of interest
+   * (direct or indirect) that is of interest
    */
   // @formatter:off
   private final String[][] TERMS = new String[][] {
@@ -75,16 +75,26 @@ public class SequenceOntologyLite implements SequenceOntologyI
     // there are many more sub-types of ncRNA...
     
     /*
-     * sequence_variant sub-types:
+     * sequence_variant sub-types
      */
     { "sequence_variant", "sequence_variant" },
+    { "structural_variant", "sequence_variant" },
     { "feature_variant", "sequence_variant" },
     { "gene_variant", "sequence_variant" },
+    { "transcript_variant", "sequence_variant" },
     // NB Ensembl uses NMD_transcript_variant as if a 'transcript'
     // but we model it here correctly as per the SO
     { "NMD_transcript_variant", "sequence_variant" },
-    { "transcript_variant", "sequence_variant" },
-    { "structural_variant", "sequence_variant" },
+    { "missense_variant", "sequence_variant" },
+    { "synonymous_variant", "sequence_variant" },
+    { "frameshift_variant", "sequence_variant" },
+    { "5_prime_UTR_variant", "sequence_variant" },
+    { "3_prime_UTR_variant", "sequence_variant" },
+    { "stop_gained", "sequence_variant" },
+    { "stop_lost", "sequence_variant" },
+    { "inframe_deletion", "sequence_variant" },
+    { "inframe_insertion", "sequence_variant" },
+    { "splice_region_variant", "sequence_variant" },
     
     /*
      * no sub-types of exon or CDS yet seen in Ensembl
@@ -121,8 +131,8 @@ public class SequenceOntologyLite implements SequenceOntologyI
 
   public SequenceOntologyLite()
   {
-    termsFound = new ArrayList<String>();
-    termsNotFound = new ArrayList<String>();
+    termsFound = new ArrayList<>();
+    termsNotFound = new ArrayList<>();
     loadStaticData();
   }
 
@@ -131,13 +141,13 @@ public class SequenceOntologyLite implements SequenceOntologyI
    */
   private void loadStaticData()
   {
-    parents = new HashMap<String, List<String>>();
+    parents = new HashMap<>();
     for (String[] pair : TERMS)
     {
       List<String> p = parents.get(pair[0]);
       if (p == null)
       {
-        p = new ArrayList<String>();
+        p = new ArrayList<>();
         parents.put(pair[0], p);
       }
       p.add(pair[1]);
diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java
new file mode 100644 (file)
index 0000000..de2f18a
--- /dev/null
@@ -0,0 +1,1474 @@
+package jalview.io.vcf;
+
+import jalview.analysis.AlignmentUtils;
+import jalview.analysis.Dna;
+import jalview.api.AlignViewControllerGuiI;
+import jalview.bin.Cache;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureSource;
+import jalview.datamodel.features.FeatureSources;
+import jalview.ext.ensembl.EnsemblMap;
+import jalview.ext.htsjdk.HtsContigDb;
+import jalview.ext.htsjdk.VCFReader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
+import jalview.util.MessageManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import htsjdk.samtools.SAMException;
+import htsjdk.samtools.SAMSequenceDictionary;
+import htsjdk.samtools.SAMSequenceRecord;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFHeader;
+import htsjdk.variant.vcf.VCFHeaderLine;
+import htsjdk.variant.vcf.VCFHeaderLineCount;
+import htsjdk.variant.vcf.VCFHeaderLineType;
+import htsjdk.variant.vcf.VCFInfoHeaderLine;
+
+/**
+ * A class to read VCF data (using the htsjdk) and add variants as sequence
+ * features on dna and any related protein product sequences
+ * 
+ * @author gmcarstairs
+ */
+public class VCFLoader
+{
+  /**
+   * A class to model the mapping from sequence to VCF coordinates. Cases include
+   * <ul>
+   * <li>a direct 1:1 mapping where the sequence is one of the VCF contigs</li>
+   * <li>a mapping of sequence to chromosomal coordinates, where sequence and VCF
+   * use the same reference assembly</li>
+   * <li>a modified mapping of sequence to chromosomal coordinates, where sequence
+   * and VCF use different reference assembles</li>
+   * </ul>
+   */
+  class VCFMap
+  {
+    final String chromosome;
+
+    final MapList map;
+
+    VCFMap(String chr, MapList m)
+    {
+      chromosome = chr;
+      map = m;
+    }
+
+    @Override
+    public String toString()
+    {
+      return chromosome + ":" + map.toString();
+    }
+  }
+
+  /*
+   * Lookup keys, and default values, for Preference entries that describe
+   * patterns for VCF and VEP fields to capture 
+   */
+  private static final String VEP_FIELDS_PREF = "VEP_FIELDS";
+
+  private static final String VCF_FIELDS_PREF = "VCF_FIELDS";
+
+  private static final String DEFAULT_VCF_FIELDS = ".*";
+
+  private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG";
+
+  /*
+   * keys to fields of VEP CSQ consequence data
+   * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+   */
+  private static final String CSQ_CONSEQUENCE_KEY = "Consequence";
+  private static final String CSQ_ALLELE_KEY = "Allele";
+  private static final String CSQ_ALLELE_NUM_KEY = "ALLELE_NUM"; // 0 (ref), 1...
+  private static final String CSQ_FEATURE_KEY = "Feature"; // Ensembl stable id
+
+  /*
+   * default VCF INFO key for VEP consequence data
+   * NB this can be overridden running VEP with --vcf_info_field
+   * - we don't handle this case (require identifier to be CSQ)
+   */
+  private static final String CSQ_FIELD = "CSQ";
+
+  /*
+   * separator for fields in consequence data is '|'
+   */
+  private static final String PIPE_REGEX = "\\|";
+
+  /*
+   * key for Allele Frequency output by VEP
+   * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+   */
+  private static final String ALLELE_FREQUENCY_KEY = "AF";
+
+  /*
+   * delimiter that separates multiple consequence data blocks
+   */
+  private static final String COMMA = ",";
+
+  /*
+   * the feature group assigned to a VCF variant in Jalview
+   */
+  private static final String FEATURE_GROUP_VCF = "VCF";
+
+  /*
+   * internal delimiter used to build keys for assemblyMappings
+   * 
+   */
+  private static final String EXCL = "!";
+
+  /*
+   * the VCF file we are processing
+   */
+  protected String vcfFilePath;
+
+  /*
+   * mappings between VCF and sequence reference assembly regions, as 
+   * key = "species!chromosome!fromAssembly!toAssembly
+   * value = Map{fromRange, toRange}
+   */
+  private Map<String, Map<int[], int[]>> assemblyMappings;
+
+  private VCFReader reader;
+
+  /*
+   * holds details of the VCF header lines (metadata)
+   */
+  private VCFHeader header;
+
+  /*
+   * a Dictionary of contigs (if present) referenced in the VCF file
+   */
+  private SAMSequenceDictionary dictionary;
+
+  /*
+   * the position (0...) of field in each block of
+   * CSQ (consequence) data (if declared in the VCF INFO header for CSQ)
+   * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+   */
+  private int csqConsequenceFieldIndex = -1;
+  private int csqAlleleFieldIndex = -1;
+  private int csqAlleleNumberFieldIndex = -1;
+  private int csqFeatureFieldIndex = -1;
+
+  // todo the same fields for SnpEff ANN data if wanted
+  // see http://snpeff.sourceforge.net/SnpEff_manual.html#input
+
+  /*
+   * a unique identifier under which to save metadata about feature
+   * attributes (selected INFO field data)
+   */
+  private String sourceId;
+
+  /*
+   * The INFO IDs of data that is both present in the VCF file, and
+   * also matched by any filters for data of interest
+   */
+  List<String> vcfFieldsOfInterest;
+
+  /*
+   * The field offsets and identifiers for VEP (CSQ) data that is both present
+   * in the VCF file, and also matched by any filters for data of interest
+   * for example 0 -> Allele, 1 -> Consequence, ..., 36 -> SIFT, ...
+   */
+  Map<Integer, String> vepFieldsOfInterest;
+
+  /**
+   * Constructor given a VCF file
+   * 
+   * @param alignment
+   */
+  public VCFLoader(String vcfFile)
+  {
+    try
+    {
+      initialise(vcfFile);
+    } catch (IOException e)
+    {
+      System.err.println("Error opening VCF file: " + e.getMessage());
+    }
+
+    // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange}
+    assemblyMappings = new HashMap<>();
+  }
+
+  /**
+   * Starts a new thread to query and load VCF variant data on to the given
+   * sequences
+   * <p>
+   * This method is not thread safe - concurrent threads should use separate
+   * instances of this class.
+   * 
+   * @param seqs
+   * @param gui
+   */
+  public void loadVCF(SequenceI[] seqs, final AlignViewControllerGuiI gui)
+  {
+    if (gui != null)
+    {
+      gui.setStatus(MessageManager.getString("label.searching_vcf"));
+    }
+
+    new Thread()
+    {
+      @Override
+      public void run()
+      {
+        VCFLoader.this.doLoad(seqs, gui);
+      }
+    }.start();
+  }
+
+  /**
+   * Reads the specified contig sequence and adds its VCF variants to it
+   * 
+   * @param contig
+   *          the id of a single sequence (contig) to load
+   * @return
+   */
+  public SequenceI loadVCFContig(String contig)
+  {
+    String ref = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY)
+            .getValue();
+    if (ref.startsWith("file://"))
+    {
+      ref = ref.substring(7);
+    }
+
+    SequenceI seq = null;
+    File dbFile = new File(ref);
+
+    if (dbFile.exists())
+    {
+      HtsContigDb db = new HtsContigDb("", dbFile);
+      seq = db.getSequenceProxy(contig);
+      loadSequenceVCF(seq, ref);
+      db.close();
+    }
+    else
+    {
+      System.err.println("VCF reference not found: " + ref);
+    }
+
+    return seq;
+  }
+
+  /**
+   * Loads VCF on to one or more sequences
+   * 
+   * @param seqs
+   * @param gui
+   *          optional callback handler for messages
+   */
+  protected void doLoad(SequenceI[] seqs, AlignViewControllerGuiI gui)
+  {
+    try
+    {
+      VCFHeaderLine ref = header
+              .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
+      String vcfAssembly = ref.getValue();
+
+      int varCount = 0;
+      int seqCount = 0;
+
+      /*
+       * query for VCF overlapping each sequence in turn
+       */
+      for (SequenceI seq : seqs)
+      {
+        int added = loadSequenceVCF(seq, vcfAssembly);
+        if (added > 0)
+        {
+          seqCount++;
+          varCount += added;
+          transferAddedFeatures(seq);
+        }
+      }
+      if (gui != null)
+      {
+        String msg = MessageManager.formatMessage("label.added_vcf",
+                varCount, seqCount);
+        gui.setStatus(msg);
+        if (gui.getFeatureSettingsUI() != null)
+        {
+          gui.getFeatureSettingsUI().discoverAllFeatureData();
+        }
+      }
+    } catch (Throwable e)
+    {
+      System.err.println("Error processing VCF: " + e.getMessage());
+      e.printStackTrace();
+      if (gui != null)
+      {
+        gui.setStatus("Error occurred - see console for details");
+      }
+    } finally
+    {
+      if (reader != null)
+      {
+        try
+        {
+          reader.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+      header = null;
+      dictionary = null;
+    }
+  }
+
+  /**
+   * Opens the VCF file and parses header data
+   * 
+   * @param filePath
+   * @throws IOException
+   */
+  private void initialise(String filePath) throws IOException
+  {
+    vcfFilePath = filePath;
+
+    reader = new VCFReader(filePath);
+
+    header = reader.getFileHeader();
+
+    try
+    {
+      dictionary = header.getSequenceDictionary();
+    } catch (SAMException e)
+    {
+      // ignore - thrown if any contig line lacks length info
+    }
+
+    sourceId = filePath;
+
+    saveMetadata(sourceId);
+
+    /*
+     * get offset of CSQ ALLELE_NUM and Feature if declared
+     */
+    parseCsqHeader();
+  }
+
+  /**
+   * Reads metadata (such as INFO field descriptions and datatypes) and saves
+   * them for future reference
+   * 
+   * @param theSourceId
+   */
+  void saveMetadata(String theSourceId)
+  {
+    List<Pattern> vcfFieldPatterns = getFieldMatchers(VCF_FIELDS_PREF,
+            DEFAULT_VCF_FIELDS);
+    vcfFieldsOfInterest = new ArrayList<>();
+
+    FeatureSource metadata = new FeatureSource(theSourceId);
+
+    for (VCFInfoHeaderLine info : header.getInfoHeaderLines())
+    {
+      String attributeId = info.getID();
+      String desc = info.getDescription();
+      VCFHeaderLineType type = info.getType();
+      FeatureAttributeType attType = null;
+      switch (type)
+      {
+      case Character:
+        attType = FeatureAttributeType.Character;
+        break;
+      case Flag:
+        attType = FeatureAttributeType.Flag;
+        break;
+      case Float:
+        attType = FeatureAttributeType.Float;
+        break;
+      case Integer:
+        attType = FeatureAttributeType.Integer;
+        break;
+      case String:
+        attType = FeatureAttributeType.String;
+        break;
+      }
+      metadata.setAttributeName(attributeId, desc);
+      metadata.setAttributeType(attributeId, attType);
+
+      if (isFieldWanted(attributeId, vcfFieldPatterns))
+      {
+        vcfFieldsOfInterest.add(attributeId);
+      }
+    }
+
+    FeatureSources.getInstance().addSource(theSourceId, metadata);
+  }
+
+  /**
+   * Answers true if the field id is matched by any of the filter patterns, else
+   * false. Matching is against regular expression patterns, and is not
+   * case-sensitive.
+   * 
+   * @param id
+   * @param filters
+   * @return
+   */
+  private boolean isFieldWanted(String id, List<Pattern> filters)
+  {
+    for (Pattern p : filters)
+    {
+      if (p.matcher(id.toUpperCase()).matches())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Records 'wanted' fields defined in the CSQ INFO header (if there is one).
+   * Also records the position of selected fields (Allele, ALLELE_NUM, Feature)
+   * required for processing.
+   * <p>
+   * CSQ fields are declared in the CSQ INFO Description e.g.
+   * <p>
+   * Description="Consequence ...from ... VEP. Format: Allele|Consequence|...
+   */
+  protected void parseCsqHeader()
+  {
+    List<Pattern> vepFieldFilters = getFieldMatchers(VEP_FIELDS_PREF,
+            DEFAULT_VEP_FIELDS);
+    vepFieldsOfInterest = new HashMap<>();
+
+    VCFInfoHeaderLine csqInfo = header.getInfoHeaderLine(CSQ_FIELD);
+    if (csqInfo == null)
+    {
+      return;
+    }
+
+    /*
+     * parse out the pipe-separated list of CSQ fields; we assume here that
+     * these form the last part of the description, and contain no spaces
+     */
+    String desc = csqInfo.getDescription();
+    int spacePos = desc.lastIndexOf(" ");
+    desc = desc.substring(spacePos + 1);
+
+    if (desc != null)
+    {
+      String[] format = desc.split(PIPE_REGEX);
+      int index = 0;
+      for (String field : format)
+      {
+        if (CSQ_CONSEQUENCE_KEY.equals(field))
+        {
+          csqConsequenceFieldIndex = index;
+        }
+        if (CSQ_ALLELE_NUM_KEY.equals(field))
+        {
+          csqAlleleNumberFieldIndex = index;
+        }
+        if (CSQ_ALLELE_KEY.equals(field))
+        {
+          csqAlleleFieldIndex = index;
+        }
+        if (CSQ_FEATURE_KEY.equals(field))
+        {
+          csqFeatureFieldIndex = index;
+        }
+
+        if (isFieldWanted(field, vepFieldFilters))
+        {
+          vepFieldsOfInterest.put(index, field);
+        }
+
+        index++;
+      }
+    }
+  }
+
+  /**
+   * Reads the Preference value for the given key, with default specified if no
+   * preference set. The value is interpreted as a comma-separated list of
+   * regular expressions, and converted into a list of compiled patterns ready
+   * for matching. Patterns are forced to upper-case for non-case-sensitive
+   * matching.
+   * <p>
+   * This supports user-defined filters for fields of interest to capture while
+   * processing data. For example, VCF_FIELDS = AF,AC* would mean that VCF INFO
+   * fields with an ID of AF, or starting with AC, would be matched.
+   * 
+   * @param key
+   * @param def
+   * @return
+   */
+  private List<Pattern> getFieldMatchers(String key, String def)
+  {
+    String pref = Cache.getDefault(key, def);
+    List<Pattern> patterns = new ArrayList<>();
+    String[] tokens = pref.split(",");
+    for (String token : tokens)
+    {
+      try
+      {
+      patterns.add(Pattern.compile(token.toUpperCase()));
+      } catch (PatternSyntaxException e)
+      {
+        System.err.println("Invalid pattern ignored: " + token);
+      }
+    }
+    return patterns;
+  }
+
+  /**
+   * Transfers VCF features to sequences to which this sequence has a mapping.
+   * If the mapping is 3:1, computes peptide variants from nucleotide variants.
+   * 
+   * @param seq
+   */
+  protected void transferAddedFeatures(SequenceI seq)
+  {
+    DBRefEntry[] dbrefs = seq.getDBRefs();
+    if (dbrefs == null)
+    {
+      return;
+    }
+    for (DBRefEntry dbref : dbrefs)
+    {
+      Mapping mapping = dbref.getMap();
+      if (mapping == null || mapping.getTo() == null)
+      {
+        continue;
+      }
+
+      SequenceI mapTo = mapping.getTo();
+      MapList map = mapping.getMap();
+      if (map.getFromRatio() == 3)
+      {
+        /*
+         * dna-to-peptide product mapping
+         */
+        AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
+      }
+      else
+      {
+        /*
+         * nucleotide-to-nucleotide mapping e.g. transcript to CDS
+         */
+        List<SequenceFeature> features = seq.getFeatures()
+                .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
+        for (SequenceFeature sf : features)
+        {
+          if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup()))
+          {
+            transferFeature(sf, mapTo, map);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Tries to add overlapping variants read from a VCF file to the given sequence,
+   * and returns the number of variant features added
+   * 
+   * @param seq
+   * @param vcfAssembly
+   * @return
+   */
+  protected int loadSequenceVCF(SequenceI seq, String vcfAssembly)
+  {
+    VCFMap vcfMap = getVcfMap(seq, vcfAssembly);
+    if (vcfMap == null)
+    {
+      return 0;
+    }
+
+    /*
+     * work with the dataset sequence here
+     */
+    SequenceI dss = seq.getDatasetSequence();
+    if (dss == null)
+    {
+      dss = seq;
+    }
+    return addVcfVariants(dss, vcfMap);
+  }
+
+  /**
+   * Answers a map from sequence coordinates to VCF chromosome ranges
+   * 
+   * @param seq
+   * @param vcfAssembly
+   * @return
+   */
+  private VCFMap getVcfMap(SequenceI seq, String vcfAssembly)
+  {
+    /*
+     * simplest case: sequence has id and length matching a VCF contig
+     */
+    VCFMap vcfMap = null;
+    if (dictionary != null)
+    {
+      vcfMap = getContigMap(seq);
+    }
+    if (vcfMap != null)
+    {
+      return vcfMap;
+    }
+
+    /*
+     * otherwise, map to VCF from chromosomal coordinates 
+     * of the sequence (if known)
+     */
+    GeneLociI seqCoords = seq.getGeneLoci();
+    if (seqCoords == null)
+    {
+      Cache.log.warn(String.format(
+              "Can't query VCF for %s as chromosome coordinates not known",
+              seq.getName()));
+      return null;
+    }
+
+    String species = seqCoords.getSpeciesId();
+    String chromosome = seqCoords.getChromosomeId();
+    String seqRef = seqCoords.getAssemblyId();
+    MapList map = seqCoords.getMap();
+
+    if (!vcfSpeciesMatchesSequence(vcfAssembly, species))
+    {
+      return null;
+    }
+
+    if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef))
+    {
+      return new VCFMap(chromosome, map);
+    }
+
+    if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl
+            || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
+    {
+      return null;
+    }
+
+    /*
+     * map chromosomal coordinates from sequence to VCF if the VCF
+     * data has a different reference assembly to the sequence
+     */
+    // TODO generalise for cases other than GRCh38 -> GRCh37 !
+    // - or get the user to choose in a dialog
+
+    List<int[]> toVcfRanges = new ArrayList<>();
+    List<int[]> fromSequenceRanges = new ArrayList<>();
+    String toRef = "GRCh37";
+
+    for (int[] range : map.getToRanges())
+    {
+      int[] fromRange = map.locateInFrom(range[0], range[1]);
+      if (fromRange == null)
+      {
+        // corrupted map?!?
+        continue;
+      }
+
+      int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef,
+              toRef);
+      if (newRange == null)
+      {
+        Cache.log.error(
+                String.format("Failed to map %s:%s:%s:%d:%d to %s", species,
+                        chromosome, seqRef, range[0], range[1], toRef));
+        continue;
+      }
+      else
+      {
+        toVcfRanges.add(newRange);
+        fromSequenceRanges.add(fromRange);
+      }
+    }
+
+    return new VCFMap(chromosome,
+            new MapList(fromSequenceRanges, toVcfRanges, 1, 1));
+  }
+
+  /**
+   * If the sequence id matches a contig declared in the VCF file, and the
+   * sequence length matches the contig length, then returns a 1:1 map of the
+   * sequence to the contig, else returns null
+   * 
+   * @param seq
+   * @return
+   */
+  private VCFMap getContigMap(SequenceI seq)
+  {
+    String id = seq.getName();
+    SAMSequenceRecord contig = dictionary.getSequence(id);
+    if (contig != null)
+    {
+      int len = seq.getLength();
+      if (len == contig.getSequenceLength())
+      {
+        MapList map = new MapList(new int[] { 1, len },
+                new int[]
+                { 1, len }, 1, 1);
+        return new VCFMap(id, map);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Answers true if we determine that the VCF data uses the same reference
+   * assembly as the sequence, else false
+   * 
+   * @param vcfAssembly
+   * @param seqRef
+   * @return
+   */
+  private boolean vcfAssemblyMatchesSequence(String vcfAssembly,
+          String seqRef)
+  {
+    // TODO improve on this stub, which handles gnomAD and
+    // hopes for the best for other cases
+
+    if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl
+            && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Answers true if the species inferred from the VCF reference identifier
+   * matches that for the sequence
+   * 
+   * @param vcfAssembly
+   * @param speciesId
+   * @return
+   */
+  boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId)
+  {
+    // PROBLEM 1
+    // there are many aliases for species - how to equate one with another?
+    // PROBLEM 2
+    // VCF ##reference header is an unstructured URI - how to extract species?
+    // perhaps check if ref includes any (Ensembl) alias of speciesId??
+    // TODO ask the user to confirm this??
+
+    if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example
+            && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id
+    {
+      return true;
+    }
+
+    if (vcfAssembly.contains("c_elegans") // VEP VCF response example
+            && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl
+    {
+      return true;
+    }
+
+    // this is not a sustainable solution...
+
+    return false;
+  }
+
+  /**
+   * Queries the VCF reader for any variants that overlap the mapped chromosome
+   * ranges of the sequence, and adds as variant features. Returns the number of
+   * overlapping variants found.
+   * 
+   * @param seq
+   * @param map
+   *          mapping from sequence to VCF coordinates
+   * @return
+   */
+  protected int addVcfVariants(SequenceI seq, VCFMap map)
+  {
+    boolean forwardStrand = map.map.isToForwardStrand();
+
+    /*
+     * query the VCF for overlaps of each contiguous chromosomal region
+     */
+    int count = 0;
+
+    for (int[] range : map.map.getToRanges())
+    {
+      int vcfStart = Math.min(range[0], range[1]);
+      int vcfEnd = Math.max(range[0], range[1]);
+      CloseableIterator<VariantContext> variants = reader
+              .query(map.chromosome, vcfStart, vcfEnd);
+      while (variants.hasNext())
+      {
+        VariantContext variant = variants.next();
+
+        int[] featureRange = map.map.locateInFrom(variant.getStart(),
+                variant.getEnd());
+
+        if (featureRange != null)
+        {
+          int featureStart = Math.min(featureRange[0], featureRange[1]);
+          int featureEnd = Math.max(featureRange[0], featureRange[1]);
+          count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
+                  forwardStrand);
+        }
+      }
+      variants.close();
+    }
+
+    return count;
+  }
+
+  /**
+   * A convenience method to get the AF value for the given alternate allele
+   * index
+   * 
+   * @param variant
+   * @param alleleIndex
+   * @return
+   */
+  protected float getAlleleFrequency(VariantContext variant, int alleleIndex)
+  {
+    float score = 0f;
+    String attributeValue = getAttributeValue(variant,
+            ALLELE_FREQUENCY_KEY, alleleIndex);
+    if (attributeValue != null)
+    {
+      try
+      {
+        score = Float.parseFloat(attributeValue);
+      } catch (NumberFormatException e)
+      {
+        // leave as 0
+      }
+    }
+
+    return score;
+  }
+
+  /**
+   * A convenience method to get an attribute value for an alternate allele
+   * 
+   * @param variant
+   * @param attributeName
+   * @param alleleIndex
+   * @return
+   */
+  protected String getAttributeValue(VariantContext variant,
+          String attributeName, int alleleIndex)
+  {
+    Object att = variant.getAttribute(attributeName);
+
+    if (att instanceof String)
+    {
+      return (String) att;
+    }
+    else if (att instanceof ArrayList)
+    {
+      return ((List<String>) att).get(alleleIndex);
+    }
+
+    return null;
+  }
+
+  /**
+   * Adds one variant feature for each allele in the VCF variant record, and
+   * returns the number of features added.
+   * 
+   * @param seq
+   * @param variant
+   * @param featureStart
+   * @param featureEnd
+   * @param forwardStrand
+   * @return
+   */
+  protected int addAlleleFeatures(SequenceI seq, VariantContext variant,
+          int featureStart, int featureEnd, boolean forwardStrand)
+  {
+    int added = 0;
+
+    /*
+     * Javadoc says getAlternateAlleles() imposes no order on the list returned
+     * so we proceed defensively to get them in strict order
+     */
+    int altAlleleCount = variant.getAlternateAlleles().size();
+    for (int i = 0; i < altAlleleCount; i++)
+    {
+      added += addAlleleFeature(seq, variant, i, featureStart, featureEnd,
+              forwardStrand);
+    }
+    return added;
+  }
+
+  /**
+   * Inspects one allele and attempts to add a variant feature for it to the
+   * sequence. The additional data associated with this allele is extracted to
+   * store in the feature's key-value map. Answers the number of features added (0
+   * or 1).
+   * 
+   * @param seq
+   * @param variant
+   * @param altAlleleIndex
+   *          (0, 1..)
+   * @param featureStart
+   * @param featureEnd
+   * @param forwardStrand
+   * @return
+   */
+  protected int addAlleleFeature(SequenceI seq, VariantContext variant,
+          int altAlleleIndex, int featureStart, int featureEnd,
+          boolean forwardStrand)
+  {
+    String reference = variant.getReference().getBaseString();
+    Allele alt = variant.getAlternateAllele(altAlleleIndex);
+    String allele = alt.getBaseString();
+
+    /*
+     * insertion after a genomic base, if on reverse strand, has to be 
+     * converted to insertion of complement after the preceding position 
+     */
+    int referenceLength = reference.length();
+    if (!forwardStrand && allele.length() > referenceLength
+            && allele.startsWith(reference))
+    {
+      featureStart -= referenceLength;
+      featureEnd = featureStart;
+      char insertAfter = seq.getCharAt(featureStart - seq.getStart());
+      reference = Dna.reverseComplement(String.valueOf(insertAfter));
+      allele = allele.substring(referenceLength) + reference;
+    }
+
+    /*
+     * build the ref,alt allele description e.g. "G,A", using the base
+     * complement if the sequence is on the reverse strand
+     */
+    StringBuilder sb = new StringBuilder();
+    sb.append(forwardStrand ? reference : Dna.reverseComplement(reference));
+    sb.append(COMMA);
+    sb.append(forwardStrand ? allele : Dna.reverseComplement(allele));
+    String alleles = sb.toString(); // e.g. G,A
+
+    /*
+     * pick out the consequence data (if any) that is for the current allele
+     * and feature (transcript) that matches the current sequence
+     */
+    String consequence = getConsequenceForAlleleAndFeature(variant, CSQ_FIELD,
+            altAlleleIndex, csqAlleleFieldIndex,
+            csqAlleleNumberFieldIndex, seq.getName().toLowerCase(),
+            csqFeatureFieldIndex);
+
+    /*
+     * pick out the ontology term for the consequence type
+     */
+    String type = SequenceOntologyI.SEQUENCE_VARIANT;
+    if (consequence != null)
+    {
+      type = getOntologyTerm(consequence);
+    }
+
+    float score = getAlleleFrequency(variant, altAlleleIndex);
+
+    SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
+            featureEnd, score, FEATURE_GROUP_VCF);
+    sf.setSource(sourceId);
+
+    sf.setValue(Gff3Helper.ALLELES, alleles);
+
+    addAlleleProperties(variant, sf, altAlleleIndex, consequence);
+
+    seq.addSequenceFeature(sf);
+
+    return 1;
+  }
+
+  /**
+   * Determines the Sequence Ontology term to use for the variant feature type in
+   * Jalview. The default is 'sequence_variant', but a more specific term is used
+   * if:
+   * <ul>
+   * <li>VEP (or SnpEff) Consequence annotation is included in the VCF</li>
+   * <li>sequence id can be matched to VEP Feature (or SnpEff Feature_ID)</li>
+   * </ul>
+   * 
+   * @param consequence
+   * @return
+   * @see http://www.sequenceontology.org/browser/current_svn/term/SO:0001060
+   */
+  String getOntologyTerm(String consequence)
+  {
+    String type = SequenceOntologyI.SEQUENCE_VARIANT;
+
+    /*
+     * could we associate Consequence data with this allele and feature (transcript)?
+     * if so, prefer the consequence term from that data
+     */
+    if (csqAlleleFieldIndex == -1) // && snpEffAlleleFieldIndex == -1
+    {
+      /*
+       * no Consequence data so we can't refine the ontology term
+       */
+      return type;
+    }
+
+    if (consequence != null)
+    {
+      String[] csqFields = consequence.split(PIPE_REGEX);
+      if (csqFields.length > csqConsequenceFieldIndex)
+      {
+        type = csqFields[csqConsequenceFieldIndex];
+      }
+    }
+    else
+    {
+      // todo the same for SnpEff consequence data matching if wanted
+    }
+
+    /*
+     * if of the form (e.g.) missense_variant&splice_region_variant,
+     * just take the first ('most severe') consequence
+     */
+    if (type != null)
+    {
+      int pos = type.indexOf('&');
+      if (pos > 0)
+      {
+        type = type.substring(0, pos);
+      }
+    }
+    return type;
+  }
+
+  /**
+   * Returns matched consequence data if it can be found, else null.
+   * <ul>
+   * <li>inspects the VCF data for key 'vcfInfoId'</li>
+   * <li>splits this on comma (to distinct consequences)</li>
+   * <li>returns the first consequence (if any) where</li>
+   * <ul>
+   * <li>the allele matches the altAlleleIndex'th allele of variant</li>
+   * <li>the feature matches the sequence name (e.g. transcript id)</li>
+   * </ul>
+   * </ul>
+   * If matched, the consequence is returned (as pipe-delimited fields).
+   * 
+   * @param variant
+   * @param vcfInfoId
+   * @param altAlleleIndex
+   * @param alleleFieldIndex
+   * @param alleleNumberFieldIndex
+   * @param seqName
+   * @param featureFieldIndex
+   * @return
+   */
+  private String getConsequenceForAlleleAndFeature(VariantContext variant,
+          String vcfInfoId, int altAlleleIndex, int alleleFieldIndex,
+          int alleleNumberFieldIndex,
+          String seqName, int featureFieldIndex)
+  {
+    if (alleleFieldIndex == -1 || featureFieldIndex == -1)
+    {
+      return null;
+    }
+    Object value = variant.getAttribute(vcfInfoId);
+
+    if (value == null || !(value instanceof List<?>))
+    {
+      return null;
+    }
+
+    /*
+     * inspect each consequence in turn (comma-separated blocks
+     * extracted by htsjdk)
+     */
+    List<String> consequences = (List<String>) value;
+
+    for (String consequence : consequences)
+    {
+      String[] csqFields = consequence.split(PIPE_REGEX);
+      if (csqFields.length > featureFieldIndex)
+      {
+        String featureIdentifier = csqFields[featureFieldIndex];
+        if (featureIdentifier.length() > 4
+                && seqName.indexOf(featureIdentifier.toLowerCase()) > -1)
+        {
+          /*
+           * feature (transcript) matched - now check for allele match
+           */
+          if (matchAllele(variant, altAlleleIndex, csqFields,
+                  alleleFieldIndex, alleleNumberFieldIndex))
+          {
+            return consequence;
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  private boolean matchAllele(VariantContext variant, int altAlleleIndex,
+          String[] csqFields, int alleleFieldIndex,
+          int alleleNumberFieldIndex)
+  {
+    /*
+     * if ALLELE_NUM is present, it must match altAlleleIndex
+     * NB first alternate allele is 1 for ALLELE_NUM, 0 for altAlleleIndex
+     */
+    if (alleleNumberFieldIndex > -1)
+    {
+      if (csqFields.length <= alleleNumberFieldIndex)
+      {
+        return false;
+      }
+      String alleleNum = csqFields[alleleNumberFieldIndex];
+      return String.valueOf(altAlleleIndex + 1).equals(alleleNum);
+    }
+
+    /*
+     * else consequence allele must match variant allele
+     */
+    if (alleleFieldIndex > -1 && csqFields.length > alleleFieldIndex)
+    {
+      String csqAllele = csqFields[alleleFieldIndex];
+      String vcfAllele = variant.getAlternateAllele(altAlleleIndex)
+              .getBaseString();
+      return csqAllele.equals(vcfAllele);
+    }
+    return false;
+  }
+
+  /**
+   * Add any allele-specific VCF key-value data to the sequence feature
+   * 
+   * @param variant
+   * @param sf
+   * @param altAlelleIndex
+   *          (0, 1..)
+   * @param consequence
+   *          if not null, the consequence specific to this sequence (transcript
+   *          feature) and allele
+   */
+  protected void addAlleleProperties(VariantContext variant,
+          SequenceFeature sf, final int altAlelleIndex, String consequence)
+  {
+    Map<String, Object> atts = variant.getAttributes();
+
+    for (Entry<String, Object> att : atts.entrySet())
+    {
+      String key = att.getKey();
+
+      /*
+       * extract Consequence data (if present) that we are able to
+       * associated with the allele for this variant feature
+       */
+      if (CSQ_FIELD.equals(key))
+      {
+        addConsequences(variant, sf, consequence);
+        continue;
+      }
+
+      /*
+       * filter out fields we don't want to capture
+       */
+      if (!vcfFieldsOfInterest.contains(key))
+      {
+        continue;
+      }
+
+      /*
+       * filter out fields we don't want to capture
+       */
+      if (!vcfFieldsOfInterest.contains(key))
+      {
+        continue;
+      }
+
+      /*
+       * we extract values for other data which are allele-specific; 
+       * these may be per alternate allele (INFO[key].Number = 'A') 
+       * or per allele including reference (INFO[key].Number = 'R') 
+       */
+      VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(key);
+      if (infoHeader == null)
+      {
+        /*
+         * can't be sure what data belongs to this allele, so
+         * play safe and don't take any
+         */
+        continue;
+      }
+
+      VCFHeaderLineCount number = infoHeader.getCountType();
+      int index = altAlelleIndex;
+      if (number == VCFHeaderLineCount.R)
+      {
+        /*
+         * one value per allele including reference, so bump index
+         * e.g. the 3rd value is for the  2nd alternate allele
+         */
+        index++;
+      }
+      else if (number != VCFHeaderLineCount.A)
+      {
+        /*
+         * don't save other values as not allele-related
+         */
+        continue;
+      }
+
+      /*
+       * take the index'th value
+       */
+      String value = getAttributeValue(variant, key, index);
+      if (value != null)
+      {
+        sf.setValue(key, value);
+      }
+    }
+  }
+
+  /**
+   * Inspects CSQ data blocks (consequences) and adds attributes on the sequence
+   * feature.
+   * <p>
+   * If <code>myConsequence</code> is not null, then this is the specific
+   * consequence data (pipe-delimited fields) that is for the current allele and
+   * transcript (sequence) being processed)
+   * 
+   * @param variant
+   * @param sf
+   * @param myConsequence
+   */
+  protected void addConsequences(VariantContext variant, SequenceFeature sf,
+          String myConsequence)
+  {
+    Object value = variant.getAttribute(CSQ_FIELD);
+
+    if (value == null || !(value instanceof List<?>))
+    {
+      return;
+    }
+
+    List<String> consequences = (List<String>) value;
+
+    /*
+     * inspect CSQ consequences; restrict to the consequence
+     * associated with the current transcript (Feature)
+     */
+    Map<String, String> csqValues = new HashMap<>();
+
+    for (String consequence : consequences)
+    {
+      if (myConsequence == null || myConsequence.equals(consequence))
+      {
+        String[] csqFields = consequence.split(PIPE_REGEX);
+
+        /*
+         * inspect individual fields of this consequence, copying non-null
+         * values which are 'fields of interest'
+         */
+        int i = 0;
+        for (String field : csqFields)
+        {
+          if (field != null && field.length() > 0)
+          {
+            String id = vepFieldsOfInterest.get(i);
+            if (id != null)
+            {
+              csqValues.put(id, field);
+            }
+          }
+          i++;
+        }
+      }
+    }
+
+    if (!csqValues.isEmpty())
+    {
+      sf.setValue(CSQ_FIELD, csqValues);
+    }
+  }
+
+  /**
+   * A convenience method to complement a dna base and return the string value
+   * of its complement
+   * 
+   * @param reference
+   * @return
+   */
+  protected String complement(byte[] reference)
+  {
+    return String.valueOf(Dna.getComplement((char) reference[0]));
+  }
+
+  /**
+   * Determines the location of the query range (chromosome positions) in a
+   * different reference assembly.
+   * <p>
+   * If the range is just a subregion of one for which we already have a mapping
+   * (for example, an exon sub-region of a gene), then the mapping is just
+   * computed arithmetically.
+   * <p>
+   * Otherwise, calls the Ensembl REST service that maps from one assembly
+   * reference's coordinates to another's
+   * 
+   * @param queryRange
+   *          start-end chromosomal range in 'fromRef' coordinates
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   *          assembly reference for the query coordinates
+   * @param toRef
+   *          assembly reference we wish to translate to
+   * @return the start-end range in 'toRef' coordinates
+   */
+  protected int[] mapReferenceRange(int[] queryRange, String chromosome,
+          String species, String fromRef, String toRef)
+  {
+    /*
+     * first try shorcut of computing the mapping as a subregion of one
+     * we already have (e.g. for an exon, if we have the gene mapping)
+     */
+    int[] mappedRange = findSubsumedRangeMapping(queryRange, chromosome,
+            species, fromRef, toRef);
+    if (mappedRange != null)
+    {
+      return mappedRange;
+    }
+
+    /*
+     * call (e.g.) http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37
+     */
+    EnsemblMap mapper = new EnsemblMap();
+    int[] mapping = mapper.getAssemblyMapping(species, chromosome, fromRef,
+            toRef, queryRange);
+
+    if (mapping == null)
+    {
+      // mapping service failure
+      return null;
+    }
+
+    /*
+     * save mapping for possible future re-use
+     */
+    String key = makeRangesKey(chromosome, species, fromRef, toRef);
+    if (!assemblyMappings.containsKey(key))
+    {
+      assemblyMappings.put(key, new HashMap<int[], int[]>());
+    }
+
+    assemblyMappings.get(key).put(queryRange, mapping);
+
+    return mapping;
+  }
+
+  /**
+   * If we already have a 1:1 contiguous mapping which subsumes the given query
+   * range, this method just calculates and returns the subset of that mapping,
+   * else it returns null. In practical terms, if a gene has a contiguous
+   * mapping between (for example) GRCh37 and GRCh38, then we assume that its
+   * subsidiary exons occupy unchanged relative positions, and just compute
+   * these as offsets, rather than do another lookup of the mapping.
+   * <p>
+   * If in future these assumptions prove invalid (e.g. for bacterial dna?!),
+   * simply remove this method or let it always return null.
+   * <p>
+   * Warning: many rapid calls to the /map service map result in a 429 overload
+   * error response
+   * 
+   * @param queryRange
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   * @param toRef
+   * @return
+   */
+  protected int[] findSubsumedRangeMapping(int[] queryRange, String chromosome,
+          String species, String fromRef, String toRef)
+  {
+    String key = makeRangesKey(chromosome, species, fromRef, toRef);
+    if (assemblyMappings.containsKey(key))
+    {
+      Map<int[], int[]> mappedRanges = assemblyMappings.get(key);
+      for (Entry<int[], int[]> mappedRange : mappedRanges.entrySet())
+      {
+        int[] fromRange = mappedRange.getKey();
+        int[] toRange = mappedRange.getValue();
+        if (fromRange[1] - fromRange[0] == toRange[1] - toRange[0])
+        {
+          /*
+           * mapping is 1:1 in length, so we trust it to have no discontinuities
+           */
+          if (MappingUtils.rangeContains(fromRange, queryRange))
+          {
+            /*
+             * fromRange subsumes our query range
+             */
+            int offset = queryRange[0] - fromRange[0];
+            int mappedRangeFrom = toRange[0] + offset;
+            int mappedRangeTo = mappedRangeFrom + (queryRange[1] - queryRange[0]);
+            return new int[] { mappedRangeFrom, mappedRangeTo };
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Transfers the sequence feature to the target sequence, locating its start
+   * and end range based on the mapping. Features which do not overlap the
+   * target sequence are ignored.
+   * 
+   * @param sf
+   * @param targetSequence
+   * @param mapping
+   *          mapping from the feature's coordinates to the target sequence
+   */
+  protected void transferFeature(SequenceFeature sf,
+          SequenceI targetSequence, MapList mapping)
+  {
+    int[] mappedRange = mapping.locateInTo(sf.getBegin(), sf.getEnd());
+  
+    if (mappedRange != null)
+    {
+      String group = sf.getFeatureGroup();
+      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);
+    }
+  }
+
+  /**
+   * Formats a ranges map lookup key
+   * 
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   * @param toRef
+   * @return
+   */
+  protected static String makeRangesKey(String chromosome, String species,
+          String fromRef, String toRef)
+  {
+    return species + EXCL + chromosome + EXCL + fromRef + EXCL
+            + toRef;
+  }
+}
index 86d0c85..1cf482d 100755 (executable)
@@ -147,6 +147,8 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenuItem runGroovy = new JMenuItem();
 
+  protected JMenuItem loadVcf;
+
   protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
 
   protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem();
@@ -1308,6 +1310,16 @@ public class GAlignFrame extends JInternalFrame
         associatedData_actionPerformed(e);
       }
     });
+    loadVcf = new JMenuItem(MessageManager.getString("label.load_vcf_file"));
+    loadVcf.setToolTipText(MessageManager.getString("label.load_vcf"));
+    loadVcf.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        loadVcf_actionPerformed();
+      }
+    });
     autoCalculate.setText(
             MessageManager.getString("label.autocalculate_consensus"));
     autoCalculate.setState(
@@ -1710,6 +1722,7 @@ public class GAlignFrame extends JInternalFrame
     fileMenu.add(exportAnnotations);
     fileMenu.add(loadTreeMenuItem);
     fileMenu.add(associatedData);
+    fileMenu.add(loadVcf);
     fileMenu.addSeparator();
     fileMenu.add(closeMenuItem);
 
@@ -1855,6 +1868,10 @@ public class GAlignFrame extends JInternalFrame
     // selectMenu.add(listenToViewSelections);
   }
 
+  protected void loadVcf_actionPerformed()
+  {
+  }
+
   /**
    * Constructs the entries on the Colour menu (but does not add them to the
    * menu).
index abc0b3d..a6e0ace 100644 (file)
@@ -39,6 +39,8 @@ import javax.swing.JMenuBar;
 import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
+import javax.swing.text.EditorKit;
+import javax.swing.text.html.HTMLEditorKit;
 
 /**
  * DOCUMENT ME!
@@ -85,6 +87,7 @@ public class GCutAndPasteHtmlTransfer extends JInternalFrame
   {
     try
     {
+      textarea.setEditorKit(new HTMLEditorKit());
       setJMenuBar(editMenubar);
       jbInit();
     } catch (Exception e)
@@ -272,4 +275,20 @@ public class GCutAndPasteHtmlTransfer extends JInternalFrame
   {
 
   }
+
+  /**
+   * Adds the given stylesheet rule to the Html editor. However note that CSS
+   * support is limited.
+   * 
+   * @param rule
+   * @see javax.swing.text.html.CSS
+   */
+  public void addStylesheetRule(String rule)
+  {
+    EditorKit editorKit = textarea.getEditorKit();
+    if (editorKit != null)
+    {
+      ((HTMLEditorKit) editorKit).getStyleSheet().addRule(rule);
+    }
+  }
 }
index 26e0919..6807382 100755 (executable)
@@ -264,10 +264,6 @@ public class GPreferences extends JPanel
 
   protected JCheckBox sortByTree = new JCheckBox();
 
-  /*
-   * DAS Settings tab
-   */
-  protected JPanel dasTab = new JPanel();
 
   /*
    * Web Services tab
@@ -326,12 +322,6 @@ public class GPreferences extends JPanel
             MessageManager.getString("label.editing"));
 
     /*
-     * See DasSourceBrowser for the real work of configuring this tab.
-     */
-    dasTab.setLayout(new BorderLayout());
-    tabbedPane.add(dasTab, MessageManager.getString("label.das_settings"));
-
-    /*
      * See WsPreferences for the real work of configuring this tab.
      */
     wsTab.setLayout(new BorderLayout());
index 1f47da3..795cd36 100644 (file)
@@ -304,14 +304,19 @@ public class FeatureRenderer extends FeatureRendererModel
       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
 
-      filterFeaturesForDisplay(overlaps, fc);
+      if (fc.isSimpleColour())
+      {
+        filterFeaturesForDisplay(overlaps);
+      }
 
       for (SequenceFeature sf : overlaps)
       {
-        Color featureColour = fc.getColor(sf);
+        Color featureColour = getColor(sf, fc);
         if (featureColour == null)
         {
-          // score feature outwith threshold for colouring
+          /*
+           * feature excluded by visibility settings, filters, or colour threshold
+           */
           continue;
         }
 
index 0a01103..e1100a8 100644 (file)
@@ -1,4 +1,4 @@
-#Mon Jun 20 15:44:52 BST 2016
+#Thu Dec 14 09:10:14 GMT 2017
 jalview.schemabinding.version2.ThresholdLine=jalview.schemabinding.version2.descriptors.ThresholdLineDescriptor
 jalview.schemabinding.version2.SequenceSetProperties=jalview.schemabinding.version2.descriptors.SequenceSetPropertiesDescriptor
 jalview.schemabinding.version2.StructureState=jalview.schemabinding.version2.descriptors.StructureStateDescriptor
@@ -10,7 +10,9 @@ jalview.schemabinding.version2.OtherData=jalview.schemabinding.version2.descript
 jalview.schemabinding.version2.Setting=jalview.schemabinding.version2.descriptors.SettingDescriptor
 jalview.schemabinding.version2.AlcodonFrame=jalview.schemabinding.version2.descriptors.AlcodonFrameDescriptor
 jalview.schemabinding.version2.AnnotationElement=jalview.schemabinding.version2.descriptors.AnnotationElementDescriptor
+jalview.schemabinding.version2.FeatureMatcherSet=jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor
 jalview.schemabinding.version2.SecondaryStructure=jalview.schemabinding.version2.descriptors.SecondaryStructureDescriptor
+jalview.schemabinding.version2.MatchCondition=jalview.schemabinding.version2.descriptors.MatchConditionDescriptor
 jalview.schemabinding.version2.SequenceSet=jalview.schemabinding.version2.descriptors.SequenceSetDescriptor
 jalview.schemabinding.version2.Viewport=jalview.schemabinding.version2.descriptors.ViewportDescriptor
 jalview.schemabinding.version2.RnaViewer=jalview.schemabinding.version2.descriptors.RnaViewerDescriptor
@@ -20,31 +22,32 @@ jalview.schemabinding.version2.UserColourScheme=jalview.schemabinding.version2.d
 jalview.schemabinding.version2.DBRef=jalview.schemabinding.version2.descriptors.DBRefDescriptor
 jalview.schemabinding.version2.AlcodMap=jalview.schemabinding.version2.descriptors.AlcodMapDescriptor
 jalview.schemabinding.version2.Annotation=jalview.schemabinding.version2.descriptors.AnnotationDescriptor
-jalview.schemabinding.version2.Wsparameters=jalview.schemabinding.version2.descriptors.WsparametersDescriptor
 jalview.schemabinding.version2.JSeq=jalview.schemabinding.version2.descriptors.JSeqDescriptor
+jalview.schemabinding.version2.MatcherSet=jalview.schemabinding.version2.descriptors.MatcherSetDescriptor
 jalview.schemabinding.version2.Sequence=jalview.schemabinding.version2.descriptors.SequenceDescriptor
 jalview.schemabinding.version2.WebServiceParameterSet=jalview.schemabinding.version2.descriptors.WebServiceParameterSetDescriptor
 jalview.schemabinding.version2.Alcodon=jalview.schemabinding.version2.descriptors.AlcodonDescriptor
+jalview.schemabinding.version2.Filter=jalview.schemabinding.version2.descriptors.FilterDescriptor
 jalview.schemabinding.version2.AnnotationColours=jalview.schemabinding.version2.descriptors.AnnotationColoursDescriptor
 jalview.schemabinding.version2.Pdbids=jalview.schemabinding.version2.descriptors.PdbidsDescriptor
 jalview.schemabinding.version2.AnnotationColourScheme=jalview.schemabinding.version2.descriptors.AnnotationColourSchemeDescriptor
 jalview.schemabinding.version2.Mapping=jalview.schemabinding.version2.descriptors.MappingDescriptor
-jalview.schemabinding.version2.MappingChoice=jalview.schemabinding.version2.descriptors.MappingChoiceDescriptor
+jalview.schemabinding.version2.CompoundMatcher=jalview.schemabinding.version2.descriptors.CompoundMatcherDescriptor
+jalview.schemabinding.version2.JalviewModelSequence=jalview.schemabinding.version2.descriptors.JalviewModelSequenceDescriptor
 jalview.schemabinding.version2.Group=jalview.schemabinding.version2.descriptors.GroupDescriptor
+jalview.schemabinding.version2.MappingChoice=jalview.schemabinding.version2.descriptors.MappingChoiceDescriptor
 jalview.schemabinding.version2.Feature=jalview.schemabinding.version2.descriptors.FeatureDescriptor
-jalview.schemabinding.version2.JalviewModelSequence=jalview.schemabinding.version2.descriptors.JalviewModelSequenceDescriptor
 jalview.schemabinding.version2.UserColours=jalview.schemabinding.version2.descriptors.UserColoursDescriptor
 jalview.schemabinding.version2.Colour=jalview.schemabinding.version2.descriptors.ColourDescriptor
-jalview.schemabinding.version2.MapListFrom=jalview.schemabinding.version2.descriptors.MapListFromDescriptor
 jalview.schemabinding.version2.PdbentryItem=jalview.schemabinding.version2.descriptors.PdbentryItemDescriptor
-jalview.schemabinding.version2.JGroup=jalview.schemabinding.version2.descriptors.JGroupDescriptor
+jalview.schemabinding.version2.MapListFrom=jalview.schemabinding.version2.descriptors.MapListFromDescriptor
 jalview.schemabinding.version2.FeatureSettings=jalview.schemabinding.version2.descriptors.FeatureSettingsDescriptor
-jalview.schemabinding.version2.VamsasModel=jalview.schemabinding.version2.descriptors.VamsasModelDescriptor
-jalview.schemabinding.version2.JalviewUserColours=jalview.schemabinding.version2.descriptors.JalviewUserColoursDescriptor
+jalview.schemabinding.version2.JGroup=jalview.schemabinding.version2.descriptors.JGroupDescriptor
 jalview.schemabinding.version2.MapListTo=jalview.schemabinding.version2.descriptors.MapListToDescriptor
+jalview.schemabinding.version2.JalviewUserColours=jalview.schemabinding.version2.descriptors.JalviewUserColoursDescriptor
+jalview.schemabinding.version2.VamsasModel=jalview.schemabinding.version2.descriptors.VamsasModelDescriptor
 jalview.schemabinding.version2.Pdbentry=jalview.schemabinding.version2.descriptors.PdbentryDescriptor
 jalview.schemabinding.version2.HiddenColumns=jalview.schemabinding.version2.descriptors.HiddenColumnsDescriptor
 jalview.schemabinding.version2.Features=jalview.schemabinding.version2.descriptors.FeaturesDescriptor
-jalview.schemabinding.version2.DseqFor=jalview.schemabinding.version2.descriptors.DseqForDescriptor
 jalview.schemabinding.version2.VAMSAS=jalview.schemabinding.version2.descriptors.VAMSASDescriptor
-jalview.schemabinding.version2.MappingChoiceItem=jalview.schemabinding.version2.descriptors.MappingChoiceItemDescriptor
+jalview.schemabinding.version2.FeatureMatcher=jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor
index 9d5a916..d1c7297 100644 (file)
@@ -27,7 +27,8 @@ public class Colour implements java.io.Serializable
   // --------------------------/
 
   /**
-   * Field _name.
+   * Single letter residue code for an alignment colour scheme, or feature type
+   * for a feature colour scheme
    */
   private java.lang.String _name;
 
@@ -42,9 +43,15 @@ public class Colour implements java.io.Serializable
   private java.lang.String _minRGB;
 
   /**
-   * loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Field _noValueColour.
    */
-  private java.lang.String _threshType;
+  private jalview.schemabinding.version2.types.NoValueColour _noValueColour = jalview.schemabinding.version2.types.NoValueColour
+          .valueOf("Min");
+
+  /**
+   * Field _threshType.
+   */
+  private jalview.schemabinding.version2.types.ColourThreshTypeType _threshType;
 
   /**
    * Field _threshold.
@@ -96,6 +103,11 @@ public class Colour implements java.io.Serializable
    */
   private boolean _has_autoScale;
 
+  /**
+   * name of feature attribute to colour by, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -103,6 +115,9 @@ public class Colour implements java.io.Serializable
   public Colour()
   {
     super();
+    setNoValueColour(jalview.schemabinding.version2.types.NoValueColour
+            .valueOf("Min"));
+    this._attributeNameList = new java.util.Vector();
   }
 
   // -----------/
@@ -110,41 +125,140 @@ public class Colour implements java.io.Serializable
   // -----------/
 
   /**
-     */
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   */
   public void deleteAutoScale()
   {
     this._has_autoScale = false;
   }
 
   /**
-     */
+   */
   public void deleteColourByLabel()
   {
     this._has_colourByLabel = false;
   }
 
   /**
-     */
+   */
   public void deleteMax()
   {
     this._has_max = false;
   }
 
   /**
-     */
+   */
   public void deleteMin()
   {
     this._has_min = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshold()
   {
     this._has_threshold = false;
   }
 
   /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
    * Returns the value of field 'autoScale'.
    * 
    * @return the value of field 'AutoScale'.
@@ -195,7 +309,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'name'.
+   * Returns the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @return the value of field 'Name'.
    */
@@ -205,6 +321,16 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'noValueColour'.
+   * 
+   * @return the value of field 'NoValueColour'.
+   */
+  public jalview.schemabinding.version2.types.NoValueColour getNoValueColour()
+  {
+    return this._noValueColour;
+  }
+
+  /**
    * Returns the value of field 'RGB'.
    * 
    * @return the value of field 'RGB'.
@@ -215,12 +341,11 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Returns the value of field 'threshType'.
    * 
    * @return the value of field 'ThreshType'.
    */
-  public java.lang.String getThreshType()
+  public jalview.schemabinding.version2.types.ColourThreshTypeType getThreshType()
   {
     return this._threshType;
   }
@@ -360,6 +485,76 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'autoScale'.
    * 
    * @param autoScale
@@ -419,7 +614,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'name'.
+   * Sets the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @param name
    *          the value of field 'name'.
@@ -430,6 +627,18 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'noValueColour'.
+   * 
+   * @param noValueColour
+   *          the value of field 'noValueColour'.
+   */
+  public void setNoValueColour(
+          final jalview.schemabinding.version2.types.NoValueColour noValueColour)
+  {
+    this._noValueColour = noValueColour;
+  }
+
+  /**
    * Sets the value of field 'RGB'.
    * 
    * @param RGB
@@ -441,13 +650,13 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Sets the value of field 'threshType'.
    * 
    * @param threshType
    *          the value of field 'threshType'.
    */
-  public void setThreshType(final java.lang.String threshType)
+  public void setThreshType(
+          final jalview.schemabinding.version2.types.ColourThreshTypeType threshType)
   {
     this._threshType = threshType;
   }
@@ -480,8 +689,8 @@ public class Colour implements java.io.Serializable
           throws org.exolab.castor.xml.MarshalException,
           org.exolab.castor.xml.ValidationException
   {
-    return (jalview.schemabinding.version2.Colour) Unmarshaller.unmarshal(
-            jalview.schemabinding.version2.Colour.class, reader);
+    return (jalview.schemabinding.version2.Colour) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.Colour.class, reader);
   }
 
   /**
diff --git a/src/jalview/schemabinding/version2/CompoundMatcher.java b/src/jalview/schemabinding/version2/CompoundMatcher.java
new file mode 100644 (file)
index 0000000..27714e2
--- /dev/null
@@ -0,0 +1,374 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class CompoundMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class CompoundMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * If true, matchers are AND-ed, if false they are OR-ed
+   */
+  private boolean _and;
+
+  /**
+   * keeps track of state for field: _and
+   */
+  private boolean _has_and;
+
+  /**
+   * Field _matcherSetList.
+   */
+  private java.util.Vector _matcherSetList;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public CompoundMatcher()
+  {
+    super();
+    this._matcherSetList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.addElement(vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(final int index,
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.add(index, vMatcherSet);
+  }
+
+  /**
+   */
+  public void deleteAnd()
+  {
+    this._has_and = false;
+  }
+
+  /**
+   * Method enumerateMatcherSet.
+   * 
+   * @return an Enumeration over all jalview.schemabinding.version2.MatcherSet
+   *         elements
+   */
+  public java.util.Enumeration enumerateMatcherSet()
+  {
+    return this._matcherSetList.elements();
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean getAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method getMatcherSet.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the jalview.schemabinding.version2.MatcherSet at the
+   *         given index
+   */
+  public jalview.schemabinding.version2.MatcherSet getMatcherSet(
+          final int index) throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    return (jalview.schemabinding.version2.MatcherSet) _matcherSetList
+            .get(index);
+  }
+
+  /**
+   * Method getMatcherSet.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public jalview.schemabinding.version2.MatcherSet[] getMatcherSet()
+  {
+    jalview.schemabinding.version2.MatcherSet[] array = new jalview.schemabinding.version2.MatcherSet[0];
+    return (jalview.schemabinding.version2.MatcherSet[]) this._matcherSetList
+            .toArray(array);
+  }
+
+  /**
+   * Method getMatcherSetCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getMatcherSetCount()
+  {
+    return this._matcherSetList.size();
+  }
+
+  /**
+   * Method hasAnd.
+   * 
+   * @return true if at least one And has been added
+   */
+  public boolean hasAnd()
+  {
+    return this._has_and;
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean isAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllMatcherSet()
+  {
+    this._matcherSetList.clear();
+  }
+
+  /**
+   * Method removeMatcherSet.
+   * 
+   * @param vMatcherSet
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+  {
+    boolean removed = _matcherSetList.remove(vMatcherSet);
+    return removed;
+  }
+
+  /**
+   * Method removeMatcherSetAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public jalview.schemabinding.version2.MatcherSet removeMatcherSetAt(
+          final int index)
+  {
+    java.lang.Object obj = this._matcherSetList.remove(index);
+    return (jalview.schemabinding.version2.MatcherSet) obj;
+  }
+
+  /**
+   * Sets the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @param and
+   *          the value of field 'and'.
+   */
+  public void setAnd(final boolean and)
+  {
+    this._and = and;
+    this._has_and = true;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setMatcherSet(final int index,
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    this._matcherSetList.set(index, vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param vMatcherSetArray
+   */
+  public void setMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet[] vMatcherSetArray)
+  {
+    // -- copy array
+    _matcherSetList.clear();
+
+    for (int i = 0; i < vMatcherSetArray.length; i++)
+    {
+      this._matcherSetList.add(vMatcherSetArray[i]);
+    }
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.CompoundMatcher
+   */
+  public static jalview.schemabinding.version2.CompoundMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.CompoundMatcher) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.CompoundMatcher.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/FeatureMatcher.java b/src/jalview/schemabinding/version2/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..4d29cab
--- /dev/null
@@ -0,0 +1,383 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class FeatureMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _by.
+   */
+  private jalview.schemabinding.version2.types.FeatureMatcherByType _by;
+
+  /**
+   * name of feature attribute to filter on, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
+  /**
+   * Field _condition.
+   */
+  private java.lang.String _condition;
+
+  /**
+   * Field _value.
+   */
+  private java.lang.String _value;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcher()
+  {
+    super();
+    this._attributeNameList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
+   * Returns the value of field 'by'.
+   * 
+   * @return the value of field 'By'.
+   */
+  public jalview.schemabinding.version2.types.FeatureMatcherByType getBy()
+  {
+    return this._by;
+  }
+
+  /**
+   * Returns the value of field 'condition'.
+   * 
+   * @return the value of field 'Condition'.
+   */
+  public java.lang.String getCondition()
+  {
+    return this._condition;
+  }
+
+  /**
+   * Returns the value of field 'value'.
+   * 
+   * @return the value of field 'Value'.
+   */
+  public java.lang.String getValue()
+  {
+    return this._value;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
+   * Sets the value of field 'by'.
+   * 
+   * @param by
+   *          the value of field 'by'.
+   */
+  public void setBy(
+          final jalview.schemabinding.version2.types.FeatureMatcherByType by)
+  {
+    this._by = by;
+  }
+
+  /**
+   * Sets the value of field 'condition'.
+   * 
+   * @param condition
+   *          the value of field 'condition'.
+   */
+  public void setCondition(final java.lang.String condition)
+  {
+    this._condition = condition;
+  }
+
+  /**
+   * Sets the value of field 'value'.
+   * 
+   * @param value
+   *          the value of field 'value'.
+   */
+  public void setValue(final java.lang.String value)
+  {
+    this._value = value;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcher
+   */
+  public static jalview.schemabinding.version2.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.FeatureMatcher.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/FeatureMatcherSet.java b/src/jalview/schemabinding/version2/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..2d79a98
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * A feature match condition, which may be simple or compound
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherSet implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Internal choice value storage
+   */
+  private java.lang.Object _choiceValue;
+
+  /**
+   * Field _matchCondition.
+   */
+  private MatchCondition _matchCondition;
+
+  /**
+   * Field _compoundMatcher.
+   */
+  private CompoundMatcher _compoundMatcher;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'choiceValue'. The field 'choiceValue' has the
+   * following description: Internal choice value storage
+   * 
+   * @return the value of field 'ChoiceValue'.
+   */
+  public java.lang.Object getChoiceValue()
+  {
+    return this._choiceValue;
+  }
+
+  /**
+   * Returns the value of field 'compoundMatcher'.
+   * 
+   * @return the value of field 'CompoundMatcher'.
+   */
+  public CompoundMatcher getCompoundMatcher()
+  {
+    return this._compoundMatcher;
+  }
+
+  /**
+   * Returns the value of field 'matchCondition'.
+   * 
+   * @return the value of field 'MatchCondition'.
+   */
+  public MatchCondition getMatchCondition()
+  {
+    return this._matchCondition;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'compoundMatcher'.
+   * 
+   * @param compoundMatcher
+   *          the value of field 'compoundMatcher'.
+   */
+  public void setCompoundMatcher(final CompoundMatcher compoundMatcher)
+  {
+    this._compoundMatcher = compoundMatcher;
+    this._choiceValue = compoundMatcher;
+  }
+
+  /**
+   * Sets the value of field 'matchCondition'.
+   * 
+   * @param matchCondition
+   *          the value of field 'matchCondition'.
+   */
+  public void setMatchCondition(final MatchCondition matchCondition)
+  {
+    this._matchCondition = matchCondition;
+    this._choiceValue = matchCondition;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcherSet
+   */
+  public static jalview.schemabinding.version2.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcherSet) Unmarshaller
+            .unmarshal(
+                    jalview.schemabinding.version2.FeatureMatcherSet.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/Filter.java b/src/jalview/schemabinding/version2/Filter.java
new file mode 100644 (file)
index 0000000..45323a7
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class Filter.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class Filter implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _featureType.
+   */
+  private java.lang.String _featureType;
+
+  /**
+   * Field _matcherSet.
+   */
+  private jalview.schemabinding.version2.MatcherSet _matcherSet;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public Filter()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'featureType'.
+   * 
+   * @return the value of field 'FeatureType'.
+   */
+  public java.lang.String getFeatureType()
+  {
+    return this._featureType;
+  }
+
+  /**
+   * Returns the value of field 'matcherSet'.
+   * 
+   * @return the value of field 'MatcherSet'.
+   */
+  public jalview.schemabinding.version2.MatcherSet getMatcherSet()
+  {
+    return this._matcherSet;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'featureType'.
+   * 
+   * @param featureType
+   *          the value of field 'featureType'.
+   */
+  public void setFeatureType(final java.lang.String featureType)
+  {
+    this._featureType = featureType;
+  }
+
+  /**
+   * Sets the value of field 'matcherSet'.
+   * 
+   * @param matcherSet
+   *          the value of field 'matcherSet'.
+   */
+  public void setMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet matcherSet)
+  {
+    this._matcherSet = matcherSet;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.Filter
+   */
+  public static jalview.schemabinding.version2.Filter unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.Filter) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.Filter.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
index 042f092..c8d52ac 100644 (file)
@@ -42,6 +42,11 @@ public class JalviewUserColours implements java.io.Serializable
    */
   private java.util.Vector _colourList;
 
+  /**
+   * Field _filterList.
+   */
+  private java.util.Vector _filterList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -50,6 +55,7 @@ public class JalviewUserColours implements java.io.Serializable
   {
     super();
     this._colourList = new java.util.Vector();
+    this._filterList = new java.util.Vector();
   }
 
   // -----------/
@@ -84,6 +90,33 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.addElement(vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.add(index, vFilter);
+  }
+
+  /**
    * Method enumerateColour.
    * 
    * @return an Enumeration over all Colour elements
@@ -94,6 +127,16 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method enumerateFilter.
+   * 
+   * @return an Enumeration over all Filter elements
+   */
+  public java.util.Enumeration enumerateFilter()
+  {
+    return this._filterList.elements();
+  }
+
+  /**
    * Method getColour.
    * 
    * @param index
@@ -107,9 +150,9 @@ public class JalviewUserColours implements java.io.Serializable
     // check bounds for index
     if (index < 0 || index >= this._colourList.size())
     {
-      throw new IndexOutOfBoundsException("getColour: Index value '"
-              + index + "' not in range [0.."
-              + (this._colourList.size() - 1) + "]");
+      throw new IndexOutOfBoundsException(
+              "getColour: Index value '" + index + "' not in range [0.."
+                      + (this._colourList.size() - 1) + "]");
     }
 
     return (Colour) _colourList.get(index);
@@ -141,6 +184,53 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method getFilter.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the Filter at the given index
+   */
+  public Filter getFilter(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    return (Filter) _filterList.get(index);
+  }
+
+  /**
+   * Method getFilter.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public Filter[] getFilter()
+  {
+    Filter[] array = new Filter[0];
+    return (Filter[]) this._filterList.toArray(array);
+  }
+
+  /**
+   * Method getFilterCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getFilterCount()
+  {
+    return this._filterList.size();
+  }
+
+  /**
    * Returns the value of field 'schemeName'.
    * 
    * @return the value of field 'SchemeName'.
@@ -217,13 +307,20 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
-     */
+   */
   public void removeAllColour()
   {
     this._colourList.clear();
   }
 
   /**
+   */
+  public void removeAllFilter()
+  {
+    this._filterList.clear();
+  }
+
+  /**
    * Method removeColour.
    * 
    * @param vColour
@@ -248,6 +345,30 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method removeFilter.
+   * 
+   * @param vFilter
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeFilter(final Filter vFilter)
+  {
+    boolean removed = _filterList.remove(vFilter);
+    return removed;
+  }
+
+  /**
+   * Method removeFilterAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public Filter removeFilterAt(final int index)
+  {
+    java.lang.Object obj = this._filterList.remove(index);
+    return (Filter) obj;
+  }
+
+  /**
    * 
    * 
    * @param index
@@ -261,9 +382,9 @@ public class JalviewUserColours implements java.io.Serializable
     // check bounds for index
     if (index < 0 || index >= this._colourList.size())
     {
-      throw new IndexOutOfBoundsException("setColour: Index value '"
-              + index + "' not in range [0.."
-              + (this._colourList.size() - 1) + "]");
+      throw new IndexOutOfBoundsException(
+              "setColour: Index value '" + index + "' not in range [0.."
+                      + (this._colourList.size() - 1) + "]");
     }
 
     this._colourList.set(index, vColour);
@@ -286,6 +407,44 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    this._filterList.set(index, vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param vFilterArray
+   */
+  public void setFilter(final Filter[] vFilterArray)
+  {
+    // -- copy array
+    _filterList.clear();
+
+    for (int i = 0; i < vFilterArray.length; i++)
+    {
+      this._filterList.add(vFilterArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'schemeName'.
    * 
    * @param schemeName
diff --git a/src/jalview/schemabinding/version2/MatchCondition.java b/src/jalview/schemabinding/version2/MatchCondition.java
new file mode 100644 (file)
index 0000000..af2f3f5
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class MatchCondition.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatchCondition extends FeatureMatcher
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatchCondition()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcher
+   */
+  public static jalview.schemabinding.version2.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.MatchCondition.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/MatcherSet.java b/src/jalview/schemabinding/version2/MatcherSet.java
new file mode 100644 (file)
index 0000000..6fde9e4
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * optional filter(s) applied to the feature type
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatcherSet extends FeatureMatcherSet
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcherSet
+   */
+  public static jalview.schemabinding.version2.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcherSet) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.MatcherSet.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
index fb6b276..31797fe 100644 (file)
@@ -7,8 +7,8 @@
 
 package jalview.schemabinding.version2;
 
-//---------------------------------/
-//- Imported classes and packages -/
+  //---------------------------------/
+ //- Imported classes and packages -/
 //---------------------------------/
 
 import org.exolab.castor.xml.Marshaller;
@@ -19,163 +19,181 @@ import org.exolab.castor.xml.Unmarshaller;
  * 
  * @version $Revision$ $Date$
  */
-public class OtherData implements java.io.Serializable
-{
-
-  // --------------------------/
-  // - Class/Member Variables -/
-  // --------------------------/
-
-  /**
-   * Field _key.
-   */
-  private java.lang.String _key;
-
-  /**
-   * Field _value.
-   */
-  private java.lang.String _value;
-
-  // ----------------/
-  // - Constructors -/
-  // ----------------/
-
-  public OtherData()
-  {
-    super();
-  }
-
-  // -----------/
-  // - Methods -/
-  // -----------/
-
-  /**
-   * Returns the value of field 'key'.
-   * 
-   * @return the value of field 'Key'.
-   */
-  public java.lang.String getKey()
-  {
-    return this._key;
-  }
-
-  /**
-   * Returns the value of field 'value'.
-   * 
-   * @return the value of field 'Value'.
-   */
-  public java.lang.String getValue()
-  {
-    return this._value;
-  }
-
-  /**
-   * Method isValid.
-   * 
-   * @return true if this object is valid according to the schema
-   */
-  public boolean isValid()
-  {
-    try
-    {
-      validate();
-    } catch (org.exolab.castor.xml.ValidationException vex)
-    {
-      return false;
+public class OtherData implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _key.
+     */
+    private java.lang.String _key;
+
+    /**
+     * key2 may be used for a sub-attribute of key
+     */
+    private java.lang.String _key2;
+
+    /**
+     * Field _value.
+     */
+    private java.lang.String _value;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public OtherData() {
+        super();
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Returns the value of field 'key'.
+     * 
+     * @return the value of field 'Key'.
+     */
+    public java.lang.String getKey(
+    ) {
+        return this._key;
+    }
+
+    /**
+     * Returns the value of field 'key2'. The field 'key2' has the
+     * following description: key2 may be used for a sub-attribute
+     * of key
+     * 
+     * @return the value of field 'Key2'.
+     */
+    public java.lang.String getKey2(
+    ) {
+        return this._key2;
+    }
+
+    /**
+     * Returns the value of field 'value'.
+     * 
+     * @return the value of field 'Value'.
+     */
+    public java.lang.String getValue(
+    ) {
+        return this._value;
+    }
+
+    /**
+     * Method isValid.
+     * 
+     * @return true if this object is valid according to the schema
+     */
+    public boolean isValid(
+    ) {
+        try {
+            validate();
+        } catch (org.exolab.castor.xml.ValidationException vex) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 
+     * 
+     * @param out
+     * @throws org.exolab.castor.xml.MarshalException if object is
+     * null or if any SAXException is thrown during marshaling
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     */
+    public void marshal(
+            final java.io.Writer out)
+    throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+        Marshaller.marshal(this, out);
+    }
+
+    /**
+     * 
+     * 
+     * @param handler
+     * @throws java.io.IOException if an IOException occurs during
+     * marshaling
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     * @throws org.exolab.castor.xml.MarshalException if object is
+     * null or if any SAXException is thrown during marshaling
+     */
+    public void marshal(
+            final org.xml.sax.ContentHandler handler)
+    throws java.io.IOException, org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+        Marshaller.marshal(this, handler);
+    }
+
+    /**
+     * Sets the value of field 'key'.
+     * 
+     * @param key the value of field 'key'.
+     */
+    public void setKey(
+            final java.lang.String key) {
+        this._key = key;
+    }
+
+    /**
+     * Sets the value of field 'key2'. The field 'key2' has the
+     * following description: key2 may be used for a sub-attribute
+     * of key
+     * 
+     * @param key2 the value of field 'key2'.
+     */
+    public void setKey2(
+            final java.lang.String key2) {
+        this._key2 = key2;
+    }
+
+    /**
+     * Sets the value of field 'value'.
+     * 
+     * @param value the value of field 'value'.
+     */
+    public void setValue(
+            final java.lang.String value) {
+        this._value = value;
+    }
+
+    /**
+     * Method unmarshal.
+     * 
+     * @param reader
+     * @throws org.exolab.castor.xml.MarshalException if object is
+     * null or if any SAXException is thrown during marshaling
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     * @return the unmarshaled
+     * jalview.schemabinding.version2.OtherData
+     */
+    public static jalview.schemabinding.version2.OtherData unmarshal(
+            final java.io.Reader reader)
+    throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+        return (jalview.schemabinding.version2.OtherData) Unmarshaller.unmarshal(jalview.schemabinding.version2.OtherData.class, reader);
+    }
+
+    /**
+     * 
+     * 
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     */
+    public void validate(
+    )
+    throws org.exolab.castor.xml.ValidationException {
+        org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+        validator.validate(this);
     }
-    return true;
-  }
-
-  /**
-   * 
-   * 
-   * @param out
-   * @throws org.exolab.castor.xml.MarshalException
-   *           if object is null or if any SAXException is thrown during
-   *           marshaling
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   */
-  public void marshal(final java.io.Writer out)
-          throws org.exolab.castor.xml.MarshalException,
-          org.exolab.castor.xml.ValidationException
-  {
-    Marshaller.marshal(this, out);
-  }
-
-  /**
-   * 
-   * 
-   * @param handler
-   * @throws java.io.IOException
-   *           if an IOException occurs during marshaling
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   * @throws org.exolab.castor.xml.MarshalException
-   *           if object is null or if any SAXException is thrown during
-   *           marshaling
-   */
-  public void marshal(final org.xml.sax.ContentHandler handler)
-          throws java.io.IOException,
-          org.exolab.castor.xml.MarshalException,
-          org.exolab.castor.xml.ValidationException
-  {
-    Marshaller.marshal(this, handler);
-  }
-
-  /**
-   * Sets the value of field 'key'.
-   * 
-   * @param key
-   *          the value of field 'key'.
-   */
-  public void setKey(final java.lang.String key)
-  {
-    this._key = key;
-  }
-
-  /**
-   * Sets the value of field 'value'.
-   * 
-   * @param value
-   *          the value of field 'value'.
-   */
-  public void setValue(final java.lang.String value)
-  {
-    this._value = value;
-  }
-
-  /**
-   * Method unmarshal.
-   * 
-   * @param reader
-   * @throws org.exolab.castor.xml.MarshalException
-   *           if object is null or if any SAXException is thrown during
-   *           marshaling
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   * @return the unmarshaled jalview.schemabinding.version2.OtherData
-   */
-  public static jalview.schemabinding.version2.OtherData unmarshal(
-          final java.io.Reader reader)
-          throws org.exolab.castor.xml.MarshalException,
-          org.exolab.castor.xml.ValidationException
-  {
-    return (jalview.schemabinding.version2.OtherData) Unmarshaller
-            .unmarshal(jalview.schemabinding.version2.OtherData.class,
-                    reader);
-  }
-
-  /**
-   * 
-   * 
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   */
-  public void validate() throws org.exolab.castor.xml.ValidationException
-  {
-    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
-    validator.validate(this);
-  }
 
 }
index c458971..59e9522 100644 (file)
@@ -73,6 +73,12 @@ public class Setting implements java.io.Serializable
   private boolean _has_mincolour;
 
   /**
+   * Field _noValueColour.
+   */
+  private jalview.schemabinding.version2.types.NoValueColour _noValueColour = jalview.schemabinding.version2.types.NoValueColour
+          .valueOf("Min");
+
+  /**
    * threshold value for graduated feature colour
    * 
    */
@@ -134,6 +140,16 @@ public class Setting implements java.io.Serializable
    */
   private boolean _has_autoScale;
 
+  /**
+   * name of feature attribute to colour by, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
+  /**
+   * optional filter(s) applied to the feature type
+   */
+  private jalview.schemabinding.version2.MatcherSet _matcherSet;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -141,6 +157,9 @@ public class Setting implements java.io.Serializable
   public Setting()
   {
     super();
+    setNoValueColour(jalview.schemabinding.version2.types.NoValueColour
+            .valueOf("Min"));
+    this._attributeNameList = new java.util.Vector();
   }
 
   // -----------/
@@ -148,76 +167,175 @@ public class Setting implements java.io.Serializable
   // -----------/
 
   /**
-     */
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   */
   public void deleteAutoScale()
   {
     this._has_autoScale = false;
   }
 
   /**
-     */
+   */
   public void deleteColour()
   {
     this._has_colour = false;
   }
 
   /**
-     */
+   */
   public void deleteColourByLabel()
   {
     this._has_colourByLabel = false;
   }
 
   /**
-     */
+   */
   public void deleteDisplay()
   {
     this._has_display = false;
   }
 
   /**
-     */
+   */
   public void deleteMax()
   {
     this._has_max = false;
   }
 
   /**
-     */
+   */
   public void deleteMin()
   {
     this._has_min = false;
   }
 
   /**
-     */
+   */
   public void deleteMincolour()
   {
     this._has_mincolour = false;
   }
 
   /**
-     */
+   */
   public void deleteOrder()
   {
     this._has_order = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshold()
   {
     this._has_threshold = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshstate()
   {
     this._has_threshstate = false;
   }
 
   /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
    * Returns the value of field 'autoScale'.
    * 
    * @return the value of field 'AutoScale'.
@@ -258,6 +376,17 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'matcherSet'. The field 'matcherSet' has the
+   * following description: optional filter(s) applied to the feature type
+   * 
+   * @return the value of field 'MatcherSet'.
+   */
+  public jalview.schemabinding.version2.MatcherSet getMatcherSet()
+  {
+    return this._matcherSet;
+  }
+
+  /**
    * Returns the value of field 'max'.
    * 
    * @return the value of field 'Max'.
@@ -290,6 +419,16 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'noValueColour'.
+   * 
+   * @return the value of field 'NoValueColour'.
+   */
+  public jalview.schemabinding.version2.types.NoValueColour getNoValueColour()
+  {
+    return this._noValueColour;
+  }
+
+  /**
    * Returns the value of field 'order'.
    * 
    * @return the value of field 'Order'.
@@ -518,6 +657,76 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'autoScale'.
    * 
    * @param autoScale
@@ -566,6 +775,19 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'matcherSet'. The field 'matcherSet' has the
+   * following description: optional filter(s) applied to the feature type
+   * 
+   * @param matcherSet
+   *          the value of field 'matcherSet'.
+   */
+  public void setMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet matcherSet)
+  {
+    this._matcherSet = matcherSet;
+  }
+
+  /**
    * Sets the value of field 'max'.
    * 
    * @param max
@@ -604,6 +826,18 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'noValueColour'.
+   * 
+   * @param noValueColour
+   *          the value of field 'noValueColour'.
+   */
+  public void setNoValueColour(
+          final jalview.schemabinding.version2.types.NoValueColour noValueColour)
+  {
+    this._noValueColour = noValueColour;
+  }
+
+  /**
    * Sets the value of field 'order'.
    * 
    * @param order
index 8b1ae9e..cca4ef1 100644 (file)
@@ -18,8 +18,8 @@ import jalview.schemabinding.version2.Colour;
  * 
  * @version $Revision$ $Date$
  */
-public class ColourDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
+public class ColourDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
 {
 
   // --------------------------/
@@ -55,6 +55,9 @@ public class ColourDescriptor extends
     super();
     _xmlName = "colour";
     _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
     org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
     org.exolab.castor.mapping.FieldHandler handler = null;
     org.exolab.castor.xml.FieldValidator fieldValidator = null;
@@ -197,11 +200,57 @@ public class ColourDescriptor extends
       typeValidator.setWhiteSpace("preserve");
     }
     desc.setValidator(fieldValidator);
-    // -- _threshType
+    // -- _noValueColour
     desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
-            java.lang.String.class, "_threshType", "threshType",
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            "_noValueColour", "noValueColour",
             org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Colour target = (Colour) object;
+        return target.getNoValueColour();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Colour target = (Colour) object;
+          target.setNoValueColour(
+                  (jalview.schemabinding.version2.types.NoValueColour) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            handler);
     desc.setImmutable(true);
+    desc.setHandler(handler);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _noValueColour
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+    // -- _threshType
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.types.ColourThreshTypeType.class,
+            "_threshType", "threshType",
+            org.exolab.castor.xml.NodeType.Attribute);
     handler = new org.exolab.castor.xml.XMLFieldHandler()
     {
       public java.lang.Object getValue(java.lang.Object object)
@@ -217,7 +266,8 @@ public class ColourDescriptor extends
         try
         {
           Colour target = (Colour) object;
-          target.setThreshType((java.lang.String) value);
+          target.setThreshType(
+                  (jalview.schemabinding.version2.types.ColourThreshTypeType) value);
         } catch (java.lang.Exception ex)
         {
           throw new IllegalStateException(ex.toString());
@@ -229,6 +279,10 @@ public class ColourDescriptor extends
         return null;
       }
     };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.ColourThreshTypeType.class,
+            handler);
+    desc.setImmutable(true);
     desc.setHandler(handler);
     desc.setMultivalued(false);
     addFieldDescriptor(desc);
@@ -236,10 +290,6 @@ public class ColourDescriptor extends
     // -- validation code for: _threshType
     fieldValidator = new org.exolab.castor.xml.FieldValidator();
     { // -- local scope
-      org.exolab.castor.xml.validators.StringValidator typeValidator;
-      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
-      fieldValidator.setValidator(typeValidator);
-      typeValidator.setWhiteSpace("preserve");
     }
     desc.setValidator(fieldValidator);
     // -- _threshold
@@ -437,8 +487,8 @@ public class ColourDescriptor extends
             target.deleteColourByLabel();
             return;
           }
-          target.setColourByLabel(((java.lang.Boolean) value)
-                  .booleanValue());
+          target.setColourByLabel(
+                  ((java.lang.Boolean) value).booleanValue());
         } catch (java.lang.Exception ex)
         {
           throw new IllegalStateException(ex.toString());
@@ -518,6 +568,66 @@ public class ColourDescriptor extends
     desc.setValidator(fieldValidator);
     // -- initialize element descriptors
 
+    // -- _attributeNameList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_attributeNameList", "attributeName",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Colour target = (Colour) object;
+        return target.getAttributeName();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Colour target = (Colour) object;
+          target.addAttributeName((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Colour target = (Colour) object;
+          target.removeAllAttributeName();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _attributeNameList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
   }
 
   // -----------/
diff --git a/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java b/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java
new file mode 100644 (file)
index 0000000..2402d68
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.CompoundMatcher;
+
+/**
+ * Class CompoundMatcherDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class CompoundMatcherDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public CompoundMatcherDescriptor()
+  {
+    super();
+    _xmlName = "compoundMatcher";
+    _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- _and
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.Boolean.TYPE, "_and", "and",
+            org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        CompoundMatcher target = (CompoundMatcher) object;
+        if (!target.hasAnd())
+        {
+          return null;
+        }
+        return (target.getAnd() ? java.lang.Boolean.TRUE
+                : java.lang.Boolean.FALSE);
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          CompoundMatcher target = (CompoundMatcher) object;
+          // ignore null values for non optional primitives
+          if (value == null)
+          {
+            return;
+          }
+
+          target.setAnd(((java.lang.Boolean) value).booleanValue());
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _and
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.BooleanValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.BooleanValidator();
+      fieldValidator.setValidator(typeValidator);
+    }
+    desc.setValidator(fieldValidator);
+    // -- initialize element descriptors
+
+    // -- _matcherSetList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.MatcherSet.class,
+            "_matcherSetList", "matcherSet",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        CompoundMatcher target = (CompoundMatcher) object;
+        return target.getMatcherSet();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          CompoundMatcher target = (CompoundMatcher) object;
+          target.addMatcherSet(
+                  (jalview.schemabinding.version2.MatcherSet) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          CompoundMatcher target = (CompoundMatcher) object;
+          target.removeAllMatcherSet();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new jalview.schemabinding.version2.MatcherSet();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matcherSetList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(2);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.CompoundMatcher.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java
new file mode 100644 (file)
index 0000000..2df2f5b
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.FeatureMatcher;
+
+/**
+ * Class FeatureMatcherDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherDescriptor()
+  {
+    super();
+    _nsURI = "www.jalview.org/colours";
+    _xmlName = "FeatureMatcher";
+    _elementDefinition = false;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- _by
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.types.FeatureMatcherByType.class,
+            "_by", "by", org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getBy();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.setBy(
+                  (jalview.schemabinding.version2.types.FeatureMatcherByType) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.FeatureMatcherByType.class,
+            handler);
+    desc.setImmutable(true);
+    desc.setHandler(handler);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _by
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+    // -- initialize element descriptors
+
+    // -- _attributeNameList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_attributeNameList", "attributeName",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getAttributeName();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.addAttributeName((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.removeAllAttributeName();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _attributeNameList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- _condition
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_condition", "condition",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getCondition();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.setCondition((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _condition
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- _value
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_value", "value",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getValue();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.setValue((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _value
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.FeatureMatcher.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java
new file mode 100644 (file)
index 0000000..b3d19bb
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+import jalview.schemabinding.version2.CompoundMatcher;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.FeatureMatcherSet;
+import jalview.schemabinding.version2.MatchCondition;
+
+/**
+ * Class FeatureMatcherSetDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherSetDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherSetDescriptor()
+  {
+    super();
+    _nsURI = "www.jalview.org/colours";
+    _xmlName = "FeatureMatcherSet";
+    _elementDefinition = false;
+
+    // -- set grouping compositor
+    setCompositorAsChoice();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- initialize element descriptors
+
+    // -- _matchCondition
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            MatchCondition.class, "_matchCondition", "matchCondition",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      @Override
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcherSet target = (FeatureMatcherSet) object;
+        return target.getMatchCondition();
+      }
+
+      @Override
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcherSet target = (FeatureMatcherSet) object;
+          target.setMatchCondition((MatchCondition) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new MatchCondition();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matchCondition
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+    // -- _compoundMatcher
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            CompoundMatcher.class, "_compoundMatcher", "compoundMatcher",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      @Override
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcherSet target = (FeatureMatcherSet) object;
+        return target.getCompoundMatcher();
+      }
+
+      @Override
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcherSet target = (FeatureMatcherSet) object;
+          target.setCompoundMatcher((CompoundMatcher) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new CompoundMatcher();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _compoundMatcher
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  @Override
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  @Override
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  @Override
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.FeatureMatcherSet.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  @Override
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  @Override
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  @Override
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  @Override
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  @Override
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java
new file mode 100644 (file)
index 0000000..f58f9ae
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.Filter;
+
+/**
+ * Class FilterDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FilterDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FilterDescriptor()
+  {
+    super();
+    _xmlName = "filter";
+    _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- _featureType
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_featureType", "featureType",
+            org.exolab.castor.xml.NodeType.Attribute);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Filter target = (Filter) object;
+        return target.getFeatureType();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Filter target = (Filter) object;
+          target.setFeatureType((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _featureType
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- initialize element descriptors
+
+    // -- _matcherSet
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.MatcherSet.class, "_matcherSet",
+            "matcherSet", org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Filter target = (Filter) object;
+        return target.getMatcherSet();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Filter target = (Filter) object;
+          target.setMatcherSet(
+                  (jalview.schemabinding.version2.MatcherSet) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new jalview.schemabinding.version2.MatcherSet();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matcherSet
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.Filter.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
index d65de13..459d645 100644 (file)
@@ -7,11 +7,13 @@
 
 package jalview.schemabinding.version2.descriptors;
 
+import jalview.schemabinding.version2.Colour;
+import jalview.schemabinding.version2.Filter;
+
 //---------------------------------/
 //- Imported classes and packages -/
 //---------------------------------/
 
-import jalview.schemabinding.version2.Colour;
 import jalview.schemabinding.version2.JalviewUserColours;
 
 /**
@@ -19,8 +21,8 @@ import jalview.schemabinding.version2.JalviewUserColours;
  * 
  * @version $Revision$ $Date$
  */
-public class JalviewUserColoursDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
+public class JalviewUserColoursDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
 {
 
   // --------------------------/
@@ -192,8 +194,8 @@ public class JalviewUserColoursDescriptor extends
       }
 
       @Override
-      public void resetValue(Object object) throws IllegalStateException,
-              IllegalArgumentException
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
       {
         try
         {
@@ -221,6 +223,64 @@ public class JalviewUserColoursDescriptor extends
     { // -- local scope
     }
     desc.setValidator(fieldValidator);
+    // -- _filterList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            Filter.class, "_filterList", "filter",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      @Override
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        JalviewUserColours target = (JalviewUserColours) object;
+        return target.getFilter();
+      }
+
+      @Override
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          JalviewUserColours target = (JalviewUserColours) object;
+          target.addFilter((Filter) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          JalviewUserColours target = (JalviewUserColours) object;
+          target.removeAllFilter();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new Filter();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _filterList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
   }
 
   // -----------/
diff --git a/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java b/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java
new file mode 100644 (file)
index 0000000..8373421
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.MatchCondition;
+
+/**
+ * Class MatchConditionDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatchConditionDescriptor extends
+        jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatchConditionDescriptor()
+  {
+    super();
+    setExtendsWithoutFlatten(
+            new jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor());
+    _xmlName = "matchCondition";
+    _elementDefinition = true;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.MatchCondition.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java b/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java
new file mode 100644 (file)
index 0000000..2807f92
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.MatcherSet;
+
+/**
+ * Class MatcherSetDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatcherSetDescriptor extends
+        jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatcherSetDescriptor()
+  {
+    super();
+    setExtendsWithoutFlatten(
+            new jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor());
+    _xmlName = "matcherSet";
+    _elementDefinition = true;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.MatcherSet.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
index f582311..ab7a626 100644 (file)
@@ -7,8 +7,8 @@
 
 package jalview.schemabinding.version2.descriptors;
 
-//---------------------------------/
-//- Imported classes and packages -/
+  //---------------------------------/
+ //- Imported classes and packages -/
 //---------------------------------/
 
 import jalview.schemabinding.version2.OtherData;
@@ -18,231 +18,255 @@ import jalview.schemabinding.version2.OtherData;
  * 
  * @version $Revision$ $Date$
  */
-public class OtherDataDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
-{
-
-  // --------------------------/
-  // - Class/Member Variables -/
-  // --------------------------/
-
-  /**
-   * Field _elementDefinition.
-   */
-  private boolean _elementDefinition;
-
-  /**
-   * Field _nsPrefix.
-   */
-  private java.lang.String _nsPrefix;
-
-  /**
-   * Field _nsURI.
-   */
-  private java.lang.String _nsURI;
-
-  /**
-   * Field _xmlName.
-   */
-  private java.lang.String _xmlName;
-
-  // ----------------/
-  // - Constructors -/
-  // ----------------/
-
-  public OtherDataDescriptor()
-  {
-    super();
-    _nsURI = "www.jalview.org";
-    _xmlName = "otherData";
-    _elementDefinition = true;
-    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
-    org.exolab.castor.mapping.FieldHandler handler = null;
-    org.exolab.castor.xml.FieldValidator fieldValidator = null;
-    // -- initialize attribute descriptors
-
-    // -- _key
-    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
-            java.lang.String.class, "_key", "key",
-            org.exolab.castor.xml.NodeType.Attribute);
-    desc.setImmutable(true);
-    handler = new org.exolab.castor.xml.XMLFieldHandler()
-    {
-      public java.lang.Object getValue(java.lang.Object object)
-              throws IllegalStateException
-      {
-        OtherData target = (OtherData) object;
-        return target.getKey();
-      }
-
-      public void setValue(java.lang.Object object, java.lang.Object value)
-              throws IllegalStateException, IllegalArgumentException
-      {
-        try
-        {
-          OtherData target = (OtherData) object;
-          target.setKey((java.lang.String) value);
-        } catch (java.lang.Exception ex)
-        {
-          throw new IllegalStateException(ex.toString());
+public class OtherDataDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _elementDefinition.
+     */
+    private boolean _elementDefinition;
+
+    /**
+     * Field _nsPrefix.
+     */
+    private java.lang.String _nsPrefix;
+
+    /**
+     * Field _nsURI.
+     */
+    private java.lang.String _nsURI;
+
+    /**
+     * Field _xmlName.
+     */
+    private java.lang.String _xmlName;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public OtherDataDescriptor() {
+        super();
+        _nsURI = "www.jalview.org";
+        _xmlName = "otherData";
+        _elementDefinition = true;
+        org.exolab.castor.xml.util.XMLFieldDescriptorImpl  desc           = null;
+        org.exolab.castor.mapping.FieldHandler             handler        = null;
+        org.exolab.castor.xml.FieldValidator               fieldValidator = null;
+        //-- initialize attribute descriptors
+        
+        //-- _key
+        desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_key", "key", org.exolab.castor.xml.NodeType.Attribute);
+        desc.setImmutable(true);
+        handler = new org.exolab.castor.xml.XMLFieldHandler() {
+            public java.lang.Object getValue( java.lang.Object object ) 
+                throws IllegalStateException
+            {
+                OtherData target = (OtherData) object;
+                return target.getKey();
+            }
+            public void setValue( java.lang.Object object, java.lang.Object value) 
+                throws IllegalStateException, IllegalArgumentException
+            {
+                try {
+                    OtherData target = (OtherData) object;
+                    target.setKey( (java.lang.String) value);
+                } catch (java.lang.Exception ex) {
+                    throw new IllegalStateException(ex.toString());
+                }
+            }
+            public java.lang.Object newInstance(java.lang.Object parent) {
+                return null;
+            }
+        };
+        desc.setHandler(handler);
+        desc.setRequired(true);
+        desc.setMultivalued(false);
+        addFieldDescriptor(desc);
+        
+        //-- validation code for: _key
+        fieldValidator = new org.exolab.castor.xml.FieldValidator();
+        fieldValidator.setMinOccurs(1);
+        { //-- local scope
+            org.exolab.castor.xml.validators.StringValidator typeValidator;
+            typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+            fieldValidator.setValidator(typeValidator);
+            typeValidator.setWhiteSpace("preserve");
+        }
+        desc.setValidator(fieldValidator);
+        //-- _key2
+        desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_key2", "key2", org.exolab.castor.xml.NodeType.Attribute);
+        desc.setImmutable(true);
+        handler = new org.exolab.castor.xml.XMLFieldHandler() {
+            public java.lang.Object getValue( java.lang.Object object ) 
+                throws IllegalStateException
+            {
+                OtherData target = (OtherData) object;
+                return target.getKey2();
+            }
+            public void setValue( java.lang.Object object, java.lang.Object value) 
+                throws IllegalStateException, IllegalArgumentException
+            {
+                try {
+                    OtherData target = (OtherData) object;
+                    target.setKey2( (java.lang.String) value);
+                } catch (java.lang.Exception ex) {
+                    throw new IllegalStateException(ex.toString());
+                }
+            }
+            public java.lang.Object newInstance(java.lang.Object parent) {
+                return null;
+            }
+        };
+        desc.setHandler(handler);
+        desc.setMultivalued(false);
+        addFieldDescriptor(desc);
+        
+        //-- validation code for: _key2
+        fieldValidator = new org.exolab.castor.xml.FieldValidator();
+        { //-- local scope
+            org.exolab.castor.xml.validators.StringValidator typeValidator;
+            typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+            fieldValidator.setValidator(typeValidator);
+            typeValidator.setWhiteSpace("preserve");
+        }
+        desc.setValidator(fieldValidator);
+        //-- _value
+        desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_value", "value", org.exolab.castor.xml.NodeType.Attribute);
+        desc.setImmutable(true);
+        handler = new org.exolab.castor.xml.XMLFieldHandler() {
+            public java.lang.Object getValue( java.lang.Object object ) 
+                throws IllegalStateException
+            {
+                OtherData target = (OtherData) object;
+                return target.getValue();
+            }
+            public void setValue( java.lang.Object object, java.lang.Object value) 
+                throws IllegalStateException, IllegalArgumentException
+            {
+                try {
+                    OtherData target = (OtherData) object;
+                    target.setValue( (java.lang.String) value);
+                } catch (java.lang.Exception ex) {
+                    throw new IllegalStateException(ex.toString());
+                }
+            }
+            public java.lang.Object newInstance(java.lang.Object parent) {
+                return null;
+            }
+        };
+        desc.setHandler(handler);
+        desc.setRequired(true);
+        desc.setMultivalued(false);
+        addFieldDescriptor(desc);
+        
+        //-- validation code for: _value
+        fieldValidator = new org.exolab.castor.xml.FieldValidator();
+        fieldValidator.setMinOccurs(1);
+        { //-- local scope
+            org.exolab.castor.xml.validators.StringValidator typeValidator;
+            typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+            fieldValidator.setValidator(typeValidator);
+            typeValidator.setWhiteSpace("preserve");
         }
-      }
+        desc.setValidator(fieldValidator);
+        //-- initialize element descriptors
+        
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
 
-      public java.lang.Object newInstance(java.lang.Object parent)
-      {
+    /**
+     * Method getAccessMode.
+     * 
+     * @return the access mode specified for this class.
+     */
+    public org.exolab.castor.mapping.AccessMode getAccessMode(
+    ) {
         return null;
-      }
-    };
-    desc.setHandler(handler);
-    desc.setRequired(true);
-    desc.setMultivalued(false);
-    addFieldDescriptor(desc);
-
-    // -- validation code for: _key
-    fieldValidator = new org.exolab.castor.xml.FieldValidator();
-    fieldValidator.setMinOccurs(1);
-    { // -- local scope
-      org.exolab.castor.xml.validators.StringValidator typeValidator;
-      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
-      fieldValidator.setValidator(typeValidator);
-      typeValidator.setWhiteSpace("preserve");
     }
-    desc.setValidator(fieldValidator);
-    // -- _value
-    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
-            java.lang.String.class, "_value", "value",
-            org.exolab.castor.xml.NodeType.Attribute);
-    desc.setImmutable(true);
-    handler = new org.exolab.castor.xml.XMLFieldHandler()
-    {
-      public java.lang.Object getValue(java.lang.Object object)
-              throws IllegalStateException
-      {
-        OtherData target = (OtherData) object;
-        return target.getValue();
-      }
-
-      public void setValue(java.lang.Object object, java.lang.Object value)
-              throws IllegalStateException, IllegalArgumentException
-      {
-        try
-        {
-          OtherData target = (OtherData) object;
-          target.setValue((java.lang.String) value);
-        } catch (java.lang.Exception ex)
-        {
-          throw new IllegalStateException(ex.toString());
-        }
-      }
 
-      public java.lang.Object newInstance(java.lang.Object parent)
-      {
-        return null;
-      }
-    };
-    desc.setHandler(handler);
-    desc.setRequired(true);
-    desc.setMultivalued(false);
-    addFieldDescriptor(desc);
-
-    // -- validation code for: _value
-    fieldValidator = new org.exolab.castor.xml.FieldValidator();
-    fieldValidator.setMinOccurs(1);
-    { // -- local scope
-      org.exolab.castor.xml.validators.StringValidator typeValidator;
-      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
-      fieldValidator.setValidator(typeValidator);
-      typeValidator.setWhiteSpace("preserve");
+    /**
+     * Method getIdentity.
+     * 
+     * @return the identity field, null if this class has no
+     * identity.
+     */
+    public org.exolab.castor.mapping.FieldDescriptor getIdentity(
+    ) {
+        return super.getIdentity();
+    }
+
+    /**
+     * Method getJavaClass.
+     * 
+     * @return the Java class represented by this descriptor.
+     */
+    public java.lang.Class getJavaClass(
+    ) {
+        return jalview.schemabinding.version2.OtherData.class;
+    }
+
+    /**
+     * Method getNameSpacePrefix.
+     * 
+     * @return the namespace prefix to use when marshaling as XML.
+     */
+    public java.lang.String getNameSpacePrefix(
+    ) {
+        return _nsPrefix;
+    }
+
+    /**
+     * Method getNameSpaceURI.
+     * 
+     * @return the namespace URI used when marshaling and
+     * unmarshaling as XML.
+     */
+    public java.lang.String getNameSpaceURI(
+    ) {
+        return _nsURI;
+    }
+
+    /**
+     * Method getValidator.
+     * 
+     * @return a specific validator for the class described by this
+     * ClassDescriptor.
+     */
+    public org.exolab.castor.xml.TypeValidator getValidator(
+    ) {
+        return this;
+    }
+
+    /**
+     * Method getXMLName.
+     * 
+     * @return the XML Name for the Class being described.
+     */
+    public java.lang.String getXMLName(
+    ) {
+        return _xmlName;
+    }
+
+    /**
+     * Method isElementDefinition.
+     * 
+     * @return true if XML schema definition of this Class is that
+     * of a global
+     * element or element with anonymous type definition.
+     */
+    public boolean isElementDefinition(
+    ) {
+        return _elementDefinition;
     }
-    desc.setValidator(fieldValidator);
-    // -- initialize element descriptors
-
-  }
-
-  // -----------/
-  // - Methods -/
-  // -----------/
-
-  /**
-   * Method getAccessMode.
-   * 
-   * @return the access mode specified for this class.
-   */
-  public org.exolab.castor.mapping.AccessMode getAccessMode()
-  {
-    return null;
-  }
-
-  /**
-   * Method getIdentity.
-   * 
-   * @return the identity field, null if this class has no identity.
-   */
-  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
-  {
-    return super.getIdentity();
-  }
-
-  /**
-   * Method getJavaClass.
-   * 
-   * @return the Java class represented by this descriptor.
-   */
-  public java.lang.Class getJavaClass()
-  {
-    return jalview.schemabinding.version2.OtherData.class;
-  }
-
-  /**
-   * Method getNameSpacePrefix.
-   * 
-   * @return the namespace prefix to use when marshaling as XML.
-   */
-  public java.lang.String getNameSpacePrefix()
-  {
-    return _nsPrefix;
-  }
-
-  /**
-   * Method getNameSpaceURI.
-   * 
-   * @return the namespace URI used when marshaling and unmarshaling as XML.
-   */
-  public java.lang.String getNameSpaceURI()
-  {
-    return _nsURI;
-  }
-
-  /**
-   * Method getValidator.
-   * 
-   * @return a specific validator for the class described by this
-   *         ClassDescriptor.
-   */
-  public org.exolab.castor.xml.TypeValidator getValidator()
-  {
-    return this;
-  }
-
-  /**
-   * Method getXMLName.
-   * 
-   * @return the XML Name for the Class being described.
-   */
-  public java.lang.String getXMLName()
-  {
-    return _xmlName;
-  }
-
-  /**
-   * Method isElementDefinition.
-   * 
-   * @return true if XML schema definition of this Class is that of a global
-   *         element or element with anonymous type definition.
-   */
-  public boolean isElementDefinition()
-  {
-    return _elementDefinition;
-  }
 
 }
index 4703f46..c816e43 100644 (file)
@@ -18,8 +18,8 @@ import jalview.schemabinding.version2.Setting;
  * 
  * @version $Revision$ $Date$
  */
-public class SettingDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
+public class SettingDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
 {
 
   // --------------------------/
@@ -56,6 +56,9 @@ public class SettingDescriptor extends
     _nsURI = "www.jalview.org";
     _xmlName = "setting";
     _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
     org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
     org.exolab.castor.mapping.FieldHandler handler = null;
     org.exolab.castor.xml.FieldValidator fieldValidator = null;
@@ -331,6 +334,52 @@ public class SettingDescriptor extends
       typeValidator.setMaxInclusive(2147483647);
     }
     desc.setValidator(fieldValidator);
+    // -- _noValueColour
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            "_noValueColour", "noValueColour",
+            org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Setting target = (Setting) object;
+        return target.getNoValueColour();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.setNoValueColour(
+                  (jalview.schemabinding.version2.types.NoValueColour) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            handler);
+    desc.setImmutable(true);
+    desc.setHandler(handler);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _noValueColour
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
     // -- _threshold
     desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
             java.lang.Float.TYPE, "_threshold", "threshold",
@@ -581,8 +630,8 @@ public class SettingDescriptor extends
             target.deleteColourByLabel();
             return;
           }
-          target.setColourByLabel(((java.lang.Boolean) value)
-                  .booleanValue());
+          target.setColourByLabel(
+                  ((java.lang.Boolean) value).booleanValue());
         } catch (java.lang.Exception ex)
         {
           throw new IllegalStateException(ex.toString());
@@ -662,6 +711,109 @@ public class SettingDescriptor extends
     desc.setValidator(fieldValidator);
     // -- initialize element descriptors
 
+    // -- _attributeNameList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_attributeNameList", "attributeName",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Setting target = (Setting) object;
+        return target.getAttributeName();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.addAttributeName((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.removeAllAttributeName();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setNameSpaceURI("www.jalview.org");
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _attributeNameList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- _matcherSet
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.MatcherSet.class, "_matcherSet",
+            "matcherSet", org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Setting target = (Setting) object;
+        return target.getMatcherSet();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.setMatcherSet(
+                  (jalview.schemabinding.version2.MatcherSet) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new jalview.schemabinding.version2.MatcherSet();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setNameSpaceURI("www.jalview.org");
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matcherSet
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
   }
 
   // -----------/
diff --git a/src/jalview/schemabinding/version2/types/.castor.cdr b/src/jalview/schemabinding/version2/types/.castor.cdr
new file mode 100644 (file)
index 0000000..d9874b6
--- /dev/null
@@ -0,0 +1,5 @@
+#Thu Dec 14 15:28:22 GMT 2017
+jalview.schemabinding.version2.types.ColourNoValueColourType=jalview.schemabinding.version2.types.descriptors.ColourNoValueColourTypeDescriptor
+jalview.schemabinding.version2.types.FeatureMatcherByType=jalview.schemabinding.version2.types.descriptors.FeatureMatcherByTypeDescriptor
+jalview.schemabinding.version2.types.NoValueColour=jalview.schemabinding.version2.types.descriptors.NoValueColourDescriptor
+jalview.schemabinding.version2.types.ColourThreshTypeType=jalview.schemabinding.version2.types.descriptors.ColourThreshTypeTypeDescriptor
diff --git a/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java b/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java
new file mode 100644 (file)
index 0000000..0330411
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class ColourThreshTypeType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class ColourThreshTypeType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The NONE type
+     */
+    public static final int NONE_TYPE = 0;
+
+    /**
+     * The instance of the NONE type
+     */
+    public static final ColourThreshTypeType NONE = new ColourThreshTypeType(NONE_TYPE, "NONE");
+
+    /**
+     * The ABOVE type
+     */
+    public static final int ABOVE_TYPE = 1;
+
+    /**
+     * The instance of the ABOVE type
+     */
+    public static final ColourThreshTypeType ABOVE = new ColourThreshTypeType(ABOVE_TYPE, "ABOVE");
+
+    /**
+     * The BELOW type
+     */
+    public static final int BELOW_TYPE = 2;
+
+    /**
+     * The instance of the BELOW type
+     */
+    public static final ColourThreshTypeType BELOW = new ColourThreshTypeType(BELOW_TYPE, "BELOW");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private ColourThreshTypeType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of ColourThreshTypeType
+     * 
+     * @return an Enumeration over all possible instances of
+     * ColourThreshTypeType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this ColourThreshTypeType
+     * 
+     * @return the type of this ColourThreshTypeType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("NONE", NONE);
+        members.put("ABOVE", ABOVE);
+        members.put("BELOW", BELOW);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * ColourThreshTypeType
+     * 
+     * @return the String representation of this ColourThreshTypeTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new ColourThreshTypeType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the ColourThreshTypeType value of parameter 'string'
+     */
+    public static jalview.schemabinding.version2.types.ColourThreshTypeType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid ColourThreshTypeType";
+            throw new IllegalArgumentException(err);
+        }
+        return (ColourThreshTypeType) obj;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java b/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java
new file mode 100644 (file)
index 0000000..6e97332
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class FeatureMatcherByType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherByType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The byLabel type
+     */
+    public static final int BYLABEL_TYPE = 0;
+
+    /**
+     * The instance of the byLabel type
+     */
+    public static final FeatureMatcherByType BYLABEL = new FeatureMatcherByType(BYLABEL_TYPE, "byLabel");
+
+    /**
+     * The byScore type
+     */
+    public static final int BYSCORE_TYPE = 1;
+
+    /**
+     * The instance of the byScore type
+     */
+    public static final FeatureMatcherByType BYSCORE = new FeatureMatcherByType(BYSCORE_TYPE, "byScore");
+
+    /**
+     * The byAttribute type
+     */
+    public static final int BYATTRIBUTE_TYPE = 2;
+
+    /**
+     * The instance of the byAttribute type
+     */
+    public static final FeatureMatcherByType BYATTRIBUTE = new FeatureMatcherByType(BYATTRIBUTE_TYPE, "byAttribute");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private FeatureMatcherByType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of FeatureMatcherByType
+     * 
+     * @return an Enumeration over all possible instances of
+     * FeatureMatcherByType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this FeatureMatcherByType
+     * 
+     * @return the type of this FeatureMatcherByType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("byLabel", BYLABEL);
+        members.put("byScore", BYSCORE);
+        members.put("byAttribute", BYATTRIBUTE);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * FeatureMatcherByType
+     * 
+     * @return the String representation of this FeatureMatcherByTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new FeatureMatcherByType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the FeatureMatcherByType value of parameter 'string'
+     */
+    public static jalview.schemabinding.version2.types.FeatureMatcherByType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid FeatureMatcherByType";
+            throw new IllegalArgumentException(err);
+        }
+        return (FeatureMatcherByType) obj;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/NoValueColour.java b/src/jalview/schemabinding/version2/types/NoValueColour.java
new file mode 100644 (file)
index 0000000..bbef3d7
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Graduated feature colour if no score (or attribute) value
+ * 
+ * @version $Revision$ $Date$
+ */
+public class NoValueColour implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * The None type
+   */
+  public static final int NONE_TYPE = 0;
+
+  /**
+   * The instance of the None type
+   */
+  public static final NoValueColour NONE = new NoValueColour(NONE_TYPE,
+          "None");
+
+  /**
+   * The Min type
+   */
+  public static final int MIN_TYPE = 1;
+
+  /**
+   * The instance of the Min type
+   */
+  public static final NoValueColour MIN = new NoValueColour(MIN_TYPE,
+          "Min");
+
+  /**
+   * The Max type
+   */
+  public static final int MAX_TYPE = 2;
+
+  /**
+   * The instance of the Max type
+   */
+  public static final NoValueColour MAX = new NoValueColour(MAX_TYPE,
+          "Max");
+
+  /**
+   * Field _memberTable.
+   */
+  private static java.util.Hashtable _memberTable = init();
+
+  /**
+   * Field type.
+   */
+  private int type = -1;
+
+  /**
+   * Field stringValue.
+   */
+  private java.lang.String stringValue = null;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  private NoValueColour(final int type, final java.lang.String value)
+  {
+    super();
+    this.type = type;
+    this.stringValue = value;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method enumerate.Returns an enumeration of all possible instances of
+   * NoValueColour
+   * 
+   * @return an Enumeration over all possible instances of NoValueColour
+   */
+  public static java.util.Enumeration enumerate()
+  {
+    return _memberTable.elements();
+  }
+
+  /**
+   * Method getType.Returns the type of this NoValueColour
+   * 
+   * @return the type of this NoValueColour
+   */
+  public int getType()
+  {
+    return this.type;
+  }
+
+  /**
+   * Method init.
+   * 
+   * @return the initialized Hashtable for the member table
+   */
+  private static java.util.Hashtable init()
+  {
+    Hashtable members = new Hashtable();
+    members.put("None", NONE);
+    members.put("Min", MIN);
+    members.put("Max", MAX);
+    return members;
+  }
+
+  /**
+   * Method readResolve. will be called during deserialization to replace the
+   * deserialized object with the correct constant instance.
+   * 
+   * @return this deserialized object
+   */
+  private java.lang.Object readResolve()
+  {
+    return valueOf(this.stringValue);
+  }
+
+  /**
+   * Method toString.Returns the String representation of this NoValueColour
+   * 
+   * @return the String representation of this NoValueColour
+   */
+  public java.lang.String toString()
+  {
+    return this.stringValue;
+  }
+
+  /**
+   * Method valueOf.Returns a new NoValueColour based on the given String value.
+   * 
+   * @param string
+   * @return the NoValueColour value of parameter 'string'
+   */
+  public static jalview.schemabinding.version2.types.NoValueColour valueOf(
+          final java.lang.String string)
+  {
+    java.lang.Object obj = null;
+    if (string != null)
+    {
+      obj = _memberTable.get(string);
+    }
+    if (obj == null)
+    {
+      String err = "" + string + " is not a valid NoValueColour";
+      throw new IllegalArgumentException(err);
+    }
+    return (NoValueColour) obj;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java
new file mode 100644 (file)
index 0000000..f978363
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types.descriptors;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.types.ColourThreshTypeType;
+
+/**
+ * Class ColourThreshTypeTypeDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class ColourThreshTypeTypeDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _elementDefinition.
+     */
+    private boolean _elementDefinition;
+
+    /**
+     * Field _nsPrefix.
+     */
+    private java.lang.String _nsPrefix;
+
+    /**
+     * Field _nsURI.
+     */
+    private java.lang.String _nsURI;
+
+    /**
+     * Field _xmlName.
+     */
+    private java.lang.String _xmlName;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public ColourThreshTypeTypeDescriptor() {
+        super();
+        _nsURI = "www.jalview.org/colours";
+        _xmlName = "ColourThreshTypeType";
+        _elementDefinition = false;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method getAccessMode.
+     * 
+     * @return the access mode specified for this class.
+     */
+    public org.exolab.castor.mapping.AccessMode getAccessMode(
+    ) {
+        return null;
+    }
+
+    /**
+     * Method getIdentity.
+     * 
+     * @return the identity field, null if this class has no
+     * identity.
+     */
+    public org.exolab.castor.mapping.FieldDescriptor getIdentity(
+    ) {
+        return super.getIdentity();
+    }
+
+    /**
+     * Method getJavaClass.
+     * 
+     * @return the Java class represented by this descriptor.
+     */
+    public java.lang.Class getJavaClass(
+    ) {
+        return jalview.schemabinding.version2.types.ColourThreshTypeType.class;
+    }
+
+    /**
+     * Method getNameSpacePrefix.
+     * 
+     * @return the namespace prefix to use when marshaling as XML.
+     */
+    public java.lang.String getNameSpacePrefix(
+    ) {
+        return _nsPrefix;
+    }
+
+    /**
+     * Method getNameSpaceURI.
+     * 
+     * @return the namespace URI used when marshaling and
+     * unmarshaling as XML.
+     */
+    public java.lang.String getNameSpaceURI(
+    ) {
+        return _nsURI;
+    }
+
+    /**
+     * Method getValidator.
+     * 
+     * @return a specific validator for the class described by this
+     * ClassDescriptor.
+     */
+    public org.exolab.castor.xml.TypeValidator getValidator(
+    ) {
+        return this;
+    }
+
+    /**
+     * Method getXMLName.
+     * 
+     * @return the XML Name for the Class being described.
+     */
+    public java.lang.String getXMLName(
+    ) {
+        return _xmlName;
+    }
+
+    /**
+     * Method isElementDefinition.
+     * 
+     * @return true if XML schema definition of this Class is that
+     * of a global
+     * element or element with anonymous type definition.
+     */
+    public boolean isElementDefinition(
+    ) {
+        return _elementDefinition;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java
new file mode 100644 (file)
index 0000000..e392e76
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types.descriptors;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.types.FeatureMatcherByType;
+
+/**
+ * Class FeatureMatcherByTypeDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherByTypeDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _elementDefinition.
+     */
+    private boolean _elementDefinition;
+
+    /**
+     * Field _nsPrefix.
+     */
+    private java.lang.String _nsPrefix;
+
+    /**
+     * Field _nsURI.
+     */
+    private java.lang.String _nsURI;
+
+    /**
+     * Field _xmlName.
+     */
+    private java.lang.String _xmlName;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public FeatureMatcherByTypeDescriptor() {
+        super();
+        _nsURI = "www.jalview.org/colours";
+        _xmlName = "FeatureMatcherByType";
+        _elementDefinition = false;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method getAccessMode.
+     * 
+     * @return the access mode specified for this class.
+     */
+    public org.exolab.castor.mapping.AccessMode getAccessMode(
+    ) {
+        return null;
+    }
+
+    /**
+     * Method getIdentity.
+     * 
+     * @return the identity field, null if this class has no
+     * identity.
+     */
+    public org.exolab.castor.mapping.FieldDescriptor getIdentity(
+    ) {
+        return super.getIdentity();
+    }
+
+    /**
+     * Method getJavaClass.
+     * 
+     * @return the Java class represented by this descriptor.
+     */
+    public java.lang.Class getJavaClass(
+    ) {
+        return jalview.schemabinding.version2.types.FeatureMatcherByType.class;
+    }
+
+    /**
+     * Method getNameSpacePrefix.
+     * 
+     * @return the namespace prefix to use when marshaling as XML.
+     */
+    public java.lang.String getNameSpacePrefix(
+    ) {
+        return _nsPrefix;
+    }
+
+    /**
+     * Method getNameSpaceURI.
+     * 
+     * @return the namespace URI used when marshaling and
+     * unmarshaling as XML.
+     */
+    public java.lang.String getNameSpaceURI(
+    ) {
+        return _nsURI;
+    }
+
+    /**
+     * Method getValidator.
+     * 
+     * @return a specific validator for the class described by this
+     * ClassDescriptor.
+     */
+    public org.exolab.castor.xml.TypeValidator getValidator(
+    ) {
+        return this;
+    }
+
+    /**
+     * Method getXMLName.
+     * 
+     * @return the XML Name for the Class being described.
+     */
+    public java.lang.String getXMLName(
+    ) {
+        return _xmlName;
+    }
+
+    /**
+     * Method isElementDefinition.
+     * 
+     * @return true if XML schema definition of this Class is that
+     * of a global
+     * element or element with anonymous type definition.
+     */
+    public boolean isElementDefinition(
+    ) {
+        return _elementDefinition;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java
new file mode 100644 (file)
index 0000000..14c58ed
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.types.NoValueColour;
+
+/**
+ * Class NoValueColourDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class NoValueColourDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public NoValueColourDescriptor()
+  {
+    super();
+    _nsURI = "www.jalview.org/colours";
+    _xmlName = "NoValueColour";
+    _elementDefinition = false;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.types.NoValueColour.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
index 54d1c6c..7d14662 100644 (file)
@@ -22,6 +22,7 @@ package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureMatcher;
 import jalview.util.ColorUtils;
 import jalview.util.Format;
 
@@ -29,10 +30,49 @@ import java.awt.Color;
 import java.util.StringTokenizer;
 
 /**
- * A class that wraps either a simple colour or a graduated colour
+ * A class that represents a colour scheme for a feature type. Options supported
+ * are currently
+ * <ul>
+ * <li>a simple colour e.g. Red</li>
+ * <li>colour by label - a colour is generated from the feature description</li>
+ * <li>graduated colour by feature score</li>
+ * <ul>
+ * <li>minimum and maximum score range must be provided</li>
+ * <li>minimum and maximum value colours should be specified</li>
+ * <li>a colour for 'no value' may optionally be provided</li>
+ * <li>colours for intermediate scores are interpolated RGB values</li>
+ * <li>there is an optional threshold above/below which to colour values</li>
+ * <li>the range may be the full value range, or may be limited by the threshold
+ * value</li>
+ * </ul>
+ * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
+ * (numeric) value of a named attribute</li> </ul>
  */
 public class FeatureColour implements FeatureColourI
 {
+  private static final String ABSOLUTE = "abso";
+
+  private static final String ABOVE = "above";
+
+  private static final String BELOW = "below";
+
+  /*
+   * constants used to read or write a Jalview Features file
+   */
+  private static final String LABEL = "label";
+
+  private static final String SCORE = "score";
+
+  private static final String ATTRIBUTE = "attribute";
+
+  private static final String NO_VALUE_MIN = "noValueMin";
+
+  private static final String NO_VALUE_MAX = "noValueMax";
+
+  private static final String NO_VALUE_NONE = "noValueNone";
+
+  static final Color DEFAULT_NO_COLOUR = null;
+
   private static final String BAR = "|";
 
   final private Color colour;
@@ -41,10 +81,30 @@ public class FeatureColour implements FeatureColourI
 
   final private Color maxColour;
 
+  /*
+   * colour to use for colour by attribute when the 
+   * attribute value is absent
+   */
+  final private Color noColour;
+
+  /*
+   * if true, then colour has a gradient based on a numerical 
+   * range (either feature score, or an attribute value)
+   */
   private boolean graduatedColour;
 
+  /*
+   * if true, colour values are generated from a text string,
+   * either feature description, or an attribute value
+   */
   private boolean colourByLabel;
 
+  /*
+   * if not null, the value of [attribute, [sub-attribute] ...]
+   *  is used for colourByLabel or graduatedColour
+   */
+  private String[] attributeName;
+
   private float threshold;
 
   private float base;
@@ -55,8 +115,6 @@ public class FeatureColour implements FeatureColourI
 
   private boolean aboveThreshold;
 
-  private boolean thresholdIsMinOrMax;
-
   private boolean isHighToLow;
 
   private boolean autoScaled;
@@ -75,16 +133,29 @@ public class FeatureColour implements FeatureColourI
 
   /**
    * Parses a Jalview features file format colour descriptor
-   * [label|][mincolour|maxcolour
-   * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
+   * <p>
+   * <code>
+   * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
+   * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
+   * <p>
+   * 'Score' is optional (default) for a graduated colour. An attribute with
+   * sub-attribute should be written as (for example) CSQ:Consequence.
+   * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code>
+   * with default noValueMin.
+   * <p>
+   * Examples:
    * <ul>
    * <li>red</li>
    * <li>a28bbb</li>
    * <li>25,125,213</li>
    * <li>label</li>
+   * <li>attribute|CSQ:PolyPhen</li>
    * <li>label|||0.0|0.0|above|12.5</li>
    * <li>label|||0.0|0.0|below|12.5</li>
    * <li>red|green|12.0|26.0|none</li>
+   * <li>score|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li>
    * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
    * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
    * </ul>
@@ -94,34 +165,71 @@ public class FeatureColour implements FeatureColourI
    * @throws IllegalArgumentException
    *           if not parseable
    */
-  public static FeatureColour parseJalviewFeatureColour(String descriptor)
+  public static FeatureColourI parseJalviewFeatureColour(String descriptor)
   {
-    StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
+    StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
     float min = Float.MIN_VALUE;
     float max = Float.MAX_VALUE;
-    boolean labelColour = false;
+    boolean byLabel = false;
+    boolean byAttribute = false;
+    String attName = null;
+    String mincol = null;
+    String maxcol = null;
 
-    String mincol = gcol.nextToken();
-    if (mincol == "|")
+    /*
+     * first token should be 'label', or 'score', or an
+     * attribute name, or simple colour, or minimum colour
+     */
+    String nextToken = gcol.nextToken();
+    if (nextToken == BAR)
     {
       throw new IllegalArgumentException(
               "Expected either 'label' or a colour specification in the line: "
                       + descriptor);
     }
-    String maxcol = null;
-    if (mincol.toLowerCase().indexOf("label") == 0)
+    if (nextToken.toLowerCase().startsWith(LABEL))
+    {
+      byLabel = true;
+      // get the token after the next delimiter:
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+    else if (nextToken.toLowerCase().startsWith(SCORE))
+    {
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+    else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
     {
-      labelColour = true;
+      byAttribute = true;
+      attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
-      // skip '|'
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
     }
+    else
+    {
+      mincol = nextToken;
+    }
 
-    if (!labelColour && !gcol.hasMoreTokens())
+    /*
+     * if only one token, it can validly be label, attributeName,
+     * or a plain colour value
+     */
+    if (!gcol.hasMoreTokens())
     {
-      /*
-       * only a simple colour specification - parse it
-       */
+      if (byLabel || byAttribute)
+      {
+        FeatureColourI fc = new FeatureColour();
+        fc.setColourByLabel(true);
+        if (byAttribute)
+        {
+          fc.setAttributeName(
+                  FeatureMatcher.fromAttributeDisplayName(attName));
+        }
+        return fc;
+      }
+
       Color colour = ColorUtils.parseColourString(descriptor);
       if (colour == null)
       {
@@ -132,34 +240,64 @@ public class FeatureColour implements FeatureColourI
     }
 
     /*
+     * continue parsing for min/max/no colour (if graduated)
+     * and for threshold (colour by text or graduated)
+     */
+
+    /*
      * autoScaled == true: colours range over actual score range
      * autoScaled == false ('abso'): colours range over min/max range
      */
     boolean autoScaled = true;
     String tok = null, minval, maxval;
+    String noValueColour = NO_VALUE_MIN;
+
     if (mincol != null)
     {
       // at least four more tokens
-      if (mincol.equals("|"))
+      if (mincol.equals(BAR))
       {
-        mincol = "";
+        mincol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       maxcol = gcol.nextToken();
-      if (maxcol.equals("|"))
+      if (maxcol.equals(BAR))
       {
-        maxcol = "";
+        maxcol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       tok = gcol.nextToken();
+
+      /*
+       * check for specifier for colour for no attribute value
+       * (new in 2.11, defaults to minColour if not specified)
+       */
+      if (tok.equalsIgnoreCase(NO_VALUE_MIN))
+      {
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+      else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
+      {
+        noValueColour = NO_VALUE_MAX;
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+      else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
+      {
+        noValueColour = NO_VALUE_NONE;
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+
       gcol.nextToken(); // skip next '|'
-      if (tok.toLowerCase().startsWith("abso"))
+      if (tok.toLowerCase().startsWith(ABSOLUTE))
       {
         minval = gcol.nextToken();
         gcol.nextToken(); // skip next '|'
@@ -201,34 +339,45 @@ public class FeatureColour implements FeatureColourI
     }
     else
     {
-      // add in some dummy min/max colours for the label-only
-      // colourscheme.
-      mincol = "FFFFFF";
-      maxcol = "000000";
+      /*
+       * dummy min/max colours for colour by text
+       * (label or attribute value)
+       */
+      mincol = "white";
+      maxcol = "black";
+      byLabel = true;
     }
 
     /*
-     * construct the FeatureColour
+     * construct the FeatureColour!
      */
     FeatureColour featureColour;
     try
     {
       Color minColour = ColorUtils.parseColourString(mincol);
       Color maxColour = ColorUtils.parseColourString(maxcol);
-      featureColour = new FeatureColour(minColour, maxColour, min, max);
-      featureColour.setColourByLabel(labelColour);
+      Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour
+              : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour);
+      featureColour = new FeatureColour(minColour, maxColour, noColour, min,
+              max);
+      featureColour.setColourByLabel(minColour == null);
       featureColour.setAutoScaled(autoScaled);
+      if (byAttribute)
+      {
+        featureColour.setAttributeName(
+                FeatureMatcher.fromAttributeDisplayName(attName));
+      }
       // add in any additional parameters
       String ttype = null, tval = null;
       if (gcol.hasMoreTokens())
       {
         // threshold type and possibly a threshold value
         ttype = gcol.nextToken();
-        if (ttype.toLowerCase().startsWith("below"))
+        if (ttype.toLowerCase().startsWith(BELOW))
         {
           featureColour.setBelowThreshold(true);
         }
-        else if (ttype.toLowerCase().startsWith("above"))
+        else if (ttype.toLowerCase().startsWith(ABOVE))
         {
           featureColour.setAboveThreshold(true);
         }
@@ -260,7 +409,7 @@ public class FeatureColour implements FeatureColourI
                 "Ignoring additional tokens in parameters in graduated colour specification\n");
         while (gcol.hasMoreTokens())
         {
-          System.err.println("|" + gcol.nextToken());
+          System.err.println(BAR + gcol.nextToken());
         }
         System.err.println("\n");
       }
@@ -288,6 +437,7 @@ public class FeatureColour implements FeatureColourI
   {
     minColour = Color.WHITE;
     maxColour = Color.BLACK;
+    noColour = DEFAULT_NO_COLOUR;
     minRed = 0f;
     minGreen = 0f;
     minBlue = 0f;
@@ -298,7 +448,8 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Constructor given a colour range and a score range
+   * Constructor given a colour range and a score range, defaulting 'no value
+   * colour' to be the same as minimum colour
    * 
    * @param low
    * @param high
@@ -307,36 +458,7 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(Color low, Color high, float min, float max)
   {
-    if (low == null)
-    {
-      low = Color.white;
-    }
-    if (high == null)
-    {
-      high = Color.black;
-    }
-    graduatedColour = true;
-    colour = null;
-    minColour = low;
-    maxColour = high;
-    threshold = Float.NaN;
-    isHighToLow = min >= max;
-    minRed = low.getRed() / 255f;
-    minGreen = low.getGreen() / 255f;
-    minBlue = low.getBlue() / 255f;
-    deltaRed = (high.getRed() / 255f) - minRed;
-    deltaGreen = (high.getGreen() / 255f) - minGreen;
-    deltaBlue = (high.getBlue() / 255f) - minBlue;
-    if (isHighToLow)
-    {
-      base = max;
-      range = min - max;
-    }
-    else
-    {
-      base = min;
-      range = max - min;
-    }
+    this(low, high, low, min, max);
   }
 
   /**
@@ -350,6 +472,7 @@ public class FeatureColour implements FeatureColourI
     colour = fc.colour;
     minColour = fc.minColour;
     maxColour = fc.maxColour;
+    noColour = fc.noColour;
     minRed = fc.minRed;
     minGreen = fc.minGreen;
     minBlue = fc.minBlue;
@@ -359,6 +482,7 @@ public class FeatureColour implements FeatureColourI
     base = fc.base;
     range = fc.range;
     isHighToLow = fc.isHighToLow;
+    attributeName = fc.attributeName;
     setAboveThreshold(fc.isAboveThreshold());
     setBelowThreshold(fc.isBelowThreshold());
     setThreshold(fc.getThreshold());
@@ -376,10 +500,54 @@ public class FeatureColour implements FeatureColourI
   public FeatureColour(FeatureColour fc, float min, float max)
   {
     this(fc);
-    graduatedColour = true;
     updateBounds(min, max);
   }
 
+  /**
+   * Constructor for a graduated colour
+   * 
+   * @param low
+   * @param high
+   * @param noValueColour
+   * @param min
+   * @param max
+   */
+  public FeatureColour(Color low, Color high, Color noValueColour,
+          float min, float max)
+  {
+    if (low == null)
+    {
+      low = Color.white;
+    }
+    if (high == null)
+    {
+      high = Color.black;
+    }
+    graduatedColour = true;
+    colour = null;
+    minColour = low;
+    maxColour = high;
+    noColour = noValueColour;
+    threshold = Float.NaN;
+    isHighToLow = min >= max;
+    minRed = low.getRed() / 255f;
+    minGreen = low.getGreen() / 255f;
+    minBlue = low.getBlue() / 255f;
+    deltaRed = (high.getRed() / 255f) - minRed;
+    deltaGreen = (high.getGreen() / 255f) - minGreen;
+    deltaBlue = (high.getBlue() / 255f) - minBlue;
+    if (isHighToLow)
+    {
+      base = max;
+      range = min - max;
+    }
+    else
+    {
+      base = min;
+      range = max - min;
+    }
+  }
+
   @Override
   public boolean isGraduatedColour()
   {
@@ -418,6 +586,12 @@ public class FeatureColour implements FeatureColourI
   }
 
   @Override
+  public Color getNoColour()
+  {
+    return noColour;
+  }
+
+  @Override
   public boolean isColourByLabel()
   {
     return colourByLabel;
@@ -470,18 +644,6 @@ public class FeatureColour implements FeatureColourI
   }
 
   @Override
-  public boolean isThresholdMinMax()
-  {
-    return thresholdIsMinOrMax;
-  }
-
-  @Override
-  public void setThresholdMinMax(boolean b)
-  {
-    thresholdIsMinOrMax = b;
-  }
-
-  @Override
   public float getThreshold()
   {
     return threshold;
@@ -506,10 +668,7 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Updates the base and range appropriately for the given minmax range
-   * 
-   * @param min
-   * @param max
+   * {@inheritDoc}
    */
   @Override
   public void updateBounds(float min, float max)
@@ -542,7 +701,10 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      return ColorUtils.createColourFromName(feature.getDescription());
+      String label = attributeName == null ? feature.getDescription()
+              : feature.getValueAsString(attributeName);
+      return label == null ? noColour : ColorUtils
+              .createColourFromName(label);
     }
 
     if (!isGraduatedColour())
@@ -552,17 +714,31 @@ public class FeatureColour implements FeatureColourI
 
     /*
      * graduated colour case, optionally with threshold
-     * Float.NaN is assigned minimum visible score colour
+     * may be based on feature score on an attribute value
+     * Float.NaN, or no value, is assigned the 'no value' colour
      */
     float scr = feature.getScore();
+    if (attributeName != null)
+    {
+      try
+      {
+        String attVal = feature.getValueAsString(attributeName);
+        scr = Float.valueOf(attVal);
+      } catch (Throwable e)
+      {
+        scr = Float.NaN;
+      }
+    }
     if (Float.isNaN(scr))
     {
-      return getMinColour();
+      return noColour;
     }
+
     if (isAboveThreshold() && scr <= threshold)
     {
       return null;
     }
+
     if (isBelowThreshold() && scr >= threshold)
     {
       return null;
@@ -635,21 +811,43 @@ public class FeatureColour implements FeatureColourI
     else
     {
       StringBuilder sb = new StringBuilder(32);
-      if (isColourByLabel())
+      if (isColourByAttribute())
       {
-        sb.append("label");
-        if (hasThreshold())
-        {
-          sb.append(BAR).append(BAR).append(BAR);
-        }
+        sb.append(ATTRIBUTE).append(BAR);
+        sb.append(
+                FeatureMatcher.toAttributeDisplayName(getAttributeName()));
+      }
+      else if (isColourByLabel())
+      {
+        sb.append(LABEL);
+      }
+      else
+      {
+        sb.append(SCORE);
       }
       if (isGraduatedColour())
       {
-        sb.append(Format.getHexString(getMinColour())).append(BAR);
+        sb.append(BAR).append(Format.getHexString(getMinColour()))
+                .append(BAR);
         sb.append(Format.getHexString(getMaxColour())).append(BAR);
+        String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
+                : (maxColour.equals(noColour) ? NO_VALUE_MAX
+                        : NO_VALUE_NONE);
+        sb.append(noValue).append(BAR);
         if (!isAutoScaled())
         {
-          sb.append("abso").append(BAR);
+          sb.append(ABSOLUTE).append(BAR);
+        }
+      }
+      else
+      {
+        /*
+         * colour by text with score threshold: empty fields for
+         * minColour and maxColour (not used)
+         */
+        if (hasThreshold())
+        {
+          sb.append(BAR).append(BAR).append(BAR);
         }
       }
       if (hasThreshold() || isGraduatedColour())
@@ -658,11 +856,11 @@ public class FeatureColour implements FeatureColourI
         sb.append(getMax()).append(BAR);
         if (isBelowThreshold())
         {
-          sb.append("below").append(BAR).append(getThreshold());
+          sb.append(BELOW).append(BAR).append(getThreshold());
         }
         else if (isAboveThreshold())
         {
-          sb.append("above").append(BAR).append(getThreshold());
+          sb.append(ABOVE).append(BAR).append(getThreshold());
         }
         else
         {
@@ -674,4 +872,22 @@ public class FeatureColour implements FeatureColourI
     return String.format("%s\t%s", featureType, colourString);
   }
 
+  @Override
+  public boolean isColourByAttribute()
+  {
+    return attributeName != null;
+  }
+
+  @Override
+  public String[] getAttributeName()
+  {
+    return attributeName;
+  }
+
+  @Override
+  public void setAttributeName(String... name)
+  {
+    attributeName = name;
+  }
+
 }
index 55df1d1..a4e6480 100755 (executable)
@@ -39,14 +39,14 @@ public class ResidueProperties
 
   public static final int[] purinepyrimidineIndex;
 
-  public static final Map<String, Integer> aa3Hash = new HashMap<String, Integer>();
+  public static final Map<String, Integer> aa3Hash = new HashMap<>();
 
-  public static final Map<String, String> aa2Triplet = new HashMap<String, String>();
+  public static final Map<String, String> aa2Triplet = new HashMap<>();
 
-  public static final Map<String, String> nucleotideName = new HashMap<String, String>();
+  public static final Map<String, String> nucleotideName = new HashMap<>();
 
   // lookup from modified amino acid (e.g. MSE) to canonical form (e.g. MET)
-  public static final Map<String, String> modifications = new HashMap<String, String>();
+  public static final Map<String, String> modifications = new HashMap<>();
 
   static
   {
@@ -496,25 +496,27 @@ public class ResidueProperties
    * Color.white, // R Color.white, // Y Color.white, // N Color.white, // Gap
    */
 
-  public static List<String> STOP = Arrays.asList("TGA", "TAA", "TAG");
+  public static String STOP = "STOP";
+
+  public static List<String> STOP_CODONS = Arrays.asList("TGA", "TAA", "TAG");
 
   public static String START = "ATG";
 
   /**
    * Nucleotide Ambiguity Codes
    */
-  public static final Map<String, String[]> ambiguityCodes = new Hashtable<String, String[]>();
+  public static final Map<String, String[]> ambiguityCodes = new Hashtable<>();
 
   /**
    * Codon triplets with additional symbols for unambiguous codons that include
    * ambiguity codes
    */
-  public static final Hashtable<String, String> codonHash2 = new Hashtable<String, String>();
+  public static final Hashtable<String, String> codonHash2 = new Hashtable<>();
 
   /**
    * all ambiguity codes for a given base
    */
-  public final static Hashtable<String, List<String>> _ambiguityCodes = new Hashtable<String, List<String>>();
+  public final static Hashtable<String, List<String>> _ambiguityCodes = new Hashtable<>();
 
   static
   {
@@ -638,7 +640,7 @@ public class ResidueProperties
         List<String> codesfor = _ambiguityCodes.get(r);
         if (codesfor == null)
         {
-          _ambiguityCodes.put(r, codesfor = new ArrayList<String>());
+          _ambiguityCodes.put(r, codesfor = new ArrayList<>());
         }
         if (!codesfor.contains(acode.getKey()))
         {
@@ -755,27 +757,27 @@ public class ResidueProperties
   }
 
   // Stores residue codes/names and colours and other things
-  public static Map<String, Map<String, Integer>> propHash = new Hashtable<String, Map<String, Integer>>();
+  public static Map<String, Map<String, Integer>> propHash = new Hashtable<>();
 
-  public static Map<String, Integer> hydrophobic = new Hashtable<String, Integer>();
+  public static Map<String, Integer> hydrophobic = new Hashtable<>();
 
-  public static Map<String, Integer> polar = new Hashtable<String, Integer>();
+  public static Map<String, Integer> polar = new Hashtable<>();
 
-  public static Map<String, Integer> small = new Hashtable<String, Integer>();
+  public static Map<String, Integer> small = new Hashtable<>();
 
-  public static Map<String, Integer> positive = new Hashtable<String, Integer>();
+  public static Map<String, Integer> positive = new Hashtable<>();
 
-  public static Map<String, Integer> negative = new Hashtable<String, Integer>();
+  public static Map<String, Integer> negative = new Hashtable<>();
 
-  public static Map<String, Integer> charged = new Hashtable<String, Integer>();
+  public static Map<String, Integer> charged = new Hashtable<>();
 
-  public static Map<String, Integer> aromatic = new Hashtable<String, Integer>();
+  public static Map<String, Integer> aromatic = new Hashtable<>();
 
-  public static Map<String, Integer> aliphatic = new Hashtable<String, Integer>();
+  public static Map<String, Integer> aliphatic = new Hashtable<>();
 
-  public static Map<String, Integer> tiny = new Hashtable<String, Integer>();
+  public static Map<String, Integer> tiny = new Hashtable<>();
 
-  public static Map<String, Integer> proline = new Hashtable<String, Integer>();
+  public static Map<String, Integer> proline = new Hashtable<>();
 
   static
   {
@@ -1149,7 +1151,7 @@ public class ResidueProperties
     String cdn = codonHash2.get(lccodon.toUpperCase());
     if ("*".equals(cdn))
     {
-      return "STOP";
+      return STOP;
     }
     return cdn;
   }
@@ -1157,7 +1159,7 @@ public class ResidueProperties
   public static Hashtable<String, String> toDssp3State;
   static
   {
-    toDssp3State = new Hashtable<String, String>();
+    toDssp3State = new Hashtable<>();
     toDssp3State.put("H", "H");
     toDssp3State.put("E", "E");
     toDssp3State.put("C", " ");
@@ -2525,7 +2527,7 @@ public class ResidueProperties
   // / cut here
   public static void main(String[] args)
   {
-    Hashtable<String, Vector<String>> aaProps = new Hashtable<String, Vector<String>>();
+    Hashtable<String, Vector<String>> aaProps = new Hashtable<>();
     System.out.println("my %aa = {");
     // invert property hashes
     for (String pname : propHash.keySet())
@@ -2536,7 +2538,7 @@ public class ResidueProperties
         Vector<String> aprops = aaProps.get(rname);
         if (aprops == null)
         {
-          aprops = new Vector<String>();
+          aprops = new Vector<>();
           aaProps.put(rname, aprops);
         }
         Integer hasprop = phash.get(rname);
@@ -2578,7 +2580,7 @@ public class ResidueProperties
   public static List<String> getResidues(boolean forNucleotide,
           boolean includeAmbiguous)
   {
-    List<String> result = new ArrayList<String>();
+    List<String> result = new ArrayList<>();
     if (forNucleotide)
     {
       for (String nuc : nucleotideName.keySet())
index d4be322..60129fb 100644 (file)
 package jalview.util;
 
 import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Random;
 
 public class ColorUtils
 {
+  private static final int MAX_CACHE_SIZE = 1729;
+  /*
+   * a cache for colours generated from text strings
+   */
+  static Map<String, Color> myColours = new HashMap<>();
 
   /**
    * Generates a random color, will mix with input color. Code taken from
@@ -260,6 +267,10 @@ public class ColorUtils
     {
       return Color.white;
     }
+    if (myColours.containsKey(name))
+    {
+      return myColours.get(name);
+    }
     int lsize = name.length();
     int start = 0;
     int end = lsize / 3;
@@ -291,6 +302,11 @@ public class ColorUtils
 
     Color color = new Color(r, g, b);
 
+    if (myColours.size() < MAX_CACHE_SIZE)
+    {
+      myColours.put(name, color);
+    }
+
     return color;
   }
 
index ae530f9..9f28ee1 100644 (file)
@@ -327,6 +327,13 @@ public class MapList
     fromHighest = Integer.MIN_VALUE;
     for (int[] range : fromRange)
     {
+      if (range.length != 2)
+      {
+        // throw new IllegalArgumentException(range);
+        System.err.println(
+                "Invalid format for fromRange " + Arrays.toString(range)
+                + " may cause errors");
+      }
       fromLowest = Math.min(fromLowest, Math.min(range[0], range[1]));
       fromHighest = Math.max(fromHighest, Math.max(range[0], range[1]));
     }
@@ -335,6 +342,13 @@ public class MapList
     toHighest = Integer.MIN_VALUE;
     for (int[] range : toRange)
     {
+      if (range.length != 2)
+      {
+        // throw new IllegalArgumentException(range);
+        System.err.println("Invalid format for toRange "
+                + Arrays.toString(range)
+                + " may cause errors");
+      }
       toLowest = Math.min(toLowest, Math.min(range[0], range[1]));
       toHighest = Math.max(toHighest, Math.max(range[0], range[1]));
     }
@@ -1103,8 +1117,33 @@ public class MapList
    */
   public boolean isFromForwardStrand()
   {
+    return isForwardStrand(getFromRanges());
+  }
+
+  /**
+   * Returns true if mapping is to forward strand, false if to reverse strand.
+   * Result is just based on the first 'to' range that is not a single position.
+   * Default is true unless proven to be false. Behaviour is not well defined if
+   * the mapping has a mixture of forward and reverse ranges.
+   * 
+   * @return
+   */
+  public boolean isToForwardStrand()
+  {
+    return isForwardStrand(getToRanges());
+  }
+
+  /**
+   * A helper method that returns true unless at least one range has start > end.
+   * Behaviour is undefined for a mixture of forward and reverse ranges.
+   * 
+   * @param ranges
+   * @return
+   */
+  private boolean isForwardStrand(List<int[]> ranges)
+  {
     boolean forwardStrand = true;
-    for (int[] range : getFromRanges())
+    for (int[] range : ranges)
     {
       if (range[1] > range[0])
       {
@@ -1129,4 +1168,72 @@ public class MapList
             || (fromRatio == 3 && toRatio == 1);
   }
 
+  /**
+   * Returns a map which is the composite of this one and the input map. That
+   * is, the output map has the fromRanges of this map, and its toRanges are the
+   * toRanges of this map as transformed by the input map.
+   * <p>
+   * Returns null if the mappings cannot be traversed (not all toRanges of this
+   * map correspond to fromRanges of the input), or if this.toRatio does not
+   * match map.fromRatio.
+   * 
+   * <pre>
+   * Example 1:
+   *    this:   from [1-100] to [501-600]
+   *    input:  from [10-40] to [60-90]
+   *    output: from [10-40] to [560-590]
+   * Example 2 ('reverse strand exons'):
+   *    this:   from [1-100] to [2000-1951], [1000-951] // transcript to loci
+   *    input:  from [1-50]  to [41-90] // CDS to transcript
+   *    output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
+   * </pre>
+   * 
+   * @param map
+   * @return
+   */
+  public MapList traverse(MapList map)
+  {
+    if (map == null)
+    {
+      return null;
+    }
+
+    /*
+     * compound the ratios by this rule:
+     * A:B with M:N gives A*M:B*N
+     * reduced by greatest common divisor
+     * so 1:3 with 3:3 is 3:9 or 1:3
+     * 1:3 with 3:1 is 3:3 or 1:1
+     * 1:3 with 1:3 is 1:9
+     * 2:5 with 3:7 is 6:35
+     */
+    int outFromRatio = getFromRatio() * map.getFromRatio();
+    int outToRatio = getToRatio() * map.getToRatio();
+    int gcd = MathUtils.gcd(outFromRatio, outToRatio);
+    outFromRatio /= gcd;
+    outToRatio /= gcd;
+
+    List<int[]> toRanges = new ArrayList<>();
+    for (int[] range : getToRanges())
+    {
+      int[] transferred = map.locateInTo(range[0], range[1]);
+      if (transferred == null || transferred.length % 2 != 0)
+      {
+        return null;
+      }
+
+      /*
+       *  convert [start1, end1, start2, end2, ...] 
+       *  to [[start1, end1], [start2, end2], ...]
+       */
+      for (int i = 0; i < transferred.length;)
+      {
+        toRanges.add(new int[] { transferred[i], transferred[i + 1] });
+        i += 2;
+      }
+    }
+
+    return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
+  }
+
 }
index 5a26ed6..b552c21 100644 (file)
@@ -943,6 +943,34 @@ public final class MappingUtils
   }
 
   /**
+   * Answers true if range's start-end positions include those of queryRange,
+   * where either range might be in reverse direction, else false
+   * 
+   * @param range
+   *          a start-end range
+   * @param queryRange
+   *          a candidate subrange of range (start2-end2)
+   * @return
+   */
+  public static boolean rangeContains(int[] range, int[] queryRange)
+  {
+    if (range == null || queryRange == null || range.length != 2
+            || queryRange.length != 2)
+    {
+      /*
+       * invalid arguments
+       */
+      return false;
+    }
+
+    int min = Math.min(range[0], range[1]);
+    int max = Math.max(range[0], range[1]);
+  
+    return (min <= queryRange[0] && max >= queryRange[0]
+            && min <= queryRange[1] && max >= queryRange[1]);
+  }
+
+  /**
    * Removes the specified number of positions from the given ranges. Provided
    * to allow a stop codon to be stripped from a CDS sequence so that it matches
    * the peptide translation length.
diff --git a/src/jalview/util/MathUtils.java b/src/jalview/util/MathUtils.java
new file mode 100644 (file)
index 0000000..72d46a2
--- /dev/null
@@ -0,0 +1,22 @@
+package jalview.util;
+
+public class MathUtils
+{
+
+  /**
+   * Returns the greatest common divisor of two integers
+   * 
+   * @param a
+   * @param b
+   * @return
+   */
+  public static int gcd(int a, int b)
+  {
+    if (b == 0)
+    {
+      return Math.abs(a);
+    }
+    return gcd(b, a % b);
+  }
+
+}
index b3456aa..2e8ace8 100644 (file)
@@ -403,4 +403,45 @@ public class StringUtils
     }
     return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
   }
+
+  /**
+   * A helper method that strips off any leading or trailing html and body tags.
+   * If no html tag is found, then also html-encodes angle bracket characters.
+   * 
+   * @param text
+   * @return
+   */
+  public static String stripHtmlTags(String text)
+  {
+    if (text == null)
+    {
+      return null;
+    }
+    String tmp2up = text.toUpperCase();
+    int startTag = tmp2up.indexOf("<HTML>");
+    if (startTag > -1)
+    {
+      text = text.substring(startTag + 6);
+      tmp2up = tmp2up.substring(startTag + 6);
+    }
+    // is omission of "<BODY>" intentional here??
+    int endTag = tmp2up.indexOf("</BODY>");
+    if (endTag > -1)
+    {
+      text = text.substring(0, endTag);
+      tmp2up = tmp2up.substring(0, endTag);
+    }
+    endTag = tmp2up.indexOf("</HTML>");
+    if (endTag > -1)
+    {
+      text = text.substring(0, endTag);
+    }
+  
+    if (startTag == -1 && (text.contains("<") || text.contains(">")))
+    {
+      text = text.replaceAll("<", "&lt;");
+      text = text.replaceAll(">", "&gt;");
+    }
+    return text;
+  }
 }
diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java
new file mode 100644 (file)
index 0000000..8816a7f
--- /dev/null
@@ -0,0 +1,102 @@
+package jalview.util.matcher;
+
+import jalview.util.MessageManager;
+
+/**
+ * An enumeration for binary conditions that a user might choose from when
+ * setting filter or match conditions for values
+ */
+public enum Condition
+{
+  Contains(false, true, "Contains"),
+  NotContains(false, true, "NotContains"), Matches(false, true, "Matches"),
+  NotMatches(false, true, "NotMatches"), Present(false, false, "Present"),
+  NotPresent(false, false, "NotPresent"), EQ(true, true, "EQ"),
+  NE(true, true, "NE"), LT(true, true, "LT"), LE(true, true, "LE"),
+  GT(true, true, "GT"), GE(true, true, "GE");
+
+  private boolean numeric;
+
+  private boolean needsAPattern;
+
+  /*
+   * value used to save a Condition to the 
+   * Jalview project file or restore it from project; 
+   * it should not be changed even if enum names change in future
+   */
+  private String stableName;
+
+  /**
+   * Answers the enum value whose 'stable name' matches the argument (not case
+   * sensitive), or null if no match
+   * 
+   * @param stableName
+   * @return
+   */
+  public static Condition fromString(String stableName)
+  {
+    for (Condition c : values())
+    {
+      if (c.stableName.equalsIgnoreCase(stableName))
+      {
+        return c;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Constructor
+   * 
+   * @param isNumeric
+   * @param needsPattern
+   * @param stablename
+   */
+  Condition(boolean isNumeric, boolean needsPattern, String stablename)
+  {
+    numeric = isNumeric;
+    needsAPattern = needsPattern;
+    stableName = stablename;
+  }
+
+  /**
+   * Answers true if the condition does a numerical comparison, else false
+   * (string comparison)
+   * 
+   * @return
+   */
+  public boolean isNumeric()
+  {
+    return numeric;
+  }
+
+  /**
+   * Answers true if the condition requires a pattern to compare against, else
+   * false
+   * 
+   * @return
+   */
+  public boolean needsAPattern()
+  {
+    return needsAPattern;
+  }
+
+  public String getStableName()
+  {
+    return stableName;
+  }
+
+  /**
+   * Answers a display name for the match condition, suitable for showing in
+   * drop-down menus. The value may be internationalized using the resource key
+   * "label.matchCondition_" with the enum name appended.
+   * 
+   * @return
+   */
+  @Override
+  public String toString()
+  {
+    return MessageManager.getStringOrReturn("label.matchCondition_",
+            name());
+  }
+}
diff --git a/src/jalview/util/matcher/Matcher.java b/src/jalview/util/matcher/Matcher.java
new file mode 100644 (file)
index 0000000..0792509
--- /dev/null
@@ -0,0 +1,251 @@
+package jalview.util.matcher;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * A bean to describe one attribute-based filter
+ */
+public class Matcher implements MatcherI
+{
+  /*
+   * the comparison condition
+   */
+  Condition condition;
+
+  /*
+   * the string pattern as entered, or the regex, to compare to
+   * also holds the string form of float value if a numeric condition
+   */
+  String pattern;
+
+  /*
+   * the pattern in upper case, for non-case-sensitive matching
+   */
+  String uppercasePattern;
+
+  /*
+   * the compiled regex if using a pattern match condition
+   * (reserved for possible future enhancement)
+   */
+  Pattern regexPattern;
+
+  /*
+   * the value to compare to for a numerical condition
+   */
+  float value;
+
+  /**
+   * Constructor
+   * 
+   * @param cond
+   * @param compareTo
+   * @return
+   * @throws NumberFormatException
+   *           if a numerical condition is specified with a non-numeric
+   *           comparison value
+   * @throws NullPointerException
+   *           if a null condition or comparison string is specified
+   */
+  public Matcher(Condition cond, String compareTo)
+  {
+    Objects.requireNonNull(cond);
+    condition = cond;
+    if (cond.isNumeric())
+    {
+      value = Float.valueOf(compareTo);
+      pattern = String.valueOf(value);
+      uppercasePattern = pattern;
+    }
+    else
+    {
+      pattern = compareTo;
+      if (pattern != null)
+      {
+        uppercasePattern = pattern.toUpperCase();
+      }
+    }
+
+    // if we add regex conditions (e.g. matchesPattern), then
+    // pattern should hold the raw regex, and
+    // regexPattern = Pattern.compile(compareTo);
+  }
+
+  /**
+   * Constructor for a numerical match condition. Note that if a string
+   * comparison condition is specified, this will be converted to a comparison
+   * with the float value as string
+   * 
+   * @param cond
+   * @param compareTo
+   */
+  public Matcher(Condition cond, float compareTo)
+  {
+    this(cond, String.valueOf(compareTo));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @SuppressWarnings("incomplete-switch")
+  @Override
+  public boolean matches(String val)
+  {
+    if (condition.isNumeric())
+    {
+      try
+      {
+        /*
+         * treat a null value (no such attribute) as
+         * failing any numerical filter condition
+         */
+        return val == null ? false : matches(Float.valueOf(val));
+      } catch (NumberFormatException e)
+      {
+        return false;
+      }
+    }
+    
+    /*
+     * a null value matches a negative condition, fails a positive test
+     */
+    if (val == null)
+    {
+      return condition == Condition.NotContains
+              || condition == Condition.NotMatches 
+              || condition == Condition.NotPresent;
+    }
+    
+    String upper = val.toUpperCase().trim();
+    boolean matched = false;
+    switch(condition) {
+    case Matches:
+      matched = upper.equals(uppercasePattern);
+      break;
+    case NotMatches:
+      matched = !upper.equals(uppercasePattern);
+      break;
+    case Contains:
+      matched = upper.indexOf(uppercasePattern) > -1;
+      break;
+    case NotContains:
+      matched = upper.indexOf(uppercasePattern) == -1;
+      break;
+    case Present:
+      matched = true;
+      break;
+    default:
+      break;
+    }
+    return matched;
+  }
+
+  /**
+   * Applies a numerical comparison match condition
+   * 
+   * @param f
+   * @return
+   */
+  @SuppressWarnings("incomplete-switch")
+  boolean matches(float f)
+  {
+    if (!condition.isNumeric())
+    {
+      return matches(String.valueOf(f));
+    }
+    
+    boolean matched = false;
+    switch (condition) {
+    case LT:
+      matched = f < value;
+      break;
+    case LE:
+      matched = f <= value;
+      break;
+    case EQ:
+      matched = f == value;
+      break;
+    case NE:
+      matched = f != value;
+      break;
+    case GT:
+      matched = f > value;
+      break;
+    case GE:
+      matched = f >= value;
+      break;
+    default:
+      break;
+    }
+
+    return matched;
+  }
+
+  /**
+   * A simple hash function that guarantees that when two objects are equal,
+   * they have the same hashcode
+   */
+  @Override
+  public int hashCode()
+  {
+    return pattern.hashCode() + condition.hashCode() + (int) value;
+  }
+
+  /**
+   * equals is overridden so that we can safely remove Matcher objects from
+   * collections (e.g. delete an attribute match condition for a feature colour)
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj == null || !(obj instanceof Matcher))
+    {
+      return false;
+    }
+    Matcher m = (Matcher) obj;
+    if (condition != m.condition || value != m.value)
+    {
+      return false;
+    }
+    if (pattern == null)
+    {
+      return m.pattern == null;
+    }
+    return uppercasePattern.equals(m.uppercasePattern);
+  }
+
+  @Override
+  public Condition getCondition()
+  {
+    return condition;
+  }
+
+  @Override
+  public String getPattern()
+  {
+    return pattern;
+  }
+
+  @Override
+  public float getFloatValue()
+  {
+    return value;
+  }
+
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append(condition.toString()).append(" ");
+    if (condition.isNumeric())
+    {
+      sb.append(pattern);
+    }
+    else
+    {
+      sb.append("'").append(pattern).append("'");
+    }
+
+    return sb.toString();
+  }
+}
diff --git a/src/jalview/util/matcher/MatcherI.java b/src/jalview/util/matcher/MatcherI.java
new file mode 100644 (file)
index 0000000..ca6d44c
--- /dev/null
@@ -0,0 +1,18 @@
+package jalview.util.matcher;
+
+public interface MatcherI
+{
+  /**
+   * Answers true if the given value is matched, else false
+   * 
+   * @param s
+   * @return
+   */
+  boolean matches(String s);
+
+  Condition getCondition();
+
+  String getPattern();
+
+  float getFloatValue();
+}
index 43b0550..553f813 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.FeatureMatcherSetI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.renderer.seqfeatures.FeatureRenderer;
 import jalview.schemes.FeatureColour;
@@ -48,16 +49,49 @@ import java.util.concurrent.ConcurrentHashMap;
 public abstract class FeatureRendererModel
         implements jalview.api.FeatureRenderer
 {
+  /*
+   * a data bean to hold one row of feature settings from the gui
+   */
+  public static class FeatureSettingsBean
+  {
+    public final String featureType;
 
-  /**
+    public final FeatureColourI featureColour;
+
+    public final FeatureMatcherSetI filter;
+
+    public final Boolean show;
+
+    public FeatureSettingsBean(String type, FeatureColourI colour,
+            FeatureMatcherSetI theFilter, Boolean isShown)
+    {
+      featureType = type;
+      featureColour = colour;
+      filter = theFilter;
+      show = isShown;
+    }
+  }
+
+  /*
    * global transparency for feature
    */
   protected float transparency = 1.0f;
 
+  /*
+   * colour scheme for each feature type
+   */
   protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<>();
 
+  /*
+   * visibility flag for each feature group
+   */
   protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
 
+  /*
+   * filters for each feature type
+   */
+  protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+
   protected String[] renderOrder;
 
   Map<String, Float> featureOrder = null;
@@ -100,6 +134,7 @@ public abstract class FeatureRendererModel
     this.renderOrder = frs.renderOrder;
     this.featureGroups = frs.featureGroups;
     this.featureColours = frs.featureColours;
+    this.featureFilters = frs.featureFilters;
     this.transparency = frs.transparency;
     this.featureOrder = frs.featureOrder;
     if (av != null && av != fr.getViewport())
@@ -284,9 +319,13 @@ public abstract class FeatureRendererModel
     List<SequenceFeature> features = sequence.findFeatures(column, column,
             visibleTypes);
 
+    /*
+     * include features unless their feature group is not displayed, or
+     * they are hidden (have no colour) based on a filter or colour threshold
+     */
     for (SequenceFeature sf : features)
     {
-      if (!featureGroupNotShown(sf))
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
       {
         result.add(sf);
       }
@@ -477,7 +516,8 @@ public abstract class FeatureRendererModel
               if (mmrange != null)
               {
                 FeatureColourI fc = featureColours.get(oldRender[j]);
-                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                        && !fc.isColourByAttribute())
                 {
                   fc.updateBounds(mmrange[0][0], mmrange[0][1]);
                 }
@@ -507,7 +547,8 @@ public abstract class FeatureRendererModel
         if (mmrange != null)
         {
           FeatureColourI fc = featureColours.get(newf[i]);
-          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                  && !fc.isColourByAttribute())
           {
             fc.updateBounds(mmrange[0][0], mmrange[0][1]);
           }
@@ -557,20 +598,11 @@ public abstract class FeatureRendererModel
     return fc;
   }
 
-  /**
-   * 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. Returns null for a score feature whose score value lies
-   * outside any colour threshold.
-   * 
-   * @param feature
-   * @return
-   */
+  @Override
   public Color getColour(SequenceFeature feature)
   {
     FeatureColourI fc = getFeatureStyle(feature.getType());
-    return fc.getColor(feature);
+    return getColor(feature, fc);
   }
 
   /**
@@ -582,7 +614,8 @@ public abstract class FeatureRendererModel
    */
   protected boolean showFeatureOfType(String type)
   {
-    return type == null ? false : av.getFeaturesDisplayed().isVisible(type);
+    return type == null ? false : (av.getFeaturesDisplayed() == null ? true
+            : av.getFeaturesDisplayed().isVisible(type));
   }
 
   @Override
@@ -651,26 +684,27 @@ public abstract class FeatureRendererModel
    * Replace current ordering with new ordering
    * 
    * @param data
-   *          { String(Type), Colour(Type), Boolean(Displayed) }
+   *          an array of { Type, Colour, Filter, Boolean }
    * @return true if any visible features have been reordered, else false
    */
-  public boolean setFeaturePriority(Object[][] data)
+  public boolean setFeaturePriority(FeatureSettingsBean[] data)
   {
     return setFeaturePriority(data, true);
   }
 
   /**
-   * Sets the priority order for features, with the highest priority (displayed
-   * on top) at the start of the data array
+   * 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) }
+   *          an array of { Type, Colour, Filter, Boolean }
    * @param visibleNew
    *          when true current featureDisplay list will be cleared
-   * @return true if any visible features have been reordered or recoloured,
-   *         else false (i.e. no need to repaint)
+   * @return true if any visible features have been reordered or recoloured, else
+   *         false (i.e. no need to repaint)
    */
-  public boolean setFeaturePriority(Object[][] data, boolean visibleNew)
+  public boolean setFeaturePriority(FeatureSettingsBean[] data,
+          boolean visibleNew)
   {
     /*
      * note visible feature ordering and colours before update
@@ -709,9 +743,9 @@ public abstract class FeatureRendererModel
     {
       for (int i = 0; i < data.length; i++)
       {
-        String type = data[i][0].toString();
-        setColour(type, (FeatureColourI) data[i][1]);
-        if (((Boolean) data[i][2]).booleanValue())
+        String type = data[i].featureType;
+        setColour(type, data[i].featureColour);
+        if (data[i].show)
         {
           av_featuresdisplayed.setVisible(type);
         }
@@ -986,7 +1020,7 @@ public abstract class FeatureRendererModel
   
     for (SequenceFeature sf : features)
     {
-      if (!featureGroupNotShown(sf))
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
       {
         result.add(sf);
       }
@@ -995,57 +1029,125 @@ public abstract class FeatureRendererModel
   }
 
   /**
-   * 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).
+   * Removes from the list of features any that duplicate the location of a
+   * feature of the same type. Should be used only for features of the same,
+   * simple, feature colour (which normally implies the same feature type). Does
+   * not check visibility settings for feature type or feature group. No
+   * filtering is done if transparency, or any feature filters, are in force.
    * 
    * @param features
-   * @param fc
    */
-  public void filterFeaturesForDisplay(List<SequenceFeature> features,
-          FeatureColourI fc)
+  public void filterFeaturesForDisplay(List<SequenceFeature> features)
   {
-    if (features.isEmpty())
+    /*
+     * don't remove 'redundant' features if 
+     * - transparency is applied (feature count affects depth of feature colour)
+     * - filters are applied (not all features may be displayable)
+     */
+    if (features.isEmpty() || transparency != 1f
+            || !featureFilters.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))
-      {
-        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)
-       * don't remove 'redundant' features if transparency is applied 
-       * (as feature count affects depth of feature colour)
        */
-      if (simpleColour && transparency == 1f)
+      if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+              && sf.getEnd() == lastFeature.getEnd()
+              && sf.isContactFeature() == lastFeature.isContactFeature()
+              && sf.getType().equals(lastFeature.getType()))
       {
-        if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
-                && sf.getEnd() == lastFeature.getEnd()
-                && sf.isContactFeature() == lastFeature.isContactFeature()
-                && sf.getType().equals(lastFeature.getType()))
-        {
-          it.remove();
-        }
+        it.remove();
       }
       lastFeature = sf;
     }
   }
 
+  @Override
+  public Map<String, FeatureMatcherSetI> getFeatureFilters()
+  {
+    return featureFilters;
+  }
+
+  @Override
+  public void setFeatureFilters(Map<String, FeatureMatcherSetI> filters)
+  {
+    featureFilters = filters;
+  }
+
+  @Override
+  public FeatureMatcherSetI getFeatureFilter(String featureType)
+  {
+    return featureFilters.get(featureType);
+  }
+
+  @Override
+  public void setFeatureFilter(String featureType, FeatureMatcherSetI filter)
+  {
+    if (filter == null || filter.isEmpty())
+    {
+      featureFilters.remove(featureType);
+    }
+    else
+    {
+      featureFilters.put(featureType, filter);
+    }
+  }
+
+  /**
+   * Answers the colour for the feature, or null if the feature is excluded by
+   * feature group visibility, by filters, or by colour threshold settings. This
+   * method does not take feature visibility into account.
+   * 
+   * @param sf
+   * @param fc
+   * @return
+   */
+  public Color getColor(SequenceFeature sf, FeatureColourI fc)
+  {
+    /*
+     * is the feature group displayed?
+     */
+    if (featureGroupNotShown(sf))
+    {
+      return null;
+    }
+
+    /*
+     * does the feature pass filters?
+     */
+    if (!featureMatchesFilters(sf))
+    {
+      return null;
+    }
+  
+    return fc.getColor(sf);
+  }
+
+  /**
+   * Answers true if there no are filters defined for the feature type, or this
+   * feature matches the filters. Answers false if the feature fails to match
+   * filters.
+   * 
+   * @param sf
+   * @return
+   */
+  protected boolean featureMatchesFilters(SequenceFeature sf)
+  {
+    FeatureMatcherSetI filter = featureFilters.get(sf.getType());
+    return filter == null ? true : filter.matches(sf);
+  }
+
 }
index dc2ae11..f594453 100644 (file)
 package jalview.viewmodel.seqfeatures;
 
 import jalview.api.FeatureColourI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.schemes.FeatureColour;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -42,6 +44,11 @@ public class FeatureRendererSettings implements Cloneable
    */
   Map<String, FeatureColourI> featureColours;
 
+  /*
+   * map of {featureType, filters}
+   */
+  Map<String, FeatureMatcherSetI> featureFilters;
+
   float transparency;
 
   Map<String, Float> featureOrder;
@@ -72,7 +79,9 @@ public class FeatureRendererSettings implements Cloneable
     renderOrder = null;
     featureGroups = new ConcurrentHashMap<String, Boolean>();
     featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+    featureFilters = new HashMap<>();
     featureOrder = new ConcurrentHashMap<String, Float>();
+
     if (fr.renderOrder != null)
     {
       this.renderOrder = new String[fr.renderOrder.length];
@@ -100,6 +109,12 @@ public class FeatureRendererSettings implements Cloneable
         featureColours.put(next, new FeatureColour((FeatureColour) val));
       }
     }
+
+    if (fr.featureFilters != null)
+    {
+      this.featureFilters.putAll(fr.featureFilters);
+    }
+
     this.transparency = fr.transparency;
     if (fr.featureOrder != null)
     {
index 1677eca..061e70c 100644 (file)
@@ -28,15 +28,12 @@ import jalview.datamodel.DBRefSource;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.SequenceI;
 import jalview.gui.CutAndPasteTransfer;
-import jalview.gui.DasSourceBrowser;
 import jalview.gui.Desktop;
 import jalview.gui.FeatureSettings;
 import jalview.gui.IProgressIndicator;
 import jalview.gui.OOMWarning;
 import jalview.util.DBRefUtils;
 import jalview.util.MessageManager;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-import jalview.ws.dbsources.das.datamodel.DasSequenceSource;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
 import java.util.ArrayList;
@@ -119,7 +116,7 @@ public class DBRefFetcher implements Runnable
           DbSourceProxy[] sources, FeatureSettings featureSettings,
           boolean isNucleotide)
   {
-    listeners = new ArrayList<FetchFinishedListenerI>();
+    listeners = new ArrayList<>();
     this.progressWindow = progressIndicatorFrame;
     alseqs = new SequenceI[seqs.length];
     SequenceI[] ds = new SequenceI[seqs.length];
@@ -165,23 +162,7 @@ public class DBRefFetcher implements Runnable
   {
     // af.featureSettings_actionPerformed(null);
     String[] defdb = null;
-    List<DbSourceProxy> selsources = new ArrayList<DbSourceProxy>();
-    Vector<jalviewSourceI> dasselsrc = (featureSettings != null)
-            ? featureSettings.getSelectedSources()
-            : new DasSourceBrowser().getSelectedSources();
-
-    for (jalviewSourceI src : dasselsrc)
-    {
-      List<DbSourceProxy> sp = src.getSequenceSourceProxies();
-      if (sp != null)
-      {
-        selsources.addAll(sp);
-        if (sp.size() > 1)
-        {
-          Cache.log.debug("Added many Db Sources for :" + src.getTitle());
-        }
-      }
-    }
+    List<DbSourceProxy> selsources = new ArrayList<>();
     // select appropriate databases based on alignFrame context.
     if (forNucleotide)
     {
@@ -191,7 +172,7 @@ public class DBRefFetcher implements Runnable
     {
       defdb = DBRefSource.PROTEINDBS;
     }
-    List<DbSourceProxy> srces = new ArrayList<DbSourceProxy>();
+    List<DbSourceProxy> srces = new ArrayList<>();
     for (String ddb : defdb)
     {
       List<DbSourceProxy> srcesfordb = sfetcher.getSourceProxy(ddb);
@@ -235,30 +216,6 @@ public class DBRefFetcher implements Runnable
   }
 
   /**
-   * retrieve all the das sequence sources and add them to the list of db
-   * sources to retrieve from
-   */
-  public void appendAllDasSources()
-  {
-    if (dbSources == null)
-    {
-      dbSources = new DbSourceProxy[0];
-    }
-    // append additional sources
-    DbSourceProxy[] otherdb = sfetcher
-            .getDbSourceProxyInstances(DasSequenceSource.class);
-    if (otherdb != null && otherdb.length > 0)
-    {
-      DbSourceProxy[] newsrc = new DbSourceProxy[dbSources.length
-              + otherdb.length];
-      System.arraycopy(dbSources, 0, newsrc, 0, dbSources.length);
-      System.arraycopy(otherdb, 0, newsrc, dbSources.length,
-              otherdb.length);
-      dbSources = newsrc;
-    }
-  }
-
-  /**
    * start the fetcher thread
    * 
    * @param waitTillFinished
@@ -311,14 +268,14 @@ public class DBRefFetcher implements Runnable
       }
       else if (seqs == null)
       {
-        seqs = new Vector<SequenceI>();
+        seqs = new Vector<>();
         seqs.addElement(seq);
       }
 
     }
     else
     {
-      seqs = new Vector<SequenceI>();
+      seqs = new Vector<>();
       seqs.addElement(seq);
     }
 
@@ -357,9 +314,9 @@ public class DBRefFetcher implements Runnable
       e.printStackTrace();
     }
 
-    Vector<SequenceI> sdataset = new Vector<SequenceI>(
+    Vector<SequenceI> sdataset = new Vector<>(
             Arrays.asList(dataset));
-    List<String> warningMessages = new ArrayList<String>();
+    List<String> warningMessages = new ArrayList<>();
 
     int db = 0;
     while (sdataset.size() > 0 && db < dbSources.length)
@@ -371,8 +328,8 @@ public class DBRefFetcher implements Runnable
       SequenceI[] currSeqs = new SequenceI[sdataset.size()];
       sdataset.copyInto(currSeqs);// seqs that are to be validated against
       // dbSources[db]
-      Vector<String> queries = new Vector<String>(); // generated queries curSeq
-      seqRefs = new Hashtable<String, Vector<SequenceI>>();
+      Vector<String> queries = new Vector<>(); // generated queries curSeq
+      seqRefs = new Hashtable<>();
 
       int seqIndex = 0;
 
@@ -574,7 +531,7 @@ public class DBRefFetcher implements Runnable
     {
       // Work out which sequences this sequence matches,
       // taking into account all accessionIds and names in the file
-      Vector<SequenceI> sequenceMatches = new Vector<SequenceI>();
+      Vector<SequenceI> sequenceMatches = new Vector<>();
       // look for corresponding accession ids
       DBRefEntry[] entryRefs = DBRefUtils
               .selectRefs(retrievedSeq.getDBRefs(), new String[]
@@ -826,7 +783,7 @@ public class DBRefFetcher implements Runnable
    */
   private SequenceI[] recoverDbSequences(SequenceI[] sequencesArray)
   {
-    Vector<SequenceI> nseq = new Vector<SequenceI>();
+    Vector<SequenceI> nseq = new Vector<>();
     for (int i = 0; sequencesArray != null
             && i < sequencesArray.length; i++)
     {
diff --git a/src/jalview/ws/DasSequenceFeatureFetcher.java b/src/jalview/ws/DasSequenceFeatureFetcher.java
deleted file mode 100644 (file)
index c661e2c..0000000
+++ /dev/null
@@ -1,925 +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.ws;
-
-import jalview.bin.Cache;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.DBRefSource;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.gui.AlignFrame;
-import jalview.gui.Desktop;
-import jalview.gui.FeatureSettings;
-import jalview.gui.JvOptionPane;
-import jalview.util.DBRefUtils;
-import jalview.util.MessageManager;
-import jalview.util.UrlLink;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
-import org.biodas.jdas.client.FeaturesClient;
-import org.biodas.jdas.client.adapters.features.DasGFFAdapter;
-import org.biodas.jdas.client.adapters.features.DasGFFAdapter.GFFAdapter;
-import org.biodas.jdas.client.threads.FeaturesClientMultipleSources;
-import org.biodas.jdas.schema.features.ERRORSEGMENT;
-import org.biodas.jdas.schema.features.FEATURE;
-import org.biodas.jdas.schema.features.LINK;
-import org.biodas.jdas.schema.features.SEGMENT;
-import org.biodas.jdas.schema.features.TYPE;
-import org.biodas.jdas.schema.features.UNKNOWNFEATURE;
-import org.biodas.jdas.schema.features.UNKNOWNSEGMENT;
-import org.biodas.jdas.schema.sources.COORDINATES;
-
-/**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
- */
-public class DasSequenceFeatureFetcher
-{
-  SequenceI[] sequences;
-
-  AlignFrame af;
-
-  FeatureSettings fsettings;
-
-  StringBuffer sbuffer = new StringBuffer();
-
-  List<jalviewSourceI> selectedSources;
-
-  boolean cancelled = false;
-
-  private void debug(String mesg)
-  {
-    debug(mesg, null);
-  }
-
-  private void debug(String mesg, Exception e)
-  {
-    if (Cache.log != null)
-    {
-      Cache.log.debug(mesg, e);
-    }
-    else
-    {
-      System.err.println(mesg);
-      if (e != null)
-      {
-        e.printStackTrace();
-      }
-    }
-  }
-
-  long startTime;
-
-  private DasSourceRegistryI sourceRegistry;
-
-  private boolean useJDASMultiThread = true;
-
-  /**
-   * Creates a new SequenceFeatureFetcher object. Uses default
-   * 
-   * @param align
-   *          DOCUMENT ME!
-   * @param ap
-   *          DOCUMENT ME!
-   */
-  public DasSequenceFeatureFetcher(SequenceI[] sequences,
-          FeatureSettings fsettings, Vector selectedSources)
-  {
-    this(sequences, fsettings, selectedSources, true, true, true);
-  }
-
-  public DasSequenceFeatureFetcher(SequenceI[] oursequences,
-          FeatureSettings fsettings, List<jalviewSourceI> selectedSources2,
-          boolean checkDbrefs, boolean promptFetchDbrefs)
-  {
-    this(oursequences, fsettings, selectedSources2, checkDbrefs,
-            promptFetchDbrefs, true);
-  }
-
-  public DasSequenceFeatureFetcher(SequenceI[] oursequences,
-          FeatureSettings fsettings, List<jalviewSourceI> selectedSources2,
-          boolean checkDbrefs, boolean promptFetchDbrefs,
-          boolean useJDasMultiThread)
-  {
-    this.useJDASMultiThread = useJDasMultiThread;
-    this.selectedSources = new ArrayList<>();
-    // filter both sequences and sources to eliminate duplicates
-    for (jalviewSourceI src : selectedSources2)
-    {
-      if (!selectedSources.contains(src))
-      {
-        selectedSources.add(src);
-      }
-      ;
-    }
-    Vector sqs = new Vector();
-    for (int i = 0; i < oursequences.length; i++)
-    {
-      if (!sqs.contains(oursequences[i]))
-      {
-        sqs.addElement(oursequences[i]);
-      }
-    }
-    sequences = new SequenceI[sqs.size()];
-    for (int i = 0; i < sequences.length; i++)
-    {
-      sequences[i] = (SequenceI) sqs.elementAt(i);
-    }
-    if (fsettings != null)
-    {
-      this.fsettings = fsettings;
-      this.af = fsettings.af;
-      af.setShowSeqFeatures(true);
-    }
-    int uniprotCount = 0;
-    for (jalviewSourceI source : selectedSources)
-    {
-      for (COORDINATES coords : source.getVersion().getCOORDINATES())
-      {
-        // TODO: match UniProt coord system canonically (?) - does
-        // UniProt==uniprot==UNIPROT ?
-        if (coords.getAuthority().toLowerCase().equals("uniprot"))
-        {
-          uniprotCount++;
-          break;
-        }
-      }
-    }
-
-    int refCount = 0;
-    for (int i = 0; i < sequences.length; i++)
-    {
-      DBRefEntry[] dbref = sequences[i].getDBRefs();
-      if (dbref != null)
-      {
-        for (int j = 0; j < dbref.length; j++)
-        {
-          if (dbref[j].getSource().equals(DBRefSource.UNIPROT))
-          {
-            refCount++;
-            break;
-          }
-        }
-      }
-    }
-
-    if (checkDbrefs && refCount < sequences.length && uniprotCount > 0)
-    {
-
-      int reply = JvOptionPane.YES_OPTION;
-      if (promptFetchDbrefs)
-      {
-        reply = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-                MessageManager.getString(
-                        "info.you_want_jalview_to_find_uniprot_accessions"),
-                MessageManager
-                        .getString("label.find_uniprot_accession_ids"),
-                JvOptionPane.YES_NO_OPTION, JvOptionPane.QUESTION_MESSAGE);
-      }
-
-      if (reply == JvOptionPane.YES_OPTION)
-      {
-        Thread thread = new Thread(new FetchDBRefs());
-        thread.start();
-      }
-      else
-      {
-        _startFetching();
-      }
-    }
-    else
-    {
-      _startFetching();
-    }
-
-  }
-
-  private void _startFetching()
-  {
-    running = true;
-    new Thread(new FetchSeqFeatures()).start();
-  }
-
-  class FetchSeqFeatures implements Runnable
-  {
-    @Override
-    public void run()
-    {
-      startFetching();
-      setGuiFetchComplete();
-    }
-  }
-
-  class FetchDBRefs implements Runnable
-  {
-    @Override
-    public void run()
-    {
-      running = true;
-      boolean isNucleotide = af.getViewport().getAlignment().isNucleotide();
-      new DBRefFetcher(sequences, af, null, af.featureSettings,
-              isNucleotide).fetchDBRefs(true);
-
-      startFetching();
-      setGuiFetchComplete();
-    }
-  }
-
-  /**
-   * Spawns Fetcher threads to add features to sequences in the dataset
-   */
-  void startFetching()
-  {
-    running = true;
-    cancelled = false;
-    startTime = System.currentTimeMillis();
-    if (af != null)
-    {
-      af.setProgressBar(MessageManager.getString(
-              "status.fetching_das_sequence_features"), startTime);
-    }
-    if (sourceRegistry == null)
-    {
-      sourceRegistry = Cache.getDasSourceRegistry();
-    }
-    if (selectedSources == null || selectedSources.size() == 0)
-    {
-      try
-      {
-        jalviewSourceI[] sources = sourceRegistry.getSources()
-                .toArray(new jalviewSourceI[0]);
-        String active = Cache.getDefault("DAS_ACTIVE_SOURCE", "uniprot");
-        StringTokenizer st = new StringTokenizer(active, "\t");
-        selectedSources = new Vector();
-        String token;
-        while (st.hasMoreTokens())
-        {
-          token = st.nextToken();
-          for (int i = 0; i < sources.length; i++)
-          {
-            if (sources[i].getTitle().equals(token))
-            {
-              selectedSources.add(sources[i]);
-              break;
-            }
-          }
-        }
-      } catch (Exception ex)
-      {
-        debug("Exception whilst setting default feature sources from registry and local preferences.",
-                ex);
-      }
-    }
-
-    if (selectedSources == null || selectedSources.size() == 0)
-    {
-      System.out.println("No DAS Sources active");
-      cancelled = true;
-      setGuiNoDassourceActive();
-      return;
-    }
-
-    sourcesRemaining = selectedSources.size();
-    FeaturesClientMultipleSources fc = new FeaturesClientMultipleSources();
-    fc.setConnProps(sourceRegistry.getSessionHandler());
-    // Now sending requests one at a time to each server
-    ArrayList<jalviewSourceI> srcobj = new ArrayList<>();
-    ArrayList<String> src = new ArrayList<>();
-    List<List<String>> ids = new ArrayList<>();
-    List<List<DBRefEntry>> idobj = new ArrayList<>();
-    List<Map<String, SequenceI>> sqset = new ArrayList<>();
-    for (jalviewSourceI _sr : selectedSources)
-    {
-
-      Map<String, SequenceI> slist = new HashMap<>();
-      List<DBRefEntry> idob = new ArrayList<>();
-      List<String> qset = new ArrayList<>();
-
-      for (SequenceI seq : sequences)
-      {
-        Object[] idset = nextSequence(_sr, seq);
-        if (idset != null)
-        {
-          List<DBRefEntry> _idob = (List<DBRefEntry>) idset[0];
-          List<String> _qset = (List<String>) idset[1];
-          if (_idob.size() > 0)
-          {
-            // add sequence's ref for each id derived from it
-            // (space inefficient, but most unambiguous)
-            // could replace with hash with _qset values as keys.
-            Iterator<DBRefEntry> dbobj = _idob.iterator();
-            for (String q : _qset)
-            {
-              SequenceI osq = slist.get(q);
-              DBRefEntry dr = dbobj.next();
-              if (osq != null && osq != seq)
-              {
-                // skip - non-canonical query
-              }
-              else
-              {
-                idob.add(dr);
-                qset.add(q);
-                slist.put(q, seq);
-              }
-            }
-          }
-        }
-      }
-      if (idob.size() > 0)
-      {
-        srcobj.add(_sr);
-        src.add(_sr.getSourceURL());
-        ids.add(qset);
-        idobj.add(idob);
-        sqset.add(slist);
-      }
-    }
-    Map<String, Map<List<String>, Exception>> errors = new HashMap<>();
-    Map<String, Map<List<String>, DasGFFAdapter>> results = new HashMap<>();
-    if (!useJDASMultiThread)
-    {
-      Iterator<String> sources = src.iterator();
-      // iterate over each query for each source and do each one individually
-      for (List<String> idl : ids)
-      {
-        String source = sources.next();
-        FeaturesClient featuresc = new FeaturesClient(
-                sourceRegistry.getSessionHandler()
-                        .getConnectionPropertyProviderFor(source));
-        for (String id : idl)
-        {
-          List<String> qid = Arrays.asList(new String[] { id });
-          try
-          {
-            DasGFFAdapter dga = featuresc.fetchData(source, qid);
-            Map<List<String>, DasGFFAdapter> ers = results.get(source);
-            if (ers == null)
-            {
-              results.put(source,
-                      ers = new HashMap<>());
-            }
-            ers.put(qid, dga);
-          } catch (Exception ex)
-          {
-            Map<List<String>, Exception> ers = errors.get(source);
-            if (ers == null)
-            {
-              errors.put(source,
-                      ers = new HashMap<>());
-            }
-            ers.put(qid, ex);
-          }
-        }
-      }
-    }
-    else
-    {
-      // pass them all at once
-      fc.fetchData(src, ids, false, results, errors);
-      fc.shutDown();
-      while (!fc.isTerminated())
-      {
-        try
-        {
-          Thread.sleep(200);
-        } catch (InterruptedException x)
-        {
-
-        }
-      }
-    }
-    Iterator<List<String>> idset = ids.iterator();
-    Iterator<List<DBRefEntry>> idobjset = idobj.iterator();
-    Iterator<Map<String, SequenceI>> seqset = sqset.iterator();
-    for (jalviewSourceI source : srcobj)
-    {
-      processResponse(seqset.next(), source, idset.next(), idobjset.next(),
-              results.get(source.getSourceURL()),
-              errors.get(source.getSourceURL()));
-    }
-  }
-
-  private void processResponse(Map<String, SequenceI> sequencemap,
-          jalviewSourceI jvsource, List<String> ids, List<DBRefEntry> idobj,
-          Map<List<String>, DasGFFAdapter> results,
-          Map<List<String>, Exception> errors)
-  {
-    Set<SequenceI> sequences = new HashSet<>();
-    String source = jvsource.getSourceURL();
-    // process features
-    DasGFFAdapter result = (results == null) ? null : results.get(ids);
-    Exception error = (errors == null) ? null : errors.get(ids);
-    if (result == null)
-    {
-      debug("das source " + source + " could not be contacted. "
-              + (error == null ? "" : error.toString()));
-    }
-    else
-    {
-
-      GFFAdapter gff = result.getGFF();
-      List<SEGMENT> segments = gff.getSegments();
-      List<ERRORSEGMENT> errorsegs = gff.getErrorSegments();
-      List<UNKNOWNFEATURE> unkfeats = gff.getUnknownFeatures();
-      List<UNKNOWNSEGMENT> unksegs = gff.getUnknownSegments();
-      debug("das source " + source + " returned " + gff.getTotal()
-              + " responses. " + (errorsegs != null ? errorsegs.size() : 0)
-              + " were incorrect segment queries, "
-              + (unkfeats != null ? unkfeats.size() : 0)
-              + " were unknown features "
-              + (unksegs != null ? unksegs.size() : 0)
-              + " were unknown segments and "
-              + (segments != null ? segments.size() : 0)
-              + " were segment responses.");
-      Iterator<DBRefEntry> dbr = idobj.iterator();
-      if (segments != null)
-      {
-        for (SEGMENT seg : segments)
-        {
-          String id = seg.getId();
-          if (ids.indexOf(id) == -1)
-          {
-            id = id.toUpperCase();
-          }
-          DBRefEntry dbref = idobj.get(ids.indexOf(id));
-          SequenceI sequence = sequencemap.get(id);
-          boolean added = false;
-          sequences.add(sequence);
-
-          for (FEATURE feat : seg.getFEATURE())
-          {
-            // standard DAS feature-> jalview sequence feature transformation
-            SequenceFeature f = newSequenceFeature(feat,
-                    jvsource.getTitle());
-            if (!parseSeqFeature(sequence, f, feat, jvsource))
-            {
-              if (dbref.getMap() != null && f.getBegin() > 0
-                      && f.getEnd() > 0)
-              {
-                debug("mapping from " + f.getBegin() + " - " + f.getEnd());
-                SequenceFeature vf[] = null;
-
-                try
-                {
-                  vf = dbref.getMap().locateFeature(f);
-                } catch (Exception ex)
-                {
-                  Cache.log.warn(
-                          "Error in 'experimental' mapping of features. Please try to reproduce and then report info to jalview-discuss@jalview.org.");
-                  Cache.log.warn("Mapping feature from " + f.getBegin()
-                          + " to " + f.getEnd() + " in dbref "
-                          + dbref.getAccessionId() + " in "
-                          + dbref.getSource());
-                  Cache.log.warn("using das Source " + source);
-                  Cache.log.warn("Exception", ex);
-                }
-
-                if (vf != null)
-                {
-                  for (int v = 0; v < vf.length; v++)
-                  {
-                    debug("mapping to " + v + ": " + vf[v].getBegin()
-                            + " - " + vf[v].getEnd());
-                    sequence.addSequenceFeature(vf[v]);
-                  }
-                }
-              }
-              else
-              {
-                sequence.addSequenceFeature(f);
-              }
-            }
-          }
-        }
-        featuresAdded(sequences);
-      }
-      else
-      {
-        // System.out.println("No features found for " + seq.getName()
-        // + " from: " + e.getDasSource().getNickname());
-      }
-    }
-  }
-
-  private void setGuiNoDassourceActive()
-  {
-
-    if (af != null)
-    {
-      af.setProgressBar(
-              MessageManager.getString("status.no_das_sources_active"),
-              startTime);
-    }
-    if (getFeatSettings() != null)
-    {
-      fsettings.noDasSourceActive();
-    }
-  }
-
-  /**
-   * Update our fsettings dialog reference if we didn't have one when we were
-   * first initialised.
-   * 
-   * @return fsettings
-   */
-  private FeatureSettings getFeatSettings()
-  {
-    if (fsettings == null)
-    {
-      if (af != null)
-      {
-        fsettings = af.featureSettings;
-      }
-    }
-    return fsettings;
-  }
-
-  public void cancel()
-  {
-    if (af != null)
-    {
-      af.setProgressBar(MessageManager.getString(
-              "status.das_feature_fetching_cancelled"), startTime);
-    }
-    cancelled = true;
-  }
-
-  int sourcesRemaining = 0;
-
-  private boolean running = false;
-
-  private void setGuiFetchComplete()
-  {
-    running = false;
-    if (!cancelled && af != null)
-    {
-      // only update the progress bar if we've completed the fetch normally
-      af.setProgressBar(MessageManager.getString(
-              "status.das_feature_fetching_complete"), startTime);
-    }
-
-    if (af != null && af.featureSettings != null)
-    {
-      af.featureSettings.discoverAllFeatureData();
-    }
-
-    if (getFeatSettings() != null)
-    {
-      fsettings.complete();
-    }
-  }
-
-  void featuresAdded(Set<SequenceI> seqs)
-  {
-    if (af == null)
-    {
-      // no gui to update with features.
-      return;
-    }
-    af.getFeatureRenderer().featuresAdded();
-
-    int start = af.getViewport().getRanges().getStartSeq();
-    int end = af.getViewport().getRanges().getEndSeq();
-    int index;
-    for (index = start; index < end; index++)
-    {
-      for (SequenceI seq : seqs)
-      {
-        if (seq == af.getViewport().getAlignment().getSequenceAt(index)
-                .getDatasetSequence())
-        {
-          af.alignPanel.paintAlignment(true, true);
-          index = end;
-          break;
-        }
-      }
-    }
-  }
-
-  Object[] nextSequence(jalviewSourceI dasSource, SequenceI seq)
-  {
-    if (cancelled)
-    {
-      return null;
-    }
-    DBRefEntry[] uprefs = DBRefUtils.selectRefs(seq.getDBRefs(),
-            new String[]
-            {
-                // jalview.datamodel.DBRefSource.PDB,
-                DBRefSource.UNIPROT,
-            // jalview.datamodel.DBRefSource.EMBL - not tested on any EMBL coord
-            // sys sources
-            });
-    // TODO: minimal list of DAS queries to make by querying with untyped ID if
-    // distinct from any typed IDs
-
-    List<DBRefEntry> ids = new ArrayList<>();
-    List<String> qstring = new ArrayList<>();
-    boolean dasCoordSysFound = false;
-
-    if (uprefs != null)
-    {
-      // do any of these ids match the source's coordinate system ?
-      for (int j = 0; !dasCoordSysFound && j < uprefs.length; j++)
-      {
-
-        for (COORDINATES csys : dasSource.getVersion().getCOORDINATES())
-        {
-          if (DBRefUtils.isDasCoordinateSystem(csys.getAuthority(),
-                  uprefs[j]))
-          {
-            debug("Launched fetcher for coordinate system "
-                    + csys.getAuthority());
-            // Will have to pass any mapping information to the fetcher
-            // - the start/end for the DBRefEntry may not be the same as the
-            // sequence's start/end
-
-            System.out.println(
-                    seq.getName() + " " + (seq.getDatasetSequence() == null)
-                            + " " + csys.getUri());
-
-            dasCoordSysFound = true; // break's out of the loop
-            ids.add(uprefs[j]);
-            qstring.add(uprefs[j].getAccessionId());
-          }
-          else
-          {
-            System.out.println("IGNORE " + csys.getAuthority());
-          }
-        }
-      }
-    }
-
-    if (!dasCoordSysFound)
-    {
-      String id = null;
-      // try and use the name as the sequence id
-      if (seq.getName().indexOf("|") > -1)
-      {
-        id = seq.getName().substring(seq.getName().lastIndexOf("|") + 1);
-        if (id.trim().length() < 4)
-        {
-          // hack - we regard a significant ID as being at least 4
-          // non-whitespace characters
-          id = seq.getName().substring(0, seq.getName().lastIndexOf("|"));
-          if (id.indexOf("|") > -1)
-          {
-            id = id.substring(id.lastIndexOf("|") + 1);
-          }
-        }
-      }
-      else
-      {
-        id = seq.getName();
-      }
-      if (id != null)
-      {
-        DBRefEntry dbre = new DBRefEntry();
-        dbre.setAccessionId(id);
-        // Should try to call a general feature fetcher that
-        // queries many sources with name to discover applicable ID references
-        ids.add(dbre);
-        qstring.add(dbre.getAccessionId());
-      }
-    }
-
-    return new Object[] { ids, qstring };
-  }
-
-  /**
-   * examine the given sequence feature to determine if it should actually be
-   * turned into sequence annotation or database cross references rather than a
-   * simple sequence feature.
-   * 
-   * @param seq
-   *          the sequence to annotate
-   * @param f
-   *          the jalview sequence feature generated from the DAS feature
-   * @param map
-   *          the sequence feature attributes
-   * @param source
-   *          the source that emitted the feature
-   * @return true if feature was consumed as another kind of annotation.
-   */
-  protected boolean parseSeqFeature(SequenceI seq, SequenceFeature f,
-          FEATURE feature, jalviewSourceI source)
-  {
-    SequenceI mseq = seq;
-    while (seq.getDatasetSequence() != null)
-    {
-      seq = seq.getDatasetSequence();
-    }
-    if (f.getType() != null)
-    {
-      String type = f.getType();
-      if (type.equalsIgnoreCase("protein_name"))
-      {
-        // parse name onto the alignment sequence or the dataset sequence.
-        if (seq.getDescription() == null
-                || seq.getDescription().trim().length() == 0)
-        {
-          // could look at the note series to pick out the first long name, for
-          // the moment just use the whole description string
-          seq.setDescription(f.getDescription());
-        }
-        if (mseq.getDescription() == null
-                || mseq.getDescription().trim().length() == 0)
-        {
-          // could look at the note series to pick out the first long name, for
-          // the moment just use the whole description string
-          mseq.setDescription(f.getDescription());
-        }
-        return true;
-      }
-      // check if source has biosapiens or other sequence ontology label
-      if (type.equalsIgnoreCase("DBXREF") || type.equalsIgnoreCase("DBREF"))
-      {
-        // try to parse the accession out
-
-        DBRefEntry dbr = new DBRefEntry();
-        dbr.setVersion(source.getTitle());
-        StringTokenizer st = new StringTokenizer(f.getDescription(), ":");
-        if (st.hasMoreTokens())
-        {
-          dbr.setSource(st.nextToken());
-        }
-        if (st.hasMoreTokens())
-        {
-          dbr.setAccessionId(st.nextToken());
-        }
-        seq.addDBRef(dbr);
-
-        if (f.links != null && f.links.size() > 0)
-        {
-          // feature is also appended to enable links to be seen.
-          // TODO: consider extending dbrefs to have their own links ?
-          // TODO: new feature: extract dbref links from DAS servers and add the
-          // URL pattern to the list of DB name associated links in the user's
-          // preferences ?
-          // for the moment - just fix up the existing feature so it displays
-          // correctly.
-          // f.setType(dbr.getSource());
-          // f.setDescription();
-          f.setValue("linkonly", Boolean.TRUE);
-          // f.setDescription("");
-          Vector newlinks = new Vector();
-          Enumeration it = f.links.elements();
-          while (it.hasMoreElements())
-          {
-            String elm;
-            UrlLink urllink = new UrlLink(elm = (String) it.nextElement());
-            if (urllink.isValid())
-            {
-              urllink.setLabel(f.getDescription());
-              newlinks.addElement(urllink.toString());
-            }
-            else
-            {
-              // couldn't parse the link properly. Keep it anyway - just in
-              // case.
-              debug("couldn't parse link string - " + elm);
-              newlinks.addElement(elm);
-            }
-          }
-          f.links = newlinks;
-          seq.addSequenceFeature(f);
-        }
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * creates a jalview sequence feature from a das feature document
-   * 
-   * @param feat
-   * @return sequence feature object created using dasfeature information
-   */
-  SequenceFeature newSequenceFeature(FEATURE feat, String nickname)
-  {
-    if (feat == null)
-    {
-      return null;
-    }
-    try
-    {
-      /**
-       * Different qNames for a DAS Feature - are string keys to the HashMaps in
-       * features "METHOD") || qName.equals("TYPE") || qName.equals("START") ||
-       * qName.equals("END") || qName.equals("NOTE") || qName.equals("LINK") ||
-       * qName.equals("SCORE")
-       */
-      String desc = new String();
-      if (feat.getNOTE() != null)
-      {
-        for (String note : feat.getNOTE())
-        {
-          desc += note;
-        }
-      }
-
-      int start = 0, end = 0;
-      float score = 0f;
-
-      try
-      {
-        start = Integer.parseInt(feat.getSTART().toString());
-      } catch (Exception ex)
-      {
-      }
-      try
-      {
-        end = Integer.parseInt(feat.getEND().toString());
-      } catch (Exception ex)
-      {
-      }
-      try
-      {
-        Object scr = feat.getSCORE();
-        if (scr != null)
-        {
-          score = (float) Double.parseDouble(scr.toString());
-
-        }
-      } catch (Exception ex)
-      {
-      }
-
-      SequenceFeature f = new SequenceFeature(getTypeString(feat.getTYPE()),
-              desc, start, end, score, nickname);
-
-      if (feat.getLINK() != null)
-      {
-        for (LINK link : feat.getLINK())
-        {
-          // Do not put feature extent in link text for non-positional features
-          if (f.begin == 0 && f.end == 0)
-          {
-            f.addLink(f.getType() + " " + link.getContent() + "|"
-                    + link.getHref());
-          }
-          else
-          {
-            f.addLink(f.getType() + " " + f.begin + "_" + f.end + " "
-                    + link.getContent() + "|" + link.getHref());
-          }
-        }
-      }
-
-      return f;
-    } catch (Exception e)
-    {
-      System.out.println("ERRR " + e);
-      e.printStackTrace();
-      System.out.println("############");
-      debug("Failed to parse " + feat.toString(), e);
-      return null;
-    }
-  }
-
-  private String getTypeString(TYPE type)
-  {
-    return type.getContent();
-  }
-
-  public boolean isRunning()
-  {
-    return running;
-  }
-
-}
index a0b77de..29d4ec7 100644 (file)
@@ -29,12 +29,10 @@ import jalview.ws.dbsources.PfamFull;
 import jalview.ws.dbsources.PfamSeed;
 import jalview.ws.dbsources.RfamSeed;
 import jalview.ws.dbsources.Uniprot;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
 import jalview.ws.seqfetcher.ASequenceFetcher;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
 import java.util.ArrayList;
-import java.util.List;
 
 /**
  * This implements the run-time discovery of sequence database clients.
@@ -50,11 +48,6 @@ public class SequenceFetcher extends ASequenceFetcher
    */
   public SequenceFetcher()
   {
-    this(true);
-  }
-
-  public SequenceFetcher(boolean addDas)
-  {
     addDBRefSourceImpl(EnsemblGene.class);
     addDBRefSourceImpl(EnsemblGenomes.class);
     addDBRefSourceImpl(EmblSource.class);
@@ -64,26 +57,19 @@ public class SequenceFetcher extends ASequenceFetcher
     addDBRefSourceImpl(PfamFull.class);
     addDBRefSourceImpl(PfamSeed.class);
     addDBRefSourceImpl(RfamSeed.class);
-
-    if (addDas)
-    {
-      registerDasSequenceSources();
-    }
   }
 
   /**
-   * return an ordered list of database sources where non-das database classes
-   * appear before das database classes
+   * return an ordered list of database sources excluding alignment only databases
    */
   public String[] getOrderedSupportedSources()
   {
     String[] srcs = this.getSupportedDb();
-    ArrayList<String> dassrc = new ArrayList<String>(),
-            nondas = new ArrayList<String>();
+    ArrayList<String> src = new ArrayList<>();
+
     for (int i = 0; i < srcs.length; i++)
     {
-      boolean das = false, skip = false;
-      String nm;
+      boolean skip = false;
       for (DbSourceProxy dbs : getSourceProxy(srcs[i]))
       {
         // Skip the alignment databases for the moment - they're not useful for
@@ -92,86 +78,28 @@ public class SequenceFetcher extends ASequenceFetcher
         {
           skip = true;
         }
-        else
-        {
-          nm = dbs.getDbName();
-          if (getSourceProxy(
-                  srcs[i]) instanceof jalview.ws.dbsources.das.datamodel.DasSequenceSource)
-          {
-            if (nm.startsWith("das:"))
-            {
-              nm = nm.substring(4);
-              das = true;
-            }
-            break;
-          }
-        }
       }
       if (skip)
       {
         continue;
       }
-      if (das)
       {
-        dassrc.add(srcs[i]);
-      }
-      else
-      {
-        nondas.add(srcs[i]);
+        src.add(srcs[i]);
       }
     }
-    String[] tosort = nondas.toArray(new String[0]),
-            sorted = nondas.toArray(new String[0]);
+    String[] tosort = src.toArray(new String[0]),
+            sorted = src.toArray(new String[0]);
     for (int j = 0, jSize = sorted.length; j < jSize; j++)
     {
       tosort[j] = tosort[j].toLowerCase();
     }
     jalview.util.QuickSort.sort(tosort, sorted);
     // construct array with all sources listed
-
-    srcs = new String[sorted.length + dassrc.size()];
     int i = 0;
     for (int j = sorted.length - 1; j >= 0; j--, i++)
     {
       srcs[i] = sorted[j];
-      sorted[j] = null;
-    }
-
-    sorted = dassrc.toArray(new String[0]);
-    tosort = dassrc.toArray(new String[0]);
-    for (int j = 0, jSize = sorted.length; j < jSize; j++)
-    {
-      tosort[j] = tosort[j].toLowerCase();
-    }
-    jalview.util.QuickSort.sort(tosort, sorted);
-    for (int j = sorted.length - 1; j >= 0; j--, i++)
-    {
-      srcs[i] = sorted[j];
     }
     return srcs;
   }
-
-  /**
-   * query the currently defined DAS source registry for sequence sources and
-   * add a DasSequenceSource instance for each source to the SequenceFetcher
-   * source list.
-   */
-  public void registerDasSequenceSources()
-  {
-    // TODO: define a context as a registry provider (either desktop,
-    // jalview.bin.cache, or something else).
-    for (jalviewSourceI source : jalview.bin.Cache.getDasSourceRegistry()
-            .getSources())
-    {
-      if (source.isSequenceSource())
-      {
-        List<DbSourceProxy> dassources = source.getSequenceSourceProxies();
-        for (DbSourceProxy seqsrc : dassources)
-        {
-          addDbRefSourceImpl(seqsrc);
-        }
-      }
-    }
-  }
-
 }
diff --git a/src/jalview/ws/dbsources/das/api/DasSourceRegistryI.java b/src/jalview/ws/dbsources/das/api/DasSourceRegistryI.java
deleted file mode 100644 (file)
index 55c50b2..0000000
+++ /dev/null
@@ -1,57 +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.ws.dbsources.das.api;
-
-import java.util.List;
-
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-
-/**
- * API for a registry that provides datasources that jalview can access
- * 
- * @author jprocter
- * 
- */
-public interface DasSourceRegistryI
-{
-
-  List<jalviewSourceI> getSources();
-
-  String getDasRegistryURL();
-
-  jalviewSourceI getSource(String nickname);
-
-  // TODO: re JAL-424 - introduce form where local source is queried for
-  // metadata, rather than have it all provided by caller.
-  jalviewSourceI createLocalSource(String uri, String name,
-          boolean sequence, boolean features);
-
-  boolean removeLocalSource(jalviewSourceI source);
-
-  void refreshSources();
-
-  String getLocalSourceString();
-
-  List<jalviewSourceI> resolveSourceNicknames(List<String> sources);
-
-  // TODO: refactor to jDAS specific interface
-  MultipleConnectionPropertyProviderI getSessionHandler();
-}
diff --git a/src/jalview/ws/dbsources/das/api/jalviewSourceI.java b/src/jalview/ws/dbsources/das/api/jalviewSourceI.java
deleted file mode 100644 (file)
index bc3c8ea..0000000
+++ /dev/null
@@ -1,87 +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.ws.dbsources.das.api;
-
-import jalview.ws.seqfetcher.DbSourceProxy;
-
-import java.util.List;
-
-import org.biodas.jdas.schema.sources.MAINTAINER;
-import org.biodas.jdas.schema.sources.VERSION;
-
-public interface jalviewSourceI
-{
-
-  String getTitle();
-
-  VERSION getVersion();
-
-  String getDocHref();
-
-  String getDescription();
-
-  String getUri();
-
-  MAINTAINER getMAINTAINER();
-
-  String getEmail();
-
-  boolean isLocal();
-
-  boolean isSequenceSource();
-
-  String[] getCapabilityList(VERSION v);
-
-  String[] getLabelsFor(VERSION v);
-
-  /**
-   * 
-   * @return null if not a sequence source, otherwise a series of database
-   *         sources that can be used to retrieve sequence data for particular
-   *         database coordinate systems
-   */
-  List<DbSourceProxy> getSequenceSourceProxies();
-
-  boolean isFeatureSource();
-
-  /**
-   * returns the base URL for the latest version of a source's DAS endpoint set
-   * 
-   * @return
-   */
-  String getSourceURL();
-
-  /**
-   * test to see if this source's latest version is older than the given source
-   * 
-   * @param jalviewSourceI
-   * @return true if newer than given source
-   */
-  boolean isNewerThan(jalviewSourceI jalviewSourceI);
-
-  /**
-   * test if the source is a reference source for the authority
-   * 
-   * @return
-   */
-  boolean isReferenceSource();
-
-}
diff --git a/src/jalview/ws/dbsources/das/datamodel/DasSequenceSource.java b/src/jalview/ws/dbsources/das/datamodel/DasSequenceSource.java
deleted file mode 100644 (file)
index 84f6d4d..0000000
+++ /dev/null
@@ -1,354 +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.ws.dbsources.das.datamodel;
-
-import jalview.bin.Cache;
-import jalview.datamodel.Alignment;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceI;
-import jalview.util.MessageManager;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-import jalview.ws.seqfetcher.DbSourceProxy;
-import jalview.ws.seqfetcher.DbSourceProxyImpl;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
-import org.biodas.jdas.client.SequenceClient;
-import org.biodas.jdas.client.adapters.sequence.DasSequenceAdapter;
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-import org.biodas.jdas.client.threads.SequenceClientMultipleSources;
-import org.biodas.jdas.schema.sequence.SEQUENCE;
-import org.biodas.jdas.schema.sources.COORDINATES;
-import org.biodas.jdas.schema.sources.SOURCE;
-import org.biodas.jdas.schema.sources.VERSION;
-
-import com.stevesoft.pat.Regex;
-
-/**
- * an instance of this class is created for each unique DAS Sequence source (ie
- * one capable of handling the 'sequence' for a particular MapMaster)
- * 
- * @author JimP
- * 
- */
-public class DasSequenceSource extends DbSourceProxyImpl
-        implements DbSourceProxy
-{
-  private jalviewSourceI jsrc;
-
-  protected SOURCE source = null;
-
-  protected VERSION version = null;
-
-  protected COORDINATES coordsys = null;
-
-  protected String dbname = "DASCS";
-
-  protected String dbrefname = "das:source";
-
-  protected MultipleConnectionPropertyProviderI connprops = null;
-
-  /**
-   * DAS sources are tier 1 - if we have a direct DB connection then we should
-   * prefer it
-   */
-  private int tier = 1;
-
-  /**
-   * create a new DbSource proxy for a DAS 1 source
-   * 
-   * @param dbnbame
-   *          Human Readable Name to use when fetching from this source
-   * @param dbrefname
-   *          DbRefName for DbRefs attached to sequences retrieved from this
-   *          source
-   * @param source
-   *          Das1Source
-   * @param coordsys
-   *          specific coordinate system to use for this source
-   * @throws Exception
-   *           if source is not capable of the 'sequence' command
-   */
-  public DasSequenceSource(String dbname, String dbrefname, SOURCE source,
-          VERSION version, COORDINATES coordsys,
-          MultipleConnectionPropertyProviderI connprops) throws Exception
-  {
-    if (!(jsrc = new JalviewSource(source, connprops, false))
-            .isSequenceSource())
-    {
-      throw new Exception(MessageManager.formatMessage(
-              "exception.das_source_doesnt_support_sequence_command",
-              new String[]
-              { source.getTitle() }));
-    }
-    this.tier = 1 + ((jsrc.isLocal() || jsrc.isReferenceSource()) ? 0 : 1);
-    this.source = source;
-    this.dbname = dbname;
-    this.dbrefname = dbrefname.toUpperCase();
-    if (coordsys != null)
-    {
-      this.coordsys = coordsys;
-    }
-    this.connprops = connprops;
-  }
-
-  public String getAccessionSeparator()
-  {
-    return "\t";
-  }
-
-  public Regex getAccessionValidator()
-  {
-    /** ? * */
-    return Regex.perlCode("m/([^:]+)(:\\d+,\\d+)?/");
-  }
-
-  public String getDbName()
-  {
-    // TODO: map to
-    return dbname + " (DAS)";
-  }
-
-  public String getDbSource()
-  {
-    return dbrefname;
-  }
-
-  public String getDbVersion()
-  {
-    return coordsys != null ? coordsys.getVersion() : "";
-  }
-
-  public AlignmentI getSequenceRecords(String queries) throws Exception
-  {
-    StringTokenizer st = new StringTokenizer(queries, "\t");
-    List<String> toks = new ArrayList<String>(),
-            src = new ArrayList<String>(), acIds = new ArrayList<String>();
-    while (st.hasMoreTokens())
-    {
-      String t;
-      toks.add(t = st.nextToken());
-      acIds.add(t.replaceAll(":[0-9,]+", ""));
-    }
-    src.add(jsrc.getSourceURL());
-    Map<String, Map<List<String>, DasSequenceAdapter>> resultset = new HashMap<String, Map<List<String>, DasSequenceAdapter>>();
-    Map<String, Map<List<String>, Exception>> errors = new HashMap<String, Map<List<String>, Exception>>();
-
-    // First try multiple sources
-    boolean multiple = true, retry = false;
-    do
-    {
-      if (!multiple)
-      {
-        retry = false;
-        // slow, fetch one at a time.
-        for (String sr : src)
-        {
-          System.err.println(
-                  "Retrieving IDs individually from das source: " + sr);
-          org.biodas.jdas.client.SequenceClient sq = new SequenceClient(
-                  connprops.getConnectionPropertyProviderFor(sr));
-          for (String q : toks)
-          {
-            List<String> qset = Arrays.asList(new String[] { q });
-            try
-            {
-              DasSequenceAdapter s = sq.fetchData(sr, qset);
-              Map<List<String>, DasSequenceAdapter> dss = resultset.get(sr);
-              if (dss == null)
-              {
-                resultset.put(sr,
-                        dss = new HashMap<List<String>, DasSequenceAdapter>());
-              }
-              dss.put(qset, s);
-            } catch (Exception x)
-            {
-              Map<List<String>, Exception> ers = errors.get(sr);
-              if (ers == null)
-              {
-                errors.put(sr,
-                        ers = new HashMap<List<String>, Exception>());
-              }
-              ers.put(qset, x);
-            }
-          }
-        }
-      }
-      else
-      {
-        SequenceClientMultipleSources sclient;
-        sclient = new SequenceClientMultipleSources();
-        sclient.fetchData(src, toks, resultset, errors);
-        sclient.shutDown();
-        while (!sclient.isTerminated())
-        {
-          try
-          {
-            Thread.sleep(200);
-
-          } catch (InterruptedException x)
-          {
-          }
-        }
-        if (resultset.isEmpty() && !errors.isEmpty())
-        {
-          retry = true;
-          multiple = false;
-        }
-      }
-    } while (retry);
-
-    if (resultset.isEmpty())
-    {
-      System.err.println("Sequence Query to " + jsrc.getTitle() + " with '"
-              + queries + "' returned no sequences.");
-      return null;
-    }
-    else
-    {
-      Vector<SequenceI> seqs = null;
-      for (Map.Entry<String, Map<List<String>, DasSequenceAdapter>> resset : resultset
-              .entrySet())
-      {
-        for (Map.Entry<List<String>, DasSequenceAdapter> result : resset
-                .getValue().entrySet())
-        {
-          DasSequenceAdapter dasseqresp = result.getValue();
-          List<String> accessions = result.getKey();
-          for (SEQUENCE e : dasseqresp.getSequence())
-          {
-            String lbl = e.getId();
-
-            if (acIds.indexOf(lbl) == -1)
-            {
-              System.err.println(
-                      "Warning - received sequence event for strange accession code ("
-                              + lbl + ")");
-            }
-            else
-            {
-              if (seqs == null)
-              {
-                if (e.getContent().length() == 0)
-                {
-                  System.err.println(
-                          "Empty sequence returned for accession code ("
-                                  + lbl + ") from " + resset.getKey()
-                                  + " (source is " + getDbName());
-                  continue;
-                }
-              }
-              seqs = new java.util.Vector<SequenceI>();
-              // JDAS returns a sequence complete with any newlines and spaces
-              // in the XML
-              Sequence sq = new Sequence(lbl,
-                      e.getContent().replaceAll("\\s+", ""));
-              sq.setStart(e.getStart().intValue());
-              sq.addDBRef(new DBRefEntry(getDbSource(),
-                      getDbVersion() + ":" + e.getVersion(), lbl));
-              seqs.addElement(sq);
-            }
-          }
-        }
-      }
-
-      if (seqs == null || seqs.size() == 0)
-        return null;
-      SequenceI[] sqs = new SequenceI[seqs.size()];
-      for (int i = 0, iSize = seqs.size(); i < iSize; i++)
-      {
-        sqs[i] = (SequenceI) seqs.elementAt(i);
-      }
-      Alignment al = new Alignment(sqs);
-      if (jsrc.isFeatureSource())
-      {
-        java.util.Vector<jalviewSourceI> srcs = new java.util.Vector<jalviewSourceI>();
-        srcs.addElement(jsrc);
-        try
-        {
-          jalview.ws.DasSequenceFeatureFetcher dssf = new jalview.ws.DasSequenceFeatureFetcher(
-                  sqs, null, srcs, false, false, multiple);
-          while (dssf.isRunning())
-          {
-            try
-            {
-              Thread.sleep(200);
-            } catch (InterruptedException x)
-            {
-
-            }
-          }
-
-        } catch (Exception x)
-        {
-          Cache.log.error(
-                  "Couldn't retrieve features for sequence from its source.",
-                  x);
-        }
-      }
-
-      return al;
-    }
-  }
-
-  public String getTestQuery()
-  {
-    return coordsys == null ? "" : coordsys.getTestRange();
-  }
-
-  public boolean isValidReference(String accession)
-  {
-    // TODO try to validate an accession against source
-    // We don't really know how to do this without querying source
-
-    return true;
-  }
-
-  /**
-   * @return the source
-   */
-  public SOURCE getSource()
-  {
-    return source;
-  }
-
-  /**
-   * @return the coordsys
-   */
-  public COORDINATES getCoordsys()
-  {
-    return coordsys;
-  }
-
-  @Override
-  public int getTier()
-  {
-    return tier;
-  }
-}
diff --git a/src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java b/src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java
deleted file mode 100644 (file)
index de7f380..0000000
+++ /dev/null
@@ -1,484 +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.ws.dbsources.das.datamodel;
-
-import jalview.bin.Cache;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.StringTokenizer;
-
-import org.biodas.jdas.client.ConnectionPropertyProviderI;
-import org.biodas.jdas.client.SourcesClient;
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-import org.biodas.jdas.dassources.Capabilities;
-import org.biodas.jdas.schema.sources.CAPABILITY;
-import org.biodas.jdas.schema.sources.SOURCE;
-import org.biodas.jdas.schema.sources.SOURCES;
-import org.biodas.jdas.schema.sources.VERSION;
-
-/**
- *
- */
-public class DasSourceRegistry
-        implements DasSourceRegistryI, MultipleConnectionPropertyProviderI
-{
-  // private org.biodas.jdas.schema.sources.SOURCE[] dasSources = null;
-  private List<jalviewSourceI> dasSources = null;
-
-  private Hashtable<String, jalviewSourceI> sourceNames = null;
-
-  private Hashtable<String, jalviewSourceI> localSources = null;
-
-  // public static String DEFAULT_REGISTRY = "http://www.dasregistry.org/das/";
-  public static String DEFAULT_REGISTRY = "http://www.ebi.ac.uk/das-srv/registry/das/";
-
-  /**
-   * true if thread is running and we are talking to DAS registry service
-   */
-  private boolean loadingDasSources = false;
-
-  public boolean isLoadingDasSources()
-  {
-    return loadingDasSources;
-  }
-
-  @Override
-  public String getDasRegistryURL()
-  {
-    String registry = jalview.bin.Cache.getDefault("DAS_REGISTRY_URL",
-            DEFAULT_REGISTRY);
-
-    if (registry.indexOf("/registry/das1/sources/") > -1)
-    {
-      jalview.bin.Cache.setProperty(jalview.bin.Cache.DAS_REGISTRY_URL,
-              DEFAULT_REGISTRY);
-      registry = DEFAULT_REGISTRY;
-    }
-    if (registry.lastIndexOf("sources.xml") == registry.length() - 11)
-    {
-      // no trailing sources.xml document for registry in JDAS
-      jalview.bin.Cache.setProperty(jalview.bin.Cache.DAS_REGISTRY_URL,
-              registry = registry.substring(0,
-                      registry.lastIndexOf("sources.xml")));
-    }
-    return registry;
-  }
-
-  /**
-   * query the default DAS Source Registry for sources. Uses value of jalview
-   * property DAS_REGISTRY_URL and the DasSourceBrowser.DEFAULT_REGISTRY if that
-   * doesn't exist.
-   * 
-   * @return list of sources
-   */
-  private List<jalviewSourceI> getDASSources()
-  {
-
-    return getDASSources(getDasRegistryURL(), this);
-  }
-
-  /**
-   * query the given URL for DasSources.
-   * 
-   * @param registryURL
-   *          return sources from registryURL
-   */
-  private static List<jalviewSourceI> getDASSources(String registryURL,
-          MultipleConnectionPropertyProviderI registry)
-  {
-    try
-    {
-      URL url = new URL(registryURL);
-      org.biodas.jdas.client.SourcesClientInterface client = new SourcesClient();
-
-      SOURCES sources = client.fetchDataRegistry(registryURL, null, null,
-              null, null, null, null);
-
-      List<SOURCE> dassources = sources.getSOURCE();
-      ArrayList<jalviewSourceI> dsrc = new ArrayList<jalviewSourceI>();
-      HashMap<String, Integer> latests = new HashMap<String, Integer>();
-      Integer latest;
-      for (SOURCE src : dassources)
-      {
-        JalviewSource jsrc = new JalviewSource(src, registry, false);
-        latest = latests.get(jsrc.getSourceURL());
-        if (latest != null)
-        {
-          if (jsrc.isNewerThan(dsrc.get(latest.intValue())))
-          {
-            dsrc.set(latest.intValue(), jsrc);
-          }
-          else
-          {
-            System.out.println(
-                    "Debug: Ignored older source " + jsrc.getTitle());
-          }
-        }
-        else
-        {
-          latests.put(jsrc.getSourceURL(), Integer.valueOf(dsrc.size()));
-          dsrc.add(jsrc);
-        }
-      }
-      return dsrc;
-    } catch (Exception ex)
-    {
-      System.out.println(
-              "DAS1 registry at " + registryURL + " no longer exists");
-      return new ArrayList<jalviewSourceI>();
-    }
-  }
-
-  public void run()
-  {
-    getSources();
-  }
-
-  @Override
-  public List<jalviewSourceI> getSources()
-  {
-    if (dasSources == null)
-    {
-      dasSources = getDASSources();
-    }
-    return appendLocalSources();
-  }
-
-  /**
-   * generate Sources from the local das source list
-   * 
-   */
-  private void addLocalDasSources()
-  {
-    if (localSources == null)
-    {
-      // get local sources from properties and initialise the local source list
-      String local = jalview.bin.Cache.getProperty("DAS_LOCAL_SOURCE");
-
-      if (local != null)
-      {
-        int n = 1;
-        StringTokenizer st = new StringTokenizer(local, "\t");
-        while (st.hasMoreTokens())
-        {
-          String token = st.nextToken();
-          int bar = token.indexOf("|");
-          if (bar == -1)
-          {
-            System.err.println(
-                    "Warning: DAS user local source appears to have no nickname (expected a '|' followed by nickname)\nOffending definition: '"
-                            + token + "'");
-          }
-          String url = token.substring(bar + 1);
-          boolean features = true, sequence = false;
-          if (url.startsWith("sequence:"))
-          {
-            url = url.substring(9);
-            // this source also serves sequences as well as features
-            sequence = true;
-          }
-          try
-          {
-            if (bar > -1)
-            {
-              createLocalSource(url, token.substring(0, bar), sequence,
-                      features);
-            }
-            else
-            {
-              createLocalSource(url, "User Source" + n, sequence, features);
-            }
-          } catch (Exception q)
-          {
-            System.err.println(
-                    "Unexpected exception when creating local source from '"
-                            + token + "'");
-            q.printStackTrace();
-          }
-          n++;
-        }
-      }
-    }
-  }
-
-  private List<jalviewSourceI> appendLocalSources()
-  {
-    List<jalviewSourceI> srclist = new ArrayList<jalviewSourceI>();
-    addLocalDasSources();
-    sourceNames = new Hashtable<String, jalviewSourceI>();
-    if (dasSources != null)
-    {
-      for (jalviewSourceI src : dasSources)
-      {
-        sourceNames.put(src.getTitle(), src);
-        srclist.add(src);
-      }
-    }
-
-    if (localSources == null)
-    {
-      return srclist;
-    }
-    Enumeration en = localSources.keys();
-    while (en.hasMoreElements())
-    {
-      String key = en.nextElement().toString();
-      jalviewSourceI jvsrc = localSources.get(key);
-      sourceNames.put(key, jvsrc);
-      srclist.add(jvsrc);
-    }
-    return srclist;
-  }
-
-  /*
-  * 
-  */
-
-  @Override
-  public jalviewSourceI createLocalSource(String url, String name,
-          boolean sequence, boolean features)
-  {
-    SOURCE local = _createLocalSource(url, name, sequence, features);
-
-    if (localSources == null)
-    {
-      localSources = new Hashtable<String, jalviewSourceI>();
-    }
-    jalviewSourceI src = new JalviewSource(local, this, true);
-    localSources.put(local.getTitle(), src);
-    return src;
-  }
-
-  private SOURCE _createLocalSource(String url, String name,
-          boolean sequence, boolean features)
-  {
-    SOURCE local = new SOURCE();
-
-    local.setUri(url);
-    local.setTitle(name);
-    local.setVERSION(new ArrayList<VERSION>());
-    VERSION v = new VERSION();
-    List<CAPABILITY> cp = new ArrayList<CAPABILITY>();
-    if (sequence)
-    {
-      /*
-       * Could try and synthesize a coordinate system for the source if needbe
-       * COORDINATES coord = new COORDINATES(); coord.setAuthority("NCBI");
-       * coord.setSource("Chromosome"); coord.setTaxid("9606");
-       * coord.setVersion("35"); version.getCOORDINATES().add(coord);
-       */
-      CAPABILITY cap = new CAPABILITY();
-      cap.setType("das1:" + Capabilities.SEQUENCE.getName());
-      cap.setQueryUri(url + "/sequence");
-      cp.add(cap);
-    }
-    if (features)
-    {
-      CAPABILITY cap = new CAPABILITY();
-      cap.setType("das1:" + Capabilities.FEATURES.getName());
-      cap.setQueryUri(url + "/features");
-      cp.add(cap);
-    }
-
-    v.getCAPABILITY().addAll(cp);
-    local.getVERSION().add(v);
-
-    return local;
-  }
-
-  @Override
-  public jalviewSourceI getSource(String nickname)
-  {
-    return sourceNames.get(nickname);
-  }
-
-  @Override
-  public boolean removeLocalSource(jalviewSourceI source)
-  {
-    if (localSources.containsValue(source))
-    {
-      localSources.remove(source.getTitle());
-      sourceNames.remove(source.getTitle());
-      dasSources.remove(source);
-      jalview.bin.Cache.setProperty("DAS_LOCAL_SOURCE",
-              getLocalSourceString());
-
-      return true;
-    }
-    return false;
-  }
-
-  @Override
-  public void refreshSources()
-  {
-    dasSources = null;
-    sourceNames = null;
-    run();
-  }
-
-  @Override
-  public List<jalviewSourceI> resolveSourceNicknames(List<String> sources)
-  {
-    ArrayList<jalviewSourceI> resolved = new ArrayList<jalviewSourceI>();
-    if (sourceNames != null)
-    {
-      for (String src : sources)
-      {
-        jalviewSourceI dsrc = sourceNames.get(src);
-        if (dsrc != null)
-        {
-          resolved.add(dsrc);
-        }
-      }
-    }
-    return resolved;
-  }
-
-  @Override
-  public String getLocalSourceString()
-  {
-    if (localSources != null)
-    {
-      StringBuffer sb = new StringBuffer();
-      Enumeration en = localSources.keys();
-      while (en.hasMoreElements())
-      {
-        String token = en.nextElement().toString();
-        jalviewSourceI srco = localSources.get(token);
-        sb.append(token + "|" + (srco.isSequenceSource() ? "sequence:" : "")
-                + srco.getUri() + "\t");
-      }
-      return sb.toString();
-    }
-    return "";
-  }
-
-  private static final Hashtable<URL, String> authStash;
-  static
-  {
-    authStash = new Hashtable<URL, String>();
-
-    try
-    {
-      // TODO: allow same credentials for https and http
-      authStash.put(
-              new URL("http://www.compbio.dundee.ac.uk/geneweb/das/myseq/"),
-              "Basic SmltOm1pSg==");
-    } catch (MalformedURLException e)
-    {
-      // TODO Auto-generated catch block
-      e.printStackTrace();
-    }
-  }
-
-  @Override
-  public MultipleConnectionPropertyProviderI getSessionHandler()
-  {
-    return this;
-  }
-
-  @Override
-  public ConnectionPropertyProviderI getConnectionPropertyProviderFor(
-          String arg0)
-  {
-
-    final ConnectionPropertyProviderI conprov = new ConnectionPropertyProviderI()
-    {
-      boolean authed = false;
-
-      @Override
-      public void setConnectionProperties(HttpURLConnection connection)
-      {
-        String auth = authStash.get(connection.getURL());
-        if (auth != null && auth.length() > 0)
-        {
-          connection.setRequestProperty("Authorisation", auth);
-          authed = true;
-        }
-        else
-        {
-          authed = false;
-        }
-      }
-
-      @Override
-      public boolean getResponseProperties(HttpURLConnection connection)
-      {
-        String auth = authStash.get(connection.getURL());
-        if (auth != null && auth.length() == 0)
-        {
-          // don't attempt to check if we authed or not - user entered empty
-          // password
-          return false;
-        }
-        if (!authed)
-        {
-          if (auth != null)
-          {
-            // try and pass credentials.
-            return true;
-          }
-          // see if we should try and create a new auth record.
-          String ameth = connection.getHeaderField("X-DAS-AuthMethods");
-          Cache.log.debug("Could authenticate to " + connection.getURL()
-                  + " with : " + ameth);
-          // TODO: search auth string and raise login box - return if auth was
-          // provided
-          return false;
-        }
-        else
-        {
-          // check to see if auth was successful
-          String asuc = connection
-                  .getHeaderField("X-DAS_AuthenticatedUser");
-          if (asuc != null && asuc.trim().length() > 0)
-          {
-            // authentication was successful
-            Cache.log.debug("Authenticated successfully to "
-                    + connection.getURL().toString());
-            return false;
-          }
-          // it wasn't - so we should tell the user it failed and ask if they
-          // want to attempt authentication again.
-          authStash.remove(connection.getURL());
-          // open a new login/password dialog with cancel button
-          // set new authStash content with password and return true
-          return true; //
-          // User cancelled auth - so put empty string in stash to indicate we
-          // don't want to auth with this server.
-          // authStash.put(connection.getURL(), "");
-          // return false;
-        }
-      }
-    };
-    return conprov;
-  }
-
-}
diff --git a/src/jalview/ws/dbsources/das/datamodel/JalviewSource.java b/src/jalview/ws/dbsources/das/datamodel/JalviewSource.java
deleted file mode 100644 (file)
index 07ba027..0000000
+++ /dev/null
@@ -1,384 +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.ws.dbsources.das.datamodel;
-
-import jalview.util.MessageManager;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-import jalview.ws.seqfetcher.DbSourceProxy;
-
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-import org.biodas.jdas.dassources.Capabilities;
-import org.biodas.jdas.dassources.utils.DasTimeFormat;
-import org.biodas.jdas.schema.sources.CAPABILITY;
-import org.biodas.jdas.schema.sources.COORDINATES;
-import org.biodas.jdas.schema.sources.MAINTAINER;
-import org.biodas.jdas.schema.sources.PROP;
-import org.biodas.jdas.schema.sources.SOURCE;
-import org.biodas.jdas.schema.sources.VERSION;
-
-public class JalviewSource implements jalviewSourceI
-{
-  SOURCE source;
-
-  MultipleConnectionPropertyProviderI connprov;
-
-  public JalviewSource(SOURCE local2,
-          MultipleConnectionPropertyProviderI connprov, boolean local)
-  {
-    this.connprov = connprov;
-    this.local = local;
-    source = local2;
-  }
-
-  @Override
-  public String getTitle()
-  {
-    return source.getTitle();
-  }
-
-  @Override
-  public VERSION getVersion()
-  {
-
-    return getVersionFor(source);
-  }
-
-  @Override
-  public String getDocHref()
-  {
-    return source.getDocHref();
-  }
-
-  @Override
-  public String getDescription()
-  {
-    return source.getDescription();
-  }
-
-  @Override
-  public String getUri()
-  {
-    return source.getUri();
-  }
-
-  @Override
-  public MAINTAINER getMAINTAINER()
-  {
-    return source.getMAINTAINER();
-  }
-
-  @Override
-  public String getEmail()
-  {
-    return (local) ? null : source.getMAINTAINER().getEmail();
-  }
-
-  boolean local = false;
-
-  @Override
-  public boolean isLocal()
-  {
-    return local;
-  }
-
-  @Override
-  public boolean isSequenceSource()
-  {
-    String seqcap = "das1:" + Capabilities.SEQUENCE.getName();
-    for (String cp : getCapabilityList(getVersionFor(source)))
-    {
-      if (cp.equals(seqcap))
-      {
-        return true;
-
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public boolean isFeatureSource()
-  {
-    String seqcap = "das1:" + Capabilities.FEATURES.getName();
-    for (String cp : getCapabilityList(getVersionFor(source)))
-    {
-      if (cp.equals(seqcap))
-      {
-        return true;
-
-      }
-    }
-    return false;
-  }
-
-  private VERSION getVersionFor(SOURCE ds)
-  {
-    VERSION latest = null;
-    for (VERSION v : ds.getVERSION())
-    {
-      if (latest == null
-              || isLaterThan(latest.getCreated(), v.getCreated()))
-      {
-        // TODO: das 1.6 - should just get the first version - ignore other
-        // versions since not specified how to construct URL from version's URI
-        // + source URI
-        latest = v;
-      }
-    }
-    return latest;
-  }
-
-  /**
-   * compare date strings. null or unparseable dates are assumed to be oldest
-   * 
-   * @param ref
-   * @param newer
-   * @return true iff ref comes before newer
-   */
-  private boolean isLaterThan(String ref, String newer)
-  {
-    Date refdate = null, newdate = null;
-    if (ref != null && ref.trim().length() > 0)
-    {
-      try
-      {
-        refdate = DasTimeFormat.fromDASString(ref.trim());
-
-      } catch (ParseException x)
-      {
-      }
-    }
-    if (newer != null && newer.trim().length() > 0)
-    {
-      try
-      {
-        newdate = DasTimeFormat.fromDASString(newer);
-      } catch (ParseException e)
-      {
-      }
-    }
-    if (refdate != null)
-    {
-      if (newdate != null)
-      {
-        return refdate.before(newdate);
-      }
-      return false;
-    }
-    if (newdate != null)
-    {
-      return true;
-    }
-    // assume first instance of source is newest in list. - TODO: check if
-    // natural ordering of source versions is newest first or oldest first
-    return false;
-  }
-
-  public String[] getLabelsFor(VERSION v)
-  {
-    ArrayList<String> labels = new ArrayList<String>();
-    for (PROP p : v.getPROP())
-    {
-      if (p.getName().equalsIgnoreCase("LABEL"))
-      {
-        labels.add(p.getValue());
-      }
-    }
-    return labels.toArray(new String[0]);
-  }
-
-  private CAPABILITY getCapability(Capabilities capability)
-  {
-    for (CAPABILITY p : getVersion().getCAPABILITY())
-    {
-      if (p.getType().equalsIgnoreCase(capability.getName()) || p.getType()
-              .equalsIgnoreCase("das1:" + capability.getName()))
-      {
-        return p;
-      }
-    }
-    return null;
-  }
-
-  public String[] getCapabilityList(VERSION v)
-  {
-
-    ArrayList<String> labels = new ArrayList<String>();
-    for (CAPABILITY p : v.getCAPABILITY())
-    {
-      // TODO: work out what to do with namespace prefix
-      // does SEQUENCE == das1:SEQUENCE and das2:SEQUENCE ?
-      // for moment, just show all capabilities...
-      if (p.getType().startsWith("das1:"))
-      {
-        labels.add(p.getType());
-      }
-    }
-    return labels.toArray(new String[0]);
-  }
-
-  @Override
-  public List<DbSourceProxy> getSequenceSourceProxies()
-  {
-    if (!isSequenceSource())
-    {
-      return null;
-    }
-    ArrayList<DbSourceProxy> seqsources = new ArrayList<DbSourceProxy>();
-    if (!local)
-    {
-      VERSION v = getVersion();
-      Map<String, COORDINATES> latestc = new Hashtable<String, COORDINATES>();
-      for (COORDINATES cs : v.getCOORDINATES())
-      {
-        COORDINATES ltst = latestc.get(cs.getUri());
-        if (ltst == null || ltst.getVersion() == null
-                || (ltst.getVersion() != null && cs.getVersion() != null
-                        && isLaterThan(ltst.getVersion(), cs.getVersion())))
-        {
-          latestc.put(cs.getUri(), cs);
-        }
-      }
-      for (COORDINATES cs : latestc.values())
-      {
-        DasSequenceSource ds;
-        /*
-         * if (css == null || css.length == 0) { // TODO: query das source
-         * directly to identify coordinate system... or // have to make up a
-         * coordinate system css = new DasCoordinateSystem[] { new
-         * DasCoordinateSystem() }; css[0].setName(d1s.getNickname());
-         * css[0].setUniqueId(d1s.getNickname()); } for (int c = 0; c <
-         * css.length; c++) {
-         */
-        try
-        {
-          seqsources.add(ds = new DasSequenceSource(
-                  getTitle() + " (" + cs.getAuthority() + " "
-                          + cs.getSource()
-                          + (cs.getVersion() != null ? " " + cs.getVersion()
-                                  : "")
-                          + ")",
-                  cs.getAuthority(), source, v, cs, connprov));
-          if (seqsources.size() > 1)
-          {
-            System.err.println("Added another sequence DB source for "
-                    + getTitle() + " (" + ds.getDbName() + ")");
-          }
-        } catch (Exception e)
-        {
-          System.err.println("Ignoring sequence coord system " + cs + " ("
-                  + cs.getContent() + ") for source " + getTitle()
-                  + "- threw exception when constructing fetcher.\n");
-          e.printStackTrace();
-        }
-      }
-    }
-    else
-    {
-      try
-      {
-        seqsources.add(new DasSequenceSource(getTitle(), getTitle(), source,
-                getVersion(), null, connprov));
-      } catch (Exception e)
-      {
-        // TODO Auto-generated catch block
-        e.printStackTrace();
-      }
-
-    }
-    if (seqsources.size() > 1)
-    {
-      // sort by name
-      DbSourceProxy[] tsort = seqsources.toArray(new DasSequenceSource[0]);
-      String[] nm = new String[tsort.length];
-      for (int i = 0; i < nm.length; i++)
-      {
-        nm[i] = tsort[i].getDbName().toLowerCase();
-      }
-      jalview.util.QuickSort.sort(nm, tsort);
-      seqsources.clear();
-      for (DbSourceProxy ssrc : tsort)
-      {
-        seqsources.add(ssrc);
-      }
-    }
-    return seqsources;
-  }
-
-  @Override
-  public String getSourceURL()
-  {
-    try
-    {
-      // kind of dumb, since
-      // org.biodas.jdas.dassources.utils.VersionAdapter.getSourceUriFromQueryUri()
-      // does this,
-      // but this way, we can access non DAS 1.6 compliant sources (which have
-      // to have a URL like <sourcename>/das/ and cause a validation exception)
-
-      for (CAPABILITY cap : getVersion().getCAPABILITY())
-      {
-        String capname = cap.getType()
-                .substring(cap.getType().indexOf(":") + 1);
-        int p = cap.getQueryUri().lastIndexOf(capname);
-        if (p < -1)
-        {
-          throw new Exception(MessageManager.formatMessage(
-                  "exception.invalid_das_source", new String[]
-                  { source.getUri() }));
-        }
-        if (cap.getQueryUri().charAt(p) == '/')
-        {
-          p--;
-        }
-        return cap.getQueryUri().substring(0, p);
-      }
-    } catch (Exception x)
-    {
-      System.err.println("Serious: Couldn't get the URL for source "
-              + source.getTitle());
-      x.printStackTrace();
-    }
-    return null;
-  }
-
-  @Override
-  public boolean isNewerThan(jalviewSourceI other)
-  {
-    return isLaterThan(getVersion().getCreated(),
-            other.getVersion().getCreated());
-  }
-
-  @Override
-  public boolean isReferenceSource()
-  {
-    // TODO check source object for indication that we are the primary for a DAS
-    // coordinate system
-    return false;
-  }
-}
index 35196fa..a7a7d34 100644 (file)
@@ -34,6 +34,7 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
@@ -64,6 +65,8 @@ import org.testng.annotations.Test;
 
 public class AlignmentUtilsTests
 {
+  private static Sequence ts = new Sequence("short",
+          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -72,9 +75,6 @@ public class AlignmentUtilsTests
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  public static Sequence ts = new Sequence("short",
-          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
-
   @Test(groups = { "Functional" })
   public void testExpandContext()
   {
@@ -1045,14 +1045,18 @@ public class AlignmentUtilsTests
     dna.addCodonFrame(acf);
 
     /*
-     * In this case, mappings originally came from matching Uniprot accessions - so need an xref on dna involving those regions. These are normally constructed from CDS annotation
+     * In this case, mappings originally came from matching Uniprot accessions 
+     * - so need an xref on dna involving those regions. 
+     * These are normally constructed from CDS annotation
      */
     DBRefEntry dna1xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep1",
             new Mapping(mapfordna1));
-    dna1.getDatasetSequence().addDBRef(dna1xref);
+    dna1.addDBRef(dna1xref);
+    assertEquals(2, dna1.getDBRefs().length); // to self and to pep1
     DBRefEntry dna2xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep2",
             new Mapping(mapfordna2));
-    dna2.getDatasetSequence().addDBRef(dna2xref);
+    dna2.addDBRef(dna2xref);
+    assertEquals(2, dna2.getDBRefs().length); // to self and to pep2
 
     /*
      * execute method under test:
@@ -1107,6 +1111,38 @@ public class AlignmentUtilsTests
     assertEquals(cdsMapping.getInverse(), dbref.getMap().getMap());
 
     /*
+     * verify cDNA has added a dbref with mapping to CDS
+     */
+    assertEquals(3, dna1.getDBRefs().length);
+    DBRefEntry dbRefEntry = dna1.getDBRefs()[2];
+    assertSame(cds1Dss, dbRefEntry.getMap().getTo());
+    MapList dnaToCdsMapping = new MapList(new int[] { 4, 6, 10, 12 },
+            new int[] { 1, 6 }, 1, 1);
+    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
+    assertEquals(3, dna2.getDBRefs().length);
+    dbRefEntry = dna2.getDBRefs()[2];
+    assertSame(cds2Dss, dbRefEntry.getMap().getTo());
+    dnaToCdsMapping = new MapList(new int[] { 1, 3, 7, 9, 13, 15 },
+            new int[] { 1, 9 }, 1, 1);
+    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
+
+    /*
+     * verify CDS has added a dbref with mapping to cDNA
+     */
+    assertEquals(2, cds1Dss.getDBRefs().length);
+    dbRefEntry = cds1Dss.getDBRefs()[1];
+    assertSame(dna1.getDatasetSequence(), dbRefEntry.getMap().getTo());
+    MapList cdsToDnaMapping = new MapList(new int[] { 1, 6 }, new int[] {
+        4, 6, 10, 12 }, 1, 1);
+    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
+    assertEquals(2, cds2Dss.getDBRefs().length);
+    dbRefEntry = cds2Dss.getDBRefs()[1];
+    assertSame(dna2.getDatasetSequence(), dbRefEntry.getMap().getTo());
+    cdsToDnaMapping = new MapList(new int[] { 1, 9 }, new int[] { 1, 3, 7,
+        9, 13, 15 }, 1, 1);
+    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
+
+    /*
      * Verify mappings from CDS to peptide, cDNA to CDS, and cDNA to peptide
      * the mappings are on the shared alignment dataset
      * 6 mappings, 2*(DNA->CDS), 2*(DNA->Pep), 2*(CDS->Pep) 
@@ -1897,6 +1933,7 @@ public class AlignmentUtilsTests
     sf6.setValue("alleles", "g, a"); // should force to upper-case
     sf6.setValue("ID", "sequence_variant:rs758803216");
     dna.addSequenceFeature(sf6);
+
     SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15,
             15, 0f, null);
     sf7.setValue("alleles", "A, T");
@@ -1986,6 +2023,7 @@ public class AlignmentUtilsTests
      * variants:
      *           GAA -> E             source: Ensembl
      *           CAA -> Q             source: dbSNP
+     *           TAA -> STOP          source: dnSNP
      *           AAG synonymous       source: COSMIC
      *           AAT -> N             source: Ensembl
      *           ...TTC synonymous    source: dbSNP
@@ -2001,39 +2039,51 @@ public class AlignmentUtilsTests
     String ensembl = "Ensembl";
     String dbSnp = "dbSNP";
     String cosmic = "COSMIC";
+
     SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
             0f, ensembl);
-    sf1.setValue("alleles", "A,G"); // GAA -> E
+    sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E
     sf1.setValue("ID", "var1.125A>G");
+
     SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1,
             0f, dbSnp);
-    sf2.setValue("alleles", "A,C"); // CAA -> Q
+    sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q
     sf2.setValue("ID", "var2");
     sf2.setValue("clinical_significance", "Dodgy");
-    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 3, 3,
-            0f, cosmic);
-    sf3.setValue("alleles", "A,G"); // synonymous
+
+    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1,
+            0f, dbSnp);
+    sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon
     sf3.setValue("ID", "var3");
-    sf3.setValue("clinical_significance", "None");
+    sf3.setValue("clinical_significance", "Bad");
+
     SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3,
+            0f, cosmic);
+    sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous
+    sf4.setValue("ID", "var4");
+    sf4.setValue("clinical_significance", "None");
+
+    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3,
             0f, ensembl);
-    sf4.setValue("alleles", "A,T"); // AAT -> N
-    sf4.setValue("ID", "sequence_variant:var4"); // prefix gets stripped off
-    sf4.setValue("clinical_significance", "Benign");
-    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 6, 6,
+    sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N
+    sf5.setValue("ID", "sequence_variant:var5"); // prefix gets stripped off
+    sf5.setValue("clinical_significance", "Benign");
+
+    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6,
             0f, dbSnp);
-    sf5.setValue("alleles", "T,C"); // synonymous
-    sf5.setValue("ID", "var5");
-    sf5.setValue("clinical_significance", "Bad");
-    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 8, 8,
-            0f, cosmic);
-    sf6.setValue("alleles", "C,A,G"); // CAC,CGC -> H,R
+    sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous
     sf6.setValue("ID", "var6");
-    sf6.setValue("clinical_significance", "Good");
+
+    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8,
+            0f, cosmic);
+    sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R
+    sf7.setValue("ID", "var7");
+    sf7.setValue("clinical_significance", "Good");
 
     List<DnaVariant> codon1Variants = new ArrayList<>();
     List<DnaVariant> codon2Variants = new ArrayList<>();
     List<DnaVariant> codon3Variants = new ArrayList<>();
+
     List<DnaVariant> codonVariants[] = new ArrayList[3];
     codonVariants[0] = codon1Variants;
     codonVariants[1] = codon2Variants;
@@ -2044,10 +2094,11 @@ public class AlignmentUtilsTests
      */
     codon1Variants.add(new DnaVariant("A", sf1));
     codon1Variants.add(new DnaVariant("A", sf2));
+    codon1Variants.add(new DnaVariant("A", sf3));
     codon2Variants.add(new DnaVariant("A"));
-    codon2Variants.add(new DnaVariant("A"));
-    codon3Variants.add(new DnaVariant("A", sf3));
+    // codon2Variants.add(new DnaVariant("A"));
     codon3Variants.add(new DnaVariant("A", sf4));
+    codon3Variants.add(new DnaVariant("A", sf5));
     AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants);
 
     /*
@@ -2058,7 +2109,7 @@ public class AlignmentUtilsTests
     codon3Variants.clear();
     codon1Variants.add(new DnaVariant("T"));
     codon2Variants.add(new DnaVariant("T"));
-    codon3Variants.add(new DnaVariant("T", sf5));
+    codon3Variants.add(new DnaVariant("T", sf6));
     AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants);
 
     /*
@@ -2068,7 +2119,7 @@ public class AlignmentUtilsTests
     codon2Variants.clear();
     codon3Variants.clear();
     codon1Variants.add(new DnaVariant("C"));
-    codon2Variants.add(new DnaVariant("C", sf6));
+    codon2Variants.add(new DnaVariant("C", sf7));
     codon3Variants.add(new DnaVariant("C"));
     AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants);
 
@@ -2076,35 +2127,43 @@ public class AlignmentUtilsTests
      * verify added sequence features for
      * var1 K -> E Ensembl
      * var2 K -> Q dbSNP
-     * var4 K -> N Ensembl
-     * var6 P -> H COSMIC
-     * var6 P -> R COSMIC
+     * var3 K -> stop
+     * var4 synonymous
+     * var5 K -> N Ensembl
+     * var6 synonymous
+     * var7 P -> H COSMIC
+     * var8 P -> R COSMIC
      */
     List<SequenceFeature> sfs = peptide.getSequenceFeatures();
     SequenceFeatures.sortFeatures(sfs, true);
-    assertEquals(5, sfs.size());
+    assertEquals(8, sfs.size());
 
     /*
      * 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)
      */
+    // AAA -> AAT -> K/N
     SequenceFeature sf = sfs.get(0);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Lys1Asn", sf.getDescription());
-    assertEquals("var4", sf.getValue("ID"));
+    assertEquals("var5", sf.getValue("ID"));
     assertEquals("Benign", sf.getValue("clinical_significance"));
-    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
+    assertEquals("ID=var5;clinical_significance=Benign",
+            sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
+    // AAA -> CAA -> K/Q
     sf = sfs.get(1);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Lys1Gln", sf.getDescription());
     assertEquals("var2", sf.getValue("ID"));
     assertEquals("Dodgy", sf.getValue("clinical_significance"));
@@ -2115,9 +2174,11 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(dbSnp, sf.getFeatureGroup());
 
+    // AAA -> GAA -> K/E
     sf = sfs.get(2);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Lys1Glu", sf.getDescription());
     assertEquals("var1.125A>G", sf.getValue("ID"));
     assertNull(sf.getValue("clinical_significance"));
@@ -2129,30 +2190,79 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
+    // AAA -> TAA -> stop codon
     sf = sfs.get(3);
+    assertEquals(1, sf.getBegin());
+    assertEquals(1, sf.getEnd());
+    assertEquals("stop_gained", sf.getType());
+    assertEquals("Aaa/Taa", sf.getDescription());
+    assertEquals("var3", sf.getValue("ID"));
+    assertEquals("Bad", sf.getValue("clinical_significance"));
+    assertEquals("ID=var3;clinical_significance=Bad", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3",
+            sf.links.get(0));
+    assertEquals(dbSnp, sf.getFeatureGroup());
+
+    // AAA -> AAG synonymous
+    sf = sfs.get(4);
+    assertEquals(1, sf.getBegin());
+    assertEquals(1, sf.getEnd());
+    assertEquals("synonymous_variant", sf.getType());
+    assertEquals("aaA/aaG", sf.getDescription());
+    assertEquals("var4", sf.getValue("ID"));
+    assertEquals("None", sf.getValue("clinical_significance"));
+    assertEquals("ID=var4;clinical_significance=None", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            sf.links.get(0));
+    assertEquals(cosmic, sf.getFeatureGroup());
+
+    // TTT -> TTC synonymous
+    sf = sfs.get(5);
+    assertEquals(2, sf.getBegin());
+    assertEquals(2, sf.getEnd());
+    assertEquals("synonymous_variant", sf.getType());
+    assertEquals("ttT/ttC", sf.getDescription());
+    assertEquals("var6", sf.getValue("ID"));
+    assertNull(sf.getValue("clinical_significance"));
+    assertEquals("ID=var6", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "ttT/ttC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
+            sf.links.get(0));
+    assertEquals(dbSnp, sf.getFeatureGroup());
+
+    // var7 generates two distinct protein variant features (two alleles)
+    // CCC -> CGC -> P/R
+    sf = sfs.get(6);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Pro3Arg", sf.getDescription());
-    assertEquals("var6", sf.getValue("ID"));
+    assertEquals("var7", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
-    assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
+    assertEquals("ID=var7;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.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
 
-    // var5 generates two distinct protein variant features
-    sf = sfs.get(4);
+    // CCC -> CAC -> P/H
+    sf = sfs.get(7);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Pro3His", sf.getDescription());
-    assertEquals("var6", sf.getValue("ID"));
+    assertEquals("var7", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
-    assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
+    assertEquals("ID=var7;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.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
   }
@@ -2534,6 +2644,70 @@ public class AlignmentUtilsTests
     assertEquals(s_as3, uas3.getSequenceAsString());
   }
 
+  @Test(groups = { "Functional" })
+  public void testTransferGeneLoci()
+  {
+    SequenceI from = new Sequence("transcript",
+            "aaacccgggTTTAAACCCGGGtttaaacccgggttt");
+    SequenceI to = new Sequence("CDS", "TTTAAACCCGGG");
+    MapList map = new MapList(new int[] { 1, 12 }, new int[] { 10, 21 }, 1,
+            1);
+
+    /*
+     * first with nothing to transfer
+     */
+    AlignmentUtils.transferGeneLoci(from, map, to);
+    assertNull(to.getGeneLoci());
+
+    /*
+     * next with gene loci set on 'from' sequence
+     */
+    int[] exons = new int[] { 100, 105, 155, 164, 210, 229 };
+    MapList geneMap = new MapList(new int[] { 1, 36 }, exons, 1, 1);
+    from.setGeneLoci("human", "GRCh38", "7", geneMap);
+    AlignmentUtils.transferGeneLoci(from, map, to);
+
+    GeneLociI toLoci = to.getGeneLoci();
+    assertNotNull(toLoci);
+    // DBRefEntry constructor upper-cases 'source'
+    assertEquals("HUMAN", toLoci.getSpeciesId());
+    assertEquals("GRCh38", toLoci.getAssemblyId());
+    assertEquals("7", toLoci.getChromosomeId());
+
+    /*
+     * transcript 'exons' are 1-6, 7-16, 17-36
+     * CDS 1:12 is transcript 10-21
+     * transcript 'CDS' is 10-16, 17-21
+     * which is 'gene' 158-164, 210-214
+     */
+    MapList toMap = toLoci.getMap();
+    assertEquals(1, toMap.getFromRanges().size());
+    assertEquals(2, toMap.getFromRanges().get(0).length);
+    assertEquals(1, toMap.getFromRanges().get(0)[0]);
+    assertEquals(12, toMap.getFromRanges().get(0)[1]);
+    assertEquals(2, toMap.getToRanges().size());
+    assertEquals(2, toMap.getToRanges().get(0).length);
+    assertEquals(158, toMap.getToRanges().get(0)[0]);
+    assertEquals(164, toMap.getToRanges().get(0)[1]);
+    assertEquals(210, toMap.getToRanges().get(1)[0]);
+    assertEquals(214, toMap.getToRanges().get(1)[1]);
+    // or summarised as (but toString might change in future):
+    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164] [210, 214] ]",
+            toMap.toString());
+
+    /*
+     * an existing value is not overridden 
+     */
+    geneMap = new MapList(new int[] { 1, 36 }, new int[] { 36, 1 }, 1, 1);
+    from.setGeneLoci("inhuman", "GRCh37", "6", geneMap);
+    AlignmentUtils.transferGeneLoci(from, map, to);
+    assertEquals("GRCh38", toLoci.getAssemblyId());
+    assertEquals("7", toLoci.getChromosomeId());
+    toMap = toLoci.getMap();
+    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164] [210, 214] ]",
+            toMap.toString());
+  }
+
   /**
    * Tests for the method that maps nucleotide to protein based on CDS features
    */
index 0265af3..b3c78be 100644 (file)
@@ -427,7 +427,7 @@ public class CrossRefTest
      * argument false suppresses adding DAS sources
      * todo: define an interface type SequenceFetcherI and mock that
      */
-    SequenceFetcher mockFetcher = new SequenceFetcher(false)
+    SequenceFetcher mockFetcher = new SequenceFetcher()
     {
       @Override
       public boolean isFetchable(String source)
@@ -505,7 +505,7 @@ public class CrossRefTest
      * argument false suppresses adding DAS sources
      * todo: define an interface type SequenceFetcherI and mock that
      */
-    SequenceFetcher mockFetcher = new SequenceFetcher(false)
+    SequenceFetcher mockFetcher = new SequenceFetcher()
     {
       @Override
       public boolean isFetchable(String source)
@@ -651,7 +651,7 @@ public class CrossRefTest
      * passed in calls to getSequences() - important to verify that
      * duplicate sequence fetches are not requested
      */
-    SequenceFetcher mockFetcher = new SequenceFetcher(false)
+    SequenceFetcher mockFetcher = new SequenceFetcher()
     {
       int call = 0;
 
index 2e89b0e..efee93b 100644 (file)
@@ -25,6 +25,8 @@ import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.analysis.Finder;
 import jalview.api.AlignViewControllerI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.Alignment;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
@@ -35,7 +37,9 @@ import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
 
+import java.awt.Color;
 import java.util.Arrays;
 import java.util.BitSet;
 
@@ -67,13 +71,14 @@ public class AlignViewControllerTest
             null));
     seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f,
             null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f,
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10,
+            10f,
             null));
     seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15,
-            0f, null));
+            10f, null));
     // disulfide bond is a 'contact feature' - only select its 'start' and 'end'
-    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", 8, 12,
-            0f, null));
+    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc",
+            8, 12, 0f, null));
 
     /*
      * select the first five columns --> Metal in seq1 cols 4-5
@@ -86,9 +91,18 @@ public class AlignViewControllerTest
     sg.addSequence(seq3, false);
     sg.addSequence(seq4, false);
 
+    /*
+     * set features visible on a viewport as only visible features are selected
+     */
+    AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1,
+        seq2, seq3, seq4 }), 100, 100);
+    af.getFeatureRenderer().findAllFeatures(true);
+
+    AlignViewController avc = new AlignViewController(af, af.getViewport(),
+            af.alignPanel);
+
     BitSet bs = new BitSet();
-    int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg,
-            bs);
+    int seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(3)); // base 0
@@ -99,7 +113,7 @@ public class AlignViewControllerTest
      */
     sg.setEndRes(6);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(2, seqCount);
     assertEquals(4, bs.cardinality());
     assertTrue(bs.get(3));
@@ -113,7 +127,7 @@ public class AlignViewControllerTest
     sg.setStartRes(13);
     sg.setEndRes(13);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(1, bs.cardinality());
     assertTrue(bs.get(13));
@@ -124,18 +138,35 @@ public class AlignViewControllerTest
     sg.setStartRes(17);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
     /*
+     * threshold Metal to hide where score < 5
+     * seq1 feature in columns 4-6 is hidden
+     * seq2 feature in columns 6-7 is shown
+     */
+    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(5f);
+    af.getFeatureRenderer().setColour("Metal", fc);
+    sg.setStartRes(0);
+    sg.setEndRes(6);
+    bs.clear();
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    assertEquals(1, seqCount);
+    assertEquals(2, bs.cardinality());
+    assertTrue(bs.get(5));
+    assertTrue(bs.get(6));
+
+    /*
      * columns 11-13 should not match disulfide bond at 8/12
      */
     sg.setStartRes(10);
     sg.setEndRes(12);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
-            sg, bs);
+    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
@@ -145,8 +176,7 @@ public class AlignViewControllerTest
     sg.setStartRes(5);
     sg.setEndRes(17);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
-            sg, bs);
+    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(8));
@@ -158,7 +188,7 @@ public class AlignViewControllerTest
     sg.setStartRes(0);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Pfam", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
   }
index fbeb365..c955979 100644 (file)
@@ -273,4 +273,47 @@ public class SequenceFeatureTest
             "group");
     assertTrue(sf.isContactFeature());
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetDetailsReport()
+  {
+    // single locus, no group, no score
+    SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null);
+    String expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>22</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+
+    // contact feature
+    sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
+            null);
+    expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>28:31</td><td></td></tr>"
+            + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+
+    sf = new SequenceFeature("variant", "G,C", 22, 33,
+            12.5f, "group");
+    sf.setValue("Parent", "ENSG001");
+    sf.setValue("Child", "ENSP002");
+    expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>22-33</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
+            + "<tr><td>Score</td><td>12.5</td><td></td></tr>"
+            + "<tr><td>Group</td><td>group</td><td></td></tr>"
+            + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
+            + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+
+    /*
+     * feature with embedded html link in description
+     */
+    String desc = "<html>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></html>";
+    sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot");
+    expected = "<br><table><tr><td>Type</td><td>Pfam</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>8-83</td><td></td></tr>"
+            + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
+            + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+  }
 }
index 5a14514..79bb2bb 100644 (file)
@@ -247,7 +247,9 @@ public class SequenceTest
     sq.sequenceChanged();
     assertEquals(6, sq.findIndex(9));
 
-    sq = new Sequence("test/8-13", "-A--B-C-D-E-F--");
+    final String aligned = "-A--B-C-D-E-F--";
+    assertEquals(15, aligned.length());
+    sq = new Sequence("test/8-13", aligned);
     assertEquals(2, sq.findIndex(8));
     sq.sequenceChanged();
     assertEquals(5, sq.findIndex(9));
@@ -263,6 +265,29 @@ public class SequenceTest
     // beyond end returns last residue column
     sq.sequenceChanged();
     assertEquals(13, sq.findIndex(99));
+
+    /*
+     * residue before sequence 'end' but beyond end of sequence returns 
+     * length of sequence (last column) (rightly or wrongly!)
+     */
+    sq = new Sequence("test/8-15", "A-B-C-"); // trailing gap case
+    assertEquals(6, sq.getLength());
+    sq.sequenceChanged();
+    assertEquals(sq.getLength(), sq.findIndex(14));
+    sq = new Sequence("test/8-99", "-A--B-C-D"); // trailing residue case
+    sq.sequenceChanged();
+    assertEquals(sq.getLength(), sq.findIndex(65));
+
+    /*
+     * residue after sequence 'start' but before first residue returns 
+     * zero (before first column) (rightly or wrongly!)
+     */
+    sq = new Sequence("test/8-15", "-A-B-C-"); // leading gap case
+    sq.sequenceChanged();
+    assertEquals(0, sq.findIndex(3));
+    sq = new Sequence("test/8-15", "A-B-C-"); // leading residue case
+    sq.sequenceChanged();
+    assertEquals(0, sq.findIndex(2));
   }
 
   /**
@@ -1402,14 +1427,61 @@ public class SequenceTest
   {
     Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
 
-    // find F given A
+    // find F given A, check cursor is now at the found position
     assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0)));
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(13, cursor.residuePosition);
+    assertEquals(10, cursor.columnPosition);
 
     // find A given F
     assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(8, cursor.residuePosition);
+    assertEquals(2, cursor.columnPosition);
 
-    // find C given C
+    // find C given C (no cursor update is done for this case)
     assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0)));
+    SequenceCursor cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor2, cursor);
+
+    /*
+     * sequence 'end' beyond end of sequence returns length of sequence 
+     *  (for compatibility with pre-cursor code)
+     *  - also verify the cursor is left in a valid state
+     */
+    sq = new Sequence("test/8-99", "-A--B-C-D-E-F--"); // trailing gap case
+    assertEquals(7, sq.findIndex(10)); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(10, cursor.residuePosition);
+    assertEquals(7, cursor.columnPosition);
+    assertEquals(sq.getLength(), sq.findIndex(65));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
+
+    sq = new Sequence("test/8-99", "-A--B-C-D-E-F"); // trailing residue case
+    sq.findIndex(10); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(sq.getLength(), sq.findIndex(65));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
+
+    /*
+     * residue after sequence 'start' but before first residue should return 
+     * zero (for compatibility with pre-cursor code)
+     */
+    sq = new Sequence("test/8-15", "-A-B-C-"); // leading gap case
+    sq.findIndex(10); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(0, sq.findIndex(3));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
+
+    sq = new Sequence("test/8-15", "A-B-C-"); // leading residue case
+    sq.findIndex(10); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(0, sq.findIndex(2));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
   }
 
   @Test(groups = { "Functional" })
diff --git a/test/jalview/datamodel/features/FeatureAttributesTest.java b/test/jalview/datamodel/features/FeatureAttributesTest.java
new file mode 100644 (file)
index 0000000..0846ec2
--- /dev/null
@@ -0,0 +1,133 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureAttributes.Datatype;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class FeatureAttributesTest
+{
+
+  /**
+   * clear down attributes map before tests
+   */
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    ((Map<?, ?>) PA.getValue(fa, "attributes")).clear();
+  }
+
+  /**
+   * clear down attributes map after tests
+   */
+  @AfterMethod(alwaysRun = true)
+  public void tearDown()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    ((Map<?, ?>) PA.getValue(fa, "attributes")).clear();
+  }
+
+  /**
+   * Test the method that keeps attribute names in non-case-sensitive order,
+   * including handling of 'compound' names
+   */
+  @Test(groups="Functional")
+  public void testAttributeNameComparator()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    Comparator<String[]> comp = (Comparator<String[]>) PA.getValue(fa,
+            "comparator");
+
+    assertEquals(
+            comp.compare(new String[] { "CSQ" }, new String[] { "csq" }), 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "a" },
+            new String[] { "csq" }) > 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ" }, new String[] { "csq",
+        "b" }) < 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "AF" }, new String[] {
+        "csq", "ac" }) > 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "ac" }, new String[] {
+        "csq", "AF" }) < 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinMax()
+  {
+    SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20,
+            "group");
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    assertNull(fa.getMinMax("Pfam", "kd"));
+    sf.setValue("domain", "xyz");
+    assertNull(fa.getMinMax("Pfam", "kd"));
+    sf.setValue("kd", "1.3");
+    assertEquals(fa.getMinMax("Pfam", "kd"), new float[] { 1.3f, 1.3f });
+    sf.setValue("kd", "-2.6");
+    assertEquals(fa.getMinMax("Pfam", "kd"), new float[] { -2.6f, 1.3f });
+    // setting 'mixed' character and numeric values wipes the min/max value
+    sf.setValue("kd", "some text");
+    assertNull(fa.getMinMax("Pfam", "kd"));
+
+    Map<String, String> csq = new HashMap<>();
+    csq.put("AF", "-3");
+    sf.setValue("CSQ", csq);
+    assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"),
+            new float[]
+            { -3f, -3f });
+    csq.put("AF", "4");
+    sf.setValue("CSQ", csq);
+    assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"),
+            new float[]
+            { -3f, 4f });
+  }
+
+  /**
+   * Test the method that returns an attribute description, provided it is
+   * recorded and unique
+   */
+  @Test(groups = "Functional")
+  public void testGetDescription()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    // with no description returns null
+    assertNull(fa.getDescription("Pfam", "kd"));
+    // with a unique description, returns that value
+    fa.addDescription("Pfam", "desc1", "kd");
+    assertEquals(fa.getDescription("Pfam", "kd"), "desc1");
+    // with ambiguous description, returns null
+    fa.addDescription("Pfam", "desc2", "kd");
+    assertNull(fa.getDescription("Pfam", "kd"));
+  }
+
+  @Test(groups = "Functional")
+  public void testDatatype()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    assertNull(fa.getDatatype("Pfam", "kd"));
+    SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20,
+            "group");
+    sf.setValue("kd", "-1");
+    sf.setValue("domain", "Metal");
+    sf.setValue("phase", "1");
+    sf.setValue("phase", "reverse");
+    assertEquals(fa.getDatatype("Pfam", "kd"), Datatype.Number);
+    assertEquals(fa.getDatatype("Pfam", "domain"), Datatype.Character);
+    assertEquals(fa.getDatatype("Pfam", "phase"), Datatype.Mixed);
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureMatcherSetTest.java b/test/jalview/datamodel/features/FeatureMatcherSetTest.java
new file mode 100644 (file)
index 0000000..a2d2c9a
--- /dev/null
@@ -0,0 +1,419 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherSetTest
+{
+  @Test(groups = "Functional")
+  public void testMatches_byAttribute()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    fms.and(fm);
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "foobar");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "-2");
+    assertTrue(fms.matches(sf));
+    sf.setValue("AF", "-1");
+    assertTrue(fms.matches(sf));
+    sf.setValue("AF", "-3");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "");
+    assertFalse(fms.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+    fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "raining cats and dogs");
+    assertTrue(fms.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testAnd()
+  {
+    // condition1: AF value contains "dog" (matches)
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+            "dog", "AF");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "how", "CSQ");
+
+    SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12,
+            6.2f, "grp");
+    sf.setValue("AF", "raining cats and dogs");
+    sf.setValue("CSQ", "showers");
+
+    assertTrue(fm1.matches(sf));
+    assertFalse(fm2.matches(sf));
+
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+    fms.and(fm1);
+    assertTrue(fms.matches(sf));
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+
+    /*
+     * OR a failed attribute condition with a matched label condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+    FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains,
+            "Helix");
+    fms.or(byLabelPass);
+    assertTrue(fms.matches(sf));
+
+    /*
+     * OR a failed attribute condition with a failed score condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+    FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT,
+            "5.9");
+    fms.or(byScoreFail);
+    assertFalse(fms.matches(sf));
+
+    /*
+     * OR failed attribute and score conditions with matched label condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.or(fm2);
+    fms.or(byScoreFail);
+    assertFalse(fms.matches(sf));
+    fms.or(byLabelPass);
+    assertTrue(fms.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm1.toString(), "AF < 1.2");
+
+    FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "path", "CLIN_SIG");
+    assertEquals(fm2.toString(), "CLIN_SIG does not contain 'path'");
+
+    /*
+     * AND them
+     */
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertEquals(fms.toString(), "");
+    fms.and(fm1);
+    assertEquals(fms.toString(), "AF < 1.2");
+    fms.and(fm2);
+    assertEquals(fms.toString(),
+            "(AF < 1.2) and (CLIN_SIG does not contain 'path')");
+
+    /*
+     * OR them
+     */
+    fms = new FeatureMatcherSet();
+    assertEquals(fms.toString(), "");
+    fms.or(fm1);
+    assertEquals(fms.toString(), "AF < 1.2");
+    fms.or(fm2);
+    assertEquals(fms.toString(),
+            "(AF < 1.2) or (CLIN_SIG does not contain 'path')");
+
+    try
+    {
+      fms.and(fm1);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testOr()
+  {
+    // condition1: AF value contains "dog" (matches)
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+            "dog", "AF");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "how", "CSQ");
+
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    sf.setValue("AF", "raining cats and dogs");
+    sf.setValue("CSQ", "showers");
+
+    assertTrue(fm1.matches(sf));
+    assertFalse(fm2.matches(sf));
+
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+    fms.or(fm1);
+    assertTrue(fms.matches(sf));
+    fms.or(fm2);
+    assertTrue(fms.matches(sf)); // true or false makes true
+
+    fms = new FeatureMatcherSet();
+    fms.or(fm2);
+    assertFalse(fms.matches(sf));
+    fms.or(fm1);
+    assertTrue(fms.matches(sf)); // false or true makes true
+
+    try
+    {
+      fms.and(fm2);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testIsEmpty()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0",
+            "AF");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.isEmpty());
+    fms.and(fm);
+    assertFalse(fms.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatchers()
+  {
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+
+    /*
+     * empty iterable:
+     */
+    Iterator<FeatureMatcherI> iterator = fms.getMatchers().iterator();
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one matcher:
+     */
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    fms.and(fm1);
+    iterator = fms.getMatchers().iterator();
+    assertSame(fm1, iterator.next());
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two matchers:
+     */
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.LT, "8f",
+            "AF");
+    fms.and(fm2);
+    iterator = fms.getMatchers().iterator();
+    assertSame(fm1, iterator.next());
+    assertSame(fm2, iterator.next());
+    assertFalse(iterator.hasNext());
+  }
+
+  /**
+   * Tests for the 'compound attribute' key i.e. where first key's value is a map
+   * from which we take the value for the second key, e.g. CSQ : Consequence
+   */
+  @Test(groups = "Functional")
+  public void testMatches_compoundKey()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "CSQ", "Consequence");
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 10, "grp");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    Map<String, String> csq = new HashMap<>();
+    sf.setValue("CSQ", csq);
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "-2");
+    assertTrue(fms.matches(sf));
+    csq.put("Consequence", "-1");
+    assertTrue(fms.matches(sf));
+    csq.put("Consequence", "-3");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "junk");
+    assertFalse(fms.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ",
+            "Consequence");
+    fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    csq.put("PolyPhen", "damaging");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "damaging");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "Catastrophic");
+    assertTrue(fms.matches(sf));
+  }
+
+  /**
+   * Tests for toStableString which (unlike toString) does not i18n the
+   * conditions
+   * 
+   * @see FeatureMatcherTest#testToStableString()
+   */
+  @Test(groups = "Functional")
+  public void testToStableString()
+  {
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm1.toStableString(), "AF LT 1.2");
+  
+    FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "path", "CLIN_SIG");
+    assertEquals(fm2.toStableString(), "CLIN_SIG NotContains path");
+  
+    /*
+     * AND them
+     */
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertEquals(fms.toStableString(), "");
+    fms.and(fm1);
+    // no brackets needed if a single condition
+    assertEquals(fms.toStableString(), "AF LT 1.2");
+    // brackets if more than one condition
+    fms.and(fm2);
+    assertEquals(fms.toStableString(),
+            "(AF LT 1.2) AND (CLIN_SIG NotContains path)");
+  
+    /*
+     * OR them
+     */
+    fms = new FeatureMatcherSet();
+    assertEquals(fms.toStableString(), "");
+    fms.or(fm1);
+    assertEquals(fms.toStableString(), "AF LT 1.2");
+    fms.or(fm2);
+    assertEquals(fms.toStableString(),
+            "(AF LT 1.2) OR (CLIN_SIG NotContains path)");
+  
+    /*
+     * attribute or value including space is quoted
+     */
+    FeatureMatcher fm3 = FeatureMatcher.byAttribute(Condition.NotMatches,
+            "foo bar", "CSQ", "Poly Phen");
+    assertEquals(fm3.toStableString(),
+            "'CSQ:Poly Phen' NotMatches 'foo bar'");
+    fms.or(fm3);
+    assertEquals(fms.toStableString(),
+            "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')");
+
+    try
+    {
+      fms.and(fm1);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Tests for parsing a string representation of a FeatureMatcherSet
+   * 
+   * @see FeatureMatcherSetTest#testToStableString()
+   */
+  @Test(groups = "Functional")
+  public void testFromString()
+  {
+    String descriptor = "AF LT 1.2";
+    FeatureMatcherSetI fms = FeatureMatcherSet.fromString(descriptor);
+
+    /*
+     * shortcut asserts by verifying a 'roundtrip', 
+     * which we trust if other tests pass :-)
+     */
+    assertEquals(fms.toStableString(), descriptor);
+
+    // brackets optional, quotes optional, condition case insensitive
+    fms = FeatureMatcherSet.fromString("('AF' lt '1.2')");
+    assertEquals(fms.toStableString(), descriptor);
+
+    descriptor = "(AF LT 1.2) AND (CLIN_SIG NotContains path)";
+    fms = FeatureMatcherSet.fromString(descriptor);
+    assertEquals(fms.toStableString(), descriptor);
+
+    // AND is not case-sensitive
+    fms = FeatureMatcherSet
+            .fromString("(AF LT 1.2) and (CLIN_SIG NotContains path)");
+    assertEquals(fms.toStableString(), descriptor);
+  
+    descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path)";
+    fms = FeatureMatcherSet.fromString(descriptor);
+    assertEquals(fms.toStableString(), descriptor);
+
+    // OR is not case-sensitive
+    fms = FeatureMatcherSet
+            .fromString("(AF LT 1.2) or (CLIN_SIG NotContains path)");
+    assertEquals(fms.toStableString(), descriptor);
+
+    // can get away without brackets on last match condition
+    fms = FeatureMatcherSet
+            .fromString("(AF LT 1.2) or CLIN_SIG NotContains path");
+    assertEquals(fms.toStableString(), descriptor);
+  
+    descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')";
+    fms = FeatureMatcherSet.fromString(descriptor);
+    assertEquals(fms.toStableString(), descriptor);
+
+    // can't mix OR and AND
+    descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) AND ('CSQ:Poly Phen' NotMatches 'foo bar')";
+    assertNull(FeatureMatcherSet.fromString(descriptor));
+
+    // can't mix AND and OR
+    descriptor = "(AF LT 1.2) and (CLIN_SIG NotContains path) or ('CSQ:Poly Phen' NotMatches 'foo bar')";
+    assertNull(FeatureMatcherSet.fromString(descriptor));
+
+    // brackets missing
+    assertNull(FeatureMatcherSet
+            .fromString("AF LT 1.2 or CLIN_SIG NotContains path"));
+
+    // invalid conjunction
+    assertNull(FeatureMatcherSet.fromString("(AF LT 1.2) but (AF GT -2)"));
+
+    // unbalanced quote (1)
+    assertNull(FeatureMatcherSet.fromString("('AF lt '1.2')"));
+
+    // unbalanced quote (2)
+    assertNull(FeatureMatcherSet.fromString("('AF' lt '1.2)"));
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureMatcherTest.java b/test/jalview/datamodel/features/FeatureMatcherTest.java
new file mode 100644 (file)
index 0000000..4bd34cb
--- /dev/null
@@ -0,0 +1,352 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherTest
+{
+  @Test(groups = "Functional")
+  public void testMatches_byLabel()
+  {
+    SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+            12, "grp");
+
+    /*
+     * contains - not case sensitive
+     */
+    assertTrue(
+            FeatureMatcher.byLabel(Condition.Contains, "IS").matches(sf));
+    assertTrue(FeatureMatcher.byLabel(Condition.Contains, "").matches(sf));
+    assertFalse(
+            FeatureMatcher.byLabel(Condition.Contains, "ISNT").matches(sf));
+
+    /*
+     * does not contain
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "isnt")
+            .matches(sf));
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "is")
+            .matches(sf));
+
+    /*
+     * matches
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY label")
+            .matches(sf));
+    assertFalse(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY")
+            .matches(sf));
+
+    /*
+     * does not match
+     */
+    assertFalse(FeatureMatcher
+            .byLabel(Condition.NotMatches, "THIS is MY label").matches(sf));
+    assertTrue(FeatureMatcher.byLabel(Condition.NotMatches, "THIS is MY")
+            .matches(sf));
+
+    /*
+     * is present / not present
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.Present, "").matches(sf));
+    assertFalse(
+            FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testMatches_byScore()
+  {
+    SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+            12, 3.2f, "grp");
+
+    assertTrue(FeatureMatcher.byScore(Condition.LT, "3.3").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LT, "3.2").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LT, "2.2").matches(sf));
+
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "3.2").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "2.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.EQ, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.EQ, "3.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.GE, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GE, "3.2").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GE, "2.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.GT, "3.3").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testMatches_byAttribute()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher
+            .byAttribute(Condition.GE, "-2", "AF");
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "foobar");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "-2");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AF", "-1");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AF", "-3");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "");
+    assertFalse(fm.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "raining cats and dogs");
+    assertTrue(fm.matches(sf));
+
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AC");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AC", "21");
+    assertTrue(fm.matches(sf));
+
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AC_Females");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AC_Females", "21");
+    assertFalse(fm.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+
+    /*
+     * toString uses the i18n translation of the enum conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm.toString(), "AF < 1.2");
+
+    /*
+     * Present / NotPresent omit the value pattern
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF");
+    assertEquals(fm.toString(), "AF is present");
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF");
+    assertEquals(fm.toString(), "AF is not present");
+
+    /*
+     * by Label
+     */
+    fm = FeatureMatcher.byLabel(Condition.Matches, "foobar");
+    assertEquals(fm.toString(),
+            MessageManager.getString("label.label") + " matches 'foobar'");
+
+    /*
+     * by Score
+     */
+    fm = FeatureMatcher.byScore(Condition.GE, "12.2");
+    assertEquals(fm.toString(),
+            MessageManager.getString("label.score") + " >= 12.2");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAttribute()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+
+    /*
+     * compound key (attribute / subattribute)
+     */
+    fm = FeatureMatcher.byAttribute(Condition.GE, "-2F", "CSQ",
+            "Consequence");
+    assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" });
+
+    /*
+     * answers null if match is by Label or by Score
+     */
+    assertNull(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .getAttribute());
+    assertNull(FeatureMatcher.byScore(Condition.LE, "-1").getAttribute());
+  }
+
+  @Test(groups = "Functional")
+  public void testIsByAttribute()
+  {
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .isByAttribute());
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByAttribute());
+    assertTrue(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+            .isByAttribute());
+  }
+
+  @Test(groups = "Functional")
+  public void testIsByLabel()
+  {
+    assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .isByLabel());
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByLabel());
+    assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+            .isByLabel());
+  }
+
+  @Test(groups = "Functional")
+  public void testIsByScore()
+  {
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .isByScore());
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "-1").isByScore());
+    assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+            .isByScore());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatcher()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f",
+            "AF");
+    assertEquals(fm.getMatcher().getCondition(), Condition.GE);
+    assertEquals(fm.getMatcher().getFloatValue(), -2F);
+    assertEquals(fm.getMatcher().getPattern(), "-2.0");
+  }
+
+  @Test(groups = "Functional")
+  public void testFromString()
+  {
+    FeatureMatcherI fm = FeatureMatcher.fromString("'AF' LT 1.2");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+    assertSame(Condition.LT, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getFloatValue(), 1.2f);
+    assertEquals(fm.getMatcher().getPattern(), "1.2");
+
+    // quotes are optional, condition is not case sensitive
+    fm = FeatureMatcher.fromString("AF lt '1.2'");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+    assertSame(Condition.LT, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getFloatValue(), 1.2f);
+    assertEquals(fm.getMatcher().getPattern(), "1.2");
+
+    fm = FeatureMatcher.fromString("'AF' Present");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+    assertSame(Condition.Present, fm.getMatcher().getCondition());
+
+    fm = FeatureMatcher.fromString("CSQ:Consequence contains damaging");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" });
+    assertSame(Condition.Contains, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "damaging");
+
+    // keyword Label is not case sensitive
+    fm = FeatureMatcher.fromString("LABEL Matches 'foobar'");
+    assertTrue(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.Matches, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "foobar");
+
+    fm = FeatureMatcher.fromString("'Label' matches 'foo bar'");
+    assertTrue(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.Matches, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "foo bar");
+
+    // quotes optional on pattern
+    fm = FeatureMatcher.fromString("'Label' matches foo bar");
+    assertTrue(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.Matches, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "foo bar");
+
+    fm = FeatureMatcher.fromString("Score GE 12.2");
+    assertFalse(fm.isByLabel());
+    assertTrue(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.GE, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "12.2");
+    assertEquals(fm.getMatcher().getFloatValue(), 12.2f);
+
+    // keyword Score is not case sensitive
+    fm = FeatureMatcher.fromString("'SCORE' ge '12.2'");
+    assertFalse(fm.isByLabel());
+    assertTrue(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.GE, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "12.2");
+    assertEquals(fm.getMatcher().getFloatValue(), 12.2f);
+
+    // invalid numeric pattern
+    assertNull(FeatureMatcher.fromString("Score eq twelve"));
+    // unbalanced opening quote
+    assertNull(FeatureMatcher.fromString("'Score ge 12.2"));
+    // unbalanced pattern quote
+    assertNull(FeatureMatcher.fromString("'Score' ge '12.2"));
+    // pattern missing
+    assertNull(FeatureMatcher.fromString("Score ge"));
+    // condition and pattern missing
+    assertNull(FeatureMatcher.fromString("Score"));
+    // everything missing
+    assertNull(FeatureMatcher.fromString(""));
+  }
+
+  /**
+   * Tests for toStableString which (unlike toString) does not i18n the
+   * conditions
+   */
+  @Test(groups = "Functional")
+  public void testToStableString()
+  {
+    // attribute name not quoted unless it contains space
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm.toStableString(), "AF LT 1.2");
+
+    /*
+     * Present / NotPresent omit the value pattern
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF");
+    assertEquals(fm.toStableString(), "AF Present");
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF");
+    assertEquals(fm.toStableString(), "AF NotPresent");
+
+    /*
+     * by Label
+     * pattern not quoted unless it contains space
+     */
+    fm = FeatureMatcher.byLabel(Condition.Matches, "foobar");
+    assertEquals(fm.toStableString(), "Label Matches foobar");
+
+    fm = FeatureMatcher.byLabel(Condition.Matches, "foo bar");
+    assertEquals(fm.toStableString(), "Label Matches 'foo bar'");
+
+    /*
+     * by Score
+     */
+    fm = FeatureMatcher.byScore(Condition.GE, "12.2");
+    assertEquals(fm.toStableString(), "Score GE 12.2");
+  }
+}
index 39d6dce..32987b0 100644 (file)
@@ -13,10 +13,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import junit.extensions.PA;
-
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class SequenceFeaturesTest
 {
   @Test(groups = "Functional")
@@ -1005,33 +1005,44 @@ public class SequenceFeaturesTest
     assertTrue(store.getFeaturesByOntology(new String[] {}).isEmpty());
     assertTrue(store.getFeaturesByOntology((String[]) null).isEmpty());
   
-    SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20,
+    SequenceFeature transcriptFeature = new SequenceFeature("transcript", "desc", 10, 20,
             Float.NaN, null);
-    store.add(sf1);
+    store.add(transcriptFeature);
 
-    // 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,
+    /*
+     * mRNA is a sub-type of transcript; added here 'as if' non-positional
+     * just to show that non-positional features are included in results
+     */
+    SequenceFeature mrnaFeature = new SequenceFeature("mRNA", "desc", 0, 0,
             Float.NaN, null);
-    store.add(sf2);
+    store.add(mrnaFeature);
 
-    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 30, 40,
+    SequenceFeature pfamFeature = new SequenceFeature("Pfam", "desc", 30, 40,
             Float.NaN, null);
-    store.add(sf3);
+    store.add(pfamFeature);
 
+    /*
+     * "transcript" matches both itself and the sub-term "mRNA"
+     */
     features = store.getFeaturesByOntology("transcript");
     assertEquals(features.size(), 2);
-    assertTrue(features.contains(sf1));
-    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(transcriptFeature));
+    assertTrue(features.contains(mrnaFeature));
 
+    /*
+     * "mRNA" matches itself but not parent term "transcript"
+     */
     features = store.getFeaturesByOntology("mRNA");
     assertEquals(features.size(), 1);
-    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(mrnaFeature));
 
+    /*
+     * "pfam" is not an SO term but is included as an exact match
+     */
     features = store.getFeaturesByOntology("mRNA", "Pfam");
     assertEquals(features.size(), 2);
-    assertTrue(features.contains(sf2));
-    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(mrnaFeature));
+    assertTrue(features.contains(pfamFeature));
 
     features = store.getFeaturesByOntology("sequence_variant");
     assertTrue(features.isEmpty());
@@ -1040,7 +1051,7 @@ public class SequenceFeaturesTest
   @Test(groups = "Functional")
   public void testSortFeatures()
   {
-    List<SequenceFeature> sfs = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> sfs = new ArrayList<>();
     SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 80,
             Float.NaN, null);
     sfs.add(sf1);
index 779962c..c9d8deb 100644 (file)
@@ -25,6 +25,7 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -241,37 +242,51 @@ public class EnsemblCdnaTest
    * accession id as parent
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblCdna testee = new EnsemblCdna();
+    SequenceI seq = new Sequence(accId, "MKLNFRQIE");
 
-    // exon with no parent not valid
-    SequenceFeature sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    // exon with no parent: not valid
+    SequenceFeature sf1 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    seq.addSequenceFeature(sf1);
 
-    // exon with wrong parent not valid
-    sf.setValue("Parent", "transcript:XYZ");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    // exon with wrong parent: not valid
+    SequenceFeature sf2 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf2.setValue("Parent", "transcript:XYZ");
+    seq.addSequenceFeature(sf2);
 
     // exon with right parent is valid
-    sf.setValue("Parent", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf3.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // exon sub-type with right parent is valid
-    sf = new SequenceFeature("coding_exon", "", 1, 2, 0f, null);
-    sf.setValue("Parent", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("coding_exon", "", 1, 2, 0f,
+            null);
+    sf4.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // transcript not valid:
-    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
-    sf.setValue("Parent", "transcript:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf5.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // CDS not valid:
-    sf = new SequenceFeature("CDS", "", 1, 2, 0f, null);
-    sf.setValue("Parent", "transcript:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf6.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf6);
+
+    List<SequenceFeature> sfs = new EnsemblCdna()
+            .getIdentifyingFeatures(seq, accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertFalse(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
   }
 
   @Test(groups = "Functional")
index 8482c90..a44ab7f 100644 (file)
@@ -24,6 +24,7 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -152,37 +153,50 @@ public class EnsemblCdsTest
    * accession id as parent
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblCds testee = new EnsemblCds();
+    SequenceI seq = new Sequence(accId, "MKDONS");
 
     // cds with no parent not valid
-    SequenceFeature sf = new SequenceFeature("CDS", "", 1, 2, 0f, null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf1 = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    seq.addSequenceFeature(sf1);
 
     // cds with wrong parent not valid
-    sf.setValue("Parent", "transcript:XYZ");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf2 = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    sf2.setValue("Parent", "transcript:XYZ");
+    seq.addSequenceFeature(sf2);
 
     // cds with right parent is valid
-    sf.setValue("Parent", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    sf3.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // cds sub-type with right parent is valid
-    sf = new SequenceFeature("CDS_predicted", "", 1, 2, 0f, null);
-    sf.setValue("Parent", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("CDS_predicted", "", 1, 2, 0f,
+            null);
+    sf4.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // transcript not valid:
-    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
-    sf.setValue("Parent", "transcript:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf5.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // exon not valid:
-    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
-    sf.setValue("Parent", "transcript:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf6.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf6);
+
+    List<SequenceFeature> sfs = new EnsemblCds().getIdentifyingFeatures(seq,
+            accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertFalse(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
   }
 
   @Test(groups = "Functional")
index 217742d..446b4f7 100644 (file)
@@ -26,6 +26,7 @@ import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.api.FeatureSettingsModelI;
 import jalview.bin.Cache;
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -77,17 +78,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // 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);
-    sf.setStrand("+");
-    genomic.addSequenceFeature(sf);
-
     // gene at (start + 10500) length 101
-    sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null);
+    SequenceFeature sf = new SequenceFeature("gene", "", 10500, 10600, 0f,
+            null);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
     genomic.addSequenceFeature(sf);
@@ -117,17 +110,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // 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);
-    sf.setStrand("-");
-    genomic.addSequenceFeature(sf);
-
     // gene at (start + 10500) length 101
-    sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null);
+    SequenceFeature sf = new SequenceFeature("gene", "", 10500, 10600, 0f,
+            null);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
     genomic.addSequenceFeature(sf);
@@ -240,40 +225,48 @@ public class EnsemblGeneTest
    * accession id as ID
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblGene testee = new EnsemblGene();
+    SequenceI seq = new Sequence(accId, "HIBEES");
 
     // gene with no ID not valid
-    SequenceFeature sf = new SequenceFeature("gene", "", 1, 2, 0f, null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf1 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    seq.addSequenceFeature(sf1);
 
     // gene with wrong ID not valid
-    sf.setValue("ID", "gene:XYZ");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf2 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf2.setValue("ID", "gene:XYZ");
+    seq.addSequenceFeature(sf2);
 
     // gene with right ID is valid
-    sf.setValue("ID", "gene:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf3.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // gene sub-type with right ID is valid
-    sf = new SequenceFeature("snRNA_gene", "", 1, 2, 0f, null);
-    sf.setValue("ID", "gene:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
-
-    // test is not case-sensitive
-    assertTrue(testee.identifiesSequence(sf, accId.toLowerCase()));
+    SequenceFeature sf4 = new SequenceFeature("snRNA_gene", "", 1, 2, 0f, null);
+    sf4.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // transcript not valid:
-    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
-    sf.setValue("ID", "gene:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf5.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // exon not valid:
-    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
-    sf.setValue("ID", "gene:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf6.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf6);
+    
+    List<SequenceFeature> sfs = new EnsemblGene()
+            .getIdentifyingFeatures(seq, accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertFalse(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
   }
 
   /**
index 8687da9..72ee492 100644 (file)
@@ -24,6 +24,7 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -162,43 +163,58 @@ public class EnsemblGenomeTest
    * accession id as ID
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblGenome testee = new EnsemblGenome();
+    SequenceI seq = new Sequence(accId, "HEARTS");
 
     // transcript with no ID not valid
-    SequenceFeature sf = new SequenceFeature("transcript", "", 1, 2, 0f,
+    SequenceFeature sf1 = new SequenceFeature("transcript", "", 1, 2, 0f,
             null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    seq.addSequenceFeature(sf1);
 
     // transcript with wrong ID not valid
-    sf.setValue("ID", "transcript");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf2 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf2.setValue("ID", "transcript");
+    seq.addSequenceFeature(sf2);
 
     // transcript with right ID is valid
-    sf.setValue("ID", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf3.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // transcript sub-type with right ID is valid
-    sf = new SequenceFeature("ncRNA", "", 1, 2, 0f, null);
-    sf.setValue("ID", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("ncRNA", "", 1, 2, 0f, null);
+    sf4.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // Ensembl treats NMD_transcript_variant as if a transcript
-    sf = new SequenceFeature("NMD_transcript_variant", "", 1, 2, 0f, null);
-    sf.setValue("ID", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("NMD_transcript_variant", "",
+            1, 2, 0f, null);
+    sf5.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // gene not valid:
-    sf = new SequenceFeature("gene", "", 1, 2, 0f, null);
-    sf.setValue("ID", "transcript:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf6.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf6);
 
     // exon not valid:
-    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
-    sf.setValue("ID", "transcript:" + accId);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf7 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf7.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf7);
+
+    List<SequenceFeature> sfs = new EnsemblGenome()
+            .getIdentifyingFeatures(seq, accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertTrue(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
+    assertFalse(sfs.contains(sf7));
   }
 
 }
index 9fad30e..be7bdf2 100644 (file)
 package jalview.ext.ensembl;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A convenience class to simplify writing unit tests (pending Mockito or
@@ -65,9 +69,10 @@ public class EnsemblSeqProxyAdapter extends EnsemblSeqProxy
   }
 
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    return false;
+    return new ArrayList<>();
   }
 
 }
index 72f5a34..69b2ad4 100644 (file)
@@ -21,9 +21,7 @@
 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.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
index 350b599..28c5cf0 100644 (file)
  */
 package jalview.ext.htsjdk;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
 import jalview.datamodel.SequenceI;
-import jalview.gui.JvOptionPane;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 
-import org.testng.Assert;
-import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 /**
@@ -35,25 +41,108 @@ import org.testng.annotations.Test;
  */
 public class TestHtsContigDb
 {
+  @Test(groups = "Functional")
+  public final void testGetSequenceProxy() throws Exception
+  {
+    String pathname = "test/jalview/ext/htsjdk/pgmb.fasta";
+    HtsContigDb db = new HtsContigDb("ADB", new File(pathname));
+    
+    assertTrue(db.isValid());
+    assertTrue(db.isIndexed()); // htsjdk opens the .fai file
+    
+    SequenceI sq = db.getSequenceProxy("Deminut");
+    assertNotNull(sq);
+    assertEquals(sq.getLength(), 606);
+
+    /*
+     * read a sequence earlier in the file
+     */
+    sq = db.getSequenceProxy("PPL_06716");
+    assertNotNull(sq);
+    assertEquals(sq.getLength(), 602);
+    
+    // dict = db.getDictionary(f, truncate))
+  }
 
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
+  /**
+   * Trying to open a .fai file directly results in IllegalArgumentException -
+   * have to provide the unindexed file name instead
+   */
+  @Test(
+    groups = "Functional",
+    expectedExceptions = java.lang.IllegalArgumentException.class)
+  public final void testGetSequenceProxy_indexed()
   {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+    String pathname = "test/jalview/ext/htsjdk/pgmb.fasta.fai";
+    new HtsContigDb("ADB", new File(pathname));
+    fail("Expected exception opening .fai file");
   }
 
+  /**
+   * Tests that exercise
+   * <ul>
+   * <li>opening an unindexed fasta file</li>
+   * <li>creating a .fai index</li>
+   * <li>opening the fasta file, now using the index</li>
+   * <li>error on creating index if overwrite not allowed</li>
+   * </ul>
+   * 
+   * @throws IOException
+   */
   @Test(groups = "Functional")
-  public final void testHTSReferenceSequence() throws Exception
+  public void testCreateFastaSequenceIndex() throws IOException
   {
-    HtsContigDb remmadb = new HtsContigDb("REEMADB", new File(
-            "test/jalview/ext/htsjdk/pgmb.fasta"));
+    File fasta = new File("test/jalview/ext/htsjdk/pgmb.fasta");
+    
+    /*
+     * create .fai with no overwrite fails if it exists
+     */
+    try {
+      HtsContigDb.createFastaSequenceIndex(fasta.toPath(), false);
+      fail("Expected exception");
+    } catch (IOException e)
+    {
+      // expected
+    }
 
-    Assert.assertTrue(remmadb.isValid());
+    /*
+     * create a copy of the .fasta (as a temp file)
+     */
+    File copyFasta = File.createTempFile("copyFasta", ".fasta");
+    copyFasta.deleteOnExit();
+    assertTrue(copyFasta.exists());
+    Files.copy(fasta.toPath(), copyFasta.toPath(),
+            StandardCopyOption.REPLACE_EXISTING);
 
-    SequenceI sq = remmadb.getSequenceProxy("Deminut");
-    Assert.assertNotNull(sq);
-    Assert.assertNotEquals(0, sq.getLength());
+    /*
+     * open the Fasta file - not indexed, as no .fai file yet exists
+     */
+    HtsContigDb db = new HtsContigDb("ADB", copyFasta);
+    assertTrue(db.isValid());
+    assertFalse(db.isIndexed());
+    db.close();
+
+    /*
+     * create the .fai index, re-open the .fasta file - now indexed
+     */
+    HtsContigDb.createFastaSequenceIndex(copyFasta.toPath(), true);
+    db = new HtsContigDb("ADB", copyFasta);
+    assertTrue(db.isValid());
+    assertTrue(db.isIndexed());
+    db.close();
   }
 
+  /**
+   * A convenience 'test' that may be run to create a .fai file for any given
+   * fasta file
+   * 
+   * @throws IOException
+   */
+  @Test(enabled = false)
+  public void testCreateIndex() throws IOException
+  {
+
+    File fasta = new File("test/jalview/io/vcf/contigs.fasta");
+    HtsContigDb.createFastaSequenceIndex(fasta.toPath(), true);
+  }
 }
diff --git a/test/jalview/ext/htsjdk/VCFReaderTest.java b/test/jalview/ext/htsjdk/VCFReaderTest.java
new file mode 100644 (file)
index 0000000..bf617ae
--- /dev/null
@@ -0,0 +1,200 @@
+package jalview.ext.htsjdk;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class VCFReaderTest
+{
+  private static final String[] VCF = new String[] {
+      "##fileformat=VCFv4.2",
+      "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+      "20\t3\t.\tC\tG\t.\tPASS\tDP=100", // SNP C/G
+      "20\t7\t.\tG\tGA\t.\tPASS\tDP=100", // insertion G/GA
+      "18\t2\t.\tACG\tA\t.\tPASS\tDP=100" }; // deletion ACG/A
+
+  // gnomAD exome variant dataset
+  private static final String VCF_PATH = "/Volumes/gjb/smacgowan/NOBACK/resources/gnomad/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+  // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+  /**
+   * A test to exercise some basic functionality of the htsjdk VCF reader,
+   * reading from a non-index VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testReadVcf_plain() throws IOException
+  {
+    File f = writeVcfFile();
+    VCFReader reader = new VCFReader(f.getAbsolutePath());
+    CloseableIterator<VariantContext> variants = reader.iterator();
+
+    /*
+     * SNP C/G variant
+     */
+    VariantContext vc = variants.next();
+    assertTrue(vc.isSNP());
+    Allele ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "C");
+    List<Allele> alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "C");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "G");
+
+    /*
+     * Insertion G -> GA
+     */
+    vc = variants.next();
+    assertFalse(vc.isSNP());
+    assertTrue(vc.isSimpleInsertion());
+    ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "G");
+    alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "G");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "GA");
+
+    /*
+     * Deletion ACG -> A
+     */
+    vc = variants.next();
+    assertFalse(vc.isSNP());
+    assertTrue(vc.isSimpleDeletion());
+    ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "ACG");
+    alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "ACG");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "A");
+
+    assertFalse(variants.hasNext());
+
+    variants.close();
+    reader.close();
+  }
+
+  /**
+   * Creates a temporary file to be read by the htsjdk VCF reader
+   * 
+   * @return
+   * @throws IOException
+   */
+  protected File writeVcfFile() throws IOException
+  {
+    File f = File.createTempFile("Test", "vcf");
+    f.deleteOnExit();
+    PrintWriter pw = new PrintWriter(f);
+    for (String vcfLine : VCF) {
+      pw.println(vcfLine);
+    }
+    pw.close();
+    return f;
+  }
+  
+  /**
+   * A 'test' that demonstrates querying an indexed VCF file for features in a
+   * specified interval
+   * 
+   * @throws IOException
+   */
+  @Test
+  public void testQuery_indexed() throws IOException
+  {
+    /*
+     * if not specified, assumes index file is filename.tbi
+     */
+    VCFReader reader = new VCFReader(VCF_PATH);
+  
+    /*
+     * gene NMT1 (human) is on chromosome 17
+     * GCHR38 (Ensembl): 45051610-45109016
+     * GCHR37 (gnoMAD): 43128978-43186384
+     * CDS begins at offset 9720, first CDS variant at offset 9724
+     */
+    CloseableIterator<VariantContext> features = reader.query("17",
+            43128978 + 9724, 43128978 + 9734); // first 11 CDS positions
+
+    assertEquals(printNext(features), 43138702);
+    assertEquals(printNext(features), 43138704);
+    assertEquals(printNext(features), 43138707);
+    assertEquals(printNext(features), 43138708);
+    assertEquals(printNext(features), 43138710);
+    assertEquals(printNext(features), 43138711);
+    assertFalse(features.hasNext());
+
+    features.close();
+    reader.close();
+  }
+
+  /**
+   * Prints the toString value of the next variant, and returns its start
+   * location
+   * 
+   * @param features
+   * @return
+   */
+  protected int printNext(CloseableIterator<VariantContext> features)
+  {
+    VariantContext next = features.next();
+    System.out.println(next.toString());
+    return next.getStart();
+  }
+
+  // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+  
+  /**
+   * Test the query method that wraps a non-indexed VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testQuery_plain() throws IOException
+  {
+    File f = writeVcfFile();
+    VCFReader reader = new VCFReader(f.getAbsolutePath());
+
+    /*
+     * query for overlap of 5-8 - should find variant at 7
+     */
+    CloseableIterator<VariantContext> variants = reader.query("20", 5, 8);
+  
+    /*
+     * INDEL G/GA variant
+     */
+    VariantContext vc = variants.next();
+    assertTrue(vc.isIndel());
+    assertEquals(vc.getStart(), 7);
+    assertEquals(vc.getEnd(), 7);
+    Allele ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "G");
+    List<Allele> alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "G");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "GA");
+
+    assertFalse(variants.hasNext());
+
+    variants.close();
+    reader.close();
+  }
+}
index b76a295..31e1887 100644 (file)
@@ -107,4 +107,29 @@ public class SequenceOntologyTest
     assertFalse(so.isA("CDS_region", "CDS"));// part_of
     assertFalse(so.isA("polypeptide", "CDS")); // derives_from
   }
+
+  @Test(groups = "Functional")
+  public void testIsSequenceVariant()
+  {
+    assertFalse(so.isA("CDS", "sequence_variant"));
+    assertTrue(so.isA("sequence_variant", "sequence_variant"));
+
+    /*
+     * these should all be sub-types of sequence_variant
+     */
+    assertTrue(so.isA("structural_variant", "sequence_variant"));
+    assertTrue(so.isA("feature_variant", "sequence_variant"));
+    assertTrue(so.isA("gene_variant", "sequence_variant"));
+    assertTrue(so.isA("transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("NMD_transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("missense_variant", "sequence_variant"));
+    assertTrue(so.isA("synonymous_variant", "sequence_variant"));
+    assertTrue(so.isA("frameshift_variant", "sequence_variant"));
+    assertTrue(so.isA("5_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("3_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("stop_gained", "sequence_variant"));
+    assertTrue(so.isA("stop_lost", "sequence_variant"));
+    assertTrue(so.isA("inframe_deletion", "sequence_variant"));
+    assertTrue(so.isA("inframe_insertion", "sequence_variant"));
+  }
 }
index dd1a4de..b0aaab9 100644 (file)
@@ -26,10 +26,12 @@ import static org.testng.Assert.assertNotSame;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
+import jalview.api.FeatureColourI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
@@ -39,6 +41,7 @@ import jalview.io.FileLoader;
 import jalview.io.Jalview2xmlTests;
 import jalview.renderer.ResidueShaderI;
 import jalview.schemes.BuriedColourScheme;
+import jalview.schemes.FeatureColour;
 import jalview.schemes.HelixColourScheme;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.StrandColourScheme;
@@ -69,26 +72,34 @@ public class AlignFrameTest
   {
     SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ");
     SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ");
-    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5,
-            Float.NaN, null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10,
-            Float.NaN, null));
+    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null));
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f,
+            null));
     seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4,
             Float.NaN, null));
     seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9,
             Float.NaN, null));
     AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight());
+    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
+            al.getHeight());
+
+    /*
+     * make all features visible (select feature columns checks visibility)
+     */
+    alignFrame.getFeatureRenderer().findAllFeatures(true);
 
     /*
      * hiding a feature not present does nothing
      */
     assertFalse(alignFrame.hideFeatureColumns("exon", true));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
+
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 0);
+
     assertFalse(alignFrame.hideFeatureColumns("exon", false));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
+
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 0);
 
@@ -97,20 +108,40 @@ public class AlignFrameTest
      */
     assertFalse(alignFrame.hideFeatureColumns("Metal", true));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
+
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 0);
 
+
+    /*
+     * threshold Metal to hide features where score < 5
+     * seq1 feature in columns 1-5 is hidden
+     * seq2 feature in columns 6-10 is shown
+     */
+    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(5f);
+    alignFrame.getFeatureRenderer().setColour("Metal", fc);
+    assertTrue(alignFrame.hideFeatureColumns("Metal", true));
+    HiddenColumns hidden = alignFrame.getViewport().getAlignment().getHiddenColumns();
+    assertEquals(hidden.getNumberOfRegions(), 1);
+    Iterator<int[]> regions = hidden.iterator();
+    int[] next = regions.next();
+    assertEquals(next[0], 5);
+    assertEquals(next[1], 9);
+
     /*
      * hide a feature present in some columns
      * sequence positions [2-4], [7-9] are column positions
      * [1-3], [6-8] base zero
      */
+    alignFrame.getViewport().showAllHiddenColumns();
     assertTrue(alignFrame.hideFeatureColumns("Turn", true));
-    Iterator<int[]> regions = alignFrame.getViewport().getAlignment()
+    regions = alignFrame.getViewport().getAlignment()
             .getHiddenColumns().iterator();
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 2);
-    int[] next = regions.next();
+    next = regions.next();
     assertEquals(next[0], 1);
     assertEquals(next[1], 3);
     next = regions.next();
diff --git a/test/jalview/gui/FeatureSettingsTest.java b/test/jalview/gui/FeatureSettingsTest.java
new file mode 100644 (file)
index 0000000..6ddebf8
--- /dev/null
@@ -0,0 +1,191 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.api.FeatureColourI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
+import jalview.util.matcher.Condition;
+
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.testng.annotations.Test;
+
+public class FeatureSettingsTest
+{
+  /**
+   * Test a roundtrip of save and reload of feature colours and filters as XML
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testSaveLoad() throws IOException
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE);
+    SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0);
+
+    /*
+     * add some features to the sequence
+     */
+    int score = 1;
+    addFeatures(seq1, "type1", score++);
+    addFeatures(seq1, "type2", score++);
+    addFeatures(seq1, "type3", score++);
+    addFeatures(seq1, "type4", score++);
+    addFeatures(seq1, "type5", score++);
+
+    /*
+     * set colour schemes for features
+     */
+    FeatureRenderer fr = af.getFeatureRenderer();
+
+    // type1: red
+    fr.setColour("type1", new FeatureColour(Color.red));
+
+    // type2: by label
+    FeatureColourI byLabel = new FeatureColour();
+    byLabel.setColourByLabel(true);
+    fr.setColour("type2", byLabel);
+
+    // type3: by score above threshold
+    FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1,
+            10);
+    byScore.setAboveThreshold(true);
+    byScore.setThreshold(2f);
+    fr.setColour("type3", byScore);
+
+    // type4: by attribute AF
+    FeatureColourI byAF = new FeatureColour();
+    byAF.setColourByLabel(true);
+    byAF.setAttributeName("AF");
+    fr.setColour("type4", byAF);
+
+    // type5: by attribute CSQ:PolyPhen below threshold
+    FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE,
+            1, 10);
+    byPolyPhen.setBelowThreshold(true);
+    byPolyPhen.setThreshold(3f);
+    byPolyPhen.setAttributeName("CSQ", "PolyPhen");
+    fr.setColour("type5", byPolyPhen);
+
+    /*
+     * set filters for feature types
+     */
+
+    // filter type1 features by (label contains "x")
+    FeatureMatcherSetI filterByX = new FeatureMatcherSet();
+    filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x"));
+    fr.setFeatureFilter("type1", filterByX);
+
+    // filter type2 features by (score <= 2.4 and score > 1.1)
+    FeatureMatcherSetI filterByScore = new FeatureMatcherSet();
+    filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4"));
+    filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1"));
+    fr.setFeatureFilter("type2", filterByScore);
+
+    // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0)
+    FeatureMatcherSetI filterByXY = new FeatureMatcherSet();
+    filterByXY
+            .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF"));
+    filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ",
+            "PolyPhen"));
+    fr.setFeatureFilter("type3", filterByXY);
+
+    /*
+     * save colours and filters to an XML file
+     */
+    File coloursFile = File.createTempFile("testSaveLoad", ".fc");
+    coloursFile.deleteOnExit();
+    FeatureSettings fs = new FeatureSettings(af);
+    fs.save(coloursFile);
+
+    /*
+     * change feature colours and filters
+     */
+    FeatureColourI pink = new FeatureColour(Color.pink);
+    fr.setColour("type1", pink);
+    fr.setColour("type2", pink);
+    fr.setColour("type3", pink);
+    fr.setColour("type4", pink);
+    fr.setColour("type5", pink);
+
+    FeatureMatcherSetI filter2 = new FeatureMatcherSet();
+    filter2.and(FeatureMatcher.byLabel(Condition.NotContains, "y"));
+    fr.setFeatureFilter("type1", filter2);
+    fr.setFeatureFilter("type2", filter2);
+    fr.setFeatureFilter("type3", filter2);
+    fr.setFeatureFilter("type4", filter2);
+    fr.setFeatureFilter("type5", filter2);
+
+    /*
+     * reload colours and filters from file and verify they are restored
+     */
+    fs.load(coloursFile);
+    FeatureColourI fc = fr.getFeatureStyle("type1");
+    assertTrue(fc.isSimpleColour());
+    assertEquals(fc.getColour(), Color.red);
+    fc = fr.getFeatureStyle("type2");
+    assertTrue(fc.isColourByLabel());
+    fc = fr.getFeatureStyle("type3");
+    assertTrue(fc.isGraduatedColour());
+    assertNull(fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(fc.getThreshold(), 2f);
+    fc = fr.getFeatureStyle("type4");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "AF" });
+    fc = fr.getFeatureStyle("type5");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" });
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(fc.getThreshold(), 3f);
+
+    assertEquals(fr.getFeatureFilter("type1").toStableString(), "Label Contains x");
+    assertEquals(fr.getFeatureFilter("type2").toStableString(),
+            "(Score LE 2.4) AND (Score GT 1.1)");
+    assertEquals(fr.getFeatureFilter("type3").toStableString(),
+            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
+  }
+
+  /**
+   * Adds two features of the given type to the given sequence, also setting the
+   * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen"
+   * 
+   * @param seq
+   * @param featureType
+   * @param score
+   */
+  private void addFeatures(SequenceI seq, String featureType, int score)
+  {
+    addFeature(seq, featureType, score++);
+    addFeature(seq, featureType, score);
+  }
+
+  private void addFeature(SequenceI seq, String featureType, int score)
+  {
+    SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2,
+            score, "grp");
+    sf.setValue("AF", score);
+    sf.setValue("CSQ", new HashMap<String, String>()
+    {
+      {
+        put("PolyPhen", Integer.toString(score));
+      }
+    });
+    seq.addSequenceFeature(sf);
+  }
+}
index 8f60021..6f60588 100644 (file)
@@ -26,6 +26,7 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
@@ -34,16 +35,21 @@ import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
 import jalview.io.FormatAdapter;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
 import jalview.util.MessageManager;
+import jalview.util.UrlConstants;
 
 import java.awt.Component;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
@@ -84,6 +90,25 @@ public class PopupMenuTest
   @BeforeMethod(alwaysRun = true)
   public void setUp() throws IOException
   {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
+            + SEQUENCE_ID
+            + "$"
+            + "|"
+            + "UNIPROT | http://www.uniprot.org/uniprot/$" + DB_ACCESSION + "$")
+            + "|"
+            + ("INTERPRO | http://www.ebi.ac.uk/interpro/entry/$"
+                    + DB_ACCESSION + "$")
+            + "|"
+            +
+            // Gene3D entry tests for case (in)sensitivity
+            ("Gene3D | http://gene3d.biochem.ucl.ac.uk/Gene3D/search?sterm=$"
+                    + DB_ACCESSION + "$&mode=protein");
+
+    UrlProviderFactoryI factory = new DesktopUrlProviderFactory(
+            UrlConstants.DEFAULT_LABEL, inMenuString, "");
+    Preferences.sequenceUrlLinks = factory.createUrlProvider();
+
     alignment = new FormatAdapter().readFile(TEST_DATA,
             DataSourceType.PASTE, FileFormat.Fasta);
     AlignFrame af = new AlignFrame(alignment, 700, 500);
@@ -499,17 +524,19 @@ public class PopupMenuTest
 
     // add all the dbrefs to the sequences: Uniprot 1 each, Interpro all 3 to
     // seq0, Gene3D to seq1
-    seqs.get(0).addDBRef(refs.get(0));
+    SequenceI seq = seqs.get(0);
+    seq.addDBRef(refs.get(0));
 
-    seqs.get(0).addDBRef(refs.get(1));
-    seqs.get(0).addDBRef(refs.get(2));
-    seqs.get(0).addDBRef(refs.get(3));
+    seq.addDBRef(refs.get(1));
+    seq.addDBRef(refs.get(2));
+    seq.addDBRef(refs.get(3));
     
     seqs.get(1).addDBRef(refs.get(4));
     seqs.get(1).addDBRef(refs.get(5));
     
     // get the Popup Menu for first sequence
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0), links);
+    List<SequenceFeature> noFeatures = Collections.<SequenceFeature> emptyList();
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     Component[] seqItems = testee.sequenceMenu.getMenuComponents();
     JMenu linkMenu = (JMenu) seqItems[6];
     Component[] linkItems = linkMenu.getMenuComponents();
@@ -523,15 +550,18 @@ public class PopupMenuTest
     // sequence id for each link should match corresponding DB accession id
     for (int i = 1; i < 4; i++)
     {
-      assertEquals(refs.get(i - 1).getSource(), ((JMenuItem) linkItems[i])
+      String msg = seq.getName() + " link[" + i + "]";
+      assertEquals(msg, refs.get(i - 1).getSource(),
+              ((JMenuItem) linkItems[i])
               .getText().split("\\|")[0]);
-      assertEquals(refs.get(i - 1).getAccessionId(),
+      assertEquals(msg, refs.get(i - 1).getAccessionId(),
               ((JMenuItem) linkItems[i])
               .getText().split("\\|")[1]);
     }
 
     // get the Popup Menu for second sequence
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(1), links);
+    seq = seqs.get(1);
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     seqItems = testee.sequenceMenu.getMenuComponents();
     linkMenu = (JMenu) seqItems[6];
     linkItems = linkMenu.getMenuComponents();
@@ -545,9 +575,11 @@ public class PopupMenuTest
     // sequence id for each link should match corresponding DB accession id
     for (int i = 1; i < 3; i++)
     {
-      assertEquals(refs.get(i + 3).getSource(), ((JMenuItem) linkItems[i])
+      String msg = seq.getName() + " link[" + i + "]";
+      assertEquals(msg, refs.get(i + 3).getSource(),
+              ((JMenuItem) linkItems[i])
               .getText().split("\\|")[0].toUpperCase());
-      assertEquals(refs.get(i + 3).getAccessionId(),
+      assertEquals(msg, refs.get(i + 3).getAccessionId(),
               ((JMenuItem) linkItems[i]).getText().split("\\|")[1]);
     }
 
@@ -556,8 +588,7 @@ public class PopupMenuTest
     nomatchlinks.add("NOMATCH | http://www.uniprot.org/uniprot/$"
             + DB_ACCESSION + "$");
 
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0),
-            nomatchlinks);
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     seqItems = testee.sequenceMenu.getMenuComponents();
     linkMenu = (JMenu) seqItems[6];
     assertFalse(linkMenu.isEnabled());
@@ -597,7 +628,7 @@ public class PopupMenuTest
             .revealAllHiddenColumns(sel);
 
     // get the Popup Menu for 7th sequence - no insertions
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(7), null);
+    testee = new PopupMenu(parentPanel, seqs.get(7), null);
     testee.hideInsertions_actionPerformed(null);
     
     HiddenColumns hidden = parentPanel.av.getAlignment().getHiddenColumns();
@@ -605,7 +636,7 @@ public class PopupMenuTest
     assertFalse(it.hasNext());
 
     // get the Popup Menu for GappySeq - this time we have insertions
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(4), null);
+    testee = new PopupMenu(parentPanel, seqs.get(4), null);
     testee.hideInsertions_actionPerformed(null);
     hidden = parentPanel.av.getAlignment().getHiddenColumns();
     it = hidden.iterator();
@@ -655,7 +686,7 @@ public class PopupMenuTest
     hidden.hideColumns(31, 40);
 
     // get the Popup Menu for LessGappySeq in the sequence group
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(5), null);
+    testee = new PopupMenu(parentPanel, seqs.get(5), null);
     testee.hideInsertions_actionPerformed(null);
     hidden = parentPanel.av.getAlignment().getHiddenColumns();
     it = hidden.iterator();
index bcb1cfd..5298680 100644 (file)
@@ -29,10 +29,10 @@ import jalview.io.FileLoader;
 import java.awt.Font;
 import java.awt.FontMetrics;
 
-import org.testng.annotations.Test;
-
 import junit.extensions.PA;
 
+import org.testng.annotations.Test;
+
 public class SeqCanvasTest
 {
   /**
index ef7615b..2b8a62f 100644 (file)
@@ -39,6 +39,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import junit.extensions.PA;
 
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
@@ -104,9 +107,9 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
     // . codonframes
     //
     //
-    HashMap<String, String> dbtoviewBit = new HashMap<>();
+    Map<String, String> dbtoviewBit = new HashMap<>();
     List<String> keyseq = new ArrayList<>();
-    HashMap<String, File> savedProjects = new HashMap<>();
+    Map<String, File> savedProjects = new HashMap<>();
 
 //    for (String[] did : new String[][] { { "UNIPROT", "P00338" } })
 //    {
@@ -201,15 +204,16 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
 
             if (pass2 == 0)
             { // retrieve and show cross-refs in this thread
-              cra = new CrossRefAction(af, seqs, dna, db);
+              cra = CrossRefAction.getHandlerFor(seqs, dna, db, af);
               cra.run();
-              if (cra.getXrefViews().size() == 0)
+              cra_views = (List<AlignmentViewPanel>) PA.getValue(cra,
+                      "xrefViews");
+              if (cra_views.size() == 0)
               {
                 failedXrefMenuItems.add("No crossrefs retrieved for "
                         + first + " -> " + db);
                 continue;
               }
-              cra_views = cra.getXrefViews();
               assertNucleotide(cra_views.get(0),
                       "Nucleotide panel included proteins for " + first
                               + " -> " + db);
@@ -301,16 +305,18 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
 
                   if (pass3 == 0)
                   {
-
                     SequenceI[] xrseqs = avp.getAlignment()
                             .getSequencesArray();
                     AlignFrame nextaf = Desktop.getAlignFrameFor(avp
                             .getAlignViewport());
 
-                    cra = new CrossRefAction(nextaf, xrseqs, avp
-                            .getAlignViewport().isNucleotide(), xrefdb);
+                    cra = CrossRefAction.getHandlerFor(xrseqs, avp
+                            .getAlignViewport().isNucleotide(), xrefdb,
+                            nextaf);
                     cra.run();
-                    if (cra.getXrefViews().size() == 0)
+                    cra_views2 = (List<AlignmentViewPanel>) PA.getValue(
+                            cra, "xrefViews");
+                    if (cra_views2.size() == 0)
                     {
                       failedXrefMenuItems
                               .add("No crossrefs retrieved for '"
@@ -318,7 +324,6 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
                                       + " via '" + nextaf.getTitle() + "'");
                       continue;
                     }
-                    cra_views2 = cra.getXrefViews();
                     assertNucleotide(cra_views2.get(0),
                             "Nucleotide panel included proteins for '"
                                     + nextxref + "' to " + xrefdb
@@ -556,8 +561,8 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
    *          viewpanel needs to be called with a distinct xrefpath to ensure
    *          each one's strings are compared)
    */
-  private void stringify(HashMap<String, String> dbtoviewBit,
-          HashMap<String, File> savedProjects, String xrefpath,
+  private void stringify(Map<String, String> dbtoviewBit,
+          Map<String, File> savedProjects, String xrefpath,
           AlignmentViewPanel avp)
   {
     if (savedProjects != null)
index 152ab84..32ca841 100644 (file)
@@ -23,7 +23,9 @@ 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.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureRenderer;
@@ -32,11 +34,17 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
+import jalview.schemes.FeatureColour;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.matcher.Condition;
 
 import java.awt.Color;
 import java.io.File;
@@ -44,6 +52,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -467,10 +476,10 @@ public class FeaturesFileTest
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     Map<String, FeatureColourI> visible = fr.getDisplayedFeatureCols();
-    List<String> visibleGroups = new ArrayList<String>(
+    List<String> visibleGroups = new ArrayList<>(
             Arrays.asList(new String[] {}));
     String exported = featuresFile.printJalviewFormat(
-            al.getSequencesArray(), visible, visibleGroups, false);
+            al.getSequencesArray(), visible, null, visibleGroups, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
@@ -479,7 +488,7 @@ public class FeaturesFileTest
      */
     visibleGroups.add("uniprot");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, visibleGroups, true);
+            visible, null, 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
@@ -493,9 +502,9 @@ public class FeaturesFileTest
     fr.setVisible("GAMMA-TURN");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, visibleGroups, false);
+            visible, null, visibleGroups, false);
     expected = "METAL\tcc9900\n"
-            + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
+            + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
@@ -508,13 +517,13 @@ public class FeaturesFileTest
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, visibleGroups, false);
+            visible, null, visibleGroups, false);
     /*
      * 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"
+            + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
@@ -539,8 +548,8 @@ public class FeaturesFileTest
      */
     FeaturesFile featuresFile = new FeaturesFile();
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
-    Map<String, FeatureColourI> visible = new HashMap<String, FeatureColourI>();
-    List<String> visibleGroups = new ArrayList<String>(
+    Map<String, FeatureColourI> visible = new HashMap<>();
+    List<String> visibleGroups = new ArrayList<>(
             Arrays.asList(new String[] {}));
     String exported = featuresFile.printGffFormat(al.getSequencesArray(),
             visible, visibleGroups, false);
@@ -623,4 +632,79 @@ public class FeaturesFileTest
             + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n";
     assertEquals(expected, exported);
   }
+
+  /**
+   * Test for parsing of feature filters as represented in a Jalview features
+   * file
+   * 
+   * @throws Exception
+   */
+  @Test(groups = { "Functional" })
+  public void testParseFilters() throws Exception
+  {
+    Map<String, FeatureMatcherSetI> filters = new HashMap<>();
+    String text = "sequence_variant\tCSQ:PolyPhen NotContains 'damaging'\n"
+            + "missense_variant\t(label contains foobar) and (Score lt 1.3)";
+    FeaturesFile featuresFile = new FeaturesFile(text,
+            DataSourceType.PASTE);
+    featuresFile.parseFilters(filters);
+    assertEquals(filters.size(), 2);
+
+    FeatureMatcherSetI fm = filters.get("sequence_variant");
+    assertNotNull(fm);
+    Iterator<FeatureMatcherI> matchers = fm.getMatchers().iterator();
+    FeatureMatcherI matcher = matchers.next();
+    assertFalse(matchers.hasNext());
+    String[] attributes = matcher.getAttribute();
+    assertArrayEquals(attributes, new String[] { "CSQ", "PolyPhen" });
+    assertSame(matcher.getMatcher().getCondition(), Condition.NotContains);
+    assertEquals(matcher.getMatcher().getPattern(), "damaging");
+
+    fm = filters.get("missense_variant");
+    assertNotNull(fm);
+    matchers = fm.getMatchers().iterator();
+    matcher = matchers.next();
+    assertTrue(matcher.isByLabel());
+    assertSame(matcher.getMatcher().getCondition(), Condition.Contains);
+    assertEquals(matcher.getMatcher().getPattern(), "foobar");
+    matcher = matchers.next();
+    assertTrue(matcher.isByScore());
+    assertSame(matcher.getMatcher().getCondition(), Condition.LT);
+    assertEquals(matcher.getMatcher().getPattern(), "1.3");
+    assertEquals(matcher.getMatcher().getFloatValue(), 1.3f);
+
+    assertFalse(matchers.hasNext());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testOutputFeatureFilters()
+  {
+    FeaturesFile ff = new FeaturesFile();
+    StringBuilder sb = new StringBuilder();
+    Map<String, FeatureColourI> visible = new HashMap<>();
+    visible.put("pfam", new FeatureColour(Color.red));
+    Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+
+    // with no filters, nothing is output
+    ff.outputFeatureFilters(sb, visible, featureFilters);
+    assertEquals("", sb.toString());
+
+    // with filter for not visible features only, nothing is output
+    FeatureMatcherSet filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byLabel(Condition.Present, null));
+    featureFilters.put("foobar", filter);
+    ff.outputFeatureFilters(sb, visible, featureFilters);
+    assertEquals("", sb.toString());
+
+    // with filters for visible feature types
+    FeatureMatcherSet filter2 = new FeatureMatcherSet();
+    filter2.and(FeatureMatcher.byAttribute(Condition.Present, null, "CSQ",
+            "PolyPhen"));
+    filter2.and(FeatureMatcher.byScore(Condition.LE, "-2.4"));
+    featureFilters.put("pfam", filter2);
+    visible.put("foobar", new FeatureColour(Color.blue));
+    ff.outputFeatureFilters(sb, visible, featureFilters);
+    String expected = "\nSTARTFILTERS\nfoobar\tLabel Present\npfam\t(CSQ:PolyPhen Present) AND (Score LE -2.4)\nENDFILTERS\n\n";
+    assertEquals(expected, sb.toString());
+  }
 }
index c0eb8c5..53bb0e7 100644 (file)
@@ -23,11 +23,13 @@ package jalview.io;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureColourI;
 import jalview.api.ViewStyleI;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
@@ -35,12 +37,17 @@ import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.Desktop;
+import jalview.gui.FeatureRenderer;
 import jalview.gui.Jalview2XML;
 import jalview.gui.JvOptionPane;
 import jalview.gui.PopupMenu;
@@ -50,13 +57,16 @@ import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.BuriedColourScheme;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
+import jalview.schemes.FeatureColour;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.RNAHelicesColour;
 import jalview.schemes.StrandColourScheme;
 import jalview.schemes.TCoffeeColourScheme;
 import jalview.structure.StructureImportSettings;
+import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 
+import java.awt.Color;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -866,4 +876,163 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     assertTrue(rs.conservationApplied());
     assertEquals(rs.getConservationInc(), 30);
   }
+
+  /**
+   * Test save and reload of feature colour schemes and filter settings
+   * 
+   * @throws IOException
+   */
+  @Test(groups = { "Functional" })
+  public void testSaveLoadFeatureColoursAndFilters() throws IOException
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE);
+    SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0);
+
+    /*
+     * add some features to the sequence
+     */
+    int score = 1;
+    addFeatures(seq1, "type1", score++);
+    addFeatures(seq1, "type2", score++);
+    addFeatures(seq1, "type3", score++);
+    addFeatures(seq1, "type4", score++);
+    addFeatures(seq1, "type5", score++);
+
+    /*
+     * set colour schemes for features
+     */
+    FeatureRenderer fr = af.getFeatureRenderer();
+    fr.findAllFeatures(true);
+
+    // type1: red
+    fr.setColour("type1", new FeatureColour(Color.red));
+
+    // type2: by label
+    FeatureColourI byLabel = new FeatureColour();
+    byLabel.setColourByLabel(true);
+    fr.setColour("type2", byLabel);
+
+    // type3: by score above threshold
+    FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1,
+            10);
+    byScore.setAboveThreshold(true);
+    byScore.setThreshold(2f);
+    fr.setColour("type3", byScore);
+
+    // type4: by attribute AF
+    FeatureColourI byAF = new FeatureColour();
+    byAF.setColourByLabel(true);
+    byAF.setAttributeName("AF");
+    fr.setColour("type4", byAF);
+
+    // type5: by attribute CSQ:PolyPhen below threshold
+    FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE,
+            1, 10);
+    byPolyPhen.setBelowThreshold(true);
+    byPolyPhen.setThreshold(3f);
+    byPolyPhen.setAttributeName("CSQ", "PolyPhen");
+    fr.setColour("type5", byPolyPhen);
+
+    /*
+     * set filters for feature types
+     */
+
+    // filter type1 features by (label contains "x")
+    FeatureMatcherSetI filterByX = new FeatureMatcherSet();
+    filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x"));
+    fr.setFeatureFilter("type1", filterByX);
+
+    // filter type2 features by (score <= 2.4 and score > 1.1)
+    FeatureMatcherSetI filterByScore = new FeatureMatcherSet();
+    filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4"));
+    filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1"));
+    fr.setFeatureFilter("type2", filterByScore);
+
+    // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0)
+    FeatureMatcherSetI filterByXY = new FeatureMatcherSet();
+    filterByXY
+            .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF"));
+    filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ",
+            "PolyPhen"));
+    fr.setFeatureFilter("type3", filterByXY);
+
+    /*
+     * save as Jalview project
+     */
+    File tfile = File.createTempFile("JalviewTest", ".jvp");
+    tfile.deleteOnExit();
+    String filePath = tfile.getAbsolutePath();
+    assertTrue(af.saveAlignment(filePath, FileFormat.Jalview),
+            "Failed to store as a project.");
+
+    /*
+     * close current alignment and load the saved project
+     */
+    af.closeMenuItem_actionPerformed(true);
+    af = null;
+    af = new FileLoader()
+            .LoadFileWaitTillLoaded(filePath, DataSourceType.FILE);
+    assertNotNull(af, "Failed to import new project");
+
+    /*
+     * verify restored feature colour schemes and filters
+     */
+    fr = af.getFeatureRenderer();
+    FeatureColourI fc = fr.getFeatureStyle("type1");
+    assertTrue(fc.isSimpleColour());
+    assertEquals(fc.getColour(), Color.red);
+    fc = fr.getFeatureStyle("type2");
+    assertTrue(fc.isColourByLabel());
+    fc = fr.getFeatureStyle("type3");
+    assertTrue(fc.isGraduatedColour());
+    assertNull(fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(fc.getThreshold(), 2f);
+    fc = fr.getFeatureStyle("type4");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "AF" });
+    fc = fr.getFeatureStyle("type5");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" });
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(fc.getThreshold(), 3f);
+
+    assertEquals(fr.getFeatureFilter("type1").toStableString(),
+            "Label Contains x");
+    assertEquals(fr.getFeatureFilter("type2").toStableString(),
+            "(Score LE 2.4) AND (Score GT 1.1)");
+    assertEquals(fr.getFeatureFilter("type3").toStableString(),
+            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
+  }
+
+  private void addFeature(SequenceI seq, String featureType, int score)
+  {
+    SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2,
+            score, "grp");
+    sf.setValue("AF", score);
+    sf.setValue("CSQ", new HashMap<String, String>()
+    {
+      {
+        put("PolyPhen", Integer.toString(score));
+      }
+    });
+    seq.addSequenceFeature(sf);
+  }
+
+  /**
+   * Adds two features of the given type to the given sequence, also setting the
+   * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen"
+   * 
+   * @param seq
+   * @param featureType
+   * @param score
+   */
+  private void addFeatures(SequenceI seq, String featureType, int score)
+  {
+    addFeature(seq, featureType, score++);
+    addFeature(seq, featureType, score);
+  }
 }
index 9e61bec..87e35c7 100644 (file)
@@ -23,15 +23,18 @@ package jalview.io;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.api.FeatureColourI;
 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 jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
-import java.util.HashMap;
-import java.util.Hashtable;
+import java.awt.Color;
 import java.util.Map;
 
 import junit.extensions.PA;
@@ -95,8 +98,9 @@ public class SequenceAnnotationReportTest
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f,
             "group");
 
-    Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
-    sar.appendFeature(sb, 1, minmax, sf);
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
+    sar.appendFeature(sb, 1, fr, sf);
     /*
      * map has no entry for this feature type - score is not shown:
      */
@@ -106,7 +110,7 @@ public class SequenceAnnotationReportTest
      * map has entry for this feature type - score is shown:
      */
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
     // <br> is appended to a buffer > 6 in length
     assertEquals("METAL 1 3; Fe2-S<br>METAL 1 3; Fe2-S Score=1.3",
             sb.toString());
@@ -116,7 +120,7 @@ public class SequenceAnnotationReportTest
      */
     minmax.put("METAL", new float[][] { { 2f, 2f }, null });
     sb.setLength(0);
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -132,8 +136,11 @@ public class SequenceAnnotationReportTest
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
+  /**
+   * A specific attribute value is included if it is used to colour the feature
+   */
   @Test(groups = "Functional")
-  public void testAppendFeature_clinicalSignificance()
+  public void testAppendFeature_colouredByAttribute()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
     StringBuilder sb = new StringBuilder();
@@ -141,12 +148,35 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
     sf.setValue("clinical_significance", "Benign");
 
-    sar.appendFeature(sb, 1, null, sf);
-    assertEquals("METAL 1 3; Fe2-S; Benign", sb.toString());
+    /*
+     * first with no colour by attribute
+     */
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S", sb.toString());
+
+    /*
+     * then with colour by an attribute the feature lacks
+     */
+    FeatureColourI fc = new FeatureColour(Color.white, Color.black, 5, 10);
+    fc.setAttributeName("Pfam");
+    fr.setColour("METAL", fc);
+    sb.setLength(0);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
+
+    /*
+     * then with colour by an attribute the feature has
+     */
+    fc.setAttributeName("clinical_significance");
+    sb.setLength(0);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
+            sb.toString());
   }
 
   @Test(groups = "Functional")
-  public void testAppendFeature_withScoreStatusClinicalSignificance()
+  public void testAppendFeature_withScoreStatusAttribute()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
     StringBuilder sb = new StringBuilder();
@@ -154,11 +184,17 @@ public class SequenceAnnotationReportTest
             "group");
     sf.setStatus("Confirmed");
     sf.setValue("clinical_significance", "Benign");
-    Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
+
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
+    FeatureColourI fc = new FeatureColour(Color.white, Color.blue, 12, 22);
+    fc.setAttributeName("clinical_significance");
+    fr.setColour("METAL", fc);
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
 
-    assertEquals("METAL 1 3; Fe2-S Score=1.3; (Confirmed); Benign",
+    assertEquals(
+            "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
             sb.toString());
   }
 
@@ -226,7 +262,7 @@ public class SequenceAnnotationReportTest
             null));
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, true, null);
-    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos</i>";
+    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos Score=1.0</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -244,10 +280,13 @@ public class SequenceAnnotationReportTest
      */
     seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f,
             null));
-    Map<String, float[][]> minmax = new HashMap<String, float[][]>();
+
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
     minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
     expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
     assertEquals(expected, sb.toString());
     
@@ -260,19 +299,20 @@ public class SequenceAnnotationReportTest
     sf.setValue("linkonly", Boolean.TRUE);
     seq.addSequenceFeature(sf);
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
     assertEquals(expected, sb.toString()); // unchanged!
 
     /*
-     * 'clinical_significance' currently being specially included
+     * 'clinical_significance' attribute only included when
+     * used for feature colouring
      */
     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>";
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -280,18 +320,24 @@ public class SequenceAnnotationReportTest
      */
     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);
+    sar.createSequenceAnnotationReport(sb, seq, false, true, fr);
     assertEquals(expected, sb.toString()); // unchanged
-    // with showDbRefs = true
+
+    // with showDbRefs = true, colour Variant features by clinical_significance
     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>";
+    FeatureColourI fc = new FeatureColour(Color.green, Color.pink, 2, 3);
+    fc.setAttributeName("clinical_significance");
+    fr.setColour("Variant", fc);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>"
+            + "Type1 ; Nonpos<br>Variant ; Havana; clinical_significance=benign</i>";
     assertEquals(expected, sb.toString());
     // with showNonPositionalFeatures = false
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, fr);
     expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
     assertEquals(expected, sb.toString());
 
diff --git a/test/jalview/io/gff/SequenceOntologyLiteTest.java b/test/jalview/io/gff/SequenceOntologyLiteTest.java
new file mode 100644 (file)
index 0000000..0766666
--- /dev/null
@@ -0,0 +1,37 @@
+package jalview.io.gff;
+
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import org.testng.annotations.Test;
+
+public class SequenceOntologyLiteTest
+{
+  @Test(groups = "Functional")
+  public void testIsA_sequenceVariant()
+  {
+    SequenceOntologyI so = new SequenceOntologyLite();
+
+    assertFalse(so.isA("CDS", "sequence_variant"));
+    assertTrue(so.isA("sequence_variant", "sequence_variant"));
+
+    /*
+     * these should all be sub-types of sequence_variant
+     */
+    assertTrue(so.isA("structural_variant", "sequence_variant"));
+    assertTrue(so.isA("feature_variant", "sequence_variant"));
+    assertTrue(so.isA("gene_variant", "sequence_variant"));
+    assertTrue(so.isA("transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("NMD_transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("missense_variant", "sequence_variant"));
+    assertTrue(so.isA("synonymous_variant", "sequence_variant"));
+    assertTrue(so.isA("frameshift_variant", "sequence_variant"));
+    assertTrue(so.isA("5_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("3_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("stop_gained", "sequence_variant"));
+    assertTrue(so.isA("stop_lost", "sequence_variant"));
+    assertTrue(so.isA("inframe_deletion", "sequence_variant"));
+    assertTrue(so.isA("inframe_insertion", "sequence_variant"));
+    assertTrue(so.isA("splice_region_variant", "sequence_variant"));
+  }
+}
diff --git a/test/jalview/io/vcf/VCFLoaderTest.java b/test/jalview/io/vcf/VCFLoaderTest.java
new file mode 100644 (file)
index 0000000..7e3c0b4
--- /dev/null
@@ -0,0 +1,681 @@
+package jalview.io.vcf;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class VCFLoaderTest
+{
+  private static final float DELTA = 0.00001f;
+
+  // columns 9717- of gene P30419 from Ensembl (much modified)
+  private static final String FASTA = ""
+          +
+          /*
+           * forward strand 'gene' and 'transcript' with two exons
+           */
+          ">gene1/1-25 chromosome:GRCh38:17:45051610:45051634:1\n"
+          + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+          + ">transcript1/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+
+          /*
+           * reverse strand gene and transcript (reverse complement alleles!)
+           */
+          + ">gene2/1-25 chromosome:GRCh38:17:45051610:45051634:-1\n"
+          + "TGTCACACTCTCGTCCGCCAGCTTG\n"
+          + ">transcript2/1-18\n" + "-GTCACACTCT----CGCCAGCT--\n"
+
+          /*
+           * 'gene' on chromosome 5 with two transcripts
+           */
+          + ">gene3/1-25 chromosome:GRCh38:5:45051610:45051634:1\n"
+          + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+          + ">transcript3/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+          + ">transcript4/1-18\n-----TGG-GGACGAGAGTGTGA-A\n";
+
+  private static final String[] VCF = { "##fileformat=VCFv4.2",
+      "##INFO=<ID=AF,Number=A,Type=Float,Description=\"Allele Frequency, for each ALT allele, in the same order as listed\">",
+      "##reference=Homo_sapiens/GRCh38",
+      "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+      // A/T,C variants in position 2 of gene sequence (precedes transcript)
+      // should create 2 variant features with respective scores
+      "17\t45051611\t.\tA\tT,C\t1666.64\tRF\tAC=15;AF=5.0e-03,4.0e-03",
+      // SNP G/C in position 4 of gene sequence, position 2 of transcript
+      // insertion G/GA is transferred to nucleotide but not to peptide
+      "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.0e-03,2.0e-03" };
+
+  @BeforeClass
+  public void setUp()
+  {
+    /*
+     * configure to capture all available VCF and VEP (CSQ) fields
+     */
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Cache.setProperty("VCF_FIELDS", ".*");
+    Cache.setProperty("VEP_FIELDS", ".*");
+    Cache.initLogger();
+  }
+
+  @Test(groups = "Functional")
+  public void testDoLoad() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    File f = makeVcf();
+    VCFLoader loader = new VCFLoader(f.getPath());
+
+    loader.doLoad(al.getSequencesArray(), null);
+
+    /*
+     * verify variant feature(s) added to gene
+     * NB alleles at a locus may not be processed, and features added,
+     * in the order in which they appear in the VCF record as method
+     * VariantContext.getAlternateAlleles() does not guarantee order
+     * - order of assertions here matches what we find (is not important) 
+     */
+    List<SequenceFeature> geneFeatures = al.getSequenceAt(0)
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(geneFeatures, true);
+    assertEquals(geneFeatures.size(), 4);
+    SequenceFeature sf = geneFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getScore(), 4.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,C");
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 5.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,T");
+
+    sf = geneFeatures.get(2);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+
+    sf = geneFeatures.get(3);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+
+    /*
+     * verify variant feature(s) added to transcript
+     */
+    List<SequenceFeature> transcriptFeatures = al.getSequenceAt(1)
+            .getSequenceFeatures();
+    assertEquals(transcriptFeatures.size(), 2);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+
+    /*
+     * verify SNP variant feature(s) computed and added to protein
+     * first codon AGC varies to ACC giving S/T
+     */
+    DBRefEntry[] dbRefs = al.getSequenceAt(1).getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    assertEquals(proteinFeatures.size(), 1);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "p.Ser1Thr");
+  }
+
+  private File makeVcf() throws IOException
+  {
+    File f = File.createTempFile("Test", ".vcf");
+    f.deleteOnExit();
+    PrintWriter pw = new PrintWriter(f);
+    for (String vcfLine : VCF)
+    {
+      pw.println(vcfLine);
+    }
+    pw.close();
+    return f;
+  }
+
+  /**
+   * Make a simple alignment with one 'gene' and one 'transcript'
+   * 
+   * @return
+   */
+  private AlignmentI buildAlignment()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(FASTA,
+            DataSourceType.PASTE);
+
+    /*
+     * map gene1 sequence to chromosome (normally done when the sequence is fetched
+     * from Ensembl and transcripts computed)
+     */
+    AlignmentI alignment = af.getViewport().getAlignment();
+    SequenceI gene1 = alignment.findName("gene1");
+    int[] to = new int[] { 45051610, 45051634 };
+    int[] from = new int[] { gene1.getStart(), gene1.getEnd() };
+    gene1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map 'transcript1' to chromosome via 'gene1'
+     * transcript1/1-18 is gene1/3-10,15-24
+     * which is chromosome 45051612-45051619,45051624-45051633
+     */
+    to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+    SequenceI transcript1 = alignment.findName("transcript1");
+    from = new int[] { transcript1.getStart(), transcript1.getEnd() };
+    transcript1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * map gene2 to chromosome reverse strand
+     */
+    SequenceI gene2 = alignment.findName("gene2");
+    to = new int[] { 45051634, 45051610 };
+    from = new int[] { gene2.getStart(), gene2.getEnd() };
+    gene2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map 'transcript2' to chromosome via 'gene2'
+     * transcript2/1-18 is gene2/2-11,16-23
+     * which is chromosome 45051633-45051624,45051619-45051612
+     */
+    to = new int[] { 45051633, 45051624, 45051619, 45051612 };
+    SequenceI transcript2 = alignment.findName("transcript2");
+    from = new int[] { transcript2.getStart(), transcript2.getEnd() };
+    transcript2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * add a protein product as a DBRef on transcript1
+     */
+    SequenceI peptide1 = new Sequence("ENSP001", "SWRECD");
+    MapList mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 },
+            3, 1);
+    Mapping map = new Mapping(peptide1, mapList);
+    DBRefEntry product = new DBRefEntry("", "", "ENSP001", map);
+    transcript1.addDBRef(product);
+
+    /*
+     * add a protein product as a DBRef on transcript2
+     */
+    SequenceI peptide2 = new Sequence("ENSP002", "VTLSPA");
+    mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+    map = new Mapping(peptide2, mapList);
+    product = new DBRefEntry("", "", "ENSP002", map);
+    transcript2.addDBRef(product);
+
+    /*
+     * map gene3 to chromosome 
+     */
+    SequenceI gene3 = alignment.findName("gene3");
+    to = new int[] { 45051610, 45051634 };
+    from = new int[] { gene3.getStart(), gene3.getEnd() };
+    gene3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map 'transcript3' to chromosome
+     */
+    SequenceI transcript3 = alignment.findName("transcript3");
+    to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+    from = new int[] { transcript3.getStart(), transcript3.getEnd() };
+    transcript3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * map 'transcript4' to chromosome
+     */
+    SequenceI transcript4 = alignment.findName("transcript4");
+    to = new int[] { 45051615, 45051617, 45051619, 45051632, 45051634,
+        45051634 };
+    from = new int[] { transcript4.getStart(), transcript4.getEnd() };
+    transcript4.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * add a protein product as a DBRef on transcript3
+     */
+    SequenceI peptide3 = new Sequence("ENSP003", "SWRECD");
+    mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+    map = new Mapping(peptide3, mapList);
+    product = new DBRefEntry("", "", "ENSP003", map);
+    transcript3.addDBRef(product);
+
+    return alignment;
+  }
+
+  /**
+   * Test with 'gene' and 'transcript' mapped to the reverse strand of the
+   * chromosome. The VCF variant positions (in forward coordinates) should get
+   * correctly located on sequence positions.
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testDoLoad_reverseStrand() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    File f = makeVcf();
+
+    VCFLoader loader = new VCFLoader(f.getPath());
+
+    loader.doLoad(al.getSequencesArray(), null);
+
+    /*
+     * verify variant feature(s) added to gene2
+     * gene2/1-25 maps to chromosome 45051634- reverse strand
+     */
+    List<SequenceFeature> geneFeatures = al.getSequenceAt(2)
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(geneFeatures, true);
+    assertEquals(geneFeatures.size(), 4);
+
+    /*
+     * variant A/T at 45051611 maps to T/A at gene position 24
+     */
+    SequenceFeature sf = geneFeatures.get(3);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 5.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,A");
+
+    /*
+     * variant A/C at 45051611 maps to T/G at gene position 24
+     */
+    sf = geneFeatures.get(2);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 4.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,G");
+
+    /*
+     * variant G/C at 45051613 maps to C/G at gene position 22
+     */
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 22);
+    assertEquals(sf.getEnd(), 22);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
+
+    /*
+     * insertion G/GA at 45051613 maps to an insertion at
+     * the preceding position (21) on reverse strand gene
+     * reference: CAAGC -> GCTTG/21-25
+     * genomic variant: CAAGAC (G/GA)
+     * gene variant: GTCTTG (G/GT at 21)
+     */
+    sf = geneFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 21);
+    assertEquals(sf.getEnd(), 21);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
+
+    /*
+     * verify 2 variant features added to transcript2
+     */
+    List<SequenceFeature> transcriptFeatures = al.getSequenceAt(3)
+            .getSequenceFeatures();
+    assertEquals(transcriptFeatures.size(), 2);
+
+    /*
+     * insertion G/GT at position 21 of gene maps to position 16 of transcript
+     */
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 16);
+    assertEquals(sf.getEnd(), 16);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
+
+    /*
+     * SNP C/G at position 22 of gene maps to position 17 of transcript
+     */
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
+
+    /*
+     * verify variant feature(s) computed and added to protein
+     * last codon GCT varies to GGT giving A/G in the last peptide position
+     */
+    DBRefEntry[] dbRefs = al.getSequenceAt(3).getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    assertEquals(proteinFeatures.size(), 1);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 6);
+    assertEquals(sf.getEnd(), 6);
+    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "p.Ala6Gly");
+  }
+
+  /**
+   * Tests that if VEP consequence (CSQ) data is present in the VCF data, then
+   * it is added to the variant feature, but restricted where possible to the
+   * consequences for a specific transcript
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testDoLoad_vepCsq() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    VCFLoader loader = new VCFLoader("test/jalview/io/vcf/testVcf.vcf");
+
+    /*
+     * VCF data file with variants at gene3 positions
+     * 1 C/A
+     * 5 C/T
+     * 9 CGT/C (deletion)
+     * 13 C/G, C/T
+     * 17 A/AC (insertion), A/G
+     */
+    loader.doLoad(al.getSequencesArray(), null);
+
+    /*
+     * verify variant feature(s) added to gene3
+     */
+    List<SequenceFeature> geneFeatures = al.findName("gene3")
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(geneFeatures, true);
+    assertEquals(geneFeatures.size(), 7);
+    SequenceFeature sf = geneFeatures.get(0);
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getScore(), 0.1f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,A");
+    // gene features include Consequence for all transcripts
+    Map map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getBegin(), 5);
+    assertEquals(sf.getEnd(), 5);
+    assertEquals(sf.getScore(), 0.2f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(2);
+    assertEquals(sf.getBegin(), 9);
+    assertEquals(sf.getEnd(), 11); // deletion over 3 positions
+    assertEquals(sf.getScore(), 0.3f, DELTA);
+    assertEquals(sf.getValue("alleles"), "CGG,C");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(3);
+    assertEquals(sf.getBegin(), 13);
+    assertEquals(sf.getEnd(), 13);
+    assertEquals(sf.getScore(), 0.5f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(4);
+    assertEquals(sf.getBegin(), 13);
+    assertEquals(sf.getEnd(), 13);
+    assertEquals(sf.getScore(), 0.4f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,G");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(5);
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17);
+    assertEquals(sf.getScore(), 0.7f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,G");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(6);
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17); // insertion
+    assertEquals(sf.getScore(), 0.6f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,AC");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    /*
+     * verify variant feature(s) added to transcript3
+     * at columns 5 (1), 17 (2), positions 3, 11
+     * note the deletion at columns 9-11 is not transferred since col 11
+     * has no mapping to transcript 3 
+     */
+    List<SequenceFeature> transcriptFeatures = al.findName("transcript3")
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(transcriptFeatures, true);
+    assertEquals(transcriptFeatures.size(), 3);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getBegin(), 3);
+    assertEquals(sf.getEnd(), 3);
+    assertEquals(sf.getScore(), 0.2f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    // transcript features only have Consequence for that transcripts
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.7f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,G");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+    sf = transcriptFeatures.get(2);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.6f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,AC");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+    /*
+     * verify variants computed on protein product for transcript3
+     * peptide is SWRECD
+     * codon variants are AGC/AGT position 1 which is synonymous
+     * and GAG/GGG which is E/G in position 4
+     * the insertion variant is not transferred to the peptide
+     */
+    DBRefEntry[] dbRefs = al.findName("transcript3").getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(proteinFeatures, true);
+    assertEquals(proteinFeatures.size(), 2);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "agC/agT");
+    sf = proteinFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "p.Glu4Gly");
+
+    /*
+     * verify variant feature(s) added to transcript4
+     * at columns 13 (2) and 17 (2), positions 7 and 11
+     */
+    transcriptFeatures = al.findName("transcript4").getSequenceFeatures();
+    SequenceFeatures.sortFeatures(transcriptFeatures, true);
+    assertEquals(transcriptFeatures.size(), 4);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getBegin(), 7);
+    assertEquals(sf.getEnd(), 7);
+    assertEquals(sf.getScore(), 0.5f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getBegin(), 7);
+    assertEquals(sf.getEnd(), 7);
+    assertEquals(sf.getScore(), 0.4f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,G");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+    sf = transcriptFeatures.get(2);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.7f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,G");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+    sf = transcriptFeatures.get(3);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.6f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,AC");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+  }
+
+  /**
+   * A test that demonstrates loading a contig sequence from an indexed sequence
+   * database which is the reference for a VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testLoadVCFContig() throws IOException
+  {
+    VCFLoader loader = new VCFLoader(
+            "test/jalview/io/vcf/testVcf2.vcf");
+
+    SequenceI seq = loader.loadVCFContig("contig123");
+    assertEquals(seq.getLength(), 15);
+    assertEquals(seq.getSequenceAsString(), "AAAAACCCCCGGGGG");
+    List<SequenceFeature> features = seq.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(features, true);
+    assertEquals(features.size(), 2);
+    SequenceFeature sf = features.get(0);
+    assertEquals(sf.getBegin(), 8);
+    assertEquals(sf.getEnd(), 8);
+    assertEquals(sf.getDescription(), "C,A");
+    sf = features.get(1);
+    assertEquals(sf.getBegin(), 12);
+    assertEquals(sf.getEnd(), 12);
+    assertEquals(sf.getDescription(), "G,T");
+
+    seq = loader.loadVCFContig("contig789");
+    assertEquals(seq.getLength(), 25);
+    assertEquals(seq.getSequenceAsString(), "GGGGGTTTTTAAAAACCCCCGGGGG");
+    features = seq.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(features, true);
+    assertEquals(features.size(), 2);
+    sf = features.get(0);
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getDescription(), "G,T");
+    sf = features.get(1);
+    assertEquals(sf.getBegin(), 21);
+    assertEquals(sf.getEnd(), 21);
+    assertEquals(sf.getDescription(), "G,A");
+
+    seq = loader.loadVCFContig("contig456");
+    assertEquals(seq.getLength(), 20);
+    assertEquals(seq.getSequenceAsString(), "CCCCCGGGGGTTTTTAAAAA");
+    features = seq.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(features, true);
+    assertEquals(features.size(), 1);
+    sf = features.get(0);
+    assertEquals(sf.getBegin(), 15);
+    assertEquals(sf.getEnd(), 15);
+    assertEquals(sf.getDescription(), "T,C");
+  }
+}
\ No newline at end of file
diff --git a/test/jalview/io/vcf/contigs.fasta b/test/jalview/io/vcf/contigs.fasta
new file mode 100644 (file)
index 0000000..ec839b6
--- /dev/null
@@ -0,0 +1,6 @@
+>contig123
+AAAAACCCCCGGGGG
+>contig456
+CCCCCGGGGGTTTTTAAAAA
+>contig789
+GGGGGTTTTTAAAAACCCCCGGGGG
diff --git a/test/jalview/io/vcf/contigs.fasta.fai b/test/jalview/io/vcf/contigs.fasta.fai
new file mode 100644 (file)
index 0000000..e9f5067
--- /dev/null
@@ -0,0 +1,3 @@
+contig123      15      11      15      16
+contig456      20      38      20      21
+contig789      25      70      25      26
diff --git a/test/jalview/io/vcf/testVcf.dat b/test/jalview/io/vcf/testVcf.dat
new file mode 100644 (file)
index 0000000..77e070c
--- /dev/null
@@ -0,0 +1,13 @@
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF_Female,Number=R,Type=Float,Description="Allele Frequency among Female genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AN,Number=1,Type=Integer,Description="Total number of alleles in called genotypes">
+##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence annotations from Ensembl VEP. Format: Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|PolyPhen">
+##reference=/Homo_sapiens/GRCh38
+#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
+5      45051610        .       C       A       81.96   RF;AC0  AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051618        .       CGG     C       41.94   AC0     AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051622        .       C       G,T     224.23  RF;AC0  AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051626        .       A       AC,G    433.35  RF;AC0  AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
diff --git a/test/jalview/io/vcf/testVcf.vcf b/test/jalview/io/vcf/testVcf.vcf
new file mode 100644 (file)
index 0000000..77e070c
--- /dev/null
@@ -0,0 +1,13 @@
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF_Female,Number=R,Type=Float,Description="Allele Frequency among Female genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AN,Number=1,Type=Integer,Description="Total number of alleles in called genotypes">
+##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence annotations from Ensembl VEP. Format: Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|PolyPhen">
+##reference=/Homo_sapiens/GRCh38
+#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
+5      45051610        .       C       A       81.96   RF;AC0  AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051618        .       CGG     C       41.94   AC0     AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051622        .       C       G,T     224.23  RF;AC0  AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051626        .       A       AC,G    433.35  RF;AC0  AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
diff --git a/test/jalview/io/vcf/testVcf2.vcf b/test/jalview/io/vcf/testVcf2.vcf
new file mode 100644 (file)
index 0000000..aa3792a
--- /dev/null
@@ -0,0 +1,13 @@
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##contig=<ID=contig123,length=15>
+##contig=<ID=contig456,length=20>
+##contig=<ID=contig789,length=25>
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##reference=test/jalview/io/vcf/contigs.fasta
+#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
+contig123      8       .       C       A       81.96   .       AC=1;AF=0.1
+contig123      12      .       G       T       1666.64 .       AC=1;AF=0.2
+contig456      15      .       T       C       41.94   .       AC=1;AF=0.3
+contig789      2       .       G       T       224.23  .       AC=1,2;AF=0
+contig789      21      .       G       A       433.35  .       AC=3;AF=0.6
index f6dfed6..d8b905e 100644 (file)
@@ -15,6 +15,7 @@ import jalview.gui.FeatureRenderer;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.Color;
 import java.util.List;
@@ -172,9 +173,9 @@ public class FeatureColourFinderTest
      * - currently no way other than mimicking reordering of
      * table in Feature Settings
      */
-    Object[][] data = new Object[2][];
-    data[0] = new Object[] { "Metal", red, true };
-    data[1] = new Object[] { "Domain", green, true };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+    data[0] = new FeatureSettingsBean("Metal", red, null, true);
+    data[1] = new FeatureSettingsBean("Domain", green, null, true);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.red);
@@ -182,7 +183,7 @@ public class FeatureColourFinderTest
     /*
      * ..and turn off display of Metal
      */
-    data[0][2] = false;
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.green);
@@ -216,8 +217,8 @@ public class FeatureColourFinderTest
     /*
      * turn off display of Metal - is this the easiest way to do it??
      */
-    Object[][] data = new Object[1][];
-    data[0] = new Object[] { "Metal", red, false };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.blue);
@@ -225,7 +226,7 @@ public class FeatureColourFinderTest
     /*
      * turn display of Metal back on
      */
-    data[0] = new Object[] { "Metal", red, true };
+    data[0] = new FeatureSettingsBean("Metal", red, null, true);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.red);
@@ -399,9 +400,9 @@ public class FeatureColourFinderTest
      * 1) 0.6 * green(0, 255, 0) + 0.4 * cyan(0, 255, 255) = (0, 255, 102)
      * 2) 0.6* red(255, 0, 0) + 0.4 * (0, 255, 102) = (153, 102, 41) rounded
      */
-    Object[][] data = new Object[2][];
-    data[0] = new Object[] { "Metal", red, true };
-    data[1] = new Object[] { "Domain", green, true };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+    data[0] = new FeatureSettingsBean("Metal", red, null, true);
+    data[1] = new FeatureSettingsBean("Domain", green, null, true);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.cyan, seq, 10);
     assertEquals(c, new Color(153, 102, 41));
@@ -411,7 +412,7 @@ public class FeatureColourFinderTest
      * Domain (green) above background (pink)
      * 0.6 * green(0, 255, 0) + 0.4 * pink(255, 175, 175) = (102, 223, 70)
      */
-    data[0][2] = false;
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.pink, seq, 10);
     assertEquals(c, new Color(102, 223, 70));
@@ -447,8 +448,8 @@ public class FeatureColourFinderTest
     /*
      * turn off display of Metal
      */
-    Object[][] data = new Object[1][];
-    data[0] = new Object[] { "Metal", red, false };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     assertTrue(finder.noFeaturesDisplayed());
 
@@ -503,9 +504,9 @@ public class FeatureColourFinderTest
     /*
      * 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 };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+    data[0] = new FeatureSettingsBean(kdFeature, fc, null, true);
+    data[1] = new FeatureSettingsBean(metalFeature, green, null, true);
     fr.setFeaturePriority(data);
 
     av.setShowSequenceFeatures(true);
index 6bb611c..11b129e 100644 (file)
@@ -22,20 +22,27 @@ package jalview.renderer.seqfeatures;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.AlignFrame;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
+import jalview.util.matcher.Condition;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.Color;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -81,9 +88,8 @@ public class FeatureRendererTest
     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"));
+    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));
@@ -136,13 +142,14 @@ public class FeatureRendererTest
      * change render order (todo: an easier way)
      * nb here last comes first in the data array
      */
-    Object[][] data = new Object[3][];
+    FeatureSettingsBean[] data = new FeatureSettingsBean[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 };
+    data[0] = new FeatureSettingsBean("Rfam", colour, null, true);
+    data[1] = new FeatureSettingsBean("Pfam", colour, null, false);
+    data[2] = new FeatureSettingsBean("Scop", colour, null, false);
     fr.setFeaturePriority(data);
-    assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam"));
+    assertEquals(fr.getRenderOrder(),
+            Arrays.asList("Scop", "Pfam", "Rfam"));
     assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
 
     /*
@@ -237,12 +244,13 @@ public class FeatureRendererTest
     /*
      * 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 };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[4];
+    data[0] = new FeatureSettingsBean("Type1", colour, null, true);
+    data[1] = new FeatureSettingsBean("Type2", colour, null, false);
+    data[2] = new FeatureSettingsBean("Type3", colour, null, true);
+    data[3] = new FeatureSettingsBean("Disulphide Bond", colour, null,
+            true);
     fr.setFeaturePriority(data);
 
     features = fr.findFeaturesAtColumn(seq, 15);
@@ -272,6 +280,37 @@ public class FeatureRendererTest
     features = fr.findFeaturesAtColumn(seq, 5);
     assertEquals(features.size(), 1);
     assertTrue(features.contains(sf8));
+
+    /*
+     * give "Type3" features a graduated colour scheme
+     * - first with no threshold
+     */
+    FeatureColourI gc = new FeatureColour(Color.yellow, Color.red, null, 0f,
+            10f);
+    fr.getFeatureColours().put("Type3", gc);
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertTrue(features.contains(sf4));
+    // now with threshold > 2f - feature score of 1f is excluded
+    gc.setAboveThreshold(true);
+    gc.setThreshold(2f);
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertFalse(features.contains(sf4));
+
+    /*
+     * make "Type3" graduated colour by attribute "AF"
+     * - first with no attribute held - feature should be excluded
+     */
+    gc.setAttributeName("AF");
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertFalse(features.contains(sf4));
+    // now with the attribute above threshold - should be included
+    sf4.setValue("AF", "2.4");
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertTrue(features.contains(sf4));
+    // now with the attribute below threshold - should be excluded
+    sf4.setValue("AF", "1.4");
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertFalse(features.contains(sf4));
   }
 
   @Test(groups = "Functional")
@@ -284,7 +323,7 @@ public class FeatureRendererTest
     FeatureRenderer fr = new FeatureRenderer(av);
 
     List<SequenceFeature> features = new ArrayList<>();
-    fr.filterFeaturesForDisplay(features, null); // empty list, does nothing
+    fr.filterFeaturesForDisplay(features); // empty list, does nothing
 
     SequenceI seq = av.getAlignment().getSequenceAt(0);
     SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
@@ -317,7 +356,7 @@ public class FeatureRendererTest
      * filter out duplicate (co-located) features
      * note: which gets removed is not guaranteed
      */
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    fr.filterFeaturesForDisplay(features);
     assertEquals(features.size(), 3);
     assertTrue(features.contains(sf1) || features.contains(sf4));
     assertFalse(features.contains(sf1) && features.contains(sf4));
@@ -326,64 +365,174 @@ public class FeatureRendererTest
     assertTrue(features.contains(sf5));
 
     /*
-     * hide group 3 - sf3 is removed, sf2 is retained
+     * hide groups 2 and 3 makes no difference to this method
      */
+    fr.setGroupVisibility("group2", false);
     fr.setGroupVisibility("group3", false);
     features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    fr.filterFeaturesForDisplay(features);
     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(sf2) || features.contains(sf3));
+    assertFalse(features.contains(sf2) && features.contains(sf3));
     assertTrue(features.contains(sf5));
 
     /*
-     * hide group 2, show group 3 - sf2 is removed, sf3 is retained
+     * no filtering if transparency is applied
      */
-    fr.setGroupVisibility("group2", false);
-    fr.setGroupVisibility("group3", true);
+    fr.setTransparency(0.5f);
     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));
+    fr.filterFeaturesForDisplay(features);
+    assertEquals(features.size(), 5);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColour()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(">s1\nABCD\n",
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
 
     /*
-     * no filtering of co-located features with graduated colour scheme
-     * filterFeaturesForDisplay does _not_ check colour threshold
-     * sf2 is removed as its group is hidden
+     * simple colour, feature type and group displayed
      */
-    features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
-            Color.white, 0f, 1f));
-    assertEquals(features.size(), 4);
-    assertFalse(features.contains(sf2));
+    FeatureColourI fc = new FeatureColour(Color.red);
+    fr.getFeatureColours().put("Cath", fc);
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
+            "group1");
+    assertEquals(fr.getColour(sf1), Color.red);
 
     /*
-     * co-located features with colour by label
-     * should not get filtered
+     * hide feature type, then unhide
+     * - feature type visibility should not affect the result
      */
-    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));
+    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+    data[0] = new FeatureSettingsBean("Cath", fc, null, false);
+    fr.setFeaturePriority(data);
+    assertEquals(fr.getColour(sf1), Color.red);
+    data[0] = new FeatureSettingsBean("Cath", fc, null, true);
+    fr.setFeaturePriority(data);
+    assertEquals(fr.getColour(sf1), Color.red);
 
     /*
-     * no filtering if transparency is applied
+     * hide feature group, then unhide
      */
-    fr.setTransparency(0.5f);
-    features = seq.getSequenceFeatures();
-    fr.setGroupVisibility("group2", true);
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.RED));
-    assertEquals(features.size(), 5);
+    fr.setGroupVisibility("group1", false);
+    assertNull(fr.getColour(sf1));
+    fr.setGroupVisibility("group1", true);
+    assertEquals(fr.getColour(sf1), Color.red);
+
+    /*
+     * graduated colour by score, no threshold, no score
+     * 
+     */
+    FeatureColourI gc = new FeatureColour(Color.yellow, Color.red,
+            Color.green, 1f, 11f);
+    fr.getFeatureColours().put("Cath", gc);
+    assertEquals(fr.getColour(sf1), Color.green);
+
+    /*
+     * graduated colour by score, no threshold, with score value
+     */
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 6, 8, 6f,
+            "group1");
+    // score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0)
+    Color expected = new Color(255, 128, 0);
+    assertEquals(fr.getColour(sf2), expected);
+
+    /*
+     * above threshold, score is above threshold - no change
+     */
+    gc.setAboveThreshold(true);
+    gc.setThreshold(5f);
+    assertEquals(fr.getColour(sf2), expected);
+
+    /*
+     * threshold is min-max; now score 6 is 1/6 of the way from 5 to 11
+     * or from yellow(255, 255, 0) to red(255, 0, 0)
+     */
+    gc = new FeatureColour(Color.yellow, Color.red, Color.green, 5f, 11f);
+    fr.getFeatureColours().put("Cath", gc);
+    gc.setAutoScaled(false); // this does little other than save a checkbox setting!
+    assertEquals(fr.getColour(sf2), new Color(255, 213, 0));
+
+    /*
+     * feature score is below threshold - no colour
+     */
+    gc.setAboveThreshold(true);
+    gc.setThreshold(7f);
+    assertNull(fr.getColour(sf2));
+
+    /*
+     * feature score is above threshold - no colour
+     */
+    gc.setBelowThreshold(true);
+    gc.setThreshold(3f);
+    assertNull(fr.getColour(sf2));
+
+    /*
+     * colour by feature attribute value
+     * first with no value held
+     */
+    gc = new FeatureColour(Color.yellow, Color.red, Color.green, 1f, 11f);
+    fr.getFeatureColours().put("Cath", gc);
+    gc.setAttributeName("AF");
+    assertEquals(fr.getColour(sf2), Color.green);
+
+    // with non-numeric attribute value
+    sf2.setValue("AF", "Five");
+    assertEquals(fr.getColour(sf2), Color.green);
+
+    // with numeric attribute value
+    sf2.setValue("AF", "6");
+    assertEquals(fr.getColour(sf2), expected);
+
+    // with numeric value outwith threshold
+    gc.setAboveThreshold(true);
+    gc.setThreshold(10f);
+    assertNull(fr.getColour(sf2));
+
+    // with filter on AF < 4
+    gc.setAboveThreshold(false);
+    assertEquals(fr.getColour(sf2), expected);
+    FeatureMatcherSetI filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF"));
+    fr.setFeatureFilter("Cath", filter);
+    assertNull(fr.getColour(sf2));
+
+    // with filter on 'Consequence contains missense'
+    filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense",
+            "Consequence"));
+    fr.setFeatureFilter("Cath", filter);
+    // if feature has no Consequence attribute, no colour
+    assertNull(fr.getColour(sf2));
+    // if attribute does not match filter, no colour
+    sf2.setValue("Consequence", "Synonymous");
+    assertNull(fr.getColour(sf2));
+    // attribute matches filter
+    sf2.setValue("Consequence", "Missense variant");
+    assertEquals(fr.getColour(sf2), expected);
+
+    // with filter on CSQ:Feature contains "ENST01234"
+    filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234",
+            "CSQ", "Feature"));
+    fr.setFeatureFilter("Cath", filter);
+    // if feature has no CSQ data, no colour
+    assertNull(fr.getColour(sf2));
+    // if CSQ data does not include Feature, no colour
+    Map<String, String> csqData = new HashMap<>();
+    csqData.put("BIOTYPE", "Transcript");
+    sf2.setValue("CSQ", csqData);
+    assertNull(fr.getColour(sf2));
+    // if attribute does not match filter, no colour
+    csqData.put("Feature", "ENST9876");
+    assertNull(fr.getColour(sf2));
+    // attribute matches filter
+    csqData.put("Feature", "ENST01234");
+    assertEquals(fr.getColour(sf2), expected);
   }
 }
index 0b5b6bd..030a90f 100644 (file)
@@ -20,7 +20,7 @@ public class Blosum62ColourSchemeTest
    * </ul>
    * <ul>
    */
-  @Test
+  @Test(groups = "Functional")
   public void testFindColour()
   {
     ColourSchemeI blosum = new Blosum62ColourScheme();
index 7a72c15..2eb718b 100644 (file)
@@ -25,7 +25,9 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.gui.JvOptionPane;
 import jalview.util.ColorUtils;
@@ -36,6 +38,8 @@ import java.awt.Color;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class FeatureColourTest
 {
 
@@ -57,6 +61,8 @@ public class FeatureColourTest
     assertTrue(fc1.getColour().equals(Color.RED));
     assertFalse(fc1.isGraduatedColour());
     assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
 
     /*
      * min-max colour
@@ -68,9 +74,31 @@ public class FeatureColourTest
     assertTrue(fc1.isGraduatedColour());
     assertFalse(fc1.isColourByLabel());
     assertTrue(fc1.isAboveThreshold());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
     assertEquals(12f, fc1.getThreshold());
     assertEquals(Color.gray, fc1.getMinColour());
     assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.gray, fc1.getNoColour());
+    assertEquals(10f, fc1.getMin());
+    assertEquals(20f, fc1.getMax());
+
+    /*
+     * min-max-noValue colour
+     */
+    fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(12f);
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertTrue(fc1.isAboveThreshold());
+    assertEquals(12f, fc1.getThreshold());
+    assertEquals(Color.gray, fc1.getMinColour());
+    assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.green, fc1.getNoColour());
     assertEquals(10f, fc1.getMin());
     assertEquals(20f, fc1.getMax());
 
@@ -82,6 +110,127 @@ public class FeatureColourTest
     fc1 = new FeatureColour(fc);
     assertTrue(fc1.isColourByLabel());
     assertFalse(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+
+    /*
+     * colour by attribute (label)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("AF");
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isColourByLabel());
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+
+    /*
+     * colour by attribute (value)
+     */
+    fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(12f);
+    fc.setAttributeName("AF");
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+    assertTrue(fc1.isAboveThreshold());
+    assertEquals(12f, fc1.getThreshold());
+    assertEquals(Color.gray, fc1.getMinColour());
+    assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.green, fc1.getNoColour());
+    assertEquals(10f, fc1.getMin());
+    assertEquals(20f, fc1.getMax());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCopyConstructor_minMax()
+  {
+    /*
+     * graduated colour
+     */
+    FeatureColour fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.isColourByLabel());
+    assertFalse(fc.isColourByAttribute());
+    assertNull(fc.getAttributeName());
+    assertEquals(1f, fc.getMin());
+    assertEquals(5f, fc.getMax());
+
+    /*
+     * update min-max bounds
+     */
+    FeatureColour fc1 = new FeatureColour(fc, 2f, 6f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(2f, fc1.getMin());
+    assertEquals(6f, fc1.getMax());
+    assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * update min-max bounds - high to low
+     */
+    fc1 = new FeatureColour(fc, 23f, 16f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(23f, fc1.getMin());
+    assertEquals(16f, fc1.getMax());
+    assertTrue((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * graduated colour by attribute
+     */
+    fc1.setAttributeName("AF");
+    fc1 = new FeatureColour(fc1, 13f, 36f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+    assertEquals(13f, fc1.getMin());
+    assertEquals(36f, fc1.getMax());
+    assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * colour by label
+     */
+    fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+    fc.setColourByLabel(true);
+    assertFalse(fc.isGraduatedColour());
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.isColourByAttribute());
+    assertNull(fc.getAttributeName());
+    assertEquals(1f, fc.getMin());
+    assertEquals(5f, fc.getMax());
+
+    /*
+     * update min-max bounds
+     */
+    fc1 = new FeatureColour(fc, 2f, 6f);
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(2f, fc1.getMin());
+    assertEquals(6f, fc1.getMax());
+
+    /*
+     * colour by attribute text
+     */
+    fc1.setAttributeName("AC");
+    fc1 = new FeatureColour(fc1, 13f, 36f);
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AC" }, fc1.getAttributeName());
+    assertEquals(13f, fc1.getMin());
+    assertEquals(36f, fc1.getMax());
   }
 
   @Test(groups = { "Functional" })
@@ -106,8 +255,11 @@ public class FeatureColourTest
   @Test(groups = { "Functional" })
   public void testGetColor_Graduated()
   {
-    // graduated colour from score 0 to 100, gray(128, 128, 128) to red(255, 0,
-    // 0)
+    /*
+     * graduated colour from 
+     * score 0 to 100
+     * gray(128, 128, 128) to red(255, 0, 0)
+     */
     FeatureColour fc = new FeatureColour(Color.GRAY, Color.RED, 0f, 100f);
     // feature score is 75 which is 3/4 of the way from GRAY to RED
     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
@@ -174,20 +326,29 @@ public class FeatureColourTest
     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 
     /*
+     * colour by attribute text (no threshold)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("CLIN_SIG");
+    assertEquals("domain\tattribute|CLIN_SIG", fc.toJalviewFormat("domain"));
+    
+    /*
      * colour by label (autoscaled) (an odd state you can reach by selecting
      * 'above threshold', then deselecting 'threshold is min/max' then 'colour
      * by label')
      */
+    fc.setAttributeName((String[]) null);
     fc.setAutoScaled(true);
     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 
     /*
-     * colour by label (above threshold) (min/max values are output though not
-     * used by this scheme)
+     * colour by label (above threshold) 
      */
     fc.setAutoScaled(false);
     fc.setThreshold(12.5f);
     fc.setAboveThreshold(true);
+    // min/max values are output though not used by this scheme
     assertEquals("domain\tlabel|||0.0|0.0|above|12.5",
             fc.toJalviewFormat("domain"));
 
@@ -199,38 +360,80 @@ public class FeatureColourTest
             fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour, no threshold
+     * colour by attributes text (below threshold)
+     */
+    fc.setBelowThreshold(true);
+    fc.setAttributeName("CSQ", "Consequence");
+    assertEquals("domain\tattribute|CSQ:Consequence|||0.0|0.0|below|12.5",
+            fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by score, no threshold
+     * - default constructor sets noValueColor = minColor
      */
     fc = new FeatureColour(Color.GREEN, Color.RED, 12f, 25f);
     String greenHex = Format.getHexString(Color.GREEN);
-    String expected = String.format("domain\t%s|%s|abso|12.0|25.0|none",
-            greenHex, redHex);
+    String expected = String.format(
+            "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
+            redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
+     * graduated colour by score, no threshold, no value gets min colour
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, Color.GREEN, 12f, 25f);
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by score, no threshold, no value gets max colour
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, Color.RED, 12f, 25f);
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+    
+    /*
      * colour ranges over the actual score ranges (not min/max)
      */
     fc.setAutoScaled(true);
-    expected = String.format("domain\t%s|%s|12.0|25.0|none", greenHex,
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|12.0|25.0|none", greenHex,
             redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour below threshold
+     * graduated colour by score, below threshold
      */
     fc.setThreshold(12.5f);
     fc.setBelowThreshold(true);
-    expected = String.format("domain\t%s|%s|12.0|25.0|below|12.5",
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|12.0|25.0|below|12.5",
             greenHex, redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour above threshold
+     * graduated colour by score, above threshold
      */
     fc.setThreshold(12.5f);
     fc.setAboveThreshold(true);
     fc.setAutoScaled(false);
-    expected = String.format("domain\t%s|%s|abso|12.0|25.0|above|12.5",
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
+            greenHex, redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by attribute, above threshold
+     */
+    fc.setAttributeName("CSQ", "AF");
+    fc.setAboveThreshold(true);
+    fc.setAutoScaled(false);
+    expected = String.format(
+            "domain\tattribute|CSQ:AF|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
             greenHex, redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
   }
@@ -244,7 +447,7 @@ public class FeatureColourTest
     /*
      * simple colour by name
      */
-    FeatureColour fc = FeatureColour.parseJalviewFeatureColour("red");
+    FeatureColourI fc = FeatureColour.parseJalviewFeatureColour("red");
     assertTrue(fc.isSimpleColour());
     assertEquals(Color.RED, fc.getColour());
 
@@ -292,7 +495,28 @@ public class FeatureColourTest
     assertEquals(12.0f, fc.getThreshold());
 
     /*
-     * graduated colour (by name) (no threshold)
+     * colour by attribute text (no threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour("attribute|CLIN_SIG");
+    assertTrue(fc.isColourByAttribute());
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.hasThreshold());
+    assertArrayEquals(new String[] { "CLIN_SIG" }, fc.getAttributeName());
+
+    /*
+     * colour by attributes text (with score threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(
+            "attribute|CSQ:Consequence|||0.0|0.0|above|12.0");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "CSQ", "Consequence" },
+            fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(12.0f, fc.getThreshold());
+
+    /*
+     * graduated colour by score (with colour names) (no threshold)
      */
     fc = FeatureColour.parseJalviewFeatureColour("red|green|10.0|20.0");
     assertTrue(fc.isGraduatedColour());
@@ -304,7 +528,35 @@ public class FeatureColourTest
     assertTrue(fc.isAutoScaled());
 
     /*
-     * graduated colour (by hex code) (above threshold)
+     * graduated colour (explicitly by 'score') (no threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("Score|red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour by attribute (no threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("attribute|AF|red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc.getAttributeName());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour by score (colours by hex code) (above threshold)
      */
     String descriptor = String.format("%s|%s|10.0|20.0|above|15",
             Format.getHexString(Color.RED),
@@ -321,9 +573,26 @@ public class FeatureColourTest
     assertTrue(fc.isAutoScaled());
 
     /*
+     * graduated colour by attributes (below threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(
+            "attribute|CSQ:AF|red|green|10.0|20.0|below|13");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "CSQ", "AF" }, fc.getAttributeName());
+    assertTrue(fc.hasThreshold());
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(13f, fc.getThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
      * graduated colour (by RGB triplet) (below threshold), absolute scale
      */
-    descriptor = String.format("255,0,0|0,255,0|abso|10.0|20.0|below|15");
+    descriptor = "255,0,0|0,255,0|abso|10.0|20.0|below|15";
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
     assertFalse(fc.isAutoScaled());
@@ -335,9 +604,63 @@ public class FeatureColourTest
     assertEquals(10f, fc.getMin());
     assertEquals(20f, fc.getMax());
 
-    descriptor = String
-            .format("blue|255,0,255|absolute|20.0|95.0|below|66.0");
+    descriptor = "blue|255,0,255|absolute|20.0|95.0|below|66.0";
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetColor_colourByAttributeText()
+  {
+    FeatureColour fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("consequence");
+    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
+            null);
+
+    /*
+     * if feature has no such attribute, use 'no value' colour
+     */
+    assertEquals(FeatureColour.DEFAULT_NO_COLOUR, fc.getColor(sf));
+
+    /*
+     * if feature has attribute, generate colour from value
+     */
+    sf.setValue("consequence", "benign");
+    Color expected = ColorUtils.createColourFromName("benign");
+    assertEquals(expected, fc.getColor(sf));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetColor_GraduatedByAttributeValue()
+  {
+    /*
+     * graduated colour based on attribute value for AF
+     * given a min-max range of 0-100
+     */
+    FeatureColour fc = new FeatureColour(new Color(50, 100, 150),
+            new Color(150, 200, 250), Color.yellow, 0f, 100f);
+    String attName = "AF";
+    fc.setAttributeName(attName);
+
+    /*
+     * first case: feature lacks the attribute - use 'no value' colour
+     */
+    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
+            null);
+    assertEquals(Color.yellow, fc.getColor(sf));
+
+    /*
+     * second case: attribute present but not numeric - treat as if absent
+     */
+    sf.setValue(attName, "twelve");
+    assertEquals(Color.yellow, fc.getColor(sf));
+
+    /*
+     * third case: valid attribute value
+     */
+    sf.setValue(attName, "20.0");
+    Color expected = new Color(70, 120, 170);
+    assertEquals(expected, fc.getColor(sf));
+  }
 }
index 95d7efe..86dcc39 100644 (file)
@@ -816,4 +816,169 @@ public class MapListTest
     assertEquals(1, merged.size());
     assertArrayEquals(new int[] { 9, 0 }, merged.get(0));
   }
+
+  /**
+   * Test the method that compounds ('traverses') two mappings
+   */
+  @Test(groups = "Functional")
+  public void testTraverse()
+  {
+    /*
+     * simple 1:1 plus 1:1 forwards
+     */
+    MapList ml1 = new MapList(new int[] { 3, 4, 8, 12 }, new int[] { 5, 8,
+        11, 13 }, 1, 1);
+    assertEquals("{[3, 4], [8, 12]}", prettyPrint(ml1.getFromRanges()));
+    assertEquals("{[5, 8], [11, 13]}", prettyPrint(ml1.getToRanges()));
+
+    MapList ml2 = new MapList(new int[] { 1, 50 }, new int[] { 40, 45, 70,
+        75, 90, 127 }, 1, 1);
+    assertEquals("{[1, 50]}", prettyPrint(ml2.getFromRanges()));
+    assertEquals("{[40, 45], [70, 75], [90, 127]}",
+            prettyPrint(ml2.getToRanges()));
+
+    MapList compound = ml1.traverse(ml2);
+
+    assertEquals(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    List<int[]> fromRanges = compound.getFromRanges();
+    assertEquals(2, fromRanges.size());
+    assertArrayEquals(new int[] { 3, 4 }, fromRanges.get(0));
+    assertArrayEquals(new int[] { 8, 12 }, fromRanges.get(1));
+    List<int[]> toRanges = compound.getToRanges();
+    assertEquals(4, toRanges.size());
+    // 5-8 maps to 44-45,70-71
+    // 11-13 maps to 74-75,90
+    assertArrayEquals(new int[] { 44, 45 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 70, 71 }, toRanges.get(1));
+    assertArrayEquals(new int[] { 74, 75 }, toRanges.get(2));
+    assertArrayEquals(new int[] { 90, 90 }, toRanges.get(3));
+
+    /*
+     * 1:1 over 1:1 backwards ('reverse strand')
+     */
+    ml1 = new MapList(new int[] { 1, 50 }, new int[] { 70, 119 }, 1, 1);
+    ml2 = new MapList(new int[] { 1, 500 },
+            new int[] { 1000, 901, 600, 201 }, 1, 1);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 1, 50 }, fromRanges.get(0));
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 931, 901 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 600, 582 }, toRanges.get(1));
+
+    /*
+     * 1:1 plus 1:3 should result in 1:3
+     */
+    ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 40 }, 1, 1);
+    ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 50, 91, 340 },
+            1, 3);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(1, compound.getFromRatio());
+    assertEquals(3, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+    // 11-40 maps to 31-50,91-160
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 31, 50 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 91, 160 }, toRanges.get(1));
+
+    /*
+     * 3:1 plus 1:1 should result in 3:1
+     */
+    ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 20 }, 3, 1);
+    ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 15, 91, 175 },
+            1, 1);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(3, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+    // 11-20 maps to 11-15, 91-95
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 11, 15 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 91, 95 }, toRanges.get(1));
+
+    /*
+     * 1:3 plus 3:1 should result in 1:1
+     */
+    ml1 = new MapList(new int[] { 21, 40 }, new int[] { 13, 72 }, 1, 3);
+    ml2 = new MapList(new int[] { 1, 300 }, new int[] { 51, 70, 121, 200 },
+            3, 1);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 21, 40 }, fromRanges.get(0));
+    // 13-72 maps 3:1 to 55-70, 121-124
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 55, 70 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 121, 124 }, toRanges.get(1));
+
+    /*
+     * 3:1 plus 1:3 should result in 1:1
+     */
+    ml1 = new MapList(new int[] { 31, 90 }, new int[] { 13, 32 }, 3, 1);
+    ml2 = new MapList(new int[] { 11, 40 }, new int[] { 41, 50, 71, 150 },
+            1, 3);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 31, 90 }, fromRanges.get(0));
+    // 13-32 maps to 47-50,71-126
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 47, 50 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 71, 126 }, toRanges.get(1));
+
+    /*
+     * method returns null if not all regions are mapped through
+     */
+    ml1 = new MapList(new int[] { 1, 50 }, new int[] { 101, 150 }, 1, 1);
+    ml2 = new MapList(new int[] { 131, 180 }, new int[] { 201, 250 }, 1, 3);
+    compound = ml1.traverse(ml2);
+    assertNull(compound);
+  }
+
+  /**
+   * Test that method that inspects for the (first) forward or reverse 'to' range.
+   * Single position ranges are ignored.
+   */
+  @Test(groups = { "Functional" })
+  public void testIsToForwardsStrand()
+  {
+    // [3-9] declares forward strand
+    MapList ml = new MapList(new int[] { 20, 11 },
+            new int[]
+            { 2, 2, 3, 9, 12, 11 }, 1, 1);
+    assertTrue(ml.isToForwardStrand());
+
+    // [11-5] declares reverse strand ([13-14] is ignored)
+    ml = new MapList(new int[] { 20, 11 },
+            new int[]
+            { 2, 2, 11, 5, 13, 14 }, 1, 1);
+    assertFalse(ml.isToForwardStrand());
+
+    // all single position ranges - defaults to forward strand
+    ml = new MapList(new int[] { 3, 1 }, new int[] { 2, 2, 4, 4, 6, 6 }, 1,
+            1);
+    assertTrue(ml.isToForwardStrand());
+  }
 }
index 022e2d6..097ccd4 100644 (file)
@@ -1150,6 +1150,95 @@ public class MappingUtilsTest
     assertEquals("[12, 11, 8, 4]", Arrays.toString(ranges));
   }
 
+  @Test(groups = { "Functional" })
+  public void testRangeContains()
+  {
+    /*
+     * both forward ranges
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        2, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 9 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        4, 5 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        0, 9 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        -10, -9 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        11, 12 }));
+
+    /*
+     * forward range, reverse query
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        9, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 2 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        5, 5 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        11, 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 0 }));
+
+    /*
+     * reverse range, forward query
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        1, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        1, 9 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        2, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        6, 6 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        6, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        11, 20 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        -3, -2 }));
+
+    /*
+     * both reverse
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        9, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 2 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        3, 3 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        11, 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 0 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        12, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        -5, -8 }));
+
+    /*
+     * bad arguments
+     */
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10, 12 },
+            new int[] {
+        1, 10 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 },
+            new int[] { 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, null));
+    assertFalse(MappingUtils.rangeContains(null, new int[] { 1, 10 }));
+  }
+
   @Test(groups = "Functional")
   public void testRemoveEndPositions()
   {
diff --git a/test/jalview/util/MathUtilsTest.java b/test/jalview/util/MathUtilsTest.java
new file mode 100644 (file)
index 0000000..dc23472
--- /dev/null
@@ -0,0 +1,26 @@
+package jalview.util;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class MathUtilsTest
+{
+  @Test(groups = "Functional")
+  public void testGcd()
+  {
+    assertEquals(MathUtils.gcd(0, 0), 0);
+    assertEquals(MathUtils.gcd(0, 1), 1);
+    assertEquals(MathUtils.gcd(1, 0), 1);
+    assertEquals(MathUtils.gcd(1, 1), 1);
+    assertEquals(MathUtils.gcd(1, -1), 1);
+    assertEquals(MathUtils.gcd(-1, 1), 1);
+    assertEquals(MathUtils.gcd(2, 3), 1);
+    assertEquals(MathUtils.gcd(4, 2), 2);
+    assertEquals(MathUtils.gcd(2, 4), 2);
+    assertEquals(MathUtils.gcd(2, -4), 2);
+    assertEquals(MathUtils.gcd(-2, 4), 2);
+    assertEquals(MathUtils.gcd(-2, -4), 2);
+    assertEquals(MathUtils.gcd(2 * 3 * 5 * 7 * 11, 3 * 7 * 13 * 17), 3 * 7);
+  }
+}
index b6f8a25..084219a 100644 (file)
@@ -228,4 +228,26 @@ public class StringUtilsTest
     assertEquals("", StringUtils.toSentenceCase(""));
     assertNull(StringUtils.toSentenceCase(null));
   }
+
+  @Test(groups = { "Functional" })
+  public void testStripHtmlTags()
+  {
+    assertNull(StringUtils.stripHtmlTags(null));
+    assertEquals("", StringUtils.stripHtmlTags(""));
+    assertEquals(
+            "<a href=\"something\">label</href>",
+            StringUtils
+                    .stripHtmlTags("<html><a href=\"something\">label</href></html>"));
+
+    // if no "<html>" tag, < and > get html-encoded (not sure why)
+    assertEquals("&lt;a href=\"something\"&gt;label&lt;/href&gt;",
+            StringUtils.stripHtmlTags("<a href=\"something\">label</href>"));
+
+    // </body> gets removed but not <body> (is this intentional?)
+    assertEquals("<body><p>hello",
+            StringUtils.stripHtmlTags("<html><body><p>hello</body></html>"));
+
+    assertEquals("kdHydro &lt; 12.53",
+            StringUtils.stripHtmlTags("kdHydro < 12.53"));
+  }
 }
diff --git a/test/jalview/util/matcher/ConditionTest.java b/test/jalview/util/matcher/ConditionTest.java
new file mode 100644 (file)
index 0000000..2a12534
--- /dev/null
@@ -0,0 +1,76 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class ConditionTest
+{
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.UK);
+    assertEquals(Condition.Contains.toString(), "Contains");
+    assertEquals(Condition.NotContains.toString(), "Does not contain");
+    assertEquals(Condition.Matches.toString(), "Matches");
+    assertEquals(Condition.NotMatches.toString(), "Does not match");
+    assertEquals(Condition.Present.toString(), "Is present");
+    assertEquals(Condition.NotPresent.toString(), "Is not present");
+    assertEquals(Condition.LT.toString(), "<");
+    assertEquals(Condition.LE.toString(), "<=");
+    assertEquals(Condition.GT.toString(), ">");
+    assertEquals(Condition.GE.toString(), ">=");
+    assertEquals(Condition.EQ.toString(), "=");
+    assertEquals(Condition.NE.toString(), "not =");
+
+    /*
+     * repeat call to get coverage of value caching
+     */
+    assertEquals(Condition.NE.toString(), "not =");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetStableName()
+  {
+    assertEquals(Condition.Contains.getStableName(), "Contains");
+    assertEquals(Condition.NotContains.getStableName(), "NotContains");
+    assertEquals(Condition.Matches.getStableName(), "Matches");
+    assertEquals(Condition.NotMatches.getStableName(), "NotMatches");
+    assertEquals(Condition.Present.getStableName(), "Present");
+    assertEquals(Condition.NotPresent.getStableName(), "NotPresent");
+    assertEquals(Condition.LT.getStableName(), "LT");
+    assertEquals(Condition.LE.getStableName(), "LE");
+    assertEquals(Condition.GT.getStableName(), "GT");
+    assertEquals(Condition.GE.getStableName(), "GE");
+    assertEquals(Condition.EQ.getStableName(), "EQ");
+    assertEquals(Condition.NE.getStableName(), "NE");
+  }
+
+  @Test(groups = "Functional")
+  public void testFromString()
+  {
+    assertEquals(Condition.fromString("Contains"), Condition.Contains);
+    // not case sensitive
+    assertEquals(Condition.fromString("contains"), Condition.Contains);
+    assertEquals(Condition.fromString("CONTAINS"), Condition.Contains);
+    assertEquals(Condition.fromString("NotContains"),
+            Condition.NotContains);
+    assertEquals(Condition.fromString("Matches"), Condition.Matches);
+    assertEquals(Condition.fromString("NotMatches"), Condition.NotMatches);
+    assertEquals(Condition.fromString("Present"), Condition.Present);
+    assertEquals(Condition.fromString("NotPresent"), Condition.NotPresent);
+    assertEquals(Condition.fromString("LT"), Condition.LT);
+    assertEquals(Condition.fromString("LE"), Condition.LE);
+    assertEquals(Condition.fromString("GT"), Condition.GT);
+    assertEquals(Condition.fromString("GE"), Condition.GE);
+    assertEquals(Condition.fromString("EQ"), Condition.EQ);
+    assertEquals(Condition.fromString("NE"), Condition.NE);
+
+    assertNull(Condition.fromString("Equals"));
+    assertNull(Condition.fromString(""));
+    assertNull(Condition.fromString(null));
+  }
+}
diff --git a/test/jalview/util/matcher/MatcherTest.java b/test/jalview/util/matcher/MatcherTest.java
new file mode 100644 (file)
index 0000000..a47fb60
--- /dev/null
@@ -0,0 +1,273 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class MatcherTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    MatcherI m = new Matcher(Condition.Contains, "foo");
+    assertEquals(m.getCondition(), Condition.Contains);
+    assertEquals(m.getPattern(), "foo");
+    assertEquals(PA.getValue(m, "uppercasePattern"), "FOO");
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GT, -2.1f);
+    assertEquals(m.getCondition(), Condition.GT);
+    assertEquals(m.getPattern(), "-2.1");
+    assertEquals(m.getFloatValue(), -2.1f);
+
+    m = new Matcher(Condition.NotContains, "-1.2f");
+    assertEquals(m.getCondition(), Condition.NotContains);
+    assertEquals(m.getPattern(), "-1.2f");
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GE, "-1.2f");
+    assertEquals(m.getCondition(), Condition.GE);
+    assertEquals(m.getPattern(), "-1.2");
+    assertEquals(m.getFloatValue(), -1.2f);
+
+    try
+    {
+      new Matcher(null, 0f);
+      fail("Expected exception");
+    } catch (NullPointerException e)
+    {
+      // expected
+    }
+
+    try
+    {
+      new Matcher(Condition.LT, "123,456");
+      fail("Expected exception");
+    } catch (NumberFormatException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Tests for float comparison conditions
+   */
+  @Test(groups = "Functional")
+  public void testMatches_float()
+  {
+    /*
+     * EQUALS test
+     */
+    MatcherI m = new Matcher(Condition.EQ, 2f);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("2.0"));
+    assertFalse(m.matches("2.01"));
+
+    /*
+     * NOT EQUALS test
+     */
+    m = new Matcher(Condition.NE, 2f);
+    assertFalse(m.matches("2"));
+    assertFalse(m.matches("2.0"));
+    assertTrue(m.matches("2.01"));
+
+    /*
+     * >= test
+     */
+    m = new Matcher(Condition.GE, 2f);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("2.1"));
+    assertFalse(m.matches("1.9"));
+
+    /*
+     * > test
+     */
+    m = new Matcher(Condition.GT, 2f);
+    assertFalse(m.matches("2"));
+    assertTrue(m.matches("2.1"));
+    assertFalse(m.matches("1.9"));
+
+    /*
+     * <= test
+     */
+    m = new Matcher(Condition.LE, 2f);
+    assertTrue(m.matches("2"));
+    assertFalse(m.matches("2.1"));
+    assertTrue(m.matches("1.9"));
+
+    /*
+     * < test
+     */
+    m = new Matcher(Condition.LT, 2f);
+    assertFalse(m.matches("2"));
+    assertFalse(m.matches("2.1"));
+    assertTrue(m.matches("1.9"));
+  }
+
+  @Test(groups = "Functional")
+  public void testMatches_floatNullOrInvalid()
+  {
+    for (Condition cond : Condition.values())
+    {
+      if (cond.isNumeric())
+      {
+        MatcherI m = new Matcher(cond, 2f);
+        assertFalse(m.matches(null));
+        assertFalse(m.matches(""));
+        assertFalse(m.matches("two"));
+      }
+    }
+  }
+
+  /**
+   * Tests for string comparison conditions
+   */
+  @Test(groups = "Functional")
+  public void testMatches_pattern()
+  {
+    /*
+     * Contains
+     */
+    MatcherI m = new Matcher(Condition.Contains, "benign");
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+    assertFalse(m.matches("pathogenic"));
+    assertFalse(m.matches(null));
+
+    /*
+     * does not contain
+     */
+    m = new Matcher(Condition.NotContains, "benign");
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+    assertTrue(m.matches("pathogenic"));
+    assertTrue(m.matches(null)); // null value passes this condition
+
+    /*
+     * matches
+     */
+    m = new Matcher(Condition.Matches, "benign");
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches(" Benign ")); // trim before testing
+    assertFalse(m.matches("MOSTLY BENIGN"));
+    assertFalse(m.matches("pathogenic"));
+    assertFalse(m.matches(null));
+
+    /*
+     * does not match
+     */
+    m = new Matcher(Condition.NotMatches, "benign");
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches(" Benign ")); // trim before testing
+    assertTrue(m.matches("MOSTLY BENIGN"));
+    assertTrue(m.matches("pathogenic"));
+    assertTrue(m.matches(null));
+
+    /*
+     * value is present (is not null)
+     */
+    m = new Matcher(Condition.Present, null);
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches(""));
+    assertFalse(m.matches(null));
+
+    /*
+     * value is not present (is null)
+     */
+    m = new Matcher(Condition.NotPresent, null);
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches(""));
+    assertTrue(m.matches(null));
+
+    /*
+     * a float with a string match condition will be treated as string
+     */
+    Matcher m1 = new Matcher(Condition.Contains, "32");
+    assertFalse(m1.matches(-203f));
+    assertTrue(m1.matches(-4321.0f));
+  }
+
+  /**
+   * If a float is passed with a string condition it gets converted to a string
+   */
+  @Test(groups = "Functional")
+  public void testMatches_floatWithStringCondition()
+  {
+    MatcherI m = new Matcher(Condition.Contains, 1.2e-6f);
+    assertTrue(m.matches("1.2e-6"));
+
+    m = new Matcher(Condition.Contains, 0.0000001f);
+    assertTrue(m.matches("1.0e-7"));
+    assertTrue(m.matches("1.0E-7"));
+    assertFalse(m.matches("0.0000001f"));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+
+    MatcherI m = new Matcher(Condition.LT, 1.2e-6f);
+    assertEquals(m.toString(), "< 1.2E-6");
+
+    m = new Matcher(Condition.NotMatches, "ABC");
+    assertEquals(m.toString(), "Does not match 'ABC'");
+
+    m = new Matcher(Condition.Contains, -1.2f);
+    assertEquals(m.toString(), "Contains '-1.2'");
+  }
+
+  @Test(groups = "Functional")
+  public void testEquals()
+  {
+    /*
+     * string condition
+     */
+    MatcherI m = new Matcher(Condition.NotMatches, "ABC");
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "ABC")));
+    // not case-sensitive:
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "abc")));
+    assertFalse(m.equals(new Matcher(Condition.Matches, "ABC")));
+    assertFalse(m.equals(new Matcher(Condition.NotMatches, "def")));
+
+    /*
+     * numeric conditions
+     */
+    m = new Matcher(Condition.LT, -1f);
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.LT, -1f)));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1f")));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1.00f")));
+    assertFalse(m.equals(new Matcher(Condition.LE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.GE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.NE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, 1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, -1.1f)));
+  }
+
+  @Test(groups = "Functional")
+  public void testHashCode()
+  {
+    MatcherI m1 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m2 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m3 = new Matcher(Condition.NotMatches, "AB");
+    MatcherI m4 = new Matcher(Condition.Matches, "ABC");
+    assertEquals(m1.hashCode(), m2.hashCode());
+    assertNotEquals(m1.hashCode(), m3.hashCode());
+    assertNotEquals(m1.hashCode(), m4.hashCode());
+    assertNotEquals(m3.hashCode(), m4.hashCode());
+  }
+}
index cc3dca8..d1e32b9 100644 (file)
@@ -62,7 +62,7 @@ public class PDBSequenceFetcherTest
     Cache.applicationProperties.setProperty("ADD_SS_ANN",
             Boolean.TRUE.toString());
 
-    sf = new SequenceFetcher(false);
+    sf = new SequenceFetcher();
   }
 
   /**
index 32afd5f..0b501ee 100644 (file)
@@ -52,26 +52,24 @@ public class SequenceFetcherTest
    */
   public static void main(String[] argv)
   {
-    // TODO: extracted from SequenceFetcher - convert to proper unit test with
+    // TODO: extracted from SequenceFetcher - convert to network dependent
+    // functional integration test with
     // assertions
 
     String usage = "SequenceFetcher.main [-nodas] [<DBNAME> [<ACCNO>]]\n"
             + "With no arguments, all DbSources will be queried with their test Accession number.\n"
             + "With one argument, the argument will be resolved to one or more db sources and each will be queried with their test accession only.\n"
-            + "If given two arguments, SequenceFetcher will try to find the DbFetcher corresponding to <DBNAME> and retrieve <ACCNO> from it.\n"
-            + "The -nodas option will exclude DAS sources from the database fetchers Jalview will try to use.";
-    boolean withDas = true;
-    if (argv != null && argv.length > 0
-            && argv[0].toLowerCase().startsWith("-nodas"))
+            + "If given two arguments, SequenceFetcher will try to find the DbFetcher corresponding to <DBNAME> and retrieve <ACCNO> from it.";
+
+    if (argv != null && argv.length > 0)
     {
-      withDas = false;
       String targs[] = new String[argv.length - 1];
       System.arraycopy(argv, 1, targs, 0, targs.length);
       argv = targs;
     }
     if (argv != null && argv.length > 0)
     {
-      List<DbSourceProxy> sps = new SequenceFetcher(withDas)
+      List<DbSourceProxy> sps = new SequenceFetcher()
               .getSourceProxy(argv[0]);
 
       if (sps != null)
@@ -102,7 +100,7 @@ public class SequenceFetcherTest
       System.out.println(usage);
       return;
     }
-    ASequenceFetcher sfetcher = new SequenceFetcher(withDas);
+    ASequenceFetcher sfetcher = new SequenceFetcher();
     String[] dbSources = sfetcher.getSupportedDb();
     for (int dbsource = 0; dbsource < dbSources.length; dbsource++)
     {
@@ -124,7 +122,7 @@ public class SequenceFetcherTest
           String testQuery)
   {
     AlignmentI ds = null;
-    Vector<Object[]> noProds = new Vector<Object[]>();
+    Vector<Object[]> noProds = new Vector<>();
     System.out.println("Source: " + sp.getDbName() + " (" + db
             + "): retrieving test:" + sp.getTestQuery());
     {
index 90d4472..e04d195 100644 (file)
@@ -51,7 +51,7 @@ public class RemoteFormatTest
     Cache.applicationProperties.setProperty("ADD_SS_ANN",
             Boolean.TRUE.toString());
 
-    sf = new SequenceFetcher(false);
+    sf = new SequenceFetcher();
   }
 
   @DataProvider(name = "AccessionData")
@@ -92,7 +92,7 @@ public class RemoteFormatTest
   @Test(groups = { "Network" })
   public void testUniprotFreeTextSearch() throws Exception
   {
-    List<FTSDataColumnI> wantedFields = new ArrayList<FTSDataColumnI>();
+    List<FTSDataColumnI> wantedFields = new ArrayList<>();
     FTSRestClientI client = UniProtFTSRestClient.getInstance();
     wantedFields.add(client.getDataColumnByNameOrCode("id"));
     wantedFields.add(client.getDataColumnByNameOrCode("entry name"));
diff --git a/test/jalview/ws/seqfetcher/DasSequenceFetcher.java b/test/jalview/ws/seqfetcher/DasSequenceFetcher.java
deleted file mode 100644 (file)
index f1dafcb..0000000
+++ /dev/null
@@ -1,49 +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.ws.seqfetcher;
-
-import static org.testng.Assert.assertTrue;
-
-import jalview.bin.Cache;
-import jalview.gui.JvOptionPane;
-
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-public class DasSequenceFetcher
-{
-
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
-  @Test(groups = { "Network" })
-  public void testDasRegistryContact()
-  {
-    Cache.getDasSourceRegistry().refreshSources();
-    assertTrue(Cache.getDasSourceRegistry().getSources().isEmpty(),
-            "Expected to find no DAS sources at the registry. Check config.");
-  }
-
-}
index 4489a93..c870f6d 100644 (file)
@@ -24,6 +24,7 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TreeSet;
 import java.util.regex.Pattern;
 
@@ -89,7 +90,9 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
 
   private int javaCount;
 
-  private HashSet<String> invalidKeys;
+  private Set<String> invalidKeys;
+
+  private Set<String> dynamicKeys;
 
   /**
    * Runs the scan given the path to the root of Java source directories
@@ -125,7 +128,7 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
   private void doMain(String srcPath) throws IOException
   {
     System.out.println("Scanning " + srcPath
-            + " for calls to MessageManager");
+            + " for calls to MessageManager\n");
     sourcePath = srcPath;
     loadMessages();
     File dir = new File(srcPath);
@@ -134,7 +137,10 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
       System.out.println(srcPath + " not found");
       return;
     }
-    invalidKeys = new HashSet<String>();
+
+    invalidKeys = new HashSet<>();
+    dynamicKeys = new HashSet<>();
+
     if (dir.isDirectory())
     {
       scanDirectory(dir);
@@ -152,17 +158,60 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
   private void reportResults()
   {
     System.out.println("\nScanned " + javaCount + " source files");
-    System.out.println("Message.properties has " + messages.size()
+    System.out.println(
+            "Messages.properties has " + messages.size()
             + " keys");
-    System.out.println("Found " + invalidKeys.size()
-            + " possibly invalid parameter calls");
+    if (!invalidKeys.isEmpty())
+    {
+      System.out.println("Found " + invalidKeys.size()
+              + " possibly invalid parameter call"
+              + (invalidKeys.size() > 1 ? "s" : ""));
+    }
 
-    System.out.println(messageKeys.size()
-            + " keys not found, either unused or constructed dynamically");
+    System.out.println("Keys not found, assumed constructed dynamically:");
+    int dynamicCount = 0;
     for (String key : messageKeys)
     {
-      System.out.println("    " + key);
+      if (isDynamic(key))
+      {
+        System.out.println("    " + key);
+        dynamicCount++;
+      }
+    }
+
+    if (dynamicCount < messageKeys.size())
+    {
+      System.out.println((messageKeys.size() - dynamicCount)
+              + " keys not found, possibly unused");
+      for (String key : messageKeys)
+      {
+        if (!isDynamic(key))
+        {
+          System.out.println("    " + key);
+        }
+      }
+    }
+    System.out
+            .println("(Run i18nAnt.xml to compare other message bundles)");
+  }
+
+  /**
+   * Answers true if the key starts with one of the recorded dynamic key stubs,
+   * else false
+   * 
+   * @param key
+   * @return
+   */
+  private boolean isDynamic(String key)
+  {
+    for (String dynamic : dynamicKeys)
+    {
+      if (key.startsWith(dynamic))
+      {
+        return true;
+      }
     }
+    return false;
   }
 
   /**
@@ -275,14 +324,17 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
         continue;
       }
 
+      String messageKey = getMessageKey(method, methodArgs);
+
       if (METHOD3 == method)
       {
         System.out.println(String.format("Dynamic key at %s line %s %s",
                 path.substring(sourcePath.length()), lineNos, line));
+        String key = messageKey.substring(1, messageKey.length() - 1);
+        dynamicKeys.add(key);
         continue;
       }
 
-      String messageKey = getMessageKey(method, methodArgs);
       if (messageKey == null)
       {
         System.out.println(String.format("Trouble parsing %s line %s %s",
@@ -370,7 +422,7 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
     messages.load(reader);
     reader.close();
 
-    messageKeys = new TreeSet<String>();
+    messageKeys = new TreeSet<>();
     for (Object key : messages.keySet())
     {
       messageKeys.add((String) key);