Merge branch 'develop' into features/JAL-1956_featureStyles
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 10 May 2016 10:17:19 +0000 (11:17 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 10 May 2016 10:17:19 +0000 (11:17 +0100)
Conflicts:
src/jalview/appletgui/FeatureRenderer.java
src/jalview/appletgui/FeatureSettings.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Jalview2XML.java
src/jalview/io/FeaturesFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/schemes/FeatureColourScheme.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/ws/jws2/AADisorderClient.java
test/jalview/io/FeaturesFileTest.java

24 files changed:
1  2 
examples/testdata/simpleGff3.gff
src/jalview/api/FeatureColourI.java
src/jalview/api/FeatureRenderer.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/FeatureRenderer.java
src/jalview/appletgui/FeatureSettings.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/FeatureRenderer.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Jalview2XML.java
src/jalview/io/FeaturesFile.java
src/jalview/io/PDBFeatureSettings.java
src/jalview/io/packed/JalviewDataset.java
src/jalview/io/packed/ParsePackedSet.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/schemes/FeatureColour.java
src/jalview/schemes/GraduatedColor.java
src/jalview/schemes/UserColourScheme.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/ws/jws1/SeqSearchWSThread.java
src/jalview/ws/jws2/AADisorderClient.java
test/jalview/io/FeaturesFileTest.java
test/jalview/schemes/FeatureColourTest.java

index 0000000,34b64ee..614b440
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,27 +1,26 @@@
+ ##gff-version 2
+ # exonerate output in gff2 format; not gff3 because
+ #   - 'similarity' is not a Sequence Ontology term
+ #   - attributes' name/values are separated by space ' ' not equals '='
+ ##source-version exonerate:protein2genome:local 2.2.0
+ ##date 2015-01-16
+ ##type DNA
+ #
+ # exonerate run with --showtargetgff generates 'features on the target' i.e. mappings to the query
+ # tab-delimited
+ # seqname source feature start end score strand frame attributes
+ #
+ seq1  exonerate:protein2genome:local  gene    8       11      3652    -       .       gene_id 0 ; sequence seq2 ; gene_orientation .
+ seq1  exonerate:protein2genome:local  cds     9       11      .       -       .       
+ seq1  exonerate:protein2genome:local  exon    9       11      .       -       .       insertions 3 ; deletions 6
+ #seq1 exonerate:protein2genome:local  similarity      8       11      3652    -       .       alignment_id 0 ; Query seq2 ; Align 11 1 3
+ seq1  exonerate:protein2genome:local  similarity      9       11      3652    -       .       alignment_id 0 ; Query seq2 ; Align 11 1 3
+ #
+ # appending FASTA sequences is strictly a GFF3 format feature
+ # but Jalview is able to handle this mixture of GFF2 / GFF3 :-)
+ #
+ ##FASTA
+ >seq1
+ ACTACGACACGACGACGACGACG
+ >seq2
+ CDEQEATGTQDAQEQAQC
 -
@@@ -93,62 -73,8 +91,69 @@@ public interface FeatureColour
     */
    float getThreshold();
  
 +  void setThreshold(float f);
 +
++  /**
++   * Answers true if the colour varies between the actual minimum and maximum
++   * score values of the feature, or false if between absolute minimum and
++   * maximum values (or if not a graduated colour).
++   * 
++   * @return
++   */
 +  boolean isAutoScaled();
 +
 +  void setAutoScaled(boolean b);
 +
 +  /**
 +   * Returns the maximum score of the graduated colour range
 +   * 
 +   * @return
 +   */
 +  float getMax();
 +
 +  /**
 +   * Returns the minimum score of the graduated colour range
 +   * 
 +   * @return
 +   */
 +  float getMin();
 +
 +  /**
 +   * Answers true if either isAboveThreshold or isBelowThreshold answers true
 +   * 
 +   * @return
 +   */
 +  boolean hasThreshold();
 +
 +  /**
 +   * Returns the computed colour for the given sequence feature
 +   * 
 +   * @param feature
 +   * @return
 +   */
 +  Color getColor(SequenceFeature feature);
 +
 +  /**
 +   * Answers true if the feature has a simple colour, or is coloured by label,
 +   * or has a graduated colour and the score of this feature instance is within
 +   * the range to render (if any), i.e. does not lie below or above any
 +   * threshold set.
 +   * 
 +   * @param feature
 +   * @return
 +   */
 +  boolean isColored(SequenceFeature feature);
 +
 +  /**
 +   * Update the min-max range for a graduated colour scheme
 +   * 
 +   * @param min
 +   * @param max
 +   */
 +  void updateBounds(float min, float max);
 +
    /**
 -   * Answers true if ?
 +   * Returns the colour in Jalview features file format
     * 
     * @return
     */
Simple merge
@@@ -368,15 -367,12 +368,12 @@@ public class AlignFrame extends Embmenu
      boolean featuresFile = false;
      try
      {
-       featuresFile = new jalview.io.FeaturesFile(file, type).parse(viewport
-               .getAlignment(), alignPanel.seqPanel.seqCanvas
-               .getFeatureRenderer().getFeatureColours(), featureLinks,
-               true, viewport.applet.getDefaultParameter("relaxedidmatch",
-                       false));
 -      Map<String, Object> colours = alignPanel.seqPanel.seqCanvas
++      Map<String, FeatureColourI> colours = alignPanel.seqPanel.seqCanvas
+               .getFeatureRenderer().getFeatureColours();
+       boolean relaxedIdMatching = viewport.applet.getDefaultParameter(
+               "relaxedidmatch", false);
+       featuresFile = new FeaturesFile(file, type).parse(
+               viewport.getAlignment(), colours, true, relaxedIdMatching);
      } catch (Exception ex)
      {
        ex.printStackTrace();
@@@ -204,10 -190,10 +189,10 @@@ public class FeatureSettings extends Pa
            int x, int y)
    {
      final String type = check.type;
 -    final Object typeCol = fr.getFeatureStyle(type);
 -    java.awt.PopupMenu men = new PopupMenu(MessageManager.formatMessage(
 +    final FeatureColourI typeCol = fr.getFeatureStyle(type);
 +    PopupMenu men = new PopupMenu(MessageManager.formatMessage(
              "label.settings_for_type", new String[] { type }));
-     MenuItem scr = new MenuItem(
+     java.awt.MenuItem scr = new MenuItem(
              MessageManager.getString("label.sort_by_score"));
      men.add(scr);
      final FeatureSettings me = this;
      }
    }
  
 -  @Override
 -  public void mousePressed(MouseEvent e)
 -  {
 -  }
 -
+   /**
+    * Hide columns containing (or not containing) a given feature type
+    * 
+    * @param type
+    * @param columnsContaining
+    */
+   void hideFeatureColumns(final String type,
+           boolean columnsContaining)
+   {
+     if (ap.alignFrame.avc.markColumnsContainingFeatures(
+             columnsContaining, false, false, type))
+     {
+       if (ap.alignFrame.avc.markColumnsContainingFeatures(
+               !columnsContaining, false, false, type))
+       {
+         ap.alignFrame.viewport.hideSelectedColumns();
+       }
+     }
+   }
++  @Override
++  public void mousePressed(MouseEvent e)
++  {
++    // TODO Auto-generated method stub
++
++  }
++
  }
index 0000000,4dd1bba..b4d2783
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,590 +1,590 @@@
+ package jalview.ext.ensembl;
+ import jalview.api.FeatureColourI;
+ import jalview.api.FeatureSettingsModelI;
+ import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.Sequence;
+ import jalview.datamodel.SequenceFeature;
+ import jalview.datamodel.SequenceI;
+ import jalview.io.gff.SequenceOntologyFactory;
+ import jalview.io.gff.SequenceOntologyI;
 -import jalview.schemes.FeatureColourAdapter;
++import jalview.schemes.FeatureColour;
+ import jalview.schemes.FeatureSettingsAdapter;
+ import jalview.util.MapList;
+ import java.awt.Color;
+ import java.io.UnsupportedEncodingException;
+ import java.net.URLDecoder;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.List;
+ import com.stevesoft.pat.Regex;
+ /**
+  * A class that fetches genomic sequence and all transcripts for an Ensembl gene
+  * 
+  * @author gmcarstairs
+  */
+ public class EnsemblGene extends EnsemblSeqProxy
+ {
+   private static final String GENE_PREFIX = "gene:";
+   /*
+    * accepts anything as we will attempt lookup of gene or 
+    * transcript id or gene name
+    */
+   private static final Regex ACCESSION_REGEX = new Regex(".*");
+   private static final EnsemblFeatureType[] FEATURES_TO_FETCH = {
+       EnsemblFeatureType.gene, EnsemblFeatureType.transcript,
+       EnsemblFeatureType.exon, EnsemblFeatureType.cds,
+       EnsemblFeatureType.variation };
+   /**
+    * Default constructor (to use rest.ensembl.org)
+    */
+   public EnsemblGene()
+   {
+     super();
+   }
+   /**
+    * Constructor given the target domain to fetch data from
+    * 
+    * @param d
+    */
+   public EnsemblGene(String d)
+   {
+     super(d);
+   }
+   @Override
+   public String getDbName()
+   {
+     return "ENSEMBL";
+   }
+   @Override
+   protected EnsemblFeatureType[] getFeaturesToFetch()
+   {
+     return FEATURES_TO_FETCH;
+   }
+   @Override
+   protected EnsemblSeqType getSourceEnsemblType()
+   {
+     return EnsemblSeqType.GENOMIC;
+   }
+   /**
+    * Returns an alignment containing the gene(s) for the given gene or
+    * transcript identifier, or external identifier (e.g. Uniprot id). If given a
+    * gene name or external identifier, returns any related gene sequences found
+    * for model organisms. If only a single gene is queried for, then its
+    * transcripts are also retrieved and added to the alignment. <br>
+    * Method:
+    * <ul>
+    * <li>resolves a transcript identifier by looking up its parent gene id</li>
+    * <li>resolves an external identifier by looking up xref-ed gene ids</li>
+    * <li>fetches the gene sequence</li>
+    * <li>fetches features on the sequence</li>
+    * <li>identifies "transcript" features whose Parent is the requested gene</li>
+    * <li>fetches the transcript sequence for each transcript</li>
+    * <li>makes a mapping from the gene to each transcript</li>
+    * <li>copies features from gene to transcript sequences</li>
+    * <li>fetches the protein sequence for each transcript, maps and saves it as
+    * a cross-reference</li>
+    * <li>aligns each transcript against the gene sequence based on the position
+    * mappings</li>
+    * </ul>
+    * 
+    * @param query
+    *          a single gene or transcript identifier or gene name
+    * @return an alignment containing a gene, and possibly transcripts, or null
+    */
+   @Override
+   public AlignmentI getSequenceRecords(String query) throws Exception
+   {
+     /*
+      * convert to a non-duplicated list of gene identifiers
+      */
+     List<String> geneIds = getGeneIds(query);
+     AlignmentI al = null;
+     for (String geneId : geneIds)
+     {
+       /*
+        * fetch the gene sequence(s) with features and xrefs
+        */
+       AlignmentI geneAlignment = super.getSequenceRecords(geneId);
+       if (geneAlignment.getHeight() == 1)
+       {
+         getTranscripts(geneAlignment, geneId);
+       }
+       if (al == null)
+       {
+         al = geneAlignment;
+       }
+       else
+       {
+         al.append(geneAlignment);
+       }
+     }
+     return al;
+   }
+   /**
+    * Converts a query, which may contain one or more gene or transcript
+    * identifiers, into a non-redundant list of gene identifiers.
+    * 
+    * @param accessions
+    * @return
+    */
+   List<String> getGeneIds(String accessions)
+   {
+     List<String> geneIds = new ArrayList<String>();
+     for (String acc : accessions.split(getAccessionSeparator()))
+     {
+       if (isGeneIdentifier(acc))
+       {
+         if (!geneIds.contains(acc))
+         {
+           geneIds.add(acc);
+         }
+       }
+       /*
+        * if given a transcript id, look up its gene parent
+        */
+       else if (isTranscriptIdentifier(acc))
+       {
+         String geneId = new EnsemblLookup(getDomain()).getParent(acc);
+         if (geneId != null && !geneIds.contains(geneId))
+         {
+           geneIds.add(geneId);
+         }
+       }
+       /*
+        * if given a gene or other external name, lookup and fetch 
+        * the corresponding gene for all model organisms 
+        */
+       else
+       {
+         List<String> ids = new EnsemblSymbol(getDomain()).getIds(acc);
+         for (String geneId : ids)
+         {
+           if (!geneIds.contains(geneId))
+           {
+             geneIds.add(geneId);
+           }
+         }
+       }
+     }
+     return geneIds;
+   }
+   /**
+    * Attempts to get Ensembl stable identifiers for model organisms for a gene
+    * name by calling the xrefs symbol REST service to resolve the gene name.
+    * 
+    * @param query
+    * @return
+    */
+   protected String getGeneIdentifiersForName(String query)
+   {
+     List<String> ids = new EnsemblSymbol(getDomain()).getIds(query);
+     if (ids != null)
+     {
+       for (String id : ids)
+       {
+         if (isGeneIdentifier(id))
+         {
+           return id;
+         }
+       }
+     }
+     return null;
+   }
+   /**
+    * Constructs all transcripts for the gene, as identified by "transcript"
+    * features whose Parent is the requested gene. The coding transcript
+    * sequences (i.e. with introns omitted) are added to the alignment.
+    * 
+    * @param al
+    * @param accId
+    * @throws Exception
+    */
+   protected void getTranscripts(AlignmentI al, String accId)
+           throws Exception
+   {
+     SequenceI gene = al.getSequenceAt(0);
+     List<SequenceFeature> transcriptFeatures = getTranscriptFeatures(accId,
+             gene);
+     for (SequenceFeature transcriptFeature : transcriptFeatures)
+     {
+       makeTranscript(transcriptFeature, al, gene);
+     }
+     clearGeneFeatures(gene);
+   }
+   /**
+    * Remove unwanted features (transcript, exon, CDS) from the gene sequence
+    * after we have used them to derive transcripts and transfer features
+    * 
+    * @param gene
+    */
+   protected void clearGeneFeatures(SequenceI gene)
+   {
+     SequenceFeature[] sfs = gene.getSequenceFeatures();
+     if (sfs != null)
+     {
+       SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+       List<SequenceFeature> filtered = new ArrayList<SequenceFeature>();
+       for (SequenceFeature sf : sfs)
+       {
+         String type = sf.getType();
+         if (!isTranscript(type) && !so.isA(type, SequenceOntologyI.EXON)
+                 && !so.isA(type, SequenceOntologyI.CDS))
+         {
+           filtered.add(sf);
+         }
+       }
+       gene.setSequenceFeatures(filtered
+               .toArray(new SequenceFeature[filtered
+               .size()]));
+     }
+   }
+   /**
+    * Constructs a spliced transcript sequence by finding 'exon' features for the
+    * given id (or failing that 'CDS'). Copies features on to the new sequence.
+    * 'Aligns' the new sequence against the gene sequence by padding with gaps,
+    * and adds it to the alignment.
+    * 
+    * @param transcriptFeature
+    * @param al
+    *          the alignment to which to add the new sequence
+    * @param gene
+    *          the parent gene sequence, with features
+    * @return
+    */
+   SequenceI makeTranscript(SequenceFeature transcriptFeature,
+           AlignmentI al, SequenceI gene)
+   {
+     String accId = getTranscriptId(transcriptFeature);
+     if (accId == null)
+     {
+       return null;
+     }
+     /*
+      * NB we are mapping from gene sequence (not genome), so do not
+      * need to check for reverse strand (gene and transcript sequences 
+      * are in forward sense)
+      */
+     /*
+      * make a gene-length sequence filled with gaps
+      * we will fill in the bases for transcript regions
+      */
+     char[] seqChars = new char[gene.getLength()];
+     Arrays.fill(seqChars, al.getGapCharacter());
+     /*
+      * look for exon features of the transcript, failing that for CDS
+      * (for example ENSG00000124610 has 1 CDS but no exon features)
+      */
+     String parentId = "transcript:" + accId;
+     List<SequenceFeature> splices = findFeatures(gene,
+             SequenceOntologyI.EXON, parentId);
+     if (splices.isEmpty())
+     {
+       splices = findFeatures(gene, SequenceOntologyI.CDS, parentId);
+     }
+     int transcriptLength = 0;
+     final char[] geneChars = gene.getSequence();
+     int offset = gene.getStart(); // to convert to 0-based positions
+     List<int[]> mappedFrom = new ArrayList<int[]>();
+     for (SequenceFeature sf : splices)
+     {
+       int start = sf.getBegin() - offset;
+       int end = sf.getEnd() - offset;
+       int spliceLength = end - start + 1;
+       System.arraycopy(geneChars, start, seqChars, start, spliceLength);
+       transcriptLength += spliceLength;
+       mappedFrom.add(new int[] { sf.getBegin(), sf.getEnd() });
+     }
+     Sequence transcript = new Sequence(accId, seqChars, 1, transcriptLength);
+     /*
+      * Ensembl has gene name as transcript Name
+      * EnsemblGenomes doesn't, but has a url-encoded description field
+      */
+     String description = (String) transcriptFeature.getValue(NAME);
+     if (description == null)
+     {
+       description = (String) transcriptFeature.getValue(DESCRIPTION);
+     }
+     if (description != null)
+     {
+       try
+       {
+         transcript.setDescription(URLDecoder.decode(description, "UTF-8"));
+       } catch (UnsupportedEncodingException e)
+       {
+         e.printStackTrace(); // as if
+       }
+     }
+     transcript.createDatasetSequence();
+     al.addSequence(transcript);
+     /*
+      * transfer features to the new sequence; we use EnsemblCdna to do this,
+      * to filter out unwanted features types (see method retainFeature)
+      */
+     List<int[]> mapTo = new ArrayList<int[]>();
+     mapTo.add(new int[] { 1, transcriptLength });
+     MapList mapping = new MapList(mappedFrom, mapTo, 1, 1);
+     EnsemblCdna cdna = new EnsemblCdna(getDomain());
+     cdna.transferFeatures(gene.getSequenceFeatures(),
+             transcript.getDatasetSequence(), mapping, parentId);
+     /*
+      * fetch and save cross-references
+      */
+     cdna.getCrossReferences(transcript);
+     /*
+      * and finally fetch the protein product and save as a cross-reference
+      */
+     cdna.addProteinProduct(transcript);
+     return transcript;
+   }
+   /**
+    * Returns the 'transcript_id' property of the sequence feature (or null)
+    * 
+    * @param feature
+    * @return
+    */
+   protected String getTranscriptId(SequenceFeature feature)
+   {
+     return (String) feature.getValue("transcript_id");
+   }
+   /**
+    * Returns a list of the transcript features on the sequence whose Parent is
+    * the gene for the accession id.
+    * 
+    * @param accId
+    * @param geneSequence
+    * @return
+    */
+   protected List<SequenceFeature> getTranscriptFeatures(String accId,
+           SequenceI geneSequence)
+   {
+     List<SequenceFeature> transcriptFeatures = new ArrayList<SequenceFeature>();
+     String parentIdentifier = GENE_PREFIX + accId;
+     SequenceFeature[] sfs = geneSequence.getSequenceFeatures();
+     if (sfs != null)
+     {
+       for (SequenceFeature sf : sfs)
+       {
+         if (isTranscript(sf.getType()))
+         {
+           String parent = (String) sf.getValue(PARENT);
+           if (parentIdentifier.equals(parent))
+           {
+             transcriptFeatures.add(sf);
+           }
+         }
+       }
+     }
+     return transcriptFeatures;
+   }
+   @Override
+   public String getDescription()
+   {
+     return "Fetches all transcripts and variant features for a gene or transcript";
+   }
+   /**
+    * Default test query is a gene id (can also enter a transcript id)
+    */
+   @Override
+   public String getTestQuery()
+   {
+     return "ENSG00000157764"; // BRAF, 5 transcripts, reverse strand
+     // ENSG00000090266 // NDUFB2, 15 transcripts, forward strand
+     // ENSG00000101812 // H2BFM histone, 3 transcripts, forward strand
+     // ENSG00000123569 // H2BFWT histone, 2 transcripts, reverse strand
+   }
+   /**
+    * 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
+    */
+   @Override
+   protected boolean identifiesSequence(SequenceFeature sf, String accId)
+   {
+     if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
+             SequenceOntologyI.GENE))
+     {
+       String id = (String) sf.getValue(ID);
+       if ((GENE_PREFIX + accId).equals(id))
+       {
+         return true;
+       }
+     }
+     return false;
+   }
+   /**
+    * Answers true unless feature type is 'gene', or 'transcript' with a parent
+    * which is a different gene. We need the gene features to identify the range,
+    * but it is redundant information on the gene sequence. Checking the parent
+    * allows us to drop transcript features which belong to different
+    * (overlapping) genes.
+    */
+   @Override
+   protected boolean retainFeature(SequenceFeature sf, String accessionId)
+   {
+     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+     String type = sf.getType();
+     if (so.isA(type, SequenceOntologyI.GENE))
+     {
+       return false;
+     }
+     if (isTranscript(type))
+     {
+       String parent = (String) sf.getValue(PARENT);
+       if (!(GENE_PREFIX + accessionId).equals(parent))
+       {
+         return false;
+       }
+     }
+     return true;
+   }
+   /**
+    * 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
+    */
+   @Override
+   protected void addProteinProduct(SequenceI querySeq)
+   {
+   }
+   @Override
+   public Regex getAccessionValidator()
+   {
+     return ACCESSION_REGEX;
+   }
+   /**
+    * Returns a descriptor for suitable feature display settings with
+    * <ul>
+    * <li>only exon or sequence_variant features (or their subtypes in the
+    * Sequence Ontology) visible</li>
+    * <li>variant features coloured red</li>
+    * <li>exon features coloured by label (exon name)</li>
+    * <li>variants displayed above (on top of) exons</li>
+    * </ul>
+    */
+   @Override
+   public FeatureSettingsModelI getFeatureColourScheme()
+   {
+     return new FeatureSettingsAdapter()
+     {
+       SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+       @Override
+       public boolean isFeatureDisplayed(String type)
+       {
+         return (so.isA(type, SequenceOntologyI.EXON) || so.isA(type,
+                 SequenceOntologyI.SEQUENCE_VARIANT));
+       }
+       @Override
+       public FeatureColourI getFeatureColour(String type)
+       {
+         if (so.isA(type, SequenceOntologyI.EXON))
+         {
 -          return new FeatureColourAdapter()
++          return new FeatureColour()
+           {
+             @Override
+             public boolean isColourByLabel()
+             {
+               return true;
+             }
+           };
+         }
+         if (so.isA(type, SequenceOntologyI.SEQUENCE_VARIANT))
+         {
 -          return new FeatureColourAdapter()
++          return new FeatureColour()
+           {
+             @Override
+             public Color getColour()
+             {
+               return Color.RED;
+             }
+           };
+         }
+         return null;
+       }
+       /**
+        * order to render sequence_variant after exon after the rest
+        */
+       @Override
+       public int compare(String feature1, String feature2)
+       {
+         if (so.isA(feature1, SequenceOntologyI.SEQUENCE_VARIANT))
+         {
+           return +1;
+         }
+         if (so.isA(feature2, SequenceOntologyI.SEQUENCE_VARIANT))
+         {
+           return -1;
+         }
+         if (so.isA(feature1, SequenceOntologyI.EXON))
+         {
+           return +1;
+         }
+         if (so.isA(feature2, SequenceOntologyI.EXON))
+         {
+           return -1;
+         }
+         return 0;
+       }
+     };
+   }
+ }
@@@ -20,8 -20,8 +20,9 @@@
   */
  package jalview.gui;
  
 +import jalview.api.FeatureColourI;
  import jalview.datamodel.AlignmentAnnotation;
