Merge remote-tracking branch 'origin/releases/Release_2_10_4_Branch' into develop
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 28 May 2018 08:19:40 +0000 (09:19 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 28 May 2018 08:19:40 +0000 (09:19 +0100)
Conflicts:
src/jalview/io/FeaturesFile.java

1  2 
build.xml
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/gui/AlignFrame.java
src/jalview/gui/Desktop.java
src/jalview/io/FeaturesFile.java
test/jalview/gui/SeqCanvasTest.java
test/jalview/renderer/seqfeatures/FeatureRendererTest.java

diff --combined build.xml
+++ b/build.xml
    </target>
  
    <target name="buildindices" depends="init, prepare" unless="help.uptodate">
+     <replace value="${JALVIEW_VERSION}">
+       <replacetoken><![CDATA[$$Version-Rel$$]]></replacetoken>
+       <fileset dir="${outputDir}/${helpDir}">
+         <include name="help.jhm" />
+       </fileset>
+     </replace>
      <java classname="com.sun.java.help.search.Indexer" classpathref="build.classpath" fork="true" dir="${outputDir}/${helpDir}">
        <arg line="html" />
      </java>
      <!-- add the add-modules j2se attribute for java 9 -->
      <replace file="${jnlpFile}" value="j2se version=&quot;1.8+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee --illegal-access=warn&quot;">
            <replacetoken>j2se version="1.8+"</replacetoken>
 -           
 -        </replace>
 +    </replace>
    </target>
  
    <target name="-dofakejnlpfileassoc" depends="-generatejnlpf" if="nojnlpfileassocs">
@@@ -242,6 -242,7 +242,6 @@@ label.documentation = Documentatio
  label.about = About...
  label.show_sequence_limits = Show Sequence Limits
  action.feature_settings = Feature Settings...
 -label.feature_settings = Feature Settings
  label.all_columns = All Columns
  label.all_sequences = All Sequences
  label.selected_columns = Selected Columns 
@@@ -266,6 -267,7 +266,7 @@@ label.use_rnaview = Use RNAView for sec
  label.autoadd_secstr = Add secondary structure annotation to alignment
  label.autoadd_temp = Add Temperature Factor annotation to alignment
  label.structure_viewer = Default structure viewer
+ label.double_click_to_browse = Double-click to browse for file
  label.chimera_path = Path to Chimera program
  label.chimera_path_tip = Jalview will first try any path entered here, else standard installation locations.<br>Double-click to browse for file.
  label.invalid_chimera_path = Chimera path not found or not executable
@@@ -273,7 -275,6 +274,7 @@@ label.chimera_missing = Chimera structu
  label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure
  label.min_colour = Minimum Colour
  label.max_colour = Maximum Colour
 +label.no_colour = No Colour
  label.use_original_colours = Use Original Colours
  label.threshold_minmax = Threshold is min/max
  label.represent_group_with = Represent Group with {0}
@@@ -281,9 -282,9 +282,9 @@@ label.selection = Selectio
  label.group_colour = Group Colour
  label.sequence = Sequence
  label.view_pdb_structure = View PDB Structure
 -label.min = Min:
 -label.max = Max:
 -label.colour_by_label = Colour by label
 +label.min_value = Min value
 +label.max_value = Max value
 +label.no_value = No value
  label.new_feature = New Feature
  label.match_case = Match Case
  label.view_alignment_editor = View in alignment editor
@@@ -368,8 -369,6 +369,8 @@@ label.optimise_order = Optimise Orde
  label.seq_sort_by_score = Sequence sort by Score
  label.load_colours = Load Colours
  label.save_colours = Save Colours
 +label.load_colours_tooltip = Load feature colours and filters from file
 +label.save_colours_tooltip = Save feature colours and filters to file
  label.fetch_das_features = Fetch DAS Features
  label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} 
  label.database_param = Database: {0}
@@@ -488,10 -487,6 +489,10 @@@ label.settings_for_type = Settings for 
  label.view_full_application = View in Full Application
  label.load_associated_tree = Load Associated Tree...
  label.load_features_annotations = Load Features/Annotations...
 +label.load_vcf = Load SNP variants from plain text or indexed VCF data
 +label.load_vcf_file = Load VCF File
 +label.searching_vcf = Loading VCF variants...
 +label.added_vcf = Added {0} VCF variants to {1} sequence(s)
  label.export_features = Export Features...
  label.export_annotations = Export Annotations...
  label.to_upper_case = To Upper Case
@@@ -530,6 -525,7 +531,6 @@@ label.threshold_feature_above_threshol
  label.threshold_feature_below_threshold = Below Threshold
  label.adjust_threshold = Adjust threshold
  label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
 -label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
  label.select_colour_minimum_value = Select Colour for Minimum Value
  label.select_colour_maximum_value = Select Colour for Maximum Value
  label.open_url_param = Open URL {0}
@@@ -781,7 -777,7 +782,7 @@@ label.pairwise_aligned_sequences = Pair
  label.original_data_for_params = Original Data for {0}
  label.points_for_params = Points for {0}
  label.transformed_points_for_params = Transformed points for {0}
 -label.graduated_color_for_params = Graduated Feature Colour for {0}
 +label.variable_color_for = Variable Feature Colour for {0}
  label.select_background_colour = Select Background Colour
  label.invalid_font = Invalid Font
  label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
@@@ -868,7 -864,7 +869,7 @@@ label.msa_service_is_unknown = The Mult
  label.service_called_is_not_seq_search_service = The Service called \n{0}\nis not a \nSequence Search Service\!
  label.seq_search_service_is_unknown = The Sequence Search Service named {0} is unknown
  label.feature_type = Feature Type
 -label.display = Display
 +label.show = Show
  label.service_url = Service URL
  label.copied_sequences = Copied sequences
  label.cut_sequences = Cut Sequences
