Merge branch 'releases/Release_2_10_4_Branch' into develop
authorJim Procter <jprocter@issues.jalview.org>
Wed, 7 Mar 2018 14:40:59 +0000 (14:40 +0000)
committerJim Procter <jprocter@issues.jalview.org>
Wed, 7 Mar 2018 14:40:59 +0000 (14:40 +0000)
check carefully that JAL-2885 (configurable Ensembl/Genomes Endpoints) functionality is still present

22 files changed:
1  2 
build.xml
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Dna.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/FeatureSettings.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceI.java
src/jalview/ext/ensembl/EnsemblInfo.java
src/jalview/ext/ensembl/EnsemblLookup.java
src/jalview/ext/ensembl/EnsemblRestClient.java
src/jalview/ext/ensembl/EnsemblSequenceFetcher.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/SeqPanel.java
src/jalview/util/MappingUtils.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/PopupMenuTest.java
test/jalview/util/MappingUtilsTest.java

diff --combined build.xml
+++ b/build.xml
      <!-- Anne's version needs 1.7 - should rebuild VARNA to java 1.6 for release -->
      <property name="j2sev" value="1.7+" />
      <!-- Java Compilation settings - source and target javac version -->
 -    <property name="javac.source" value="1.7" />
 -    <property name="javac.target" value="1.7" />
 +    <property name="javac.source" value="1.8" />
 +    <property name="javac.target" value="1.8" />
  
      <!-- Permissions for running Java applets and applications. -->
      <!-- Defaults are those suitable for deploying jalview webstart www.jalview.org -->
            <offline_allowed />
          </information>
          <resources>
 -          <j2se version="1.7+" />
 +          <j2se version="1.8+" />
            <jar main="true" href="jalview.jar"/>
            <fileset dir="${packageDir}">
              <exclude name="jalview.jar" />
  
      <jnlpf toFile="${jnlpFile}" />
      <!-- add the add-modules j2se attribute for java 9 -->
 -    <replace file="${jnlpFile}" value="j2se version=&quot;1.7+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee --illegal-access=warn&quot;">
 -          <replacetoken>j2se version="1.7+"</replacetoken>
 -           
 -        </replace>
 +    <replace file="${jnlpFile}" value="j2se version=&quot;1.8+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee --illegal-access=warn&quot;">
 +          <replacetoken>j2se version="1.8+"</replacetoken>
 +    </replace>
    </target>
  
    <target name="-dofakejnlpfileassoc" depends="-generatejnlpf" if="nojnlpfileassocs">
  </target>
  
  <target name="packageApplet" depends="compileApplet, buildPropertiesFile">
-   <copy file="${resourceDir}/images/idwidth.gif" toFile="${outputDir}/images/idwidth.gif" />
    <copy file="${resourceDir}/images/link.gif" toFile="${outputDir}/images/link.gif" />
    <copy todir="${outputDir}/lang">
      <fileset dir="${resourceDir}/lang">
        <include name="MCview/**" />
        <include name="jalview/**" />
        <include name=".build_properties" />
-       <include name="images/idwidth.gif" />
        <include name="images/link.gif" />
        <include name="lang/**" />
      </fileset>
        <include name="plugin.jar" />
      </fileset>
    </path>
 -  <taskdef resource="proguard/ant/task.properties" classpath="utils/proguard.jar" />
 +  <taskdef resource="proguard/ant/task.properties" classpath="utils/proguard_5.3.3.jar" />
  
    <proguard verbose="true" >
      <injar file="in.jar" />
@@@ -29,7 -29,6 +29,7 @@@ import jalview.datamodel.Alignment
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.GeneLociI;
  import jalview.datamodel.IncompleteCodonException;
  import jalview.datamodel.Mapping;
  import jalview.datamodel.Sequence;
@@@ -37,7 -36,6 +37,7 @@@ import jalview.datamodel.SequenceFeatur
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.features.SequenceFeatures;
 +import jalview.io.gff.Gff3Helper;
  import jalview.io.gff.SequenceOntologyI;
  import jalview.schemes.ResidueProperties;
  import jalview.util.Comparison;