+ import jalview.datamodel.SequenceI;
  import jalview.io.AnnotationFile;
  import jalview.io.FeaturesFile;
  import jalview.io.JalviewFileChooser;
@@@ -155,17 -155,20 +156,27 @@@ public class AnnotationExporter extend
              .getString("label.no_features_on_alignment");
      if (features)
      {
 +      Map<String, FeatureColourI> displayedFeatureColours = ap
 +              .getFeatureRenderer().getDisplayedFeatureCols();
+       FeaturesFile formatter = new FeaturesFile();
+       SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
 -      Map<String, Object> featureColours = ap.getFeatureRenderer()
++      Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
+               .getDisplayedFeatureCols();
+       boolean includeNonPositional = ap.av.isShowNPFeats();
        if (GFFFormat.isSelected())
        {
-         text = new FeaturesFile().printGFFFormat(ap.av.getAlignment()
-                 .getDataset().getSequencesArray(), displayedFeatureColours, true, ap.av.isShowNPFeats());// ap.av.featuresDisplayed//);
++        text = new FeaturesFile().printGffFormat(ap.av.getAlignment()
++                .getDataset().getSequencesArray(), displayedFeatureColours,
++                true, ap.av.isShowNPFeats());
+         text = formatter.printGffFormat(sequences, featureColours, true,
+                 includeNonPositional);
        }
        else
        {
 +        text = new FeaturesFile().printJalviewFormat(ap.av.getAlignment()
 +                .getDataset().getSequencesArray(), displayedFeatureColours, true, ap.av.isShowNPFeats()); // ap.av.featuresDisplayed);
+         text = formatter.printJalviewFormat(sequences, featureColours,
+                 true, includeNonPositional);
        }
      }
      else
@@@ -438,7 -439,19 +440,18 @@@ public class FeatureRenderer extend
      {
        colour.setBackground(bigPanel.getBackground());
        colour.setForeground(Color.black);
 -      FeatureSettings.renderGraduatedColor(colour, (GraduatedColor) col2);
 -      // colour.setForeground(colour.getBackground());
 +      FeatureSettings.renderGraduatedColor(colour, col);
      }
    }
+   /**
+    * Orders features in render precedence (last in order is last to render, so
+    * displayed on top of other features)
+    * 
+    * @param order
+    */
+   public void orderFeatures(Comparator<String> order)
+   {
+     Arrays.sort(renderOrder, order);
+   }
  }