@@@ -1320,41 -1316,9 +1321,41 @@@ label.select_hidden_colour = Select hid
  label.overview = Overview
  label.reset_to_defaults = Reset to defaults
  label.oview_calc = Recalculating overview...
 +label.feature_details = Feature details
 +label.matchCondition_contains = Contains
 +label.matchCondition_notcontains = Does not contain
 +label.matchCondition_matches = Matches
 +label.matchCondition_notmatches = Does not match
 +label.matchCondition_present = Is present
 +label.matchCondition_notpresent = Is not present
 +label.matchCondition_eq = =
 +label.matchCondition_ne = not =
 +label.matchCondition_lt = <
 +label.matchCondition_le = <=
 +label.matchCondition_gt = >
 +label.matchCondition_ge = >=
 +label.numeric_required = The value should be numeric
 +label.filter = Filter
 +label.filters = Filters
 +label.join_conditions = Join conditions with
 +label.score = Score
 +label.colour_by_label = Colour by label
 +label.variable_colour = Variable colour...
 +label.select_colour = Select colour
  option.enable_disable_autosearch = When ticked, search is performed automatically
  option.autosearch = Autosearch
  label.retrieve_ids = Retrieve IDs
 +label.display_settings_for = Display settings for {0} features
 +label.simple = Simple
 +label.simple_colour = Simple Colour
 +label.colour_by_text = Colour by text
 +label.graduated_colour = Graduated Colour
 +label.by_text_of = By text of
 +label.by_range_of = By range of
 +label.filters_tooltip = Click to set or amend filters
 +label.or = Or
 +label.and = And
 +label.sequence_feature_colours = Sequence Feature Colours
  label.best_quality = Best Quality
  label.best_resolution = Best Resolution
  label.most_protein_chain = Most Protein Chain
@@@ -226,6 -226,7 +226,6 @@@ label.automatic_scrolling = Desplazamie
  label.documentation = Documentación
  label.about = Acerca de...
  label.show_sequence_limits = Mostrar los límites de la secuencia
 -label.feature_settings = Ajustar funciones...
  label.all_columns = Todas las columnas
  label.all_sequences = Todas las secuencias
  label.selected_columns = Columnas seleccionadas
@@@ -242,7 -243,6 +242,7 @@@ label.apply_all_groups = Aplicar a todo
  label.autocalculated_annotation = Anotación autocalculada
  label.min_colour = Color mínimo
  label.max_colour = Color máximo
 +label.no_colour = Sin color
  label.use_original_colours = Usar colores originales
  label.threshold_minmax = El umbral es mín/máx
  label.represent_group_with = Representar al grupo con
@@@ -250,9 -250,8 +250,9 @@@ label.selection = Selecciona
  label.group_colour = Color del grupo
  label.sequence = Secuencia
  label.view_pdb_structure = Ver estructura PDB
 -label.min = Mín:
 -label.max = Máx:
 +label.max_value = Valor máximo
 +label.min_value = Valor mínimo
 +label.no_value = Sin valor
  label.colour_by_label = Color por etiquetas
  label.new_feature = Nueva función
  label.match_case = Hacer corresponder mayúsculas y minúsculas
@@@ -337,8 -336,6 +337,8 @@@ label.optimise_order = Optimizar orde
  label.seq_sort_by_score = Ordenar las secuencias por puntuación
  label.load_colours = Cargar colores
  label.save_colours = Guardar colores
 +label.load_colours_tooltip = Cargar colores y filtros desde fichero
 +label.save_colours_tooltip = Guardar colores y filtros en fichero
  label.fetch_das_features = Recuperar funciones DAS
  label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para buscar de {2} 
  label.database_param = Base de datos: {0}
@@@ -455,10 -452,6 +455,10 @@@ label.settings_for_type = Ajustes para 
  label.view_full_application = Ver en la aplicación completa 
  label.load_associated_tree = Cargar árbol asociado ...
  label.load_features_annotations = Cargar características/anotaciones ...
 +label.load_vcf = Cargar variantes SNP desde fichero VCF texto o tab-indexado
 +label.load_vcf_file = Cargar fichero VCF
 +label.searching_vcf = Cargando variantes VCF...
 +label.added_vcf= {0} variantes VCF añadidas a {1} secuencia(s)
  label.export_features = Exportar características...
  label.export_annotations = Exportar anotaciones ...
  label.to_upper_case = Pasar a mayúsculas
@@@ -492,6 -485,7 +492,6 @@@ label.threshold_feature_above_threshol
  label.threshold_feature_below_threshold = Por debajo del umbral
  label.adjust_threshold = Ajustar umbral
  label.toggle_absolute_relative_display_threshold = Cambiar entre mostrar el umbral absoluto y el relativo.
 -label.display_features_same_type_different_label_using_different_colour = Mostrar las características del mismo tipo con una etiqueta diferente y empleando un color distinto (p.e. características del dominio)
  label.select_colour_minimum_value = Seleccionar el color para el valor mínimo
  label.select_colour_maximum_value = Seleccionar el color para el valor máximo
  label.open_url_param = Abrir URL {0}
@@@ -711,7 -705,7 +711,7 @@@ label.pairwise_aligned_sequences = Secu
  label.original_data_for_params = Datos originales de {0}
  label.points_for_params = Puntos de {0}
  label.transformed_points_for_params = Puntos transformados de {0}
 -label.graduated_color_for_params = Color graduado para la característica de {0}
 +label.variable_color_for = Color variable para la característica de {0}
  label.select_background_colour = Seleccionar color de fondo
  label.invalid_font = Fuente no válida
  label.separate_multiple_accession_ids = Separar los accession id con un punto y coma ";"
@@@ -794,7 -788,7 +794,7 @@@ label.msa_service_is_unknown = El Servi
  label.service_called_is_not_seq_search_service = El Servicio llamando \n{0}\nno es un \nServicio de B\u00FAsqueda de Secuencias\!
  label.seq_search_service_is_unknown = El Servicio de Búsqueda de Sencuencias llamado {0} es desconocido
  label.feature_type = Tipo de característisca
 -label.display = Representación
 +label.show = Mostrar
  label.service_url = URL del servicio
  label.copied_sequences = Secuencias copiadas
  label.cut_sequences = Cortar secuencias
@@@ -1179,6 -1173,7 +1179,7 @@@ action.select_by_annotation=Seleccionar
  action.export_features=Exportar Características
  error.invalid_regex=Expresión regular inválida
  label.autoadd_temp=Añadir anotación factor de temperatura al alineamiento
+ label.double_click_to_browse = Haga doble clic para buscar fichero 
  label.chimera_path_tip=Jalview intentará primero las rutas introducidas aquí, Y si no las rutas usuales de instalación
  label.structure_chooser=Selector de Estructuras
  label.structure_chooser_manual_association=Selector de Estructuras - asociación manual