@@@ -107,15 -105,6 +107,15 @@@ public class AlignmentUtil
      {
        return variant == null ? null : variant.getFeatureGroup();
      }
 +
 +    /**
 +     * toString for aid in the debugger only
 +     */
 +    @Override
 +    public String toString()
 +    {
 +      return base + ":" + (variant == null ? "" : variant.getDescription());
 +    }
    }
  
    /**
     * Answers true if the mappings include one between the given (dataset)
     * sequences.
     */
 -  public static boolean mappingExists(List<AlignedCodonFrame> mappings,
 +  protected static boolean mappingExists(List<AlignedCodonFrame> mappings,
            SequenceI aaSeq, SequenceI cdnaSeq)
    {
      if (mappings != null)
      {
        String lastCodon = String.valueOf(cdnaSeqChars,
                cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase();
 -      for (String stop : ResidueProperties.STOP)
 +      for (String stop : ResidueProperties.STOP_CODONS)
        {
          if (lastCodon.equals(stop))
          {
         * allow * in protein to match untranslatable in dna
         */
        final char aaRes = aaSeqChars[aaPos];
 -      if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
 +      if ((translated == null || ResidueProperties.STOP.equals(translated))
 +              && aaRes == '*')
        {
          continue;
        }
      if (dnaPos == cdnaSeqChars.length - CODON_LENGTH)
      {
        String codon = String.valueOf(cdnaSeqChars, dnaPos, CODON_LENGTH);
 -      if ("STOP".equals(ResidueProperties.codonTranslate(codon)))
 +      if (ResidueProperties.STOP
 +              .equals(ResidueProperties.codonTranslate(codon)))
        {
          return true;
        }
        productSeqs = new HashSet<>();
        for (SequenceI seq : products)
        {
 -        productSeqs.add(seq.getDatasetSequence() == null ? seq
 -                : seq.getDatasetSequence());
 +        productSeqs.add(seq.getDatasetSequence() == null ? seq : seq
 +                .getDatasetSequence());
        }
      }
  
            /*
             * add a mapping from CDS to the (unchanged) mapped to range
             */
 -          List<int[]> cdsRange = Collections
 -                  .singletonList(new int[]
 -                  { 1, cdsSeq.getLength() });
 +          List<int[]> cdsRange = Collections.singletonList(new int[] { 1,
 +              cdsSeq.getLength() });
            MapList cdsToProteinMap = new MapList(cdsRange,
                    mapList.getToRanges(), mapList.getFromRatio(),
                    mapList.getToRatio());
             * add another mapping from original 'from' range to CDS
             */
            AlignedCodonFrame dnaToCdsMapping = new AlignedCodonFrame();
 -          MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
 +          final MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
                    cdsRange, 1, 1);
            dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeqDss,
                    dnaToCdsMap);
            }
  
            /*
 +           * transfer dna chromosomal loci (if known) to the CDS
 +           * sequence (via the mapping)
 +           */
 +          final MapList cdsToDnaMap = dnaToCdsMap.getInverse();
 +          transferGeneLoci(dnaSeq, cdsToDnaMap, cdsSeq);
 +
 +          /*
             * add DBRef with mapping from protein to CDS
             * (this enables Get Cross-References from protein alignment)
             * This is tricky because we can't have two DBRefs with the
  
            for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
            {
 -            // creates a complementary cross-reference to the source sequence's
 -            // primary reference.
 -
 -            DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(),
 -                    primRef.getSource() + ":" + primRef.getVersion(),
 -                    primRef.getAccessionId());
 -            cdsCrossRef
 -                    .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap)));
 +            /*
 +             * create a cross-reference from CDS to the source sequence's
 +             * primary reference and vice versa
 +             */
 +            String source = primRef.getSource();
 +            String version = primRef.getVersion();
 +            DBRefEntry cdsCrossRef = new DBRefEntry(source, source + ":"
 +                    + version, primRef.getAccessionId());
 +            cdsCrossRef.setMap(new Mapping(dnaDss, new MapList(cdsToDnaMap)));
              cdsSeqDss.addDBRef(cdsCrossRef);
  
 +            dnaSeq.addDBRef(new DBRefEntry(source, version, cdsSeq
 +                    .getName(), new Mapping(cdsSeqDss, dnaToCdsMap)));
 +
              // problem here is that the cross-reference is synthesized -
              // cdsSeq.getName() may be like 'CDS|dnaaccession' or
              // 'CDS|emblcdsacc'
              // assuming cds version same as dna ?!?
  
 -            DBRefEntry proteinToCdsRef = new DBRefEntry(primRef.getSource(),
 -                    primRef.getVersion(), cdsSeq.getName());
 +            DBRefEntry proteinToCdsRef = new DBRefEntry(source, version,
 +                    cdsSeq.getName());
              //
 -            proteinToCdsRef.setMap(
 -                    new Mapping(cdsSeqDss, cdsToProteinMap.getInverse()));
 +            proteinToCdsRef.setMap(new Mapping(cdsSeqDss, cdsToProteinMap
 +                    .getInverse()));
              proteinProduct.addDBRef(proteinToCdsRef);
            }
  
        }
      }
  
 -    AlignmentI cds = new Alignment(
 -            cdsSeqs.toArray(new SequenceI[cdsSeqs.size()]));
 +    AlignmentI cds = new Alignment(cdsSeqs.toArray(new SequenceI[cdsSeqs
 +            .size()]));
      cds.setDataset(dataset);
  
      return cds;
    }
  
    /**
 +   * Tries to transfer gene loci (dbref to chromosome positions) from fromSeq to
 +   * toSeq, mediated by the given mapping between the sequences
 +   * 
 +   * @param fromSeq
 +   * @param targetToFrom
 +   *          Map
 +   * @param targetSeq
 +   */
 +  protected static void transferGeneLoci(SequenceI fromSeq,
 +          MapList targetToFrom, SequenceI targetSeq)
 +  {
 +    if (targetSeq.getGeneLoci() != null)
 +    {
 +      // already have - don't override
 +      return;
 +    }
 +    GeneLociI fromLoci = fromSeq.getGeneLoci();
 +    if (fromLoci == null)
 +    {
 +      return;
 +    }
 +
 +    MapList newMap = targetToFrom.traverse(fromLoci.getMap());
 +
 +    if (newMap != null)
 +    {
 +      targetSeq.setGeneLoci(fromLoci.getSpeciesId(),
 +              fromLoci.getAssemblyId(), fromLoci.getChromosomeId(), newMap);
 +    }
 +  }
 +
 +  /**
     * A helper method that finds a CDS sequence in the alignment dataset that is
     * mapped to the given protein sequence, and either is, or has a mapping from,
     * the given dna sequence.
     * @param seqMappings
     *          the set of mappings involving dnaSeq
     * @param aMapping
-    *          an initial candidate from seqMappings
+    *          a transcript-to-peptide mapping
     * @return
     */
    static SequenceI findCdsForProtein(List<AlignedCodonFrame> mappings,
      if (mappedFromLength == dnaLength
              || mappedFromLength == dnaLength - CODON_LENGTH)
      {
-       return seqDss;
+       /*
+        * if sequence has CDS features, this is a transcript with no UTR
+        * - do not take this as the CDS sequence! (JAL-2789)
+        */
+       if (seqDss.getFeatures().getFeaturesByOntology(SequenceOntologyI.CDS)
+               .isEmpty())
+       {
+         return seqDss;
+       }
      }
  
      /*
            {
              /*
              * found a 3:1 mapping to the protein product which covers
-             * the whole dna sequence i.e. is from CDS; finally check it
-             * is from the dna start sequence
+             * the whole dna sequence i.e. is from CDS; finally check the CDS
+             * is mapped from the given dna start sequence
              */
              SequenceI cdsSeq = map.getFromSeq();
+             // todo this test is weak if seqMappings contains multiple mappings;
+             // we get away with it if transcript:cds relationship is 1:1
              List<AlignedCodonFrame> dnaToCdsMaps = MappingUtils
                      .findMappingsForSequence(cdsSeq, seqMappings);
              if (!dnaToCdsMaps.isEmpty())
    }
  
    /**
 -   * add any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
 +   * Adds any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
     * the given mapping.
     * 
     * @param cdsSeq
     * @param contig
 +   * @param proteinProduct
     * @param mapping
 -   * @return list of DBRefEntrys added.
 +   * @return list of DBRefEntrys added
     */
 -  public static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
 +  protected static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
            SequenceI contig, SequenceI proteinProduct, Mapping mapping)
    {
 -    // gather direct refs from contig congrent with mapping
 +    // gather direct refs from contig congruent with mapping
      List<DBRefEntry> direct = new ArrayList<>();
      HashSet<String> directSources = new HashSet<>();
++
      if (contig.getDBRefs() != null)
      {
        for (DBRefEntry dbr : contig.getDBRefs())
     *          subtypes in the Sequence Ontology)
     * @param omitting
     */
 -  public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
 +  protected static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
            MapList mapping, String select, String... omitting)
    {
      SequenceI copyTo = toSeq;
      int mappedDnaLength = MappingUtils.getLength(ranges);
  
      /*
-      * if not a whole number of codons, something is wrong,
-      * abort mapping
+      * if not a whole number of codons, truncate mapping
       */
-     if (mappedDnaLength % CODON_LENGTH > 0)
+     int codonRemainder = mappedDnaLength % CODON_LENGTH;
+     if (codonRemainder > 0)
      {
-       return null;
+       mappedDnaLength -= codonRemainder;
+       MappingUtils.removeEndPositions(codonRemainder, ranges);
      }
  
      int proteinLength = proteinSeq.getLength();
     * @param dnaSeq
     * @return
     */
 -  public static List<int[]> findCdsPositions(SequenceI dnaSeq)
 +  protected static List<int[]> findCdsPositions(SequenceI dnaSeq)
    {
      List<int[]> result = new ArrayList<>();
  
      {
        if (var.variant != null)
        {
 -        String alleles = (String) var.variant.getValue("alleles");
 +        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
          if (alleles != null)
          {
            for (String base : alleles.split(","))
            {
 -            String codon = base + base2 + base3;
 -            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
 +            if (!base1.equalsIgnoreCase(base))
              {
 -              count++;
 +              String codon = base.toUpperCase() + base2.toLowerCase()
 +                      + base3.toLowerCase();
 +              String canonical = base1.toUpperCase() + base2.toLowerCase()
 +                      + base3.toLowerCase();
 +              if (addPeptideVariant(peptide, peptidePos, residue, var,
 +                      codon, canonical))
 +              {
 +                count++;
 +              }
              }
            }
          }
      {
        if (var.variant != null)
        {
 -        String alleles = (String) var.variant.getValue("alleles");
 +        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
          if (alleles != null)
          {
            for (String base : alleles.split(","))
            {
 -            String codon = base1 + base + base3;
 -            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
 +            if (!base2.equalsIgnoreCase(base))
              {
 -              count++;
 +              String codon = base1.toLowerCase() + base.toUpperCase()
 +                      + base3.toLowerCase();
 +              String canonical = base1.toLowerCase() + base2.toUpperCase()
 +                      + base3.toLowerCase();
 +              if (addPeptideVariant(peptide, peptidePos, residue, var,
 +                      codon, canonical))
 +              {
 +                count++;
 +              }
              }
            }
          }
      {
        if (var.variant != null)
        {
 -        String alleles = (String) var.variant.getValue("alleles");
 +        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
          if (alleles != null)
          {
            for (String base : alleles.split(","))
            {
 -            String codon = base1 + base2 + base;
 -            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
 +            if (!base3.equalsIgnoreCase(base))
              {
 -              count++;
 +              String codon = base1.toLowerCase() + base2.toLowerCase()
 +                      + base.toUpperCase();
 +              String canonical = base1.toLowerCase() + base2.toLowerCase()
 +                      + base3.toUpperCase();
 +              if (addPeptideVariant(peptide, peptidePos, residue, var,
 +                      codon, canonical))
 +              {
 +                count++;
 +              }
              }
            }
          }
    }
  
    /**
 -   * Helper method that adds a peptide variant feature, provided the given codon
 -   * translates to a value different to the current residue (is a non-synonymous
 -   * variant). ID and clinical_significance attributes of the dna variant (if
 -   * present) are copied to the new feature.
 +   * Helper method that adds a peptide variant feature. ID and
 +   * clinical_significance attributes of the dna variant (if present) are copied
 +   * to the new feature.
     * 
     * @param peptide
     * @param peptidePos
     * @param residue
     * @param var
     * @param codon
 +   *          the variant codon e.g. aCg
 +   * @param canonical
 +   *          the 'normal' codon e.g. aTg
     * @return true if a feature was added, else false
     */
    static boolean addPeptideVariant(SequenceI peptide, int peptidePos,
 -          String residue, DnaVariant var, String codon)
 +          String residue, DnaVariant var, String codon, String canonical)
    {
      /*
       * get peptide translation of codon e.g. GAT -> D
       * e.g. multibase variants or HGMD_MUTATION etc
       * are currently ignored here
       */
 -    String trans = codon.contains("-") ? "-"
 +    String trans = codon.contains("-") ? null
              : (codon.length() > CODON_LENGTH ? null
                      : ResidueProperties.codonTranslate(codon));
 -    if (trans != null && !trans.equals(residue))
 +    if (trans == null)
 +    {
 +      return false;
 +    }
 +    String desc = canonical + "/" + codon;
 +    String featureType = "";
 +    if (trans.equals(residue))
 +    {
 +      featureType = SequenceOntologyI.SYNONYMOUS_VARIANT;
 +    }
 +    else if (ResidueProperties.STOP.equals(trans))
 +    {
 +      featureType = SequenceOntologyI.STOP_GAINED;
 +    }
 +    else
      {
        String residue3Char = StringUtils
                .toSentenceCase(ResidueProperties.aa2Triplet.get(residue));
        String trans3Char = StringUtils
                .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
 -      String desc = "p." + residue3Char + peptidePos + trans3Char;
 -      SequenceFeature sf = new SequenceFeature(
 -              SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
 -              peptidePos, var.getSource());
 -      StringBuilder attributes = new StringBuilder(32);
 -      String id = (String) var.variant.getValue(ID);
 -      if (id != null)
 -      {
 -        if (id.startsWith(SEQUENCE_VARIANT))
 -        {
 -          id = id.substring(SEQUENCE_VARIANT.length());
 -        }
 -        sf.setValue(ID, id);
 -        attributes.append(ID).append("=").append(id);
 -        // TODO handle other species variants JAL-2064
 -        StringBuilder link = new StringBuilder(32);
 -        try
 -        {
 -          link.append(desc).append(" ").append(id).append(
 -                  "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
 -                  .append(URLEncoder.encode(id, "UTF-8"));
 -          sf.addLink(link.toString());
 -        } catch (UnsupportedEncodingException e)
 -        {
 -          // as if
 -        }
 -      }
 -      String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
 -      if (clinSig != null)
 +      desc = "p." + residue3Char + peptidePos + trans3Char;
 +      featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT;
 +    }
 +    SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos,
 +            peptidePos, var.getSource());
 +
 +    StringBuilder attributes = new StringBuilder(32);
 +    String id = (String) var.variant.getValue(ID);
 +    if (id != null)
 +    {
 +      if (id.startsWith(SEQUENCE_VARIANT))
        {
 -        sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
 -        attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
 -                .append(clinSig);
 +        id = id.substring(SEQUENCE_VARIANT.length());
        }
 -      peptide.addSequenceFeature(sf);
 -      if (attributes.length() > 0)
 +      sf.setValue(ID, id);
 +      attributes.append(ID).append("=").append(id);
 +      // TODO handle other species variants JAL-2064
 +      StringBuilder link = new StringBuilder(32);
 +      try
 +      {
 +        link.append(desc).append(" ").append(id).append(
 +                "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
 +                .append(URLEncoder.encode(id, "UTF-8"));
 +        sf.addLink(link.toString());
 +      } catch (UnsupportedEncodingException e)
        {
 -        sf.setAttributes(attributes.toString());
 +        // as if
        }
 -      return true;
      }
 -    return false;
 +    String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
 +    if (clinSig != null)
 +    {
 +      sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
 +      attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
 +              .append(clinSig);
 +    }
 +    peptide.addSequenceFeature(sf);
 +    if (attributes.length() > 0)
 +    {
 +      sf.setAttributes(attributes.toString());
 +    }
 +    return true;
    }
  
    /**
     * Builds a map whose key is position in the protein sequence, and value is a
 -   * list of the base and all variants for each corresponding codon position
 +   * list of the base and all variants for each corresponding codon position.
 +   * <p>
 +   * This depends on dna variants being held as a comma-separated list as
 +   * property "alleles" on variant features.
     * 
     * @param dnaSeq
     * @param dnaToProtein
          // not handling multi-locus variant features
          continue;
        }
 +
 +      /*
 +       * ignore variant if not a SNP
 +       */
 +      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
 +      if (alls == null)
 +      {
 +        continue; // non-SNP VCF variant perhaps - can't process this
 +      }
 +
 +      String[] alleles = alls.toUpperCase().split(",");
 +      boolean isSnp = true;
 +      for (String allele : alleles)
 +      {
 +        if (allele.trim().length() > 1)
 +        {
 +          isSnp = false;
 +        }
 +      }
 +      if (!isSnp)
 +      {
 +        continue;
 +      }
 +
        int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
        if (mapsTo == null)
        {
        }
  
        /*
 -       * extract dna variants to a string array
 -       */
 -      String alls = (String) sf.getValue("alleles");
 -      if (alls == null)
 -      {
 -        continue;
 -      }
 -      String[] alleles = alls.toUpperCase().split(",");
 -      int i = 0;
 -      for (String allele : alleles)
 -      {
 -        alleles[i++] = allele.trim(); // lose any space characters "A, G"
 -      }
 -
 -      /*
         * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
         */
        int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
@@@ -44,6 -44,7 +44,7 @@@ import jalview.util.ShiftList
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Comparator;
+ import java.util.Iterator;
  import java.util.List;
  
  public class Dna
     * 'final' variables describe the inputs to the translation, which should not
     * be modified.
     */
-   final private List<SequenceI> selection;
+   private final List<SequenceI> selection;
  
-   final private String[] seqstring;
+   private final String[] seqstring;
  
-   final private int[] contigs;
+   private final Iterator<int[]> contigs;
  
-   final private char gapChar;
+   private final char gapChar;
  
-   final private AlignmentAnnotation[] annotations;
+   private final AlignmentAnnotation[] annotations;
  
-   final private int dnaWidth;
+   private final int dnaWidth;
  
-   final private AlignmentI dataset;
+   private final AlignmentI dataset;
+   private ShiftList vismapping;
+   private int[] startcontigs;
  
    /*
     * Working variables for the translation.
@@@ -91,7 -96,7 +96,7 @@@
     * @param viewport
     * @param visibleContigs
     */
-   public Dna(AlignViewportI viewport, int[] visibleContigs)
+   public Dna(AlignViewportI viewport, Iterator<int[]> visibleContigs)
    {
      this.selection = Arrays.asList(viewport.getSequenceSelection());
      this.seqstring = viewport.getViewAsString(true);
      this.annotations = viewport.getAlignment().getAlignmentAnnotation();
      this.dnaWidth = viewport.getAlignment().getWidth();
      this.dataset = viewport.getAlignment().getDataset();
+     initContigs();
+   }
+   /**
+    * Initialise contigs used as starting point for translateCodingRegion
+    */
+   private void initContigs()
+   {
+     vismapping = new ShiftList(); // map from viscontigs to seqstring
+     // intervals
+     int npos = 0;
+     int[] lastregion = null;
+     ArrayList<Integer> tempcontigs = new ArrayList<>();
+     while (contigs.hasNext())
+     {
+       int[] region = contigs.next();
+       if (lastregion == null)
+       {
+         vismapping.addShift(npos, region[0]);
+       }
+       else
+       {
+         // hidden region
+         vismapping.addShift(npos, region[0] - lastregion[1] + 1);
+       }
+       lastregion = region;
+       tempcontigs.add(region[0]);
+       tempcontigs.add(region[1]);
+     }
+     startcontigs = new int[tempcontigs.size()];
+     int i = 0;
+     for (Integer val : tempcontigs)
+     {
+       startcontigs[i] = val;
+       i++;
+     }
+     tempcontigs = null;
    }
  
    /**
            List<SequenceI> proteinSeqs)
    {
      List<int[]> skip = new ArrayList<>();
-     int skipint[] = null;
-     ShiftList vismapping = new ShiftList(); // map from viscontigs to seqstring
-     // intervals
-     int vc;
-     int[] scontigs = new int[contigs.length];
+     int[] skipint = null;
++
      int npos = 0;
-     for (vc = 0; vc < contigs.length; vc += 2)
-     {
-       if (vc == 0)
-       {
-         vismapping.addShift(npos, contigs[vc]);
-       }
-       else
-       {
-         // hidden region
-         vismapping.addShift(npos, contigs[vc] - contigs[vc - 1] + 1);
-       }
-       scontigs[vc] = contigs[vc];
-       scontigs[vc + 1] = contigs[vc + 1];
-     }
+     int vc = 0;
+     int[] scontigs = new int[startcontigs.length];
+     System.arraycopy(startcontigs, 0, scontigs, 0, startcontigs.length);
  
      // allocate a roughly sized buffer for the protein sequence
      StringBuilder protein = new StringBuilder(seqstring.length() / 2);
              skip.add(skipint);
              skipint = null;
            }
 -          if (aa.equals("STOP"))
 +          if (aa.equals(ResidueProperties.STOP))
            {
              aa = STOP_ASTERIX;
            }
    }
  
    /**
 +   * Answers the reverse complement of the input string
 +   * 
 +   * @see #getComplement(char)
 +   * @param s
 +   * @return
 +   */
 +  public static String reverseComplement(String s)
 +  {
 +    StringBuilder sb = new StringBuilder(s.length());
 +    for (int i = s.length() - 1; i >= 0; i--)
 +    {
 +      sb.append(Dna.getComplement(s.charAt(i)));
 +    }
 +    return sb.toString();
 +  }
 +
 +  /**
     * Returns dna complement (preserving case) for aAcCgGtTuU. Ambiguity codes
     * are treated as on http://reverse-complement.com/. Anything else is left
     * unchanged.
@@@ -1445,10 -1445,9 +1445,10 @@@ public class AlignFrame extends Embmenu
      FeaturesFile formatter = new FeaturesFile();
      if (format.equalsIgnoreCase("Jalview"))
      {
 -      features = formatter.printJalviewFormat(viewport.getAlignment()
 -              .getSequencesArray(), getDisplayedFeatureCols(),
 -              getDisplayedFeatureGroups(), true);
 +      features = formatter.printJalviewFormat(
 +              viewport.getAlignment().getSequencesArray(),
 +              getDisplayedFeatureCols(), null, getDisplayedFeatureGroups(),
 +              true);
      }
      else
      {
  
    static StringBuffer copiedSequences;
  
-   static Vector<int[]> copiedHiddenColumns;
+   static HiddenColumns copiedHiddenColumns;
  
    protected void copy_actionPerformed()
    {
  
      if (viewport.hasHiddenColumns() && viewport.getSelectionGroup() != null)
      {
-       copiedHiddenColumns = new Vector<>(viewport.getAlignment()
-               .getHiddenColumns().getHiddenColumnsCopy());
        int hiddenOffset = viewport.getSelectionGroup().getStartRes();
-       for (int[] region : copiedHiddenColumns)
-       {
-         region[0] = region[0] - hiddenOffset;
-         region[1] = region[1] - hiddenOffset;
-       }
+       int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
+       // create new HiddenColumns object with copy of hidden regions
+       // between startRes and endRes, offset by startRes
+       copiedHiddenColumns = new HiddenColumns(
+               viewport.getAlignment().getHiddenColumns(), hiddenOffset,
+               hiddenCutoff, hiddenOffset);
      }
      else
      {
    {
      try
      {
        if (copiedSequences == null)
        {
          return;
        }
  
-       StringTokenizer st = new StringTokenizer(copiedSequences.toString());
+       StringTokenizer st = new StringTokenizer(copiedSequences.toString(),
+               "\t");
        Vector seqs = new Vector();
        while (st.hasMoreElements())
        {
          }
          AlignFrame af = new AlignFrame(new Alignment(newSeqs),
                  viewport.applet, newtitle, false);
-         if (copiedHiddenColumns != null)
-         {
-           for (int i = 0; i < copiedHiddenColumns.size(); i++)
-           {
-             int[] region = copiedHiddenColumns.elementAt(i);
-             af.viewport.hideColumns(region[0], region[1]);
-           }
-         }
+         af.viewport.setHiddenColumns(copiedHiddenColumns);
  
          jalview.bin.JalviewLite.addFrame(af, newtitle, frameWidth,
                  frameHeight);
@@@ -25,7 -25,6 +25,7 @@@ import jalview.api.FeatureSettingsContr
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.SequenceI;
  import jalview.util.MessageManager;
 +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
  
  import java.awt.BorderLayout;
  import java.awt.Button;
@@@ -66,7 -65,7 +66,7 @@@ import java.util.Set
  
  public class FeatureSettings extends Panel
          implements ItemListener, MouseListener, MouseMotionListener,
-         ActionListener, AdjustmentListener, FeatureSettingsControllerI
+         AdjustmentListener, FeatureSettingsControllerI
  {
    FeatureRenderer fr;
  
        add(scrollPane, BorderLayout.CENTER);
      }
  
-     Button invert = new Button("Invert Selection");
-     invert.addActionListener(this);
+     Button invert = new Button(
+             MessageManager.getString("label.invert_selection"));
+     invert.addActionListener(new ActionListener()
+     {
+       @Override
+       public void actionPerformed(ActionEvent e)
+       {
+         invertSelection();
+       }
+     });
  
      Panel lowerPanel = new Panel(new GridLayout(2, 1, 5, 10));
      lowerPanel.add(invert);
      }
    }
  
-   @Override
-   public void actionPerformed(ActionEvent evt)
+   protected void invertSelection()
    {
      for (int i = 0; i < featurePanel.getComponentCount(); i++)
      {
    {
      Component[] comps = featurePanel.getComponents();
      int cSize = comps.length;
 -
 -    Object[][] tmp = new Object[cSize][3];
 -    int tmpSize = 0;
 -    for (int i = 0; i < cSize; i++)
 +    FeatureSettingsBean[] rowData = new FeatureSettingsBean[cSize];
 +    int i = 0;
 +    for (Component comp : comps)
      {
 -      MyCheckbox check = (MyCheckbox) comps[i];
 -      tmp[tmpSize][0] = check.type;
 -      tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
 -      tmp[tmpSize][2] = new Boolean(check.getState());
 -      tmpSize++;
 +      MyCheckbox check = (MyCheckbox) comp;
 +      // feature filter set to null as not (yet) offered in applet
 +      FeatureColourI colour = fr.getFeatureStyle(check.type);
 +      rowData[i] = new FeatureSettingsBean(check.type, colour, null,
 +              check.getState());
 +      i++;
      }
  
 -    Object[][] data = new Object[tmpSize][3];
 -    System.arraycopy(tmp, 0, data, 0, tmpSize);
 -
 -    fr.setFeaturePriority(data);
 +    fr.setFeaturePriority(rowData);
  
      ap.paintAlignment(updateOverview, updateOverview);
    }
@@@ -34,6 -34,7 +34,7 @@@ import java.util.Arrays
  import java.util.BitSet;
  import java.util.Collections;
  import java.util.Enumeration;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Vector;
@@@ -408,7 -409,7 +409,7 @@@ public class Sequence extends ASequenc
    {
      if (pdbIds == null)
      {
-       pdbIds = new Vector<PDBEntry>();
+       pdbIds = new Vector<>();
        pdbIds.add(entry);
        return true;
      }
    }
  
    /**
 -   * DOCUMENT ME!
 +   * Sets the sequence description, and also parses out any special formats of
 +   * interest
     * 
     * @param desc
 -   *          DOCUMENT ME!
     */
    @Override
    public void setDescription(String desc)
      this.description = desc;
    }
  
 +  @Override
 +  public void setGeneLoci(String speciesId, String assemblyId,
 +          String chromosomeId, MapList map)
 +  {
 +    addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME
 +            + ":" + chromosomeId, new Mapping(map)));
 +  }
 +
    /**
 -   * DOCUMENT ME!
 +   * Returns the gene loci mapping for the sequence (may be null)
     * 
 -   * @return DOCUMENT ME!
 +   * @return
 +   */
 +  @Override
 +  public GeneLociI getGeneLoci()
 +  {
 +    DBRefEntry[] refs = getDBRefs();
 +    if (refs != null)
 +    {
 +      for (final DBRefEntry ref : refs)
 +      {
 +        if (ref.isChromosome())
 +        {
 +          return new GeneLociI()
 +          {
 +            @Override
 +            public String getSpeciesId()
 +            {
 +              return ref.getSource();
 +            }
 +
 +            @Override
 +            public String getAssemblyId()
 +            {
 +              return ref.getVersion();
 +            }
 +
 +            @Override
 +            public String getChromosomeId()
 +            {
 +              // strip off "chromosome:" prefix to chrId
 +              return ref.getAccessionId().substring(
 +                      DBRefEntry.CHROMOSOME.length() + 1);
 +            }
 +
 +            @Override
 +            public MapList getMap()
 +            {
 +              return ref.getMap().getMap();
 +            }
 +          };
 +        }
 +      }
 +    }
 +    return null;
 +  }
 +
 +  /**
 +   * Answers the description
 +   * 
 +   * @return
     */
    @Override
    public String getDescription()
      return map;
    }
  
+   /**
+    * Build a bitset corresponding to sequence gaps
+    * 
+    * @return a BitSet where set values correspond to gaps in the sequence
+    */
+   @Override
+   public BitSet gapBitset()
+   {
+     BitSet gaps = new BitSet(sequence.length);
+     int j = 0;
+     while (j < sequence.length)
+     {
+       if (jalview.util.Comparison.isGap(sequence[j]))
+       {
+         gaps.set(j);
+       }
+       j++;
+     }
+     return gaps;
+   }
    @Override
    public int[] findPositionMap()
    {
    @Override
    public List<int[]> getInsertions()
    {
-     ArrayList<int[]> map = new ArrayList<int[]>();
+     ArrayList<int[]> map = new ArrayList<>();
      int lastj = -1, j = 0;
      int pos = start;
      int seqlen = sequence.length;
    {
      if (this.annotation == null)
      {
-       this.annotation = new Vector<AlignmentAnnotation>();
+       this.annotation = new Vector<>();
      }
      if (!this.annotation.contains(annotation))
      {
        return null;
      }
  
-     Vector<AlignmentAnnotation> subset = new Vector<AlignmentAnnotation>();
+     Vector<AlignmentAnnotation> subset = new Vector<>();
      Enumeration<AlignmentAnnotation> e = annotation.elements();
      while (e.hasMoreElements())
      {
    public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
            String label)
    {
-     List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
+     List<AlignmentAnnotation> result = new ArrayList<>();
      if (this.annotation != null)
      {
        for (AlignmentAnnotation ann : annotation)
      }
      synchronized (dbrefs)
      {
-       List<DBRefEntry> primaries = new ArrayList<DBRefEntry>();
+       List<DBRefEntry> primaries = new ArrayList<>();
        DBRefEntry[] tmp = new DBRefEntry[1];
        for (DBRefEntry ref : dbrefs)
        {
  
      return count;
    }
+   @Override
+   public String getSequenceStringFromIterator(Iterator<int[]> it)
+   {
+     StringBuilder newSequence = new StringBuilder();
+     while (it.hasNext())
+     {
+       int[] block = it.next();
+       if (it.hasNext())
+       {
+         newSequence.append(getSequence(block[0], block[1] + 1));
+       }
+       else
+       {
+         newSequence.append(getSequence(block[0], block[1]));
+       }
+     }
+     return newSequence.toString();
+   }
+   @Override
+   public int firstResidueOutsideIterator(Iterator<int[]> regions)
+   {
+     int start = 0;
+     if (!regions.hasNext())
+     {
+       return findIndex(getStart()) - 1;
+     }
+     // Simply walk along the sequence whilst watching for region
+     // boundaries
+     int hideStart = getLength();
+     int hideEnd = -1;
+     boolean foundStart = false;
+     // step through the non-gapped positions of the sequence
+     for (int i = getStart(); i <= getEnd() && (!foundStart); i++)
+     {
+       // get alignment position of this residue in the sequence
+       int p = findIndex(i) - 1;
+       // update region start/end
+       while (hideEnd < p && regions.hasNext())
+       {
+         int[] region = regions.next();
+         hideStart = region[0];
+         hideEnd = region[1];
+       }
+       if (hideEnd < p)
+       {
+         hideStart = getLength();
+       }
+       // update boundary for sequence
+       if (p < hideStart)
+       {
+         start = p;
+         foundStart = true;
+       }
+     }
+     if (foundStart)
+     {
+       return start;
+     }
+     // otherwise, sequence was completely hidden
+     return 0;
+   }
  }
  package jalview.datamodel;
  
  import jalview.datamodel.features.SequenceFeaturesI;
 +import jalview.util.MapList;
  
  import java.util.BitSet;
+ import java.util.Iterator;
  import java.util.List;
  import java.util.Vector;
  
@@@ -224,6 -224,13 +225,13 @@@ public interface SequenceI extends ASeq
    public int[] gapMap();
  
    /**
+    * Build a bitset corresponding to sequence gaps
+    * 
+    * @return a BitSet where set values correspond to gaps in the sequence
+    */
+   public BitSet gapBitset();
+   /**
     * Returns an int array where indices correspond to each position in sequence
     * char array and the element value gives the result of findPosition for that
     * index in the sequence.
    public int replace(char c1, char c2);
  
    /**
 +   * Answers the GeneLociI, or null if not known
 +   * 
 +   * @return
 +   */
 +  GeneLociI getGeneLoci();
 +
 +  /**
 +   * Sets the mapping to gene loci for the sequence
 +   * 
 +   * @param speciesId
 +   * @param assemblyId
 +   * @param chromosomeId
 +   * @param map
 +   */
 +  void setGeneLoci(String speciesId, String assemblyId,
 +          String chromosomeId, MapList map);
++
++
++  /**
+    * Returns the sequence string constructed from the substrings of a sequence
+    * defined by the int[] ranges provided by an iterator. E.g. the iterator
+    * could iterate over all visible regions of the alignment
+    * 
+    * @param it
+    *          the iterator to use
+    * @return a String corresponding to the sequence
+    */
+   public String getSequenceStringFromIterator(Iterator<int[]> it);
+   /**
+    * Locate the first position in this sequence which is not contained in an
+    * iterator region. If no such position exists, return 0
+    * 
+    * @param it
+    *          iterator over regions
+    * @return first residue not contained in regions
+    */
+   public int firstResidueOutsideIterator(Iterator<int[]> it);
  }
 -/*
 - * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
 - * Copyright (C) $$Year-Rel$$ The Jalview Authors
 - * 
 - * This file is part of Jalview.
 - * 
 - * Jalview is free software: you can redistribute it and/or
 - * modify it under the terms of the GNU General Public License 
 - * as published by the Free Software Foundation, either version 3
 - * of the License, or (at your option) any later version.
 - *  
 - * Jalview is distributed in the hope that it will be useful, but 
 - * WITHOUT ANY WARRANTY; without even the implied warranty 
 - * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
 - * PURPOSE.  See the GNU General Public License for more details.
 - * 
 - * You should have received a copy of the GNU General Public License
 - * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 - * The Jalview Authors are detailed in the 'AUTHORS' file.
 - */
  package jalview.ext.ensembl;
  
 -/**
 - * A data class to model the data and rest version of one Ensembl domain,
 - * currently for rest.ensembl.org and rest.ensemblgenomes.org
 - * 
 - * @author gmcarstairs
 - */
 -class EnsemblInfo
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.DBRefSource;
 +
 +import java.io.BufferedReader;
 +import java.io.IOException;
 +import java.net.MalformedURLException;
 +import java.net.URL;
 +import java.util.HashMap;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.Set;
 +
 +import org.json.simple.JSONArray;
 +import org.json.simple.parser.JSONParser;
 +import org.json.simple.parser.ParseException;
 +
 +public class EnsemblInfo extends EnsemblRestClient
  {
 -  /*
 -   * The http domain this object is holding data values for
 -   */
 -  String domain;
  
    /*
 -   * The latest version Jalview has tested for, e.g. "4.5"; a minor version change should be
 -   * ok, a major version change may break stuff 
 +   * cached results of REST /info/divisions service, currently
 +   * <pre>
 +   * { 
 +   *  { "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
 +   *    "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
 +   *    "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
 +   *    "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
 +   *    "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
 +   *    "ENSEMBL", "http://rest.ensembl.org" }
 +   *  }
 +   * </pre>
 +   * The values for EnsemblGenomes are retrieved by a REST call, that for
 +   * Ensembl is added programmatically for convenience of lookup
     */
 -  String expectedRestVersion;
 +  private static Map<String, String> divisions;
  
 -  /*
 -   * Major / minor / point version e.g. "4.5.1"
 -   * @see http://rest.ensembl.org/info/rest/?content-type=application/json
 -   */
 -  String restVersion;
 +  @Override
 +  public String getDbName()
 +  {
 +    return "ENSEMBL";
 +  }
  
 -  /*
 -   * data version
 -   * @see http://rest.ensembl.org/info/data/?content-type=application/json
 -   */
 -  String dataVersion;
 +  @Override
 +  public AlignmentI getSequenceRecords(String queries) throws Exception
 +  {
 +    return null;
 +  }
  
 -  /*
 -   * true when http://rest.ensembl.org/info/ping/?content-type=application/json
 -   * returns response code 200 and not {"error":"Database is unavailable"}
 +  @Override
 +  protected URL getUrl(List<String> ids) throws MalformedURLException
 +  {
 +    return null;
 +  }
 +
 +  @Override
 +  protected boolean useGetRequest()
 +  {
 +    return true;
 +  }
 +
 +  @Override
 +  protected String getRequestMimeType(boolean multipleIds)
 +  {
 +    return "application/json";
 +  }
 +
 +  @Override
 +  protected String getResponseMimeType()
 +  {
 +    return "application/json";
 +  }
 +
 +  /**
 +   * Answers the domain (http://rest.ensembl.org or
 +   * http://rest.ensemblgenomes.org) for the given division, or null if not
 +   * recognised by Ensembl.
 +   * 
 +   * @param division
 +   * @return
     */
 -  boolean restAvailable;
 +  public String getDomain(String division)
 +  {
 +    if (divisions == null)
 +    {
 +      fetchDivisions();
 +    }
 +    return divisions.get(division.toUpperCase());
 +  }
  
 -  /*
 -   * absolute time when availability was last checked
 +  /**
 +   * On first request only, populate the lookup map by fetching the list of
 +   * divisions known to EnsemblGenomes.
     */
 -  long lastAvailableCheckTime;
 +  void fetchDivisions()
 +  {
 +    divisions = new HashMap<>();
  
 -  /*
 -   * absolute time when version numbers were last checked
 +    /*
 +     * for convenience, pre-fill ensembl.org as the domain for "ENSEMBL"
 +     */
-     divisions.put(DBRefSource.ENSEMBL.toUpperCase(), ENSEMBL_REST);
++    divisions.put(DBRefSource.ENSEMBL.toUpperCase(), ensemblDomain);
 +
 +    BufferedReader br = null;
 +    try
 +    {
-       URL url = getDivisionsUrl(ENSEMBL_GENOMES_REST);
++      URL url = getDivisionsUrl(ensemblGenomesDomain);
 +      if (url != null)
 +      {
 +        br = getHttpResponse(url, null);
 +      }
-       parseResponse(br, ENSEMBL_GENOMES_REST);
++      parseResponse(br, ensemblGenomesDomain);
 +    } catch (IOException e)
 +    {
 +      // ignore
 +    } finally
 +    {
 +      if (br != null)
 +      {
 +        try
 +        {
 +          br.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Parses the JSON response to /info/divisions, and add each to the lookup map
 +   * 
 +   * @param br
 +   * @param domain
     */
 -  long lastVersionCheckTime;
 +  void parseResponse(BufferedReader br, String domain)
 +  {
 +    JSONParser jp = new JSONParser();
 +
 +    try
 +    {
 +      JSONArray parsed = (JSONArray) jp.parse(br);
  
 -  // flag set to true if REST major version is not the one expected
 -  boolean restMajorVersionMismatch;
 +      Iterator rvals = parsed.iterator();
 +      while (rvals.hasNext())
 +      {
 +        String division = rvals.next().toString();
 +        divisions.put(division.toUpperCase(), domain);
 +      }
 +    } catch (IOException | ParseException | NumberFormatException e)
 +    {
 +      // ignore
 +    }
 +  }
  
    /**
 -   * Constructor given expected REST version number e.g 4.5 or 3.4.3
 +   * Constructs the URL for the EnsemblGenomes /info/divisions REST service
 +   * @param domain TODO
     * 
 -   * @param restExpected
 +   * @return
 +   * @throws MalformedURLException
     */
 -  EnsemblInfo(String theDomain, String restExpected)
 +  URL getDivisionsUrl(String domain) throws MalformedURLException
    {
 -    domain = theDomain;
 -    expectedRestVersion = restExpected;
 -    lastAvailableCheckTime = -1;
 -    lastVersionCheckTime = -1;
 +    return new URL(domain
 +            + "/info/divisions?content-type=application/json");
    }
  
 +  /**
 +   * Returns the set of 'divisions' recognised by Ensembl or EnsemblGenomes
 +   * 
 +   * @return
 +   */
 +  public Set<String> getDivisions() {
 +    if (divisions == null)
 +    {
 +      fetchDivisions();
 +    }
 +
 +    return divisions.keySet();
 +  }
  }
   */
  package jalview.ext.ensembl;
  
 +import jalview.bin.Cache;
  import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.GeneLociI;
 +import jalview.util.MapList;
  
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.util.Arrays;
 +import java.util.Collections;
  import java.util.List;
  
  import org.json.simple.JSONObject;
@@@ -38,15 -34,25 +38,26 @@@ import org.json.simple.parser.JSONParse
  import org.json.simple.parser.ParseException;
  
  /**
 - * A client for the Ensembl lookup REST endpoint, used to find the gene
 - * identifier given a gene, transcript or protein identifier.
 + * A client for the Ensembl /lookup REST endpoint, used to find the gene
 + * identifier given a gene, transcript or protein identifier, or to extract the
 + * species or chromosomal coordinates from the same service response
   * 
   * @author gmcarstairs
   */
  public class EnsemblLookup extends EnsemblRestClient
  {
 -
 +  private static final String SPECIES = "species";
+   private static final String OBJECT_TYPE_TRANSLATION = "Translation";
+   private static final String PARENT = "Parent";
+   private static final String OBJECT_TYPE_TRANSCRIPT = "Transcript";
+   private static final String ID = "id";
+   private static final String OBJECT_TYPE_GENE = "Gene";
+   private static final String OBJECT_TYPE = "object_type";
+   /**
+    * keep track of last identifier retrieved to break loops
+    */
+   private String lastId;
  
    /**
     * Default constructor (to use rest.ensembl.org)
    }
  
    /**
 -   * Returns the gene id related to the given identifier, which may be for a
 -   * gene, transcript or protein
 +   * Returns the gene id related to the given identifier (which may be for a
 +   * gene, transcript or protein)
     * 
     * @param identifier
     * @return
     */
    public String getGeneId(String identifier)
    {
 -    return getGeneId(identifier, null);
 +    return parseGeneId(getResult(identifier, null));
    }
  
    /**
 -   * Calls the Ensembl lookup REST endpoint and retrieves the 'Parent' for the
 +   * Calls the Ensembl lookup REST endpoint and retrieves the 'species' for the
     * given identifier, or null if not found
     * 
     * @param identifier
 +   * @return
 +   */
 +  public String getSpecies(String identifier)
 +  {
 +    String species = null;
 +    JSONObject json = getResult(identifier, null);
 +    if (json != null)
 +    {
 +      Object o = json.get(SPECIES);
 +      if (o != null)
 +      {
 +        species = o.toString();
 +      }
 +    }
 +    return species;
 +  }
 +
 +  /**
 +   * Calls the /lookup/id rest service and returns the response as a JSONObject,
 +   * or null if any error
 +   * 
 +   * @param identifier
     * @param objectType
     *          (optional)
     * @return
     */
 -  public String getGeneId(String identifier, String objectType)
 +  protected JSONObject getResult(String identifier, String objectType)
    {
      List<String> ids = Arrays.asList(new String[] { identifier });
  
      BufferedReader br = null;
      try
      {
        URL url = getUrl(identifier, objectType);
+       if (identifier.equals(lastId))
+       {
+         System.err.println("** Ensembl lookup " + url.toString()
+                 + " looping on Parent!");
+         return null;
+       }
+       lastId = identifier;
        if (url != null)
        {
          br = getHttpResponse(url, ids);
        }
 -      return br == null ? null : parseResponse(br);
 -    } catch (IOException e)
 +      return br == null ? null : (JSONObject) (new JSONParser().parse(br));
 +    } catch (IOException | ParseException e)
      {
 -      // ignore
 +      System.err.println("Error parsing " + identifier + " lookup response "
 +              + e.getMessage());
        return null;
      } finally
      {
     * Parses the JSON response and returns the gene identifier, or null if not
     * found. If the returned object_type is Gene, returns the id, if Transcript
     * returns the Parent. If it is Translation (peptide identifier), then the
 -   * Parent is the transcript identifier, so we redo the search with this value.
 +   * Parent is the transcript identifier, so we redo the search with this value,
 +   * specifying that object_type should be Transcript.
     * 
 -   * @param br
 +   * @param jsonObject
     * @return
 -   * @throws IOException
     */
 -  protected String parseResponse(BufferedReader br) throws IOException
 +  protected String parseGeneId(JSONObject json)
    {
 +    if (json == null)
 +    {
 +      // e.g. lookup failed with 404 not found
 +      return null;
 +    }
 +
      String geneId = null;
 -    JSONParser jp = new JSONParser();
 +    String type = json.get(OBJECT_TYPE).toString();
 +    if (OBJECT_TYPE_GENE.equalsIgnoreCase(type))
 +    {
 +      // got the gene - just returns its id
 +      geneId = json.get(JSON_ID).toString();
 +    }
 +    else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type))
 +    {
 +      // got the transcript - return its (Gene) Parent
 +      geneId = json.get(PARENT).toString();
 +    }
 +    else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type))
 +    {
 +      // got the protein - look up its Parent, restricted to type Transcript
 +      String transcriptId = json.get(PARENT).toString();
 +      geneId = parseGeneId(getResult(transcriptId, OBJECT_TYPE_TRANSCRIPT));
 +    }
 +
 +    return geneId;
 +  }
 +
 +  /**
 +   * Calls the /lookup/id rest service for the given id, and if successful,
 +   * parses and returns the gene's chromosomal coordinates
 +   * 
 +   * @param geneId
 +   * @return
 +   */
 +  public GeneLociI getGeneLoci(String geneId)
 +  {
 +    return parseGeneLoci(getResult(geneId, OBJECT_TYPE_GENE));
 +  }
 +
 +  /**
 +   * Parses the /lookup/id response for species, asssembly_name,
 +   * seq_region_name, start, end and returns an object that wraps them, or null
 +   * if unsuccessful
 +   * 
 +   * @param json
 +   * @return
 +   */
 +  GeneLociI parseGeneLoci(JSONObject json)
 +  {
 +    if (json == null)
 +    {
 +      return null;
 +    }
 +
      try
      {
 -      JSONObject val = (JSONObject) jp.parse(br);
 -      String type = val.get(OBJECT_TYPE).toString();
 -      if (OBJECT_TYPE_GENE.equalsIgnoreCase(type))
 -      {
 -        // got the gene - just returns its id
 -        geneId = val.get(ID).toString();
 -      }
 -      else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type))
 -      {
 -        // got the transcript - return its (Gene) Parent
 -        geneId = val.get(PARENT).toString();
 -      }
 -      else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type))
 +      final String species = json.get("species").toString();
 +      final String assembly = json.get("assembly_name").toString();
 +      final String chromosome = json.get("seq_region_name").toString();
 +      String strand = json.get("strand").toString();
 +      int start = Integer.parseInt(json.get("start").toString());
 +      int end = Integer.parseInt(json.get("end").toString());
 +      int fromEnd = end - start + 1;
 +      boolean reverseStrand = "-1".equals(strand);
 +      int toStart = reverseStrand ? end : start;
 +      int toEnd = reverseStrand ? start : end;
 +      List<int[]> fromRange = Collections.singletonList(new int[] { 1,
 +          fromEnd });
 +      List<int[]> toRange = Collections.singletonList(new int[] { toStart,
 +          toEnd });
 +      final MapList map = new MapList(fromRange, toRange, 1, 1);
 +      return new GeneLociI()
        {
 -        // got the protein - get its Parent, restricted to type Transcript
 -        String transcriptId = val.get(PARENT).toString();
 -        geneId = getGeneId(transcriptId, OBJECT_TYPE_TRANSCRIPT);
 -      }
 -    } catch (ParseException e)
 +
 +        @Override
 +        public String getSpeciesId()
 +        {
 +          return species == null ? "" : species;
 +        }
 +
 +        @Override
 +        public String getAssemblyId()
 +        {
 +          return assembly;
 +        }
 +
 +        @Override
 +        public String getChromosomeId()
 +        {
 +          return chromosome;
 +        }
 +
 +        @Override
 +        public MapList getMap()
 +        {
 +          return map;
 +        }
 +      };
 +    } catch (NullPointerException | NumberFormatException e)
      {
 -      // ignore
 +      Cache.log.error("Error looking up gene loci: " + e.getMessage());
 +      e.printStackTrace();
      }
 -    return geneId;
 +    return null;
    }
  
  }
@@@ -72,10 -72,7 +72,10 @@@ abstract class EnsemblRestClient extend
  
    private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
  
 -  private static Map<String, EnsemblInfo> domainData = new HashMap<>();
 +  private static Map<String, EnsemblData> domainData;
 +
 +  // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
 +  private static final String PING_URL = "http://rest.ensembl.org/info/ping.json";
  
    private final static long AVAILABILITY_RETEST_INTERVAL = 10000L; // 10 seconds
  
  
    static
    {
 +    domainData = new HashMap<>();
-     domainData.put(ENSEMBL_REST,
-             new EnsemblData(ENSEMBL_REST, LATEST_ENSEMBL_REST_VERSION));
-     domainData.put(ENSEMBL_GENOMES_REST, new EnsemblData(
-             ENSEMBL_GENOMES_REST, LATEST_ENSEMBLGENOMES_REST_VERSION));
+     domainData.put(DEFAULT_ENSEMBL_BASEURL,
 -            new EnsemblInfo(DEFAULT_ENSEMBL_BASEURL, LATEST_ENSEMBL_REST_VERSION));
 -    domainData.put(DEFAULT_ENSEMBL_GENOMES_BASEURL,
 -            new EnsemblInfo(
++            new EnsemblData(DEFAULT_ENSEMBL_BASEURL, LATEST_ENSEMBL_REST_VERSION));
++    domainData.put(DEFAULT_ENSEMBL_GENOMES_BASEURL, new EnsemblData(
+             DEFAULT_ENSEMBL_GENOMES_BASEURL, LATEST_ENSEMBLGENOMES_REST_VERSION));
    }
  
    protected volatile boolean inProgress = false;
     */
    public EnsemblRestClient()
    {
-     this(ENSEMBL_REST);
+     super();
+     /*
+      * initialise domain info lazily
+      */
+     if (!domainData.containsKey(ensemblDomain))
+     {
+       domainData.put(ensemblDomain,
 -              new EnsemblInfo(ensemblDomain, LATEST_ENSEMBL_REST_VERSION));
++              new EnsemblData(ensemblDomain, LATEST_ENSEMBL_REST_VERSION));
+     }
+     if (!domainData.containsKey(ensemblGenomesDomain))
+     {
 -      domainData.put(ensemblGenomesDomain, new EnsemblInfo(
++      domainData.put(ensemblGenomesDomain, new EnsemblData(
+               ensemblGenomesDomain, LATEST_ENSEMBLGENOMES_REST_VERSION));
+     }
    }
  
    /**
    boolean checkEnsembl()
    {
      BufferedReader br = null;
+     String pingUrl = getDomain() + "/info/ping" + CONTENT_TYPE_JSON;
      try
      {
        // note this format works for both ensembl and ensemblgenomes
        // info/ping.json works for ensembl only (March 2016)
-       URL ping = new URL(getDomain() + "/info/ping" + CONTENT_TYPE_JSON);
+       URL ping = new URL(pingUrl);
  
        /*
         * expect {"ping":1} if ok
      } catch (Throwable t)
      {
        System.err.println(
-               "Error connecting to " + PING_URL + ": " + t.getMessage());
+               "Error connecting to " + pingUrl + ": " + t.getMessage());
      } finally
      {
        if (br != null)
     */
    protected boolean isEnsemblAvailable()
    {
 -    EnsemblInfo info = domainData.get(getDomain());
 +    EnsemblData info = domainData.get(getDomain());
  
      long now = System.currentTimeMillis();
  
     */
    private void checkEnsemblRestVersion()
    {
 -    EnsemblInfo info = domainData.get(getDomain());
 +    EnsemblData info = domainData.get(getDomain());
  
      JSONParser jp = new JSONParser();
      URL url = null;
@@@ -20,6 -20,7 +20,7 @@@
   */
  package jalview.ext.ensembl;
  
+ import jalview.bin.Cache;
  import jalview.datamodel.DBRefSource;
  import jalview.ws.seqfetcher.DbSourceProxyImpl;
  
@@@ -32,6 -33,16 +33,16 @@@ import com.stevesoft.pat.Regex
   */
  abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
  {
+   // domain properties lookup keys:
+   protected static final String ENSEMBL_BASEURL = "ENSEMBL_BASEURL";
+   protected static final String ENSEMBL_GENOMES_BASEURL = "ENSEMBL_GENOMES_BASEURL";
+   // domain properties default values:
+   protected static final String DEFAULT_ENSEMBL_BASEURL = "https://rest.ensembl.org";
+   protected static final String DEFAULT_ENSEMBL_GENOMES_BASEURL = "https://rest.ensemblgenomes.org";
    /*
     * accepts ENSG/T/E/P with 11 digits
     * or ENSMUSP or similar for other species
@@@ -41,9 -52,9 +52,9 @@@
            "(ENS([A-Z]{3}|)[GTEP]{1}[0-9]{11}$)" + "|"
                    + "(CCDS[0-9.]{3,}$)");
  
-   protected static final String ENSEMBL_GENOMES_REST = "http://rest.ensemblgenomes.org";
+   protected final String ensemblGenomesDomain;
  
-   protected static final String ENSEMBL_REST = "http://rest.ensembl.org";
+   protected final String ensemblDomain;
  
    protected static final String OBJECT_TYPE_TRANSLATION = "Translation";
  
@@@ -53,7 -64,7 +64,7 @@@
  
    protected static final String PARENT = "Parent";
  
 -  protected static final String ID = "id";
 +  protected static final String JSON_ID = "id";
  
    protected static final String OBJECT_TYPE = "object_type";
  
      constrained, regulatory
    }
  
-   private String domain = ENSEMBL_REST;
+   private String domain;
+   /**
+    * Constructor
+    */
+   public EnsemblSequenceFetcher()
+   {
+     /*
+      * the default domain names may be overridden in .jalview_properties;
+      * this allows an easy change from http to https in future if needed
+      */
+     ensemblDomain = Cache.getDefault(ENSEMBL_BASEURL,
+             DEFAULT_ENSEMBL_BASEURL);
+     ensemblGenomesDomain = Cache.getDefault(ENSEMBL_GENOMES_BASEURL,
+             DEFAULT_ENSEMBL_GENOMES_BASEURL);
+     domain = ensemblDomain;
+   }
  
    @Override
    public String getDbSource()
    {
      // NB ensure Uniprot xrefs are canonicalised from "Ensembl" to "ENSEMBL"
-     if (ENSEMBL_GENOMES_REST.equals(getDomain()))
+     if (ensemblGenomesDomain.equals(getDomain()))
      {
        return DBRefSource.ENSEMBLGENOMES;
      }
@@@ -81,7 -81,6 +81,7 @@@ import jalview.io.JnetAnnotationMaker
  import jalview.io.NewickFile;
  import jalview.io.ScoreMatrixFile;
  import jalview.io.TCoffeeScoreFile;
 +import jalview.io.vcf.VCFLoader;
  import jalview.jbgui.GAlignFrame;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemes;
@@@ -840,7 -839,6 +840,7 @@@ public class AlignFrame extends GAlignF
      AlignmentI al = getViewport().getAlignment();
      boolean nucleotide = al.isNucleotide();
  
 +    loadVcf.setVisible(nucleotide);
      showTranslation.setVisible(nucleotide);
      showReverse.setVisible(nucleotide);
      showReverseComplement.setVisible(nucleotide);
    @Override
    public void exportFeatures_actionPerformed(ActionEvent e)
    {
 -    new AnnotationExporter().exportFeatures(alignPanel);
 +    new AnnotationExporter(alignPanel).exportFeatures();
    }
  
    @Override
    public void exportAnnotations_actionPerformed(ActionEvent e)
    {
 -    new AnnotationExporter().exportAnnotations(alignPanel);
 +    new AnnotationExporter(alignPanel).exportAnnotations();
    }
  
    @Override
        return;
      }
  
-     ArrayList<int[]> hiddenColumns = null;
+     HiddenColumns hiddenColumns = null;
      if (viewport.hasHiddenColumns())
      {
-       hiddenColumns = new ArrayList<>();
        int hiddenOffset = viewport.getSelectionGroup().getStartRes();
        int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
-       ArrayList<int[]> hiddenRegions = viewport.getAlignment()
-               .getHiddenColumns().getHiddenColumnsCopy();
-       for (int[] region : hiddenRegions)
-       {
-         if (region[0] >= hiddenOffset && region[1] <= hiddenCutoff)
-         {
-           hiddenColumns
-                   .add(new int[]
-                   { region[0] - hiddenOffset, region[1] - hiddenOffset });
-         }
-       }
+       // create new HiddenColumns object with copy of hidden regions
+       // between startRes and endRes, offset by startRes
+       hiddenColumns = new HiddenColumns(
+               viewport.getAlignment().getHiddenColumns(), hiddenOffset,
+               hiddenCutoff, hiddenOffset);
      }
  
      Desktop.jalviewClipboard = new Object[] { seqs,
          if (Desktop.jalviewClipboard != null
                  && Desktop.jalviewClipboard[2] != null)
          {
-           List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
-           for (int[] region : hc)
-           {
-             af.viewport.hideColumns(region[0], region[1]);
-           }
+           HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+           af.viewport.setHiddenColumns(hc);
          }
  
          // >>>This is a fix for the moment, until a better solution is
        if (Desktop.jalviewClipboard != null
                && Desktop.jalviewClipboard[2] != null)
        {
-         List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
-         for (int region[] : hc)
-         {
-           af.viewport.hideColumns(region[0], region[1]);
-         }
+         HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+         af.viewport.setHiddenColumns(hc);
        }
  
        // >>>This is a fix for the moment, until a better solution is
    protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
            final String source)
    {
 -    new Thread(CrossRefAction.showProductsFor(sel, _odna, source, this))
 +    new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this))
              .start();
    }
  
              new JnetAnnotationMaker();
              JnetAnnotationMaker.add_annotation(predictions,
                      viewport.getAlignment(), 0, false);
-             SequenceI repseq = viewport.getAlignment().getSequenceAt(0);
-             viewport.getAlignment().setSeqrep(repseq);
-             HiddenColumns cs = new HiddenColumns();
-             cs.hideInsertionsFor(repseq);
-             viewport.getAlignment().setHiddenColumns(cs);
+             viewport.getAlignment().setupJPredAlignment();
              isAnnotation = true;
            }
            // else if (IdentifyFile.FeaturesFile.equals(format))
        new CalculationChooser(AlignFrame.this);
      }
    }
 +
 +  @Override
 +  protected void loadVcf_actionPerformed()
 +  {
 +    JalviewFileChooser chooser = new JalviewFileChooser(
 +            Cache.getProperty("LAST_DIRECTORY"));
 +    chooser.setFileView(new JalviewFileView());
 +    chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
 +    chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
 +
 +    int value = chooser.showOpenDialog(null);
 +
 +    if (value == JalviewFileChooser.APPROVE_OPTION)
 +    {
 +      String choice = chooser.getSelectedFile().getPath();
 +      Cache.setProperty("LAST_DIRECTORY", choice);
 +      SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
 +      new VCFLoader(choice).loadVCF(seqs, this);
 +    }
 +
 +  }
  }
  
  class PrintThread extends Thread