Simple merge
@@@ -1196,31 -1196,31 +1197,31 @@@ public class Jalview2XM
                  .toArray(new String[0]);
  
          Vector<String> settingsAdded = new Vector<String>();
 -        Object gstyle = null;
 -        GraduatedColor gcol = null;
          if (renderOrder != null)
          {
-           for (int ro = 0; ro < renderOrder.length; ro++)
+           for (String featureType : renderOrder)
            {
-             FeatureColourI gstyle = ap.getSeqPanel().seqCanvas
 -            gstyle = ap.getSeqPanel().seqCanvas.getFeatureRenderer()
++            FeatureColourI fcol = ap.getSeqPanel().seqCanvas
 +                    .getFeatureRenderer()
-                     .getFeatureStyle(renderOrder[ro]);
+                     .getFeatureStyle(featureType);
              Setting setting = new Setting();
-             setting.setType(renderOrder[ro]);
-             if (!gstyle.isSimpleColour())
+             setting.setType(featureType);
 -            if (gstyle instanceof GraduatedColor)
++            if (!fcol.isSimpleColour())
              {
-               setting.setColour(gstyle.getMaxColour().getRGB());
-               setting.setMincolour(gstyle.getMinColour().getRGB());
-               setting.setMin(gstyle.getMin());
-               setting.setMax(gstyle.getMax());
-               setting.setColourByLabel(gstyle.isColourByLabel());
-               setting.setAutoScale(gstyle.isAutoScaled());
-               setting.setThreshold(gstyle.getThreshold());
 -              gcol = (GraduatedColor) gstyle;
 -              setting.setColour(gcol.getMaxColor().getRGB());
 -              setting.setMincolour(gcol.getMinColor().getRGB());
 -              setting.setMin(gcol.getMin());
 -              setting.setMax(gcol.getMax());
 -              setting.setColourByLabel(gcol.isColourByLabel());
 -              setting.setAutoScale(gcol.isAutoScale());
 -              setting.setThreshold(gcol.getThresh());
 -              setting.setThreshstate(gcol.getThreshType());
++              setting.setColour(fcol.getMaxColour().getRGB());
++              setting.setMincolour(fcol.getMinColour().getRGB());
++              setting.setMin(fcol.getMin());
++              setting.setMax(fcol.getMax());
++              setting.setColourByLabel(fcol.isColourByLabel());
++              setting.setAutoScale(fcol.isAutoScaled());
++              setting.setThreshold(fcol.getThreshold());
 +              // -1 = No threshold, 0 = Below, 1 = Above
-               setting.setThreshstate(gstyle.isAboveThreshold() ? 1
-                       : (gstyle.isBelowThreshold() ? 0 : -1));
++              setting.setThreshstate(fcol.isAboveThreshold() ? 1
++                      : (fcol.isBelowThreshold() ? 0 : -1));
              }
              else
              {
-               setting.setColour(gstyle.getColour().getRGB());
 -              setting.setColour(((Color) gstyle).getRGB());
++              setting.setColour(fcol.getColour().getRGB());
              }
  
              setting.setDisplay(av.getFeaturesDisplayed().isVisible(
   */
  package jalview.io;
  
+ import jalview.analysis.AlignmentUtils;
  import jalview.analysis.SequenceIdMatcher;
+ import jalview.api.AlignViewportI;
 +import jalview.api.FeatureColourI;
+ import jalview.api.FeaturesSourceI;
  import jalview.datamodel.AlignedCodonFrame;
+ import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.SequenceDummy;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
+ import jalview.io.gff.GffHelperBase;
+ import jalview.io.gff.GffHelperFactory;
+ import jalview.io.gff.GffHelperI;
 -import jalview.schemes.AnnotationColourGradient;
 -import jalview.schemes.GraduatedColor;
 +import jalview.schemes.FeatureColour;
  import jalview.schemes.UserColourScheme;
 -import jalview.util.Format;
  import jalview.util.MapList;
+ import jalview.util.ParseHtmlBodyAndLinks;
+ import jalview.util.StringUtils;
  
 -import java.awt.Color;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Arrays;
@@@ -38,28 -49,42 +47,41 @@@ import java.util.HashMap
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
- import java.util.StringTokenizer;
- import java.util.Vector;
+ import java.util.Map.Entry;
 -import java.util.StringTokenizer;
  
  /**
-  * Parse and create Jalview Features files Detects GFF format features files and
-  * parses. Does not implement standard print() - call specific printFeatures or
-  * printGFF. Uses AlignmentI.findSequence(String id) to find the sequence object
-  * for the features annotation - this normally works on an exact match.
+  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
+  * format. These are tab-delimited formats but with differences in the use of
+  * columns.
+  * 
+  * A Jalview feature file may define feature colours and then declare that the
+  * remainder of the file is in GFF format with the line 'GFF'.
+  * 
+  * GFF3 files may include alignment mappings for features, which Jalview will
+  * attempt to model, and may include sequence data following a ##FASTA line.
+  * 
   * 
   * @author AMW
-  * @version $Revision$
+  * @author jbprocter
+  * @author gmcarstairs
   */
- public class FeaturesFile extends AlignFile
+ public class FeaturesFile extends AlignFile implements FeaturesSourceI
  {
-   /**
-    * work around for GFF interpretation bug where source string becomes
-    * description rather than a group
-    */
-   private boolean doGffSource = true;
+   private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED";
+   private static final String NOTE = "Note";
+   protected static final String TAB = "\t";
+   protected static final String GFF_VERSION = "##gff-version";
  
-   private int gffversion;
+   private AlignmentI lastmatchedAl = null;
+   private SequenceIdMatcher matcher = null;
+   protected AlignmentI dataset;
+   protected int gffVersion;
  
    /**
     * Creates a new FeaturesFile object.
     *          - process html strings into plain text
     * @return true if features were added
     */
-   public boolean parse(AlignmentI align, Map colours, boolean removeHTML)
-   {
-     return parse(align, colours, null, removeHTML, false);
-   }
-   /**
-    * Parse GFF or sequence features file optionally using case-independent
-    * matching, discarding URLs
-    * 
-    * @param align
-    *          - alignment/dataset containing sequences that are to be annotated
-    * @param colours
-    *          - hashtable to store feature colour 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 colours, boolean removeHTML,
-           boolean relaxedIdMatching)
 -  public boolean parse(AlignmentI align, Map<String, Object> colours,
++  public boolean parse(AlignmentI align,
++          Map<String, FeatureColourI> colours,
+           boolean removeHTML)
    {
-     return parse(align, colours, null, removeHTML, relaxedIdMatching);
+     return parse(align, colours, removeHTML, false);
    }
  
    /**
     *          - when true, ID matches to compound sequence IDs are allowed
     * @return true if features were added
     */
-   public boolean parse(AlignmentI align, Map colours, Map featureLink,
 -  public boolean parse(AlignmentI align, Map<String, Object> colours,
++  public boolean parse(AlignmentI align,
++          Map<String, FeatureColourI> colours,
            boolean removeHTML, boolean relaxedIdmatching)
    {
+     Map<String, String> gffProps = new HashMap<String, String>();
+     /*
+      * keep track of any sequences we try to create from the data
+      */
+     List<SequenceI> newseqs = new ArrayList<SequenceI>();
  
      String line = null;
      try
              continue;
            }
          }
-         if (st.countTokens() > 1 && st.countTokens() < 4)
+         if (gffColumns.length > 1 && gffColumns.length < 4)
          {
-           GFFFile = false;
-           theType = st.nextToken();
-           if (theType.equalsIgnoreCase("startgroup"))
+           /*
+            * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
+            * a feature type colour specification
+            */
+           String ft = gffColumns[0];
+           if (ft.equalsIgnoreCase("startgroup"))
            {
-             featureGroup = st.nextToken();
-             if (st.hasMoreElements())
-             {
-               groupLink = st.nextToken();
-               featureLink.put(featureGroup, groupLink);
-             }
+             featureGroup = gffColumns[1];
            }
-           else if (theType.equalsIgnoreCase("endgroup"))
+           else if (ft.equalsIgnoreCase("endgroup"))
            {
              // We should check whether this is the current group,
--            // but at present theres no way of showing more than 1 group
-             st.nextToken();
++            // but at present there's no way of showing more than 1 group
              featureGroup = null;
            }
            else
            {
-             String colscheme = st.nextToken();
-             try
-             {
-               FeatureColourI colour = FeatureColour
-                       .parseJalviewFeatureColour(colscheme);
-               if (colour != null)
-               {
-                 colours.put(theType, colour);
-               }
-               if (st.hasMoreElements())
-               {
-                 String link = st.nextToken();
-                 typeLink.put(theType, link);
-                 if (featureLink == null)
-                 {
-                   featureLink = new Hashtable();
-                 }
-                 featureLink.put(theType, link);
-               }
-             } catch (IllegalArgumentException e)
 -            parseFeatureColour(line, ft, gffColumns, colours);
++            String colscheme = gffColumns[1];
++            FeatureColourI colour = FeatureColour
++                    .parseJalviewFeatureColour(colscheme);
++            if (colour != null)
 +            {
-               System.err.println("Error parsing feature colour scheme "
-                       + colscheme + " : " + e.getMessage());
++              colours.put(ft, colour);
 +            }
            }
            continue;
          }
    }
  
    /**
-    * take a sequence feature and examine its attributes to decide how it should
-    * be added to a sequence
+    * 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.
     * 
-    * @param seq
-    *          - the destination sequence constructed or discovered in the
-    *          current context
-    * @param sf
-    *          - the base feature with ATTRIBUTES property containing any
-    *          additional attributes
-    * @param gFFFile
-    *          - true if we are processing a GFF annotation file
-    * @return true if sf was actually added to the sequence, false if it was
-    *         processed in another way
+    * @param line
+    * @param gffColumns
+    * @param alignment
+    * @param featureColours
+    * @param removeHTML
+    * @param relaxedIdmatching
+    * @param featureGroup
     */
-   public boolean processOrAddSeqFeature(AlignmentI align,
-           List<SequenceI> newseqs, SequenceI seq, SequenceFeature sf,
-           boolean gFFFile, boolean relaxedIdMatching)
+   protected boolean parseJalviewFeature(String line, String[] gffColumns,
 -          AlignmentI alignment, Map<String, Object> featureColours,
++          AlignmentI alignment, Map<String, FeatureColourI> featureColours,
+           boolean removeHTML, boolean relaxedIdMatching, String featureGroup)
    {
-     String attr = (String) sf.getValue("ATTRIBUTES");
-     boolean add = true;
-     if (gFFFile && attr != null)
+     /*
+      * tokens: description seqid seqIndex start end type [score]
+      */
+     if (gffColumns.length < 6)
      {
-       int nattr = 8;
-       for (String attset : attr.split("\t"))
-       {
-         if (attset == null || attset.trim().length() == 0)
-         {
-           continue;
-         }
-         nattr++;
-         Map<String, List<String>> set = new HashMap<String, List<String>>();
-         // normally, only expect one column - 9 - in this field
-         // the attributes (Gff3) or groups (gff2) field
-         for (String pair : attset.trim().split(";"))
-         {
-           pair = pair.trim();
-           if (pair.length() == 0)
-           {
-             continue;
-           }
-           // expect either space seperated (gff2) or '=' separated (gff3)
-           // key/value pairs here
-           int eqpos = pair.indexOf('='), sppos = pair.indexOf(' ');
-           String key = null, value = null;
-           if (sppos > -1 && (eqpos == -1 || sppos < eqpos))
-           {
-             key = pair.substring(0, sppos);
-             value = pair.substring(sppos + 1);
-           }
-           else
-           {
-             if (eqpos > -1 && (sppos == -1 || eqpos < sppos))
-             {
-               key = pair.substring(0, eqpos);
-               value = pair.substring(eqpos + 1);
-             }
-             else
-             {
-               key = pair;
-             }
-           }
-           if (key != null)
-           {
-             List<String> vals = set.get(key);
-             if (vals == null)
-             {
-               vals = new ArrayList<String>();
-               set.put(key, vals);
-             }
-             if (value != null)
-             {
-               vals.add(value.trim());
-             }
-           }
-         }
-         try
-         {
-           add &= processGffKey(set, nattr, seq, sf, align, newseqs,
-                   relaxedIdMatching); // process decides if
-                                       // feature is actually
-                                       // added
-         } catch (InvalidGFF3FieldException ivfe)
-         {
-           System.err.println(ivfe);
-         }
-       }
-     }
-     if (add)
-     {
-       seq.addSequenceFeature(sf);
+       System.err.println("Ignoring feature line '" + line
+               + "' with too few columns (" + gffColumns.length + ")");
+       return false;
      }
-     return add;
-   }
-   public class InvalidGFF3FieldException extends Exception
-   {
-     String field, value;
+     String desc = gffColumns[0];
+     String seqId = gffColumns[1];
+     SequenceI seq = findSequence(seqId, alignment, null, relaxedIdMatching);
  
-     public InvalidGFF3FieldException(String field,
-             Map<String, List<String>> set, String message)
+     if (!ID_NOT_SPECIFIED.equals(seqId))
      {
-       super(message + " (Field was " + field + " and value was "
-               + set.get(field).toString());
-       this.field = field;
-       this.value = set.get(field).toString();
+       seq = findSequence(seqId, alignment, null, relaxedIdMatching);
      }
-   }
-   /**
-    * take a set of keys for a feature and interpret them
-    * 
-    * @param set
-    * @param nattr
-    * @param seq
-    * @param sf
-    * @return
-    */
-   public boolean processGffKey(Map<String, List<String>> set, int nattr,
-           SequenceI seq, SequenceFeature sf, AlignmentI align,
-           List<SequenceI> newseqs, boolean relaxedIdMatching)
-           throws InvalidGFF3FieldException
-   {
-     String attr;
-     // decide how to interpret according to type
-     if (sf.getType().equals("similarity"))
+     else
      {
-       int strand = sf.getStrand();
-       // exonerate cdna/protein map
-       // look for fields
-       List<SequenceI> querySeq = findNames(align, newseqs,
-               relaxedIdMatching, set.get(attr = "Query"));
-       if (querySeq == null || querySeq.size() != 1)
+       seqId = null;
+       seq = null;
+       String seqIndex = gffColumns[2];
+       try
        {
-         throw new InvalidGFF3FieldException(attr, set,
-                 "Expecting exactly one sequence in Query field (got "
-                         + set.get(attr) + ")");
-       }
-       if (set.containsKey(attr = "Align"))
+         int idx = Integer.parseInt(seqIndex);
+         seq = alignment.getSequenceAt(idx);
+       } catch (NumberFormatException ex)
        {
-         // process the align maps and create cdna/protein maps
-         // ideally, the query sequences are in the alignment, but maybe not...
-         AlignedCodonFrame alco = new AlignedCodonFrame();
-         MapList codonmapping = constructCodonMappingFromAlign(set, attr,
-                 strand);
-         // add codon mapping, and hope!
-         alco.addMap(seq, querySeq.get(0), codonmapping);
-         align.addCodonFrame(alco);
-         // everything that's needed to be done is done
-         // no features to create here !
-         return false;
+         System.err.println("Invalid sequence index: " + seqIndex);
        }
+     }
  
+     if (seq == null)
+     {
+       System.out.println("Sequence not found: " + line);
+       return false;
      }
-     return true;
-   }
  
-   private MapList constructCodonMappingFromAlign(
-           Map<String, List<String>> set, String attr, int strand)
-           throws InvalidGFF3FieldException
-   {
-     if (strand == 0)
+     int startPos = Integer.parseInt(gffColumns[3]);
+     int endPos = Integer.parseInt(gffColumns[4]);
+     String ft = gffColumns[5];
+     if (!featureColours.containsKey(ft))
      {
-       throw new InvalidGFF3FieldException(attr, set,
-               "Invalid strand for a codon mapping (cannot be 0)");
+       /* 
+        * Perhaps an old style groups file with no colours -
+        * synthesize a colour from the feature type
+        */
+       UserColourScheme ucs = new UserColourScheme(ft);
 -      featureColours.put(ft, ucs.findColour('A'));
++      featureColours.put(ft, new FeatureColour(ucs.findColour('A')));
      }
-     List<Integer> fromrange = new ArrayList<Integer>(), torange = new ArrayList<Integer>();
-     int lastppos = 0, lastpframe = 0;
-     for (String range : set.get(attr))
+     SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos,
+             endPos, featureGroup);
+     if (gffColumns.length > 6)
      {
-       List<Integer> ints = new ArrayList<Integer>();
-       StringTokenizer st = new StringTokenizer(range, " ");
-       while (st.hasMoreTokens())
-       {
-         String num = st.nextToken();
-         try
-         {
-           ints.add(new Integer(num));
-         } catch (NumberFormatException nfe)
-         {
-           throw new InvalidGFF3FieldException(attr, set,
-                   "Invalid number in field " + num);
-         }
-       }
-       // Align positionInRef positionInQuery LengthInRef
-       // contig_1146 exonerate:protein2genome:local similarity 8534 11269
-       // 3652 - . alignment_id 0 ;
-       // Query DDB_G0269124
-       // Align 11270 143 120
-       // corresponds to : 120 bases align at pos 143 in protein to 11270 on
-       // dna in strand direction
-       // Align 11150 187 282
-       // corresponds to : 282 bases align at pos 187 in protein to 11150 on
-       // dna in strand direction
-       //
-       // Align 10865 281 888
-       // Align 9977 578 1068
-       // Align 8909 935 375
-       //
-       if (ints.size() != 3)
-       {
-         throw new InvalidGFF3FieldException(attr, set,
-                 "Invalid number of fields for this attribute ("
-                         + ints.size() + ")");
-       }
-       fromrange.add(new Integer(ints.get(0).intValue()));
-       fromrange.add(new Integer(ints.get(0).intValue() + strand
-               * ints.get(2).intValue()));
-       // how are intron/exon boundaries that do not align in codons
-       // represented
-       if (ints.get(1).equals(lastppos) && lastpframe > 0)
+       float score = Float.NaN;
+       try
        {
-         // extend existing to map
-         lastppos += ints.get(2) / 3;
-         lastpframe = ints.get(2) % 3;
-         torange.set(torange.size() - 1, new Integer(lastppos));
-       }
-       else
+         score = new Float(gffColumns[6]).floatValue();
+         // update colourgradient bounds if allowed to
+       } catch (NumberFormatException ex)
        {
-         // new to map range
-         torange.add(ints.get(1));
-         lastppos = ints.get(1) + ints.get(2) / 3;
-         lastpframe = ints.get(2) % 3;
-         torange.add(new Integer(lastppos));
+         // leave as NaN
        }
-     }
-     // from and to ranges must end up being a series of start/end intervals
-     if (fromrange.size() % 2 == 1)
-     {
-       throw new InvalidGFF3FieldException(attr, set,
-               "Couldn't parse the DNA alignment range correctly");
-     }
-     if (torange.size() % 2 == 1)
-     {
-       throw new InvalidGFF3FieldException(attr, set,
-               "Couldn't parse the protein alignment range correctly");
-     }
-     // finally, build the map
-     int[] frommap = new int[fromrange.size()], tomap = new int[torange
-             .size()];
-     int p = 0;
-     for (Integer ip : fromrange)
-     {
-       frommap[p++] = ip.intValue();
-     }
-     p = 0;
-     for (Integer ip : torange)
-     {
-       tomap[p++] = ip.intValue();
+       sf.setScore(score);
      }
  
-     return new MapList(frommap, tomap, 3, 1);
-   }
+     parseDescriptionHTML(sf, removeHTML);
  
-   private List<SequenceI> findNames(AlignmentI align,
-           List<SequenceI> newseqs, boolean relaxedIdMatching,
-           List<String> list)
-   {
-     List<SequenceI> found = new ArrayList<SequenceI>();
-     for (String seqId : list)
+     seq.addSequenceFeature(sf);
+     while (seqId != null
+             && (seq = alignment.findName(seq, seqId, false)) != null)
      {
-       SequenceI seq = findName(align, seqId, relaxedIdMatching, newseqs);
-       if (seq != null)
-       {
-         found.add(seq);
-       }
+       seq.addSequenceFeature(new SequenceFeature(sf));
      }
-     return found;
+     return true;
    }
  
-   private AlignmentI lastmatchedAl = null;
-   private SequenceIdMatcher matcher = null;
    /**
 -   * Process a feature type colour specification
 -   * 
 -   * @param line
 -   *          the current input line (for error messages only)
 -   * @param featureType
 -   *          the first token on the line
 -   * @param gffColumns
 -   *          holds tokens on the line
 -   * @param colours
 -   *          map to which to add derived colour specification
 -   */
 -  protected void parseFeatureColour(String line, String featureType,
 -          String[] gffColumns, Map<String, Object> colours)
 -  {
 -    Object colour = null;
 -    String colscheme = gffColumns[1];
 -    if (colscheme.indexOf("|") > -1
 -            || colscheme.trim().equalsIgnoreCase("label"))
 -    {
 -      colour = parseGraduatedColourScheme(line, colscheme);
 -    }
 -    else
 -    {
 -      UserColourScheme ucs = new UserColourScheme(colscheme);
 -      colour = ucs.findColour('A');
 -    }
 -    if (colour != null)
 -    {
 -      colours.put(featureType, colour);
 -    }
 -  }
 -
 -  /**
 -   * Parse a Jalview graduated colour descriptor
 -   * 
 -   * @param line
 -   * @param colourDescriptor
 -   * @return
 -   */
 -  protected GraduatedColor parseGraduatedColourScheme(String line,
 -          String colourDescriptor)
 -  {
 -    // Parse '|' separated graduated colourscheme fields:
 -    // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]
 -    // can either provide 'label' only, first is optional, next two
 -    // colors are required (but may be
 -    // left blank), next is optional, nxt two min/max are required.
 -    // first is either 'label'
 -    // first/second and third are both hexadecimal or word equivalent
 -    // colour.
 -    // next two are values parsed as floats.
 -    // fifth is either 'above','below', or 'none'.
 -    // sixth is a float value and only required when fifth is either
 -    // 'above' or 'below'.
 -    StringTokenizer gcol = new StringTokenizer(colourDescriptor, "|", true);
 -    // set defaults
 -    float min = Float.MIN_VALUE, max = Float.MAX_VALUE;
 -    boolean labelCol = false;
 -    // Parse spec line
 -    String mincol = gcol.nextToken();
 -    if (mincol == "|")
 -    {
 -      System.err
 -              .println("Expected either 'label' or a colour specification in the line: "
 -                      + line);
 -      return null;
 -    }
 -    String maxcol = null;
 -    if (mincol.toLowerCase().indexOf("label") == 0)
 -    {
 -      labelCol = true;
 -      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip '|'
 -      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
 -    }
 -    String abso = null, minval, maxval;
 -    if (mincol != null)
 -    {
 -      // at least four more tokens
 -      if (mincol.equals("|"))
 -      {
 -        mincol = "";
 -      }
 -      else
 -      {
 -        gcol.nextToken(); // skip next '|'
 -      }
 -      // continue parsing rest of line
 -      maxcol = gcol.nextToken();
 -      if (maxcol.equals("|"))
 -      {
 -        maxcol = "";
 -      }
 -      else
 -      {
 -        gcol.nextToken(); // skip next '|'
 -      }
 -      abso = gcol.nextToken();
 -      gcol.nextToken(); // skip next '|'
 -      if (abso.toLowerCase().indexOf("abso") != 0)
 -      {
 -        minval = abso;
 -        abso = null;
 -      }
 -      else
 -      {
 -        minval = gcol.nextToken();
 -        gcol.nextToken(); // skip next '|'
 -      }
 -      maxval = gcol.nextToken();
 -      if (gcol.hasMoreTokens())
 -      {
 -        gcol.nextToken(); // skip next '|'
 -      }
 -      try
 -      {
 -        if (minval.length() > 0)
 -        {
 -          min = Float.valueOf(minval);
 -        }
 -      } catch (Exception e)
 -      {
 -        System.err
 -                .println("Couldn't parse the minimum value for graduated colour for type ("
 -                        + colourDescriptor
 -                        + ") - did you misspell 'auto' for the optional automatic colour switch ?");
 -        e.printStackTrace();
 -      }
 -      try
 -      {
 -        if (maxval.length() > 0)
 -        {
 -          max = Float.valueOf(maxval);
 -        }
 -      } catch (Exception e)
 -      {
 -        System.err
 -                .println("Couldn't parse the maximum value for graduated colour for type ("
 -                        + colourDescriptor + ")");
 -        e.printStackTrace();
 -      }
 -    }
 -    else
 -    {
 -      // add in some dummy min/max colours for the label-only
 -      // colourscheme.
 -      mincol = "FFFFFF";
 -      maxcol = "000000";
 -    }
 -
 -    GraduatedColor colour = null;
 -    try
 -    {
 -      colour = new GraduatedColor(
 -              new UserColourScheme(mincol).findColour('A'),
 -              new UserColourScheme(maxcol).findColour('A'), min, max);
 -    } catch (Exception e)
 -    {
 -      System.err.println("Couldn't parse the graduated colour scheme ("
 -              + colourDescriptor + ")");
 -      e.printStackTrace();
 -    }
 -    if (colour != null)
 -    {
 -      colour.setColourByLabel(labelCol);
 -      colour.setAutoScaled(abso == null);
 -      // 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"))
 -        {
 -          colour.setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);
 -        }
 -        else if (ttype.toLowerCase().startsWith("above"))
 -        {
 -          colour.setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);
 -        }
 -        else
 -        {
 -          colour.setThreshType(AnnotationColourGradient.NO_THRESHOLD);
 -          if (!ttype.toLowerCase().startsWith("no"))
 -          {
 -            System.err.println("Ignoring unrecognised threshold type : "
 -                    + ttype);
 -          }
 -        }
 -      }
 -      if (colour.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
 -      {
 -        try
 -        {
 -          gcol.nextToken();
 -          tval = gcol.nextToken();
 -          colour.setThresh(new Float(tval).floatValue());
 -        } catch (Exception e)
 -        {
 -          System.err.println("Couldn't parse threshold value as a float: ("
 -                  + tval + ")");
 -          e.printStackTrace();
 -        }
 -      }
 -      // parse the thresh-is-min token ?
 -      if (gcol.hasMoreTokens())
 -      {
 -        System.err
 -                .println("Ignoring additional tokens in parameters in graduated colour specification\n");
 -        while (gcol.hasMoreTokens())
 -        {
 -          System.err.println("|" + gcol.nextToken());
 -        }
 -        System.err.println("\n");
 -      }
 -    }
 -    return colour;
 -  }
 -
 -  /**
     * clear any temporary handles used to speed up ID matching
     */