@@@ -1225,13 -1220,13 +1226,13 @@@ exception.resource_not_be_found=El recu
  label.aacon_calculations=cálculos AACon
  label.pdb_web-service_error=Error de servicio web PDB
  exception.unable_to_detect_internet_connection=Jalview no puede detectar una conexión a Internet
- label.chimera_path=Ruta de acceso a programa Chimera
+ label.chimera_path=Ruta de acceso a Chimera
  warn.delete_all=<html>Borrar todas las secuencias cerrará la ventana del alineamiento.<br>Confirmar o Cancelar.
  label.select_all=Seleccionar Todos
  label.alpha_helix=Hélice Alfa
  label.chimera_help=Ayuda para Chimera
  label.find_tip=Buscar alineamiento, selección o IDs de secuencia para una subsecuencia (sin huecos)
- label.structure_viewer=Visualizador de estructura por defecto
+ label.structure_viewer=Visualizador por defecto
  label.embbed_biojson=Incrustar BioJSON al exportar HTML
  label.transparency_tip=Ajustar la transparencia a "ver a través" los colores de las características.
  label.choose_annotations=Escoja anotaciones
@@@ -1321,41 -1316,9 +1322,41 @@@ label.select_hidden_colour = Selecciona
  label.overview = Resumen
  label.reset_to_defaults = Restablecen a los predeterminados
  label.oview_calc = Recalculando resumen
 +label.feature_details = Detalles de característica 
 +label.matchCondition_contains = Contiene
 +label.matchCondition_notcontains = No contiene
 +label.matchCondition_matches = Es igual a
 +label.matchCondition_notmatches = No es igual a
 +label.matchCondition_present = Está presente
 +label.matchCondition_notpresent = No está presente
 +label.matchCondition_eq = =
 +label.matchCondition_ne = not =
 +label.matchCondition_lt = <
 +label.matchCondition_le = <=
 +label.matchCondition_gt = >
 +label.matchCondition_ge = >=
 +label.numeric_required = Valor numérico requerido
 +label.filter = Filtro
 +label.filters = Filtros
 +label.join_conditions = Combinar condiciones con
 +label.score = Puntuación
 +label.colour_by_label = Colorear por texto
 +label.variable_colour = Color variable...
 +label.select_colour = Seleccionar color
  option.enable_disable_autosearch = Marcar para buscar automáticamente
  option.autosearch = Auto búsqueda
  label.retrieve_ids = Recuperar IDs
 +label.display_settings_for = Visualización de características {0}
 +label.simple = Simple
 +label.simple_colour = Color simple
 +label.colour_by_text = Colorear por texto
 +label.graduated_colour = Color graduado
 +label.by_text_of = Por texto de
 +label.by_range_of = Por rango de
 +label.filters_tooltip = Haga clic para configurar o modificar los filtros
 +label.or = O
 +label.and = Y
 +label.sequence_feature_colours = Colores de características de las secuencias
  label.best_quality = Mejor Calidad
  label.best_resolution = Mejor Resolución
  label.most_protein_chain = Más Cadena de Proteína
@@@ -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
    @Override
    protected void copy_actionPerformed(ActionEvent e)
    {
 -    System.gc();
      if (viewport.getSelectionGroup() == null)
      {
        return;
                  alignPanel.setOverviewPanel(null);
                };
              });