@@@ -25,6 -25,7 +25,7 @@@ import jalview.analysis.AlignmentUtils
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
@@@ -41,8 -42,6 +42,6 @@@ import java.awt.Font
  import java.awt.FontMetrics;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
- import java.awt.Image;
- import java.awt.MediaTracker;
  import java.awt.RenderingHints;
  import java.awt.Toolkit;
  import java.awt.datatransfer.StringSelection;
@@@ -56,6 -55,7 +55,7 @@@ import java.awt.image.BufferedImage
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
+ import java.util.Iterator;
  import java.util.regex.Pattern;
  
  import javax.swing.JCheckBoxMenuItem;
@@@ -73,9 -73,16 +73,16 @@@ import javax.swing.ToolTipManager
  public class AnnotationLabels extends JPanel
          implements MouseListener, MouseMotionListener, ActionListener
  {
-   // width in pixels within which height adjuster arrows are shown and active
+   /**
+    * width in pixels within which height adjuster arrows are shown and active
+    */
    private static final int HEIGHT_ADJUSTER_WIDTH = 50;
  
+   /**
+    * height in pixels for allowing height adjuster to be active
+    */
+   private static int HEIGHT_ADJUSTER_HEIGHT = 10;
    private static final Pattern LEFT_ANGLE_BRACKET_PATTERN = Pattern
            .compile("<");
  
    private static final String COPYCONS_SEQ = MessageManager
            .getString("label.copy_consensus_sequence");
  
-   private static Image adjusterImage;
-   private static int adjusterImageHeight;
    private final boolean debugRedraw = false;
  
    private AlignmentPanel ap;
      av = ap.av;
      ToolTipManager.sharedInstance().registerComponent(this);
  
-     if (adjusterImage == null)
-     {
-       loadAdjusterImage();
-     }
      addMouseListener(this);
      addMouseMotionListener(this);
      addMouseWheelListener(ap.getAnnotationPanel());
    }
  
    /**
-    * Loads the gif for the panel height adjustment
-    */
-   protected void loadAdjusterImage()
-   {
-     java.net.URL url = getClass().getResource("/images/idwidth.gif");
-     Image temp = null;
-     if (url != null)
-     {
-       temp = Toolkit.getDefaultToolkit().createImage(url);
-     }
-     try
-     {
-       MediaTracker mt = new MediaTracker(this);
-       mt.addImage(temp, 0);
-       mt.waitForID(0);
-     } catch (Exception ex)
-     {
-     }
-     BufferedImage bi = new BufferedImage(temp.getHeight(this),
-             temp.getWidth(this), BufferedImage.TYPE_INT_RGB);
-     Graphics2D g = (Graphics2D) bi.getGraphics();
-     g.rotate(Math.toRadians(90));
-     g.drawImage(temp, 0, -bi.getWidth(this), this);
-     adjusterImage = bi;
-     adjusterImageHeight = bi.getHeight();
-   }
-   /**
     * DOCUMENT ME!
     * 
     * @param y
      }
      else if (evt.getActionCommand().equals(OUTPUT_TEXT))
      {
 -      new AnnotationExporter().exportAnnotations(ap,
 -              new AlignmentAnnotation[]
 -              { aa[selectedRow] });
 +      new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]);
      }
      else if (evt.getActionCommand().equals(COPYCONS_SEQ))
      {
    protected void showOrHideAdjuster(MouseEvent evt)
    {
      boolean was = resizePanel;
-     resizePanel = evt.getY() < adjusterImageHeight && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
+     resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
  
      if (resizePanel != was)
      {
      Alignment ds = new Alignment(dseqs);
      if (av.hasHiddenColumns())
      {
-       omitHidden = av.getAlignment().getHiddenColumns()
-               .getVisibleSequenceStrings(0, sq.getLength(), seqs);
+       Iterator<int[]> it = av.getAlignment().getHiddenColumns()
+               .getVisContigsIterator(0, sq.getLength(), false);
+       omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
      }
  
      int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
      Toolkit.getDefaultToolkit().getSystemClipboard()
              .setContents(new StringSelection(output), Desktop.instance);
  
-     ArrayList<int[]> hiddenColumns = null;
+     HiddenColumns hiddenColumns = null;
  
      if (av.hasHiddenColumns())
      {
-       hiddenColumns = av.getAlignment().getHiddenColumns()
-               .getHiddenColumnsCopy();
+       hiddenColumns = new HiddenColumns(
+               av.getAlignment().getHiddenColumns());
      }
  
      Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
        }
      }
  
-     if (resizePanel)
-     {
-       // g.drawImage(adjusterImage, 2, 0 - getScrollOffset(), this);
-     }
-     else if (dragEvent != null && aa != null)
+     if (!resizePanel && dragEvent != null && aa != null)
      {
        g.setColor(Color.lightGray);
        g.drawString(aa[selectedRow].label, dragEvent.getX(),
@@@ -22,23 -22,19 +22,23 @@@ package jalview.gui
  
  import jalview.api.FeatureColourI;
  import jalview.api.FeatureSettingsControllerI;
 -import jalview.bin.Cache;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcherI;
 +import jalview.datamodel.features.FeatureMatcherSet;
 +import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.gui.Help.HelpId;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
 +import jalview.schemabinding.version2.Filter;
  import jalview.schemabinding.version2.JalviewUserColours;
 +import jalview.schemabinding.version2.MatcherSet;
  import jalview.schemes.FeatureColour;
 -import jalview.util.Format;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
 -import jalview.util.QuickSort;
  import jalview.viewmodel.AlignmentViewport;
 +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 +import jalview.ws.DasSequenceFeatureFetcher;
  import jalview.ws.dbsources.das.api.jalviewSourceI;
  
  import java.awt.BorderLayout;
@@@ -48,7 -44,6 +48,7 @@@ import java.awt.Dimension
  import java.awt.Font;
  import java.awt.Graphics;
  import java.awt.GridLayout;
 +import java.awt.Point;
  import java.awt.Rectangle;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
@@@ -66,8 -61,6 +66,8 @@@ import java.io.InputStreamReader
  import java.io.OutputStreamWriter;
  import java.io.PrintWriter;
  import java.util.Arrays;
 +import java.util.Comparator;
 +import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Hashtable;
  import java.util.Iterator;
@@@ -93,6 -86,7 +93,6 @@@ import javax.swing.JPanel
  import javax.swing.JPopupMenu;
  import javax.swing.JScrollPane;
  import javax.swing.JSlider;
 -import javax.swing.JTabbedPane;
  import javax.swing.JTable;
  import javax.swing.ListSelectionModel;
  import javax.swing.SwingConstants;
@@@ -102,34 -96,15 +102,34 @@@ import javax.swing.event.ChangeListener
  import javax.swing.table.AbstractTableModel;
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
 +import javax.swing.table.TableColumn;
  
  public class FeatureSettings extends JPanel
          implements FeatureSettingsControllerI
  {
 -  DasSourceBrowser dassourceBrowser;
 +  private static final String SEQUENCE_FEATURE_COLOURS = MessageManager
 +          .getString("label.sequence_feature_colours");
 +
 +  /*
 +   * column indices of fields in Feature Settings table
 +   */
 +  static final int TYPE_COLUMN = 0;
 +
 +  static final int COLOUR_COLUMN = 1;
 +
 +  static final int FILTER_COLUMN = 2;
 +
 +  static final int SHOW_COLUMN = 3;
 +
 +  private static final int COLUMN_COUNT = 4;
  
 -  jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher;
 +  private static final int MIN_WIDTH = 400;
 +
 +  private static final int MIN_HEIGHT = 400;
  
 -  JPanel settingsPane = new JPanel();
 +  DasSourceBrowser dassourceBrowser;
 +
 +  DasSequenceFeatureFetcher dasFeatureFetcher;
  
    JPanel dasSettingsPane = new JPanel();
  
  
    public final AlignFrame af;
  
 +  /*
 +   * 'original' fields hold settings to restore on Cancel
 +   */
    Object[][] originalData;
  
    private float originalTransparency;
  
 +  private Map<String, FeatureMatcherSetI> originalFilters;
 +
    final JInternalFrame frame;
  
    JScrollPane scrollPane = new JScrollPane();
  
    JSlider transparency = new JSlider();
  
 -  JPanel transPanel = new JPanel(new GridLayout(1, 2));
 -
 -  private static final int MIN_WIDTH = 400;
 -
 -  private static final int MIN_HEIGHT = 400;
 -  
 -  /**
 +  /*
     * when true, constructor is still executing - so ignore UI events
     */
    protected volatile boolean inConstruction = true;
  
 +  int selectedRow = -1;
 +
 +  JButton fetchDAS = new JButton();
 +
 +  JButton saveDAS = new JButton();
 +
 +  JButton cancelDAS = new JButton();
 +
 +  boolean resettingTable = false;
 +
 +  /*
 +   * true when Feature Settings are updating from feature renderer
 +   */
 +  private boolean handlingUpdate = false;
 +
 +  /*
 +   * holds {featureCount, totalExtent} for each feature type
 +   */
 +  Map<String, float[]> typeWidth = null;
 +
    /**
     * Constructor
     * 
     * @param af
     */
 -  public FeatureSettings(AlignFrame af)
 +  public FeatureSettings(AlignFrame alignFrame)
    {
 -    this.af = af;
 +    this.af = alignFrame;
      fr = af.getFeatureRenderer();
 -    // allow transparency to be recovered
 -    transparency.setMaximum(100
 -            - (int) ((originalTransparency = fr.getTransparency()) * 100));
 +
 +    // save transparency for restore on Cancel
 +    originalTransparency = fr.getTransparency();
 +    int originalTransparencyAsPercent = (int) (originalTransparency * 100);
 +    transparency.setMaximum(100 - originalTransparencyAsPercent);
 +
 +    originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
  
      try
      {
        @Override
        public String getToolTipText(MouseEvent e)
        {
 -        if (table.columnAtPoint(e.getPoint()) == 0)
 +        String tip = null;
 +        int column = table.columnAtPoint(e.getPoint());
 +        switch (column)
          {
 -          /*
 -           * Tooltip for feature name only
 -           */
 -          return JvSwingUtils.wrapTooltip(true, MessageManager
 +        case TYPE_COLUMN:
 +          tip = JvSwingUtils.wrapTooltip(true, MessageManager
                    .getString("label.feature_settings_click_drag"));
 +          break;
 +        case FILTER_COLUMN:
 +          int row = table.rowAtPoint(e.getPoint());
 +          FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row,
 +                  column);
 +          tip = o.isEmpty()
 +                  ? MessageManager.getString("label.filters_tooltip")
 +                  : o.toString();
 +          break;
 +        default:
 +          break;
          }
 -        return null;
 +        return tip;
        }
      };
      table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12));
      table.setFont(new Font("Verdana", Font.PLAIN, 12));
 -    table.setDefaultRenderer(Color.class, new ColorRenderer());
 -
 -    table.setDefaultEditor(Color.class, new ColorEditor(this));
  
 +    // table.setDefaultRenderer(Color.class, new ColorRenderer());
 +    // table.setDefaultEditor(Color.class, new ColorEditor(this));
 +    //
      table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
      table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
 +
 +    table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this));
 +    table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer());
 +
 +    TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
 +            new ColorRenderer(), new ColorEditor(this));
 +    table.addColumn(colourColumn);
 +
 +    TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75,
 +            new FilterRenderer(), new FilterEditor(this));
 +    table.addColumn(filterColumn);
 +
      table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  
      table.addMouseListener(new MouseAdapter()
        public void mousePressed(MouseEvent evt)
        {
          selectedRow = table.rowAtPoint(evt.getPoint());
 +        String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
          if (evt.isPopupTrigger())
          {
 -          popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
 -                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
 -                  evt.getX(), evt.getY());
 +          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
 +          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
 +                  evt.getY());
          }
          else if (evt.getClickCount() == 2)
          {
            boolean toggleSelection = Platform.isControlDown(evt);
            boolean extendSelection = evt.isShiftDown();
            fr.ap.alignFrame.avc.markColumnsContainingFeatures(
 -                  invertSelection, extendSelection, toggleSelection,
 -                  (String) table.getValueAt(selectedRow, 0));
 +                  invertSelection, extendSelection, toggleSelection, type);
          }
        }
  
          selectedRow = table.rowAtPoint(evt.getPoint());
          if (evt.isPopupTrigger())
          {
 -          popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
 -                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
 -                  evt.getX(), evt.getY());
 +          String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
 +          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
 +          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
 +                  evt.getY());
          }
        }
      });
          if (!fs.resettingTable && !fs.handlingUpdate)
          {
            fs.handlingUpdate = true;
 -          fs.resetTable(null); // new groups may be added with new seuqence
 -          // feature types only
 +          fs.resetTable(null);
 +          // new groups may be added with new sequence feature types only
            fs.handlingUpdate = false;
          }
        }
      {
        Desktop.addInternalFrame(frame,
                MessageManager.getString("label.sequence_feature_settings"),
 -              475, 480);
 +              600, 480);
      }
      else
      {
        Desktop.addInternalFrame(frame,
                MessageManager.getString("label.sequence_feature_settings"),
 -              400, 450);
 +              600, 450);
      }
      frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
  
      inConstruction = false;
    }
  
 -  protected void popupSort(final int selectedRow, final String type,
 +  protected void popupSort(final int rowSelected, final String type,
            final Object typeCol, final Map<String, float[][]> minmax, int x,
            int y)
    {
  
      });
      men.add(dens);
 -    if (minmax != null)
 +
 +    /*
 +     * variable colour options include colour by label, by score,
 +     * by selected attribute text, or attribute value
 +     */
 +    final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
 +            MessageManager.getString("label.variable_colour"));
 +    mxcol.setSelected(!featureColour.isSimpleColour());
 +    men.add(mxcol);
 +    mxcol.addActionListener(new ActionListener()
      {
 -      final float[][] typeMinMax = minmax.get(type);
 -      /*
 -       * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); //
 -       * this is broken at the moment and isn't that useful anyway!
 -       * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new
 -       * ActionListener() {
 -       * 
 -       * public void actionPerformed(ActionEvent e) {
 -       * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
 -       * null); } else { minmax.put(type, typeMinMax); } }
 -       * 
 -       * });
 -       * 
 -       * men.add(chb);
 -       */
 -      if (typeMinMax != null && typeMinMax[0] != null)
 -      {
 -        // if (table.getValueAt(row, column));
 -        // graduated colourschemes for those where minmax exists for the
 -        // positional features
 -        final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
 -                "Graduated Colour");
 -        mxcol.setSelected(!featureColour.isSimpleColour());
 -        men.add(mxcol);
 -        mxcol.addActionListener(new ActionListener()
 -        {
 -          JColorChooser colorChooser;
 +      JColorChooser colorChooser;
  
 -          @Override
 -          public void actionPerformed(ActionEvent e)
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        if (e.getSource() == mxcol)
 +        {
 +          if (featureColour.isSimpleColour())
            {
 -            if (e.getSource() == mxcol)
 -            {
 -              if (featureColour.isSimpleColour())
 -              {
 -                FeatureColourChooser fc = new FeatureColourChooser(me.fr,
 -                        type);
 -                fc.addActionListener(this);
 -              }
 -              else
 -              {
 -                // bring up simple color chooser
 -                colorChooser = new JColorChooser();
 -                JDialog dialog = JColorChooser.createDialog(me,
 -                        "Select new Colour", true, // modal
 -                        colorChooser, this, // OK button handler
 -                        null); // no CANCEL button handler
 -                colorChooser.setColor(featureColour.getMaxColour());
 -                dialog.setVisible(true);
 -              }
 -            }
 -            else
 -            {
 -              if (e.getSource() instanceof FeatureColourChooser)
 -              {
 -                FeatureColourChooser fc = (FeatureColourChooser) e
 -                        .getSource();
 -                table.setValueAt(fc.getLastColour(), selectedRow, 1);
 -                table.validate();
 -              }
 -              else
 -              {
 -                // probably the color chooser!
 -                table.setValueAt(new FeatureColour(colorChooser.getColor()),
 -                        selectedRow, 1);
 -                table.validate();
 -                me.updateFeatureRenderer(
 -                        ((FeatureTableModel) table.getModel()).getData(),
 -                        false);
 -              }
 -            }
 +            FeatureTypeSettings fc = new FeatureTypeSettings(me.fr, type);
 +            fc.addActionListener(this);
            }
 -
 -        });
 +          else
 +          {
 +            // bring up simple color chooser
 +            colorChooser = new JColorChooser();
 +            String title = MessageManager
 +                    .getString("label.select_colour");
 +            JDialog dialog = JColorChooser.createDialog(me,
 +                    title, true, // modal
 +                    colorChooser, this, // OK button handler
 +                    null); // no CANCEL button handler
 +            colorChooser.setColor(featureColour.getMaxColour());
 +            dialog.setVisible(true);
 +          }
 +        }
 +        else
 +        {
 +          if (e.getSource() instanceof FeatureTypeSettings)
 +          {
 +            /*
 +             * update after OK in feature colour dialog; the updated
 +             * colour will have already been set in the FeatureRenderer
 +             */
 +            FeatureColourI fci = fr.getFeatureColours().get(type);
 +            table.setValueAt(fci, rowSelected, 1);
 +            table.validate();
 +          }
 +          else
 +          {
 +            // probably the color chooser!
 +            table.setValueAt(new FeatureColour(colorChooser.getColor()),
 +                    rowSelected, 1);
 +            table.validate();
 +            me.updateFeatureRenderer(
 +                    ((FeatureTableModel) table.getModel()).getData(),
 +                    false);
 +          }
 +        }
        }
 -    }
 +
 +    });
 +
      JMenuItem selCols = new JMenuItem(
              MessageManager.getString("label.select_columns_containing"));
      selCols.addActionListener(new ActionListener()
      men.show(table, x, y);
    }
  
 -  /**
 -   * true when Feature Settings are updating from feature renderer
 -   */
 -  private boolean handlingUpdate = false;
 -
 -  /**
 -   * holds {featureCount, totalExtent} for each feature type
 -   */
 -  Map<String, float[]> typeWidth = null;
 -
    @Override
    synchronized public void discoverAllFeatureData()
    {
      return visible;
    }
  
 -  boolean resettingTable = false;
 -
    synchronized void resetTable(String[] groupChanged)
    {
      if (resettingTable)
        }
      }
  
 -    Object[][] data = new Object[displayableTypes.size()][3];
 +    Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
      int dataIndex = 0;
  
      if (fr.hasRenderOrder())
            continue;
          }
  
 -        data[dataIndex][0] = type;
 -        data[dataIndex][1] = fr.getFeatureStyle(type);
 -        data[dataIndex][2] = new Boolean(
 +        data[dataIndex][TYPE_COLUMN] = type;
 +        data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
 +        FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
 +        data[dataIndex][FILTER_COLUMN] = featureFilter == null
 +                ? new FeatureMatcherSet()
 +                : featureFilter;
 +        data[dataIndex][SHOW_COLUMN] = new Boolean(
                  af.getViewport().getFeaturesDisplayed().isVisible(type));
          dataIndex++;
          displayableTypes.remove(type);
      while (!displayableTypes.isEmpty())
      {
        String type = displayableTypes.iterator().next();
 -      data[dataIndex][0] = type;
 +      data[dataIndex][TYPE_COLUMN] = type;
  
 -      data[dataIndex][1] = fr.getFeatureStyle(type);
 -      if (data[dataIndex][1] == null)
 +      data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
 +      if (data[dataIndex][COLOUR_COLUMN] == null)
        {
          // "Colour has been updated in another view!!"
          fr.clearRenderOrder();
          return;
        }
 -
 -      data[dataIndex][2] = new Boolean(true);
 +      FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
 +      data[dataIndex][FILTER_COLUMN] = featureFilter == null
 +              ? new FeatureMatcherSet()
 +              : featureFilter;
 +      data[dataIndex][SHOW_COLUMN] = new Boolean(true);
        dataIndex++;
        displayableTypes.remove(type);
      }
  
      if (originalData == null)
      {
 -      originalData = new Object[data.length][3];
 +      originalData = new Object[data.length][COLUMN_COUNT];
        for (int i = 0; i < data.length; i++)
        {
 -        System.arraycopy(data[i], 0, originalData[i], 0, 3);
 +        System.arraycopy(data[i], 0, originalData[i], 0, COLUMN_COUNT);
        }
      }
      else
    }
  
    /**
 -   * Updates 'originalData' (used for restore on Cancel) if we detect that
 -   * changes have been made outwith this dialog
 +   * Updates 'originalData' (used for restore on Cancel) if we detect that changes
 +   * have been made outwith this dialog
     * <ul>
     * <li>a new feature type added (and made visible)</li>
     * <li>a feature colour changed (in the Amend Features dialog)</li>
              .getData();
      for (Object[] row : foundData)
      {
 -      String type = (String) row[0];
 +      String type = (String) row[TYPE_COLUMN];
        boolean found = false;
        for (Object[] current : currentData)
        {
 -        if (type.equals(current[0]))
 +        if (type.equals(current[TYPE_COLUMN]))
          {
            found = true;
            /*
             * currently dependent on object equality here;
             * really need an equals method on FeatureColour
             */
 -          if (!row[1].equals(current[1]))
 +          if (!row[COLOUR_COLUMN].equals(current[COLOUR_COLUMN]))
            {
              /*
               * feature colour has changed externally - update originalData
               */
              for (Object[] original : originalData)
              {
 -              if (type.equals(original[0]))
 +              if (type.equals(original[TYPE_COLUMN]))
                {
 -                original[1] = row[1];
 +                original[COLOUR_COLUMN] = row[COLOUR_COLUMN];
                  break;
                }
              }
          /*
           * new feature detected - add to original data (on top)
           */
 -        Object[][] newData = new Object[originalData.length + 1][3];
 +        Object[][] newData = new Object[originalData.length
 +                + 1][COLUMN_COUNT];
          for (int i = 0; i < originalData.length; i++)
          {
 -          System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
 +          System.arraycopy(originalData[i], 0, newData[i + 1], 0,
 +                  COLUMN_COUNT);
          }
          newData[0] = row;
          originalData = newData;
  
    /**
     * Remove from the groups panel any checkboxes for groups that are not in the
 -   * foundGroups set. This enables removing a group from the display when the
 -   * last feature in that group is deleted.
 +   * foundGroups set. This enables removing a group from the display when the last
 +   * feature in that group is deleted.
     * 
     * @param foundGroups
     */
      }
    }
  
 +  /**
 +   * Offers a file chooser dialog, and then loads the feature colours and
 +   * filters from file in XML format and unmarshals to Jalview feature settings
 +   */
    void load()
    {
      JalviewFileChooser chooser = new JalviewFileChooser("fc",
 -            "Sequence Feature Colours");
 +            SEQUENCE_FEATURE_COLOURS);
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.load_feature_colours"));
      if (value == JalviewFileChooser.APPROVE_OPTION)
      {
        File file = chooser.getSelectedFile();
 +      load(file);
 +    }
 +  }
  
 -      try
 -      {
 -        InputStreamReader in = new InputStreamReader(
 -                new FileInputStream(file), "UTF-8");
 +  /**
 +   * Loads feature colours and filters from XML stored in the given file
 +   * 
 +   * @param file
 +   */
 +  void load(File file)
 +  {
 +    try
 +    {
 +      InputStreamReader in = new InputStreamReader(
 +              new FileInputStream(file), "UTF-8");
  
 -        JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
 +      JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
  
 -        for (int i = jucs.getColourCount() - 1; i >= 0; i--)
 -        {
 -          String name;
 -          jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
 -          if (newcol.hasMax())
 -          {
 -            Color mincol = null, maxcol = null;
 -            try
 -            {
 -              mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16));
 -              maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16));
 +      /*
 +       * load feature colours
 +       */
 +      for (int i = jucs.getColourCount() - 1; i >= 0; i--)
 +      {
 +        jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
 +        FeatureColourI colour = Jalview2XML.unmarshalColour(newcol);
 +        fr.setColour(newcol.getName(), colour);
 +        fr.setOrder(newcol.getName(), i / (float) jucs.getColourCount());
 +      }
  
 -            } catch (Exception e)
 -            {
 -              Cache.log.warn("Couldn't parse out graduated feature color.",
 -                      e);
 -            }
 -            FeatureColourI gcol = new FeatureColour(mincol, maxcol,
 -                    newcol.getMin(), newcol.getMax());
 -            if (newcol.hasAutoScale())
 -            {
 -              gcol.setAutoScaled(newcol.getAutoScale());
 -            }
 -            if (newcol.hasColourByLabel())
 -            {
 -              gcol.setColourByLabel(newcol.getColourByLabel());
 -            }
 -            if (newcol.hasThreshold())
 -            {
 -              gcol.setThreshold(newcol.getThreshold());
 -            }
 -            if (newcol.getThreshType().length() > 0)
 -            {
 -              String ttyp = newcol.getThreshType();
 -              if (ttyp.equalsIgnoreCase("ABOVE"))
 -              {
 -                gcol.setAboveThreshold(true);
 -              }
 -              if (ttyp.equalsIgnoreCase("BELOW"))
 -              {
 -                gcol.setBelowThreshold(true);
 -              }
 -            }
 -            fr.setColour(name = newcol.getName(), gcol);
 -          }
 -          else
 -          {
 -            Color color = new Color(
 -                    Integer.parseInt(jucs.getColour(i).getRGB(), 16));
 -            fr.setColour(name = jucs.getColour(i).getName(),
 -                    new FeatureColour(color));
 -          }
 -          fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
 -        }
 -        if (table != null)
 +      /*
 +       * load feature filters; loaded filters will replace any that are
 +       * currently defined, other defined filters are left unchanged 
 +       */
 +      for (int i = 0; i < jucs.getFilterCount(); i++)
 +      {
 +        jalview.schemabinding.version2.Filter filterModel = jucs
 +                .getFilter(i);
 +        String featureType = filterModel.getFeatureType();
 +        FeatureMatcherSetI filter = Jalview2XML.unmarshalFilter(featureType,
 +                filterModel.getMatcherSet());
 +        if (!filter.isEmpty())
          {
 -          resetTable(null);
 -          Object[][] data = ((FeatureTableModel) table.getModel())
 -                  .getData();
 -          ensureOrder(data);
 -          updateFeatureRenderer(data, false);
 -          table.repaint();
 +          fr.setFeatureFilter(featureType, filter);
          }
 -      } catch (Exception ex)
 -      {
 -        System.out.println("Error loading User Colour File\n" + ex);
        }
 +
 +      /*
 +       * update feature settings table
 +       */
 +      if (table != null)
 +      {
 +        resetTable(null);
 +        Object[][] data = ((FeatureTableModel) table.getModel())
 +                .getData();
 +        ensureOrder(data);
 +        updateFeatureRenderer(data, false);
 +        table.repaint();
 +      }
 +    } catch (Exception ex)
 +    {
 +      System.out.println("Error loading User Colour File\n" + ex);
      }
    }
  
 +  /**
 +   * Offers a file chooser dialog, and then saves the current feature colours
 +   * and any filters to the selected file in XML format
 +   */
    void save()
    {
      JalviewFileChooser chooser = new JalviewFileChooser("fc",
 -            "Sequence Feature Colours");
 +            SEQUENCE_FEATURE_COLOURS);
      chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.save_feature_colours"));
  
      if (value == JalviewFileChooser.APPROVE_OPTION)
      {
 -      String choice = chooser.getSelectedFile().getPath();
 -      jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
 -      ucs.setSchemeName("Sequence Features");
 -      try
 -      {
 -        PrintWriter out = new PrintWriter(new OutputStreamWriter(
 -                new FileOutputStream(choice), "UTF-8"));
 +      save(chooser.getSelectedFile());
 +    }
 +  }
  
 -        Set<String> fr_colours = fr.getAllFeatureColours();
 -        Iterator<String> e = fr_colours.iterator();
 -        float[] sortOrder = new float[fr_colours.size()];
 -        String[] sortTypes = new String[fr_colours.size()];
 -        int i = 0;
 -        while (e.hasNext())
 +  /**
 +   * Saves feature colours and filters to the given file
 +   * 
 +   * @param file
 +   */
 +  void save(File file)
 +  {
 +    JalviewUserColours ucs = new JalviewUserColours();
 +    ucs.setSchemeName("Sequence Features");
 +    try
 +    {
 +      PrintWriter out = new PrintWriter(new OutputStreamWriter(
 +              new FileOutputStream(file), "UTF-8"));
 +
 +      /*
 +       * sort feature types by colour order, from 0 (highest)
 +       * to 1 (lowest)
 +       */
 +      Set<String> fr_colours = fr.getAllFeatureColours();
 +      String[] sortedTypes = fr_colours
 +              .toArray(new String[fr_colours.size()]);
 +      Arrays.sort(sortedTypes, new Comparator<String>()
 +      {
 +        @Override
 +        public int compare(String type1, String type2)
          {
 -          sortTypes[i] = e.next();
 -          sortOrder[i] = fr.getOrder(sortTypes[i]);
 -          i++;
 +          return Float.compare(fr.getOrder(type1), fr.getOrder(type2));
          }
 -        QuickSort.sort(sortOrder, sortTypes);
 -        sortOrder = null;
 -        for (i = 0; i < sortTypes.length; i++)
 +      });
 +
 +      /*
 +       * save feature colours
 +       */
 +      for (String featureType : sortedTypes)
 +      {
 +        FeatureColourI fcol = fr.getFeatureStyle(featureType);
 +        jalview.schemabinding.version2.Colour col = Jalview2XML.marshalColour(
 +                featureType, fcol);
 +        ucs.addColour(col);
 +      }
 +
 +      /*
 +       * save any feature filters
 +       */
 +      for (String featureType : sortedTypes)
 +      {
 +        FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
 +        if (filter != null && !filter.isEmpty())
          {
 -          jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
 -          col.setName(sortTypes[i]);
 -          FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
 -          if (fcol.isSimpleColour())
 -          {
 -            col.setRGB(Format.getHexString(fcol.getColour()));
 -          }
 -          else
 -          {
 -            col.setRGB(Format.getHexString(fcol.getMaxColour()));
 -            col.setMin(fcol.getMin());
 -            col.setMax(fcol.getMax());
 -            col.setMinRGB(
 -                    jalview.util.Format.getHexString(fcol.getMinColour()));
 -            col.setAutoScale(fcol.isAutoScaled());
 -            col.setThreshold(fcol.getThreshold());
 -            col.setColourByLabel(fcol.isColourByLabel());
 -            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
 -                    : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
 -          }
 -          ucs.addColour(col);
 +          Iterator<FeatureMatcherI> iterator = filter.getMatchers().iterator();
 +          FeatureMatcherI firstMatcher = iterator.next();
 +          MatcherSet ms = Jalview2XML.marshalFilter(firstMatcher, iterator,
 +                  filter.isAnded());
 +          Filter filterModel = new Filter();
 +          filterModel.setFeatureType(featureType);
 +          filterModel.setMatcherSet(ms);
 +          ucs.addFilter(filterModel);
          }
 -        ucs.marshal(out);
 -        out.close();
 -      } catch (Exception ex)
 -      {
 -        ex.printStackTrace();
        }
 +
 +      ucs.marshal(out);
 +      out.close();
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
      }
    }
  
    public void invertSelection()
    {
-     for (int i = 0; i < table.getRowCount(); i++)
+     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
+     for (int i = 0; i < data.length; i++)
      {
-       Boolean value = (Boolean) table.getValueAt(i, SHOW_COLUMN);
-       table.setValueAt(new Boolean(!value.booleanValue()), i, SHOW_COLUMN);
+       data[i][2] = !(Boolean) data[i][2];
      }
+     af.alignPanel.paintAlignment(true, true);
    }
  
    public void orderByAvWidth()
      float[] width = new float[data.length];
      float[] awidth;
      float max = 0;
 -    int num = 0;
 +
      for (int i = 0; i < data.length; i++)
      {
 -      awidth = typeWidth.get(data[i][0]);
 +      awidth = typeWidth.get(data[i][TYPE_COLUMN]);
        if (awidth[0] > 0)
        {
          width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
          // weight - but have to make per
          // sequence, too (awidth[2])
          // if (width[i]==1) // hack to distinguish single width sequences.
 -        num++;
        }
        else
        {
        // awidth = (float[]) typeWidth.get(data[i][0]);
        if (width[i] == 0)
        {
 -        width[i] = fr.getOrder(data[i][0].toString());
 +        width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString());
          if (width[i] < 0)
          {
 -          width[i] = fr.setOrder(data[i][0].toString(), i / data.length);
 +          width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(),
 +                  i / data.length);
          }
        }
        else
        {
          width[i] /= max; // normalize
 -        fr.setOrder(data[i][0].toString(), width[i]); // store for later
 +        fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for later
        }
        if (i > 0)
        {
    }
  
    /**
 -   * Update the priority order of features; only repaint if this changed the
 -   * order of visible features
 +   * Update the priority order of features; only repaint if this changed the order
 +   * of visible features
     * 
     * @param data
     * @param visibleNew
     */
    private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
    {
 -    if (fr.setFeaturePriority(data, visibleNew))
 +    FeatureSettingsBean[] rowData = getTableAsBeans(data);
 +
 +    if (fr.setFeaturePriority(rowData, visibleNew))
      {
        af.alignPanel.paintAlignment(true, true);
      }
    }
  
 -  int selectedRow = -1;
 -
 -  JTabbedPane tabbedPane = new JTabbedPane();
 -
 -  BorderLayout borderLayout1 = new BorderLayout();
 -
 -  BorderLayout borderLayout2 = new BorderLayout();
 -
 -  BorderLayout borderLayout3 = new BorderLayout();
 -
 -  JPanel bigPanel = new JPanel();
 -
 -  BorderLayout borderLayout4 = new BorderLayout();
 -
 -  JButton invert = new JButton();
 -
 -  JPanel buttonPanel = new JPanel();
 -
 -  JButton cancel = new JButton();
 -
 -  JButton ok = new JButton();
 -
 -  JButton loadColours = new JButton();
 -
 -  JButton saveColours = new JButton();
 -
 -  JPanel dasButtonPanel = new JPanel();
 -
 -  JButton fetchDAS = new JButton();
 -
 -  JButton saveDAS = new JButton();
 -
 -  JButton cancelDAS = new JButton();
 -
 -  JButton optimizeOrder = new JButton();
 -
 -  JButton sortByScore = new JButton();
 +  /**
 +   * Converts table data into an array of data beans
 +   */
 +  private FeatureSettingsBean[] getTableAsBeans(Object[][] data)
 +  {
 +    FeatureSettingsBean[] rowData = new FeatureSettingsBean[data.length];
 +    for (int i = 0; i < data.length; i++)
 +    {
 +      String type = (String) data[i][TYPE_COLUMN];
 +      FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN];
 +      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN];
 +      Boolean isShown = (Boolean) data[i][SHOW_COLUMN];
 +      rowData[i] = new FeatureSettingsBean(type, colour, theFilter,
 +              isShown);
 +    }
 +    return rowData;
 +  }
  
 -  JButton sortByDens = new JButton();
 +  private void jbInit() throws Exception
 +  {
 +    this.setLayout(new BorderLayout());
  
 -  JButton help = new JButton();
 +    JPanel settingsPane = new JPanel();
 +    settingsPane.setLayout(new BorderLayout());
  
 -  JPanel transbuttons = new JPanel(new GridLayout(5, 1));
 +    dasSettingsPane.setLayout(new BorderLayout());
  
 -  private void jbInit() throws Exception
 -  {
 -    this.setLayout(borderLayout1);
 -    settingsPane.setLayout(borderLayout2);
 -    dasSettingsPane.setLayout(borderLayout3);
 -    bigPanel.setLayout(borderLayout4);
 +    JPanel bigPanel = new JPanel();
 +    bigPanel.setLayout(new BorderLayout());
  
      groupPanel = new JPanel();
      bigPanel.add(groupPanel, BorderLayout.NORTH);
  
 +    JButton invert = new JButton(
 +            MessageManager.getString("label.invert_selection"));
      invert.setFont(JvSwingUtils.getLabelFont());
 -    invert.setText(MessageManager.getString("label.invert_selection"));
      invert.addActionListener(new ActionListener()
      {
        @Override
          invertSelection();
        }
      });
 +
 +    JButton optimizeOrder = new JButton(
 +            MessageManager.getString("label.optimise_order"));
      optimizeOrder.setFont(JvSwingUtils.getLabelFont());
 -    optimizeOrder.setText(MessageManager.getString("label.optimise_order"));
      optimizeOrder.addActionListener(new ActionListener()
      {
        @Override
          orderByAvWidth();
        }
      });
 +
 +    JButton sortByScore = new JButton(
 +            MessageManager.getString("label.seq_sort_by_score"));
      sortByScore.setFont(JvSwingUtils.getLabelFont());
 -    sortByScore
 -            .setText(MessageManager.getString("label.seq_sort_by_score"));
      sortByScore.addActionListener(new ActionListener()
      {
        @Override
          af.avc.sortAlignmentByFeatureScore(null);
        }
      });
 -    sortByDens.setFont(JvSwingUtils.getLabelFont());
 -    sortByDens.setText(
 +    JButton sortByDens = new JButton(
              MessageManager.getString("label.sequence_sort_by_density"));
 +    sortByDens.setFont(JvSwingUtils.getLabelFont());
      sortByDens.addActionListener(new ActionListener()
      {
        @Override
          af.avc.sortAlignmentByFeatureDensity(null);
        }
      });
 +
 +    JButton help = new JButton(MessageManager.getString("action.help"));
      help.setFont(JvSwingUtils.getLabelFont());
 -    help.setText(MessageManager.getString("action.help"));
      help.addActionListener(new ActionListener()
      {
        @Override
          }
        }
      });
 +
 +    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
      cancel.setFont(JvSwingUtils.getLabelFont());
 -    cancel.setText(MessageManager.getString("action.cancel"));
      cancel.addActionListener(new ActionListener()
      {
        @Override
        public void actionPerformed(ActionEvent e)
        {
          fr.setTransparency(originalTransparency);
 +        fr.setFeatureFilters(originalFilters);
          updateFeatureRenderer(originalData);
          close();
        }
      });
 +
 +    JButton ok = new JButton(MessageManager.getString("action.ok"));
      ok.setFont(JvSwingUtils.getLabelFont());
 -    ok.setText(MessageManager.getString("action.ok"));
      ok.addActionListener(new ActionListener()
      {
        @Override
          close();
        }
      });
 +
 +    JButton loadColours = new JButton(
 +            MessageManager.getString("label.load_colours"));
      loadColours.setFont(JvSwingUtils.getLabelFont());
 -    loadColours.setText(MessageManager.getString("label.load_colours"));
 +    loadColours.setToolTipText(
 +            MessageManager.getString("label.load_colours_tooltip"));
      loadColours.addActionListener(new ActionListener()
      {
        @Override
          load();
        }
      });
 +
 +    JButton saveColours = new JButton(
 +            MessageManager.getString("label.save_colours"));
      saveColours.setFont(JvSwingUtils.getLabelFont());
 -    saveColours.setText(MessageManager.getString("label.save_colours"));
 +    saveColours.setToolTipText(
 +            MessageManager.getString("label.save_colours_tooltip"));
      saveColours.addActionListener(new ActionListener()
      {
        @Override
          if (!inConstruction)
          {
            fr.setTransparency((100 - transparency.getValue()) / 100f);
 -          af.alignPanel.paintAlignment(true,true);
 +          af.alignPanel.paintAlignment(true, true);
          }
        }
      });
          saveDAS_actionPerformed(e);
        }
      });
 +
 +    JPanel dasButtonPanel = new JPanel();
      dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
      dasSettingsPane.setBorder(null);
      cancelDAS.setEnabled(false);
          cancelDAS_actionPerformed(e);
        }
      });
 -    this.add(tabbedPane, java.awt.BorderLayout.CENTER);
 -    tabbedPane.addTab(MessageManager.getString("label.feature_settings"),
 -            settingsPane);
 -    tabbedPane.addTab(MessageManager.getString("label.das_settings"),
 -            dasSettingsPane);
 -    bigPanel.add(transPanel, java.awt.BorderLayout.SOUTH);
 +
 +    JPanel transPanel = new JPanel(new GridLayout(1, 2));
 +    bigPanel.add(transPanel, BorderLayout.SOUTH);
 +
 +    JPanel transbuttons = new JPanel(new GridLayout(5, 1));
      transbuttons.add(optimizeOrder);
      transbuttons.add(invert);
      transbuttons.add(sortByScore);
      transbuttons.add(sortByDens);
      transbuttons.add(help);
 -    JPanel sliderPanel = new JPanel();
 -    sliderPanel.add(transparency);
      transPanel.add(transparency);
      transPanel.add(transbuttons);
 +
 +    JPanel buttonPanel = new JPanel();
      buttonPanel.add(ok);
      buttonPanel.add(cancel);
      buttonPanel.add(loadColours);
      buttonPanel.add(saveColours);
 -    bigPanel.add(scrollPane, java.awt.BorderLayout.CENTER);
 -    dasSettingsPane.add(dasButtonPanel, java.awt.BorderLayout.SOUTH);
 +    bigPanel.add(scrollPane, BorderLayout.CENTER);
 +    dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH);
      dasButtonPanel.add(fetchDAS);
      dasButtonPanel.add(cancelDAS);
      dasButtonPanel.add(saveDAS);
 -    settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER);
 -    settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH);
 +    settingsPane.add(bigPanel, BorderLayout.CENTER);
 +    settingsPane.add(buttonPanel, BorderLayout.SOUTH);
 +    this.add(settingsPane);
    }
  
    public void fetchDAS_actionPerformed(ActionEvent e)
    // ///////////////////////////////////////////////////////////////////////
    class FeatureTableModel extends AbstractTableModel
    {
      private String[] columnNames = {
          MessageManager.getString("label.feature_type"),
          MessageManager.getString("action.colour"),
 -        MessageManager.getString("label.display") };
 +        MessageManager.getString("label.filter"),
 +        MessageManager.getString("label.show") };
  
      private Object[][] data;
  
 +    FeatureTableModel(Object[][] data)
 +    {
 +      this.data = data;
 +    }
 +
      public Object[][] getData()
      {
        return data;
        return data[row][col];
      }
  
 +    /**
 +     * Answers the class of the object in column c of the first row of the table
 +     */
      @Override
 -    public Class getColumnClass(int c)
 +    public Class<?> getColumnClass(int c)
      {
 -      return getValueAt(0, c).getClass();
 +      Object v = getValueAt(0, c);
 +      return v == null ? null : v.getClass();
      }
  
      @Override
              boolean isSelected, boolean hasFocus, int row, int column)
      {
        FeatureColourI cellColour = (FeatureColourI) color;
 -      // JLabel comp = new JLabel();
 -      // comp.
        setOpaque(true);
 -      // comp.
 -      // setBounds(getBounds());
 -      Color newColor;
        setToolTipText(baseTT);
        setBackground(tbl.getBackground());
        if (!cellColour.isSimpleColour())
          Rectangle cr = tbl.getCellRect(row, column, false);
          FeatureSettings.renderGraduatedColor(this, cellColour,
                  (int) cr.getWidth(), (int) cr.getHeight());
 -
        }
        else
        {
          this.setText("");
          this.setIcon(null);
 -        newColor = cellColour.getColour();
 -        setBackground(newColor);
 +        setBackground(cellColour.getColour());
        }
        if (isSelected)
        {
      }
    }
  
 +  class FilterRenderer extends JLabel implements TableCellRenderer
 +  {
 +    javax.swing.border.Border unselectedBorder = null;
 +
 +    javax.swing.border.Border selectedBorder = null;
 +
 +    public FilterRenderer()
 +    {
 +      setOpaque(true); // MUST do this for background to show up.
 +      setHorizontalTextPosition(SwingConstants.CENTER);
 +      setVerticalTextPosition(SwingConstants.CENTER);
 +    }
 +
 +    @Override
 +    public Component getTableCellRendererComponent(JTable tbl,
 +            Object filter, boolean isSelected, boolean hasFocus, int row,
 +            int column)
 +    {
 +      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter;
 +      setOpaque(true);
 +      String asText = theFilter.toString();
 +      setBackground(tbl.getBackground());
 +      this.setText(asText);
 +      this.setIcon(null);
 +
 +      if (isSelected)
 +      {
 +        if (selectedBorder == null)
 +        {
 +          selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
 +                  tbl.getSelectionBackground());
 +        }
 +        setBorder(selectedBorder);
 +      }
 +      else
 +      {
 +        if (unselectedBorder == null)
 +        {
 +          unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
 +                  tbl.getBackground());
 +        }
 +        setBorder(unselectedBorder);
 +      }
 +
 +      return this;
 +    }
 +  }
 +
    /**
     * update comp using rendering settings from gcol
     * 
            int w, int h)
    {
      boolean thr = false;
 -    String tt = "";
 -    String tx = "";
 +    StringBuilder tt = new StringBuilder();
 +    StringBuilder tx = new StringBuilder();
 +
 +    if (gcol.isColourByAttribute())
 +    {
 +      tx.append(String.join(":", gcol.getAttributeName()));
 +    }
 +    else if (!gcol.isColourByLabel())
 +    {
 +      tx.append(MessageManager.getString("label.score"));
 +    }
 +    tx.append(" ");
      if (gcol.isAboveThreshold())
      {
        thr = true;
 -      tx += ">";
 -      tt += "Thresholded (Above " + gcol.getThreshold() + ") ";
 +      tx.append(">");
 +      tt.append("Thresholded (Above ").append(gcol.getThreshold())
 +              .append(") ");
      }
      if (gcol.isBelowThreshold())
      {
        thr = true;
 -      tx += "<";
 -      tt += "Thresholded (Below " + gcol.getThreshold() + ") ";
 +      tx.append("<");
 +      tt.append("Thresholded (Below ").append(gcol.getThreshold())
 +              .append(") ");
      }
      if (gcol.isColourByLabel())
      {
 -      tt = "Coloured by label text. " + tt;
 +      tt.append("Coloured by label text. ").append(tt);
        if (thr)
        {
 -        tx += " ";
 +        tx.append(" ");
 +      }
 +      if (!gcol.isColourByAttribute())
 +      {
 +        tx.append("Label");
        }
 -      tx += "Label";
        comp.setIcon(null);
      }
      else
        // + ", " + minCol.getBlue() + ")");
      }
      comp.setHorizontalAlignment(SwingConstants.CENTER);
 -    comp.setText(tx);
 +    comp.setText(tx.toString());
      if (tt.length() > 0)
      {
        if (comp.getToolTipText() == null)
        {
 -        comp.setToolTipText(tt);
 +        comp.setToolTipText(tt.toString());
        }
        else
        {
 -        comp.setToolTipText(tt + " " + comp.getToolTipText());
 +        comp.setToolTipText(
 +                tt.append(" ").append(comp.getToolTipText()).toString());
        }
      }
    }
 +
 +  class ColorEditor extends AbstractCellEditor
 +          implements TableCellEditor, ActionListener
 +  {
 +    FeatureSettings me;
 +
 +    FeatureColourI currentColor;
 +
 +    FeatureTypeSettings chooser;
 +
 +    String type;
 +
 +    JButton button;
 +
 +    JColorChooser colorChooser;
 +
 +    JDialog dialog;
 +
 +    protected static final String EDIT = "edit";
 +
 +    int rowSelected = 0;
 +
 +    public ColorEditor(FeatureSettings me)
 +    {
 +      this.me = me;
 +      // Set up the editor (from the table's point of view),
 +      // which is a button.
 +      // This button brings up the color chooser dialog,
 +      // which is the editor from the user's point of view.
 +      button = new JButton();
 +      button.setActionCommand(EDIT);
 +      button.addActionListener(this);
 +      button.setBorderPainted(false);
 +      // Set up the dialog that the button brings up.
 +      colorChooser = new JColorChooser();
 +      dialog = JColorChooser.createDialog(button,
 +              MessageManager.getString("label.select_colour"), true, // modal
 +              colorChooser, this, // OK button handler
 +              null); // no CANCEL button handler
 +    }
 +
 +    /**
 +     * Handles events from the editor button and from the dialog's OK button.
 +     */
 +    @Override
 +    public void actionPerformed(ActionEvent e)
 +    {
 +      // todo test e.getSource() instead here
 +      if (EDIT.equals(e.getActionCommand()))
 +      {
 +        // The user has clicked the cell, so
 +        // bring up the dialog.
 +        if (currentColor.isSimpleColour())
 +        {
 +          // bring up simple color chooser
 +          button.setBackground(currentColor.getColour());
 +          colorChooser.setColor(currentColor.getColour());
 +          dialog.setVisible(true);
 +        }
 +        else
 +        {
 +          // bring up graduated chooser.
 +          chooser = new FeatureTypeSettings(me.fr, type);
 +          chooser.setRequestFocusEnabled(true);
 +          chooser.requestFocus();
 +          chooser.addActionListener(this);
 +          chooser.showTab(true);
 +        }
 +        // Make the renderer reappear.
 +        fireEditingStopped();
 +
 +      }
 +      else
 +      {
 +        if (currentColor.isSimpleColour())
 +        {
 +          /*
 +           * read off colour picked in colour chooser after OK pressed
 +           */
 +          currentColor = new FeatureColour(colorChooser.getColor());
 +          me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
 +        }
 +        else
 +        {
 +          /*
 +           * after OK in variable colour dialog, any changes to colour 
 +           * (or filters!) are already set in FeatureRenderer, so just
 +           * update table data without triggering updateFeatureRenderer
 +           */
 +          currentColor = fr.getFeatureColours().get(type);
 +          FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
 +          if (currentFilter == null)
 +          {
 +            currentFilter = new FeatureMatcherSet();
 +          }
 +          Object[] data = ((FeatureTableModel) table.getModel())
 +                  .getData()[rowSelected];
 +          data[COLOUR_COLUMN] = currentColor;
 +          data[FILTER_COLUMN] = currentFilter;
 +        }
 +        fireEditingStopped();
 +        me.table.validate();
 +      }
 +    }
 +
 +    // Implement the one CellEditor method that AbstractCellEditor doesn't.
 +    @Override
 +    public Object getCellEditorValue()
 +    {
 +      return currentColor;
 +    }
 +
 +    // Implement the one method defined by TableCellEditor.
 +    @Override
 +    public Component getTableCellEditorComponent(JTable theTable, Object value,
 +            boolean isSelected, int row, int column)
 +    {
 +      currentColor = (FeatureColourI) value;
 +      this.rowSelected = row;
 +      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
 +      button.setOpaque(true);
 +      button.setBackground(me.getBackground());
 +      if (!currentColor.isSimpleColour())
 +      {
 +        JLabel btn = new JLabel();
 +        btn.setSize(button.getSize());
 +        FeatureSettings.renderGraduatedColor(btn, currentColor);
 +        button.setBackground(btn.getBackground());
 +        button.setIcon(btn.getIcon());
 +        button.setText(btn.getText());
 +      }
 +      else
 +      {
 +        button.setText("");
 +        button.setIcon(null);
 +        button.setBackground(currentColor.getColour());
 +      }
 +      return button;
 +    }
 +  }
 +
 +  /**
 +   * The cell editor for the Filter column. It displays the text of any filters
 +   * for the feature type in that row (in full as a tooltip, possible abbreviated
 +   * as display text). On click in the cell, opens the Feature Display Settings
 +   * dialog at the Filters tab.
 +   */
 +  class FilterEditor extends AbstractCellEditor
 +          implements TableCellEditor, ActionListener
 +  {
 +    FeatureSettings me;
 +
 +    FeatureMatcherSetI currentFilter;
 +
 +    Point lastLocation;
 +
 +    String type;
 +
 +    JButton button;
 +
 +    protected static final String EDIT = "edit";
 +
 +    int rowSelected = 0;
 +
 +    public FilterEditor(FeatureSettings me)
 +    {
 +      this.me = me;
 +      button = new JButton();
 +      button.setActionCommand(EDIT);
 +      button.addActionListener(this);
 +      button.setBorderPainted(false);
 +    }
 +
 +    /**
 +     * Handles events from the editor button
 +     */
 +    @Override
 +    public void actionPerformed(ActionEvent e)
 +    {
 +      if (button == e.getSource())
 +      {
 +        FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
 +        chooser.addActionListener(this);
 +        chooser.setRequestFocusEnabled(true);
 +        chooser.requestFocus();
 +        if (lastLocation != null)
 +        {
 +          // todo open at its last position on screen
 +          chooser.setBounds(lastLocation.x, lastLocation.y,
 +                  chooser.getWidth(), chooser.getHeight());
 +          chooser.validate();
 +        }
 +        chooser.showTab(false);
 +        fireEditingStopped();
 +      }
 +      else if (e.getSource() instanceof Component)
 +      {
 +
 +        /*
 +         * after OK in variable colour dialog, any changes to filter
 +         * (or colours!) are already set in FeatureRenderer, so just
 +         * update table data without triggering updateFeatureRenderer
 +         */
 +        FeatureColourI currentColor = fr.getFeatureColours().get(type);
 +        currentFilter = me.fr.getFeatureFilter(type);
 +        if (currentFilter == null)
 +        {
 +          currentFilter = new FeatureMatcherSet();
 +        }
 +        Object[] data = ((FeatureTableModel) table.getModel())
 +                .getData()[rowSelected];
 +        data[COLOUR_COLUMN] = currentColor;
 +        data[FILTER_COLUMN] = currentFilter;
 +        fireEditingStopped();
 +        me.table.validate();
 +      }
 +    }
 +
 +    @Override
 +    public Object getCellEditorValue()
 +    {
 +      return currentFilter;
 +    }
 +
 +    @Override
 +    public Component getTableCellEditorComponent(JTable theTable, Object value,
 +            boolean isSelected, int row, int column)
 +    {
 +      currentFilter = (FeatureMatcherSetI) value;
 +      this.rowSelected = row;
 +      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
 +      button.setOpaque(true);
 +      button.setBackground(me.getBackground());
 +      button.setText(currentFilter.toString());
 +      button.setToolTipText(currentFilter.toString());
 +      button.setIcon(null);
 +      return button;
 +    }
 +  }
  }
  
  class FeatureIcon implements Icon
      }
    }
  }
 -
 -class ColorEditor extends AbstractCellEditor
 -        implements TableCellEditor, ActionListener
 -{
 -  FeatureSettings me;
 -
 -  FeatureColourI currentColor;
 -
 -  FeatureColourChooser chooser;
 -
 -  String type;
 -
 -  JButton button;
 -
 -  JColorChooser colorChooser;
 -
 -  JDialog dialog;
 -
 -  protected static final String EDIT = "edit";
 -
 -  int selectedRow = 0;
 -
 -  public ColorEditor(FeatureSettings me)
 -  {
 -    this.me = me;
 -    // Set up the editor (from the table's point of view),
 -    // which is a button.
 -    // This button brings up the color chooser dialog,
 -    // which is the editor from the user's point of view.
 -    button = new JButton();
 -    button.setActionCommand(EDIT);
 -    button.addActionListener(this);
 -    button.setBorderPainted(false);
 -    // Set up the dialog that the button brings up.
 -    colorChooser = new JColorChooser();
 -    dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal
 -            colorChooser, this, // OK button handler
 -            null); // no CANCEL button handler
 -  }
 -
 -  /**
 -   * Handles events from the editor button and from the dialog's OK button.
 -   */
 -  @Override
 -  public void actionPerformed(ActionEvent e)
 -  {
 -
 -    if (EDIT.equals(e.getActionCommand()))
 -    {
 -      // The user has clicked the cell, so
 -      // bring up the dialog.
 -      if (currentColor.isSimpleColour())
 -      {
 -        // bring up simple color chooser
 -        button.setBackground(currentColor.getColour());
 -        colorChooser.setColor(currentColor.getColour());
 -        dialog.setVisible(true);
 -      }
 -      else
 -      {
 -        // bring up graduated chooser.
 -        chooser = new FeatureColourChooser(me.fr, type);
 -        chooser.setRequestFocusEnabled(true);
 -        chooser.requestFocus();
 -        chooser.addActionListener(this);
 -      }
 -      // Make the renderer reappear.
 -      fireEditingStopped();
 -
 -    }
 -    else
 -    { // User pressed dialog's "OK" button.
 -      if (currentColor.isSimpleColour())
 -      {
 -        currentColor = new FeatureColour(colorChooser.getColor());
 -      }
 -      else
 -      {
 -        currentColor = chooser.getLastColour();
 -      }
 -      me.table.setValueAt(getCellEditorValue(), selectedRow, 1);
 -      fireEditingStopped();
 -      me.table.validate();
 -    }
 -  }
 -
 -  // Implement the one CellEditor method that AbstractCellEditor doesn't.
 -  @Override
 -  public Object getCellEditorValue()
 -  {
 -    return currentColor;
 -  }
 -
 -  // Implement the one method defined by TableCellEditor.
 -  @Override
 -  public Component getTableCellEditorComponent(JTable table, Object value,
 -          boolean isSelected, int row, int column)
 -  {
 -    currentColor = (FeatureColourI) value;
 -    this.selectedRow = row;
 -    type = me.table.getValueAt(row, 0).toString();
 -    button.setOpaque(true);
 -    button.setBackground(me.getBackground());
 -    if (!currentColor.isSimpleColour())
 -    {
 -      JLabel btn = new JLabel();
 -      btn.setSize(button.getSize());
 -      FeatureSettings.renderGraduatedColor(btn, currentColor);
 -      button.setBackground(btn.getBackground());
 -      button.setIcon(btn.getIcon());
 -      button.setText(btn.getText());
 -    }
 -    else
 -    {
 -      button.setText("");
 -      button.setIcon(null);
 -      button.setBackground(currentColor.getColour());
 -    }
 -    return button;
 -  }
 -}