-   private void resetMatcher()
+   protected void resetMatcher()
    {
      lastmatchedAl = null;
      matcher = null;
     *          hash of feature types and colours
     * @return features file contents
     */
-   public String printJalviewFormat(SequenceI[] seqs,
-           Map<String, FeatureColourI> map)
+   public String printJalviewFormat(SequenceI[] sequences,
 -          Map<String, Object> visible)
++          Map<String, FeatureColourI> visible)
    {
-     return printJalviewFormat(seqs, map, true, true);
+     return printJalviewFormat(sequences, visible, true, true);
    }
  
    /**
     * @return features file contents
     */
    public String printJalviewFormat(SequenceI[] sequences,
-           Map<String, FeatureColourI> visible,
-           boolean visOnly, boolean nonpos)
 -          Map<String, Object> visible, boolean visOnly, boolean nonpos)
++          Map<String, FeatureColourI> visible, boolean visOnly,
++          boolean nonpos)
    {
+     StringBuilder out = new StringBuilder(256);
+     boolean featuresGen = false;
      if (visOnly && !nonpos && (visible == null || visible.size() < 1))
      {
        // no point continuing.
        // viewed features
        // TODO: decide if feature links should also be written here ?
        Iterator<String> en = visible.keySet().iterator();
 -      String featureType, color;
        while (en.hasNext())
        {
-         String featureType = en.next();
 -        featureType = en.next().toString();
 -
 -        if (visible.get(featureType) instanceof GraduatedColor)
 -        {
 -          GraduatedColor gc = (GraduatedColor) visible.get(featureType);
 -          color = (gc.isColourByLabel() ? "label|" : "")
 -                  + Format.getHexString(gc.getMinColor()) + "|"
 -                  + Format.getHexString(gc.getMaxColor())
 -                  + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
 -                  + gc.getMax() + "|";
 -          if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
 -          {
 -            if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
 -            {
 -              color += "below";
 -            }
 -            else
 -            {
 -              if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
 -              {
 -                System.err.println("WARNING: Unsupported threshold type ("
 -                        + gc.getThreshType() + ") : Assuming 'above'");
 -              }
 -              color += "above";
 -            }
 -            // add the value
 -            color += "|" + gc.getThresh();
 -          }
 -          else
 -          {
 -            color += "none";
 -          }
 -        }
 -        else if (visible.get(featureType) instanceof Color)
 -        {
 -          color = Format.getHexString((Color) visible.get(featureType));
 -        }
 -        else
 -        {
 -          // legacy support for integer objects containing colour triplet values
 -          color = Format.getHexString(new Color(Integer.parseInt(visible
 -                  .get(featureType).toString())));
 -        }
 -        out.append(featureType);
 -        out.append(TAB);
 -        out.append(color);
 -        out.append(newline);
++        String featureType = en.next().toString();
 +        FeatureColourI colour = visible.get(featureType);
 +        out.append(colour.toJalviewFormat(featureType)).append(newline);
        }
      }
++
      // Work out which groups are both present and visible
-     Vector groups = new Vector();
+     List<String> groups = new ArrayList<String>();
      int groupIndex = 0;
      boolean isnonpos = false;
  
    }
  
    /**
-    * generate a gff file for sequence features includes non-pos features by
-    * default.
+    * Parse method that is called when a GFF file is dragged to the desktop
+    */
+   @Override
+   public void parse()
+   {
+     AlignViewportI av = getViewport();
+     if (av != null)
+     {
+       if (av.getAlignment() != null)
+       {
+         dataset = av.getAlignment().getDataset();
+       }
+       if (dataset == null)
+       {
+         // working in the applet context ?
+         dataset = av.getAlignment();
+       }
+     }
+     else
+     {
+       dataset = new Alignment(new SequenceI[] {});
+     }
+     boolean parseResult = parse(dataset, null, false, true);
+     if (!parseResult)
+     {
+       // pass error up somehow
+     }
+     if (av != null)
+     {
+       // update viewport with the dataset data ?
+     }
+     else
+     {
+       setSeqs(dataset.getSequencesArray());
+     }
+   }
+   /**
+    * Implementation of unused abstract method
     * 
-    * @param seqs
-    * @param map
+    * @return error message
+    */
+   @Override
+   public String print()
+   {
+     return "Use printGffFormat() or printJalviewFormat()";
+   }
+   /**
+    * Returns features output in GFF2 format, including hidden and non-positional
+    * features
+    * 
+    * @param sequences
+    *          the sequences whose features are to be output
+    * @param visible
+    *          a map whose keys are the type names of visible features
     * @return
     */
-   public String printGFFFormat(SequenceI[] seqs,
-           Map<String, FeatureColourI> map)
+   public String printGffFormat(SequenceI[] sequences,
 -          Map<String, Object> visible)
++          Map<String, FeatureColourI> visible)
    {
-     return printGFFFormat(seqs, map, true, true);
+     return printGffFormat(sequences, visible, true, true);
    }
  