+     if (getKeyListeners().length > 0)
+     {
+       frame.addKeyListener(getKeyListeners()[0]);
+     }
  
      alignPanel.setOverviewPanel(overview);
    }
    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();
    }
  
              int assocfiles = 0;
              if (filesmatched.size() > 0)
              {
-               if (Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false)
-                       || JvOptionPane.showConfirmDialog(thisaf,
-                               MessageManager.formatMessage(
-                                       "label.automatically_associate_structure_files_with_sequences_same_name",
-                                       new Object[]
-                                       { Integer.valueOf(filesmatched.size())
-                                               .toString() }),
-                               MessageManager.getString(
-                                       "label.automatically_associate_structure_files_by_name"),
-                               JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION)
+               boolean autoAssociate = Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
+               if (!autoAssociate)
+               {
+                 String msg = MessageManager.formatMessage(
+                         "label.automatically_associate_structure_files_with_sequences_same_name",
+                         new Object[]
+                         { Integer.valueOf(filesmatched.size())
+                                 .toString() });
+                 String ttl = MessageManager.getString(
+                         "label.automatically_associate_structure_files_by_name");
+                 int choice = JvOptionPane.showConfirmDialog(thisaf, msg,
+                         ttl, JvOptionPane.YES_NO_OPTION);
+                 autoAssociate = choice == JvOptionPane.YES_OPTION;
+               }
+               if (autoAssociate)
                {
                  for (Object[] fm : filesmatched)
                  {
                    alignPanel.paintAlignment(true, false);
                  }
                }
+               else
+               {
+                 /*
+                  * add declined structures as sequences
+                  */
+                 for (Object[] o : filesmatched)
+                 {
+                   filesnotmatched.add((String) o[0]);
+                 }
+               }
              }
              if (filesnotmatched.size() > 0)
              {
        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
@@@ -32,6 -32,7 +32,7 @@@ import jalview.io.FileFormatException
  import jalview.io.FileFormatI;
  import jalview.io.FileFormats;
  import jalview.io.FileLoader;
+ import jalview.io.FormatAdapter;
  import jalview.io.IdentifyFile;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
@@@ -116,6 -117,8 +117,8 @@@ import javax.swing.event.InternalFrameE
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
+ import org.stackoverflowusers.file.WindowsShortcut;
  /**
   * Jalview Desktop
   * 
@@@ -900,6 -903,8 +903,6 @@@ public class Desktop extends jalview.jb
            menuItem.removeActionListener(menuItem.getActionListeners()[0]);
          }
          windowMenu.remove(menuItem);
 -
 -        System.gc();
        };
      });
  
      {
        ssm.resetAll();
      }
 -    System.gc();
    }
  
    @Override
      return groovyConsole;
    }
  
+   /**
+    * handles the payload of a drag and drop event.
+    * 
+    * TODO refactor to desktop utilities class
+    * 
+    * @param files
+    *          - Data source strings extracted from the drop event
+    * @param protocols
+    *          - protocol for each data source extracted from the drop event
+    * @param evt
+    *          - the drop event
+    * @param t
+    *          - the payload from the drop event
+    * @throws Exception
+    */
    public static void transferFromDropTarget(List<String> files,
            List<DataSourceType> protocols, DropTargetDropEvent evt,
            Transferable t) throws Exception
    {
  
      DataFlavor uriListFlavor = new DataFlavor(
-             "text/uri-list;class=java.lang.String");
+             "text/uri-list;class=java.lang.String"), urlFlavour = null;
+     try
+     {
+       urlFlavour = new DataFlavor(
+               "application/x-java-url; class=java.net.URL");
+     } catch (ClassNotFoundException cfe)
+     {
+       Cache.log.debug("Couldn't instantiate the URL dataflavor.", cfe);
+     }
+     if (urlFlavour != null && t.isDataFlavorSupported(urlFlavour))
+     {
+       try
+       {
+       java.net.URL url = (URL) t.getTransferData(urlFlavour);
+         // nb: java 8 osx bug https://bugs.openjdk.java.net/browse/JDK-8156099
+         // means url may be null.
+       if (url != null)
+       {
+         protocols.add(DataSourceType.URL);
+         files.add(url.toString());
+         Cache.log.debug("Drop handled as URL dataflavor "
+                 + files.get(files.size() - 1));
+           return;
+         }
+         else
+         {
+           if (Platform.isAMac())
+           {
+             System.err.println(
+                     "Please ignore plist error - occurs due to problem with java 8 on OSX");
+           }
+           ;
+       }
+       } catch (Throwable ex)
+       {
+         Cache.log.debug("URL drop handler failed.", ex);
+       }
+     }
      if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
      {
        // Works on Windows and MacOSX
          // fallback to text: workaround - on OSX where there's a JVM bug
          Cache.log.debug("standard URIListFlavor failed. Trying text");
          // try text fallback
-         data = (String) t.getTransferData(
-                 new DataFlavor("text/plain;class=java.lang.String"));
-         if (Cache.log.isDebugEnabled())
+         DataFlavor textDf = new DataFlavor(
+                 "text/plain;class=java.lang.String");
+         if (t.isDataFlavorSupported(textDf))
          {
-           Cache.log.debug("fallback returned " + data);
+           data = (String) t.getTransferData(textDf);
          }
+         Cache.log.debug("Plain text drop content returned "
+                 + (data == null ? "Null - failed" : data));
        }
-       while (protocols.size() < files.size())
-       {
-         Cache.log.debug("Adding missing FILE protocol for "
-                 + files.get(protocols.size()));
-         protocols.add(DataSourceType.FILE);
-       }
-       for (java.util.StringTokenizer st = new java.util.StringTokenizer(
-               data, "\r\n"); st.hasMoreTokens();)
+       if (data != null)
        {
-         added = true;
-         String s = st.nextToken();
-         if (s.startsWith("#"))
+         while (protocols.size() < files.size())
          {
-           // the line is a comment (as per the RFC 2483)
-           continue;
-         }
-         java.net.URI uri = new java.net.URI(s);
-         if (uri.getScheme().toLowerCase().startsWith("http"))
-         {
-           protocols.add(DataSourceType.URL);
-           files.add(uri.toString());
+           Cache.log.debug("Adding missing FILE protocol for "
+                   + files.get(protocols.size()));
+           protocols.add(DataSourceType.FILE);
          }
-         else
+         for (java.util.StringTokenizer st = new java.util.StringTokenizer(
+                 data, "\r\n"); st.hasMoreTokens();)
          {
-           // otherwise preserve old behaviour: catch all for file objects
-           java.io.File file = new java.io.File(uri);
-           protocols.add(DataSourceType.FILE);
-           files.add(file.toString());
+           added = true;
+           String s = st.nextToken();
+           if (s.startsWith("#"))
+           {
+             // the line is a comment (as per the RFC 2483)
+             continue;
+           }
+           java.net.URI uri = new java.net.URI(s);
+           if (uri.getScheme().toLowerCase().startsWith("http"))
+           {
+             protocols.add(DataSourceType.URL);
+             files.add(uri.toString());
+           }
+           else
+           {
+             // otherwise preserve old behaviour: catch all for file objects
+             java.io.File file = new java.io.File(uri);
+             protocols.add(DataSourceType.FILE);
+             files.add(file.toString());
+           }
          }
        }
        if (Cache.log.isDebugEnabled())
        {
          if (data == null || !added)
          {
-           Cache.log.debug(
-                   "Couldn't resolve drop data. Here are the supported flavors:");
-           for (DataFlavor fl : t.getTransferDataFlavors())
+           if (t.getTransferDataFlavors() != null
+                   && t.getTransferDataFlavors().length > 0)
            {
              Cache.log.debug(
-                     "Supported transfer dataflavor: " + fl.toString());
-             Object df = t.getTransferData(fl);
-             if (df != null)
+                     "Couldn't resolve drop data. Here are the supported flavors:");
+             for (DataFlavor fl : t.getTransferDataFlavors())
              {
-               Cache.log.debug("Retrieves: " + df);
-             }
-             else
-             {
-               Cache.log.debug("Retrieved nothing");
+               Cache.log.debug(
+                       "Supported transfer dataflavor: " + fl.toString());
+               Object df = t.getTransferData(fl);
+               if (df != null)
+               {
+                 Cache.log.debug("Retrieves: " + df);
+               }
+               else
+               {
+                 Cache.log.debug("Retrieved nothing");
+               }
              }
            }
+           else
+           {
+             Cache.log.debug("Couldn't resolve dataflavor for drop: "
+                     + t.toString());
+           }
+         }
+       }
+     }
+     if (Platform.isWindows())
+     {
+       Cache.log.debug("Scanning dropped content for Windows Link Files");
+       // resolve any .lnk files in the file drop
+       for (int f = 0; f < files.size(); f++)
+       {
+         String source = files.get(f).toLowerCase();
+         if (protocols.get(f).equals(DataSourceType.FILE)
+                 && (source.endsWith(".lnk") || source.endsWith(".url")
+                         || source.endsWith(".site")))
+         {
+           try {
+             File lf = new File(files.get(f));
+             // process link file to get a URL
+             Cache.log.debug("Found potential link file: " + lf);
+             WindowsShortcut wscfile = new WindowsShortcut(lf);
+             String fullname = wscfile.getRealFilename();
+             protocols.set(f, FormatAdapter.checkProtocol(fullname));
+             files.set(f, fullname);
+             Cache.log.debug("Parsed real filename " + fullname
+                     + " to extract protocol: " + protocols.get(f));
+           }
+           catch (Exception ex)
+           {
+             Cache.log.error("Couldn't parse "+files.get(f)+" as a link file.",ex);
+           }
          }
        }
      }