@@@ -37,10 -37,6 +37,10 @@@ import jalview.datamodel.SequenceGroup
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.StructureViewerModel;
  import jalview.datamodel.StructureViewerModel.StructureData;
 +import jalview.datamodel.features.FeatureMatcher;
 +import jalview.datamodel.features.FeatureMatcherI;
 +import jalview.datamodel.features.FeatureMatcherSet;
 +import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.ext.varna.RnaModel;
  import jalview.gui.StructureViewer.ViewerType;
  import jalview.io.DataSourceType;
@@@ -52,8 -48,6 +52,8 @@@ import jalview.schemabinding.version2.A
  import jalview.schemabinding.version2.AnnotationColours;
  import jalview.schemabinding.version2.AnnotationElement;
  import jalview.schemabinding.version2.CalcIdParam;
 +import jalview.schemabinding.version2.Colour;
 +import jalview.schemabinding.version2.CompoundMatcher;
  import jalview.schemabinding.version2.DBRef;
  import jalview.schemabinding.version2.Features;
  import jalview.schemabinding.version2.Group;
@@@ -66,8 -60,6 +66,8 @@@ import jalview.schemabinding.version2.M
  import jalview.schemabinding.version2.MapListTo;
  import jalview.schemabinding.version2.Mapping;
  import jalview.schemabinding.version2.MappingChoice;
 +import jalview.schemabinding.version2.MatchCondition;
 +import jalview.schemabinding.version2.MatcherSet;
  import jalview.schemabinding.version2.OtherData;
  import jalview.schemabinding.version2.PdbentryItem;
  import jalview.schemabinding.version2.Pdbids;