-   public String printGFFFormat(SequenceI[] seqs,
-           Map<String, FeatureColourI> map, boolean visOnly, boolean nonpos)
+   /**
+    * Returns features output in GFF2 format
+    * 
+    * @param sequences
+    *          the sequences whose features are to be output
+    * @param visible
+    *          a map whose keys are the type names of visible features
+    * @param outputVisibleOnly
+    * @param includeNonPositionalFeatures
+    * @return
+    */
+   public String printGffFormat(SequenceI[] sequences,
 -          Map<String, Object> visible, boolean outputVisibleOnly,
++          Map<String, FeatureColourI> visible, boolean outputVisibleOnly,
+           boolean includeNonPositionalFeatures)
    {
-     StringBuffer out = new StringBuffer();
-     SequenceFeature[] next;
+     StringBuilder out = new StringBuilder(256);
+     int version = gffVersion == 0 ? 2 : gffVersion;
+     out.append(String.format("%s %d\n", GFF_VERSION, version));
      String source;
      boolean isnonpos;
-     for (int i = 0; i < seqs.length; i++)
+     for (SequenceI seq : sequences)
      {
-       if (seqs[i].getSequenceFeatures() != null)
+       SequenceFeature[] features = seq.getSequenceFeatures();
+       if (features != null)
        {
-         next = seqs[i].getSequenceFeatures();
-         for (int j = 0; j < next.length; j++)
+         for (SequenceFeature sf : features)
          {
-           isnonpos = next[j].begin == 0 && next[j].end == 0;
-           if ((!nonpos && isnonpos)
-                   || (!isnonpos && visOnly && !map
-                           .containsKey(next[j].type)))
+           isnonpos = sf.begin == 0 && sf.end == 0;
+           if (!includeNonPositionalFeatures && isnonpos)
+           {
+             /*
+              * ignore non-positional features if not wanted
+              */
+             continue;
+           }
+           // TODO why the test !isnonpos here?
+           // what about not visible non-positional features?
+           if (!isnonpos && outputVisibleOnly
+                   && !visible.containsKey(sf.type))
            {
+             /*
+              * ignore not visible features if not wanted
+              */
              continue;
            }
  
index 0000000,83bb37e..ecce1a3
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,66 +1,66 @@@
+ package jalview.io;
+ import jalview.api.FeatureColourI;
 -import jalview.schemes.FeatureColourAdapter;
++import jalview.schemes.FeatureColour;
+ import jalview.schemes.FeatureSettingsAdapter;
+ import java.awt.Color;
+ public class PDBFeatureSettings extends FeatureSettingsAdapter
+ {
+   public static final String FEATURE_INSERTION = "INSERTION";
+   public static final String FEATURE_RES_NUM = "RESNUM";
+   @Override
+   public boolean isFeatureDisplayed(String type)
+   {
+     return type.equalsIgnoreCase(FEATURE_INSERTION)
+             || type.equalsIgnoreCase(FEATURE_RES_NUM);
+   }
+   @Override
+   public FeatureColourI getFeatureColour(String type)
+   {
+     if (type.equalsIgnoreCase(FEATURE_INSERTION))
+     {
 -      return new FeatureColourAdapter()
++      return new FeatureColour()
+       {
+         @Override
+         public Color getColour()
+         {
+           return Color.RED;
+         }
+       };
+     }
+     return null;
+   }
+   /**
+    * Order to render insertion after ResNum
+    */
+   @Override
+   public int compare(String feature1, String feature2)
+   {
+     if (feature1.equalsIgnoreCase(FEATURE_INSERTION))
+     {
+       return +1;
+     }
+     if (feature2.equalsIgnoreCase(FEATURE_INSERTION))
+     {
+       return -1;
+     }
+     if (feature1.equalsIgnoreCase(FEATURE_RES_NUM))
+     {
+       return +1;
+     }
+     if (feature2.equalsIgnoreCase(FEATURE_RES_NUM))
+     {
+       return -1;
+     }
+     return 0;
+   }
+ }
@@@ -20,6 -20,6 +20,7 @@@
   */
  package jalview.io.packed;
  
++import jalview.api.FeatureColourI;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.SequenceI;
  import jalview.io.NewickFile;
@@@ -55,7 -57,7 +58,7 @@@ public class JalviewDatase
    /**
     * @return the featureColours
     */
-   public Hashtable getFeatureColours()
 -  public Map<String, Object> getFeatureColours()
++  public Map<String, FeatureColourI> getFeatureColours()
    {
      return featureColours;
    }
@@@ -64,7 -66,7 +67,7 @@@
     * @param featureColours
     *          the featureColours to set
     */
-   public void setFeatureColours(Hashtable featureColours)
 -  public void setFeatureColours(Map<String, Object> featureColours)
++  public void setFeatureColours(Map<String, FeatureColourI> featureColours)
    {
      this.featureColours = featureColours;
    }
    /**
     * current set of feature colours
     */
-   Hashtable featureColours;
 -  Map<String, Object> featureColours;
++  Map<String, FeatureColourI> featureColours;
  
    /**
     * original identity of each sequence in results
      seqDetails = new Hashtable();
      al = new ArrayList<AlignmentSet>();
      parentDataset = null;
-     featureColours = new Hashtable();
 -    featureColours = new HashMap<String, Object>();
++    featureColours = new HashMap<String, FeatureColourI>();
    }
  
    /**
     * 
     * @param parentAlignment
     */
-   public JalviewDataset(AlignmentI aldataset, Hashtable fc,
 -  public JalviewDataset(AlignmentI aldataset, Map<String, Object> fc,
++  public JalviewDataset(AlignmentI aldataset,
++          Map<String, FeatureColourI> fc,
            Hashtable seqDets)
    {
+     // TODO not used - remove?
      this(aldataset, fc, seqDets, null);
    }
  
     *          (may be null) alignment to associate new annotation and trees
     *          with.
     */
-   public JalviewDataset(AlignmentI aldataset, Hashtable fc,
 -  public JalviewDataset(AlignmentI aldataset, Map<String, Object> fc,
++  public JalviewDataset(AlignmentI aldataset,
++          Map<String, FeatureColourI> fc,
            Hashtable seqDets, AlignmentI parentAlignment)
    {
      this();
@@@ -20,6 -20,6 +20,7 @@@
   */
  package jalview.io.packed;
  
++import jalview.api.FeatureColourI;
  import jalview.datamodel.AlignmentI;
  import jalview.io.AppletFormatAdapter;
  import jalview.io.FileParse;
@@@ -157,7 -157,7 +158,7 @@@ public class ParsePackedSe
          // if not, create one.
          if (context.featureColours == null)
          {
-           context.featureColours = new Hashtable();
 -          context.featureColours = new HashMap<String, Object>();
++          context.featureColours = new HashMap<String, FeatureColourI>();
          }
          try
          {
index bd58273,0000000..213868b
mode 100644,000000..100644
--- /dev/null
@@@ -1,673 -1,0 +1,672 @@@
 +package jalview.schemes;
 +
 +import jalview.api.FeatureColourI;
 +import jalview.datamodel.SequenceFeature;
 +import jalview.util.Format;
 +
 +import java.awt.Color;
 +import java.util.StringTokenizer;
 +
 +/**
 + * A class that wraps either a simple colour or a graduated colour
 + */
 +public class FeatureColour implements FeatureColourI
 +{
 +  private static final String BAR = "|";
 +
 +  final private Color colour;
 +
 +  final private Color minColour;
 +
 +  final private Color maxColour;
 +
 +  private boolean graduatedColour;
 +
 +  private boolean colourByLabel;
 +
 +  private float threshold;
 +
 +  private float base;
 +
 +  private float range;
 +
 +  private boolean belowThreshold;
 +
 +  private boolean aboveThreshold;
 +
 +  private boolean thresholdIsMinOrMax;
 +
 +  private boolean isHighToLow;
 +
 +  private boolean autoScaled;
 +
 +  final private float minRed;
 +
 +  final private float minGreen;
 +
 +  final private float minBlue;
 +
 +  final private float deltaRed;
 +
 +  final private float deltaGreen;
 +
 +  final private float deltaBlue;
 +
 +  /**
 +   * Parses a Jalview features file format colour descriptor
 +   * [label|][mincolour|maxcolour
 +   * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
 +   * <ul>
 +   * <li>red</li>
 +   * <li>a28bbb</li>
 +   * <li>25,125,213</li>
 +   * <li>label</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>a28bbb|3eb555|12.0|26.0|above|12.5</li>
 +   * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
 +   * </ul>
 +   * 
 +   * @param descriptor
 +   * @return
 +   * @throws IllegalArgumentException
 +   *           if not parseable
 +   */
 +  public static FeatureColour parseJalviewFeatureColour(String descriptor)
 +  {
 +    StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
 +    float min = Float.MIN_VALUE;
 +    float max = Float.MAX_VALUE;
 +    boolean labelColour = false;
 +
 +    String mincol = gcol.nextToken();
 +    if (mincol == "|")
 +    {
 +      throw new IllegalArgumentException(
 +              "Expected either 'label' or a colour specification in the line: "
 +                      + descriptor);
 +    }
 +    String maxcol = null;
 +    if (mincol.toLowerCase().indexOf("label") == 0)
 +    {
 +      labelColour = true;
 +      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
 +      // skip '|'
 +      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
 +    }
 +
 +    if (!labelColour && !gcol.hasMoreTokens())
 +    {
 +      /*
 +       * only a simple colour specification - parse it
 +       */
 +      Color colour = UserColourScheme.getColourFromString(descriptor);
 +      if (colour == null)
 +      {
 +        throw new IllegalArgumentException("Invalid colour descriptor: "
 +                + descriptor);
 +      }
 +      return new FeatureColour(colour);
 +    }
 +
 +    /*
-      * autoScaled == true: colours range over actual score range; autoScaled ==
-      * false ('abso'): colours range over min/max range
++     * autoScaled == true: colours range over actual score range
++     * autoScaled == false ('abso'): colours range over min/max range
 +     */
-     boolean autoScaled = false;
++    boolean autoScaled = true;
 +    String tok = null, minval, maxval;
 +    if (mincol != null)
 +    {
 +      // at least four more tokens
 +      if (mincol.equals("|"))
 +      {
 +        mincol = "";
 +      }
 +      else
 +      {
 +        gcol.nextToken(); // skip next '|'
 +      }
 +      maxcol = gcol.nextToken();
 +      if (maxcol.equals("|"))
 +      {
 +        maxcol = "";
 +      }
 +      else
 +      {
 +        gcol.nextToken(); // skip next '|'
 +      }
 +      tok = gcol.nextToken();
 +      gcol.nextToken(); // skip next '|'
-       if (tok.toLowerCase().indexOf("abso") != 0)
++      if (tok.toLowerCase().startsWith("abso"))
 +      {
-         minval = tok;
-         autoScaled = true;
++        minval = gcol.nextToken();
++        gcol.nextToken(); // skip next '|'
++        autoScaled = false;
 +      }
 +      else
 +      {
-         minval = gcol.nextToken();
-         gcol.nextToken(); // skip next '|'
++        minval = tok;
 +      }
 +      maxval = gcol.nextToken();
 +      if (gcol.hasMoreTokens())
 +      {
 +        gcol.nextToken(); // skip next '|'
 +      }
 +      try
 +      {
 +        if (minval.length() > 0)
 +        {
 +          min = new Float(minval).floatValue();
 +        }
 +      } catch (Exception e)
 +      {
 +        throw new IllegalArgumentException(
 +                "Couldn't parse the minimum value for graduated colour ("
 +                        + descriptor + ")");
 +      }
 +      try
 +      {
 +        if (maxval.length() > 0)
 +        {
 +          max = new Float(maxval).floatValue();
 +        }
 +      } catch (Exception e)
 +      {
 +        throw new IllegalArgumentException(
 +                "Couldn't parse the maximum value for graduated colour ("
 +                        + descriptor + ")");
 +      }
 +    }
 +    else
 +    {
 +      // add in some dummy min/max colours for the label-only
 +      // colourscheme.
 +      mincol = "FFFFFF";
 +      maxcol = "000000";
 +    }
 +
 +    /*
 +     * construct the FeatureColour
 +     */
 +    FeatureColour featureColour;
 +    try
 +    {
 +      featureColour = new FeatureColour(
 +              new UserColourScheme(mincol).findColour('A'),
 +              new UserColourScheme(maxcol).findColour('A'), min, max);
 +      featureColour.setColourByLabel(labelColour);
 +      featureColour.setAutoScaled(autoScaled);
 +      // 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"))
 +        {
 +          featureColour.setBelowThreshold(true);
 +        }
 +        else if (ttype.toLowerCase().startsWith("above"))
 +        {
 +          featureColour.setAboveThreshold(true);
 +        }
 +        else
 +        {
 +          if (!ttype.toLowerCase().startsWith("no"))
 +          {
 +            System.err.println("Ignoring unrecognised threshold type : "
 +                    + ttype);
 +          }
 +        }
 +      }
 +      if (featureColour.hasThreshold())
 +      {
 +        try
 +        {
 +          gcol.nextToken();
 +          tval = gcol.nextToken();
 +          featureColour.setThreshold(new Float(tval).floatValue());
 +        } catch (Exception e)
 +        {
 +          System.err.println("Couldn't parse threshold value as a float: ("
 +                  + tval + ")");
 +        }
 +      }
 +      if (gcol.hasMoreTokens())
 +      {
 +        System.err
 +                .println("Ignoring additional tokens in parameters in graduated colour specification\n");
 +        while (gcol.hasMoreTokens())
 +        {
 +          System.err.println("|" + gcol.nextToken());
 +        }
 +        System.err.println("\n");
 +      }
 +      return featureColour;
 +    } catch (Exception e)
 +    {
 +      throw new IllegalArgumentException(e.getMessage());
 +    }
 +  }
 +
 +  /**
 +   * Default constructor
 +   */
 +  public FeatureColour()
 +  {
 +    this((Color) null);
 +  }
 +
 +  /**
 +   * Constructor given a simple colour
 +   * 
 +   * @param c
 +   */
 +  public FeatureColour(Color c)
 +  {
 +    minColour = Color.WHITE;
 +    maxColour = Color.BLACK;
 +    minRed = 0f;
 +    minGreen = 0f;
 +    minBlue = 0f;
 +    deltaRed = 0f;
 +    deltaGreen = 0f;
 +    deltaBlue = 0f;
 +    colour = c;
 +  }
 +
 +  /**
 +   * Constructor given a colour range and a score range
 +   * 
 +   * @param low
 +   * @param high
 +   * @param min
 +   * @param max
 +   */
 +  public FeatureColour(Color low, Color high, float min, float max)
 +  {
 +    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;
 +    }
 +  }
 +
 +  /**
 +   * Copy constructor
 +   * 
 +   * @param fc
 +   */
 +  public FeatureColour(FeatureColour fc)
 +  {
 +    colour = fc.colour;
 +    minColour = fc.minColour;
 +    maxColour = fc.maxColour;
 +    minRed = fc.minRed;
 +    minGreen = fc.minGreen;
 +    minBlue = fc.minBlue;
 +    deltaRed = fc.deltaRed;
 +    deltaGreen = fc.deltaGreen;
 +    deltaBlue = fc.deltaBlue;
 +    base = fc.base;
 +    range = fc.range;
 +    isHighToLow = fc.isHighToLow;
 +    setAboveThreshold(fc.isAboveThreshold());
 +    setBelowThreshold(fc.isBelowThreshold());
 +    setThreshold(fc.getThreshold());
 +    setAutoScaled(fc.isAutoScaled());
 +    setColourByLabel(fc.isColourByLabel());
 +  }
 +  
 +  /**
 +   * Copy constructor with new min/max ranges
 +   * @param fc
 +   * @param min
 +   * @param max
 +   */
 +  public FeatureColour(FeatureColour fc, float min, float max)
 +  {
 +    this(fc);
 +    graduatedColour = true;
 +    updateBounds(min, max);
 +  }
 +
 +  @Override
 +  public boolean isGraduatedColour()
 +  {
 +    return graduatedColour;
 +  }
 +
 +  /**
 +   * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to
 +   * false.
 +   */
-   @Override
-   public void setGraduatedColour(boolean b)
++  void setGraduatedColour(boolean b)
 +  {
 +    graduatedColour = b;
 +    if (b)
 +    {
 +      setColourByLabel(false);
 +    }
 +  }
 +
 +  @Override
 +  public Color getColour()
 +  {
 +    return colour;
 +  }
 +
 +  @Override
 +  public Color getMinColour()
 +  {
 +    return minColour;
 +  }
 +
 +  @Override
 +  public Color getMaxColour()
 +  {
 +    return maxColour;
 +  }
 +
 +  @Override
 +  public boolean isColourByLabel()
 +  {
 +    return colourByLabel;
 +  }
 +
 +  /**
 +   * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to
 +   * false.
 +   */
 +  @Override
 +  public void setColourByLabel(boolean b)
 +  {
 +    colourByLabel = b;
 +    if (b)
 +    {
 +      setGraduatedColour(false);
 +    }
 +  }
 +  @Override
 +  public boolean isBelowThreshold()
 +  {
 +    return belowThreshold;
 +  }
 +
 +  @Override
 +  public void setBelowThreshold(boolean b)
 +  {
 +    belowThreshold = b;
 +    if (b)
 +    {
 +      setAboveThreshold(false);
 +    }
 +  }
 +
 +  @Override
 +  public boolean isAboveThreshold()
 +  {
 +    return aboveThreshold;
 +  }
 +
 +  @Override
 +  public void setAboveThreshold(boolean b)
 +  {
 +    aboveThreshold = b;
 +    if (b)
 +    {
 +      setBelowThreshold(false);
 +    }
 +  }
 +
 +  @Override
 +  public boolean isThresholdMinMax()
 +  {
 +    return thresholdIsMinOrMax;
 +  }
 +
 +  @Override
 +  public void setThresholdMinMax(boolean b)
 +  {
 +    thresholdIsMinOrMax = b;
 +  }
 +
 +  @Override
 +  public float getThreshold()
 +  {
 +    return threshold;
 +  }
 +
 +  @Override
 +  public void setThreshold(float f)
 +  {
 +    threshold = f;
 +  }
 +
 +  @Override
 +  public boolean isAutoScaled()
 +  {
 +    return autoScaled;
 +  }
 +
 +  @Override
 +  public void setAutoScaled(boolean b)
 +  {
 +    this.autoScaled = b;
 +  }
 +
 +  /**
 +   * Updates the base and range appropriately for the given minmax range
 +   * 
 +   * @param min
 +   * @param max
 +   */
 +  @Override
 +  public void updateBounds(float min, float max)
 +  {
 +    if (max < min)
 +    {
 +      base = max;
 +      range = min - max;
 +      isHighToLow = true;
 +    }
 +    else
 +    {
 +      base = min;
 +      range = max - min;
 +      isHighToLow = false;
 +    }
 +  }
 +
 +  /**
 +   * Returns the colour for the given instance of the feature. This may be a
 +   * simple colour, a colour generated from the feature description (if
 +   * isColourByLabel()), or a colour derived from the feature score (if
 +   * isGraduatedColour()).
 +   * 
 +   * @param feature
 +   * @return
 +   */
 +  @Override
 +  public Color getColor(SequenceFeature feature)
 +  {
 +    if (isColourByLabel())
 +    {
 +      return UserColourScheme
 +              .createColourFromName(feature.getDescription());
 +    }
 +
 +    if (!isGraduatedColour())
 +    {
 +      return getColour();
 +    }
 +
 +    // todo should we check for above/below threshold here?
 +    if (range == 0.0)
 +    {
 +      return getMaxColour();
 +    }
 +    float scr = feature.getScore();
 +    if (Float.isNaN(scr))
 +    {
 +      return getMinColour();
 +    }
 +    float scl = (scr - base) / range;
 +    if (isHighToLow)
 +    {
 +      scl = -scl;
 +    }
 +    if (scl < 0f)
 +    {
 +      scl = 0f;
 +    }
 +    if (scl > 1f)
 +    {
 +      scl = 1f;
 +    }
 +    return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen, minBlue + scl * deltaBlue);
 +  }
 +
 +  /**
 +   * Returns the maximum score of the graduated colour range
 +   * 
 +   * @return
 +   */
 +  @Override
 +  public float getMax()
 +  {
 +    // regenerate the original values passed in to the constructor
 +    return (isHighToLow) ? base : (base + range);
 +  }
 +
 +  /**
 +   * Returns the minimum score of the graduated colour range
 +   * 
 +   * @return
 +   */
 +  @Override
 +  public float getMin()
 +  {
 +    // regenerate the original value passed in to the constructor
 +    return (isHighToLow) ? (base + range) : base;
 +  }
 +
 +  /**
 +   * Answers true if the feature has a simple colour, or is coloured by label,
 +   * or has a graduated colour and the score of this feature instance is within
 +   * the range to render (if any), i.e. does not lie below or above any
 +   * threshold set.
 +   * 
 +   * @param feature
 +   * @return
 +   */
 +  @Override
 +  public boolean isColored(SequenceFeature feature)
 +  {
 +    if (isColourByLabel() || !isGraduatedColour())
 +    {
 +      return true;
 +    }
 +
 +    float val = feature.getScore();
 +    if (Float.isNaN(val))
 +    {
 +      return true;
 +    }
 +    if (Float.isNaN(this.threshold))
 +    {
 +      return true;
 +    }
 +
 +    if (isAboveThreshold() && val <= threshold)
 +    {
 +      return false;
 +    }
 +    if (isBelowThreshold() && val >= threshold)
 +    {
 +      return false;
 +    }
 +    return true;
 +  }
 +
 +  @Override
 +  public boolean isSimpleColour()
 +  {
 +    return (!isColourByLabel() && !isGraduatedColour());
 +  }
 +
 +  @Override
 +  public boolean hasThreshold()
 +  {
 +    return isAboveThreshold() || isBelowThreshold();
 +  }
 +
 +  @Override
 +  public String toJalviewFormat(String featureType)
 +  {
 +    String colourString = null;
 +    if (isSimpleColour())
 +    {
 +      colourString = Format.getHexString(getColour());
 +    }
 +    else
 +    {
 +      StringBuilder sb = new StringBuilder(32);
 +      if (isColourByLabel())
 +      {
 +        sb.append("label");
 +        if (hasThreshold())
 +        {
 +          sb.append(BAR).append(BAR).append(BAR);
 +        }
 +      }
 +      if (isGraduatedColour())
 +      {
 +        sb.append(Format.getHexString(getMinColour())).append(BAR);
 +        sb.append(Format.getHexString(getMaxColour())).append(BAR);
-         if (isAutoScaled())
++        if (!isAutoScaled())
 +        {
 +          sb.append("abso").append(BAR);
 +        }
 +      }
 +      if (hasThreshold() || isGraduatedColour())
 +      {
 +        sb.append(getMin()).append(BAR);
 +        sb.append(getMax()).append(BAR);
 +        if (isBelowThreshold())
 +        {
 +          sb.append("below").append(BAR).append(getThreshold());
 +        }
 +        else if (isAboveThreshold())
 +        {
 +          sb.append("above").append(BAR).append(getThreshold());
 +        }
 +        else
 +        {
 +          sb.append("none");
 +        }
 +      }
 +      colourString = sb.toString();
 +    }
 +    return String.format("%s\t%s", featureType, colourString);
 +  }
 +
 +}
diff --cc src/jalview/schemes/GraduatedColor.java
index 8a55f79,2d1c572..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,298 -1,304 +1,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.schemes;
--
- import jalview.datamodel.SequenceFeature;
- import java.awt.Color;
- /**
-  * Value and/or thresholded colour scale used for colouring by annotation and
-  * feature score
-  * 
-  * @author JimP
-  * 
-  */
- public class GraduatedColor
- {
-   int thresholdState = AnnotationColourGradient.NO_THRESHOLD; // or
-                                                               // ABOVE_THRESHOLD
-                                                               // or
-                                                               // BELOW_THRESHOLD
-   float lr, lg, lb, dr, dg, db;
-   /**
-    * linear scaling parameters, base, minimum colour threshold, range of linear
-    * scale from lower to upper
-    */
-   float base, range, thrsh;
-   /**
-    * when true, colour from u to u-d rather than u to u+d
-    */
-   boolean tolow = false;
-   /**
-    * when false, min/max range has been manually set so should not be
-    * dynamically adjusted.
-    */
-   boolean autoScale = true;
-   /**
-    * construct a graduatedColor object from simple parameters
-    * 
-    * @param low
-    * @param high
-    * @param min
-    * @param max
-    *          color low->high from min->max
-    */
-   public GraduatedColor(Color low, Color high, float min, float max)
-   {
-     thrsh = Float.NaN;
-     tolow = min >= max;
-     lr = low.getRed() / 255f;
-     lg = low.getGreen() / 255f;
-     lb = low.getBlue() / 255f;
-     dr = (high.getRed() / 255f) - lr;
-     dg = (high.getGreen() / 255f) - lg;
-     db = (high.getBlue() / 255f) - lb;
-     if (tolow)
-     {
-       base = max;
-       range = min - max;
-     }
-     else
-     {
-       base = min;
-       range = max - min;
-     }
-   }
-   public GraduatedColor(GraduatedColor oldcs)
-   {
-     lr = oldcs.lr;
-     lg = oldcs.lg;
-     lb = oldcs.lb;
-     dr = oldcs.dr;
-     dg = oldcs.dg;
-     db = oldcs.db;
-     base = oldcs.base;
-     range = oldcs.range;
-     tolow = oldcs.tolow;
-     thresholdState = oldcs.thresholdState;
-     thrsh = oldcs.thrsh;
-     autoScale = oldcs.autoScale;
-     colourByLabel = oldcs.colourByLabel;
-   }
-   /**
-    * make a new gradient from an old one with a different scale range
-    * 
-    * @param oldcs
-    * @param min
-    * @param max
-    */
-   public GraduatedColor(GraduatedColor oldcs, float min, float max)
-   {
-     this(oldcs);
-     updateBounds(min, max);
-   }
-   public Color getMinColor()
-   {
-     return new Color(lr, lg, lb);
-   }
-   public Color getMaxColor()
-   {
-     return new Color(lr + dr, lg + dg, lb + db);
-   }
-   /**
-    * 
-    * @return true if original min/max scale was from high to low
-    */
-   public boolean getTolow()
-   {
-     return tolow;
-   }
-   public void setTolow(boolean tolower)
-   {
-     tolow = tolower;
-   }
-   public boolean isColored(SequenceFeature feature)
-   {
-     float val = feature.getScore();
-     if (Float.isNaN(val))
-     {
-       return true;
-     }
-     if (this.thresholdState == AnnotationColourGradient.NO_THRESHOLD)
-     {
-       return true;
-     }
-     if (Float.isNaN(this.thrsh))
-     {
-       return true;
-     }
-     boolean rtn = thresholdState == AnnotationColourGradient.ABOVE_THRESHOLD;
-     if (val <= thrsh)
-     {
-       return !rtn; // ? !tolow : tolow;
-     }
-     else
-     {
-       return rtn; // ? tolow : !tolow;
-     }
-   }
-   /**
-    * default implementor of a getColourFromString method. TODO: abstract an
-    * interface enabling pluggable colour from string
-    */
-   private UserColourScheme ucs = null;
-   private boolean colourByLabel = false;
-   /**
-    * 
-    * @return true if colourByLabel style is set
-    */
-   public boolean isColourByLabel()
-   {
-     return colourByLabel;
-   }
-   /**
-    * @param colourByLabel
-    *          the colourByLabel to set
-    */
-   public void setColourByLabel(boolean colourByLabel)
-   {
-     this.colourByLabel = colourByLabel;
-   }
-   public Color findColor(SequenceFeature feature)
-   {
-     if (colourByLabel)
-     {
-       // TODO: allow user defined feature label colourschemes. Colour space is
-       // {type,regex,%anytype%}x{description string, regex, keyword}
-       if (ucs == null)
-       {
-         ucs = new UserColourScheme();
-       }
-       return ucs.createColourFromName(feature.getDescription());
-     }
-     if (range == 0.0)
-     {
-       return getMaxColor();
-     }
-     float scr = feature.getScore();
-     if (Float.isNaN(scr))
-     {
-       return getMinColor();
-     }
-     float scl = (scr - base) / range;
-     if (tolow)
-     {
-       scl = -scl;
-     }
-     if (scl < 0f)
-     {
-       scl = 0f;
-     }
-     if (scl > 1f)
-     {
-       scl = 1f;
-     }
-     return new Color(lr + scl * dr, lg + scl * dg, lb + scl * db);
-   }
-   public void setThresh(float value)
-   {
-     thrsh = value;
-   }
-   public float getThresh()
-   {
-     return thrsh;
-   }
-   public void setThreshType(int aboveThreshold)
-   {
-     thresholdState = aboveThreshold;
-   }
-   public int getThreshType()
-   {
-     return thresholdState;
-   }
-   public float getMax()
-   {
-     // regenerate the original values passed in to the constructor
-     return (tolow) ? base : (base + range);
-   }
-   public float getMin()
-   {
-     // regenerate the original value passed in to the constructor
-     return (tolow) ? (base + range) : base;
-   }
-   public boolean isAutoScale()
-   {
-     return autoScale;
-   }
-   public void setAutoScaled(boolean autoscale)
-   {
-     autoScale = autoscale;
-   }
-   /**
-    * update the base and range appropriatly for the given minmax range
-    * 
-    * @param a
-    *          float[] {min,max} array containing minmax range for the associated
-    *          score values
-    */
-   public void updateBounds(float min, float max)
-   {
-     if (max < min)
-     {
-       base = max;
-       range = min - max;
-       tolow = true;
-     }
-     else
-     {
-       base = min;
-       range = max - min;
-       tolow = false;
-     }
-   }
- }
 -import jalview.api.FeatureColourI;
 -import jalview.datamodel.SequenceFeature;
 -
 -import java.awt.Color;
 -
 -/**
 - * Value and/or thresholded colour scale used for colouring by annotation and
 - * feature score
 - * 
 - * @author JimP
 - * 
 - */
 -public class GraduatedColor
 -{
 -  int thresholdState = AnnotationColourGradient.NO_THRESHOLD; // or
 -                                                              // ABOVE_THRESHOLD
 -                                                              // or
 -                                                              // BELOW_THRESHOLD
 -
 -  float lr, lg, lb, dr, dg, db;
 -
 -  /**
 -   * linear scaling parameters, base, minimum colour threshold, range of linear
 -   * scale from lower to upper
 -   */
 -  float base, range, thrsh;
 -
 -  /**
 -   * when true, colour from u to u-d rather than u to u+d
 -   */
 -  boolean tolow = false;
 -
 -  /**
 -   * when false, min/max range has been manually set so should not be
 -   * dynamically adjusted.
 -   */
 -  boolean autoScale = true;
 -
 -  /**
 -   * construct a graduatedColor object from simple parameters
 -   * 
 -   * @param low
 -   * @param high
 -   * @param min
 -   * @param max
 -   *          color low->high from min->max
 -   */
 -  public GraduatedColor(Color low, Color high, float min, float max)
 -  {
 -    thrsh = Float.NaN;
 -    tolow = min >= max;
 -    lr = low.getRed() / 255f;
 -    lg = low.getGreen() / 255f;
 -    lb = low.getBlue() / 255f;
 -    dr = (high.getRed() / 255f) - lr;
 -    dg = (high.getGreen() / 255f) - lg;
 -    db = (high.getBlue() / 255f) - lb;
 -    if (tolow)
 -    {
 -      base = max;
 -      range = min - max;
 -    }
 -    else
 -    {
 -      base = min;
 -      range = max - min;
 -    }
 -  }
 -
 -  public GraduatedColor(GraduatedColor oldcs)
 -  {
 -    lr = oldcs.lr;
 -    lg = oldcs.lg;
 -    lb = oldcs.lb;
 -    dr = oldcs.dr;
 -    dg = oldcs.dg;
 -    db = oldcs.db;
 -    base = oldcs.base;
 -    range = oldcs.range;
 -    tolow = oldcs.tolow;
 -    thresholdState = oldcs.thresholdState;
 -    thrsh = oldcs.thrsh;
 -    autoScale = oldcs.autoScale;
 -    colourByLabel = oldcs.colourByLabel;
 -  }
 -
 -  /**
 -   * make a new gradient from an old one with a different scale range
 -   * 
 -   * @param oldcs
 -   * @param min
 -   * @param max
 -   */
 -  public GraduatedColor(GraduatedColor oldcs, float min, float max)
 -  {
 -    this(oldcs);
 -    updateBounds(min, max);
 -  }
 -
 -  public GraduatedColor(FeatureColourI col)
 -  {
 -    setColourByLabel(col.isColourByLabel());
 -  }
 -
 -  public Color getMinColor()
 -  {
 -    return new Color(lr, lg, lb);
 -  }
 -
 -  public Color getMaxColor()
 -  {
 -    return new Color(lr + dr, lg + dg, lb + db);
 -  }
 -
 -  /**
 -   * 
 -   * @return true if original min/max scale was from high to low
 -   */
 -  public boolean getTolow()
 -  {
 -    return tolow;
 -  }
 -
 -  public void setTolow(boolean tolower)
 -  {
 -    tolow = tolower;
 -  }
 -
 -  public boolean isColored(SequenceFeature feature)
 -  {
 -    float val = feature.getScore();
 -    if (Float.isNaN(val))
 -    {
 -      return true;
 -    }
 -    if (this.thresholdState == AnnotationColourGradient.NO_THRESHOLD)
 -    {
 -      return true;
 -    }
 -    if (Float.isNaN(this.thrsh))
 -    {
 -      return true;
 -    }
 -    boolean rtn = thresholdState == AnnotationColourGradient.ABOVE_THRESHOLD;
 -    if (val <= thrsh)
 -    {
 -      return !rtn; // ? !tolow : tolow;
 -    }
 -    else
 -    {
 -      return rtn; // ? tolow : !tolow;
 -    }
 -  }
 -
 -  /**
 -   * default implementor of a getColourFromString method. TODO: abstract an
 -   * interface enabling pluggable colour from string
 -   */
 -  private UserColourScheme ucs = null;
 -
 -  private boolean colourByLabel = false;
 -
 -  /**
 -   * 
 -   * @return true if colourByLabel style is set
 -   */
 -  public boolean isColourByLabel()
 -  {
 -    return colourByLabel;
 -  }
 -
 -  /**
 -   * @param colourByLabel
 -   *          the colourByLabel to set
 -   */
 -  public void setColourByLabel(boolean colourByLabel)
 -  {
 -    this.colourByLabel = colourByLabel;
 -  }
 -
 -  public Color findColor(SequenceFeature feature)
 -  {
 -    if (colourByLabel)
 -    {
 -      // TODO: allow user defined feature label colourschemes. Colour space is
 -      // {type,regex,%anytype%}x{description string, regex, keyword}
 -      if (ucs == null)
 -      {
 -        ucs = new UserColourScheme();
 -      }
 -      return ucs.createColourFromName(feature.getDescription());
 -    }
 -    if (range == 0.0)
 -    {
 -      return getMaxColor();
 -    }
 -    float scr = feature.getScore();
 -    if (Float.isNaN(scr))
 -    {
 -      return getMinColor();
 -    }
 -    float scl = (scr - base) / range;
 -    if (tolow)
 -    {
 -      scl = -scl;
 -    }
 -    if (scl < 0f)
 -    {
 -      scl = 0f;
 -    }
 -    if (scl > 1f)
 -    {
 -      scl = 1f;
 -    }
 -    return new Color(lr + scl * dr, lg + scl * dg, lb + scl * db);
 -  }
 -
 -  public void setThresh(float value)
 -  {
 -    thrsh = value;
 -  }
 -
 -  public float getThresh()
 -  {
 -    return thrsh;
 -  }
 -
 -  public void setThreshType(int aboveThreshold)
 -  {
 -    thresholdState = aboveThreshold;
 -  }
 -
 -  public int getThreshType()
 -  {
 -    return thresholdState;
 -  }
 -
 -  public float getMax()
 -  {
 -    // regenerate the original values passed in to the constructor
 -    return (tolow) ? base : (base + range);
 -  }
 -
 -  public float getMin()
 -  {
 -    // regenerate the original value passed in to the constructor
 -    return (tolow) ? (base + range) : base;
 -  }
 -
 -  public boolean isAutoScale()
 -  {
 -    return autoScale;
 -  }
 -
 -  public void setAutoScaled(boolean autoscale)
 -  {
 -    autoScale = autoscale;
 -  }
 -
 -  /**
 -   * update the base and range appropriatly for the given minmax range
 -   * 
 -   * @param a
 -   *          float[] {min,max} array containing minmax range for the associated
 -   *          score values
 -   */
 -  public void updateBounds(float min, float max)
 -  {
 -    if (max < min)
 -    {
 -      base = max;
 -      range = min - max;
 -      tolow = true;
 -    }
 -    else
 -    {
 -      base = min;
 -      range = max - min;
 -      tolow = false;
 -    }
 -  }
 -}
@@@ -570,11 -604,26 +571,9 @@@ public abstract class FeatureRendererMo
    }
  
    @Override
 -  public void setColour(String featureType, Object col)
 +  public void setColour(String featureType, FeatureColourI col)
    {
-     {
-       featureColours.put(featureType, col);
-     }
 -    // overwrite
 -    // Color _col = (col instanceof Color) ? ((Color) col) : (col instanceof
 -    // GraduatedColor) ? ((GraduatedColor) col).getMaxColor() : null;
 -    // Object c = featureColours.get(featureType);
 -    // if (c == null || c instanceof Color || (c instanceof GraduatedColor &&
 -    // !((GraduatedColor)c).getMaxColor().equals(_col)))
 -    if (col instanceof FeatureColourI)
 -    {
 -      if (((FeatureColourI) col).isGraduatedColour())
 -      {
 -        col = new GraduatedColor((FeatureColourI) col);
 -      }
 -      else
 -      {
 -        col = ((FeatureColourI) col).getColour();
 -      }
 -    }
 -      featureColours.put(featureType, col);
++     featureColours.put(featureType, col);
    }
  
    public void setTransparency(float value)
     *          { String(Type), Colour(Type), Boolean(Displayed) }
     * @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)
     */
-   public void setFeaturePriority(Object[][] data, boolean visibleNew)
+   public boolean setFeaturePriority(Object[][] data, boolean visibleNew)
    {
+     /*
+      * note visible feature ordering and colours before update
+      */
+     List<String> visibleFeatures = getDisplayedFeatureTypes();
 -    Map<String, Object> visibleColours = new HashMap<String, Object>(
++    Map<String, FeatureColourI> visibleColours = new HashMap<String, FeatureColourI>(
+             getFeatureColours());
      FeaturesDisplayedI av_featuresdisplayed = null;
      if (visibleNew)
      {
@@@ -21,6 -21,6 +21,7 @@@
  package jalview.ws.jws1;
  
  import jalview.analysis.AlignSeq;
++import jalview.api.FeatureColourI;
  import jalview.bin.Cache;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentView;
@@@ -170,7 -171,8 +172,8 @@@ class SeqSearchWSThread extends JWS1Thr
       * 
       * @return null or { Alignment(+features and annotation), NewickFile)}
       */
-     public Object[] getAlignment(Alignment dataset, Map featureColours)
+     public Object[] getAlignment(Alignment dataset,
 -            Map<String, Object> featureColours)
++            Map<String, FeatureColourI> featureColours)
      {
  
        if (result != null && result.isFinished())
      // NewickFile nf[] = new NewickFile[jobs.length];
      for (int j = 0; j < jobs.length; j++)
      {
-       Map featureColours = new HashMap();
 -      Map<String, Object> featureColours = new HashMap<String, Object>();
++      Map<String, FeatureColourI> featureColours = new HashMap<String, FeatureColourI>();
        Alignment al = null;
        NewickFile nf = null;
        if (jobs[j].hasResults())
  package jalview.io;
  
  import static org.testng.AssertJUnit.assertEquals;
+ import static org.testng.AssertJUnit.assertFalse;
  import static org.testng.AssertJUnit.assertNotNull;
+ import static org.testng.AssertJUnit.assertNull;
  import static org.testng.AssertJUnit.assertTrue;
  
 +import jalview.api.FeatureColourI;
+ import jalview.api.FeatureRenderer;
+ import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
+ import jalview.datamodel.SequenceDummy;
  import jalview.datamodel.SequenceFeature;
+ import jalview.datamodel.SequenceI;
  import jalview.gui.AlignFrame;
 -import jalview.schemes.AnnotationColourGradient;
 -import jalview.schemes.GraduatedColor;
  
  import java.awt.Color;
  import java.io.File;
@@@ -150,4 -128,358 +127,319 @@@ public class FeaturesFileTes
      assertEquals("netphos", sf.featureGroup);
      assertEquals("PHOSPHORYLATION (T)", sf.type);
    }
+   /**
+    * Test parsing a features file with a mix of Jalview and GFF formatted
+    * content
+    * 
+    * @throws Exception
+    */
+   @Test(groups = { "Functional" })
+   public void testParse_mixedJalviewGff() throws Exception
+   {
+     File f = new File("examples/uniref50.fa");
+     AlignmentI al = readAlignmentFile(f);
+     AlignFrame af = new AlignFrame(al, 500, 500);
 -    Map<String, Object> colours = af.getFeatureRenderer()
++    Map<String, FeatureColourI> colours = af.getFeatureRenderer()
+             .getFeatureColours();
+     // GFF2 uses space as name/value separator in column 9
+     String gffData = "METAL\tcc9900\n" + "GFF\n"
+             + "FER_CAPAA\tuniprot\tMETAL\t44\t45\t4.0\t.\t.\tNote Iron-sulfur; Note 2Fe-2S\n"
+             + "FER1_SOLLC\tuniprot\tPfam\t55\t130\t2.0\t.\t.";
+     FeaturesFile featuresFile = new FeaturesFile(gffData,
+             FormatAdapter.PASTE);
+     assertTrue("Failed to parse features file",
+             featuresFile.parse(al.getDataset(), colours, true));
+     // verify colours read or synthesized
+     colours = af.getFeatureRenderer().getFeatureColours();
+     assertEquals("1 feature group colours not found", 1, colours.size());
 -    assertEquals(colours.get("METAL"), new Color(0xcc9900));
++    assertEquals(colours.get("METAL").getColour(), new Color(0xcc9900));
+     // verify feature on FER_CAPAA
+     SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+             .getSequenceFeatures();
+     assertEquals(1, sfs.length);
+     SequenceFeature sf = sfs[0];
+     assertEquals("Iron-sulfur,2Fe-2S", sf.description);
+     assertEquals(44, sf.begin);
+     assertEquals(45, sf.end);
+     assertEquals("uniprot", sf.featureGroup);
+     assertEquals("METAL", sf.type);
+     assertEquals(4f, sf.getScore(), 0.001f);
+     // verify feature on FER1_SOLLC
+     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
+     assertEquals(1, sfs.length);
+     sf = sfs[0];
+     assertEquals("uniprot", sf.description);
+     assertEquals(55, sf.begin);
+     assertEquals(130, sf.end);
+     assertEquals("uniprot", sf.featureGroup);
+     assertEquals("Pfam", sf.type);
+     assertEquals(2f, sf.getScore(), 0.001f);
+   }
+   public static AlignmentI readAlignmentFile(File f) throws IOException
+   {
+     System.out.println("Reading file: " + f);
+     String ff = f.getPath();
+     FormatAdapter rf = new FormatAdapter();
+     AlignmentI al = rf.readFile(ff, FormatAdapter.FILE,
+             new IdentifyFile().identify(ff, FormatAdapter.FILE));
+     al.setDataset(null); // creates dataset sequences
+     assertNotNull("Couldn't read supplied alignment data.", al);
+     return al;
+   }
+   /**
 -   * Test various ways of describing a feature colour scheme
 -   * 
 -   * @throws Exception
 -   */
 -  @Test(groups = { "Functional" })
 -  public void testParseGraduatedColourScheme() throws Exception
 -  {
 -    FeaturesFile ff = new FeaturesFile();
 -
 -    // colour by label:
 -    GraduatedColor gc = ff.parseGraduatedColourScheme(
 -            "BETA-TURN-IR\t9a6a94", "label");
 -    assertTrue(gc.isColourByLabel());
 -    assertEquals(Color.white, gc.getMinColor());
 -    assertEquals(Color.black, gc.getMaxColor());
 -    assertTrue(gc.isAutoScale());
 -
 -    // using colour name, rgb, etc:
 -    String spec = "blue|255,0,255|absolute|20.0|95.0|below|66.0";
 -    gc = ff.parseGraduatedColourScheme("BETA-TURN-IR\t" + spec, spec);
 -    assertFalse(gc.isColourByLabel());
 -    assertEquals(Color.blue, gc.getMinColor());
 -    assertEquals(new Color(255, 0, 255), gc.getMaxColor());
 -    assertFalse(gc.isAutoScale());
 -    assertFalse(gc.getTolow());
 -    assertEquals(20.0f, gc.getMin(), 0.001f);
 -    assertEquals(95.0f, gc.getMax(), 0.001f);
 -    assertEquals(AnnotationColourGradient.BELOW_THRESHOLD,
 -            gc.getThreshType());
 -    assertEquals(66.0f, gc.getThresh(), 0.001f);
 -
 -    // inverse gradient high to low:
 -    spec = "blue|255,0,255|95.0|20.0|below|66.0";
 -    gc = ff.parseGraduatedColourScheme("BETA-TURN-IR\t" + spec, spec);
 -    assertTrue(gc.isAutoScale());
 -    assertTrue(gc.getTolow());
 -  }
 -
 -  /**
+    * Test parsing a features file with GFF formatted content only
+    * 
+    * @throws Exception
+    */
+   @Test(groups = { "Functional" })
+   public void testParse_pureGff3() throws Exception
+   {
+     File f = new File("examples/uniref50.fa");
+     AlignmentI al = readAlignmentFile(f);
+     AlignFrame af = new AlignFrame(al, 500, 500);
 -    Map<String, Object> colours = af.getFeatureRenderer()
++    Map<String, FeatureColourI> colours = af.getFeatureRenderer()
+             .getFeatureColours();
+     // GFF3 uses '=' separator for name/value pairs in colum 9
+     String gffData = "##gff-version 3\n"
+             + "FER_CAPAA\tuniprot\tMETAL\t39\t39\t0.0\t.\t.\t"
+             + "Note=Iron-sulfur (2Fe-2S);Note=another note;evidence=ECO:0000255|PROSITE-ProRule:PRU00465\n"
+             + "FER1_SOLLC\tuniprot\tPfam\t55\t130\t3.0\t.\t.\tID=$23";
+     FeaturesFile featuresFile = new FeaturesFile(gffData,
+             FormatAdapter.PASTE);
+     assertTrue("Failed to parse features file",
+             featuresFile.parse(al.getDataset(), colours, true));
+     // verify feature on FER_CAPAA
+     SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+             .getSequenceFeatures();
+     assertEquals(1, sfs.length);
+     SequenceFeature sf = sfs[0];
+     // description parsed from Note attribute
+     assertEquals("Iron-sulfur (2Fe-2S),another note", sf.description);
+     assertEquals(39, sf.begin);
+     assertEquals(39, sf.end);
+     assertEquals("uniprot", sf.featureGroup);
+     assertEquals("METAL", sf.type);
+     assertEquals(
+             "Note=Iron-sulfur (2Fe-2S);Note=another note;evidence=ECO:0000255|PROSITE-ProRule:PRU00465",
+             sf.getValue("ATTRIBUTES"));
+     // verify feature on FER1_SOLLC1
+     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
+     assertEquals(1, sfs.length);
+     sf = sfs[0];
+     // ID used for description if available
+     assertEquals("$23", sf.description);
+     assertEquals(55, sf.begin);
+     assertEquals(130, sf.end);
+     assertEquals("uniprot", sf.featureGroup);
+     assertEquals("Pfam", sf.type);
+     assertEquals(3f, sf.getScore(), 0.001f);
+   }
+   /**
+    * Test parsing a features file with Jalview format features (but no colour
+    * descriptors or startgroup to give the hint not to parse as GFF)
+    * 
+    * @throws Exception
+    */
+   @Test(groups = { "Functional" })
+   public void testParse_jalviewFeaturesOnly() throws Exception
+   {
+     File f = new File("examples/uniref50.fa");
+     AlignmentI al = readAlignmentFile(f);
+     AlignFrame af = new AlignFrame(al, 500, 500);
 -    Map<String, Object> colours = af.getFeatureRenderer()
++    Map<String, FeatureColourI> colours = af.getFeatureRenderer()
+             .getFeatureColours();
+     /*
+      * one feature on FER_CAPAA and one on sequence 3 (index 2) FER1_SOLLC
+      */
+     String featureData = "Iron-sulfur (2Fe-2S)\tFER_CAPAA\t-1\t39\t39\tMETAL\n"
+             + "Iron-phosphorus (2Fe-P)\tID_NOT_SPECIFIED\t2\t86\t87\tMETALLIC\n";
+     FeaturesFile featuresFile = new FeaturesFile(featureData,
+             FormatAdapter.PASTE);
+     assertTrue("Failed to parse features file",
+             featuresFile.parse(al.getDataset(), colours, true));
+     // verify FER_CAPAA feature
+     SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+             .getSequenceFeatures();
+     assertEquals(1, sfs.length);
+     SequenceFeature sf = sfs[0];
+     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
+     assertEquals(39, sf.begin);
+     assertEquals(39, sf.end);
+     assertEquals("METAL", sf.type);
+     // verify FER1_SOLLC feature
+     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
+     assertEquals(1, sfs.length);
+     sf = sfs[0];
+     assertEquals("Iron-phosphorus (2Fe-P)", sf.description);
+     assertEquals(86, sf.begin);
+     assertEquals(87, sf.end);
+     assertEquals("METALLIC", sf.type);
+   }
+   private void checkDatasetfromSimpleGff3(AlignmentI dataset)
+   {
+     assertEquals("no sequences extracted from GFF3 file", 2,
+             dataset.getHeight());
+   
+     SequenceI seq1 = dataset.findName("seq1");
+     SequenceI seq2 = dataset.findName("seq2");
+     assertNotNull(seq1);
+     assertNotNull(seq2);
+     assertFalse(
+             "Failed to replace dummy seq1 with real sequence",
+             seq1 instanceof SequenceDummy
+                     && ((SequenceDummy) seq1).isDummy());
+     assertFalse(
+             "Failed to replace dummy seq2 with real sequence",
+             seq2 instanceof SequenceDummy
+                     && ((SequenceDummy) seq2).isDummy());
+     String placeholderseq = new SequenceDummy("foo").getSequenceAsString();
+     assertFalse("dummy replacement buggy for seq1",
+             placeholderseq.equals(seq1.getSequenceAsString()));
+     assertFalse("dummy replacement buggy for seq2",
+             placeholderseq.equals(seq2.getSequenceAsString()));
+     assertNotNull("No features added to seq1", seq1.getSequenceFeatures());
+     assertEquals("Wrong number of features", 3,
+             seq1.getSequenceFeatures().length);
+     assertNull(seq2.getSequenceFeatures());
+     assertEquals(
+             "Wrong number of features",
+             0,
+             seq2.getSequenceFeatures() == null ? 0 : seq2
+                     .getSequenceFeatures().length);
+     assertTrue(
+             "Expected at least one CDNA/Protein mapping for seq1",
+             dataset.getCodonFrame(seq1) != null
+                     && dataset.getCodonFrame(seq1).size() > 0);
+   
+   }
+   @Test(groups = { "Functional" })
+   public void readGff3File() throws IOException
+   {
+     FeaturesFile gffreader = new FeaturesFile(true, simpleGffFile,
+             FormatAdapter.FILE);
+     Alignment dataset = new Alignment(gffreader.getSeqsAsArray());
+     gffreader.addProperties(dataset);
+     checkDatasetfromSimpleGff3(dataset);
+   }
+   @Test(groups = { "Functional" })
+   public void simpleGff3FileClass() throws IOException
+   {
+     AlignmentI dataset = new Alignment(new SequenceI[] {});
+     FeaturesFile ffile = new FeaturesFile(simpleGffFile,
+             FormatAdapter.FILE);
+   
+     boolean parseResult = ffile.parse(dataset, null, false, false);
+     assertTrue("return result should be true", parseResult);
+     checkDatasetfromSimpleGff3(dataset);
+   }
+   @Test(groups = { "Functional" })
+   public void simpleGff3FileLoader() throws IOException
+   {
+     AlignFrame af = new FileLoader(false).LoadFileWaitTillLoaded(
+             simpleGffFile, FormatAdapter.FILE);
+     assertTrue(
+             "Didn't read the alignment into an alignframe from Gff3 File",
+             af != null);
+     checkDatasetfromSimpleGff3(af.getViewport().getAlignment());
+   }
+   @Test(groups = { "Functional" })
+   public void simpleGff3RelaxedIdMatching() throws IOException
+   {
+     AlignmentI dataset = new Alignment(new SequenceI[] {});
+     FeaturesFile ffile = new FeaturesFile(simpleGffFile,
+             FormatAdapter.FILE);
+   
+     boolean parseResult = ffile.parse(dataset, null, false, true);
+     assertTrue("return result (relaxedID matching) should be true",
+             parseResult);
+     checkDatasetfromSimpleGff3(dataset);
+   }
+   @Test(groups = { "Functional" })
+   public void testPrintJalviewFormat() throws Exception
+   {
+     File f = new File("examples/uniref50.fa");
+     AlignmentI al = readAlignmentFile(f);
+     AlignFrame af = new AlignFrame(al, 500, 500);
 -    Map<String, Object> colours = af.getFeatureRenderer()
++    Map<String, FeatureColourI> colours = af.getFeatureRenderer()
+             .getFeatureColours();
+     String features = "METAL\tcc9900\n"
+             + "GAMMA-TURN\tred|0,255,255|20.0|95.0|below|66.0\n"
+             + "Pfam\tred\n"
+             + "STARTGROUP\tuniprot\n"
+             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\n"
+             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\n"
+             + "<html>Pfam domain<a href=\"http://pfam.sanger.ac.uk/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\n"
+             + "ENDGROUP\tuniprot\n";
+     FeaturesFile featuresFile = new FeaturesFile(features,
+             FormatAdapter.PASTE);
+     featuresFile.parse(al.getDataset(), colours, false);
+     /*
+      * first with no features displayed
+      */
+     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
 -    Map<String, Object> visible = fr
++    Map<String, FeatureColourI> visible = fr
+             .getDisplayedFeatureCols();
+     String exported = featuresFile.printJalviewFormat(
+             al.getSequencesArray(), visible);
+     String expected = "No Features Visible";
+     assertEquals(expected, exported);
+     /*
+      * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
+      */
+     fr.setVisible("METAL");
+     fr.setVisible("GAMMA-TURN");
+     visible = fr.getDisplayedFeatureCols();
+     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
+             visible);
+     expected = "METAL\tcc9900\n"
+             + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
+             + "\nSTARTGROUP\tuniprot\n"
+             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
+             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
+             + "ENDGROUP\tuniprot\n";
+     assertEquals(expected, exported);
+     /*
+      * now set Pfam visible
+      */
+     fr.setVisible("Pfam");
+     visible = fr.getDisplayedFeatureCols();
+     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
+             visible);
+     /*
+      * note the order of feature types is uncontrolled - derives from
+      * FeaturesDisplayed.featuresDisplayed which is a HashSet
+      */
+     expected = "METAL\tcc9900\n"
+             + "Pfam\tff0000\n"
+             + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
+             + "\nSTARTGROUP\tuniprot\n"
+             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
+             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
+             + "<html>Pfam domain<a href=\"http://pfam.sanger.ac.uk/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n"
+             + "ENDGROUP\tuniprot\n";
+     assertEquals(expected, exported);
+   }
  }
index 483ea5d,0000000..9b1bd73
mode 100644,000000..100644
--- /dev/null
@@@ -1,300 -1,0 +1,301 @@@
 +package jalview.schemes;
 +
 +import static org.testng.AssertJUnit.assertEquals;
 +import static org.testng.AssertJUnit.assertFalse;
 +import static org.testng.AssertJUnit.assertTrue;
 +import static org.testng.AssertJUnit.fail;
 +
 +import jalview.datamodel.SequenceFeature;
 +import jalview.util.Format;
 +
 +import java.awt.Color;
 +
 +import org.testng.annotations.Test;
 +
 +public class FeatureColourTest
 +{
 +  @Test(groups = { "Functional" })
 +  public void testIsColored_simpleColour()
 +  {
 +    FeatureColour fc = new FeatureColour(Color.RED);
 +    assertTrue(fc.isColored(new SequenceFeature()));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testIsColored_colourByLabel()
 +  {
 +    FeatureColour fc = new FeatureColour();
 +    fc.setColourByLabel(true);
 +    assertTrue(fc.isColored(new SequenceFeature()));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testIsColored_aboveThreshold()
 +  {
 +    // graduated colour range from score 20 to 100
 +    FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 20f,
 +            100f);
 +
 +    // score 0 is adjusted to bottom of range
 +    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 0f,
 +            null);
 +    assertTrue(fc.isColored(sf));
 +    assertEquals(Color.WHITE, fc.getColor(sf));
 +
 +    // score 120 is adjusted to top of range
 +    sf.setScore(120f);
 +    assertEquals(Color.BLACK, fc.getColor(sf));
 +
 +    // value below threshold is still rendered
 +    // setting threshold has no effect yet...
 +    fc.setThreshold(60f);
 +    sf.setScore(36f);
 +    assertTrue(fc.isColored(sf));
 +    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
 +
 +    // now apply threshold:
 +    fc.setAboveThreshold(true);
 +    assertFalse(fc.isColored(sf));
 +    // colour is still returned though ?!?
 +    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
 +
 +    sf.setScore(84); // above threshold now
 +    assertTrue(fc.isColored(sf));
 +    assertEquals(new Color(51, 51, 51), fc.getColor(sf));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testGetColor_simpleColour()
 +  {
 +    FeatureColour fc = new FeatureColour(Color.RED);
 +    assertEquals(Color.RED, fc.getColor(new SequenceFeature()));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testGetColor_colourByLabel()
 +  {
 +    FeatureColour fc = new FeatureColour();
 +    fc.setColourByLabel(true);
 +    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
 +            null);
 +    Color expected = UserColourScheme.createColourFromName("desc");
 +    assertEquals(expected, fc.getColor(sf));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testGetColor_Graduated()
 +  {
 +    // 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,
 +            null);
 +    // the colour gradient is computed in float values from 0-1 (where 1 == 255)
 +    float red = 128 / 255f + 3 / 4f * (255 - 128) / 255f;
 +    float green = 128 / 255f + 3 / 4f * (0 - 128) / 255f;
 +    float blue = 128 / 255f + 3 / 4f * (0 - 128) / 255f;
 +    Color expected = new Color(red, green, blue);
 +    assertEquals(expected, fc.getColor(sf));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testGetColor_belowThreshold()
 +  {
 +    // gradient from [50, 150] from WHITE(255, 255, 255) to BLACK(0, 0, 0)
 +    FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 50f,
 +            150f);
 +    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 70f,
 +            null);
 +    fc.setThreshold(100f); // ignore for now
 +    assertTrue(fc.isColored(sf));
 +    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
 +
 +    fc.setAboveThreshold(true); // feature lies below threshold
 +    assertFalse(fc.isColored(sf));
 +    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
 +  }
 +
 +  /**
 +   * Test output of feature colours to Jalview features file format
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testToJalviewFormat()
 +  {
 +    /*
 +     * plain colour - to RGB hex code
 +     */
 +    FeatureColour fc = new FeatureColour(Color.RED);
 +    String redHex = Format.getHexString(Color.RED);
 +    String hexColour = redHex;
 +    assertEquals("domain\t" + hexColour, fc.toJalviewFormat("domain"));
 +    
 +    /*
 +     * colour by label (no threshold)
 +     */
 +    fc = new FeatureColour();
 +    fc.setColourByLabel(true);
 +    assertEquals("domain\tlabel", 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.setAutoScaled(true);
 +    assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 +
 +    /*
 +     * colour by label (above threshold) (min/max values are output though not
 +     * used by this scheme)
 +     */
 +    fc.setAutoScaled(false);
 +    fc.setThreshold(12.5f);
 +    fc.setAboveThreshold(true);
 +    assertEquals("domain\tlabel|||0.0|0.0|above|12.5",
 +            fc.toJalviewFormat("domain"));
 +
 +    /*
 +     * colour by label (below threshold)
 +     */
 +    fc.setBelowThreshold(true);
 +    assertEquals("domain\tlabel|||0.0|0.0|below|12.5",
 +            fc.toJalviewFormat("domain"));
 +
 +    /*
 +     * graduated colour, no threshold
 +     */
 +    fc = new FeatureColour(Color.GREEN, Color.RED, 12f, 25f);
 +    String greenHex = Format.getHexString(Color.GREEN);
-     String expected = String.format("domain\t%s|%s|12.0|25.0|none",
++    String expected = String.format("domain\t%s|%s|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|abso|12.0|25.0|none", greenHex,
++    expected = String.format("domain\t%s|%s|12.0|25.0|none", greenHex,
 +            redHex);
 +    assertEquals(expected, fc.toJalviewFormat("domain"));
 +
 +    /*
 +     * graduated colour below threshold
 +     */
 +    fc.setThreshold(12.5f);
 +    fc.setBelowThreshold(true);
-     expected = String.format("domain\t%s|%s|abso|12.0|25.0|below|12.5",
++    expected = String.format("domain\t%s|%s|12.0|25.0|below|12.5",
 +            greenHex, redHex);
 +    assertEquals(expected, fc.toJalviewFormat("domain"));
 +
 +    /*
 +     * graduated colour 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",
 +            greenHex, redHex);
 +    assertEquals(expected, fc.toJalviewFormat("domain"));
 +  }
 +
 +  /**
 +   * Test parsing of feature colours from Jalview features file format
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testParseJalviewFeatureColour()
 +  {
 +    /*
 +     * simple colour by name
 +     */
 +    FeatureColour fc = FeatureColour.parseJalviewFeatureColour("red");
 +    assertTrue(fc.isSimpleColour());
 +    assertEquals(Color.RED, fc.getColour());
 +
 +    /*
 +     * simple colour by hex code
 +     */
 +    fc = FeatureColour.parseJalviewFeatureColour(Format
 +            .getHexString(Color.RED));
 +    assertTrue(fc.isSimpleColour());
 +    assertEquals(Color.RED, fc.getColour());
 +
 +    /*
 +     * simple colour by rgb triplet
 +     */
 +    fc = FeatureColour.parseJalviewFeatureColour("255,0,0");
 +    assertTrue(fc.isSimpleColour());
 +    assertEquals(Color.RED, fc.getColour());
 +
 +    /*
 +     * malformed colour
 +     */
 +    try
 +    {
 +      fc = FeatureColour.parseJalviewFeatureColour("oops");
 +      fail("expected exception");
 +    } catch (IllegalArgumentException e)
 +    {
 +      assertEquals("Invalid colour descriptor: oops", e.getMessage());
 +    }
 +
 +    /*
 +     * colour by label (no threshold)
 +     */
 +    fc = FeatureColour.parseJalviewFeatureColour("label");
 +    assertTrue(fc.isColourByLabel());
 +    assertFalse(fc.hasThreshold());
 +
 +    /*
 +     * colour by label (with threshold)
 +     */
 +    fc = FeatureColour
 +            .parseJalviewFeatureColour("label|||0.0|0.0|above|12.0");
 +    assertTrue(fc.isColourByLabel());
 +    assertTrue(fc.isAboveThreshold());
 +    assertEquals(12.0f, fc.getThreshold());
 +
 +    /*
 +     * graduated colour (by name) (no threshold)
 +     */
 +    fc = FeatureColour.parseJalviewFeatureColour("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 hex code) (above threshold)
 +     */
 +    String descriptor = String.format("%s|%s|10.0|20.0|above|15",
 +            Format.getHexString(Color.RED),
 +            Format.getHexString(Color.GREEN));
 +    fc = FeatureColour.parseJalviewFeatureColour(descriptor);
 +    assertTrue(fc.isGraduatedColour());
 +    assertTrue(fc.hasThreshold());
 +    assertTrue(fc.isAboveThreshold());
 +    assertEquals(15f, 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");
 +    fc = FeatureColour.parseJalviewFeatureColour(descriptor);
 +    assertTrue(fc.isGraduatedColour());
 +    assertFalse(fc.isAutoScaled());
 +    assertTrue(fc.hasThreshold());
 +    assertTrue(fc.isBelowThreshold());
 +    assertEquals(15f, fc.getThreshold());
 +    assertEquals(Color.RED, fc.getMinColour());
 +    assertEquals(Color.GREEN, fc.getMaxColour());
 +    assertEquals(10f, fc.getMin());
 +    assertEquals(20f, fc.getMax());
 +  }
 +}