@@@ -31,8 -31,6 +31,8 @@@ import jalview.datamodel.AlignmentI
  import jalview.datamodel.SequenceDummy;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcherSet;
 +import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.io.gff.GffHelperBase;
  import jalview.io.gff.GffHelperFactory;
  import jalview.io.gff.GffHelperI;
@@@ -70,22 -68,10 +70,20 @@@ import java.util.Map.Entry
   */
  public class FeaturesFile extends AlignFile implements FeaturesSourceI
  {
 +  private static final String TAB_REGEX = "\\t";
 +
 +  private static final String STARTGROUP = "STARTGROUP";
 +
 +  private static final String ENDGROUP = "ENDGROUP";
 +
 +  private static final String STARTFILTERS = "STARTFILTERS";
 +
 +  private static final String ENDFILTERS = "ENDFILTERS";
 +
    private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED";
  
    private static final String NOTE = "Note";
  
-   protected static final String TAB = "\t";
    protected static final String GFF_VERSION = "##gff-version";
  
    private AlignmentI lastmatchedAl = null;
     * @param align
     *          - alignment/dataset containing sequences that are to be annotated
     * @param colours
 -   *          - hashtable to store feature colour definitions
 +   *          - map to store feature colour definitions
     * @param removeHTML
     *          - process html strings into plain text
     * @param relaxedIdmatching
            Map<String, FeatureColourI> colours, boolean removeHTML,
            boolean relaxedIdmatching)
    {
 +    return parse(align, colours, null, removeHTML, relaxedIdmatching);
 +  }
 +
 +  /**
 +   * Parse GFF or Jalview format sequence features file
 +   * 
 +   * @param align
 +   *          - alignment/dataset containing sequences that are to be annotated
 +   * @param colours
 +   *          - map to store feature colour definitions
 +   * @param filters
 +   *          - map to store feature filter definitions
 +   * @param removeHTML
 +   *          - process html strings into plain text
 +   * @param relaxedIdmatching
 +   *          - when true, ID matches to compound sequence IDs are allowed
 +   * @return true if features were added
 +   */
 +  public boolean parse(AlignmentI align,
 +          Map<String, FeatureColourI> colours,
 +          Map<String, FeatureMatcherSetI> filters, boolean removeHTML,
 +          boolean relaxedIdmatching)
 +  {
      Map<String, String> gffProps = new HashMap<>();
      /*
       * keep track of any sequences we try to create from the data
            continue;
          }
  
 -        gffColumns = line.split("\\t"); // tab as regex
 +        gffColumns = line.split(TAB_REGEX);
          if (gffColumns.length == 1)
          {
            if (line.trim().equalsIgnoreCase("GFF"))
            }
          }
  
 -        if (gffColumns.length > 1 && gffColumns.length < 4)
 +        if (gffColumns.length > 0 && gffColumns.length < 4)
          {
            /*
             * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
             * a feature type colour specification
             */
            String ft = gffColumns[0];
 -          if (ft.equalsIgnoreCase("startgroup"))
 +          if (ft.equalsIgnoreCase(STARTFILTERS))
 +          {
 +            parseFilters(filters);
 +            continue;
 +          }
 +          if (ft.equalsIgnoreCase(STARTGROUP))
            {
              featureGroup = gffColumns[1];
            }
 -          else if (ft.equalsIgnoreCase("endgroup"))
 +          else if (ft.equalsIgnoreCase(ENDGROUP))
            {
              // We should check whether this is the current group,
              // but at present there's no way of showing more than 1 group
    }
  
    /**
 +   * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type
 +   * filter to the map for each line parsed. After exit from this method,
 +   * nextLine() should return the line after ENDFILTERS (or we are already at
 +   * end of file if ENDFILTERS was missing).
 +   * 
 +   * @param filters
 +   * @throws IOException
 +   */
 +  protected void parseFilters(Map<String, FeatureMatcherSetI> filters)
 +          throws IOException
 +  {
 +    String line;
 +    while ((line = nextLine()) != null)
 +    {
 +      if (line.toUpperCase().startsWith(ENDFILTERS))
 +      {
 +        return;
 +      }
 +      String[] tokens = line.split(TAB_REGEX);
 +      if (tokens.length != 2)
 +      {
 +        System.err.println(String.format("Invalid token count %d for %d",
 +                tokens.length, line));
 +      }
 +      else
 +      {
 +        String featureType = tokens[0];
 +        FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]);
 +        if (fm != null && filters != null)
 +        {
 +          filters.put(featureType, fm);
 +        }
 +      }
 +    }
 +  }
 +
 +  /**
     * Try to parse a Jalview format feature specification and add it as a
     * sequence feature to any matching sequences in the alignment. Returns true
     * if successful (a feature was added), or false if not.
    }
  
    /**
 -   * Returns contents of a Jalview format features file, for visible features,
 -   * as filtered by type and group. Features with a null group are displayed if
 -   * their feature type is visible. Non-positional features may optionally be
 -   * included (with no check on type or group).
 +   * Returns contents of a Jalview format features file, for visible features, as
 +   * filtered by type and group. Features with a null group are displayed if their
 +   * feature type is visible. Non-positional features may optionally be included
 +   * (with no check on type or group).
     * 
     * @param sequences
     *          source of features
     * @param visible
     *          map of colour for each visible feature type
 +   * @param featureFilters
     * @param visibleFeatureGroups
     * @param includeNonPositional
     *          if true, include non-positional features (regardless of group or
     */
    public String printJalviewFormat(SequenceI[] sequences,
            Map<String, FeatureColourI> visible,
 +          Map<String, FeatureMatcherSetI> featureFilters,
            List<String> visibleFeatureGroups, boolean includeNonPositional)
    {
      if (!includeNonPositional && (visible == null || visible.isEmpty()))
              .toArray(new String[visible.keySet().size()]);
  
      /*
 +     * feature filters if any
 +     */
 +    outputFeatureFilters(out, visible, featureFilters);
 +
 +    /*
       * sort groups alphabetically, and ensure that features with a
       * null or empty group are output after those in named groups
       */
        }
      }
  
 -    for (String group : sortedGroups)
 +    /*
 +     * positional features within groups
 +     */
 +    foundSome |= outputFeaturesByGroup(out, sortedGroups, types, sequences);
 +
 +    return foundSome ? out.toString() : "No Features Visible";
 +  }
 +
 +  /**
 +   * Outputs any feature filters defined for visible feature types, sandwiched by
 +   * STARTFILTERS and ENDFILTERS lines
 +   * 
 +   * @param out
 +   * @param visible
 +   * @param featureFilters
 +   */
 +  void outputFeatureFilters(StringBuilder out,
 +          Map<String, FeatureColourI> visible,
 +          Map<String, FeatureMatcherSetI> featureFilters)
 +  {
 +    if (visible == null || featureFilters == null
 +            || featureFilters.isEmpty())
 +    {
 +      return;
 +    }
 +
 +    boolean first = true;
 +    for (String featureType : visible.keySet())
 +    {
 +      FeatureMatcherSetI filter = featureFilters.get(featureType);
 +      if (filter != null)
 +      {
 +        if (first)
 +        {
 +          first = false;
 +          out.append(newline).append(STARTFILTERS).append(newline);
 +        }
 +        out.append(featureType).append(TAB).append(filter.toStableString())
 +                .append(newline);
 +      }
 +    }
 +    if (!first)
 +    {
 +      out.append(ENDFILTERS).append(newline).append(newline);
 +    }
 +
 +  }
 +
 +  /**
 +   * Appends output of sequence features within feature groups to the output
 +   * buffer. Groups other than the null or empty group are sandwiched by
 +   * STARTGROUP and ENDGROUP lines.
 +   * 
 +   * @param out
 +   * @param groups
 +   * @param featureTypes
 +   * @param sequences
 +   * @return
 +   */
 +  private boolean outputFeaturesByGroup(StringBuilder out,
 +          List<String> groups, String[] featureTypes, SequenceI[] sequences)
 +  {
 +    boolean foundSome = false;
 +    for (String group : groups)
      {
        boolean isNamedGroup = (group != null && !"".equals(group));
        if (isNamedGroup)
        {
          out.append(newline);
 -        out.append("STARTGROUP").append(TAB);
 +        out.append(STARTGROUP).append(TAB);
          out.append(group);
          out.append(newline);
        }
        {
          String sequenceName = sequences[i].getName();
          List<SequenceFeature> features = new ArrayList<>();
 -        if (types.length > 0)
 +        if (featureTypes.length > 0)
          {
            features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
 -                  true, group, types));
 +                  true, group, featureTypes));
          }
  
          for (SequenceFeature sequenceFeature : features)
  
        if (isNamedGroup)
        {
 -        out.append("ENDGROUP").append(TAB);
 +        out.append(ENDGROUP).append(TAB);
          out.append(group);
          out.append(newline);
        }
      }
 -
 -    return foundSome ? out.toString() : "No Features Visible";
 +    return foundSome;
    }
  
    /**
@@@ -1,3 -1,23 +1,23 @@@
+ /*
+  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+  * Copyright (C) $$Year-Rel$$ The Jalview Authors
+  * 
+  * This file is part of Jalview.
+  * 
+  * Jalview is free software: you can redistribute it and/or
+  * modify it under the terms of the GNU General Public License 
+  * as published by the Free Software Foundation, either version 3
+  * of the License, or (at your option) any later version.
+  *  
+  * Jalview is distributed in the hope that it will be useful, but 
+  * WITHOUT ANY WARRANTY; without even the implied warranty 
+  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+  * PURPOSE.  See the GNU General Public License for more details.
+  * 
+  * You should have received a copy of the GNU General Public License
+  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+  * The Jalview Authors are detailed in the 'AUTHORS' file.
+  */
  package jalview.gui;
  
  import static org.testng.Assert.assertEquals;