@@@ -83,9 -75,6 +83,9 @@@ import jalview.schemabinding.version2.T
  import jalview.schemabinding.version2.Tree;
  import jalview.schemabinding.version2.UserColours;
  import jalview.schemabinding.version2.Viewport;
 +import jalview.schemabinding.version2.types.ColourThreshTypeType;
 +import jalview.schemabinding.version2.types.FeatureMatcherByType;
 +import jalview.schemabinding.version2.types.NoValueColour;
  import jalview.schemes.AnnotationColourGradient;
  import jalview.schemes.ColourSchemeI;
  import jalview.schemes.ColourSchemeProperty;
@@@ -94,12 -83,10 +94,12 @@@ import jalview.schemes.ResiduePropertie
  import jalview.schemes.UserColourScheme;
  import jalview.structure.StructureSelectionManager;
  import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.Format;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
  import jalview.util.StringUtils;
  import jalview.util.jarInputStreamProvider;
 +import jalview.util.matcher.Condition;
  import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.ViewportRanges;
  import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
@@@ -128,7 -115,6 +128,7 @@@ import java.net.MalformedURLException
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.Arrays;
 +import java.util.Collections;
  import java.util.Enumeration;
  import java.util.HashMap;
  import java.util.HashSet;
@@@ -893,33 -879,15 +893,33 @@@ public class Jalview2XM
          }
          if (sf.otherDetails != null)
          {
 -          String key;
 -          Iterator<String> keys = sf.otherDetails.keySet().iterator();
 -          while (keys.hasNext())
 +          /*
 +           * save feature attributes, which may be simple strings or
 +           * map valued (have sub-attributes)
 +           */
 +          for (Entry<String, Object> entry : sf.otherDetails.entrySet())
            {
 -            key = keys.next();
 -            OtherData keyValue = new OtherData();
 -            keyValue.setKey(key);
 -            keyValue.setValue(sf.otherDetails.get(key).toString());
 -            features.addOtherData(keyValue);
 +            String key = entry.getKey();
 +            Object value = entry.getValue();
 +            if (value instanceof Map<?, ?>)
 +            {
 +              for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
 +                      .entrySet())
 +              {
 +                OtherData otherData = new OtherData();
 +                otherData.setKey(key);
 +                otherData.setKey2(subAttribute.getKey());
 +                otherData.setValue(subAttribute.getValue().toString());
 +                features.addOtherData(otherData);
 +              }
 +            }
 +            else
 +            {
 +              OtherData otherData = new OtherData();
 +              otherData.setKey(key);
 +              otherData.setValue(value.toString());
 +              features.addOtherData(otherData);
 +            }
            }
          }
  
        {
          jalview.schemabinding.version2.FeatureSettings fs = new jalview.schemabinding.version2.FeatureSettings();
  
 -        String[] renderOrder = ap.getSeqPanel().seqCanvas
 -                .getFeatureRenderer().getRenderOrder()
 -                .toArray(new String[0]);
 +        FeatureRenderer fr = ap.getSeqPanel().seqCanvas
 +                .getFeatureRenderer();
 +        String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
  
          Vector<String> settingsAdded = new Vector<>();
          if (renderOrder != null)
          {
            for (String featureType : renderOrder)
            {
 -            FeatureColourI fcol = ap.getSeqPanel().seqCanvas
 -                    .getFeatureRenderer().getFeatureStyle(featureType);
              Setting setting = new Setting();
              setting.setType(featureType);
 +
 +            /*
 +             * save any filter for the feature type
 +             */
 +            FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
 +            if (filter != null)  {
 +              Iterator<FeatureMatcherI> filters = filter.getMatchers().iterator();
 +              FeatureMatcherI firstFilter = filters.next();
 +              setting.setMatcherSet(Jalview2XML.marshalFilter(
 +                      firstFilter, filters, filter.isAnded()));
 +            }
 +
 +            /*
 +             * save colour scheme for the feature type
 +             */
 +            FeatureColourI fcol = fr.getFeatureStyle(featureType);
              if (!fcol.isSimpleColour())
              {
                setting.setColour(fcol.getMaxColour().getRGB());
                setting.setMin(fcol.getMin());
                setting.setMax(fcol.getMax());
                setting.setColourByLabel(fcol.isColourByLabel());
 +              if (fcol.isColourByAttribute())
 +              {
 +                setting.setAttributeName(fcol.getAttributeName());
 +              }
                setting.setAutoScale(fcol.isAutoScaled());
                setting.setThreshold(fcol.getThreshold());
 +              Color noColour = fcol.getNoColour();
 +              if (noColour == null)
 +              {
 +                setting.setNoValueColour(NoValueColour.NONE);
 +              }
 +              else if (noColour.equals(fcol.getMaxColour()))
 +              {
 +                setting.setNoValueColour(NoValueColour.MAX);
 +              }
 +              else
 +              {
 +                setting.setNoValueColour(NoValueColour.MIN);
 +              }
                // -1 = No threshold, 0 = Below, 1 = Above
                setting.setThreshstate(fcol.isAboveThreshold() ? 1
                        : (fcol.isBelowThreshold() ? 0 : -1));
  
              setting.setDisplay(
                      av.getFeaturesDisplayed().isVisible(featureType));
 -            float rorder = ap.getSeqPanel().seqCanvas.getFeatureRenderer()
 +            float rorder = fr
                      .getOrder(featureType);
              if (rorder > -1)
              {
          }
  
          // is groups actually supposed to be a map here ?
 -        Iterator<String> en = ap.getSeqPanel().seqCanvas
 -                .getFeatureRenderer().getFeatureGroups().iterator();
 +        Iterator<String> en = fr.getFeatureGroups().iterator();
          Vector<String> groupsAdded = new Vector<>();
          while (en.hasNext())
          {
            }
            Group g = new Group();
            g.setName(grp);
 -          g.setDisplay(((Boolean) ap.getSeqPanel().seqCanvas
 -                  .getFeatureRenderer().checkGroupVisibility(grp, false))
 +          g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
                            .booleanValue());
            fs.addGroup(g);
            groupsAdded.addElement(grp);
          }
          else
          {
-           ArrayList<int[]> hiddenRegions = hidden.getHiddenColumnsCopy();
-           for (int[] region : hiddenRegions)
+           Iterator<int[]> hiddenRegions = hidden.iterator();
+           while (hiddenRegions.hasNext())
            {
+             int[] region = hiddenRegions.next();
              HiddenColumns hc = new HiddenColumns();
              hc.setStart(region[0]);
              hc.setEnd(region[1]);
                      features[f].getEnd(), features[f].getScore(),
                      features[f].getFeatureGroup());
              sf.setStatus(features[f].getStatus());
 +
 +            /*
 +             * load any feature attributes - include map-valued attributes
 +             */
 +            Map<String, Map<String, String>> mapAttributes = new HashMap<>();
              for (int od = 0; od < features[f].getOtherDataCount(); od++)
              {
                OtherData keyValue = features[f].getOtherData(od);
 -              if (keyValue.getKey().startsWith("LINK"))
 +              String attributeName = keyValue.getKey();
 +              String attributeValue = keyValue.getValue();
 +              if (attributeName.startsWith("LINK"))
                {
 -                sf.addLink(keyValue.getValue());
 +                sf.addLink(attributeValue);
                }
                else
                {
 -                sf.setValue(keyValue.getKey(), keyValue.getValue());
 +                String subAttribute = keyValue.getKey2();
 +                if (subAttribute == null)
 +                {
 +                  // simple string-valued attribute
 +                  sf.setValue(attributeName, attributeValue);
 +                }
 +                else
 +                {
 +                  // attribute 'key' has sub-attribute 'key2'
 +                  if (!mapAttributes.containsKey(attributeName))
 +                  {
 +                    mapAttributes.put(attributeName, new HashMap<>());
 +                  }
 +                  mapAttributes.get(attributeName).put(subAttribute,
 +                          attributeValue);
 +                }
                }
 -
              }
 +            for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
 +                    .entrySet())
 +            {
 +              sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
 +            }
 +
              // adds feature to datasequence's feature set (since Jalview 2.10)
              al.getSequenceAt(i).addSequenceFeature(sf);
            }
        af.viewport.setShowGroupConservation(false);
      }
  
 -    // recover featre settings
 +    // recover feature settings
      if (jms.getFeatureSettings() != null)
      {
 +      FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas
 +              .getFeatureRenderer();
        FeaturesDisplayed fdi;
        af.viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
        String[] renderOrder = new String[jms.getFeatureSettings()
                .getSettingCount(); fs++)
        {
          Setting setting = jms.getFeatureSettings().getSetting(fs);
 +        String featureType = setting.getType();
 +
 +        /*
 +         * restore feature filters (if any)
 +         */
 +        MatcherSet filters = setting.getMatcherSet();
 +        if (filters != null)
 +        {
 +          FeatureMatcherSetI filter = Jalview2XML
 +                  .unmarshalFilter(featureType, filters);
 +          if (!filter.isEmpty())
 +          {
 +            fr.setFeatureFilter(featureType, filter);
 +          }
 +        }
 +
 +        /*
 +         * restore feature colour scheme
 +         */
 +        Color maxColour = new Color(setting.getColour());
          if (setting.hasMincolour())
          {
 -          FeatureColourI gc = setting.hasMin()
 -                  ? new FeatureColour(new Color(setting.getMincolour()),
 -                          new Color(setting.getColour()), setting.getMin(),
 -                          setting.getMax())
 -                  : new FeatureColour(new Color(setting.getMincolour()),
 -                          new Color(setting.getColour()), 0, 1);
 +          /*
 +           * minColour is always set unless a simple colour
 +           * (including for colour by label though it doesn't use it)
 +           */
 +          Color minColour = new Color(setting.getMincolour());
 +          Color noValueColour = minColour;
 +          NoValueColour noColour = setting.getNoValueColour();
 +          if (noColour == NoValueColour.NONE)
 +          {
 +            noValueColour = null;
 +          }
 +          else if (noColour == NoValueColour.MAX)
 +          {
 +            noValueColour = maxColour;
 +          }
 +          float min = setting.hasMin() ? setting.getMin() : 0f;
 +          float max = setting.hasMin() ? setting.getMax() : 1f;
 +          FeatureColourI gc = new FeatureColour(minColour, maxColour,
 +                  noValueColour, min, max);
 +          if (setting.getAttributeNameCount() > 0)
 +          {
 +            gc.setAttributeName(setting.getAttributeName());
 +          }
            if (setting.hasThreshold())
            {
              gc.setThreshold(setting.getThreshold());
              gc.setColourByLabel(setting.getColourByLabel());
            }
            // and put in the feature colour table.
 -          featureColours.put(setting.getType(), gc);
 +          featureColours.put(featureType, gc);
          }
          else
          {
 -          featureColours.put(setting.getType(),
 -                  new FeatureColour(new Color(setting.getColour())));
 +          featureColours.put(featureType,
 +                  new FeatureColour(maxColour));
          }
 -        renderOrder[fs] = setting.getType();
 +        renderOrder[fs] = featureType;
          if (setting.hasOrder())
          {
 -          featureOrder.put(setting.getType(), setting.getOrder());
 +          featureOrder.put(featureType, setting.getOrder());
          }
          else
          {
 -          featureOrder.put(setting.getType(), new Float(
 +          featureOrder.put(featureType, new Float(
                    fs / jms.getFeatureSettings().getSettingCount()));
          }
          if (setting.getDisplay())
          {
 -          fdi.setVisible(setting.getType());
 +          fdi.setVisible(featureType);
          }
        }
        Map<String, Boolean> fgtable = new Hashtable<>();
        // jms.getFeatureSettings().getTransparency() : 0.0, featureOrder);
        FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
                fgtable, featureColours, 1.0f, featureOrder);
 -      af.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
 -              .transferSettings(frs);
 -
 +      fr.transferSettings(frs);
      }
  
      if (view.getHiddenColumnsCount() > 0)
  
      if (this.frefedSequence == null)
      {
 -      frefedSequence = new Vector<SeqFref>();
 +      frefedSequence = new Vector<>();
      }
  
      viewportsAdded.clear();
    {
      return counter++;
    }
 +
 +  /**
 +   * Populates an XML model of the feature colour scheme for one feature type
 +   * 
 +   * @param featureType
 +   * @param fcol
 +   * @return
 +   */
 +  protected static jalview.schemabinding.version2.Colour marshalColour(
 +          String featureType, FeatureColourI fcol)
 +  {
 +    jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
 +    if (fcol.isSimpleColour())
 +    {
 +      col.setRGB(Format.getHexString(fcol.getColour()));
 +    }
 +    else
 +    {
 +      col.setRGB(Format.getHexString(fcol.getMaxColour()));
 +      col.setMin(fcol.getMin());
 +      col.setMax(fcol.getMax());
 +      col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour()));
 +      col.setAutoScale(fcol.isAutoScaled());
 +      col.setThreshold(fcol.getThreshold());
 +      col.setColourByLabel(fcol.isColourByLabel());
 +      col.setThreshType(fcol.isAboveThreshold() ? ColourThreshTypeType.ABOVE
 +              : (fcol.isBelowThreshold() ? ColourThreshTypeType.BELOW
 +                      : ColourThreshTypeType.NONE));
 +      if (fcol.isColourByAttribute())
 +      {
 +        col.setAttributeName(fcol.getAttributeName());
 +      }
 +      Color noColour = fcol.getNoColour();
 +      if (noColour == null)
 +      {
 +        col.setNoValueColour(NoValueColour.NONE);
 +      }
 +      else if (noColour == fcol.getMaxColour())
 +      {
 +        col.setNoValueColour(NoValueColour.MAX);
 +      }
 +      else
 +      {
 +        col.setNoValueColour(NoValueColour.MIN);
 +      }
 +    }
 +    col.setName(featureType);
 +    return col;
 +  }
 +
 +  /**
 +   * Populates an XML model of the feature filter(s) for one feature type
 +   * 
 +   * @param firstMatcher
 +   *          the first (or only) match condition)
 +   * @param filter
 +   *          remaining match conditions (if any)
 +   * @param and
 +   *          if true, conditions are and-ed, else or-ed
 +   */
 +  protected static MatcherSet marshalFilter(FeatureMatcherI firstMatcher,
 +          Iterator<FeatureMatcherI> filters, boolean and)
 +  {
 +    MatcherSet result = new MatcherSet();
 +  
 +    if (filters.hasNext())
 +    {
 +      /*
 +       * compound matcher
 +       */
 +      CompoundMatcher compound = new CompoundMatcher();
 +      compound.setAnd(and);
 +      MatcherSet matcher1 = marshalFilter(firstMatcher,
 +              Collections.emptyIterator(), and);
 +      compound.addMatcherSet(matcher1);
 +      FeatureMatcherI nextMatcher = filters.next();
 +      MatcherSet matcher2 = marshalFilter(nextMatcher, filters, and);
 +      compound.addMatcherSet(matcher2);
 +      result.setCompoundMatcher(compound);
 +    }
 +    else
 +    {
 +      /*
 +       * single condition matcher
 +       */
 +      MatchCondition matcherModel = new MatchCondition();
 +      matcherModel.setCondition(
 +              firstMatcher.getMatcher().getCondition().getStableName());
 +      matcherModel.setValue(firstMatcher.getMatcher().getPattern());
 +      if (firstMatcher.isByAttribute())
 +      {
 +        matcherModel.setBy(FeatureMatcherByType.BYATTRIBUTE);
 +        matcherModel.setAttributeName(firstMatcher.getAttribute());
 +      }
 +      else if (firstMatcher.isByLabel())
 +      {
 +        matcherModel.setBy(FeatureMatcherByType.BYLABEL);
 +      }
 +      else if (firstMatcher.isByScore())
 +      {
 +        matcherModel.setBy(FeatureMatcherByType.BYSCORE);
 +      }
 +      result.setMatchCondition(matcherModel);
 +    }
 +  
 +    return result;
 +  }
 +
 +  /**
 +   * Loads one XML model of a feature filter to a Jalview object
 +   * 
 +   * @param featureType
 +   * @param matcherSetModel
 +   * @return
 +   */
 +  protected static FeatureMatcherSetI unmarshalFilter(
 +          String featureType, MatcherSet matcherSetModel)
 +  {
 +    FeatureMatcherSetI result = new FeatureMatcherSet();
 +    try
 +    {
 +      unmarshalFilterConditions(result, matcherSetModel, true);
 +    } catch (IllegalStateException e)
 +    {
 +      // mixing AND and OR conditions perhaps
 +      System.err.println(
 +              String.format("Error reading filter conditions for '%s': %s",
 +                      featureType, e.getMessage()));
 +      // return as much as was parsed up to the error
 +    }
 +  
 +    return result;
 +  }
 +
 +  /**
 +   * Adds feature match conditions to matcherSet as unmarshalled from XML
 +   * (possibly recursively for compound conditions)
 +   * 
 +   * @param matcherSet
 +   * @param matcherSetModel
 +   * @param and
 +   *          if true, multiple conditions are AND-ed, else they are OR-ed
 +   * @throws IllegalStateException
 +   *           if AND and OR conditions are mixed
 +   */
 +  protected static void unmarshalFilterConditions(
 +          FeatureMatcherSetI matcherSet, MatcherSet matcherSetModel,
 +          boolean and)
 +  {
 +    MatchCondition mc = matcherSetModel.getMatchCondition();
 +    if (mc != null)
 +    {
 +      /*
 +       * single condition
 +       */
 +      FeatureMatcherByType filterBy = mc.getBy();
 +      Condition cond = Condition.fromString(mc.getCondition());
 +      String pattern = mc.getValue();
 +      FeatureMatcherI matchCondition = null;
 +      if (filterBy == FeatureMatcherByType.BYLABEL)
 +      {
 +        matchCondition = FeatureMatcher.byLabel(cond, pattern);
 +      }
 +      else if (filterBy == FeatureMatcherByType.BYSCORE)
 +      {
 +        matchCondition = FeatureMatcher.byScore(cond, pattern);
 +  
 +      }
 +      else if (filterBy == FeatureMatcherByType.BYATTRIBUTE)
 +      {
 +        String[] attNames = mc.getAttributeName();
 +        matchCondition = FeatureMatcher.byAttribute(cond, pattern,
 +                attNames);
 +      }
 +  
 +      /*
 +       * note this throws IllegalStateException if AND-ing to a 
 +       * previously OR-ed compound condition, or vice versa
 +       */
 +      if (and)
 +      {
 +        matcherSet.and(matchCondition);
 +      }
 +      else
 +      {
 +        matcherSet.or(matchCondition);
 +      }
 +    }
 +    else
 +    {
 +      /*
 +       * compound condition
 +       */
 +      MatcherSet[] matchers = matcherSetModel.getCompoundMatcher()
 +              .getMatcherSet();
 +      boolean anded = matcherSetModel.getCompoundMatcher().getAnd();
 +      if (matchers.length == 2)
 +      {
 +        unmarshalFilterConditions(matcherSet, matchers[0], anded);
 +        unmarshalFilterConditions(matcherSet, matchers[1], anded);
 +      }
 +      else
 +      {
 +        System.err.println("Malformed compound filter condition");
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Loads one XML model of a feature colour to a Jalview object
 +   * 
 +   * @param colourModel
 +   * @return
 +   */
 +  protected static FeatureColourI unmarshalColour(
 +          jalview.schemabinding.version2.Colour colourModel)
 +  {
 +    FeatureColourI colour = null;
 +  
 +    if (colourModel.hasMax())
 +    {
 +      Color mincol = null;
 +      Color maxcol = null;
 +      Color noValueColour = null;
 +  
 +      try
 +      {
 +        mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
 +        maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
 +      } catch (Exception e)
 +      {
 +        Cache.log.warn("Couldn't parse out graduated feature color.", e);
 +      }
 +  
 +      NoValueColour noCol = colourModel.getNoValueColour();
 +      if (noCol == NoValueColour.MIN)
 +      {
 +        noValueColour = mincol;
 +      }
 +      else if (noCol == NoValueColour.MAX)
 +      {
 +        noValueColour = maxcol;
 +      }
 +  
 +      colour = new FeatureColour(mincol, maxcol, noValueColour,
 +              colourModel.getMin(),
 +              colourModel.getMax());
 +      String[] attributes = colourModel.getAttributeName();
 +      if (attributes != null && attributes.length > 0)
 +      {
 +        colour.setAttributeName(attributes);
 +      }
 +      if (colourModel.hasAutoScale())
 +      {
 +        colour.setAutoScaled(colourModel.getAutoScale());
 +      }
 +      if (colourModel.hasColourByLabel())
 +      {
 +        colour.setColourByLabel(colourModel.getColourByLabel());
 +      }
 +      if (colourModel.hasThreshold())
 +      {
 +        colour.setThreshold(colourModel.getThreshold());
 +      }
 +      ColourThreshTypeType ttyp = colourModel.getThreshType();
 +      if (ttyp != null)
 +      {
 +        if (ttyp == ColourThreshTypeType.ABOVE)
 +        {
 +          colour.setAboveThreshold(true);
 +        }
 +        else if (ttyp == ColourThreshTypeType.BELOW)
 +        {
 +          colour.setBelowThreshold(true);
 +        }
 +      }
 +    }
 +    else
 +    {
 +      Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
 +      colour = new FeatureColour(color);
 +    }
 +  
 +    return colour;
 +  }
  }
@@@ -34,6 -34,7 +34,6 @@@ import jalview.datamodel.Annotation
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.PDBEntry;
 -import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
@@@ -49,7 -50,6 +49,7 @@@ import jalview.schemes.PIDColourScheme
  import jalview.util.GroupUrlLink;
  import jalview.util.GroupUrlLink.UrlStringTooLongException;
  import jalview.util.MessageManager;
 +import jalview.util.StringUtils;
  import jalview.util.UrlLink;
  
  import java.awt.Color;
@@@ -176,31 -176,25 +176,31 @@@ public class PopupMenu extends JPopupMe
     * Creates a new PopupMenu object.
     * 
     * @param ap
 -   *          DOCUMENT ME!
     * @param seq
 -   *          DOCUMENT ME!
 +   * @param features
 +   *          non-positional features (for seq not null), or positional features
 +   *          at residue (for seq equal to null)
     */
 -  public PopupMenu(final AlignmentPanel ap, Sequence seq,
 -          List<String> links)
 +  public PopupMenu(final AlignmentPanel ap, SequenceI seq,
 +          List<SequenceFeature> features)
    {
 -    this(ap, seq, links, null);
 +    this(ap, seq, features, null);
    }
  
    /**
 +   * Constructor
     * 
 -   * @param ap
 +   * @param alignPanel
     * @param seq
 -   * @param links
 +   *          the sequence under the cursor if in the Id panel, null if in the
 +   *          sequence panel
 +   * @param features
 +   *          non-positional features if in the Id panel, features at the
 +   *          clicked residue if in the sequence panel
     * @param groupLinks
     */
 -  public PopupMenu(final AlignmentPanel ap, final SequenceI seq,
 -          List<String> links, List<String> groupLinks)
 +  public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
 +          List<SequenceFeature> features, List<String> groupLinks)
    {
      // /////////////////////////////////////////////////////////
      // If this is activated from the sequence panel, the user may want to
      //
      // If from the IDPanel, we must display the sequence menu
      // ////////////////////////////////////////////////////////
 -    this.ap = ap;
 +    this.ap = alignPanel;
      sequence = seq;
  
      for (String ff : FileFormats.getInstance().getWritableFormats(true))
      /*
       * And repeat for the current selection group (if there is one):
       */
 -    final List<SequenceI> selectedGroup = (ap.av.getSelectionGroup() == null
 +    final List<SequenceI> selectedGroup = (alignPanel.av.getSelectionGroup() == null
              ? Collections.<SequenceI> emptyList()
 -            : ap.av.getSelectionGroup().getSequences());
 +            : alignPanel.av.getSelectionGroup().getSequences());
      buildAnnotationTypesMenus(groupShowAnnotationsMenu,
              groupHideAnnotationsMenu, selectedGroup);
      configureReferenceAnnotationsMenu(groupAddReferenceAnnotations,
      if (seq != null)
      {
        sequenceMenu.setText(sequence.getName());
 -      if (seq == ap.av.getAlignment().getSeqrep())
 +      if (seq == alignPanel.av.getAlignment().getSeqrep())
        {
          makeReferenceSeq.setText(
                  MessageManager.getString("action.unmark_as_reference"));
                  MessageManager.getString("action.set_as_reference"));
        }
  
 -      if (!ap.av.getAlignment().isNucleotide())
 +      if (!alignPanel.av.getAlignment().isNucleotide())
        {
          remove(rnaStructureMenu);
        }
           * add menu items to 2D-render any alignment or sequence secondary
           * structure annotation
           */
 -        AlignmentAnnotation[] aas = ap.av.getAlignment()
 +        AlignmentAnnotation[] aas = alignPanel.av.getAlignment()
                  .getAlignmentAnnotation();
          if (aas != null)
          {
                  @Override
                  public void actionPerformed(ActionEvent e)
                  {
 -                  new AppVarna(seq, aa, ap);
 +                  new AppVarna(seq, aa, alignPanel);
                  }
                });
                rnaStructureMenu.add(menuItem);
                  public void actionPerformed(ActionEvent e)
                  {
                    // TODO: VARNA does'nt print gaps in the sequence
 -                  new AppVarna(seq, aa, ap);
 +                  new AppVarna(seq, aa, alignPanel);
                  }
                });
                rnaStructureMenu.add(menuItem);
        });
        add(menuItem);
  
 -      if (ap.av.getSelectionGroup() != null
 -              && ap.av.getSelectionGroup().getSize() > 1)
 +      if (alignPanel.av.getSelectionGroup() != null
 +              && alignPanel.av.getSelectionGroup().getSize() > 1)
        {
          menuItem = new JMenuItem(MessageManager
                  .formatMessage("label.represent_group_with", new Object[]
          sequenceMenu.add(menuItem);
        }
  
 -      if (ap.av.hasHiddenRows())
 +      if (alignPanel.av.hasHiddenRows())
        {
 -        final int index = ap.av.getAlignment().findIndex(seq);
 +        final int index = alignPanel.av.getAlignment().findIndex(seq);
  
 -        if (ap.av.adjustForHiddenSeqs(index)
 -                - ap.av.adjustForHiddenSeqs(index - 1) > 1)
 +        if (alignPanel.av.adjustForHiddenSeqs(index)
 +                - alignPanel.av.adjustForHiddenSeqs(index - 1) > 1)
          {
            menuItem = new JMenuItem(
                    MessageManager.getString("action.reveal_sequences"));
              @Override
              public void actionPerformed(ActionEvent e)
              {
 -              ap.av.showSequence(index);
 -              if (ap.overviewPanel != null)
 +              alignPanel.av.showSequence(index);
 +              if (alignPanel.overviewPanel != null)
                {
 -                ap.overviewPanel.updateOverviewImage();
 +                alignPanel.overviewPanel.updateOverviewImage();
                }
              }
            });
        }
      }
      // for the case when no sequences are even visible
 -    if (ap.av.hasHiddenRows())
 +    if (alignPanel.av.hasHiddenRows())
      {
        {
          menuItem = new JMenuItem(
            @Override
            public void actionPerformed(ActionEvent e)
            {
 -            ap.av.showAllHiddenSeqs();
 -            if (ap.overviewPanel != null)
 +            alignPanel.av.showAllHiddenSeqs();
 +            if (alignPanel.overviewPanel != null)
              {
 -              ap.overviewPanel.updateOverviewImage();
 +              alignPanel.overviewPanel.updateOverviewImage();
              }
            }
          });
        }
      }
  
 -    SequenceGroup sg = ap.av.getSelectionGroup();
 +    SequenceGroup sg = alignPanel.av.getSelectionGroup();
      boolean isDefinedGroup = (sg != null)
 -            ? ap.av.getAlignment().getGroups().contains(sg)
 +            ? alignPanel.av.getAlignment().getGroups().contains(sg)
              : false;
  
      if (sg != null && sg.getSize() > 0)
        Hashtable<String, PDBEntry> pdbe = new Hashtable<>(), reppdb = new Hashtable<>();
  
        SequenceI sqass = null;
 -      for (SequenceI sq : ap.av.getSequenceSelection())
 +      for (SequenceI sq : alignPanel.av.getSequenceSelection())
        {
          Vector<PDBEntry> pes = sq.getDatasetSequence().getAllPDBEntries();
          if (pes != null && pes.size() > 0)
        rnaStructureMenu.setVisible(false);
      }
  
 -    if (links != null && links.size() > 0)
 +    addLinks(seq, features);
 +
 +    if (seq == null)
 +    {
 +      addFeatureDetails(features);
 +    }
 +  }
 +
 +  /**
 +   * Add a link to show feature details for each sequence feature
 +   * 
 +   * @param features
 +   */
 +  protected void addFeatureDetails(List<SequenceFeature> features)
 +  {
 +    if (features == null || features.isEmpty())
      {
 -      addFeatureLinks(seq, links);
 +      return;
      }
 +    JMenu details = new JMenu(
 +            MessageManager.getString("label.feature_details"));
 +    add(details);
 +
 +    for (final SequenceFeature sf : features)
 +    {
 +      int start = sf.getBegin();
 +      int end = sf.getEnd();
 +      String desc = null;
 +      if (start == end)
 +      {
 +        desc = String.format("%s %d", sf.getType(), start);
 +      }
 +      else
 +      {
 +        desc = String.format("%s %d-%d", sf.getType(), start, end);
 +      }
 +      String tooltip = desc;
 +      String description = sf.getDescription();
 +      if (description != null)
 +      {
 +        description = StringUtils.stripHtmlTags(description);
 +        if (description.length() > 12)
 +        {
 +          desc = desc + " " + description.substring(0, 12) + "..";
 +        }
 +        else
 +        {
 +          desc = desc + " " + description;
 +        }
 +        tooltip = tooltip + " " + description;
 +      }
 +      if (sf.getFeatureGroup() != null)
 +      {
 +        tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
 +      }
 +      JMenuItem item = new JMenuItem(desc);
 +      item.setToolTipText(tooltip);
 +      item.addActionListener(new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent e)
 +        {
 +          showFeatureDetails(sf);
 +        }
 +      });
 +      details.add(item);
 +    }
 +  }
 +
 +  /**
 +   * Opens a panel showing a text report of feature dteails
 +   * 
 +   * @param sf
 +   */
 +  protected void showFeatureDetails(SequenceFeature sf)
 +  {
 +    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
 +    // it appears Java's CSS does not support border-collaps :-(
 +    cap.addStylesheetRule("table { border-collapse: collapse;}");
 +    cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
 +    cap.setText(sf.getDetailsReport());
 +
 +    Desktop.addInternalFrame(cap,
 +            MessageManager.getString("label.feature_details"), 500, 500);
    }
  
    /**
     * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided.
 +   * When seq is not null, these are links for the sequence id, which may be to
 +   * external web sites for the sequence accession, and/or links embedded in
 +   * non-positional features. When seq is null, only links embedded in the
 +   * provided features are added.
     * 
     * @param seq
 -   * @param links
 +   * @param features
     */
 -  void addFeatureLinks(final SequenceI seq, List<String> links)
 +  void addLinks(final SequenceI seq, List<SequenceFeature> features)
    {
      JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
 +
 +    List<String> nlinks = null;
 +    if (seq != null)
 +    {
 +      nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
 +    }
 +    else
 +    {
 +      nlinks = new ArrayList<>();
 +    }
 +
 +    if (features != null)
 +    {
 +      for (SequenceFeature sf : features)
 +      {
 +        if (sf.links != null)
 +        {
 +          for (String link : sf.links)
 +          {
 +            nlinks.add(link);
 +          }
 +        }
 +      }
 +    }
 +
      Map<String, List<String>> linkset = new LinkedHashMap<>();
  
 -    for (String link : links)
 +    for (String link : nlinks)
      {
        UrlLink urlLink = null;
        try
  
      addshowLinks(linkMenu, linkset.values());
  
 -    // disable link menu if there are no valid entries
 +    // only add link menu if it has entries
      if (linkMenu.getItemCount() > 0)
      {
 -      linkMenu.setEnabled(true);
 -    }
 -    else
 -    {
 -      linkMenu.setEnabled(false);
 -    }
 -
 -    if (sequence != null)
 -    {
 -      sequenceMenu.add(linkMenu);
 -    }
 -    else
 -    {
 -      add(linkMenu);
 +      if (sequence != null)
 +      {
 +        sequenceMenu.add(linkMenu);
 +      }
 +      else
 +      {
 +        add(linkMenu);
 +      }
      }
 -
    }
  
    /**
  
    protected void hideInsertions_actionPerformed(ActionEvent actionEvent)
    {
-     HiddenColumns hidden = new HiddenColumns();
-     BitSet inserts = new BitSet(), mask = new BitSet();
-     // set mask to preserve existing hidden columns outside selected group
-     if (ap.av.hasHiddenColumns())
-     {
-       ap.av.getAlignment().getHiddenColumns().markHiddenRegions(mask);
-     }
+     HiddenColumns hidden = ap.av.getAlignment().getHiddenColumns();
+     BitSet inserts = new BitSet();
  
      boolean markedPopup = false;
      // mark inserts in current selection
      {
        // mark just the columns in the selection group to be hidden
        inserts.set(ap.av.getSelectionGroup().getStartRes(),
-               ap.av.getSelectionGroup().getEndRes() + 1);
-       // and clear that part of the mask
-       mask.andNot(inserts);
+               ap.av.getSelectionGroup().getEndRes() + 1); // TODO why +1?
  
        // now clear columns without gaps
        for (SequenceI sq : ap.av.getSelectionGroup().getSequences())
          }
          inserts.and(sq.getInsertionsAsBits());
        }
-     }
-     else
-     {
-       // initially, mark all columns to be hidden
-       inserts.set(0, ap.av.getAlignment().getWidth());
-       // and clear out old hidden regions completely
-       mask.clear();
+       hidden.clearAndHideColumns(inserts, ap.av.getSelectionGroup().getStartRes(),
+               ap.av.getSelectionGroup().getEndRes());
      }
  
      // now mark for sequence under popup if we haven't already done it
-     if (!markedPopup && sequence != null)
+     else if (!markedPopup && sequence != null)
      {
-       inserts.and(sequence.getInsertionsAsBits());
-     }
+       inserts.or(sequence.getInsertionsAsBits());
  
-     // finally, preserve hidden regions outside selection
-     inserts.or(mask);
-     // and set hidden columns accordingly
-     hidden.hideMarkedBits(inserts);
-     ap.av.getAlignment().setHiddenColumns(hidden);
+       // and set hidden columns accordingly
+       hidden.hideColumns(inserts);
+     }
      refresh();
    }
  
                new Object[]
                { seq.getDisplayId(true) }) + "</h2></p><p>");
        new SequenceAnnotationReport(null).createSequenceAnnotationReport(
 -              contents, seq, true, true,
 -              (ap.getSeqPanel().seqCanvas.fr != null)
 -                      ? ap.getSeqPanel().seqCanvas.fr.getMinMax()
 -                      : null);
 +              contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
        contents.append("</p>");
      }
      cap.setText("<html>" + contents.toString() + "</html>");
@@@ -59,6 -59,7 +59,6 @@@ import java.awt.event.MouseListener
  import java.awt.event.MouseMotionListener;
  import java.awt.event.MouseWheelEvent;
  import java.awt.event.MouseWheelListener;
 -import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
@@@ -75,11 -76,12 +75,11 @@@ import javax.swing.ToolTipManager
  public class SeqPanel extends JPanel
          implements MouseListener, MouseMotionListener, MouseWheelListener,
          SequenceListener, SelectionListener
 -
  {
 -  /** DOCUMENT ME!! */
 +  private static final int MAX_TOOLTIP_LENGTH = 300;
 +
    public SeqCanvas seqCanvas;
  
 -  /** DOCUMENT ME!! */
    public AlignmentPanel ap;
  
    /*
    SearchResultsI lastSearchResults;
  
    /**
 -   * Creates a new SeqPanel object.
 +   * Creates a new SeqPanel object
     * 
 -   * @param avp
 -   *          DOCUMENT ME!
 -   * @param p
 -   *          DOCUMENT ME!
 +   * @param viewport
 +   * @param alignPanel
     */
 -  public SeqPanel(AlignViewport av, AlignmentPanel ap)
 +  public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
    {
      linkImageURL = getClass().getResource("/images/link.gif");
      seqARep = new SequenceAnnotationReport(linkImageURL.toString());
      ToolTipManager.sharedInstance().registerComponent(this);
      ToolTipManager.sharedInstance().setInitialDelay(0);
      ToolTipManager.sharedInstance().setDismissDelay(10000);
 -    this.av = av;
 +    this.av = viewport;
      setBackground(Color.white);
  
 -    seqCanvas = new SeqCanvas(ap);
 +    seqCanvas = new SeqCanvas(alignPanel);
      setLayout(new BorderLayout());
      add(seqCanvas, BorderLayout.CENTER);
  
 -    this.ap = ap;
 +    this.ap = alignPanel;
  
 -    if (!av.isDataset())
 +    if (!viewport.isDataset())
      {
        addMouseMotionListener(this);
        addMouseListener(this);
        addMouseWheelListener(this);
 -      ssm = av.getStructureSelectionManager();
 +      ssm = viewport.getStructureSelectionManager();
        ssm.addStructureViewerListener(this);
        ssm.addSelectionListener(this);
      }
      if (av.hasHiddenColumns())
      {
        res = av.getAlignment().getHiddenColumns()
-               .adjustForHiddenColumns(res);
+               .visibleToAbsoluteColumn(res);
      }
  
      return res;
        int original = seqCanvas.cursorX - dx;
        int maxWidth = av.getAlignment().getWidth();
  
-       // TODO: once JAL-2759 is ready, change this loop to something more
-       // efficient
-       while (!hidden.isVisible(seqCanvas.cursorX)
-               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0
-               && dx != 0)
+       if (!hidden.isVisible(seqCanvas.cursorX))
        {
-         seqCanvas.cursorX += dx;
+         int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
+         int[] region = hidden.getRegionWithEdgeAtRes(visx);
+         if (region != null) // just in case
+         {
+           if (dx == 1)
+           {
+             // moving right
+             seqCanvas.cursorX = region[1] + 1;
+           }
+           else if (dx == -1)
+           {
+             // moving left
+             seqCanvas.cursorX = region[0] - 1;
+           }
+         }
+         seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
        }
  
        if (seqCanvas.cursorX >= maxWidth
        {
          // scrollToWrappedVisible expects x-value to have hidden cols subtracted
          int x = av.getAlignment().getHiddenColumns()
-                 .findColumnPosition(seqCanvas.cursorX);
+                 .absoluteToVisibleColumn(seqCanvas.cursorX);
          av.getRanges().scrollToWrappedVisible(x);
        }
        else
        List<SequenceFeature> features = ap.getFeatureRenderer()
                .findFeaturesAtColumn(sequence, column + 1);
        seqARep.appendFeatures(tooltipText, pos, features,
 -              this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
 +              this.ap.getSeqPanel().seqCanvas.fr);
      }
      if (tooltipText.length() == 6) // <html>
      {
      }
      else
      {
 +      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
 +      {
 +        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
 +        tooltipText.append("...");
 +      }
        String textString = tooltipText.toString();
        if (lastTooltip == null || !lastTooltip.equals(textString))
        {
      {
        fixedColumns = true;
        int y1 = av.getAlignment().getHiddenColumns()
-               .getHiddenBoundaryLeft(startres);
+               .getNextHiddenBoundary(true, startres);
        int y2 = av.getAlignment().getHiddenColumns()
-               .getHiddenBoundaryRight(startres);
+               .getNextHiddenBoundary(false, startres);
  
        if ((insertGap && startres > y1 && lastres < y1)
                || (!insertGap && startres < y2 && lastres > y2))
            if (sg.getSize() == av.getAlignment().getHeight())
            {
              if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                     .getHiddenColumns().getHiddenBoundaryRight(startres)))
+                     .getHiddenColumns()
+                     .getNextHiddenBoundary(false, startres)))
              {
                endEditing();
                return;
      final int column = findColumn(evt);
      final int seq = findSeq(evt);
      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 -    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
 +    List<SequenceFeature> features = ap.getFeatureRenderer()
              .findFeaturesAtColumn(sequence, column + 1);
 -    List<String> links = new ArrayList<>();
 -    for (SequenceFeature sf : allFeatures)
 -    {
 -      if (sf.links != null)
 -      {
 -        for (String link : sf.links)
 -        {
 -          links.add(link);
 -        }
 -      }
 -    }
  
 -    PopupMenu pop = new PopupMenu(ap, null, links);
 +    PopupMenu pop = new PopupMenu(ap, null, features);
      pop.show(this, evt.getX(), evt.getY());
    }
  
@@@ -542,9 -542,11 +542,11 @@@ public final class MappingUtil
                toSequences, fromGapChar);
      }
  