@@@ -9,10 -29,10 +29,10 @@@ import jalview.io.FileLoader
  import java.awt.Font;
  import java.awt.FontMetrics;
  
 -import org.testng.annotations.Test;
 -
  import junit.extensions.PA;
  
 +import org.testng.annotations.Test;
 +
  public class SeqCanvasTest
  {
    /**
@@@ -1,28 -1,41 +1,48 @@@
+ /*
+  * 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.renderer.seqfeatures;
  
  import static org.testng.Assert.assertEquals;
  import static org.testng.Assert.assertFalse;
 +import static org.testng.Assert.assertNull;
  import static org.testng.Assert.assertTrue;
  
  import jalview.api.AlignViewportI;
  import jalview.api.FeatureColourI;
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcher;
 +import jalview.datamodel.features.FeatureMatcherSet;
 +import jalview.datamodel.features.FeatureMatcherSetI;
  import jalview.gui.AlignFrame;
  import jalview.io.DataSourceType;
  import jalview.io.FileLoader;
  import jalview.schemes.FeatureColour;
 +import jalview.util.matcher.Condition;
 +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
  
  import java.awt.Color;
  import java.util.ArrayList;
  import java.util.Arrays;
 +import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
@@@ -68,8 -81,9 +88,8 @@@ public class FeatureRendererTes
      seqs.get(2).addSequenceFeature(
              new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup"));
      // bug in findAllFeatures - group not checked for a known feature type
 -    seqs.get(2).addSequenceFeature(
 -            new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN,
 -                    "RfamGroup"));
 +    seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9,
 +            Float.NaN, "RfamGroup"));
      // existing feature type with null group
      seqs.get(3).addSequenceFeature(
              new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
       * change render order (todo: an easier way)
       * nb here last comes first in the data array
       */
 -    Object[][] data = new Object[3][];
 +    FeatureSettingsBean[] data = new FeatureSettingsBean[3];
      FeatureColourI colour = new FeatureColour(Color.RED);
 -    data[0] = new Object[] { "Rfam", colour, true };
 -    data[1] = new Object[] { "Pfam", colour, false };
 -    data[2] = new Object[] { "Scop", colour, false };
 +    data[0] = new FeatureSettingsBean("Rfam", colour, null, true);
 +    data[1] = new FeatureSettingsBean("Pfam", colour, null, false);
 +    data[2] = new FeatureSettingsBean("Scop", colour, null, false);
      fr.setFeaturePriority(data);
 -    assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam"));
 +    assertEquals(fr.getRenderOrder(),
 +            Arrays.asList("Scop", "Pfam", "Rfam"));
      assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
  
      /*
      /*
       * make "Type2" not displayed
       */
 -    Object[][] data = new Object[4][];
      FeatureColourI colour = new FeatureColour(Color.RED);
 -    data[0] = new Object[] { "Type1", colour, true };
 -    data[1] = new Object[] { "Type2", colour, false };
 -    data[2] = new Object[] { "Type3", colour, true };
 -    data[3] = new Object[] { "Disulphide Bond", colour, true };
 +    FeatureSettingsBean[] data = new FeatureSettingsBean[4];
 +    data[0] = new FeatureSettingsBean("Type1", colour, null, true);
 +    data[1] = new FeatureSettingsBean("Type2", colour, null, false);
 +    data[2] = new FeatureSettingsBean("Type3", colour, null, true);
 +    data[3] = new FeatureSettingsBean("Disulphide Bond", colour, null,
 +            true);
      fr.setFeaturePriority(data);
  
      features = fr.findFeaturesAtColumn(seq, 15);
      features = fr.findFeaturesAtColumn(seq, 5);
      assertEquals(features.size(), 1);
      assertTrue(features.contains(sf8));
 +
 +    /*
 +     * give "Type3" features a graduated colour scheme
 +     * - first with no threshold
 +     */
 +    FeatureColourI gc = new FeatureColour(Color.yellow, Color.red, null, 0f,
 +            10f);
 +    fr.getFeatureColours().put("Type3", gc);
 +    features = fr.findFeaturesAtColumn(seq, 8);
 +    assertTrue(features.contains(sf4));
 +    // now with threshold > 2f - feature score of 1f is excluded
 +    gc.setAboveThreshold(true);
 +    gc.setThreshold(2f);
 +    features = fr.findFeaturesAtColumn(seq, 8);
 +    assertFalse(features.contains(sf4));
 +
 +    /*
 +     * make "Type3" graduated colour by attribute "AF"
 +     * - first with no attribute held - feature should be excluded
 +     */
 +    gc.setAttributeName("AF");
 +    features = fr.findFeaturesAtColumn(seq, 8);
 +    assertFalse(features.contains(sf4));
 +    // now with the attribute above threshold - should be included
 +    sf4.setValue("AF", "2.4");
 +    features = fr.findFeaturesAtColumn(seq, 8);
 +    assertTrue(features.contains(sf4));
 +    // now with the attribute below threshold - should be excluded
 +    sf4.setValue("AF", "1.4");
 +    features = fr.findFeaturesAtColumn(seq, 8);
 +    assertFalse(features.contains(sf4));
    }
  
    @Test(groups = "Functional")
      FeatureRenderer fr = new FeatureRenderer(av);
  
      List<SequenceFeature> features = new ArrayList<>();
 -    fr.filterFeaturesForDisplay(features, null); // empty list, does nothing
 +    fr.filterFeaturesForDisplay(features); // empty list, does nothing
  
      SequenceI seq = av.getAlignment().getSequenceAt(0);
      SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
       * filter out duplicate (co-located) features
       * note: which gets removed is not guaranteed
       */
 -    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
 +    fr.filterFeaturesForDisplay(features);
      assertEquals(features.size(), 3);
      assertTrue(features.contains(sf1) || features.contains(sf4));
      assertFalse(features.contains(sf1) && features.contains(sf4));
      assertTrue(features.contains(sf5));
  
      /*
 -     * hide group 3 - sf3 is removed, sf2 is retained
 +     * hide groups 2 and 3 makes no difference to this method
       */
 +    fr.setGroupVisibility("group2", false);
      fr.setGroupVisibility("group3", false);
      features = seq.getSequenceFeatures();
 -    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
 +    fr.filterFeaturesForDisplay(features);
      assertEquals(features.size(), 3);
      assertTrue(features.contains(sf1) || features.contains(sf4));
      assertFalse(features.contains(sf1) && features.contains(sf4));
 -    assertTrue(features.contains(sf2));
 -    assertFalse(features.contains(sf3));
 +    assertTrue(features.contains(sf2) || features.contains(sf3));
 +    assertFalse(features.contains(sf2) && features.contains(sf3));
      assertTrue(features.contains(sf5));
  
      /*
 -     * hide group 2, show group 3 - sf2 is removed, sf3 is retained
 +     * no filtering if transparency is applied
       */
 -    fr.setGroupVisibility("group2", false);
 -    fr.setGroupVisibility("group3", true);
 +    fr.setTransparency(0.5f);
      features = seq.getSequenceFeatures();
 -    fr.filterFeaturesForDisplay(features, null);
 -    assertEquals(features.size(), 3);
 -    assertTrue(features.contains(sf1) || features.contains(sf4));
 -    assertFalse(features.contains(sf1) && features.contains(sf4));
 -    assertFalse(features.contains(sf2));
 -    assertTrue(features.contains(sf3));
 -    assertTrue(features.contains(sf5));
 +    fr.filterFeaturesForDisplay(features);
 +    assertEquals(features.size(), 5);
 +  }
 +
 +  @Test(groups = "Functional")
 +  public void testGetColour()
 +  {
 +    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(">s1\nABCD\n",
 +            DataSourceType.PASTE);
 +    AlignViewportI av = af.getViewport();
 +    FeatureRenderer fr = new FeatureRenderer(av);
  
      /*
 -     * no filtering of co-located features with graduated colour scheme
 -     * filterFeaturesForDisplay does _not_ check colour threshold
 -     * sf2 is removed as its group is hidden
 +     * simple colour, feature type and group displayed
       */
 -    features = seq.getSequenceFeatures();
 -    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
 -            Color.white, 0f, 1f));
 -    assertEquals(features.size(), 4);
 -    assertFalse(features.contains(sf2));
 +    FeatureColourI fc = new FeatureColour(Color.red);
 +    fr.getFeatureColours().put("Cath", fc);
 +    SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
 +            "group1");
 +    assertEquals(fr.getColour(sf1), Color.red);
  
      /*
 -     * co-located features with colour by label
 -     * should not get filtered
 +     * hide feature type, then unhide
 +     * - feature type visibility should not affect the result
       */
 -    features = seq.getSequenceFeatures();
 -    FeatureColour fc = new FeatureColour(Color.black);
 -    fc.setColourByLabel(true);
 -    fr.filterFeaturesForDisplay(features, fc);
 -    assertEquals(features.size(), 4);
 -    assertTrue(features.contains(sf1));
 -    assertTrue(features.contains(sf3));
 -    assertTrue(features.contains(sf4));
 -    assertTrue(features.contains(sf5));
 +    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
 +    data[0] = new FeatureSettingsBean("Cath", fc, null, false);
 +    fr.setFeaturePriority(data);
 +    assertEquals(fr.getColour(sf1), Color.red);
 +    data[0] = new FeatureSettingsBean("Cath", fc, null, true);
 +    fr.setFeaturePriority(data);
 +    assertEquals(fr.getColour(sf1), Color.red);
  
      /*
 -     * no filtering if transparency is applied
 +     * hide feature group, then unhide
       */
 -    fr.setTransparency(0.5f);
 -    features = seq.getSequenceFeatures();
 -    fr.setGroupVisibility("group2", true);
 -    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.RED));
 -    assertEquals(features.size(), 5);
 +    fr.setGroupVisibility("group1", false);
 +    assertNull(fr.getColour(sf1));
 +    fr.setGroupVisibility("group1", true);
 +    assertEquals(fr.getColour(sf1), Color.red);
 +
 +    /*
 +     * graduated colour by score, no threshold, no score
 +     * 
 +     */
 +    FeatureColourI gc = new FeatureColour(Color.yellow, Color.red,
 +            Color.green, 1f, 11f);
 +    fr.getFeatureColours().put("Cath", gc);
 +    assertEquals(fr.getColour(sf1), Color.green);
 +
 +    /*
 +     * graduated colour by score, no threshold, with score value
 +     */
 +    SequenceFeature sf2 = new SequenceFeature("Cath", "", 6, 8, 6f,
 +            "group1");
 +    // score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0)
 +    Color expected = new Color(255, 128, 0);
 +    assertEquals(fr.getColour(sf2), expected);
 +
 +    /*
 +     * above threshold, score is above threshold - no change
 +     */
 +    gc.setAboveThreshold(true);
 +    gc.setThreshold(5f);
 +    assertEquals(fr.getColour(sf2), expected);
 +
 +    /*
 +     * threshold is min-max; now score 6 is 1/6 of the way from 5 to 11
 +     * or from yellow(255, 255, 0) to red(255, 0, 0)
 +     */
 +    gc = new FeatureColour(Color.yellow, Color.red, Color.green, 5f, 11f);
 +    fr.getFeatureColours().put("Cath", gc);
 +    gc.setAutoScaled(false); // this does little other than save a checkbox setting!
 +    assertEquals(fr.getColour(sf2), new Color(255, 213, 0));
 +
 +    /*
 +     * feature score is below threshold - no colour
 +     */
 +    gc.setAboveThreshold(true);
 +    gc.setThreshold(7f);
 +    assertNull(fr.getColour(sf2));
 +
 +    /*
 +     * feature score is above threshold - no colour
 +     */
 +    gc.setBelowThreshold(true);
 +    gc.setThreshold(3f);
 +    assertNull(fr.getColour(sf2));
 +
 +    /*
 +     * colour by feature attribute value
 +     * first with no value held
 +     */
 +    gc = new FeatureColour(Color.yellow, Color.red, Color.green, 1f, 11f);
 +    fr.getFeatureColours().put("Cath", gc);
 +    gc.setAttributeName("AF");
 +    assertEquals(fr.getColour(sf2), Color.green);
 +
 +    // with non-numeric attribute value
 +    sf2.setValue("AF", "Five");
 +    assertEquals(fr.getColour(sf2), Color.green);
 +
 +    // with numeric attribute value
 +    sf2.setValue("AF", "6");
 +    assertEquals(fr.getColour(sf2), expected);
 +
 +    // with numeric value outwith threshold
 +    gc.setAboveThreshold(true);
 +    gc.setThreshold(10f);
 +    assertNull(fr.getColour(sf2));
 +
 +    // with filter on AF < 4
 +    gc.setAboveThreshold(false);
 +    assertEquals(fr.getColour(sf2), expected);
 +    FeatureMatcherSetI filter = new FeatureMatcherSet();
 +    filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF"));
 +    fr.setFeatureFilter("Cath", filter);
 +    assertNull(fr.getColour(sf2));
 +
 +    // with filter on 'Consequence contains missense'
 +    filter = new FeatureMatcherSet();
 +    filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense",
 +            "Consequence"));
 +    fr.setFeatureFilter("Cath", filter);
 +    // if feature has no Consequence attribute, no colour
 +    assertNull(fr.getColour(sf2));
 +    // if attribute does not match filter, no colour
 +    sf2.setValue("Consequence", "Synonymous");
 +    assertNull(fr.getColour(sf2));
 +    // attribute matches filter
 +    sf2.setValue("Consequence", "Missense variant");
 +    assertEquals(fr.getColour(sf2), expected);
 +
 +    // with filter on CSQ:Feature contains "ENST01234"
 +    filter = new FeatureMatcherSet();
 +    filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234",
 +            "CSQ", "Feature"));
 +    fr.setFeatureFilter("Cath", filter);
 +    // if feature has no CSQ data, no colour
 +    assertNull(fr.getColour(sf2));
 +    // if CSQ data does not include Feature, no colour
 +    Map<String, String> csqData = new HashMap<>();
 +    csqData.put("BIOTYPE", "Transcript");
 +    sf2.setValue("CSQ", csqData);
 +    assertNull(fr.getColour(sf2));
 +    // if attribute does not match filter, no colour
 +    csqData.put("Feature", "ENST9876");
 +    assertNull(fr.getColour(sf2));
 +    // attribute matches filter
 +    csqData.put("Feature", "ENST01234");
 +    assertEquals(fr.getColour(sf2), expected);
    }
  }