-     for (int[] hidden : hiddencols.getHiddenColumnsCopy())
+     Iterator<int[]> regions = hiddencols.iterator();
+     while (regions.hasNext())
      {
-       mapHiddenColumns(hidden, codonFrames, newHidden, fromSequences,
+       mapHiddenColumns(regions.next(), codonFrames, newHidden,
+               fromSequences,
                toSequences, fromGapChar);
      }
      return; // mappedColumns;
    }
  
    /**
 +   * Answers true if range's start-end positions include those of queryRange,
 +   * where either range might be in reverse direction, else false
 +   * 
 +   * @param range
 +   *          a start-end range
 +   * @param queryRange
 +   *          a candidate subrange of range (start2-end2)
 +   * @return
 +   */
 +  public static boolean rangeContains(int[] range, int[] queryRange)
 +  {
 +    if (range == null || queryRange == null || range.length != 2
 +            || queryRange.length != 2)
 +    {
 +      /*
 +       * invalid arguments
 +       */
 +      return false;
 +    }
 +
 +    int min = Math.min(range[0], range[1]);
 +    int max = Math.max(range[0], range[1]);
 +  
 +    return (min <= queryRange[0] && max >= queryRange[0]
 +            && min <= queryRange[1] && max >= queryRange[1]);
 +  }
 +
 +  /**
     * Removes the specified number of positions from the given ranges. Provided
     * to allow a stop codon to be stripped from a CDS sequence so that it matches
     * the peptide translation length.
@@@ -34,7 -34,6 +34,7 @@@ import jalview.datamodel.AlignmentAnnot
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
  import jalview.datamodel.DBRefEntry;
 +import jalview.datamodel.GeneLociI;
  import jalview.datamodel.Mapping;
  import jalview.datamodel.SearchResultMatchI;
  import jalview.datamodel.SearchResultsI;
@@@ -48,6 -47,7 +48,7 @@@ import jalview.io.DataSourceType
  import jalview.io.FileFormat;
  import jalview.io.FileFormatI;
  import jalview.io.FormatAdapter;
+ import jalview.io.gff.SequenceOntologyI;
  import jalview.util.MapList;
  import jalview.util.MappingUtils;
  
@@@ -64,8 -64,6 +65,8 @@@ import org.testng.annotations.Test
  
  public class AlignmentUtilsTests
  {
 +  private static Sequence ts = new Sequence("short",
 +          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
@@@ -74,6 -72,9 +75,6 @@@
      JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
    }
  
 -  public static Sequence ts = new Sequence("short",
 -          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
 -
    @Test(groups = { "Functional" })
    public void testExpandContext()
    {
      dna.addCodonFrame(acf);
  
      /*
 -     * In this case, mappings originally came from matching Uniprot accessions - so need an xref on dna involving those regions. These are normally constructed from CDS annotation
 +     * In this case, mappings originally came from matching Uniprot accessions 
 +     * - so need an xref on dna involving those regions. 
 +     * These are normally constructed from CDS annotation
       */
      DBRefEntry dna1xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep1",
              new Mapping(mapfordna1));
 -    dna1.getDatasetSequence().addDBRef(dna1xref);
 +    dna1.addDBRef(dna1xref);
 +    assertEquals(2, dna1.getDBRefs().length); // to self and to pep1
      DBRefEntry dna2xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep2",
              new Mapping(mapfordna2));
 -    dna2.getDatasetSequence().addDBRef(dna2xref);
 +    dna2.addDBRef(dna2xref);
 +    assertEquals(2, dna2.getDBRefs().length); // to self and to pep2
  
      /*
       * execute method under test:
      assertEquals(cdsMapping.getInverse(), dbref.getMap().getMap());
  
      /*
 +     * verify cDNA has added a dbref with mapping to CDS
 +     */
 +    assertEquals(3, dna1.getDBRefs().length);
 +    DBRefEntry dbRefEntry = dna1.getDBRefs()[2];
 +    assertSame(cds1Dss, dbRefEntry.getMap().getTo());
 +    MapList dnaToCdsMapping = new MapList(new int[] { 4, 6, 10, 12 },
 +            new int[] { 1, 6 }, 1, 1);
 +    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
 +    assertEquals(3, dna2.getDBRefs().length);
 +    dbRefEntry = dna2.getDBRefs()[2];
 +    assertSame(cds2Dss, dbRefEntry.getMap().getTo());
 +    dnaToCdsMapping = new MapList(new int[] { 1, 3, 7, 9, 13, 15 },
 +            new int[] { 1, 9 }, 1, 1);
 +    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
 +
 +    /*
 +     * verify CDS has added a dbref with mapping to cDNA
 +     */
 +    assertEquals(2, cds1Dss.getDBRefs().length);
 +    dbRefEntry = cds1Dss.getDBRefs()[1];
 +    assertSame(dna1.getDatasetSequence(), dbRefEntry.getMap().getTo());
 +    MapList cdsToDnaMapping = new MapList(new int[] { 1, 6 }, new int[] {
 +        4, 6, 10, 12 }, 1, 1);
 +    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
 +    assertEquals(2, cds2Dss.getDBRefs().length);
 +    dbRefEntry = cds2Dss.getDBRefs()[1];
 +    assertSame(dna2.getDatasetSequence(), dbRefEntry.getMap().getTo());
 +    cdsToDnaMapping = new MapList(new int[] { 1, 9 }, new int[] { 1, 3, 7,
 +        9, 13, 15 }, 1, 1);
 +    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
 +
 +    /*
       * Verify mappings from CDS to peptide, cDNA to CDS, and cDNA to peptide
       * the mappings are on the shared alignment dataset
       * 6 mappings, 2*(DNA->CDS), 2*(DNA->Pep), 2*(CDS->Pep) 
      sf6.setValue("alleles", "g, a"); // should force to upper-case
      sf6.setValue("ID", "sequence_variant:rs758803216");
      dna.addSequenceFeature(sf6);
 +
      SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15,
              15, 0f, null);
      sf7.setValue("alleles", "A, T");
       * variants:
       *           GAA -> E             source: Ensembl
       *           CAA -> Q             source: dbSNP
 +     *           TAA -> STOP          source: dnSNP
       *           AAG synonymous       source: COSMIC
       *           AAT -> N             source: Ensembl
       *           ...TTC synonymous    source: dbSNP
      String ensembl = "Ensembl";
      String dbSnp = "dbSNP";
      String cosmic = "COSMIC";
 +
      SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
              0f, ensembl);
 -    sf1.setValue("alleles", "A,G"); // GAA -> E
 +    sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E
      sf1.setValue("ID", "var1.125A>G");
 +
      SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1,
              0f, dbSnp);
 -    sf2.setValue("alleles", "A,C"); // CAA -> Q
 +    sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q
      sf2.setValue("ID", "var2");
      sf2.setValue("clinical_significance", "Dodgy");
 -    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 3, 3,
 -            0f, cosmic);
 -    sf3.setValue("alleles", "A,G"); // synonymous
 +
 +    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1,
 +            0f, dbSnp);
 +    sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon
      sf3.setValue("ID", "var3");
 -    sf3.setValue("clinical_significance", "None");
 +    sf3.setValue("clinical_significance", "Bad");
 +
      SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3,
 +            0f, cosmic);
 +    sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous
 +    sf4.setValue("ID", "var4");
 +    sf4.setValue("clinical_significance", "None");
 +
 +    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3,
              0f, ensembl);
 -    sf4.setValue("alleles", "A,T"); // AAT -> N
 -    sf4.setValue("ID", "sequence_variant:var4"); // prefix gets stripped off
 -    sf4.setValue("clinical_significance", "Benign");
 -    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 6, 6,
 +    sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N
 +    sf5.setValue("ID", "sequence_variant:var5"); // prefix gets stripped off
 +    sf5.setValue("clinical_significance", "Benign");
 +
 +    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6,
              0f, dbSnp);
 -    sf5.setValue("alleles", "T,C"); // synonymous
 -    sf5.setValue("ID", "var5");
 -    sf5.setValue("clinical_significance", "Bad");
 -    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 8, 8,
 -            0f, cosmic);
 -    sf6.setValue("alleles", "C,A,G"); // CAC,CGC -> H,R
 +    sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous
      sf6.setValue("ID", "var6");
 -    sf6.setValue("clinical_significance", "Good");
 +
 +    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8,
 +            0f, cosmic);
 +    sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R
 +    sf7.setValue("ID", "var7");
 +    sf7.setValue("clinical_significance", "Good");
  
      List<DnaVariant> codon1Variants = new ArrayList<>();
      List<DnaVariant> codon2Variants = new ArrayList<>();
      List<DnaVariant> codon3Variants = new ArrayList<>();
++
      List<DnaVariant> codonVariants[] = new ArrayList[3];
      codonVariants[0] = codon1Variants;
      codonVariants[1] = codon2Variants;
       */
      codon1Variants.add(new DnaVariant("A", sf1));
      codon1Variants.add(new DnaVariant("A", sf2));
 +    codon1Variants.add(new DnaVariant("A", sf3));
      codon2Variants.add(new DnaVariant("A"));
 -    codon2Variants.add(new DnaVariant("A"));
 -    codon3Variants.add(new DnaVariant("A", sf3));
 +    // codon2Variants.add(new DnaVariant("A"));
      codon3Variants.add(new DnaVariant("A", sf4));
 +    codon3Variants.add(new DnaVariant("A", sf5));
      AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants);
  
      /*
      codon3Variants.clear();
      codon1Variants.add(new DnaVariant("T"));
      codon2Variants.add(new DnaVariant("T"));
 -    codon3Variants.add(new DnaVariant("T", sf5));
 +    codon3Variants.add(new DnaVariant("T", sf6));
      AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants);
  
      /*
      codon2Variants.clear();
      codon3Variants.clear();
      codon1Variants.add(new DnaVariant("C"));
 -    codon2Variants.add(new DnaVariant("C", sf6));
 +    codon2Variants.add(new DnaVariant("C", sf7));
      codon3Variants.add(new DnaVariant("C"));
      AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants);
  
       * verify added sequence features for
       * var1 K -> E Ensembl
       * var2 K -> Q dbSNP
 -     * var4 K -> N Ensembl
 -     * var6 P -> H COSMIC
 -     * var6 P -> R COSMIC
 +     * var3 K -> stop
 +     * var4 synonymous
 +     * var5 K -> N Ensembl
 +     * var6 synonymous
 +     * var7 P -> H COSMIC
 +     * var8 P -> R COSMIC
       */
      List<SequenceFeature> sfs = peptide.getSequenceFeatures();
      SequenceFeatures.sortFeatures(sfs, true);
 -    assertEquals(5, sfs.size());
 +    assertEquals(8, sfs.size());
  
      /*
       * features are sorted by start position ascending, but in no
       * particular order where start positions match; asserts here
       * simply match the data returned (the order is not important)
       */
 +    // AAA -> AAT -> K/N
      SequenceFeature sf = sfs.get(0);
      assertEquals(1, sf.getBegin());
      assertEquals(1, sf.getEnd());
 +    assertEquals("nonsynonymous_variant", sf.getType());
      assertEquals("p.Lys1Asn", sf.getDescription());
 -    assertEquals("var4", sf.getValue("ID"));
 +    assertEquals("var5", sf.getValue("ID"));
      assertEquals("Benign", sf.getValue("clinical_significance"));
 -    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
 +    assertEquals("ID=var5;clinical_significance=Benign",
 +            sf.getAttributes());
      assertEquals(1, sf.links.size());
      assertEquals(
 -            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
 +            "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5",
              sf.links.get(0));
      assertEquals(ensembl, sf.getFeatureGroup());
  
 +    // AAA -> CAA -> K/Q
      sf = sfs.get(1);
      assertEquals(1, sf.getBegin());
      assertEquals(1, sf.getEnd());
 +    assertEquals("nonsynonymous_variant", sf.getType());
      assertEquals("p.Lys1Gln", sf.getDescription());
      assertEquals("var2", sf.getValue("ID"));
      assertEquals("Dodgy", sf.getValue("clinical_significance"));
              sf.links.get(0));
      assertEquals(dbSnp, sf.getFeatureGroup());
  
 +    // AAA -> GAA -> K/E
      sf = sfs.get(2);
      assertEquals(1, sf.getBegin());
      assertEquals(1, sf.getEnd());
 +    assertEquals("nonsynonymous_variant", sf.getType());
      assertEquals("p.Lys1Glu", sf.getDescription());
      assertEquals("var1.125A>G", sf.getValue("ID"));
      assertNull(sf.getValue("clinical_significance"));
              sf.links.get(0));
      assertEquals(ensembl, sf.getFeatureGroup());
  
 +    // AAA -> TAA -> stop codon
      sf = sfs.get(3);
 +    assertEquals(1, sf.getBegin());
 +    assertEquals(1, sf.getEnd());
 +    assertEquals("stop_gained", sf.getType());
 +    assertEquals("Aaa/Taa", sf.getDescription());
 +    assertEquals("var3", sf.getValue("ID"));
 +    assertEquals("Bad", sf.getValue("clinical_significance"));
 +    assertEquals("ID=var3;clinical_significance=Bad", sf.getAttributes());
 +    assertEquals(1, sf.links.size());
 +    assertEquals(
 +            "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3",
 +            sf.links.get(0));
 +    assertEquals(dbSnp, sf.getFeatureGroup());
 +
 +    // AAA -> AAG synonymous
 +    sf = sfs.get(4);
 +    assertEquals(1, sf.getBegin());
 +    assertEquals(1, sf.getEnd());
 +    assertEquals("synonymous_variant", sf.getType());
 +    assertEquals("aaA/aaG", sf.getDescription());
 +    assertEquals("var4", sf.getValue("ID"));
 +    assertEquals("None", sf.getValue("clinical_significance"));
 +    assertEquals("ID=var4;clinical_significance=None", sf.getAttributes());
 +    assertEquals(1, sf.links.size());
 +    assertEquals(
 +            "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
 +            sf.links.get(0));
 +    assertEquals(cosmic, sf.getFeatureGroup());
 +
 +    // TTT -> TTC synonymous
 +    sf = sfs.get(5);
 +    assertEquals(2, sf.getBegin());
 +    assertEquals(2, sf.getEnd());
 +    assertEquals("synonymous_variant", sf.getType());
 +    assertEquals("ttT/ttC", sf.getDescription());
 +    assertEquals("var6", sf.getValue("ID"));
 +    assertNull(sf.getValue("clinical_significance"));
 +    assertEquals("ID=var6", sf.getAttributes());
 +    assertEquals(1, sf.links.size());
 +    assertEquals(
 +            "ttT/ttC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
 +            sf.links.get(0));
 +    assertEquals(dbSnp, sf.getFeatureGroup());
 +
 +    // var7 generates two distinct protein variant features (two alleles)
 +    // CCC -> CGC -> P/R
 +    sf = sfs.get(6);
      assertEquals(3, sf.getBegin());
      assertEquals(3, sf.getEnd());
 +    assertEquals("nonsynonymous_variant", sf.getType());
      assertEquals("p.Pro3Arg", sf.getDescription());
 -    assertEquals("var6", sf.getValue("ID"));
 +    assertEquals("var7", sf.getValue("ID"));
      assertEquals("Good", sf.getValue("clinical_significance"));
 -    assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
 +    assertEquals("ID=var7;clinical_significance=Good", sf.getAttributes());
      assertEquals(1, sf.links.size());
      assertEquals(
 -            "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
 +            "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
              sf.links.get(0));
      assertEquals(cosmic, sf.getFeatureGroup());
  
 -    // var5 generates two distinct protein variant features
 -    sf = sfs.get(4);
 +    // CCC -> CAC -> P/H
 +    sf = sfs.get(7);
      assertEquals(3, sf.getBegin());
      assertEquals(3, sf.getEnd());
 +    assertEquals("nonsynonymous_variant", sf.getType());
      assertEquals("p.Pro3His", sf.getDescription());
 -    assertEquals("var6", sf.getValue("ID"));
 +    assertEquals("var7", sf.getValue("ID"));
      assertEquals("Good", sf.getValue("clinical_significance"));
 -    assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
 +    assertEquals("ID=var7;clinical_significance=Good", sf.getAttributes());
      assertEquals(1, sf.links.size());
      assertEquals(
 -            "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
 +            "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
              sf.links.get(0));
      assertEquals(cosmic, sf.getFeatureGroup());
    }
      assertEquals(s_as3, uas3.getSequenceAsString());
    }
  
 +  @Test(groups = { "Functional" })
 +  public void testTransferGeneLoci()
 +  {
 +    SequenceI from = new Sequence("transcript",
 +            "aaacccgggTTTAAACCCGGGtttaaacccgggttt");
 +    SequenceI to = new Sequence("CDS", "TTTAAACCCGGG");
 +    MapList map = new MapList(new int[] { 1, 12 }, new int[] { 10, 21 }, 1,
 +            1);
 +
 +    /*
 +     * first with nothing to transfer
 +     */
 +    AlignmentUtils.transferGeneLoci(from, map, to);
 +    assertNull(to.getGeneLoci());
 +
 +    /*
 +     * next with gene loci set on 'from' sequence
 +     */
 +    int[] exons = new int[] { 100, 105, 155, 164, 210, 229 };
 +    MapList geneMap = new MapList(new int[] { 1, 36 }, exons, 1, 1);
 +    from.setGeneLoci("human", "GRCh38", "7", geneMap);
 +    AlignmentUtils.transferGeneLoci(from, map, to);
 +
 +    GeneLociI toLoci = to.getGeneLoci();
 +    assertNotNull(toLoci);
 +    // DBRefEntry constructor upper-cases 'source'
 +    assertEquals("HUMAN", toLoci.getSpeciesId());
 +    assertEquals("GRCh38", toLoci.getAssemblyId());
 +    assertEquals("7", toLoci.getChromosomeId());
 +
 +    /*
 +     * transcript 'exons' are 1-6, 7-16, 17-36
 +     * CDS 1:12 is transcript 10-21
 +     * transcript 'CDS' is 10-16, 17-21
 +     * which is 'gene' 158-164, 210-214
 +     */
 +    MapList toMap = toLoci.getMap();
 +    assertEquals(1, toMap.getFromRanges().size());
 +    assertEquals(2, toMap.getFromRanges().get(0).length);
 +    assertEquals(1, toMap.getFromRanges().get(0)[0]);
 +    assertEquals(12, toMap.getFromRanges().get(0)[1]);
 +    assertEquals(1, toMap.getToRanges().size());
 +    assertEquals(4, toMap.getToRanges().get(0).length);
 +    assertEquals(158, toMap.getToRanges().get(0)[0]);
 +    assertEquals(164, toMap.getToRanges().get(0)[1]);
 +    assertEquals(210, toMap.getToRanges().get(0)[2]);
 +    assertEquals(214, toMap.getToRanges().get(0)[3]);
 +    // or summarised as (but toString might change in future):
 +    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164, 210, 214] ]",
 +            toMap.toString());
 +
 +    /*
 +     * an existing value is not overridden 
 +     */
 +    geneMap = new MapList(new int[] { 1, 36 }, new int[] { 36, 1 }, 1, 1);
 +    from.setGeneLoci("inhuman", "GRCh37", "6", geneMap);
 +    AlignmentUtils.transferGeneLoci(from, map, to);
 +    assertEquals("GRCh38", toLoci.getAssemblyId());
 +    assertEquals("7", toLoci.getChromosomeId());
 +    toMap = toLoci.getMap();
 +    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164, 210, 214] ]",
 +            toMap.toString());
 +  }
 +
    /**
     * Tests for the method that maps nucleotide to protein based on CDS features
     */
       * Case 2: CDS 3 times length of peptide + stop codon
       * (note code does not currently check trailing codon is a stop codon)
       */
-     dna = new Sequence("dna", "AACGacgtCTCCTTGA");
+     dna = new Sequence("dna", "AACGacgtCTCCTCCC");
      dna.createDatasetSequence();
      dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
      dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 16, null));
              Arrays.deepToString(ml.getFromRanges().toArray()));
  
      /*
-      * Case 3: CDS not 3 times length of peptide - no mapping is made
+      * Case 3: CDS longer than 3 * peptide + stop codon - no mapping is made
+      */
+     dna = new Sequence("dna", "AACGacgtCTCCTTGATCA");
+     dna.createDatasetSequence();
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 19, null));
+     ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+     assertNull(ml);
+     /*
+      * Case 4: CDS shorter than 3 * peptide - no mapping is made
+      */
+     dna = new Sequence("dna", "AACGacgtCTCC");
+     dna.createDatasetSequence();
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+     dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 12, null));
+     ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+     assertNull(ml);
+     /*
+      * Case 5: CDS 3 times length of peptide + part codon - mapping is truncated
       */
      dna = new Sequence("dna", "AACGacgtCTCCTTG");
      dna.createDatasetSequence();
      dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
      dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 15, null));
      ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
-     assertNull(ml);
+     assertEquals(3, ml.getFromRatio());
+     assertEquals(1, ml.getToRatio());
+     assertEquals("[[1, 3]]",
+             Arrays.deepToString(ml.getToRanges().toArray()));
+     assertEquals("[[1, 4], [9, 13]]",
+             Arrays.deepToString(ml.getFromRanges().toArray()));
  
      /*
-      * Case 4: incomplete start codon corresponding to X in peptide
+      * Case 6: incomplete start codon corresponding to X in peptide
       */
      dna = new Sequence("dna", "ACGacgtCTCCTTGG");
      dna.createDatasetSequence();
      assertEquals("[[3, 3], [8, 12]]",
              Arrays.deepToString(ml.getFromRanges().toArray()));
    }
+   /**
+    * Tests for the method that locates the CDS sequence that has a mapping to
+    * the given protein. That is, given a transcript-to-peptide mapping, find the
+    * cds-to-peptide mapping that relates to both, and return the CDS sequence.
+    */
+   @Test
+   public void testFindCdsForProtein()
+   {
+     List<AlignedCodonFrame> mappings = new ArrayList<>();
+     AlignedCodonFrame acf1 = new AlignedCodonFrame();
+     mappings.add(acf1);
+     SequenceI dna1 = new Sequence("dna1", "cgatATcgGCTATCTATGacg");
+     dna1.createDatasetSequence();
+     // NB we currently exclude STOP codon from CDS sequences
+     // the test would need to change if this changes in future
+     SequenceI cds1 = new Sequence("cds1", "ATGCTATCT");
+     cds1.createDatasetSequence();
+     SequenceI pep1 = new Sequence("pep1", "MLS");
+     pep1.createDatasetSequence();
+     List<AlignedCodonFrame> seqMappings = new ArrayList<>();
+     MapList mapList = new MapList(
+             new int[]
+             { 5, 6, 9, 15 }, new int[] { 1, 3 }, 3, 1);
+     Mapping dnaToPeptide = new Mapping(pep1.getDatasetSequence(), mapList);
+     
+     // add dna to peptide mapping
+     seqMappings.add(acf1);
+     acf1.addMap(dna1.getDatasetSequence(), pep1.getDatasetSequence(),
+             mapList);
+     /*
+      * first case - no dna-to-CDS mapping exists - search fails
+      */
+     SequenceI seq = AlignmentUtils.findCdsForProtein(mappings, dna1,
+             seqMappings, dnaToPeptide);
+     assertNull(seq);
+     /*
+      * second case - CDS-to-peptide mapping exists but no dna-to-CDS
+      * - search fails
+      */
+     // todo this test fails if the mapping is added to acf1, not acf2
+     // need to tidy up use of lists of mappings in AlignedCodonFrame
+     AlignedCodonFrame acf2 = new AlignedCodonFrame();
+     mappings.add(acf2);
+     MapList cdsToPeptideMapping = new MapList(new int[]
+     { 1, 9 }, new int[] { 1, 3 }, 3, 1);
+     acf2.addMap(cds1.getDatasetSequence(), pep1.getDatasetSequence(),
+             cdsToPeptideMapping);
+     assertNull(AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+             dnaToPeptide));
+     /*
+      * third case - add dna-to-CDS mapping - CDS is now found!
+      */
+     MapList dnaToCdsMapping = new MapList(new int[] { 5, 6, 9, 15 },
+             new int[]
+             { 1, 9 }, 1, 1);
+     acf1.addMap(dna1.getDatasetSequence(), cds1.getDatasetSequence(),
+             dnaToCdsMapping);
+     seq = AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+             dnaToPeptide);
+     assertSame(seq, cds1.getDatasetSequence());
+   }
+   /**
+    * Tests for the method that locates the CDS sequence that has a mapping to
+    * the given protein. That is, given a transcript-to-peptide mapping, find the
+    * cds-to-peptide mapping that relates to both, and return the CDS sequence.
+    * This test is for the case where transcript and CDS are the same length.
+    */
+   @Test
+   public void testFindCdsForProtein_noUTR()
+   {
+     List<AlignedCodonFrame> mappings = new ArrayList<>();
+     AlignedCodonFrame acf1 = new AlignedCodonFrame();
+     mappings.add(acf1);
+   
+     SequenceI dna1 = new Sequence("dna1", "ATGCTATCTTAA");
+     dna1.createDatasetSequence();
+   
+     // NB we currently exclude STOP codon from CDS sequences
+     // the test would need to change if this changes in future
+     SequenceI cds1 = new Sequence("cds1", "ATGCTATCT");
+     cds1.createDatasetSequence();
+   
+     SequenceI pep1 = new Sequence("pep1", "MLS");
+     pep1.createDatasetSequence();
+     List<AlignedCodonFrame> seqMappings = new ArrayList<>();
+     MapList mapList = new MapList(
+             new int[]
+             { 1, 9 }, new int[] { 1, 3 }, 3, 1);
+     Mapping dnaToPeptide = new Mapping(pep1.getDatasetSequence(), mapList);
+     
+     // add dna to peptide mapping
+     seqMappings.add(acf1);
+     acf1.addMap(dna1.getDatasetSequence(), pep1.getDatasetSequence(),
+             mapList);
+   
+     /*
+      * first case - transcript lacks CDS features - it appears to be
+      * the CDS sequence and is returned
+      */
+     SequenceI seq = AlignmentUtils.findCdsForProtein(mappings, dna1,
+             seqMappings, dnaToPeptide);
+     assertSame(seq, dna1.getDatasetSequence());
+   
+     /*
+      * second case - transcript has CDS feature - this means it is
+      * not returned as a match for CDS (CDS sequences don't have CDS features)
+      */
+     dna1.addSequenceFeature(
+             new SequenceFeature(SequenceOntologyI.CDS, "cds", 1, 12, null));
+     seq = AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+             dnaToPeptide);
+     assertNull(seq);
+     /*
+      * third case - CDS-to-peptide mapping exists but no dna-to-CDS
+      * - search fails
+      */
+     // todo this test fails if the mapping is added to acf1, not acf2
+     // need to tidy up use of lists of mappings in AlignedCodonFrame
+     AlignedCodonFrame acf2 = new AlignedCodonFrame();
+     mappings.add(acf2);
+     MapList cdsToPeptideMapping = new MapList(new int[]
+     { 1, 9 }, new int[] { 1, 3 }, 3, 1);
+     acf2.addMap(cds1.getDatasetSequence(), pep1.getDatasetSequence(),
+             cdsToPeptideMapping);
+     assertNull(AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+             dnaToPeptide));
+   
+     /*
+      * fourth case - add dna-to-CDS mapping - CDS is now found!
+      */
+     MapList dnaToCdsMapping = new MapList(new int[] { 1, 9 },
+             new int[]
+             { 1, 9 }, 1, 1);
+     acf1.addMap(dna1.getDatasetSequence(), cds1.getDatasetSequence(),
+             dnaToCdsMapping);
+     seq = AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+             dnaToPeptide);
+     assertSame(seq, cds1.getDatasetSequence());
+   }
  }
@@@ -26,11 -26,10 +26,12 @@@ import static org.testng.Assert.assertN
  import static org.testng.Assert.assertSame;
  import static org.testng.Assert.assertTrue;
  
 +import jalview.api.FeatureColourI;
  import jalview.bin.Cache;
  import jalview.bin.Jalview;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
++import jalview.datamodel.HiddenColumns;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceGroup;
@@@ -40,7 -39,6 +41,7 @@@ import jalview.io.FileLoader
  import jalview.io.Jalview2xmlTests;
  import jalview.renderer.ResidueShaderI;
  import jalview.schemes.BuriedColourScheme;
 +import jalview.schemes.FeatureColour;
  import jalview.schemes.HelixColourScheme;
  import jalview.schemes.JalviewColourScheme;
  import jalview.schemes.StrandColourScheme;
@@@ -48,7 -46,7 +49,7 @@@ import jalview.schemes.TurnColourScheme
  import jalview.util.MessageManager;
  
  import java.awt.Color;
- import java.util.List;
+ import java.util.Iterator;
  
  import org.testng.annotations.AfterMethod;
  import org.testng.annotations.BeforeClass;
@@@ -71,73 -69,53 +72,81 @@@ public class AlignFrameTes
    {
      SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ");
      SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ");
 -    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5,
 -            Float.NaN, null));
 -    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10,
 -            Float.NaN, null));
 +    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null));
 +    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f,
 +            null));
      seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4,
              Float.NaN, null));
      seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9,
              Float.NaN, null));
      AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
 -    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight());
 +    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
 +            al.getHeight());
 +
 +    /*
 +     * make all features visible (select feature columns checks visibility)
 +     */
 +    alignFrame.getFeatureRenderer().findAllFeatures(true);
  
      /*
       * hiding a feature not present does nothing
       */
      assertFalse(alignFrame.hideFeatureColumns("exon", true));
      assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-     assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
-             .getHiddenColumnsCopy().isEmpty());
++
+     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+             .getNumberOfRegions(), 0);
++
      assertFalse(alignFrame.hideFeatureColumns("exon", false));
      assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-     assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
-             .getHiddenColumnsCopy().isEmpty());
++
+     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+             .getNumberOfRegions(), 0);
  
      /*
       * hiding a feature in all columns does nothing
       */
      assertFalse(alignFrame.hideFeatureColumns("Metal", true));
      assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-     List<int[]> hidden = alignFrame.getViewport().getAlignment()
-             .getHiddenColumns().getHiddenColumnsCopy();
-     assertTrue(hidden.isEmpty());
++
+     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+             .getNumberOfRegions(), 0);
 +
 +    /*
 +     * threshold Metal to hide features where score < 5
 +     * seq1 feature in columns 1-5 is hidden
 +     * seq2 feature in columns 6-10 is shown
 +     */
 +    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
 +    fc.setAboveThreshold(true);
 +    fc.setThreshold(5f);
 +    alignFrame.getFeatureRenderer().setColour("Metal", fc);
 +    assertTrue(alignFrame.hideFeatureColumns("Metal", true));
-     hidden = alignFrame.getViewport().getAlignment().getHiddenColumns()
-             .getHiddenColumnsCopy();
-     assertEquals(hidden.size(), 1);
-     assertEquals(hidden.get(0)[0], 5);
-     assertEquals(hidden.get(0)[1], 9);
++    HiddenColumns hidden = alignFrame.getViewport().getAlignment().getHiddenColumns();
++    assertEquals(hidden.getNumberOfRegions(), 1);
++    Iterator<int[]> regions = hidden.iterator();
++    int[] next = regions.next();
++    assertEquals(next[0], 5);
++    assertEquals(next[1], 9);
 +
      /*
       * hide a feature present in some columns
       * sequence positions [2-4], [7-9] are column positions
       * [1-3], [6-8] base zero
       */
 +    alignFrame.getViewport().showAllHiddenColumns();
      assertTrue(alignFrame.hideFeatureColumns("Turn", true));
-     hidden = alignFrame.getViewport().getAlignment().getHiddenColumns()
-             .getHiddenColumnsCopy();
-     assertEquals(hidden.size(), 2);
-     assertEquals(hidden.get(0)[0], 1);
-     assertEquals(hidden.get(0)[1], 3);
-     assertEquals(hidden.get(1)[0], 6);
-     assertEquals(hidden.get(1)[1], 8);
 -    Iterator<int[]> regions = alignFrame.getViewport().getAlignment()
++    regions = alignFrame.getViewport().getAlignment()
+             .getHiddenColumns().iterator();
+     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+             .getNumberOfRegions(), 2);
 -    int[] next = regions.next();
++    next = regions.next();
+     assertEquals(next[0], 1);
+     assertEquals(next[1], 3);
+     next = regions.next();
+     assertEquals(next[0], 6);
+     assertEquals(next[1], 8);
    }
  
    @BeforeClass(alwaysRun = true)
@@@ -26,26 -26,25 +26,31 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertFalse;
  import static org.testng.AssertJUnit.assertTrue;
  
 +import jalview.bin.Cache;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.Annotation;
+ import jalview.datamodel.ColumnSelection;
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.DBRefSource;
+ import jalview.datamodel.HiddenColumns;
+ import jalview.datamodel.Sequence;
 +import jalview.datamodel.SequenceFeature;
+ import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  import jalview.io.DataSourceType;
  import jalview.io.FileFormat;
  import jalview.io.FormatAdapter;
 +import jalview.urls.api.UrlProviderFactoryI;
 +import jalview.urls.desktop.DesktopUrlProviderFactory;
  import jalview.util.MessageManager;
 +import jalview.util.UrlConstants;
  
  import java.awt.Component;
  import java.io.IOException;
  import java.util.ArrayList;
 +import java.util.Collections;
+ import java.util.Iterator;
  import java.util.List;
  
  import javax.swing.JMenu;
@@@ -85,25 -84,6 +90,25 @@@ public class PopupMenuTes
    @BeforeMethod(alwaysRun = true)
    public void setUp() throws IOException
    {
 +    Cache.loadProperties("test/jalview/io/testProps.jvprops");
 +    String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
 +            + SEQUENCE_ID
 +            + "$"
 +            + "|"
 +            + "UNIPROT | http://www.uniprot.org/uniprot/$" + DB_ACCESSION + "$")
 +            + "|"
 +            + ("INTERPRO | http://www.ebi.ac.uk/interpro/entry/$"
 +                    + DB_ACCESSION + "$")
 +            + "|"
 +            +
 +            // Gene3D entry tests for case (in)sensitivity
 +            ("Gene3D | http://gene3d.biochem.ucl.ac.uk/Gene3D/search?sterm=$"
 +                    + DB_ACCESSION + "$&mode=protein");
 +
 +    UrlProviderFactoryI factory = new DesktopUrlProviderFactory(
 +            UrlConstants.DEFAULT_LABEL, inMenuString, "");
 +    Preferences.sequenceUrlLinks = factory.createUrlProvider();
 +
      alignment = new FormatAdapter().readFile(TEST_DATA,
              DataSourceType.PASTE, FileFormat.Fasta);
      AlignFrame af = new AlignFrame(alignment, 700, 500);
    public void testConfigureReferenceAnnotationsMenu_noSequenceSelected()
    {
      JMenuItem menu = new JMenuItem();
-     List<SequenceI> seqs = new ArrayList<SequenceI>();
+     List<SequenceI> seqs = new ArrayList<>();
      testee.configureReferenceAnnotationsMenu(menu, seqs);
      assertFalse(menu.isEnabled());
      // now try null list
      List<SequenceI> seqs = parentPanel.getAlignment().getSequences();
  
      // create list of links and list of DBRefs
-     List<String> links = new ArrayList<String>();
-     List<DBRefEntry> refs = new ArrayList<DBRefEntry>();
+     List<String> links = new ArrayList<>();
+     List<DBRefEntry> refs = new ArrayList<>();
  
      // links as might be added into Preferences | Connections dialog
      links.add("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
  
      // add all the dbrefs to the sequences: Uniprot 1 each, Interpro all 3 to
      // seq0, Gene3D to seq1
 -    seqs.get(0).addDBRef(refs.get(0));
 +    SequenceI seq = seqs.get(0);
 +    seq.addDBRef(refs.get(0));
  
 -    seqs.get(0).addDBRef(refs.get(1));
 -    seqs.get(0).addDBRef(refs.get(2));
 -    seqs.get(0).addDBRef(refs.get(3));
 +    seq.addDBRef(refs.get(1));
 +    seq.addDBRef(refs.get(2));
 +    seq.addDBRef(refs.get(3));
      
      seqs.get(1).addDBRef(refs.get(4));
      seqs.get(1).addDBRef(refs.get(5));
      
      // get the Popup Menu for first sequence
 -    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0), links);
 +    List<SequenceFeature> noFeatures = Collections.<SequenceFeature> emptyList();
 +    testee = new PopupMenu(parentPanel, seq, noFeatures);
      Component[] seqItems = testee.sequenceMenu.getMenuComponents();
      JMenu linkMenu = (JMenu) seqItems[6];
      Component[] linkItems = linkMenu.getMenuComponents();
      // sequence id for each link should match corresponding DB accession id
      for (int i = 1; i < 4; i++)
      {
 -      assertEquals(refs.get(i - 1).getSource(), ((JMenuItem) linkItems[i])
 +      String msg = seq.getName() + " link[" + i + "]";
 +      assertEquals(msg, refs.get(i - 1).getSource(),
 +              ((JMenuItem) linkItems[i])
                .getText().split("\\|")[0]);
 -      assertEquals(refs.get(i - 1).getAccessionId(),
 +      assertEquals(msg, refs.get(i - 1).getAccessionId(),
                ((JMenuItem) linkItems[i])
                .getText().split("\\|")[1]);
      }
  
      // get the Popup Menu for second sequence
 -    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(1), links);
 +    seq = seqs.get(1);
 +    testee = new PopupMenu(parentPanel, seq, noFeatures);
      seqItems = testee.sequenceMenu.getMenuComponents();
      linkMenu = (JMenu) seqItems[6];
      linkItems = linkMenu.getMenuComponents();
      // sequence id for each link should match corresponding DB accession id
      for (int i = 1; i < 3; i++)
      {
 -      assertEquals(refs.get(i + 3).getSource(), ((JMenuItem) linkItems[i])
 +      String msg = seq.getName() + " link[" + i + "]";
 +      assertEquals(msg, refs.get(i + 3).getSource(),
 +              ((JMenuItem) linkItems[i])
                .getText().split("\\|")[0].toUpperCase());
 -      assertEquals(refs.get(i + 3).getAccessionId(),
 +      assertEquals(msg, refs.get(i + 3).getAccessionId(),
                ((JMenuItem) linkItems[i]).getText().split("\\|")[1]);
      }
  
      // if there are no valid links the Links submenu is disabled
-     List<String> nomatchlinks = new ArrayList<String>();
+     List<String> nomatchlinks = new ArrayList<>();
      nomatchlinks.add("NOMATCH | http://www.uniprot.org/uniprot/$"
              + DB_ACCESSION + "$");
  
 -    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0),
 -            nomatchlinks);
 +    testee = new PopupMenu(parentPanel, seq, noFeatures);
      seqItems = testee.sequenceMenu.getMenuComponents();
      linkMenu = (JMenu) seqItems[6];
      assertFalse(linkMenu.isEnabled());
  
    }
+   /**
+    * Test for adding feature links
+    */
+   @Test(groups = { "Functional" })
+   public void testHideInsertions()
+   {
+     // get sequences from the alignment
+     List<SequenceI> seqs = parentPanel.getAlignment().getSequences();
+     
+     // add our own seqs to avoid problems with changes to existing sequences
+     // (gap at end of sequences varies depending on how tests are run!)
+     Sequence seqGap1 = new Sequence("GappySeq",
+             "AAAA----AA-AAAAAAA---AAA-----------AAAAAAAAAA--");
+     seqGap1.createDatasetSequence();
+     seqs.add(seqGap1);
+     Sequence seqGap2 = new Sequence("LessGappySeq",
+             "AAAAAA-AAAAA---AAA--AAAAA--AAAAAAA-AAAAAA");
+     seqGap2.createDatasetSequence();
+     seqs.add(seqGap2);
+     Sequence seqGap3 = new Sequence("AnotherGapSeq",
+             "AAAAAA-AAAAAA--AAAAAA-AAAAAAAAAAA---AAAAAAAA");
+     seqGap3.createDatasetSequence();
+     seqs.add(seqGap3);
+     Sequence seqGap4 = new Sequence("NoGaps",
+             "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+     seqGap4.createDatasetSequence();
+     seqs.add(seqGap4);
+     ColumnSelection sel = new ColumnSelection();
+     parentPanel.av.getAlignment().getHiddenColumns()
+             .revealAllHiddenColumns(sel);
+     // get the Popup Menu for 7th sequence - no insertions
 -    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(7), null);
++    testee = new PopupMenu(parentPanel, seqs.get(7), null);
+     testee.hideInsertions_actionPerformed(null);
+     
+     HiddenColumns hidden = parentPanel.av.getAlignment().getHiddenColumns();
+     Iterator<int[]> it = hidden.iterator();
+     assertFalse(it.hasNext());
+     // get the Popup Menu for GappySeq - this time we have insertions
 -    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(4), null);
++    testee = new PopupMenu(parentPanel, seqs.get(4), null);
+     testee.hideInsertions_actionPerformed(null);
+     hidden = parentPanel.av.getAlignment().getHiddenColumns();
+     it = hidden.iterator();
+     assertTrue(it.hasNext());
+     int[] region = it.next();
+     assertEquals(region[0], 4);
+     assertEquals(region[1], 7);
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 10);
+     assertEquals(region[1], 10);
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 18);
+     assertEquals(region[1], 20);
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 24);
+     assertEquals(region[1], 34);
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 45);
+     assertEquals(region[1], 46);
+     assertFalse(it.hasNext());
+     sel = new ColumnSelection();
+     hidden.revealAllHiddenColumns(sel);
+     // make a sequence group and hide insertions within the group
+     SequenceGroup sg = new SequenceGroup();
+     sg.setStartRes(8);
+     sg.setEndRes(42);
+     sg.addSequence(seqGap2, false);
+     sg.addSequence(seqGap3, false);
+     parentPanel.av.setSelectionGroup(sg);
+     // hide columns outside and within selection
+     // only hidden columns outside the collection will be retained (unless also
+     // gaps in the selection)
+     hidden.hideColumns(1, 10);
+     hidden.hideColumns(31, 40);
+     // get the Popup Menu for LessGappySeq in the sequence group
 -    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(5), null);
++    testee = new PopupMenu(parentPanel, seqs.get(5), null);
+     testee.hideInsertions_actionPerformed(null);
+     hidden = parentPanel.av.getAlignment().getHiddenColumns();
+     it = hidden.iterator();
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 1);
+     assertEquals(region[1], 7);
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 13);
+     assertEquals(region[1], 14);
+     assertTrue(it.hasNext());
+     region = it.next();
+     assertEquals(region[0], 34);
+     assertEquals(region[1], 34);
+   }
  }
@@@ -50,6 -50,7 +50,7 @@@ import java.awt.Color
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Arrays;
+ import java.util.Iterator;
  import java.util.List;
  
  import org.testng.annotations.BeforeClass;
@@@ -913,9 -914,9 +914,9 @@@ public class MappingUtilsTes
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
      assertEquals("[]", dnaSelection.getSelected().toString());
-     List<int[]> hidden = dnaHidden.getHiddenColumnsCopy();
-     assertEquals(1, hidden.size());
-     assertEquals("[0, 4]", Arrays.toString(hidden.get(0)));
+     Iterator<int[]> regions = dnaHidden.iterator();
+     assertEquals(1, dnaHidden.getNumberOfRegions());
+     assertEquals("[0, 4]", Arrays.toString(regions.next()));
  
      /*
       * Column 1 in protein picks up Seq1/K which maps to cols 0-3 in dna
      proteinSelection.hideSelectedColumns(1, hiddenCols);
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
-     hidden = dnaHidden.getHiddenColumnsCopy();
-     assertEquals(1, hidden.size());
-     assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
+     regions = dnaHidden.iterator();
+     assertEquals(1, dnaHidden.getNumberOfRegions());
+     assertEquals("[0, 3]", Arrays.toString(regions.next()));
  
      /*
       * Column 2 in protein picks up gaps only - no mapping
      proteinSelection.hideSelectedColumns(2, hiddenCols);
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
-     assertTrue(dnaHidden.getHiddenColumnsCopy().isEmpty());
+     assertEquals(0, dnaHidden.getNumberOfRegions());
  
      /*
       * Column 3 in protein picks up Seq1/P, Seq2/Q, Seq3/S which map to columns
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
      assertEquals("[0, 1, 2, 3]", dnaSelection.getSelected().toString());
-     hidden = dnaHidden.getHiddenColumnsCopy();
-     assertEquals(1, hidden.size());
-     assertEquals("[5, 10]", Arrays.toString(hidden.get(0)));
+     regions = dnaHidden.iterator();
+     assertEquals(1, dnaHidden.getNumberOfRegions());
+     assertEquals("[5, 10]", Arrays.toString(regions.next()));
  
      /*
       * Combine hiding columns 1 and 3 to get discontiguous hidden columns
      proteinSelection.hideSelectedColumns(3, hiddenCols);
      MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
              proteinView, dnaView, dnaSelection, dnaHidden);
-     hidden = dnaHidden.getHiddenColumnsCopy();
-     assertEquals(2, hidden.size());
-     assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
-     assertEquals("[5, 10]", Arrays.toString(hidden.get(1)));
+     regions = dnaHidden.iterator();
+     assertEquals(2, dnaHidden.getNumberOfRegions());
+     assertEquals("[0, 3]", Arrays.toString(regions.next()));
+     assertEquals("[5, 10]", Arrays.toString(regions.next()));
    }
  
    @Test(groups = { "Functional" })
      assertEquals("[12, 11, 8, 4]", Arrays.toString(ranges));
    }
  
 +  @Test(groups = { "Functional" })
 +  public void testRangeContains()
 +  {
 +    /*
 +     * both forward ranges
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        1, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        2, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        1, 9 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        4, 5 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        0, 9 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        -10, -9 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        1, 11 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        11, 12 }));
 +
 +    /*
 +     * forward range, reverse query
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        10, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        9, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        10, 2 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        5, 5 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        11, 1 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
 +        10, 0 }));
 +
 +    /*
 +     * reverse range, forward query
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        1, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        1, 9 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        2, 10 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        6, 6 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        6, 11 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        11, 20 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        -3, -2 }));
 +
 +    /*
 +     * both reverse
 +     */
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        10, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        9, 1 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        10, 2 }));
 +    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        3, 3 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        11, 1 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        10, 0 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        12, 11 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
 +        -5, -8 }));
 +
 +    /*
 +     * bad arguments
 +     */
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10, 12 },
 +            new int[] {
 +        1, 10 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 },
 +            new int[] { 1 }));
 +    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, null));
 +    assertFalse(MappingUtils.rangeContains(null, new int[] { 1, 10 }));
 +  }
 +
    @Test(groups = "Functional")
    public void testRemoveEndPositions()
    {