<classpathentry kind="lib" path="lib/VARNAv3-93.jar"/>
<classpathentry kind="lib" path="lib/jfreesvg-2.1.jar"/>
<classpathentry kind="lib" path="lib/quaqua-filechooser-only-8.0.jar"/>
- <classpathentry kind="lib" path="lib/htsjdk-1.133.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/plugin"/>
<classpathentry kind="lib" path="lib/xml-apis.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
- <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Plugin.jar"/>
<classpathentry kind="lib" path="lib/jersey-client-1.19.jar"/>
<classpathentry kind="lib" path="lib/jersey-core-1.19.jar"/>
<classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
<classpathentry kind="lib" path="lib/biojava-core-4.1.0.jar"/>
<classpathentry kind="lib" path="lib/biojava-ontology-4.1.0.jar"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+ <classpathentry kind="lib" path="lib/htsjdk-2.12.0.jar"/>
<classpathentry kind="lib" path="lib/groovy-all-2.4.12-indy.jar"/>
<classpathentry kind="output" path="classes"/>
</classpath>
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=52
<!-- 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="1.7+" initial-heap-size="${inih}" max-heap-size="${maxh}" java-vm-args="--add-modules=java.se.ee --illegal-access=warn"">
- <replacetoken>j2se version="1.7+"</replacetoken>
-
- </replace>
+ <replace file="${jnlpFile}" value="j2se version="1.8+" initial-heap-size="${inih}" max-heap-size="${maxh}" java-vm-args="--add-modules=java.se.ee --illegal-access=warn"">
+ <replacetoken>j2se version="1.8+"</replacetoken>
+ </replace>
</target>
<target name="-dofakejnlpfileassoc" depends="-generatejnlpf" if="nojnlpfileassocs">
<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" />
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
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}
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
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
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}
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 ";"
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
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
\ No newline at end of file
+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
\ No newline at end of file
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
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
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
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
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}
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 ";"
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
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 = Marque para buscar automáticamente
+option.autosearch = Búsqueda automática
+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
\ No newline at end of file
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;
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;
{
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)
productSeqs = new HashSet<SequenceI>();
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.
}
/**
- * 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<DBRefEntry>();
HashSet<String> directSources = new HashSet<String>();
if (contig.getDBRefs() != null)
* 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;
* @param dnaSeq
* @return
*/
- public static List<int[]> findCdsPositions(SequenceI dnaSeq)
+ protected static List<int[]> findCdsPositions(SequenceI dnaSeq)
{
List<int[]> result = new ArrayList<int[]>();
{
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(","))
{
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(","))
{
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(","))
/**
* 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
}
/**
+ * 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.
Color getMaxColour();
/**
+ * Returns the 'no value' colour (used when a feature lacks score, or the
+ * attribute, being used for colouring)
+ *
+ * @return
+ */
+ Color getNoColour();
+
+ /**
* Answers true if the feature has a single colour, i.e. if isColourByLabel()
* and isGraduatedColour() both answer false
*
void setAboveThreshold(boolean b);
/**
- * Answers true if the threshold is the minimum value (when
- * isAboveThreshold()) or maximum value (when isBelowThreshold()) of the
- * colour range; only applicable when isGraduatedColour and either
- * isAboveThreshold() or isBelowThreshold() answers true
- *
- * @return
- */
- boolean isThresholdMinMax();
-
- void setThresholdMinMax(boolean b);
-
- /**
* Returns the threshold value (if any), else zero
*
* @return
Color getColor(SequenceFeature feature);
/**
- * Update the min-max range for a graduated colour scheme
+ * Update the min-max range for a graduated colour scheme. Note that the
+ * colour scheme may be configured to colour by feature score, or a
+ * (numeric-valued) attribute - the caller should ensure that the correct
+ * range is being set.
*
* @param min
* @param max
* @return
*/
String toJalviewFormat(String featureType);
+
+ /**
+ * Answers true if colour is by attribute text or numerical value
+ *
+ * @return
+ */
+ boolean isColourByAttribute();
+
+ /**
+ * Answers the name of the attribute (and optional sub-attribute...) used for
+ * colouring if any, or null
+ *
+ * @return
+ */
+ String[] getAttributeName();
+
+ /**
+ * Sets the name of the attribute (and optional sub-attribute...) used for
+ * colouring if any, or null to remove this property
+ *
+ * @return
+ */
+ void setAttributeName(String... name);
}
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
import java.awt.Color;
import java.awt.Graphics;
List<String> getGroups(boolean visible);
/**
- * change visibility for a range of groups
+ * Set visibility for a list of groups
*
* @param toset
* @param visible
void setGroupVisibility(List<String> toset, boolean visible);
/**
- * change visibiilty of given group
+ * Set visibility of the given feature group
*
* @param group
* @param visible
void setGroupVisibility(String group, boolean visible);
/**
- * Returns features at the specified aligned column on the given sequence.
- * Non-positional features are not included. If the column has a gap, then
- * enclosing features are included (but not contact features).
+ * Returns visible features at the specified aligned column on the given
+ * sequence. Non-positional features are not included. If the column has a gap,
+ * then enclosing features are included (but not contact features).
*
* @param sequence
* @param column
*/
float getTransparency();
+ /**
+ * Answers the filters applied to the given feature type, or null if none is
+ * set
+ *
+ * @param featureType
+ * @return
+ */
+ FeatureMatcherSetI getFeatureFilter(String featureType);
+
+ /**
+ * Answers a shallow copy of the feature filters map
+ *
+ * @return
+ */
+ public Map<String, FeatureMatcherSetI> getFeatureFilters();
+
+ /**
+ * Sets the filters for the feature type, or removes them if a null or empty
+ * filter is passed
+ *
+ * @param featureType
+ * @param filter
+ */
+ void setFeatureFilter(String featureType, FeatureMatcherSetI filter);
+
+ /**
+ * Replaces all feature filters with the given map
+ *
+ * @param filters
+ */
+ void setFeatureFilters(Map<String, FeatureMatcherSetI> filters);
+
+ /**
+ * Returns the colour for a particular feature instance. This includes
+ * calculation of 'colour by label', or of a graduated score colour, if
+ * applicable.
+ * <p>
+ * Returns null if
+ * <ul>
+ * <li>feature type is not visible, or</li>
+ * <li>feature group is not visible, or</li>
+ * <li>feature values lie outside any colour threshold, or</li>
+ * <li>feature is excluded by filter conditions</li>
+ * </ul>
+ *
+ * @param feature
+ * @return
+ */
+ Color getColour(SequenceFeature feature);
}
.formatMessage("label.annotation_for_displayid", new Object[]
{ seq.getDisplayId(true) }));
new SequenceAnnotationReport(null).createSequenceAnnotationReport(
- contents, seq, true, true,
- (ap.seqPanel.seqCanvas.fr != null)
- ? ap.seqPanel.seqCanvas.fr.getMinMax()
- : null);
+ contents, seq, true, true, ap.seqPanel.seqCanvas.fr);
contents.append("</p>");
}
Frame frame = new Frame();
*/
private static final int SCALE_FACTOR_1K = 1000;
+ private static final String COLON = ":";
+
private JVDialog frame;
private Frame owner;
slider.addAdjustmentListener(this);
slider.addMouseListener(this);
owner = (af != null) ? af : fs.frame;
- frame = new JVDialog(owner, MessageManager
- .formatMessage("label.graduated_color_for_params", new String[]
- { type }), true, 480, 248);
+ frame = new JVDialog(owner, MessageManager.formatMessage(
+ "label.variable_color_for", new String[] { type }), true, 480,
+ 248);
frame.setMainPanel(this);
validate();
frame.setVisible(true);
private void jbInit() throws Exception
{
- Label minLabel = new Label(MessageManager.getString("label.min")),
- maxLabel = new Label(MessageManager.getString("label.max"));
+ Label minLabel = new Label(
+ MessageManager.getString("label.min_value") + COLON);
+ Label maxLabel = new Label(
+ MessageManager.getString("label.max_value") + COLON);
minLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
maxLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
// minColour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
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;
{
Component[] comps = featurePanel.getComponents();
int cSize = comps.length;
-
- Object[][] tmp = new Object[cSize][3];
- int tmpSize = 0;
- for (int i = 0; i < cSize; i++)
- {
- MyCheckbox check = (MyCheckbox) comps[i];
- tmp[tmpSize][0] = check.type;
- tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
- tmp[tmpSize][2] = new Boolean(check.getState());
- tmpSize++;
+ FeatureSettingsBean[] rowData = new FeatureSettingsBean[cSize];
+ int i = 0;
+ for (Component comp : comps)
+ {
+ MyCheckbox check = (MyCheckbox) comp;
+ // feature filter set to null as not (yet) offered in applet
+ FeatureColourI colour = fr.getFeatureStyle(check.type);
+ rowData[i] = new FeatureSettingsBean(check.type, colour, null,
+ check.getState());
+ i++;
}
- Object[][] data = new Object[tmpSize][3];
- System.arraycopy(tmp, 0, data, 0, tmpSize);
-
- fr.setFeaturePriority(data);
+ fr.setFeaturePriority(rowData);
ap.paintAlignment(updateOverview, updateOverview);
}
private AlignViewControllerGuiI avcg;
public AlignViewController(AlignViewControllerGuiI alignFrame,
- AlignViewportI viewport, AlignmentViewPanel alignPanel)
+ AlignViewportI vp, AlignmentViewPanel ap)
{
this.avcg = alignFrame;
- this.viewport = viewport;
- this.alignPanel = alignPanel;
+ this.viewport = vp;
+ this.alignPanel = ap;
}
@Override
- public void setViewportAndAlignmentPanel(AlignViewportI viewport,
- AlignmentViewPanel alignPanel)
+ public void setViewportAndAlignmentPanel(AlignViewportI vp,
+ AlignmentViewPanel ap)
{
- this.alignPanel = alignPanel;
- this.viewport = viewport;
-
+ this.alignPanel = ap;
+ this.viewport = vp;
}
@Override
/**
* Sets a bit in the BitSet for each column (base 0) in the sequence
- * collection which includes the specified feature type. Returns the number of
- * sequences which have the feature in the selected range.
+ * collection which includes a visible feature of the specified feature type.
+ * Returns the number of sequences which have the feature visible in the
+ * selected range.
*
* @param featureType
* @param sqcol
* @param bs
* @return
*/
- static int findColumnsWithFeature(String featureType,
+ int findColumnsWithFeature(String featureType,
SequenceCollectionI sqcol, BitSet bs)
{
+ FeatureRenderer fr = alignPanel == null ? null : alignPanel
+ .getFeatureRenderer();
+
final int startColumn = sqcol.getStartRes() + 1; // converted to base 1
final int endColumn = sqcol.getEndRes() + 1;
List<SequenceI> seqs = sqcol.getSequences();
List<SequenceFeature> sfs = sq.findFeatures(startColumn,
endColumn, featureType);
- if (!sfs.isEmpty())
- {
- nseq++;
- }
-
+ boolean found = false;
for (SequenceFeature sf : sfs)
{
+ if (fr.getColour(sf) == null)
+ {
+ continue;
+ }
+ if (!found)
+ {
+ nseq++;
+ }
+ found = true;
+
int sfStartCol = sq.findIndex(sf.getBegin());
int sfEndCol = sq.findIndex(sf.getEnd());
public class DBRefEntry implements DBRefEntryI
{
- String source = "", version = "", accessionId = "";
+ /*
+ * the mapping to chromosome (genome) is held as an instance with
+ * source = speciesId
+ * version = assemblyId
+ * accessionId = "chromosome:" + chromosomeId
+ * map = mapping from sequence to reference assembly
+ */
+ public static final String CHROMOSOME = "chromosome";
+
+ String source = "";
+
+ String version = "";
+
+ String accessionId = "";
/**
* maps from associated sequence to the database sequence's coordinate system
}
return true;
}
+
+ /**
+ * Mappings to chromosome are held with accessionId as "chromosome:id"
+ *
+ * @return
+ */
+ public boolean isChromosome()
+ {
+ return accessionId != null && accessionId.startsWith(CHROMOSOME + ":");
+ }
}
--- /dev/null
+package jalview.datamodel;
+
+import jalview.util.MapList;
+
+/**
+ * An interface to model one or more contiguous regions on one chromosome
+ */
+public interface GeneLociI
+{
+ /**
+ * Answers the species identifier
+ *
+ * @return
+ */
+ String getSpeciesId();
+
+ /**
+ * Answers the reference assembly identifier
+ *
+ * @return
+ */
+ String getAssemblyId();
+
+ /**
+ * Answers the chromosome identifier e.g. "2", "Y", "II"
+ *
+ * @return
+ */
+ String getChromosomeId();
+
+ /**
+ * Answers the mapping from sequence to chromosome loci. For a reverse strand
+ * mapping, the chromosomal ranges will have start > end.
+ *
+ * @return
+ */
+ MapList getMap();
+}
}
/**
- * 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()
*/
package jalview.datamodel;
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureAttributes;
import jalview.datamodel.features.FeatureLocationI;
+import jalview.datamodel.features.FeatureSourceI;
+import jalview.datamodel.features.FeatureSources;
+import jalview.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.TreeMap;
import java.util.Vector;
/**
- * DOCUMENT ME!
- *
- * @author $author$
- * @version $Revision$
+ * A class that models a single contiguous feature on a sequence. If flag
+ * 'contactFeature' is true, the start and end positions are interpreted instead
+ * as two contact points.
*/
public class SequenceFeature implements FeatureLocationI
{
// private key for ENA location designed not to conflict with real GFF data
private static final String LOCATION = "!Location";
+ private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
+
/*
* ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
* name1=value1;name2=value2,value3;...etc
public Vector<String> links;
+ /*
+ * the identifier (if known) for the FeatureSource held in FeatureSources,
+ * as a provider of metadata about feature attributes
+ */
+ private String source;
+
/**
* Constructs a duplicate feature. Note: Uses makes a shallow copy of the
* otherDetails map, so the new and original SequenceFeature may reference the
this(newType, sf.getDescription(), newBegin, newEnd, newScore,
newGroup);
+ this.source = sf.source;
+
if (sf.otherDetails != null)
{
- otherDetails = new HashMap<String, Object>();
+ otherDetails = new HashMap<>();
for (Entry<String, Object> entry : sf.otherDetails.entrySet())
{
otherDetails.put(entry.getKey(), entry.getValue());
}
if (sf.links != null && sf.links.size() > 0)
{
- links = new Vector<String>();
+ links = new Vector<>();
for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
{
links.addElement(sf.links.elementAt(i));
{
if (links == null)
{
- links = new Vector<String>();
+ links = new Vector<>();
}
if (!links.contains(labelLink))
}
/**
+ * Answers the value of the specified attribute as string, or null if no such
+ * value. If more than one attribute name is provided, tries to resolve as keys
+ * to nested maps. For example, if attribute "CSQ" holds a map of key-value
+ * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
+ * in that map.
+ *
+ * @param key
+ * @return
+ */
+ public String getValueAsString(String... key)
+ {
+ if (otherDetails == null)
+ {
+ return null;
+ }
+ Object value = otherDetails.get(key[0]);
+ if (key.length > 1 && value instanceof Map<?, ?>)
+ {
+ value = ((Map) value).get(key[1]);
+ }
+ return value == null ? null : value.toString();
+ }
+
+ /**
* Returns a property value for the given key if known, else the specified
* default value
*
{
if (otherDetails == null)
{
- otherDetails = new HashMap<String, Object>();
+ otherDetails = new HashMap<>();
}
otherDetails.put(key, value);
+ recordAttribute(key, value);
}
}
+ /**
+ * Notifies the addition of a feature attribute. This lets us keep track of
+ * which attributes are present on each feature type, and also the range of
+ * numerical-valued attributes.
+ *
+ * @param key
+ * @param value
+ */
+ protected void recordAttribute(String key, Object value)
+ {
+ String attDesc = null;
+ if (source != null)
+ {
+ attDesc = FeatureSources.getInstance().getSource(source)
+ .getAttributeName(key);
+ }
+
+ FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
+ key);
+ }
+
/*
* The following methods are added to maintain the castor Uniprot mapping file
* for the moment.
{
return begin == 0 && end == 0;
}
+
+ /**
+ * Answers an html-formatted report of feature details
+ *
+ * @return
+ */
+ public String getDetailsReport()
+ {
+ FeatureSourceI metadata = FeatureSources.getInstance()
+ .getSource(source);
+
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("<br>");
+ sb.append("<table>");
+ sb.append(String.format(ROW_DATA, "Type", type, ""));
+ sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
+ : begin + (isContactFeature() ? ":" : "-") + end, ""));
+ String desc = StringUtils.stripHtmlTags(description);
+ sb.append(String.format(ROW_DATA, "Description", desc, ""));
+ if (!Float.isNaN(score) && score != 0f)
+ {
+ sb.append(String.format(ROW_DATA, "Score", score, ""));
+ }
+ if (featureGroup != null)
+ {
+ sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
+ }
+
+ if (otherDetails != null)
+ {
+ TreeMap<String, Object> ordered = new TreeMap<>(
+ String.CASE_INSENSITIVE_ORDER);
+ ordered.putAll(otherDetails);
+
+ for (Entry<String, Object> entry : ordered.entrySet())
+ {
+ String key = entry.getKey();
+ if (ATTRIBUTES.equals(key))
+ {
+ continue; // to avoid double reporting
+ }
+
+ Object value = entry.getValue();
+ if (value instanceof Map<?, ?>)
+ {
+ /*
+ * expand values in a Map attribute across separate lines
+ */
+ Map<?, ?> values = (Map<?, ?>) value;
+ for (Entry<?, ?> e : values.entrySet())
+ {
+ sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
+ .getValue().toString()));
+ }
+ }
+ else
+ {
+ // tried <td title="key"> but it failed to provide a tooltip :-(
+ String attDesc = null;
+ if (metadata != null)
+ {
+ attDesc = metadata.getAttributeName(key);
+ }
+ String s = entry.getValue().toString();
+ if (isValueInteresting(key, s, metadata))
+ {
+ sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
+ : attDesc, s));
+ }
+ }
+ }
+ }
+ sb.append("</table>");
+
+ String text = sb.toString();
+ return text;
+ }
+
+ /**
+ * Answers true if we judge the value is worth displaying, by some heuristic
+ * rules, else false
+ *
+ * @param key
+ * @param value
+ * @param metadata
+ * @return
+ */
+ boolean isValueInteresting(String key, String value,
+ FeatureSourceI metadata)
+ {
+ /*
+ * currently suppressing zero values as well as null or empty
+ */
+ if (value == null || "".equals(value) || ".".equals(value)
+ || "0".equals(value))
+ {
+ return false;
+ }
+
+ if (metadata == null)
+ {
+ return true;
+ }
+
+ FeatureAttributeType attType = metadata.getAttributeType(key);
+ if (attType != null
+ && (attType == FeatureAttributeType.Float || attType
+ .equals(FeatureAttributeType.Integer)))
+ {
+ try
+ {
+ float fval = Float.valueOf(value);
+ if (fval == 0f)
+ {
+ return false;
+ }
+ } catch (NumberFormatException e)
+ {
+ // ignore
+ }
+ }
+
+ return true; // default to interesting
+ }
+
+ /**
+ * Sets the feature source identifier
+ *
+ * @param theSource
+ */
+ public void setSource(String theSource)
+ {
+ source = theSource;
+ }
}
package jalview.datamodel;
import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.MapList;
import java.util.BitSet;
import java.util.List;
* @param c2
*/
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);
}
--- /dev/null
+package jalview.datamodel.features;
+
+/**
+ * A class to model the datatype of feature attributes.
+ *
+ * @author gmcarstairs
+ *
+ */
+public enum FeatureAttributeType
+{
+ String, Integer, Float, Character, Flag;
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * A singleton class to hold the set of attributes known for each feature type
+ */
+public class FeatureAttributes
+{
+ public enum Datatype
+ {
+ Character, Number, Mixed
+ }
+
+ private static FeatureAttributes instance = new FeatureAttributes();
+
+ /*
+ * map, by feature type, of a map, by attribute name, of
+ * attribute description and min-max range (if known)
+ */
+ private Map<String, Map<String[], AttributeData>> attributes;
+
+ /*
+ * a case-insensitive comparator so that attributes are ordered e.g.
+ * AC
+ * af
+ * CSQ:AFR_MAF
+ * CSQ:Allele
+ */
+ private Comparator<String[]> comparator = new Comparator<String[]>()
+ {
+ @Override
+ public int compare(String[] o1, String[] o2)
+ {
+ int i = 0;
+ while (i < o1.length || i < o2.length)
+ {
+ if (o2.length <= i)
+ {
+ return o1.length <= i ? 0 : 1;
+ }
+ if (o1.length <= i)
+ {
+ return -1;
+ }
+ int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
+ if (comp != 0)
+ {
+ return comp;
+ }
+ i++;
+ }
+ return 0; // same length and all matched
+ }
+ };
+
+ private class AttributeData
+ {
+ /*
+ * description(s) for this attribute, if known
+ * (different feature source might have differing descriptions)
+ */
+ List<String> description;
+
+ /*
+ * minimum value (of any numeric values recorded)
+ */
+ float min = 0f;
+
+ /*
+ * maximum value (of any numeric values recorded)
+ */
+ float max = 0f;
+
+ /*
+ * flag is set true if any numeric value is detected for this attribute
+ */
+ boolean hasValue = false;
+
+ Datatype type;
+
+ /**
+ * Note one instance of this attribute, recording unique, non-null names,
+ * and the min/max of any numerical values
+ *
+ * @param desc
+ * @param value
+ */
+ void addInstance(String desc, String value)
+ {
+ addDescription(desc);
+
+ if (value != null)
+ {
+ try
+ {
+ float f = Float.valueOf(value);
+ min = hasValue ? Float.min(min, f) : f;
+ max = hasValue ? Float.max(max, f) : f;
+ hasValue = true;
+ type = (type == null || type == Datatype.Number) ? Datatype.Number
+ : Datatype.Mixed;
+ } catch (NumberFormatException e)
+ {
+ // not a number, ignore for min-max purposes
+ type = (type == null || type == Datatype.Character)
+ ? Datatype.Character
+ : Datatype.Mixed;
+ }
+ }
+ }
+
+ /**
+ * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
+ * @return
+ */
+ public String getDescription()
+ {
+ if (description != null && description.size() == 1)
+ {
+ return description.get(0);
+ }
+ return null;
+ }
+
+ public Datatype getType()
+ {
+ return type;
+ }
+
+ /**
+ * Adds the given description to the list of known descriptions (without
+ * duplication)
+ *
+ * @param desc
+ */
+ public void addDescription(String desc)
+ {
+ if (desc != null)
+ {
+ if (description == null)
+ {
+ description = new ArrayList<>();
+ }
+ if (!description.contains(desc))
+ {
+ description.add(desc);
+ }
+ }
+ }
+ }
+
+ /**
+ * Answers the singleton instance of this class
+ *
+ * @return
+ */
+ public static FeatureAttributes getInstance()
+ {
+ return instance;
+ }
+
+ private FeatureAttributes()
+ {
+ attributes = new HashMap<>();
+ }
+
+ /**
+ * Answers the attribute names known for the given feature type, in
+ * alphabetical order (not case sensitive), or an empty set if no attributes
+ * are known. An attribute name is typically 'simple' e.g. "AC", but may be
+ * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
+ *
+ * @param featureType
+ * @return
+ */
+ public List<String[]> getAttributes(String featureType)
+ {
+ if (!attributes.containsKey(featureType))
+ {
+ return Collections.<String[]> emptyList();
+ }
+
+ return new ArrayList<>(attributes.get(featureType).keySet());
+ }
+
+ /**
+ * Answers true if at least one attribute is known for the given feature type,
+ * else false
+ *
+ * @param featureType
+ * @return
+ */
+ public boolean hasAttributes(String featureType)
+ {
+ if (attributes.containsKey(featureType))
+ {
+ if (!attributes.get(featureType).isEmpty())
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Records the given attribute name and description for the given feature
+ * type, and updates the min-max for any numeric value
+ *
+ * @param featureType
+ * @param description
+ * @param value
+ * @param attName
+ */
+ public void addAttribute(String featureType, String description,
+ Object value, String... attName)
+ {
+ if (featureType == null || attName == null)
+ {
+ return;
+ }
+
+ /*
+ * if attribute value is a map, drill down one more level to
+ * record its sub-fields
+ */
+ if (value instanceof Map<?, ?>)
+ {
+ for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+ {
+ String[] attNames = new String[attName.length + 1];
+ System.arraycopy(attName, 0, attNames, 0, attName.length);
+ attNames[attName.length] = entry.getKey().toString();
+ addAttribute(featureType, description, entry.getValue(), attNames);
+ }
+ return;
+ }
+
+ String valueAsString = value.toString();
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts == null)
+ {
+ atts = new TreeMap<>(comparator);
+ attributes.put(featureType, atts);
+ }
+ AttributeData attData = atts.get(attName);
+ if (attData == null)
+ {
+ attData = new AttributeData();
+ atts.put(attName, attData);
+ }
+ attData.addInstance(description, valueAsString);
+ }
+
+ /**
+ * Answers the description of the given attribute for the given feature type,
+ * if known and unique, else null
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public String getDescription(String featureType, String... attName)
+ {
+ String desc = null;
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null)
+ {
+ desc = attData.getDescription();
+ }
+ }
+ return desc;
+ }
+
+ /**
+ * Answers the [min, max] value range of the given attribute for the given
+ * feature type, if known, else null. Attributes which only have text values
+ * would normally return null, however text values which happen to be numeric
+ * could result in a 'min-max' range.
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public float[] getMinMax(String featureType, String... attName)
+ {
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null && attData.hasValue)
+ {
+ return new float[] { attData.min, attData.max };
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Records the given attribute description for the given feature type
+ *
+ * @param featureType
+ * @param attName
+ * @param description
+ */
+ public void addDescription(String featureType, String description,
+ String... attName)
+ {
+ if (featureType == null || attName == null)
+ {
+ return;
+ }
+
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts == null)
+ {
+ atts = new TreeMap<>(comparator);
+ attributes.put(featureType, atts);
+ }
+ AttributeData attData = atts.get(attName);
+ if (attData == null)
+ {
+ attData = new AttributeData();
+ atts.put(attName, attData);
+ }
+ attData.addDescription(description);
+ }
+
+ /**
+ * Answers the datatype of the feature, which is one of Character, Number or
+ * Mixed (or null if not known), as discovered from values recorded.
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public Datatype getDatatype(String featureType, String... attName)
+ {
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null)
+ {
+ return attData.getType();
+ }
+ }
+ return null;
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.Matcher;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An immutable class that models one or more match conditions, each of which is
+ * applied to the value obtained by lookup given the match key.
+ * <p>
+ * For example, the value provider could be a SequenceFeature's attributes map,
+ * and the conditions might be
+ * <ul>
+ * <li>CSQ contains "pathological"</li>
+ * <li>AND</li>
+ * <li>AF <= 1.0e-5</li>
+ * </ul>
+ *
+ * @author gmcarstairs
+ *
+ */
+public class FeatureMatcher implements FeatureMatcherI
+{
+ /*
+ * a dummy matcher that comes in useful for the 'add a filter' gui row
+ */
+ public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
+ .byLabel(Condition.values()[0], "");
+
+ private static final String COLON = ":";
+
+ /*
+ * if true, match is against feature description
+ */
+ final private boolean byLabel;
+
+ /*
+ * if true, match is against feature score
+ */
+ final private boolean byScore;
+
+ /*
+ * if not null, match is against feature attribute [sub-attribute]
+ */
+ final private String[] key;
+
+ final private MatcherI matcher;
+
+ /**
+ * A factory constructor method for a matcher that applies its match condition
+ * to the feature label (description)
+ *
+ * @param cond
+ * @param pattern
+ * @return
+ */
+ public static FeatureMatcher byLabel(Condition cond, String pattern)
+ {
+ return new FeatureMatcher(new Matcher(cond, pattern), true, false,
+ null);
+ }
+
+ /**
+ * A factory constructor method for a matcher that applies its match condition
+ * to the feature score
+ *
+ * @param cond
+ * @param pattern
+ * @return
+ */
+ public static FeatureMatcher byScore(Condition cond, String pattern)
+ {
+ return new FeatureMatcher(new Matcher(cond, pattern), false, true,
+ null);
+ }
+
+ /**
+ * A factory constructor method for a matcher that applies its match condition
+ * to the named feature attribute [and optional sub-attribute]
+ *
+ * @param cond
+ * @param pattern
+ * @param attName
+ * @return
+ */
+ public static FeatureMatcher byAttribute(Condition cond, String pattern,
+ String... attName)
+ {
+ return new FeatureMatcher(new Matcher(cond, pattern), false, false,
+ attName);
+ }
+
+ private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
+ String[] theKey)
+ {
+ key = theKey;
+ matcher = m;
+ byLabel = forLabel;
+ byScore = forScore;
+ }
+ @Override
+ public boolean matches(SequenceFeature feature)
+ {
+ String value = byLabel ? feature.getDescription()
+ : (byScore ? String.valueOf(feature.getScore())
+ : feature.getValueAsString(key));
+ return matcher.matches(value);
+ }
+
+ @Override
+ public String[] getAttribute()
+ {
+ return key;
+ }
+
+ @Override
+ public MatcherI getMatcher()
+ {
+ return matcher;
+ }
+
+ /**
+ * Answers a string description of this matcher, suitable for display, debugging
+ * or logging. The format may change in future.
+ */
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ if (byLabel)
+ {
+ sb.append(MessageManager.getString("label.label"));
+ }
+ else if (byScore)
+ {
+ sb.append(MessageManager.getString("label.score"));
+ }
+ else
+ {
+ sb.append(String.join(COLON, key));
+ }
+
+ Condition condition = matcher.getCondition();
+ sb.append(" ").append(condition.toString().toLowerCase());
+ if (condition.isNumeric())
+ {
+ sb.append(" ").append(matcher.getPattern());
+ }
+ else if (condition.needsAPattern())
+ {
+ sb.append(" '").append(matcher.getPattern()).append("'");
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public boolean isByLabel()
+ {
+ return byLabel;
+ }
+
+ @Override
+ public boolean isByScore()
+ {
+ return byScore;
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An interface for an object that can apply a match condition to a
+ * SequenceFeature object
+ *
+ * @author gmcarstairs
+ */
+public interface FeatureMatcherI
+{
+ /**
+ * Answers true if the value provided for this matcher's key passes this
+ * matcher's match condition
+ *
+ * @param feature
+ * @return
+ */
+ boolean matches(SequenceFeature feature);
+
+ /**
+ * Answers the attribute key this matcher operates on (or null if match is by
+ * Label or Score)
+ *
+ * @return
+ */
+ String[] getAttribute();
+
+ /**
+ * Answers true if match is against feature label (description), else false
+ *
+ * @return
+ */
+ boolean isByLabel();
+
+ /**
+ * Answers true if match is against feature score, else false
+ *
+ * @return
+ */
+ boolean isByScore();
+
+ /**
+ * Answers the match condition that is applied
+ *
+ * @return
+ */
+ MatcherI getMatcher();
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FeatureMatcherSet implements FeatureMatcherSetI
+{
+ private static final String OR_I18N = MessageManager
+ .getString("label.or");
+
+ private static final String AND_18N = MessageManager
+ .getString("label.and");
+
+ List<FeatureMatcherI> matchConditions;
+
+ boolean andConditions;
+
+ /**
+ * Constructor
+ */
+ public FeatureMatcherSet()
+ {
+ matchConditions = new ArrayList<>();
+ }
+
+ @Override
+ public boolean matches(SequenceFeature feature)
+ {
+ /*
+ * no conditions matches anything
+ */
+ if (matchConditions.isEmpty())
+ {
+ return true;
+ }
+
+ /*
+ * AND until failure
+ */
+ if (andConditions)
+ {
+ for (FeatureMatcherI m : matchConditions)
+ {
+ if (!m.matches(feature))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * OR until match
+ */
+ for (FeatureMatcherI m : matchConditions)
+ {
+ if (m.matches(feature))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public FeatureMatcherSetI and(FeatureMatcherI m)
+ {
+ if (!andConditions && matchConditions.size() > 1)
+ {
+ throw new IllegalStateException("Can't add an AND to OR conditions");
+ }
+ matchConditions.add(m);
+ andConditions = true;
+
+ return this;
+ }
+
+ @Override
+ public FeatureMatcherSetI or(FeatureMatcherI m)
+ {
+ if (andConditions && matchConditions.size() > 1)
+ {
+ throw new IllegalStateException("Can't add an OR to AND conditions");
+ }
+ matchConditions.add(m);
+ andConditions = false;
+
+ return this;
+ }
+
+ @Override
+ public boolean isAnded()
+ {
+ return andConditions;
+ }
+
+ @Override
+ public Iterable<FeatureMatcherI> getMatchers()
+ {
+ return matchConditions;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (FeatureMatcherI matcher : matchConditions)
+ {
+ if (!first)
+ {
+ String joiner = andConditions ? AND_18N : OR_I18N;
+ sb.append(" ").append(joiner.toLowerCase()).append(" ");
+ }
+ first = false;
+ sb.append("(").append(matcher.toString()).append(")");
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public boolean isEmpty()
+ {
+ return matchConditions == null || matchConditions.isEmpty();
+ }
+
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+/**
+ * An interface to describe a set of one or more feature matchers, where all
+ * matchers are combined with either AND or OR
+ *
+ * @author gmcarstairs
+ *
+ */
+public interface FeatureMatcherSetI
+{
+ /**
+ * Answers true if the feature provided passes this matcher's match condition
+ *
+ * @param feature
+ * @return
+ */
+ boolean matches(SequenceFeature feature);
+
+ /**
+ * Answers a new object that matches the logical AND of this and m
+ *
+ * @param m
+ * @return
+ * @throws IllegalStateException
+ * if an attempt is made to AND to existing OR-ed conditions
+ */
+ FeatureMatcherSetI and(FeatureMatcherI m);
+
+ /**
+ * Answers true if any second condition is AND-ed with this one, false if it
+ * is OR-ed
+ *
+ * @return
+ */
+ boolean isAnded();
+
+ /**
+ * Answers a new object that matches the logical OR of this and m
+ *
+ * @param m
+ * @return
+ * @throws IllegalStateException
+ * if an attempt is made to OR to existing AND-ed conditions
+ */
+ FeatureMatcherSetI or(FeatureMatcherI m);
+
+ /**
+ * Answers an iterator over the combined match conditions
+ *
+ * @return
+ */
+ Iterable<FeatureMatcherI> getMatchers();
+
+ /**
+ * Answers true if this object contains no conditions
+ *
+ * @return
+ */
+ boolean isEmpty();
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class to model one source of feature data, including metadata about
+ * attributes of features
+ *
+ * @author gmcarstairs
+ *
+ */
+public class FeatureSource implements FeatureSourceI
+{
+ private String name;
+
+ private Map<String, String> attributeNames;
+
+ private Map<String, FeatureAttributeType> attributeTypes;
+
+ /**
+ * Constructor
+ *
+ * @param theName
+ */
+ public FeatureSource(String theName)
+ {
+ this.name = theName;
+ attributeNames = new HashMap<>();
+ attributeTypes = new HashMap<>();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getAttributeName(String attributeId)
+ {
+ return attributeNames.get(attributeId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public FeatureAttributeType getAttributeType(String attributeId)
+ {
+ return attributeTypes.get(attributeId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAttributeName(String id, String attName)
+ {
+ attributeNames.put(id, attName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAttributeType(String id, FeatureAttributeType type)
+ {
+ attributeTypes.put(id, type);
+ }
+
+}
--- /dev/null
+package jalview.datamodel.features;
+
+public interface FeatureSourceI
+{
+ /**
+ * Answers a name for the feature source (not necessarily unique)
+ *
+ * @return
+ */
+ String getName();
+
+ /**
+ * Answers the 'long name' of an attribute given its id (short name or
+ * abbreviation), or null if not known
+ *
+ * @param attributeId
+ * @return
+ */
+ String getAttributeName(String attributeId);
+
+ /**
+ * Sets the 'long name' of an attribute given its id (short name or
+ * abbreviation).
+ *
+ * @param id
+ * @param name
+ */
+ void setAttributeName(String id, String name);
+
+ /**
+ * Answers the datatype of the attribute with given id, or null if not known
+ *
+ * @param attributeId
+ * @return
+ */
+ FeatureAttributeType getAttributeType(String attributeId);
+
+ /**
+ * Sets the datatype of the attribute with given id
+ *
+ * @param id
+ * @param type
+ */
+ void setAttributeType(String id, FeatureAttributeType type);
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A singleton to hold metadata about feature attributes, keyed by a unique
+ * feature source identifier
+ *
+ * @author gmcarstairs
+ *
+ */
+public class FeatureSources
+{
+ private static FeatureSources instance = new FeatureSources();
+
+ private Map<String, FeatureSourceI> sources;
+
+ /**
+ * Answers the singleton instance of this class
+ *
+ * @return
+ */
+ public static FeatureSources getInstance()
+ {
+ return instance;
+ }
+
+ private FeatureSources()
+ {
+ sources = new HashMap<>();
+ }
+
+ /**
+ * Answers the FeatureSource with the given unique identifier, or null if not
+ * known
+ *
+ * @param sourceId
+ * @return
+ */
+ public FeatureSourceI getSource(String sourceId)
+ {
+ return sources.get(sourceId);
+ }
+
+ /**
+ * Adds the given source under the given key. This will replace any existing
+ * source with the same id, it is the caller's responsibility to ensure keys
+ * are unique if necessary.
+ *
+ * @param sourceId
+ * @param source
+ */
+ public void addSource(String sourceId, FeatureSource source)
+ {
+ sources.put(sourceId, source);
+ }
+}
--- /dev/null
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ *
+ * This file is part of Jalview.
+ *
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * Jalview is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.ext.ensembl;
+
+/**
+ * A data class to model the data and rest version of one Ensembl domain,
+ * currently for rest.ensembl.org and rest.ensemblgenomes.org
+ *
+ * @author gmcarstairs
+ */
+class EnsemblData
+{
+ /*
+ * The http domain this object is holding data values for
+ */
+ String domain;
+
+ /*
+ * The latest version Jalview has tested for, e.g. "4.5"; a minor version change should be
+ * ok, a major version change may break stuff
+ */
+ String expectedRestVersion;
+
+ /*
+ * Major / minor / point version e.g. "4.5.1"
+ * @see http://rest.ensembl.org/info/rest/?content-type=application/json
+ */
+ String restVersion;
+
+ /*
+ * data version
+ * @see http://rest.ensembl.org/info/data/?content-type=application/json
+ */
+ String dataVersion;
+
+ /*
+ * true when http://rest.ensembl.org/info/ping/?content-type=application/json
+ * returns response code 200 and not {"error":"Database is unavailable"}
+ */
+ boolean restAvailable;
+
+ /*
+ * absolute time when availability was last checked
+ */
+ long lastAvailableCheckTime;
+
+ /*
+ * absolute time when version numbers were last checked
+ */
+ long lastVersionCheckTime;
+
+ // flag set to true if REST major version is not the one expected
+ boolean restMajorVersionMismatch;
+
+ /*
+ * absolute time to wait till if we overloaded the REST service
+ */
+ long retryAfter;
+
+ /**
+ * Constructor given expected REST version number e.g 4.5 or 3.4.3
+ *
+ * @param restExpected
+ */
+ EnsemblData(String theDomain, String restExpected)
+ {
+ domain = theDomain;
+ expectedRestVersion = restExpected;
+ lastAvailableCheckTime = -1;
+ lastVersionCheckTime = -1;
+ }
+
+}
import jalview.api.FeatureColourI;
import jalview.api.FeatureSettingsModelI;
import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
{
continue;
}
+
if (geneAlignment.getHeight() == 1)
{
+ findGeneLoci(geneAlignment.getSequenceAt(0), geneId);
getTranscripts(geneAlignment, geneId);
}
if (al == null)
}
/**
+ * Calls the /lookup/id REST service, parses the response for gene
+ * coordinates, and if successful, adds these to the sequence. If this fails,
+ * fall back on trying to parse the sequence description in case it is in
+ * Ensembl-gene format e.g. chromosome:GRCh38:17:45051610:45109016:1.
+ *
+ * @param seq
+ * @param geneId
+ */
+ void findGeneLoci(SequenceI seq, String geneId)
+ {
+ GeneLociI geneLoci = new EnsemblLookup(getDomain()).getGeneLoci(geneId);
+ if (geneLoci != null)
+ {
+ seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+ geneLoci.getChromosomeId(), geneLoci.getMap());
+ }
+ else
+ {
+ parseChromosomeLocations(seq);
+ }
+ }
+
+ /**
+ * Parses and saves fields of an Ensembl-style description e.g.
+ * chromosome:GRCh38:17:45051610:45109016:1
+ *
+ * @param seq
+ */
+ boolean parseChromosomeLocations(SequenceI seq)
+ {
+ String description = seq.getDescription();
+ if (description == null)
+ {
+ return false;
+ }
+ String[] tokens = description.split(":");
+ if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME))
+ {
+ String ref = tokens[1];
+ String chrom = tokens[2];
+ try
+ {
+ int chStart = Integer.parseInt(tokens[3]);
+ int chEnd = Integer.parseInt(tokens[4]);
+ boolean forwardStrand = "1".equals(tokens[5]);
+ String species = ""; // not known here
+ int[] from = new int[] { seq.getStart(), seq.getEnd() };
+ int[] to = new int[] { forwardStrand ? chStart : chEnd,
+ forwardStrand ? chEnd : chStart };
+ MapList map = new MapList(from, to, 1, 1);
+ seq.setGeneLoci(species, ref, chrom, map);
+ return true;
+ } catch (NumberFormatException e)
+ {
+ System.err.println("Bad integers in description " + description);
+ }
+ }
+ return false;
+ }
+
+ /**
* Converts a query, which may contain one or more gene, transcript, or
* external (to Ensembl) identifiers, into a non-redundant list of gene
* identifiers.
cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(),
transcript.getDatasetSequence(), mapping, parentId);
+ mapTranscriptToChromosome(transcript, gene, mapping);
+
/*
* fetch and save cross-references
*/
}
/**
+ * If the gene has a mapping to chromosome coordinates, derive the transcript
+ * chromosome regions and save on the transcript sequence
+ *
+ * @param transcript
+ * @param gene
+ * @param mapping
+ * the mapping from gene to transcript positions
+ */
+ protected void mapTranscriptToChromosome(SequenceI transcript,
+ SequenceI gene, MapList mapping)
+ {
+ GeneLociI loci = gene.getGeneLoci();
+ if (loci == null)
+ {
+ return;
+ }
+
+ MapList geneMapping = loci.getMap();
+
+ List<int[]> exons = mapping.getFromRanges();
+ List<int[]> transcriptLoci = new ArrayList<>();
+
+ for (int[] exon : exons)
+ {
+ transcriptLoci.add(geneMapping.locateInTo(exon[0], exon[1]));
+ }
+
+ List<int[]> transcriptRange = Arrays.asList(new int[] {
+ transcript.getStart(), transcript.getEnd() });
+ MapList mapList = new MapList(transcriptRange, transcriptLoci, 1, 1);
+
+ transcript.setGeneLoci(loci.getSpeciesId(), loci.getAssemblyId(),
+ loci.getChromosomeId(), mapList);
+ }
+
+ /**
* Returns the 'transcript_id' property of the sequence feature (or null)
*
* @param feature
-/*
- * 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);
+
+ BufferedReader br = null;
+ try
+ {
+ URL url = getDivisionsUrl(ENSEMBL_GENOMES_REST);
+ if (url != null)
+ {
+ br = getHttpResponse(url, null);
+ }
+ parseResponse(br, ENSEMBL_GENOMES_REST);
+ } 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 java.util.function.Function;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
- * A client for the Ensembl lookup REST endpoint; used to find the Parent gene
- * identifier given a transcript identifier.
+ * A client for the Ensembl lookup REST endpoint
*
* @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_TRANSLATION = "Translation";
private static final String OBJECT_TYPE_TRANSCRIPT = "Transcript";
private static final String ID = "id";
private static final String OBJECT_TYPE_GENE = "Gene";
}
/**
- * Calls the Ensembl lookup REST endpoint and retrieves the 'Parent' for the
- * given identifier, or null if not found
+ * Calls the Ensembl lookup REST endpoint and returns
+ * <ul>
+ * <li>the 'id' for the identifier if its type is "Gene"</li>
+ * <li>the 'Parent' if its type is 'Transcript'</li>
+ * <ul>
+ * If the type is 'Translation', does a recursive call to this method, passing
+ * in the 'Parent' (transcript id).
*
* @param identifier
* @return
*/
public String getGeneId(String identifier)
{
+ return (String) getResult(identifier, br -> parseGeneId(br));
+ }
+
+ /**
+ * 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)
+ {
+ return (String) getResult(identifier, br -> getAttribute(br, SPECIES));
+ }
+
+ /**
+ * Calls the /lookup/id rest service and delegates parsing of the JSON
+ * response to the supplied parser
+ *
+ * @param identifier
+ * @param parser
+ * @return
+ */
+ protected Object getResult(String identifier,
+ Function<BufferedReader, Object> parser)
+ {
List<String> ids = Arrays.asList(new String[] { identifier });
BufferedReader br = null;
{
br = getHttpResponse(url, ids);
}
- return br == null ? null : parseResponse(br);
+ return br == null ? null : parser.apply(br);
} catch (IOException e)
{
// ignore
}
/**
+ * Answers the value of 'attribute' from the JSON response, or null if not
+ * found
+ *
+ * @param br
+ * @param attribute
+ * @return
+ */
+ protected String getAttribute(BufferedReader br, String attribute)
+ {
+ String value = null;
+ JSONParser jp = new JSONParser();
+ try
+ {
+ JSONObject val = (JSONObject) jp.parse(br);
+ value = val.get(attribute).toString();
+ } catch (ParseException | NullPointerException | IOException e)
+ {
+ // ignore
+ }
+ return value;
+ }
+
+ /**
* 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
*
* @param br
* @return
- * @throws IOException
*/
- protected String parseResponse(BufferedReader br) throws IOException
+ protected String parseGeneId(BufferedReader br)
{
String geneId = null;
JSONParser jp = new JSONParser();
+ " looping on Parent!");
}
}
- } catch (ParseException e)
+ } catch (ParseException | IOException e)
{
// ignore
}
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 (GeneLociI) getResult(geneId, br -> parseGeneLoci(br));
+ }
+
+ /**
+ * 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 br
+ * @return
+ */
+ GeneLociI parseGeneLoci(BufferedReader br)
+ {
+ JSONParser jp = new JSONParser();
+ try
+ {
+ JSONObject val = (JSONObject) jp.parse(br);
+ final String species = val.get("species").toString();
+ final String assembly = val.get("assembly_name").toString();
+ final String chromosome = val.get("seq_region_name").toString();
+ String strand = val.get("strand").toString();
+ int start = Integer.parseInt(val.get("start").toString());
+ int end = Integer.parseInt(val.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()
+ {
+
+ @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 (ParseException | NullPointerException | IOException
+ | NumberFormatException | ClassCastException e)
+ {
+ Cache.log.error("Error looking up gene loci: " + e.getMessage());
+ }
+ return null;
+ }
+
}
--- /dev/null
+package jalview.ext.ensembl;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
+import jalview.util.MapList;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public class EnsemblMap extends EnsemblRestClient
+{
+ private static final String MAPPED = "mapped";
+
+ private static final String MAPPINGS = "mappings";
+
+ private static final String CDS = "cds";
+
+ private static final String CDNA = "cdna";
+
+ /**
+ * Default constructor (to use rest.ensembl.org)
+ */
+ public EnsemblMap()
+ {
+ super();
+ }
+
+ /**
+ * Constructor given the target domain to fetch data from
+ *
+ * @param
+ */
+ public EnsemblMap(String domain)
+ {
+ super(domain);
+ }
+
+ @Override
+ public String getDbName()
+ {
+ return DBRefSource.ENSEMBL;
+ }
+
+ @Override
+ public AlignmentI getSequenceRecords(String queries) throws Exception
+ {
+ return null; // not used
+ }
+
+ /**
+ * Constructs a URL of the format <code>
+ * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json
+ * </code>
+ *
+ * @param species
+ * @param chromosome
+ * @param fromRef
+ * @param toRef
+ * @param startPos
+ * @param endPos
+ * @return
+ * @throws MalformedURLException
+ */
+ protected URL getAssemblyMapUrl(String species, String chromosome, String fromRef,
+ String toRef, int startPos, int endPos)
+ throws MalformedURLException
+ {
+ /*
+ * start-end might be reverse strand - present forwards to the service
+ */
+ boolean forward = startPos <= endPos;
+ int start = forward ? startPos : endPos;
+ int end = forward ? endPos : startPos;
+ String strand = forward ? "1" : "-1";
+ String url = String.format(
+ "%s/map/%s/%s/%s:%d..%d:%s/%s?content-type=application/json",
+ getDomain(), species, fromRef, chromosome, start, end, strand,
+ toRef);
+ return new URL(url);
+ }
+
+ @Override
+ protected boolean useGetRequest()
+ {
+ return true;
+ }
+
+ @Override
+ protected String getRequestMimeType(boolean multipleIds)
+ {
+ return "application/json";
+ }
+
+ @Override
+ protected String getResponseMimeType()
+ {
+ return "application/json";
+ }
+
+ @Override
+ protected URL getUrl(List<String> ids) throws MalformedURLException
+ {
+ return null; // not used
+ }
+
+ /**
+ * Calls the REST /map service to get the chromosomal coordinates (start/end)
+ * in 'toRef' that corresponding to the (start/end) queryRange in 'fromRef'
+ *
+ * @param species
+ * @param chromosome
+ * @param fromRef
+ * @param toRef
+ * @param queryRange
+ * @return
+ * @see http://rest.ensemblgenomes.org/documentation/info/assembly_map
+ */
+ public int[] getAssemblyMapping(String species, String chromosome,
+ String fromRef, String toRef, int[] queryRange)
+ {
+ URL url = null;
+ BufferedReader br = null;
+
+ try
+ {
+ url = getAssemblyMapUrl(species, chromosome, fromRef, toRef, queryRange[0],
+ queryRange[1]);
+ br = getHttpResponse(url, null);
+ return (parseAssemblyMappingResponse(br));
+ } catch (Throwable t)
+ {
+ System.out.println("Error calling " + url + ": " + t.getMessage());
+ return null;
+ } finally
+ {
+ if (br != null)
+ {
+ try
+ {
+ br.close();
+ } catch (IOException e)
+ {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the JSON response from the /map/<species>/ REST service. The
+ * format is (with some fields omitted)
+ *
+ * <pre>
+ * {"mappings":
+ * [{
+ * "original": {"end":45109016,"start":45051610},
+ * "mapped" : {"end":43186384,"start":43128978}
+ * }] }
+ * </pre>
+ *
+ * @param br
+ * @return
+ */
+ protected int[] parseAssemblyMappingResponse(BufferedReader br)
+ {
+ int[] result = null;
+ JSONParser jp = new JSONParser();
+
+ try
+ {
+ JSONObject parsed = (JSONObject) jp.parse(br);
+ JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
+
+ Iterator rvals = mappings.iterator();
+ while (rvals.hasNext())
+ {
+ // todo check for "mapped"
+ JSONObject val = (JSONObject) rvals.next();
+ JSONObject mapped = (JSONObject) val.get(MAPPED);
+ int start = Integer.parseInt(mapped.get("start").toString());
+ int end = Integer.parseInt(mapped.get("end").toString());
+ String strand = mapped.get("strand").toString();
+ if ("1".equals(strand))
+ {
+ result = new int[] { start, end };
+ }
+ else
+ {
+ result = new int[] { end, start };
+ }
+ }
+ } catch (IOException | ParseException | NumberFormatException e)
+ {
+ // ignore
+ }
+ return result;
+ }
+
+ /**
+ * Calls the REST /map/cds/id service, and returns a DBRefEntry holding the
+ * returned chromosomal coordinates, or returns null if the call fails
+ *
+ * @param division
+ * e.g. Ensembl, EnsemblMetazoa
+ * @param accession
+ * e.g. ENST00000592782, Y55B1AR.1.1
+ * @param start
+ * @param end
+ * @return
+ */
+ public GeneLociI getCdsMapping(String division, String accession,
+ int start, int end)
+ {
+ return getIdMapping(division, accession, start, end, CDS);
+ }
+
+ /**
+ * Calls the REST /map/cdna/id service, and returns a DBRefEntry holding the
+ * returned chromosomal coordinates, or returns null if the call fails
+ *
+ * @param division
+ * e.g. Ensembl, EnsemblMetazoa
+ * @param accession
+ * e.g. ENST00000592782, Y55B1AR.1.1
+ * @param start
+ * @param end
+ * @return
+ */
+ public GeneLociI getCdnaMapping(String division, String accession,
+ int start, int end)
+ {
+ return getIdMapping(division, accession, start, end, CDNA);
+ }
+
+ GeneLociI getIdMapping(String division, String accession, int start,
+ int end, String cdsOrCdna)
+ {
+ URL url = null;
+ BufferedReader br = null;
+
+ try
+ {
+ String domain = new EnsemblInfo().getDomain(division);
+ if (domain != null)
+ {
+ url = getIdMapUrl(domain, accession, start, end, cdsOrCdna);
+ br = getHttpResponse(url, null);
+ return (parseIdMappingResponse(br, accession, domain));
+ }
+ return null;
+ } catch (Throwable t)
+ {
+ System.out.println("Error calling " + url + ": " + t.getMessage());
+ return null;
+ } finally
+ {
+ if (br != null)
+ {
+ try
+ {
+ br.close();
+ } catch (IOException e)
+ {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Constructs a URL to the /map/cds/<id> or /map/cdna/<id> REST service. The
+ * REST call is to either ensembl or ensemblgenomes, as determined from the
+ * division, e.g. Ensembl or EnsemblProtists.
+ *
+ * @param domain
+ * @param accession
+ * @param start
+ * @param end
+ * @param cdsOrCdna
+ * @return
+ * @throws MalformedURLException
+ */
+ URL getIdMapUrl(String domain, String accession, int start, int end,
+ String cdsOrCdna) throws MalformedURLException
+ {
+ String url = String
+ .format("%s/map/%s/%s/%d..%d?include_original_region=1&content-type=application/json",
+ domain, cdsOrCdna, accession, start, end);
+ return new URL(url);
+ }
+
+ /**
+ * Parses the JSON response from the /map/cds/ or /map/cdna REST service. The
+ * format is
+ *
+ * <pre>
+ * {"mappings":
+ * [
+ * {"assembly_name":"TAIR10","end":2501311,"seq_region_name":"1","gap":0,
+ * "strand":-1,"coord_system":"chromosome","rank":0,"start":2501114},
+ * {"assembly_name":"TAIR10","end":2500815,"seq_region_name":"1","gap":0,
+ * "strand":-1,"coord_system":"chromosome","rank":0,"start":2500714}
+ * ]
+ * }
+ * </pre>
+ *
+ * @param br
+ * @param accession
+ * @param domain
+ * @return
+ */
+ GeneLociI parseIdMappingResponse(BufferedReader br, String accession,
+ String domain)
+ {
+ JSONParser jp = new JSONParser();
+
+ try
+ {
+ JSONObject parsed = (JSONObject) jp.parse(br);
+ JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
+
+ Iterator rvals = mappings.iterator();
+ String assembly = null;
+ String chromosome = null;
+ int fromEnd = 0;
+ List<int[]> regions = new ArrayList<>();
+
+ while (rvals.hasNext())
+ {
+ JSONObject val = (JSONObject) rvals.next();
+ JSONObject original = (JSONObject) val.get("original");
+ fromEnd = Integer.parseInt(original.get("end").toString());
+
+ JSONObject mapped = (JSONObject) val.get(MAPPED);
+ int start = Integer.parseInt(mapped.get("start").toString());
+ int end = Integer.parseInt(mapped.get("end").toString());
+ String ass = mapped.get("assembly_name").toString();
+ if (assembly != null && !assembly.equals(ass))
+ {
+ System.err
+ .println("EnsemblMap found multiple assemblies - can't resolve");
+ return null;
+ }
+ assembly = ass;
+ String chr = mapped.get("seq_region_name").toString();
+ if (chromosome != null && !chromosome.equals(chr))
+ {
+ System.err
+ .println("EnsemblMap found multiple chromosomes - can't resolve");
+ return null;
+ }
+ chromosome = chr;
+ String strand = mapped.get("strand").toString();
+ if ("-1".equals(strand))
+ {
+ regions.add(new int[] { end, start });
+ }
+ else
+ {
+ regions.add(new int[] { start, end });
+ }
+ }
+
+ /*
+ * processed all mapped regions on chromosome, assemble the result,
+ * having first fetched the species id for the accession
+ */
+ final String species = new EnsemblLookup(domain)
+ .getSpecies(accession);
+ final String as = assembly;
+ final String chr = chromosome;
+ List<int[]> fromRange = Collections.singletonList(new int[] { 1,
+ fromEnd });
+ final MapList map = new MapList(fromRange, regions, 1, 1);
+ return new GeneLociI()
+ {
+
+ @Override
+ public String getSpeciesId()
+ {
+ return species == null ? "" : species;
+ }
+
+ @Override
+ public String getAssemblyId()
+ {
+ return as;
+ }
+
+ @Override
+ public String getChromosomeId()
+ {
+ return chr;
+ }
+
+ @Override
+ public MapList getMap()
+ {
+ return map;
+ }
+ };
+ } catch (IOException | ParseException | NumberFormatException e)
+ {
+ // ignore
+ }
+
+ return null;
+ }
+
+}
private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
- private static Map<String, EnsemblInfo> domainData;
+ 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";
{
domainData = new HashMap<>();
domainData.put(ENSEMBL_REST,
- new EnsemblInfo(ENSEMBL_REST, LATEST_ENSEMBL_REST_VERSION));
- domainData.put(ENSEMBL_GENOMES_REST, new EnsemblInfo(
+ new EnsemblData(ENSEMBL_REST, LATEST_ENSEMBL_REST_VERSION));
+ domainData.put(ENSEMBL_GENOMES_REST, new EnsemblData(
ENSEMBL_GENOMES_REST, LATEST_ENSEMBLGENOMES_REST_VERSION));
}
*/
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;
import jalview.exceptions.JalviewException;
import jalview.io.FastaFile;
import jalview.io.FileParse;
+import jalview.io.gff.Gff3Helper;
import jalview.io.gff.SequenceOntologyFactory;
import jalview.io.gff.SequenceOntologyI;
import jalview.util.Comparison;
*/
public abstract class EnsemblSeqProxy extends EnsemblRestClient
{
- private static final String ALLELES = "alleles";
-
protected static final String PARENT = "Parent";
protected static final String ID = "ID";
*/
static void reverseComplementAlleles(SequenceFeature sf)
{
- final String alleles = (String) sf.getValue(ALLELES);
+ final String alleles = (String) sf.getValue(Gff3Helper.ALLELES);
if (alleles == null)
{
return;
reverseComplementAllele(complement, allele);
}
String comp = complement.toString();
- sf.setValue(ALLELES, comp);
+ sf.setValue(Gff3Helper.ALLELES, comp);
sf.setDescription(comp);
/*
String atts = sf.getAttributes();
if (atts != null)
{
- atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp);
+ atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles,
+ Gff3Helper.ALLELES + "=" + comp);
sf.setAttributes(atts);
}
}
if (br != null)
{
String geneId = parseSymbolResponse(br);
- System.out.println(url + " returned " + geneId);
if (geneId != null && !result.contains(geneId))
{
result.add(geneId);
--- /dev/null
+package jalview.ext.htsjdk;
+
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFFileReader;
+import htsjdk.variant.vcf.VCFHeader;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A thin wrapper for htsjdk classes to read either plain, or compressed, or
+ * compressed and indexed VCF files
+ */
+public class VCFReader implements Closeable, Iterable<VariantContext>
+{
+ private static final String GZ = "gz";
+
+ private static final String TBI_EXTENSION = ".tbi";
+
+ private boolean indexed;
+
+ private VCFFileReader reader;
+
+ /**
+ * Constructor given a raw or compressed VCF file or a (tabix) index file
+ * <p>
+ * For now, file type is inferred from its suffix: .gz or .bgz for compressed
+ * data, .tbi for an index file, anything else is assumed to be plain text
+ * VCF.
+ *
+ * @param f
+ * @throws IOException
+ */
+ public VCFReader(String filePath) throws IOException
+ {
+ if (filePath.endsWith(GZ))
+ {
+ if (new File(filePath + TBI_EXTENSION).exists())
+ {
+ indexed = true;
+ }
+ }
+ else if (filePath.endsWith(TBI_EXTENSION))
+ {
+ indexed = true;
+ filePath = filePath.substring(0, filePath.length() - 4);
+ }
+
+ reader = new VCFFileReader(new File(filePath), indexed);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ if (reader != null)
+ {
+ reader.close();
+ }
+ }
+
+ /**
+ * Returns an iterator over VCF variants in the file. The client should call
+ * close() on the iterator when finished with it.
+ */
+ @Override
+ public CloseableIterator<VariantContext> iterator()
+ {
+ return reader == null ? null : reader.iterator();
+ }
+
+ /**
+ * Queries for records overlapping the region specified. Note that this method
+ * is performant if the VCF file is indexed, and may be very slow if it is
+ * not.
+ * <p>
+ * Client code should call close() on the iterator when finished with it.
+ *
+ * @param chrom
+ * the chromosome to query
+ * @param start
+ * query interval start
+ * @param end
+ * query interval end
+ * @return
+ */
+ public CloseableIterator<VariantContext> query(final String chrom,
+ final int start, final int end)
+ {
+ if (reader == null) {
+ return null;
+ }
+ if (indexed)
+ {
+ return reader.query(chrom, start, end);
+ }
+ else
+ {
+ return queryUnindexed(chrom, start, end);
+ }
+ }
+
+ /**
+ * Returns an iterator over variant records read from a flat file which
+ * overlap the specified chromosomal positions. Call close() on the iterator
+ * when finished with it!
+ *
+ * @param chrom
+ * @param start
+ * @param end
+ * @return
+ */
+ protected CloseableIterator<VariantContext> queryUnindexed(
+ final String chrom, final int start, final int end)
+ {
+ final CloseableIterator<VariantContext> it = reader.iterator();
+
+ return new CloseableIterator<VariantContext>()
+ {
+ boolean atEnd = false;
+
+ // prime look-ahead buffer with next matching record
+ private VariantContext next = findNext();
+
+ private VariantContext findNext()
+ {
+ if (atEnd)
+ {
+ return null;
+ }
+ VariantContext variant = null;
+ while (it.hasNext())
+ {
+ variant = it.next();
+ int vstart = variant.getStart();
+
+ if (vstart > end)
+ {
+ atEnd = true;
+ close();
+ return null;
+ }
+
+ int vend = variant.getEnd();
+ // todo what is the undeprecated way to get
+ // the chromosome for the variant?
+ if (chrom.equals(variant.getChr()) && (vstart <= end)
+ && (vend >= start))
+ {
+ return variant;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ boolean hasNext = !atEnd && (next != null);
+ if (!hasNext)
+ {
+ close();
+ }
+ return hasNext;
+ }
+
+ @Override
+ public VariantContext next()
+ {
+ /*
+ * return the next match, and then re-prime
+ * it with the following one (if any)
+ */
+ VariantContext temp = next;
+ next = findNext();
+ return temp;
+ }
+
+ @Override
+ public void remove()
+ {
+ // not implemented
+ }
+
+ @Override
+ public void close()
+ {
+ it.close();
+ }
+ };
+ }
+
+ /**
+ * Returns an object that models the VCF file headers
+ *
+ * @return
+ */
+ public VCFHeader getFileHeader()
+ {
+ return reader == null ? null : reader.getFileHeader();
+ }
+
+ /**
+ * Answers true if we are processing a tab-indexed VCF file, false if it is a
+ * plain text (uncompressed) file.
+ *
+ * @return
+ */
+ public boolean isIndex()
+ {
+ return indexed;
+ }
+}
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;
AlignmentI al = getViewport().getAlignment();
boolean nucleotide = al.isNucleotide();
+ loadVcf.setVisible(nucleotide);
showTranslation.setVisible(nucleotide);
showReverse.setVisible(nucleotide);
showReverseComplement.setVisible(nucleotide);
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 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);
+ new VCFLoader(viewport.getAlignment()).loadVCF(choice, this);
+ }
+
+ }
}
class PrintThread extends Thread
* around to the bottom of the window stack (as the original implementation
* does)
*
- * @see com.sun.java.swing.plaf.windows.WindowsDesktopManager
*/
public class AquaInternalFrameManager extends DefaultDesktopManager
{
JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
treePanel.setOpaque(false);
- treePanel.setBorder(BorderFactory
- .createTitledBorder(MessageManager.getString("label.tree")));
+ JvSwingUtils.createTitledBorder(treePanel,
+ MessageManager.getString("label.tree"), true);
// then copy the inset dimensions for the border-less PCA panel
JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
import jalview.bin.Cache;
import jalview.datamodel.Alignment;
import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
import jalview.datamodel.SequenceI;
+import jalview.ext.ensembl.EnsemblInfo;
+import jalview.ext.ensembl.EnsemblMap;
import jalview.io.gff.SequenceOntologyI;
import jalview.structure.StructureSelectionManager;
+import jalview.util.DBRefUtils;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
import jalview.util.MessageManager;
import jalview.ws.SequenceFetcher;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
-
-import javax.swing.JOptionPane;
+import java.util.Map;
+import java.util.Set;
/**
* Factory constructor and runnable for discovering and displaying
private SequenceI[] sel;
- private boolean _odna;
+ private final boolean _odna;
private String source;
- List<AlignmentViewPanel> xrefViews = new ArrayList<AlignmentViewPanel>();
+ List<AlignmentViewPanel> xrefViews = new ArrayList<>();
- public List<jalview.api.AlignmentViewPanel> getXrefViews()
+ List<AlignmentViewPanel> getXrefViews()
{
return xrefViews;
}
{
return;
}
+
+ /*
+ * try to look up chromosomal coordinates for nucleotide
+ * sequences (if not already retrieved)
+ */
+ findGeneLoci(xrefs.getSequences());
+
/*
* get display scheme (if any) to apply to features
*/
if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
{
- boolean copyAlignmentIsAligned = false;
- if (dna)
- {
- copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
- xrefsAlignment.getSequencesArray());
- if (copyAlignment.getHeight() == 0)
- {
- JvOptionPane.showMessageDialog(alignFrame,
- MessageManager.getString("label.cant_map_cds"),
- MessageManager.getString("label.operation_failed"),
- JvOptionPane.OK_OPTION);
- System.err.println("Failed to make CDS alignment");
- }
-
- /*
- * pending getting Embl transcripts to 'align',
- * we are only doing this for Ensembl
- */
- // TODO proper criteria for 'can align as cdna'
- if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
- || AlignmentUtils.looksLikeEnsembl(alignment))
- {
- copyAlignment.alignAs(alignment);
- copyAlignmentIsAligned = true;
- }
- }
- else
+ copyAlignment = copyAlignmentForSplitFrame(alignment, dataset, dna,
+ xrefs, xrefsAlignment);
+ if (copyAlignment == null)
{
- copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
- xrefs.getSequencesArray(), dataset);
- }
- copyAlignment
- .setGapCharacter(alignFrame.viewport.getGapCharacter());
-
- StructureSelectionManager ssm = StructureSelectionManager
- .getStructureSelectionManager(Desktop.instance);
-
- /*
- * register any new mappings for sequence mouseover etc
- * (will not duplicate any previously registered mappings)
- */
- ssm.registerMappings(dataset.getCodonFrames());
-
- if (copyAlignment.getHeight() <= 0)
- {
- System.err.println(
- "No Sequences generated for xRef type " + source);
- return;
- }
- /*
- * align protein to dna
- */
- if (dna && copyAlignmentIsAligned)
- {
- xrefsAlignment.alignAs(copyAlignment);
- }
- else
- {
- /*
- * align cdna to protein - currently only if
- * fetching and aligning Ensembl transcripts!
- */
- // TODO: generalise for other sources of locus/transcript/cds data
- if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
- {
- copyAlignment.alignAs(xrefsAlignment);
- }
+ return; // failed
}
}
+
/*
* build AlignFrame(s) according to available alignment data
*/
xrefViews.add(newFrame.alignPanel);
return; // via finally clause
}
+
AlignFrame copyThis = new AlignFrame(copyAlignment,
AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
copyThis.setTitle(alignFrame.getTitle());
}
/**
+ * Tries to add chromosomal coordinates to any nucleotide sequence which does
+ * not already have them. Coordinates are retrieved from Ensembl given an
+ * Ensembl identifier, either on the sequence itself or on a peptide sequence
+ * it has a reference to.
+ *
+ * <pre>
+ * Example (human):
+ * - fetch EMBLCDS cross-references for Uniprot entry P30419
+ * - the EMBL sequences do not have xrefs to Ensembl
+ * - the Uniprot entry has xrefs to
+ * ENSP00000258960, ENSP00000468424, ENST00000258960, ENST00000592782
+ * - either of the transcript ids can be used to retrieve gene loci e.g.
+ * http://rest.ensembl.org/map/cds/ENST00000592782/1..100000
+ * Example (invertebrate):
+ * - fetch EMBLCDS cross-references for Uniprot entry Q43517 (FER1_SOLLC)
+ * - the Uniprot entry has an xref to ENSEMBLPLANTS Solyc10g044520.1.1
+ * - can retrieve gene loci with
+ * http://rest.ensemblgenomes.org/map/cds/Solyc10g044520.1.1/1..100000
+ * </pre>
+ *
+ * @param sequences
+ */
+ public static void findGeneLoci(List<SequenceI> sequences)
+ {
+ Map<DBRefEntry, GeneLociI> retrievedLoci = new HashMap<>();
+ for (SequenceI seq : sequences)
+ {
+ findGeneLoci(seq, retrievedLoci);
+ }
+ }
+
+ /**
+ * Tres to find chromosomal coordinates for the sequence, by searching its
+ * direct and indirect cross-references for Ensembl. If the loci have already
+ * been retrieved, just reads them out of the map of retrievedLoci; this is
+ * the case of an alternative transcript for the same protein. Otherwise calls
+ * a REST service to retrieve the loci, and if successful, adds them to the
+ * sequence and to the retrievedLoci.
+ *
+ * @param seq
+ * @param retrievedLoci
+ */
+ static void findGeneLoci(SequenceI seq,
+ Map<DBRefEntry, GeneLociI> retrievedLoci)
+ {
+ /*
+ * don't replace any existing chromosomal coordinates
+ */
+ if (seq == null || seq.isProtein() || seq.getGeneLoci() != null
+ || seq.getDBRefs() == null)
+ {
+ return;
+ }
+
+ Set<String> ensemblDivisions = new EnsemblInfo().getDivisions();
+
+ /*
+ * first look for direct dbrefs from sequence to Ensembl
+ */
+ String[] divisionsArray = ensemblDivisions
+ .toArray(new String[ensemblDivisions.size()]);
+ DBRefEntry[] seqRefs = seq.getDBRefs();
+ DBRefEntry[] directEnsemblRefs = DBRefUtils.selectRefs(seqRefs,
+ divisionsArray);
+ if (directEnsemblRefs != null)
+ {
+ for (DBRefEntry ensemblRef : directEnsemblRefs)
+ {
+ if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
+ {
+ return;
+ }
+ }
+ }
+
+ /*
+ * else look for indirect dbrefs from sequence to Ensembl
+ */
+ for (DBRefEntry dbref : seq.getDBRefs())
+ {
+ if (dbref.getMap() != null && dbref.getMap().getTo() != null)
+ {
+ DBRefEntry[] dbrefs = dbref.getMap().getTo().getDBRefs();
+ DBRefEntry[] indirectEnsemblRefs = DBRefUtils.selectRefs(dbrefs,
+ divisionsArray);
+ if (indirectEnsemblRefs != null)
+ {
+ for (DBRefEntry ensemblRef : indirectEnsemblRefs)
+ {
+ if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
+ {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieves chromosomal coordinates for the Ensembl (or EnsemblGenomes)
+ * identifier in dbref. If successful, and the sequence length matches gene
+ * loci length, then add it to the sequence, and to the retrievedLoci map.
+ * Answers true if successful, else false.
+ *
+ * @param seq
+ * @param dbref
+ * @param retrievedLoci
+ * @return
+ */
+ static boolean fetchGeneLoci(SequenceI seq, DBRefEntry dbref,
+ Map<DBRefEntry, GeneLociI> retrievedLoci)
+ {
+ String accession = dbref.getAccessionId();
+ String division = dbref.getSource();
+
+ /*
+ * hack: ignore cross-references to Ensembl protein ids
+ * (or use map/translation perhaps?)
+ * todo: is there an equivalent in EnsemblGenomes?
+ */
+ if (accession.startsWith("ENSP"))
+ {
+ return false;
+ }
+ EnsemblMap mapper = new EnsemblMap();
+
+ /*
+ * try CDS mapping first
+ */
+ GeneLociI geneLoci = mapper.getCdsMapping(division, accession, 1,
+ seq.getLength());
+ if (geneLoci != null)
+ {
+ MapList map = geneLoci.getMap();
+ int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
+ if (mappedFromLength == seq.getLength())
+ {
+ seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+ geneLoci.getChromosomeId(), geneLoci.getMap());
+ retrievedLoci.put(dbref, geneLoci);
+ return true;
+ }
+ }
+
+ /*
+ * else try CDNA mapping
+ */
+ geneLoci = mapper.getCdnaMapping(division, accession, 1,
+ seq.getLength());
+ if (geneLoci != null)
+ {
+ MapList map = geneLoci.getMap();
+ int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
+ if (mappedFromLength == seq.getLength())
+ {
+ seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+ geneLoci.getChromosomeId(), geneLoci.getMap());
+ retrievedLoci.put(dbref, geneLoci);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param alignment
+ * @param dataset
+ * @param dna
+ * @param xrefs
+ * @param xrefsAlignment
+ * @return
+ */
+ protected AlignmentI copyAlignmentForSplitFrame(AlignmentI alignment,
+ AlignmentI dataset, boolean dna, AlignmentI xrefs,
+ AlignmentI xrefsAlignment)
+ {
+ AlignmentI copyAlignment;
+ boolean copyAlignmentIsAligned = false;
+ if (dna)
+ {
+ copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
+ xrefsAlignment.getSequencesArray());
+ if (copyAlignment.getHeight() == 0)
+ {
+ JvOptionPane.showMessageDialog(alignFrame,
+ MessageManager.getString("label.cant_map_cds"),
+ MessageManager.getString("label.operation_failed"),
+ JvOptionPane.OK_OPTION);
+ System.err.println("Failed to make CDS alignment");
+ return null;
+ }
+
+ /*
+ * pending getting Embl transcripts to 'align',
+ * we are only doing this for Ensembl
+ */
+ // TODO proper criteria for 'can align as cdna'
+ if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
+ || AlignmentUtils.looksLikeEnsembl(alignment))
+ {
+ copyAlignment.alignAs(alignment);
+ copyAlignmentIsAligned = true;
+ }
+ }
+ else
+ {
+ copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
+ xrefs.getSequencesArray(), dataset);
+ }
+ copyAlignment
+ .setGapCharacter(alignFrame.viewport.getGapCharacter());
+
+ StructureSelectionManager ssm = StructureSelectionManager
+ .getStructureSelectionManager(Desktop.instance);
+
+ /*
+ * register any new mappings for sequence mouseover etc
+ * (will not duplicate any previously registered mappings)
+ */
+ ssm.registerMappings(dataset.getCodonFrames());
+
+ if (copyAlignment.getHeight() <= 0)
+ {
+ System.err.println(
+ "No Sequences generated for xRef type " + source);
+ return null;
+ }
+
+ /*
+ * align protein to dna
+ */
+ if (dna && copyAlignmentIsAligned)
+ {
+ xrefsAlignment.alignAs(copyAlignment);
+ }
+ else
+ {
+ /*
+ * align cdna to protein - currently only if
+ * fetching and aligning Ensembl transcripts!
+ */
+ // TODO: generalise for other sources of locus/transcript/cds data
+ if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
+ {
+ copyAlignment.alignAs(xrefsAlignment);
+ }
+ }
+
+ return copyAlignment;
+ }
+
+ /**
* Makes an alignment containing the given sequences, and adds them to the
* given dataset, which is also set as the dataset for the new alignment
*
return al;
}
- public CrossRefAction(AlignFrame alignFrame, SequenceI[] sel,
- boolean _odna, String source)
+ /**
+ * Constructor
+ *
+ * @param af
+ * @param seqs
+ * @param fromDna
+ * @param dbSource
+ */
+ CrossRefAction(AlignFrame af, SequenceI[] seqs, boolean fromDna,
+ String dbSource)
{
- this.alignFrame = alignFrame;
- this.sel = sel;
- this._odna = _odna;
- this.source = source;
+ this.alignFrame = af;
+ this.sel = seqs;
+ this._odna = fromDna;
+ this.source = dbSource;
}
- public static CrossRefAction showProductsFor(final SequenceI[] sel,
- final boolean _odna, final String source,
+ public static CrossRefAction getHandlerFor(final SequenceI[] sel,
+ final boolean fromDna, final String source,
final AlignFrame alignFrame)
{
- return new CrossRefAction(alignFrame, sel, _odna, source);
+ return new CrossRefAction(alignFrame, sel, fromDna, source);
}
}
*/
public void setText(String text)
{
+ textarea.setDocument(textarea.getEditorKit().createDefaultDocument());
textarea.setText(text);
}
+++ /dev/null
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- *
- * This file is part of Jalview.
- *
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *
- * Jalview is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- * PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.gui;
-
-import jalview.api.FeatureColourI;
-import jalview.datamodel.GraphLine;
-import jalview.schemes.FeatureColour;
-import jalview.util.MessageManager;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.FlowLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.FocusAdapter;
-import java.awt.event.FocusEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-
-import javax.swing.BorderFactory;
-import javax.swing.JCheckBox;
-import javax.swing.JColorChooser;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSlider;
-import javax.swing.JTextField;
-import javax.swing.border.LineBorder;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-
-public class FeatureColourChooser extends JalviewDialog
-{
- // FeatureSettings fs;
- private FeatureRenderer fr;
-
- private FeatureColourI cs;
-
- private FeatureColourI oldcs;
-
- private AlignmentPanel ap;
-
- private boolean adjusting = false;
-
- final private float min;
-
- final private float max;
-
- final private float scaleFactor;
-
- private String type = null;
-
- private JPanel minColour = new JPanel();
-
- private JPanel maxColour = new JPanel();
-
- private JComboBox<String> threshold = new JComboBox<>();
-
- private JSlider slider = new JSlider();
-
- private JTextField thresholdValue = new JTextField(20);
-
- // TODO implement GUI for tolower flag
- // JCheckBox toLower = new JCheckBox();
-
- private JCheckBox thresholdIsMin = new JCheckBox();
-
- private JCheckBox colourByLabel = new JCheckBox();
-
- private GraphLine threshline;
-
- private Color oldmaxColour;
-
- private Color oldminColour;
-
- private ActionListener colourEditor = null;
-
- /**
- * Constructor
- *
- * @param frender
- * @param theType
- */
- public FeatureColourChooser(FeatureRenderer frender, String theType)
- {
- this(frender, false, theType);
- }
-
- /**
- * Constructor, with option to make a blocking dialog (has to complete in the
- * AWT event queue thread). Currently this option is always set to false.
- *
- * @param frender
- * @param blocking
- * @param theType
- */
- FeatureColourChooser(FeatureRenderer frender, boolean blocking,
- String theType)
- {
- this.fr = frender;
- this.type = theType;
- ap = fr.ap;
- String title = MessageManager
- .formatMessage("label.graduated_color_for_params", new String[]
- { theType });
- initDialogFrame(this, true, blocking, title, 480, 185);
-
- slider.addChangeListener(new ChangeListener()
- {
- @Override
- public void stateChanged(ChangeEvent evt)
- {
- if (!adjusting)
- {
- thresholdValue.setText((slider.getValue() / scaleFactor) + "");
- sliderValueChanged();
- }
- }
- });
- slider.addMouseListener(new MouseAdapter()
- {
- @Override
- public void mouseReleased(MouseEvent evt)
- {
- /*
- * only update Overview and/or structure colouring
- * when threshold slider drag ends (mouse up)
- */
- if (ap != null)
- {
- ap.paintAlignment(true, true);
- }
- }
- });
-
- float mm[] = fr.getMinMax().get(theType)[0];
- min = mm[0];
- max = mm[1];
-
- /*
- * ensure scale factor allows a scaled range with
- * 10 integer divisions ('ticks'); if we have got here,
- * we should expect that max != min
- */
- scaleFactor = (max == min) ? 1f : 100f / (max - min);
-
- oldcs = fr.getFeatureColours().get(theType);
- if (!oldcs.isSimpleColour())
- {
- if (oldcs.isAutoScaled())
- {
- // update the scale
- cs = new FeatureColour((FeatureColour) oldcs, min, max);
- }
- else
- {
- cs = new FeatureColour((FeatureColour) oldcs);
- }
- }
- else
- {
- // promote original color to a graduated color
- Color bl = oldcs.getColour();
- if (bl == null)
- {
- bl = Color.BLACK;
- }
- // original colour becomes the maximum colour
- cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
- cs.setColourByLabel(false);
- }
- minColour.setBackground(oldminColour = cs.getMinColour());
- maxColour.setBackground(oldmaxColour = cs.getMaxColour());
- adjusting = true;
-
- try
- {
- jbInit();
- } catch (Exception ex)
- {
- }
- // update the gui from threshold state
- thresholdIsMin.setSelected(!cs.isAutoScaled());
- colourByLabel.setSelected(cs.isColourByLabel());
- if (cs.hasThreshold())
- {
- // initialise threshold slider and selector
- threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
- slider.setEnabled(true);
- slider.setValue((int) (cs.getThreshold() * scaleFactor));
- thresholdValue.setEnabled(true);
- threshline = new GraphLine((max - min) / 2f, "Threshold",
- Color.black);
- threshline.value = cs.getThreshold();
- }
-
- adjusting = false;
-
- changeColour(false);
- waitForInput();
- }
-
- private void jbInit() throws Exception
- {
-
- minColour.setFont(JvSwingUtils.getLabelFont());
- minColour.setBorder(BorderFactory.createLineBorder(Color.black));
- minColour.setPreferredSize(new Dimension(40, 20));
- minColour.setToolTipText(MessageManager.getString("label.min_colour"));
- minColour.addMouseListener(new MouseAdapter()
- {
- @Override
- public void mousePressed(MouseEvent e)
- {
- if (minColour.isEnabled())
- {
- minColour_actionPerformed();
- }
- }
- });
- maxColour.setFont(JvSwingUtils.getLabelFont());
- maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
- maxColour.setPreferredSize(new Dimension(40, 20));
- maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
- maxColour.addMouseListener(new MouseAdapter()
- {
- @Override
- public void mousePressed(MouseEvent e)
- {
- if (maxColour.isEnabled())
- {
- maxColour_actionPerformed();
- }
- }
- });
- maxColour.setBorder(new LineBorder(Color.black));
- JLabel minText = new JLabel(MessageManager.getString("label.min"));
- minText.setFont(JvSwingUtils.getLabelFont());
- JLabel maxText = new JLabel(MessageManager.getString("label.max"));
- maxText.setFont(JvSwingUtils.getLabelFont());
- this.setLayout(new BorderLayout());
- JPanel jPanel1 = new JPanel();
- jPanel1.setBackground(Color.white);
- JPanel jPanel2 = new JPanel();
- jPanel2.setLayout(new FlowLayout());
- jPanel2.setBackground(Color.white);
- threshold.addActionListener(new ActionListener()
- {
- @Override
- public void actionPerformed(ActionEvent e)
- {
- threshold_actionPerformed();
- }
- });
- threshold.setToolTipText(MessageManager
- .getString("label.threshold_feature_display_by_score"));
- threshold.addItem(MessageManager
- .getString("label.threshold_feature_no_threshold")); // index 0
- threshold.addItem(MessageManager
- .getString("label.threshold_feature_above_threshold")); // index 1
- threshold.addItem(MessageManager
- .getString("label.threshold_feature_below_threshold")); // index 2
-
- JPanel jPanel3 = new JPanel();
- jPanel3.setLayout(new FlowLayout());
- thresholdValue.addActionListener(new ActionListener()
- {
- @Override
- public void actionPerformed(ActionEvent e)
- {
- thresholdValue_actionPerformed();
- }
- });
- thresholdValue.addFocusListener(new FocusAdapter()
- {
- @Override
- public void focusLost(FocusEvent e)
- {
- thresholdValue_actionPerformed();
- }
- });
- slider.setPaintLabels(false);
- slider.setPaintTicks(true);
- slider.setBackground(Color.white);
- slider.setEnabled(false);
- slider.setOpaque(false);
- slider.setPreferredSize(new Dimension(100, 32));
- slider.setToolTipText(
- MessageManager.getString("label.adjust_threshold"));
- thresholdValue.setEnabled(false);
- thresholdValue.setColumns(7);
- jPanel3.setBackground(Color.white);
- thresholdIsMin.setBackground(Color.white);
- thresholdIsMin
- .setText(MessageManager.getString("label.threshold_minmax"));
- thresholdIsMin.setToolTipText(MessageManager
- .getString("label.toggle_absolute_relative_display_threshold"));
- thresholdIsMin.addActionListener(new ActionListener()
- {
- @Override
- public void actionPerformed(ActionEvent actionEvent)
- {
- thresholdIsMin_actionPerformed();
- }
- });
- colourByLabel.setBackground(Color.white);
- colourByLabel
- .setText(MessageManager.getString("label.colour_by_label"));
- colourByLabel.setToolTipText(MessageManager.getString(
- "label.display_features_same_type_different_label_using_different_colour"));
- colourByLabel.addActionListener(new ActionListener()
- {
- @Override
- public void actionPerformed(ActionEvent actionEvent)
- {
- colourByLabel_actionPerformed();
- }
- });
-
- JPanel colourPanel = new JPanel();
- colourPanel.setBackground(Color.white);
- jPanel1.add(ok);
- jPanel1.add(cancel);
- jPanel2.add(colourByLabel, BorderLayout.WEST);
- jPanel2.add(colourPanel, BorderLayout.EAST);
- colourPanel.add(minText);
- colourPanel.add(minColour);
- colourPanel.add(maxText);
- colourPanel.add(maxColour);
- this.add(jPanel3, BorderLayout.CENTER);
- jPanel3.add(threshold);
- jPanel3.add(slider);
- jPanel3.add(thresholdValue);
- jPanel3.add(thresholdIsMin);
- this.add(jPanel1, BorderLayout.SOUTH);
- this.add(jPanel2, BorderLayout.NORTH);
- }
-
- /**
- * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
- * set the selected colour (if the user does not cancel out of the dialog)
- */
- protected void minColour_actionPerformed()
- {
- Color col = JColorChooser.showDialog(this,
- MessageManager.getString("label.select_colour_minimum_value"),
- minColour.getBackground());
- if (col != null)
- {
- minColour.setBackground(col);
- minColour.setForeground(col);
- }
- minColour.repaint();
- changeColour(true);
- }
-
- /**
- * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
- * set the selected colour (if the user does not cancel out of the dialog)
- */
- protected void maxColour_actionPerformed()
- {
- Color col = JColorChooser.showDialog(this,
- MessageManager.getString("label.select_colour_maximum_value"),
- maxColour.getBackground());
- if (col != null)
- {
- maxColour.setBackground(col);
- maxColour.setForeground(col);
- }
- maxColour.repaint();
- changeColour(true);
- }
-
- /**
- * Constructs and sets the selected colour options as the colour for the
- * feature type, and repaints the alignment, and optionally the Overview
- * and/or structure viewer if open
- *
- * @param updateStructsAndOverview
- */
- void changeColour(boolean updateStructsAndOverview)
- {
- // Check if combobox is still adjusting
- if (adjusting)
- {
- return;
- }
-
- boolean aboveThreshold = false;
- boolean belowThreshold = false;
- if (threshold.getSelectedIndex() == 1)
- {
- aboveThreshold = true;
- }
- else if (threshold.getSelectedIndex() == 2)
- {
- belowThreshold = true;
- }
- boolean hasThreshold = aboveThreshold || belowThreshold;
-
- slider.setEnabled(true);
- thresholdValue.setEnabled(true);
-
- FeatureColourI acg;
- if (cs.isColourByLabel())
- {
- acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
- }
- else
- {
- acg = new FeatureColour(oldminColour = minColour.getBackground(),
- oldmaxColour = maxColour.getBackground(), min, max);
- }
-
- if (!hasThreshold)
- {
- slider.setEnabled(false);
- thresholdValue.setEnabled(false);
- thresholdValue.setText("");
- thresholdIsMin.setEnabled(false);
- }
- else if (threshline == null)
- {
- /*
- * todo not yet implemented: visual indication of feature threshold
- */
- threshline = new GraphLine((max - min) / 2f, "Threshold",
- Color.black);
- }
-
- if (hasThreshold)
- {
- adjusting = true;
- acg.setThreshold(threshline.value);
-
- float range = (max - min) * scaleFactor;
-
- slider.setMinimum((int) (min * scaleFactor));
- slider.setMaximum((int) (max * scaleFactor));
- // slider.setValue((int) (threshline.value * scaleFactor));
- slider.setValue(Math.round(threshline.value * scaleFactor));
- thresholdValue.setText(threshline.value + "");
- slider.setMajorTickSpacing((int) (range / 10f));
- slider.setEnabled(true);
- thresholdValue.setEnabled(true);
- thresholdIsMin.setEnabled(!colourByLabel.isSelected());
- adjusting = false;
- }
-
- acg.setAboveThreshold(aboveThreshold);
- acg.setBelowThreshold(belowThreshold);
- if (thresholdIsMin.isSelected() && hasThreshold)
- {
- acg.setAutoScaled(false);
- if (aboveThreshold)
- {
- acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
- }
- else
- {
- acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
- }
- }
- else
- {
- acg.setAutoScaled(true);
- }
- acg.setColourByLabel(colourByLabel.isSelected());
- if (acg.isColourByLabel())
- {
- maxColour.setEnabled(false);
- minColour.setEnabled(false);
- maxColour.setBackground(this.getBackground());
- maxColour.setForeground(this.getBackground());
- minColour.setBackground(this.getBackground());
- minColour.setForeground(this.getBackground());
-
- }
- else
- {
- maxColour.setEnabled(true);
- minColour.setEnabled(true);
- maxColour.setBackground(oldmaxColour);
- minColour.setBackground(oldminColour);
- maxColour.setForeground(oldmaxColour);
- minColour.setForeground(oldminColour);
- }
- fr.setColour(type, acg);
- cs = acg;
- ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
- }
-
- @Override
- protected void raiseClosed()
- {
- if (this.colourEditor != null)
- {
- colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
- }
- }
-
- @Override
- public void okPressed()
- {
- changeColour(false);
- }
-
- @Override
- public void cancelPressed()
- {
- reset();
- }
-
- /**
- * Action when the user cancels the dialog. All previous settings should be
- * restored and rendered on the alignment, and any linked Overview window or
- * structure.
- */
- void reset()
- {
- fr.setColour(type, oldcs);
- ap.paintAlignment(true, true);
- cs = null;
- }
-
- /**
- * Action on change of choice of No / Above / Below Threshold
- */
- protected void threshold_actionPerformed()
- {
- changeColour(true);
- }
-
- /**
- * Action on text entry of a threshold value
- */
- protected void thresholdValue_actionPerformed()
- {
- try
- {
- float f = Float.parseFloat(thresholdValue.getText());
- slider.setValue((int) (f * scaleFactor));
- threshline.value = f;
-
- /*
- * force repaint of any Overview window or structure
- */
- ap.paintAlignment(true, true);
- } catch (NumberFormatException ex)
- {
- }
- }
-
- /**
- * Action on change of threshold slider value. This may be done interactively
- * (by moving the slider), or programmatically (to update the slider after
- * manual input of a threshold value).
- */
- protected void sliderValueChanged()
- {
- /*
- * squash rounding errors by forcing min/max of slider to
- * actual min/max of feature score range
- */
- int value = slider.getValue();
- threshline.value = value == slider.getMaximum() ? max
- : (value == slider.getMinimum() ? min : value / scaleFactor);
- cs.setThreshold(threshline.value);
-
- /*
- * repaint alignment, but not Overview or structure,
- * to avoid overload while dragging the slider
- */
- changeColour(false);
- }
-
- protected void thresholdIsMin_actionPerformed()
- {
- changeColour(true);
- }
-
- protected void colourByLabel_actionPerformed()
- {
- changeColour(true);
- }
-
- void addActionListener(ActionListener graduatedColorEditor)
- {
- if (colourEditor != null)
- {
- System.err.println(
- "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
- }
- colourEditor = graduatedColorEditor;
- }
-
- /**
- * Answers the last colour setting selected by user - either oldcs (which may
- * be a java.awt.Color) or the new GraduatedColor
- *
- * @return
- */
- FeatureColourI getLastColour()
- {
- if (cs == null)
- {
- return oldcs;
- }
- return cs;
- }
-
-}
final JSpinner end = new JSpinner();
start.setPreferredSize(new Dimension(80, 20));
end.setPreferredSize(new Dimension(80, 20));
- final FeatureRenderer me = this;
final JLabel colour = new JLabel();
colour.setOpaque(true);
// colour.setBorder(BorderFactory.createEtchedBorder());
colour.setMaximumSize(new Dimension(30, 16));
colour.addMouseListener(new MouseAdapter()
{
- FeatureColourChooser fcc = null;
-
+ /*
+ * open colour chooser on click in colour panel
+ */
@Override
public void mousePressed(MouseEvent evt)
{
}
else
{
- if (fcc == null)
+ /*
+ * variable colour dialog - on OK, refetch the updated
+ * feature colour and update this display
+ */
+ final String ft = features.get(featureIndex).getType();
+ final String type = ft == null ? lastFeatureAdded : ft;
+ FeatureTypeSettings fcc = new FeatureTypeSettings(
+ FeatureRenderer.this, type);
+ fcc.setRequestFocusEnabled(true);
+ fcc.requestFocus();
+ fcc.addActionListener(new ActionListener()
{
- final String ft = features.get(featureIndex).getType();
- final String type = ft == null ? lastFeatureAdded : ft;
- fcc = new FeatureColourChooser(me, type);
- fcc.setRequestFocusEnabled(true);
- fcc.requestFocus();
-
- fcc.addActionListener(new ActionListener()
+ @Override
+ public void actionPerformed(ActionEvent e)
{
-
- @Override
- public void actionPerformed(ActionEvent e)
- {
- fcol = fcc.getLastColour();
- fcc = null;
- setColour(type, fcol);
- updateColourButton(mainPanel, colour, fcol);
- }
- });
-
- }
+ fcol = FeatureRenderer.this.getFeatureStyle(ft);
+ setColour(type, fcol);
+ updateColourButton(mainPanel, colour, fcol);
+ }
+ });
}
}
});
import jalview.bin.Cache;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.SequenceI;
+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.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;
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;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
-import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.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;
+ /*
+ * 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 = fr.getFeatureFilters();
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
*/
{
for (int i = 0; i < table.getRowCount(); i++)
{
- Boolean value = (Boolean) table.getValueAt(i, 2);
+ Boolean value = (Boolean) table.getValueAt(i, SHOW_COLUMN);
- table.setValueAt(new Boolean(!value.booleanValue()), i, 2);
+ table.setValueAt(new Boolean(!value.booleanValue()), i, SHOW_COLUMN);
}
}
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.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.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
{
- FeatureTableModel(Object[][] data)
- {
- this.data = data;
- }
-
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.append(" ").append(comp.getToolTipText()).toString());
+ }
+ }
+ }
+
+ class ColorEditor extends AbstractCellEditor
+ implements TableCellEditor, ActionListener
+ {
+ FeatureSettings me;
+
+ FeatureColourI currentColor;
+
+ FeatureTypeSettings chooser;
+
+ String type;
+
+ JButton button;
+
+ JColorChooser colorChooser;
+
+ JDialog dialog;
+
+ protected static final String EDIT = "edit";
+
+ int rowSelected = 0;
+
+ public ColorEditor(FeatureSettings me)
+ {
+ this.me = me;
+ // Set up the editor (from the table's point of view),
+ // which is a button.
+ // This button brings up the color chooser dialog,
+ // which is the editor from the user's point of view.
+ button = new JButton();
+ button.setActionCommand(EDIT);
+ button.addActionListener(this);
+ button.setBorderPainted(false);
+ // Set up the dialog that the button brings up.
+ colorChooser = new JColorChooser();
+ dialog = JColorChooser.createDialog(button,
+ MessageManager.getString("label.select_colour"), true, // modal
+ colorChooser, this, // OK button handler
+ null); // no CANCEL button handler
+ }
+
+ /**
+ * Handles events from the editor button and from the dialog's OK button.
+ */
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ // todo test e.getSource() instead here
+ if (EDIT.equals(e.getActionCommand()))
+ {
+ // The user has clicked the cell, so
+ // bring up the dialog.
+ if (currentColor.isSimpleColour())
+ {
+ // bring up simple color chooser
+ button.setBackground(currentColor.getColour());
+ colorChooser.setColor(currentColor.getColour());
+ dialog.setVisible(true);
+ }
+ else
+ {
+ // bring up graduated chooser.
+ chooser = new FeatureTypeSettings(me.fr, type);
+ chooser.setRequestFocusEnabled(true);
+ chooser.requestFocus();
+ chooser.addActionListener(this);
+ chooser.showTab(true);
+ }
+ // Make the renderer reappear.
+ fireEditingStopped();
+
+ }
+ else
+ {
+ if (currentColor.isSimpleColour())
+ {
+ /*
+ * read off colour picked in colour chooser after OK pressed
+ */
+ currentColor = new FeatureColour(colorChooser.getColor());
+ me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
+ }
+ else
+ {
+ /*
+ * after OK in variable colour dialog, any changes to colour
+ * (or filters!) are already set in FeatureRenderer, so just
+ * update table data without triggering updateFeatureRenderer
+ */
+ currentColor = fr.getFeatureColours().get(type);
+ FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
+ if (currentFilter == null)
+ {
+ currentFilter = new FeatureMatcherSet();
+ }
+ Object[] data = ((FeatureTableModel) table.getModel())
+ .getData()[rowSelected];
+ data[COLOUR_COLUMN] = currentColor;
+ data[FILTER_COLUMN] = currentFilter;
+ }
+ fireEditingStopped();
+ me.table.validate();
+ }
+ }
+
+ // Implement the one CellEditor method that AbstractCellEditor doesn't.
+ @Override
+ public Object getCellEditorValue()
+ {
+ return currentColor;
+ }
+
+ // Implement the one method defined by TableCellEditor.
+ @Override
+ public Component getTableCellEditorComponent(JTable theTable, Object value,
+ boolean isSelected, int row, int column)
+ {
+ currentColor = (FeatureColourI) value;
+ this.rowSelected = row;
+ type = me.table.getValueAt(row, TYPE_COLUMN).toString();
+ button.setOpaque(true);
+ button.setBackground(me.getBackground());
+ if (!currentColor.isSimpleColour())
+ {
+ JLabel btn = new JLabel();
+ btn.setSize(button.getSize());
+ FeatureSettings.renderGraduatedColor(btn, currentColor);
+ button.setBackground(btn.getBackground());
+ button.setIcon(btn.getIcon());
+ button.setText(btn.getText());
}
else
{
- comp.setToolTipText(tt + " " + comp.getToolTipText());
+ button.setText("");
+ button.setIcon(null);
+ button.setBackground(currentColor.getColour());
+ }
+ return button;
+ }
+ }
+
+ /**
+ * The cell editor for the Filter column. It displays the text of any filters
+ * for the feature type in that row (in full as a tooltip, possible abbreviated
+ * as display text). On click in the cell, opens the Feature Display Settings
+ * dialog at the Filters tab.
+ */
+ class FilterEditor extends AbstractCellEditor
+ implements TableCellEditor, ActionListener
+ {
+ FeatureSettings me;
+
+ FeatureMatcherSetI currentFilter;
+
+ Point lastLocation;
+
+ String type;
+
+ JButton button;
+
+ protected static final String EDIT = "edit";
+
+ int rowSelected = 0;
+
+ public FilterEditor(FeatureSettings me)
+ {
+ this.me = me;
+ button = new JButton();
+ button.setActionCommand(EDIT);
+ button.addActionListener(this);
+ button.setBorderPainted(false);
+ }
+
+ /**
+ * Handles events from the editor button
+ */
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if (button == e.getSource())
+ {
+ FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
+ chooser.addActionListener(this);
+ chooser.setRequestFocusEnabled(true);
+ chooser.requestFocus();
+ if (lastLocation != null)
+ {
+ // todo open at its last position on screen
+ chooser.setBounds(lastLocation.x, lastLocation.y,
+ chooser.getWidth(), chooser.getHeight());
+ chooser.validate();
+ }
+ chooser.showTab(false);
+ fireEditingStopped();
+ }
+ else if (e.getSource() instanceof Component)
+ {
+
+ /*
+ * after OK in variable colour dialog, any changes to filter
+ * (or colours!) are already set in FeatureRenderer, so just
+ * update table data without triggering updateFeatureRenderer
+ */
+ FeatureColourI currentColor = fr.getFeatureColours().get(type);
+ currentFilter = me.fr.getFeatureFilter(type);
+ if (currentFilter == null)
+ {
+ currentFilter = new FeatureMatcherSet();
+ }
+ Object[] data = ((FeatureTableModel) table.getModel())
+ .getData()[rowSelected];
+ data[COLOUR_COLUMN] = currentColor;
+ data[FILTER_COLUMN] = currentFilter;
+ fireEditingStopped();
+ me.table.validate();
}
}
+
+ @Override
+ public Object getCellEditorValue()
+ {
+ return currentFilter;
+ }
+
+ @Override
+ public Component getTableCellEditorComponent(JTable theTable, Object value,
+ boolean isSelected, int row, int column)
+ {
+ currentFilter = (FeatureMatcherSetI) value;
+ this.rowSelected = row;
+ type = me.table.getValueAt(row, TYPE_COLUMN).toString();
+ button.setOpaque(true);
+ button.setBackground(me.getBackground());
+ button.setText(currentFilter.toString());
+ button.setToolTipText(currentFilter.toString());
+ button.setIcon(null);
+ return button;
+ }
}
}
}
}
}
-
-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;
- }
-}
--- /dev/null
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ *
+ * This file is part of Jalview.
+ *
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * Jalview is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.gui;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.GraphLine;
+import jalview.datamodel.features.FeatureAttributes;
+import jalview.datamodel.features.FeatureAttributes.Datatype;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.schemes.FeatureColour;
+import jalview.util.ColorUtils;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSlider;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.border.LineBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.plaf.basic.BasicArrowButton;
+
+/**
+ * A dialog where the user can configure colour scheme, and any filters, for one
+ * feature type
+ * <p>
+ * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
+ * filter options)
+ */
+public class FeatureTypeSettings extends JalviewDialog
+{
+ private final static String LABEL_18N = MessageManager
+ .getString("label.label");
+
+ private final static String SCORE_18N = MessageManager
+ .getString("label.score");
+
+ private static final int RADIO_WIDTH = 130;
+
+ private static final String COLON = ":";
+
+ private static final int MAX_TOOLTIP_LENGTH = 50;
+
+ private static final int NO_COLOUR_OPTION = 0;
+
+ private static final int MIN_COLOUR_OPTION = 1;
+
+ private static final int MAX_COLOUR_OPTION = 2;
+
+ private static final int ABOVE_THRESHOLD_OPTION = 1;
+
+ private static final int BELOW_THRESHOLD_OPTION = 2;
+
+ private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
+ "##.##");
+
+ /*
+ * FeatureRenderer holds colour scheme and filters for feature types
+ */
+ private final FeatureRenderer fr; // todo refactor to allow interface type here
+
+ /*
+ * the view panel to update when settings change
+ */
+ private final AlignmentViewPanel ap;
+
+ private final String featureType;
+
+ /*
+ * the colour and filters to reset to on Cancel
+ */
+ private final FeatureColourI originalColour;
+
+ private final FeatureMatcherSetI originalFilter;
+
+ /*
+ * set flag to true when setting values programmatically,
+ * to avoid invocation of action handlers
+ */
+ private boolean adjusting = false;
+
+ /*
+ * minimum of the value range for graduated colour
+ * (may be for feature score or for a numeric attribute)
+ */
+ private float min;
+
+ /*
+ * maximum of the value range for graduated colour
+ */
+ private float max;
+
+ /*
+ * scale factor for conversion between absolute min-max and slider
+ */
+ private float scaleFactor;
+
+ /*
+ * radio button group, to select what to colour by:
+ * simple colour, by category (text), or graduated
+ */
+ private JRadioButton simpleColour = new JRadioButton();
+
+ private JRadioButton byCategory = new JRadioButton();
+
+ private JRadioButton graduatedColour = new JRadioButton();
+
+ private JPanel singleColour = new JPanel();
+
+ private JPanel minColour = new JPanel();
+
+ private JPanel maxColour = new JPanel();
+
+ private JComboBox<String> threshold = new JComboBox<>();
+
+ private JSlider slider = new JSlider();
+
+ private JTextField thresholdValue = new JTextField(20);
+
+ private JCheckBox thresholdIsMin = new JCheckBox();
+
+ private GraphLine threshline;
+
+ private ActionListener featureSettings = null;
+
+ private ActionListener changeColourAction;
+
+ /*
+ * choice of option for 'colour for no value'
+ */
+ private JComboBox<String> noValueCombo;
+
+ /*
+ * choice of what to colour by text (Label or attribute)
+ */
+ private JComboBox<String> colourByTextCombo;
+
+ /*
+ * choice of what to colour by range (Score or attribute)
+ */
+ private JComboBox<String> colourByRangeCombo;
+
+ private JRadioButton andFilters;
+
+ private JRadioButton orFilters;
+
+ /*
+ * filters for the currently selected feature type
+ */
+ private List<FeatureMatcherI> filters;
+
+ // set white normally, black to debug layout
+ private Color debugBorderColour = Color.white;
+
+ private JPanel chooseFiltersPanel;
+
+ private JTabbedPane tabbedPane;
+
+ /**
+ * Constructor
+ *
+ * @param frender
+ * @param theType
+ */
+ public FeatureTypeSettings(FeatureRenderer frender, String theType)
+ {
+ this(frender, false, theType);
+ }
+
+ /**
+ * Constructor, with option to make a blocking dialog (has to complete in the
+ * AWT event queue thread). Currently this option is always set to false.
+ *
+ * @param frender
+ * @param blocking
+ * @param theType
+ */
+ FeatureTypeSettings(FeatureRenderer frender, boolean blocking,
+ String theType)
+ {
+ this.fr = frender;
+ this.featureType = theType;
+ ap = fr.ap;
+ originalFilter = fr.getFeatureFilter(theType);
+ originalColour = fr.getFeatureColours().get(theType);
+
+ adjusting = true;
+
+ try
+ {
+ initialise();
+ } catch (Exception ex)
+ {
+ ex.printStackTrace();
+ return;
+ }
+
+ updateColoursTab();
+
+ updateFiltersTab();
+
+ adjusting = false;
+
+ colourChanged(false);
+
+ String title = MessageManager
+ .formatMessage("label.display_settings_for", new String[]
+ { theType });
+ initDialogFrame(this, true, blocking, title, 600, 360);
+
+ waitForInput();
+ }
+
+ /**
+ * Configures the widgets on the Colours tab according to the current feature
+ * colour scheme
+ */
+ private void updateColoursTab()
+ {
+ FeatureColourI fc = fr.getFeatureColours().get(featureType);
+
+ /*
+ * suppress action handling while updating values programmatically
+ */
+ adjusting = true;
+ try
+ {
+ /*
+ * single colour
+ */
+ if (fc.isSimpleColour())
+ {
+ simpleColour.setSelected(true);
+ singleColour.setBackground(fc.getColour());
+ singleColour.setForeground(fc.getColour());
+ }
+
+ /*
+ * colour by text (Label or attribute text)
+ */
+ if (fc.isColourByLabel())
+ {
+ byCategory.setSelected(true);
+ colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
+ if (fc.isColourByAttribute())
+ {
+ String[] attributeName = fc.getAttributeName();
+ colourByTextCombo
+ .setSelectedItem(toAttributeDisplayName(attributeName));
+ }
+ else
+ {
+ colourByTextCombo.setSelectedItem(LABEL_18N);
+ }
+ }
+ else
+ {
+ colourByTextCombo.setEnabled(false);
+ }
+
+ if (!fc.isGraduatedColour())
+ {
+ colourByRangeCombo.setEnabled(false);
+ minColour.setEnabled(false);
+ maxColour.setEnabled(false);
+ noValueCombo.setEnabled(false);
+ threshold.setEnabled(false);
+ slider.setEnabled(false);
+ thresholdValue.setEnabled(false);
+ thresholdIsMin.setEnabled(false);
+ return;
+ }
+
+ /*
+ * Graduated colour, by score or attribute value range
+ */
+ graduatedColour.setSelected(true);
+ colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
+ minColour.setEnabled(true);
+ maxColour.setEnabled(true);
+ noValueCombo.setEnabled(true);
+ threshold.setEnabled(true);
+ minColour.setBackground(fc.getMinColour());
+ maxColour.setBackground(fc.getMaxColour());
+
+ if (fc.isColourByAttribute())
+ {
+ String[] attributeName = fc.getAttributeName();
+ colourByRangeCombo
+ .setSelectedItem(toAttributeDisplayName(attributeName));
+ }
+ else
+ {
+ colourByRangeCombo.setSelectedItem(SCORE_18N);
+ }
+ Color noColour = fc.getNoColour();
+ if (noColour == null)
+ {
+ noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
+ }
+ else if (noColour.equals(fc.getMinColour()))
+ {
+ noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
+ }
+ else if (noColour.equals(fc.getMaxColour()))
+ {
+ noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
+ }
+
+ /*
+ * update min-max scaling if there is a range to work with,
+ * else disable the widgets (this shouldn't happen if only
+ * valid options are offered in the combo box)
+ */
+ scaleFactor = (max == min) ? 1f : 100f / (max - min);
+ float range = (max - min) * scaleFactor;
+ slider.setMinimum((int) (min * scaleFactor));
+ slider.setMaximum((int) (max * scaleFactor));
+ slider.setMajorTickSpacing((int) (range / 10f));
+
+ threshline = new GraphLine((max - min) / 2f, "Threshold",
+ Color.black);
+ threshline.value = fc.getThreshold();
+
+ if (fc.hasThreshold())
+ {
+ threshold.setSelectedIndex(
+ fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
+ : BELOW_THRESHOLD_OPTION);
+ slider.setEnabled(true);
+ slider.setValue((int) (fc.getThreshold() * scaleFactor));
+ thresholdValue.setText(String.valueOf(getRoundedSliderValue()));
+ thresholdValue.setEnabled(true);
+ thresholdIsMin.setEnabled(true);
+ }
+ else
+ {
+ slider.setEnabled(false);
+ thresholdValue.setEnabled(false);
+ thresholdIsMin.setEnabled(false);
+ }
+ thresholdIsMin.setSelected(!fc.isAutoScaled());
+ } finally
+ {
+ adjusting = false;
+ }
+ }
+
+ /**
+ * Configures the initial layout
+ */
+ private void initialise()
+ {
+ this.setLayout(new BorderLayout());
+ tabbedPane = new JTabbedPane();
+ this.add(tabbedPane, BorderLayout.CENTER);
+
+ /*
+ * an ActionListener that applies colour changes
+ */
+ changeColourAction = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ colourChanged(true);
+ }
+ };
+
+ /*
+ * first tab: colour options
+ */
+ JPanel coloursPanel = initialiseColoursPanel();
+ tabbedPane.addTab(MessageManager.getString("action.colour"),
+ coloursPanel);
+
+ /*
+ * second tab: filter options
+ */
+ JPanel filtersPanel = initialiseFiltersPanel();
+ tabbedPane.addTab(MessageManager.getString("label.filters"),
+ filtersPanel);
+
+ JPanel okCancelPanel = initialiseOkCancelPanel();
+
+ this.add(okCancelPanel, BorderLayout.SOUTH);
+ }
+
+ /**
+ * Updates the min-max range if Colour By selected item is Score, or an
+ * attribute, with a min-max range
+ */
+ protected void updateColourMinMax()
+ {
+ if (!graduatedColour.isSelected())
+ {
+ return;
+ }
+
+ String colourBy = (String) colourByRangeCombo.getSelectedItem();
+ float[] minMax = getMinMax(colourBy);
+
+ if (minMax != null)
+ {
+ min = minMax[0];
+ max = minMax[1];
+ }
+ }
+
+ /**
+ * Retrieves the min-max range:
+ * <ul>
+ * <li>of feature score, if colour or filter is by Score</li>
+ * <li>else of the selected attribute</li>
+ * </ul>
+ *
+ * @param attName
+ * @return
+ */
+ private float[] getMinMax(String attName)
+ {
+ float[] minMax = null;
+ if (SCORE_18N.equals(attName))
+ {
+ minMax = fr.getMinMax().get(featureType)[0];
+ }
+ else
+ {
+ // colour by attribute range
+ minMax = FeatureAttributes.getInstance().getMinMax(featureType,
+ fromAttributeDisplayName(attName));
+ }
+ return minMax;
+ }
+
+ /**
+ * Lay out fields for graduated colour (by score or attribute value)
+ *
+ * @return
+ */
+ private JPanel initialiseGraduatedColourPanel()
+ {
+ JPanel graduatedColourPanel = new JPanel();
+ graduatedColourPanel.setLayout(
+ new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
+ JvSwingUtils.createTitledBorder(graduatedColourPanel,
+ MessageManager.getString("label.graduated_colour"), true);
+ graduatedColourPanel.setBackground(Color.white);
+
+ /*
+ * first row: graduated colour radio button, score/attribute drop-down
+ */
+ JPanel graduatedChoicePanel = new JPanel(
+ new FlowLayout(FlowLayout.LEFT));
+ graduatedChoicePanel.setBackground(Color.white);
+ graduatedColour = new JRadioButton(
+ MessageManager.getString("label.by_range_of") + COLON);
+ graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+ graduatedColour.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ if (graduatedColour.isSelected())
+ {
+ colourChanged(true);
+ }
+ }
+ });
+ graduatedChoicePanel.add(graduatedColour);
+
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(featureType);
+ colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
+ colourByRangeCombo.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ colourChanged(true);
+ }
+ });
+
+ /*
+ * disable graduated colour option if no range found
+ */
+ graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
+
+ graduatedChoicePanel.add(colourByRangeCombo);
+ graduatedColourPanel.add(graduatedChoicePanel);
+
+ /*
+ * second row - min/max/no colours
+ */
+ JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ colourRangePanel.setBackground(Color.white);
+ graduatedColourPanel.add(colourRangePanel);
+
+ minColour.setFont(JvSwingUtils.getLabelFont());
+ minColour.setBorder(BorderFactory.createLineBorder(Color.black));
+ minColour.setPreferredSize(new Dimension(40, 20));
+ minColour.setToolTipText(MessageManager.getString("label.min_colour"));
+ minColour.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mousePressed(MouseEvent e)
+ {
+ if (minColour.isEnabled())
+ {
+ showColourChooser(minColour, "label.select_colour_minimum_value");
+ }
+ }
+ });
+
+ maxColour.setFont(JvSwingUtils.getLabelFont());
+ maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
+ maxColour.setPreferredSize(new Dimension(40, 20));
+ maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
+ maxColour.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mousePressed(MouseEvent e)
+ {
+ if (maxColour.isEnabled())
+ {
+ showColourChooser(maxColour, "label.select_colour_maximum_value");
+ }
+ }
+ });
+ maxColour.setBorder(new LineBorder(Color.black));
+
+ /*
+ * default max colour to current colour (if a plain colour),
+ * or to Black if colour by label; make min colour a pale
+ * version of max colour
+ */
+ FeatureColourI fc = fr.getFeatureColours().get(featureType);
+ Color bg = fc.isSimpleColour() ? fc.getColour() : Color.BLACK;
+ maxColour.setBackground(bg);
+ minColour.setBackground(ColorUtils.bleachColour(bg, 0.9f));
+
+ noValueCombo = new JComboBox<>();
+ noValueCombo.addItem(MessageManager.getString("label.no_colour"));
+ noValueCombo.addItem(MessageManager.getString("label.min_colour"));
+ noValueCombo.addItem(MessageManager.getString("label.max_colour"));
+ noValueCombo.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ colourChanged(true);
+ }
+ });
+
+ JLabel minText = new JLabel(
+ MessageManager.getString("label.min_value") + COLON);
+ minText.setFont(JvSwingUtils.getLabelFont());
+ JLabel maxText = new JLabel(
+ MessageManager.getString("label.max_value") + COLON);
+ maxText.setFont(JvSwingUtils.getLabelFont());
+ JLabel noText = new JLabel(
+ MessageManager.getString("label.no_value") + COLON);
+ noText.setFont(JvSwingUtils.getLabelFont());
+
+ colourRangePanel.add(minText);
+ colourRangePanel.add(minColour);
+ colourRangePanel.add(maxText);
+ colourRangePanel.add(maxColour);
+ colourRangePanel.add(noText);
+ colourRangePanel.add(noValueCombo);
+
+ /*
+ * third row - threshold options and value
+ */
+ JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ thresholdPanel.setBackground(Color.white);
+ graduatedColourPanel.add(thresholdPanel);
+
+ threshold.addActionListener(changeColourAction);
+ threshold.setToolTipText(MessageManager
+ .getString("label.threshold_feature_display_by_score"));
+ threshold.addItem(MessageManager
+ .getString("label.threshold_feature_no_threshold")); // index 0
+ threshold.addItem(MessageManager
+ .getString("label.threshold_feature_above_threshold")); // index 1
+ threshold.addItem(MessageManager
+ .getString("label.threshold_feature_below_threshold")); // index 2
+
+ thresholdValue.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ thresholdValue_actionPerformed();
+ }
+ });
+ thresholdValue.addFocusListener(new FocusAdapter()
+ {
+ @Override
+ public void focusLost(FocusEvent e)
+ {
+ thresholdValue_actionPerformed();
+ }
+ });
+ slider.setPaintLabels(false);
+ slider.setPaintTicks(true);
+ slider.setBackground(Color.white);
+ slider.setEnabled(false);
+ slider.setOpaque(false);
+ slider.setPreferredSize(new Dimension(100, 32));
+ slider.setToolTipText(
+ MessageManager.getString("label.adjust_threshold"));
+
+ slider.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent evt)
+ {
+ if (!adjusting)
+ {
+ thresholdValue
+ .setText(String.valueOf(slider.getValue() / scaleFactor));
+ sliderValueChanged();
+ }
+ }
+ });
+ slider.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mouseReleased(MouseEvent evt)
+ {
+ /*
+ * only update Overview and/or structure colouring
+ * when threshold slider drag ends (mouse up)
+ */
+ if (ap != null)
+ {
+ ap.paintAlignment(true, true);
+ }
+ }
+ });
+
+ thresholdValue.setEnabled(false);
+ thresholdValue.setColumns(7);
+
+ thresholdPanel.add(threshold);
+ thresholdPanel.add(slider);
+ thresholdPanel.add(thresholdValue);
+
+ thresholdIsMin.setBackground(Color.white);
+ thresholdIsMin
+ .setText(MessageManager.getString("label.threshold_minmax"));
+ thresholdIsMin.setToolTipText(MessageManager
+ .getString("label.toggle_absolute_relative_display_threshold"));
+ thresholdIsMin.addActionListener(changeColourAction);
+ thresholdPanel.add(thresholdIsMin);
+
+ return graduatedColourPanel;
+ }
+
+ /**
+ * Lay out OK and Cancel buttons
+ *
+ * @return
+ */
+ private JPanel initialiseOkCancelPanel()
+ {
+ JPanel okCancelPanel = new JPanel();
+ // okCancelPanel.setBackground(Color.white);
+ okCancelPanel.add(ok);
+ okCancelPanel.add(cancel);
+ return okCancelPanel;
+ }
+
+ /**
+ * Lay out Colour options panel, containing
+ * <ul>
+ * <li>plain colour, with colour picker</li>
+ * <li>colour by text, with choice of Label or other attribute</li>
+ * <li>colour by range, of score or other attribute, when available</li>
+ * </ul>
+ *
+ * @return
+ */
+ private JPanel initialiseColoursPanel()
+ {
+ JPanel colourByPanel = new JPanel();
+ colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
+
+ /*
+ * simple colour radio button and colour picker
+ */
+ JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ simpleColourPanel.setBackground(Color.white);
+ JvSwingUtils.createTitledBorder(simpleColourPanel,
+ MessageManager.getString("label.simple"), true);
+ colourByPanel.add(simpleColourPanel);
+
+ simpleColour = new JRadioButton(
+ MessageManager.getString("label.simple_colour"));
+ simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+ simpleColour.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ if (simpleColour.isSelected() && !adjusting)
+ {
+ showColourChooser(singleColour, "label.select_colour");
+ }
+ }
+
+ });
+
+ singleColour.setFont(JvSwingUtils.getLabelFont());
+ singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
+ singleColour.setPreferredSize(new Dimension(40, 20));
+ singleColour.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mousePressed(MouseEvent e)
+ {
+ if (simpleColour.isSelected())
+ {
+ showColourChooser(singleColour, "label.select_colour");
+ }
+ }
+ });
+ simpleColourPanel.add(simpleColour); // radio button
+ simpleColourPanel.add(singleColour); // colour picker button
+
+ /*
+ * colour by text (category) radio button and drop-down choice list
+ */
+ JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ byTextPanel.setBackground(Color.white);
+ JvSwingUtils.createTitledBorder(byTextPanel,
+ MessageManager.getString("label.colour_by_text"), true);
+ colourByPanel.add(byTextPanel);
+ byCategory = new JRadioButton(
+ MessageManager.getString("label.by_text_of") + COLON);
+ byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+ byCategory.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ if (byCategory.isSelected())
+ {
+ colourChanged(true);
+ }
+ }
+ });
+ byTextPanel.add(byCategory);
+
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(featureType);
+ colourByTextCombo = populateAttributesDropdown(attNames, false, true);
+ colourByTextCombo.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ colourChanged(true);
+ }
+ });
+ byTextPanel.add(colourByTextCombo);
+
+ /*
+ * graduated colour panel
+ */
+ JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
+ colourByPanel.add(graduatedColourPanel);
+
+ /*
+ * 3 radio buttons select between simple colour,
+ * by category (text), or graduated
+ */
+ ButtonGroup bg = new ButtonGroup();
+ bg.add(simpleColour);
+ bg.add(byCategory);
+ bg.add(graduatedColour);
+
+ return colourByPanel;
+ }
+
+ private void showColourChooser(JPanel colourPanel, String key)
+ {
+ Color col = JColorChooser.showDialog(this,
+ MessageManager.getString(key), colourPanel.getBackground());
+ if (col != null)
+ {
+ colourPanel.setBackground(col);
+ colourPanel.setForeground(col);
+ }
+ colourPanel.repaint();
+ colourChanged(true);
+ }
+
+ /**
+ * Constructs and sets the selected colour options as the colour for the feature
+ * type, and repaints the alignment, and optionally the Overview and/or
+ * structure viewer if open
+ *
+ * @param updateStructsAndOverview
+ */
+ void colourChanged(boolean updateStructsAndOverview)
+ {
+ if (adjusting)
+ {
+ /*
+ * ignore action handlers while setting values programmatically
+ */
+ return;
+ }
+
+ /*
+ * ensure min-max range is for the latest choice of
+ * 'graduated colour by'
+ */
+ updateColourMinMax();
+
+ FeatureColourI acg = makeColourFromInputs();
+
+ /*
+ * save the colour, and repaint stuff
+ */
+ fr.setColour(featureType, acg);
+ ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
+
+ updateColoursTab();
+ }
+
+ /**
+ * Converts the input values into an instance of FeatureColour
+ *
+ * @return
+ */
+ private FeatureColourI makeColourFromInputs()
+ {
+ /*
+ * easiest case - a single colour
+ */
+ if (simpleColour.isSelected())
+ {
+ return new FeatureColour(singleColour.getBackground());
+ }
+
+ /*
+ * next easiest case - colour by Label, or attribute text
+ */
+ if (byCategory.isSelected())
+ {
+ Color c = this.getBackground();
+ FeatureColourI fc = new FeatureColour(c, c, null, 0f, 0f);
+ fc.setColourByLabel(true);
+ String byWhat = (String) colourByTextCombo.getSelectedItem();
+ if (!LABEL_18N.equals(byWhat))
+ {
+ fc.setAttributeName(fromAttributeDisplayName(byWhat));
+ }
+ return fc;
+ }
+
+ /*
+ * remaining case - graduated colour by score, or attribute value
+ */
+ Color noColour = null;
+ if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
+ {
+ noColour = minColour.getBackground();
+ }
+ else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
+ {
+ noColour = maxColour.getBackground();
+ }
+
+ float thresh = 0f;
+ try
+ {
+ thresh = Float.valueOf(thresholdValue.getText());
+ } catch (NumberFormatException e)
+ {
+ // invalid inputs are already handled on entry
+ }
+
+ /*
+ * min-max range is to (or from) threshold value if
+ * 'threshold is min/max' is selected
+ */
+ float minValue = min;
+ float maxValue = max;
+ final int thresholdOption = threshold.getSelectedIndex();
+ if (thresholdIsMin.isSelected()
+ && thresholdOption == ABOVE_THRESHOLD_OPTION)
+ {
+ minValue = thresh;
+ }
+ if (thresholdIsMin.isSelected()
+ && thresholdOption == BELOW_THRESHOLD_OPTION)
+ {
+ maxValue = thresh;
+ }
+
+ /*
+ * make the graduated colour
+ */
+ FeatureColourI fc = new FeatureColour(minColour.getBackground(),
+ maxColour.getBackground(), noColour, minValue, maxValue);
+
+ /*
+ * set attribute to colour by if selected
+ */
+ String byWhat = (String) colourByRangeCombo.getSelectedItem();
+ if (!SCORE_18N.equals(byWhat))
+ {
+ fc.setAttributeName(fromAttributeDisplayName(byWhat));
+ }
+
+ /*
+ * set threshold options and 'autoscaled' which is
+ * false if 'threshold is min/max' is selected
+ * else true (colour range is on actual range of values)
+ */
+ fc.setThreshold(thresh);
+ fc.setAutoScaled(!thresholdIsMin.isSelected());
+ fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
+ fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
+
+ if (threshline == null)
+ {
+ /*
+ * todo not yet implemented: visual indication of feature threshold
+ */
+ threshline = new GraphLine((max - min) / 2f, "Threshold",
+ Color.black);
+ }
+
+ return fc;
+ }
+
+ /**
+ * A helper method that converts a 'compound' attribute name from its display
+ * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
+ *
+ * @param attribute
+ * @return
+ */
+ private String[] fromAttributeDisplayName(String attribute)
+ {
+ return attribute == null ? null : attribute.split(COLON);
+ }
+
+ /**
+ * A helper method that converts a 'compound' attribute name to its display
+ * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
+ *
+ * @param attName
+ * @return
+ */
+ private String toAttributeDisplayName(String[] attName)
+ {
+ return attName == null ? "" : String.join(COLON, attName);
+ }
+
+ @Override
+ protected void raiseClosed()
+ {
+ if (this.featureSettings != null)
+ {
+ featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
+ }
+ }
+
+ /**
+ * Action on OK is just to dismiss the dialog - any changes have already been
+ * applied
+ */
+ @Override
+ public void okPressed()
+ {
+ }
+
+ /**
+ * Action on Cancel is to restore colour scheme and filters as they were when
+ * the dialog was opened
+ */
+ @Override
+ public void cancelPressed()
+ {
+ fr.setColour(featureType, originalColour);
+ fr.setFeatureFilter(featureType, originalFilter);
+ ap.paintAlignment(true, true);
+ }
+
+ /**
+ * Action on text entry of a threshold value
+ */
+ protected void thresholdValue_actionPerformed()
+ {
+ try
+ {
+ adjusting = true;
+ float f = Float.parseFloat(thresholdValue.getText());
+ slider.setValue((int) (f * scaleFactor));
+ threshline.value = f;
+ thresholdValue.setBackground(Color.white); // ok
+
+ /*
+ * force repaint of any Overview window or structure
+ */
+ ap.paintAlignment(true, true);
+ } catch (NumberFormatException ex)
+ {
+ thresholdValue.setBackground(Color.red); // not ok
+ } finally
+ {
+ adjusting = false;
+ }
+ }
+
+ /**
+ * Action on change of threshold slider value. This may be done interactively
+ * (by moving the slider), or programmatically (to update the slider after
+ * manual input of a threshold value).
+ */
+ protected void sliderValueChanged()
+ {
+ threshline.value = getRoundedSliderValue();
+
+ /*
+ * repaint alignment, but not Overview or structure,
+ * to avoid overload while dragging the slider
+ */
+ colourChanged(false);
+ }
+
+ /**
+ * Converts the slider value to its absolute value by dividing by the
+ * scaleFactor. Rounding errors are squashed by forcing min/max of slider range
+ * to the actual min/max of feature score range
+ *
+ * @return
+ */
+ private float getRoundedSliderValue()
+ {
+ int value = slider.getValue();
+ float f = value == slider.getMaximum() ? max
+ : (value == slider.getMinimum() ? min : value / scaleFactor);
+ return f;
+ }
+
+ void addActionListener(ActionListener listener)
+ {
+ if (featureSettings != null)
+ {
+ System.err.println(
+ "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
+ }
+ featureSettings = listener;
+ }
+
+ /**
+ * A helper method to build the drop-down choice of attributes for a feature. If
+ * 'withRange' is true, then Score, and any attributes with a min-max range, are
+ * added. If 'withText' is true, Label and any known attributes are added. This
+ * allows 'categorical numerical' attributes e.g. codon position to be coloured
+ * by text.
+ * <p>
+ * Where metadata is available with a description for an attribute, that is
+ * added as a tooltip.
+ * <p>
+ * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
+ * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
+ * <p>
+ * This method does not add any ActionListener to the JComboBox.
+ *
+ * @param attNames
+ * @param withRange
+ * @param withText
+ */
+ protected JComboBox<String> populateAttributesDropdown(
+ List<String[]> attNames, boolean withRange, boolean withText)
+ {
+ List<String> displayAtts = new ArrayList<>();
+ List<String> tooltips = new ArrayList<>();
+
+ if (withText)
+ {
+ displayAtts.add(LABEL_18N);
+ tooltips.add(MessageManager.getString("label.description"));
+ }
+ if (withRange)
+ {
+ float[][] minMax = fr.getMinMax().get(featureType);
+ if (minMax != null && minMax[0][0] != minMax[0][1])
+ {
+ displayAtts.add(SCORE_18N);
+ tooltips.add(SCORE_18N);
+ }
+ }
+
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ for (String[] attName : attNames)
+ {
+ float[] minMax = fa.getMinMax(featureType, attName);
+ boolean hasRange = minMax != null && minMax[0] != minMax[1];
+ if (!withText && !hasRange)
+ {
+ continue;
+ }
+ displayAtts.add(toAttributeDisplayName(attName));
+ String desc = fa.getDescription(featureType, attName);
+ if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+ {
+ desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+ }
+ tooltips.add(desc == null ? "" : desc);
+ }
+
+ JComboBox<String> attCombo = JvSwingUtils
+ .buildComboWithTooltips(displayAtts, tooltips);
+
+ return attCombo;
+ }
+
+ /**
+ * Populates initial layout of the feature attribute filters panel
+ */
+ private JPanel initialiseFiltersPanel()
+ {
+ filters = new ArrayList<>();
+
+ JPanel filtersPanel = new JPanel();
+ filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
+ filtersPanel.setBackground(Color.white);
+ JvSwingUtils.createTitledBorder(filtersPanel,
+ MessageManager.getString("label.filters"), true);
+
+ JPanel andOrPanel = initialiseAndOrPanel();
+ filtersPanel.add(andOrPanel);
+
+ /*
+ * panel with filters - populated by refreshFiltersDisplay,
+ * which also sets the layout manager
+ */
+ chooseFiltersPanel = new JPanel();
+ chooseFiltersPanel.setBackground(Color.white);
+ filtersPanel.add(chooseFiltersPanel);
+
+ return filtersPanel;
+ }
+
+ /**
+ * Lays out the panel with radio buttons to AND or OR filter conditions
+ *
+ * @return
+ */
+ private JPanel initialiseAndOrPanel()
+ {
+ JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ andOrPanel.setBackground(Color.white);
+ andOrPanel.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+ andFilters = new JRadioButton(MessageManager.getString("label.and"));
+ orFilters = new JRadioButton(MessageManager.getString("label.or"));
+ ActionListener actionListener = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ filtersChanged();
+ }
+ };
+ andFilters.addActionListener(actionListener);
+ orFilters.addActionListener(actionListener);
+ ButtonGroup andOr = new ButtonGroup();
+ andOr.add(andFilters);
+ andOr.add(orFilters);
+ andFilters.setSelected(true);
+ andOrPanel.add(
+ new JLabel(MessageManager.getString("label.join_conditions")));
+ andOrPanel.add(andFilters);
+ andOrPanel.add(orFilters);
+ return andOrPanel;
+ }
+
+ /**
+ * Refreshes the display to show any filters currently configured for the
+ * selected feature type (editable, with 'remove' option), plus one extra row
+ * for adding a condition. This should be called after a filter has been
+ * removed, added or amended.
+ */
+ private void updateFiltersTab()
+ {
+ /*
+ * clear the panel and list of filter conditions
+ */
+ chooseFiltersPanel.removeAll();
+ filters.clear();
+
+ /*
+ * look up attributes known for feature type
+ */
+ List<String[]> attNames = FeatureAttributes.getInstance()
+ .getAttributes(featureType);
+
+ /*
+ * if this feature type has filters set, load them first
+ */
+ FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
+ if (featureFilters != null)
+ {
+ if (!featureFilters.isAnded())
+ {
+ orFilters.setSelected(true);
+ }
+ featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
+ }
+
+ /*
+ * and an empty filter for the user to populate (add)
+ */
+ filters.add(FeatureMatcher.NULL_MATCHER);
+
+ /*
+ * use GridLayout to 'justify' rows to the top of the panel, until
+ * there are too many to fit in, then fall back on BoxLayout
+ */
+ if (filters.size() <= 5)
+ {
+ chooseFiltersPanel.setLayout(new GridLayout(5, 1));
+ }
+ else
+ {
+ chooseFiltersPanel.setLayout(
+ new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
+ }
+
+ /*
+ * render the conditions in rows, each in its own JPanel
+ */
+ int filterIndex = 0;
+ for (FeatureMatcherI filter : filters)
+ {
+ JPanel row = addFilter(filter, attNames, filterIndex);
+ row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+ chooseFiltersPanel.add(row);
+ filterIndex++;
+ }
+
+ this.validate();
+ this.repaint();
+ }
+
+ /**
+ * A helper method that constructs a row (panel) with one filter condition:
+ * <ul>
+ * <li>a drop-down list of Label, Score and attribute names to choose from</li>
+ * <li>a drop-down list of conditions to choose from</li>
+ * <li>a text field for input of a match pattern</li>
+ * <li>optionally, a 'remove' button</li>
+ * </ul>
+ * The filter values are set as defaults for the input fields. The 'remove'
+ * button is added unless the pattern is empty (incomplete filter condition).
+ * <p>
+ * Action handlers on these fields provide for
+ * <ul>
+ * <li>validate pattern field - should be numeric if condition is numeric</li>
+ * <li>save filters and refresh display on any (valid) change</li>
+ * <li>remove filter and refresh on 'Remove'</li>
+ * <li>update conditions list on change of Label/Score/Attribute</li>
+ * <li>refresh value field tooltip with min-max range on change of
+ * attribute</li>
+ * </ul>
+ *
+ * @param filter
+ * @param attNames
+ * @param filterIndex
+ * @return
+ */
+ protected JPanel addFilter(FeatureMatcherI filter,
+ List<String[]> attNames, int filterIndex)
+ {
+ String[] attName = filter.getAttribute();
+ Condition cond = filter.getMatcher().getCondition();
+ String pattern = filter.getMatcher().getPattern();
+
+ JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ filterRow.setBackground(Color.white);
+
+ /*
+ * drop-down choice of attribute, with description as a tooltip
+ * if we can obtain it
+ */
+ final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
+ true, true);
+ String filterBy = setSelectedAttribute(attCombo, filter);
+
+ JComboBox<Condition> condCombo = new JComboBox<>();
+
+ JTextField patternField = new JTextField(8);
+ patternField.setText(pattern);
+
+ /*
+ * action handlers that validate and (if valid) apply changes
+ */
+ ActionListener actionListener = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if (validateFilter(patternField, condCombo))
+ {
+ if (updateFilter(attCombo, condCombo, patternField, filterIndex))
+ {
+ filtersChanged();
+ }
+ }
+ }
+ };
+ ItemListener itemListener = new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ actionListener.actionPerformed(null);
+ }
+ };
+
+ if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
+ {
+ attCombo.setSelectedIndex(0);
+ }
+ else
+ {
+ attCombo.setSelectedItem(toAttributeDisplayName(attName));
+ }
+ attCombo.addItemListener(new ItemListener()
+ {
+ @Override
+ public void itemStateChanged(ItemEvent e)
+ {
+ /*
+ * on change of attribute, refresh the conditions list to
+ * ensure it is appropriate for the attribute datatype
+ */
+ populateConditions((String) attCombo.getSelectedItem(),
+ (Condition) condCombo.getSelectedItem(), condCombo,
+ patternField);
+ actionListener.actionPerformed(null);
+ }
+ });
+
+ filterRow.add(attCombo);
+
+ /*
+ * drop-down choice of test condition
+ */
+ populateConditions(filterBy, cond, condCombo, patternField);
+ condCombo.setPreferredSize(new Dimension(150, 20));
+ condCombo.addItemListener(itemListener);
+ filterRow.add(condCombo);
+
+ /*
+ * pattern to match against
+ */
+ patternField.addActionListener(actionListener);
+ patternField.addFocusListener(new FocusAdapter()
+ {
+ @Override
+ public void focusLost(FocusEvent e)
+ {
+ actionListener.actionPerformed(null);
+ }
+ });
+ filterRow.add(patternField);
+
+ /*
+ * disable pattern field for condition 'Present / NotPresent'
+ */
+ Condition selectedCondition = (Condition) condCombo.getSelectedItem();
+ if (!selectedCondition.needsAPattern())
+ {
+ patternField.setEnabled(false);
+ }
+
+ /*
+ * if a numeric condition is selected, show the value range
+ * as a tooltip on the value input field
+ */
+ setPatternTooltip(filterBy, selectedCondition, patternField);
+
+ /*
+ * add remove button if filter is populated (non-empty pattern)
+ */
+ if (!patternField.isEnabled()
+ || (pattern != null && pattern.trim().length() > 0))
+ {
+ // todo: gif for button drawing '-' or 'x'
+ JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
+ removeCondition
+ .setToolTipText(MessageManager.getString("label.delete_row"));
+ removeCondition.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ filters.remove(filterIndex);
+ filtersChanged();
+ }
+ });
+ filterRow.add(removeCondition);
+ }
+
+ return filterRow;
+ }
+
+ /**
+ * Sets the selected item in the Label/Score/Attribute drop-down to match the
+ * filter
+ *
+ * @param attCombo
+ * @param filter
+ */
+ private String setSelectedAttribute(JComboBox<String> attCombo,
+ FeatureMatcherI filter)
+ {
+ String item = null;
+ if (filter.isByScore())
+ {
+ item = SCORE_18N;
+ }
+ else if (filter.isByLabel())
+ {
+ item = LABEL_18N;
+ }
+ else
+ {
+ item = toAttributeDisplayName(filter.getAttribute());
+ }
+ attCombo.setSelectedItem(item);
+ return item;
+ }
+
+ /**
+ * If a numeric comparison condition is selected, retrieve the min-max range for
+ * the value (score or attribute), and set it as a tooltip on the value file
+ *
+ * @param attName
+ * @param selectedCondition
+ * @param patternField
+ */
+ private void setPatternTooltip(String attName,
+ Condition selectedCondition, JTextField patternField)
+ {
+ patternField.setToolTipText("");
+
+ if (selectedCondition.isNumeric())
+ {
+ float[] minMax = getMinMax(attName);
+ if (minMax != null)
+ {
+ String tip = String.format("(%s - %s)",
+ DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1]));
+ patternField.setToolTipText(tip);
+ }
+ }
+ }
+
+ /**
+ * Populates the drop-down list of comparison conditions for the given attribute
+ * name. The conditions added depend on the datatype of the attribute values.
+ * The supplied condition is set as the selected item in the list, provided it
+ * is in the list. If the pattern is now invalid (non-numeric pattern for a
+ * numeric condition), it is cleared.
+ *
+ * @param attName
+ * @param cond
+ * @param condCombo
+ * @param patternField
+ */
+ private void populateConditions(String attName, Condition cond,
+ JComboBox<Condition> condCombo, JTextField patternField)
+ {
+ Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
+ fromAttributeDisplayName(attName));
+ if (LABEL_18N.equals(attName))
+ {
+ type = Datatype.Character;
+ }
+ else if (SCORE_18N.equals(attName))
+ {
+ type = Datatype.Number;
+ }
+
+ /*
+ * remove itemListener before starting
+ */
+ ItemListener listener = condCombo.getItemListeners()[0];
+ condCombo.removeItemListener(listener);
+ boolean condIsValid = false;
+ condCombo.removeAllItems();
+ for (Condition c : Condition.values())
+ {
+ if ((c.isNumeric() && type != Datatype.Character)
+ || (!c.isNumeric() && type != Datatype.Number))
+ {
+ condCombo.addItem(c);
+ if (c == cond)
+ {
+ condIsValid = true;
+ }
+ }
+ }
+
+ /*
+ * set the selected condition (does nothing if not in the list)
+ */
+ if (condIsValid)
+ {
+ condCombo.setSelectedItem(cond);
+ }
+ else
+ {
+ condCombo.setSelectedIndex(0);
+ }
+
+ condCombo.addItemListener(listener);
+
+ /*
+ * clear pattern if it is now invalid for condition
+ */
+ if (((Condition) condCombo.getSelectedItem()).isNumeric())
+ {
+ try
+ {
+ String pattern = patternField.getText().trim();
+ if (pattern.length() > 0)
+ {
+ Float.valueOf(pattern);
+ }
+ } catch (NumberFormatException e)
+ {
+ patternField.setText("");
+ }
+ }
+ }
+
+ /**
+ * Answers true unless a numeric condition has been selected with a non-numeric
+ * value. Sets the value field to RED with a tooltip if in error.
+ * <p>
+ * If the pattern is expected but is empty, this method returns false, but does
+ * not mark the field as invalid. This supports selecting an attribute for a new
+ * condition before a match pattern has been entered.
+ *
+ * @param value
+ * @param condCombo
+ */
+ protected boolean validateFilter(JTextField value,
+ JComboBox<Condition> condCombo)
+ {
+ if (value == null || condCombo == null)
+ {
+ return true; // fields not populated
+ }
+
+ Condition cond = (Condition) condCombo.getSelectedItem();
+ if (!cond.needsAPattern())
+ {
+ return true;
+ }
+
+ value.setBackground(Color.white);
+ value.setToolTipText("");
+ String v1 = value.getText().trim();
+ if (v1.length() == 0)
+ {
+ // return false;
+ }
+
+ if (cond.isNumeric() && v1.length() > 0)
+ {
+ try
+ {
+ Float.valueOf(v1);
+ } catch (NumberFormatException e)
+ {
+ value.setBackground(Color.red);
+ value.setToolTipText(
+ MessageManager.getString("label.numeric_required"));
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Constructs a filter condition from the given input fields, and replaces the
+ * condition at filterIndex with the new one. Does nothing if the pattern field
+ * is blank (unless the match condition is one that doesn't require a pattern,
+ * e.g. 'Is present'). Answers true if the filter was updated, else false.
+ * <p>
+ * This method may update the tooltip on the filter value field to show the
+ * value range, if a numeric condition is selected. This ensures the tooltip is
+ * updated when a numeric valued attribute is chosen on the last 'add a filter'
+ * row.
+ *
+ * @param attCombo
+ * @param condCombo
+ * @param valueField
+ * @param filterIndex
+ */
+ protected boolean updateFilter(JComboBox<String> attCombo,
+ JComboBox<Condition> condCombo, JTextField valueField,
+ int filterIndex)
+ {
+ String attName = (String) attCombo.getSelectedItem();
+ Condition cond = (Condition) condCombo.getSelectedItem();
+ String pattern = valueField.getText().trim();
+
+ setPatternTooltip(attName, cond, valueField);
+
+ if (pattern.length() == 0 && cond.needsAPattern())
+ {
+ return false;
+ }
+
+ /*
+ * Construct a matcher that operates on Label, Score,
+ * or named attribute
+ */
+ FeatureMatcherI km = null;
+ if (LABEL_18N.equals(attName))
+ {
+ km = FeatureMatcher.byLabel(cond, pattern);
+ }
+ else if (SCORE_18N.equals(attName))
+ {
+ km = FeatureMatcher.byScore(cond, pattern);
+ }
+ else
+ {
+ km = FeatureMatcher.byAttribute(cond, pattern,
+ fromAttributeDisplayName(attName));
+ }
+
+ filters.set(filterIndex, km);
+
+ return true;
+ }
+
+ /**
+ * Makes the dialog visible, at the Feature Colour tab or at the Filters tab
+ *
+ * @param coloursTab
+ */
+ public void showTab(boolean coloursTab)
+ {
+ setVisible(true);
+ tabbedPane.setSelectedIndex(coloursTab ? 0 : 1);
+ }
+
+ /**
+ * Action on any change to feature filtering, namely
+ * <ul>
+ * <li>change of selected attribute</li>
+ * <li>change of selected condition</li>
+ * <li>change of match pattern</li>
+ * <li>removal of a condition</li>
+ * </ul>
+ * The inputs are parsed into a combined filter and this is set for the feature
+ * type, and the alignment redrawn.
+ */
+ protected void filtersChanged()
+ {
+ /*
+ * update the filter conditions for the feature type
+ */
+ boolean anded = andFilters.isSelected();
+ FeatureMatcherSetI combined = new FeatureMatcherSet();
+
+ for (FeatureMatcherI filter : filters)
+ {
+ String pattern = filter.getMatcher().getPattern();
+ Condition condition = filter.getMatcher().getCondition();
+ if (pattern.trim().length() > 0 || !condition.needsAPattern())
+ {
+ if (anded)
+ {
+ combined.and(filter);
+ }
+ else
+ {
+ combined.or(filter);
+ }
+ }
+ }
+
+ /*
+ * save the filter conditions in the FeatureRenderer
+ * (note this might now be an empty filter with no conditions)
+ */
+ fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
+ ap.paintAlignment(true, true);
+
+ updateFiltersTab();
+ }
+}
SequenceI sequence = av.getAlignment().getSequenceAt(seq);
StringBuilder tip = new StringBuilder(64);
seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
- av.isShowDBRefs(), av.isShowNPFeats(),
- sp.seqCanvas.fr.getMinMax());
+ av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
setToolTipText(JvSwingUtils.wrapTooltip(true,
sequence.getDisplayId(true) + " " + tip.toString()));
}
* and any non-positional features
*/
List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
- for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
+ List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
+ for (SequenceFeature sf : features)
{
if (sf.links != null)
{
}
}
- PopupMenu pop = new PopupMenu(alignPanel, sq, nlinks,
+ PopupMenu pop = new PopupMenu(alignPanel, sq, features,
Preferences.getGroupURLLinks());
pop.show(this, e.getX(), e.getY());
}
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
-import java.awt.event.WindowListener;
import javax.swing.JButton;
import javax.swing.JDialog;
closeDialog();
}
});
- frame.addWindowListener(new WindowListener()
+ frame.addWindowListener(new WindowAdapter()
{
-
- @Override
- public void windowOpened(WindowEvent e)
- {
- // TODO Auto-generated method stub
-
- }
-
- @Override
- public void windowIconified(WindowEvent e)
- {
- // TODO Auto-generated method stub
-
- }
-
- @Override
- public void windowDeiconified(WindowEvent e)
- {
- // TODO Auto-generated method stub
-
- }
-
- @Override
- public void windowDeactivated(WindowEvent e)
- {
- // TODO Auto-generated method stub
-
- }
-
@Override
public void windowClosing(WindowEvent e)
{
// user has cancelled the dialog
closeDialog();
}
-
- @Override
- public void windowClosed(WindowEvent e)
- {
- }
-
- @Override
- public void windowActivated(WindowEvent e)
- {
- // TODO Auto-generated method stub
-
- }
});
}
{
try
{
- frame.dispose();
raiseClosed();
+ frame.dispose();
} catch (Exception ex)
{
}
import java.awt.BorderLayout;
import java.awt.Color;
+import java.awt.Component;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.List;
import java.util.Objects;
import javax.swing.AbstractButton;
+import javax.swing.BorderFactory;
import javax.swing.JButton;
+import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.SwingConstants;
+import javax.swing.border.Border;
+import javax.swing.border.TitledBorder;
/**
* useful functions for building Swing GUIs
comp.setFont(JvSwingUtils.getLabelFont());
}
+ /**
+ * A helper method to build a drop-down choice of values, with tooltips for
+ * the entries
+ *
+ * @param entries
+ * @param tooltips
+ */
+ public static JComboBox<String> buildComboWithTooltips(
+ List<String> entries, List<String> tooltips)
+ {
+ JComboBox<String> combo = new JComboBox<>();
+ final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
+ combo.setRenderer(renderer);
+ for (String attName : entries)
+ {
+ combo.addItem(attName);
+ }
+ renderer.setTooltips(tooltips);
+ final MouseAdapter mouseListener = new MouseAdapter()
+ {
+ @Override
+ public void mouseEntered(MouseEvent e)
+ {
+ int j = combo.getSelectedIndex();
+ if (j > -1)
+ {
+ combo.setToolTipText(tooltips.get(j));
+ }
+ }
+ @Override
+ public void mouseExited(MouseEvent e)
+ {
+ combo.setToolTipText(null);
+ }
+ };
+ for (Component c : combo.getComponents())
+ {
+ c.addMouseListener(mouseListener);
+ }
+ return combo;
+ }
+
+ /**
+ * Adds a titled border to the component in the default font and position (top
+ * left), optionally witht italic text
+ *
+ * @param comp
+ * @param title
+ * @param italic
+ */
+ public static TitledBorder createTitledBorder(JComponent comp,
+ String title, boolean italic)
+ {
+ Font font = comp.getFont();
+ if (italic)
+ {
+ font = new Font(font.getName(), Font.ITALIC, font.getSize());
+ }
+ Border border = BorderFactory.createTitledBorder("");
+ TitledBorder titledBorder = BorderFactory.createTitledBorder(border,
+ title, TitledBorder.LEADING, TitledBorder.DEFAULT_POSITION,
+ font);
+ comp.setBorder(titledBorder);
+
+ return titledBorder;
+ }
+
}
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;
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;
* 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())
+ {
+ return;
+ }
+ JMenu details = new JMenu(
+ MessageManager.getString("label.feature_details"));
+ add(details);
+
+ for (final SequenceFeature sf : features)
{
- addFeatureLinks(seq, links);
+ int start = sf.getBegin();
+ int end = sf.getEnd();
+ String desc = null;
+ if (start == end)
+ {
+ desc = String.format("%s %d", sf.getType(), start);
+ }
+ else
+ {
+ desc = String.format("%s %d-%d", sf.getType(), start, end);
+ }
+ String description = sf.getDescription();
+ if (description != null)
+ {
+ description = StringUtils.stripHtmlTags(description);
+ if (description.length() <= 6)
+ {
+ desc = desc + " " + description;
+ }
+ else
+ {
+ desc = desc + " " + description.substring(0, 6) + "..";
+ }
+ }
+ if (sf.getFeatureGroup() != null)
+ {
+ desc = desc + " (" + sf.getFeatureGroup() + ")";
+ }
+ JMenuItem item = new JMenuItem(desc);
+ 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);
+ }
}
-
}
/**
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>");
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;
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);
}
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))
{
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());
}
*/
package jalview.io;
+import jalview.api.FeatureColourI;
import jalview.datamodel.DBRefEntry;
import jalview.datamodel.DBRefSource;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
-import jalview.io.gff.GffConstants;
import jalview.util.MessageManager;
+import jalview.util.StringUtils;
import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
import java.util.Arrays;
import java.util.Collection;
/*
* Comparator to order DBRefEntry by Source + accession id (case-insensitive),
- * with 'Primary' sources placed before others
+ * with 'Primary' sources placed before others, and 'chromosome' first of all
*/
private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
{
@Override
public int compare(DBRefEntry ref1, DBRefEntry ref2)
{
+ if (ref1.isChromosome())
+ {
+ return -1;
+ }
+ if (ref2.isChromosome())
+ {
+ return 1;
+ }
String s1 = ref1.getSource();
String s2 = ref2.getSource();
boolean s1Primary = isPrimarySource(s1);
{
return 1;
}
- int comp = s1 == null ? -1
- : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
+ int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
+ .compareToIgnoreCase(s2));
if (comp == 0)
{
String a1 = ref1.getAccessionId();
String a2 = ref2.getAccessionId();
- comp = a1 == null ? -1
- : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
+ comp = a1 == null ? -1 : (a2 == null ? 1 : a1
+ .compareToIgnoreCase(a2));
}
return comp;
}
}
};
- public SequenceAnnotationReport(String linkImageURL)
+ public SequenceAnnotationReport(String linkURL)
{
- this.linkImageURL = linkImageURL;
+ this.linkImageURL = linkURL;
}
/**
* @param minmax
*/
public void appendFeatures(final StringBuilder sb, int rpos,
- List<SequenceFeature> features, Map<String, float[][]> minmax)
+ List<SequenceFeature> features, FeatureRendererModel fr)
{
if (features != null)
{
for (SequenceFeature feature : features)
{
- appendFeature(sb, rpos, minmax, feature);
+ appendFeature(sb, rpos, fr, feature);
}
}
}
* @param feature
*/
void appendFeature(final StringBuilder sb, int rpos,
- Map<String, float[][]> minmax, SequenceFeature feature)
+ FeatureRendererModel fr, SequenceFeature feature)
{
if (feature.isContactFeature())
{
sb.append(feature.getType()).append(" ").append(feature.getBegin())
.append(":").append(feature.getEnd());
}
+ return;
}
- else
+
+ if (sb.length() > 6)
{
- if (sb.length() > 6)
+ sb.append("<br>");
+ }
+ // TODO: remove this hack to display link only features
+ boolean linkOnly = feature.getValue("linkonly") != null;
+ if (!linkOnly)
+ {
+ sb.append(feature.getType()).append(" ");
+ if (rpos != 0)
{
- sb.append("<br>");
+ // we are marking a positional feature
+ sb.append(feature.begin);
}
- // TODO: remove this hack to display link only features
- boolean linkOnly = feature.getValue("linkonly") != null;
- if (!linkOnly)
+ if (feature.begin != feature.end)
{
- sb.append(feature.getType()).append(" ");
- if (rpos != 0)
- {
- // we are marking a positional feature
- sb.append(feature.begin);
- }
- if (feature.begin != feature.end)
- {
- sb.append(" ").append(feature.end);
- }
+ sb.append(" ").append(feature.end);
+ }
- if (feature.getDescription() != null
- && !feature.description.equals(feature.getType()))
- {
- String tmpString = feature.getDescription();
- String tmp2up = tmpString.toUpperCase();
- int startTag = tmp2up.indexOf("<HTML>");
- if (startTag > -1)
- {
- tmpString = tmpString.substring(startTag + 6);
- tmp2up = tmp2up.substring(startTag + 6);
- }
- int endTag = tmp2up.indexOf("</BODY>");
- if (endTag > -1)
- {
- tmpString = tmpString.substring(0, endTag);
- tmp2up = tmp2up.substring(0, endTag);
- }
- endTag = tmp2up.indexOf("</HTML>");
- if (endTag > -1)
- {
- tmpString = tmpString.substring(0, endTag);
- }
+ String description = feature.getDescription();
+ if (description != null && !description.equals(feature.getType()))
+ {
+ description = StringUtils.stripHtmlTags(description);
+ sb.append("; ").append(description);
+ }
- if (startTag > -1)
- {
- sb.append("; ").append(tmpString);
- }
- else
- {
- if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
- {
- // The description does not specify html is to
- // be used, so we must remove < > symbols
- tmpString = tmpString.replaceAll("<", "<");
- tmpString = tmpString.replaceAll(">", ">");
+ if (showScore(feature, fr))
+ {
+ sb.append(" Score=").append(String.valueOf(feature.getScore()));
+ }
+ String status = (String) feature.getValue("status");
+ if (status != null && status.length() > 0)
+ {
+ sb.append("; (").append(status).append(")");
+ }
- sb.append("; ");
- sb.append(tmpString);
- }
- else
- {
- sb.append("; ").append(tmpString);
- }
- }
- }
- // check score should be shown
- if (!Float.isNaN(feature.getScore()))
+ /*
+ * add attribute value if coloured by attribute
+ */
+ if (fr != null)
+ {
+ FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
+ if (fc != null && fc.isColourByAttribute())
{
- float[][] rng = (minmax == null) ? null
- : minmax.get(feature.getType());
- if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
+ String[] attName = fc.getAttributeName();
+ String attVal = feature.getValueAsString(attName);
+ if (attVal != null)
{
- sb.append(" Score=").append(String.valueOf(feature.getScore()));
+ sb.append("; ").append(String.join(":", attName)).append("=")
+ .append(attVal);
}
}
- String status = (String) feature.getValue("status");
- if (status != null && status.length() > 0)
- {
- sb.append("; (").append(status).append(")");
- }
- String clinSig = (String) feature
- .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
- if (clinSig != null)
- {
- sb.append("; ").append(clinSig);
- }
}
}
}
/**
+ * Answers true if score should be shown, else false. Score is shown if it is
+ * not NaN, and the feature type has a non-trivial min-max score range
+ */
+ boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
+ {
+ if (Float.isNaN(feature.getScore()))
+ {
+ return false;
+ }
+ if (fr == null)
+ {
+ return true;
+ }
+ float[][] minMax = fr.getMinMax().get(feature.getType());
+
+ /*
+ * minMax[0] is the [min, max] score range for positional features
+ */
+ if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
+ {
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Format and appends any hyperlinks for the sequence feature to the string
* buffer
*
{
for (List<String> urllink : createLinksFrom(null, urlstring))
{
- sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
- + urllink.get(0) + "\">"
+ sb.append("<br/> <a href=\""
+ + urllink.get(3)
+ + "\" target=\""
+ + urllink.get(0)
+ + "\">"
+ (urllink.get(0).toLowerCase()
- .equals(urllink.get(1).toLowerCase())
- ? urllink.get(0)
- : (urllink.get(0) + ":"
- + urllink.get(1)))
- + "</a></br>");
+ .equals(urllink.get(1).toLowerCase()) ? urllink
+ .get(0) : (urllink.get(0) + ":" + urllink
+ .get(1))) + "</a></br>");
}
} catch (Exception x)
{
- System.err.println(
- "problem when creating links from " + urlstring);
+ System.err.println("problem when creating links from "
+ + urlstring);
x.printStackTrace();
}
}
*/
Collection<List<String>> createLinksFrom(SequenceI seq, String link)
{
- Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
+ Map<String, List<String>> urlSets = new LinkedHashMap<>();
UrlLink urlLink = new UrlLink(link);
if (!urlLink.isValid())
{
public void createSequenceAnnotationReport(final StringBuilder tip,
SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
- Map<String, float[][]> minmax)
+ FeatureRendererModel fr)
{
createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
- minmax, false);
+ fr, false);
}
/**
* whether to include database references for the sequence
* @param showNpFeats
* whether to include non-positional sequence features
- * @param minmax
+ * @param fr
* @param summary
* @return
*/
int createSequenceAnnotationReport(final StringBuilder sb,
SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
- Map<String, float[][]> minmax, boolean summary)
+ FeatureRendererModel fr, boolean summary)
{
String tmp;
sb.append("<i>");
{
ds = ds.getDatasetSequence();
}
-
+
if (showDbRefs)
{
maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
.getNonPositionalFeatures())
{
int sz = -sb.length();
- appendFeature(sb, 0, minmax, sf);
+ appendFeature(sb, 0, fr, sf);
sz += sb.length();
maxWidth = Math.max(maxWidth, sz);
}
}
if (moreSources)
{
- sb.append("<br>").append(source)
- .append(COMMA).append(ELLIPSIS);
+ sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
}
if (ellipsis)
{
public void createTooltipAnnotationReport(final StringBuilder tip,
SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
- Map<String, float[][]> minmax)
+ FeatureRendererModel fr)
{
- int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
- showNpFeats, minmax, true);
+ int maxWidth = createSequenceAnnotationReport(tip, sequence,
+ showDbRefs, showNpFeats, fr, true);
if (maxWidth > 60)
{
*/
public class Gff3Helper extends GffHelperBase
{
+ public static final String ALLELES = "alleles";
+
protected static final String TARGET = "Target";
protected static final String ID = "ID";
/*
* Ensembl returns dna variants as 'alleles'
*/
- desc = StringUtils.listToDelimitedString(attributes.get("alleles"),
+ desc = StringUtils.listToDelimitedString(attributes.get(ALLELES),
",");
}
--- /dev/null
+package jalview.io.vcf;
+
+import jalview.analysis.AlignmentUtils;
+import jalview.analysis.Dna;
+import jalview.api.AlignViewControllerGuiI;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureSource;
+import jalview.datamodel.features.FeatureSources;
+import jalview.ext.ensembl.EnsemblMap;
+import jalview.ext.htsjdk.VCFReader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
+import jalview.util.MessageManager;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import htsjdk.samtools.SAMSequenceRecord;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFContigHeaderLine;
+import htsjdk.variant.vcf.VCFHeader;
+import htsjdk.variant.vcf.VCFHeaderLine;
+import htsjdk.variant.vcf.VCFHeaderLineCount;
+import htsjdk.variant.vcf.VCFHeaderLineType;
+import htsjdk.variant.vcf.VCFInfoHeaderLine;
+
+/**
+ * A class to read VCF data (using the htsjdk) and add variants as sequence
+ * features on dna and any related protein product sequences
+ *
+ * @author gmcarstairs
+ */
+public class VCFLoader
+{
+ /**
+ * A class to model the mapping from sequence to VCF coordinates. Cases include
+ * <ul>
+ * <li>a direct 1:1 mapping where the sequence is one of the VCF contigs</li>
+ * <li>a mapping of sequence to chromosomal coordinates, where sequence and VCF
+ * use the same reference assembly</li>
+ * <li>a modified mapping of sequence to chromosomal coordinates, where sequence
+ * and VCF use different reference assembles</li>
+ * </ul>
+ */
+ class VCFMap
+ {
+ final String chromosome;
+
+ final MapList map;
+
+ VCFMap(String chr, MapList m)
+ {
+ chromosome = chr;
+ map = m;
+ }
+ }
+
+ /*
+ * Lookup keys, and default values, for Preference entries that describe
+ * patterns for VCF and VEP fields to capture
+ */
+ private static final String VEP_FIELDS_PREF = "VEP_FIELDS";
+
+ private static final String VCF_FIELDS_PREF = "VCF_FIELDS";
+
+ private static final String DEFAULT_VCF_FIELDS = ".*";
+
+ private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG";
+
+ /*
+ * keys to fields of VEP CSQ consequence data
+ * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+ */
+ private static final String ALLELE_KEY = "Allele";
+
+ private static final String ALLELE_NUM_KEY = "ALLELE_NUM"; // 0 (ref), 1...
+ private static final String FEATURE_KEY = "Feature"; // Ensembl stable id
+
+ /*
+ * default VCF INFO key for VEP consequence data
+ * NB this can be overridden running VEP with --vcf_info_field
+ * - we don't handle this case (require identifier to be CSQ)
+ */
+ private static final String CSQ_FIELD = "CSQ";
+
+ /*
+ * separator for fields in consequence data is '|'
+ */
+ private static final String PIPE_REGEX = "\\|";
+
+ /*
+ * key for Allele Frequency output by VEP
+ * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+ */
+ private static final String ALLELE_FREQUENCY_KEY = "AF";
+
+ /*
+ * delimiter that separates multiple consequence data blocks
+ */
+ private static final String COMMA = ",";
+
+ /*
+ * the feature group assigned to a VCF variant in Jalview
+ */
+ private static final String FEATURE_GROUP_VCF = "VCF";
+
+ /*
+ * internal delimiter used to build keys for assemblyMappings
+ *
+ */
+ private static final String EXCL = "!";
+
+ /*
+ * the alignment we are associating VCF data with
+ */
+ private AlignmentI al;
+
+ /*
+ * mappings between VCF and sequence reference assembly regions, as
+ * key = "species!chromosome!fromAssembly!toAssembly
+ * value = Map{fromRange, toRange}
+ */
+ private Map<String, Map<int[], int[]>> assemblyMappings;
+
+ /*
+ * holds details of the VCF header lines (metadata)
+ */
+ private VCFHeader header;
+
+ /*
+ * the position (0...) of field in each block of
+ * CSQ (consequence) data (if declared in the VCF INFO header for CSQ)
+ * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+ */
+ private int csqAlleleFieldIndex = -1;
+ private int csqAlleleNumberFieldIndex = -1;
+ private int csqFeatureFieldIndex = -1;
+
+ /*
+ * a unique identifier under which to save metadata about feature
+ * attributes (selected INFO field data)
+ */
+ private String sourceId;
+
+ /*
+ * The INFO IDs of data that is both present in the VCF file, and
+ * also matched by any filters for data of interest
+ */
+ List<String> vcfFieldsOfInterest;
+
+ /*
+ * The field offsets and identifiers for VEP (CSQ) data that is both present
+ * in the VCF file, and also matched by any filters for data of interest
+ * for example 0 -> Allele, 1 -> Consequence, ..., 36 -> SIFT, ...
+ */
+ Map<Integer, String> vepFieldsOfInterest;
+
+ /**
+ * Constructor given an alignment context
+ *
+ * @param alignment
+ */
+ public VCFLoader(AlignmentI alignment)
+ {
+ al = alignment;
+
+ // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange}
+ assemblyMappings = new HashMap<>();
+ }
+
+ /**
+ * Starts a new thread to query and load VCF variant data on to the alignment
+ * <p>
+ * This method is not thread safe - concurrent threads should use separate
+ * instances of this class.
+ *
+ * @param filePath
+ * @param gui
+ */
+ public void loadVCF(final String filePath,
+ final AlignViewControllerGuiI gui)
+ {
+ if (gui != null)
+ {
+ gui.setStatus(MessageManager.getString("label.searching_vcf"));
+ }
+
+ new Thread()
+ {
+
+ @Override
+ public void run()
+ {
+ VCFLoader.this.doLoad(filePath, gui);
+ }
+
+ }.start();
+ }
+
+ /**
+ * Loads VCF on to an alignment - provided it can be related to one or more
+ * sequence's chromosomal coordinates
+ *
+ * @param filePath
+ * @param gui
+ * optional callback handler for messages
+ */
+ protected void doLoad(String filePath, AlignViewControllerGuiI gui)
+ {
+ VCFReader reader = null;
+ try
+ {
+ // long start = System.currentTimeMillis();
+ reader = new VCFReader(filePath);
+
+ header = reader.getFileHeader();
+
+ sourceId = filePath;
+
+ saveMetadata(sourceId);
+
+ /*
+ * get offset of CSQ ALLELE_NUM and Feature if declared
+ */
+ parseCsqHeader();
+
+ VCFHeaderLine ref = header
+ .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
+ String vcfAssembly = ref.getValue();
+
+ int varCount = 0;
+ int seqCount = 0;
+
+ /*
+ * query for VCF overlapping each sequence in turn
+ */
+ for (SequenceI seq : al.getSequences())
+ {
+ int added = loadSequenceVCF(seq, reader, vcfAssembly);
+ if (added > 0)
+ {
+ seqCount++;
+ varCount += added;
+ transferAddedFeatures(seq);
+ }
+ }
+ if (gui != null)
+ {
+ // long elapsed = System.currentTimeMillis() - start;
+ String msg = MessageManager.formatMessage("label.added_vcf",
+ varCount, seqCount);
+ gui.setStatus(msg);
+ if (gui.getFeatureSettingsUI() != null)
+ {
+ gui.getFeatureSettingsUI().discoverAllFeatureData();
+ }
+ }
+ } catch (Throwable e)
+ {
+ System.err.println("Error processing VCF: " + e.getMessage());
+ e.printStackTrace();
+ if (gui != null)
+ {
+ gui.setStatus("Error occurred - see console for details");
+ }
+ } finally
+ {
+ if (reader != null)
+ {
+ try
+ {
+ reader.close();
+ } catch (IOException e)
+ {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads metadata (such as INFO field descriptions and datatypes) and saves
+ * them for future reference
+ *
+ * @param theSourceId
+ */
+ void saveMetadata(String theSourceId)
+ {
+ List<Pattern> vcfFieldPatterns = getFieldMatchers(VCF_FIELDS_PREF,
+ DEFAULT_VCF_FIELDS);
+ vcfFieldsOfInterest = new ArrayList<>();
+
+ FeatureSource metadata = new FeatureSource(theSourceId);
+
+ for (VCFInfoHeaderLine info : header.getInfoHeaderLines())
+ {
+ String attributeId = info.getID();
+ String desc = info.getDescription();
+ VCFHeaderLineType type = info.getType();
+ FeatureAttributeType attType = null;
+ switch (type)
+ {
+ case Character:
+ attType = FeatureAttributeType.Character;
+ break;
+ case Flag:
+ attType = FeatureAttributeType.Flag;
+ break;
+ case Float:
+ attType = FeatureAttributeType.Float;
+ break;
+ case Integer:
+ attType = FeatureAttributeType.Integer;
+ break;
+ case String:
+ attType = FeatureAttributeType.String;
+ break;
+ }
+ metadata.setAttributeName(attributeId, desc);
+ metadata.setAttributeType(attributeId, attType);
+
+ if (isFieldWanted(attributeId, vcfFieldPatterns))
+ {
+ vcfFieldsOfInterest.add(attributeId);
+ }
+ }
+
+ FeatureSources.getInstance().addSource(theSourceId, metadata);
+ }
+
+ /**
+ * Answers true if the field id is matched by any of the filter patterns, else
+ * false. Matching is against regular expression patterns, and is not
+ * case-sensitive.
+ *
+ * @param id
+ * @param filters
+ * @return
+ */
+ private boolean isFieldWanted(String id, List<Pattern> filters)
+ {
+ for (Pattern p : filters)
+ {
+ if (p.matcher(id.toUpperCase()).matches())
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Records 'wanted' fields defined in the CSQ INFO header (if there is one).
+ * Also records the position of selected fields (Allele, ALLELE_NUM, Feature)
+ * required for processing.
+ * <p>
+ * CSQ fields are declared in the CSQ INFO Description e.g.
+ * <p>
+ * Description="Consequence ...from ... VEP. Format: Allele|Consequence|...
+ */
+ protected void parseCsqHeader()
+ {
+ List<Pattern> vepFieldFilters = getFieldMatchers(VEP_FIELDS_PREF,
+ DEFAULT_VEP_FIELDS);
+ vepFieldsOfInterest = new HashMap<>();
+
+ VCFInfoHeaderLine csqInfo = header.getInfoHeaderLine(CSQ_FIELD);
+ if (csqInfo == null)
+ {
+ return;
+ }
+
+ /*
+ * parse out the pipe-separated list of CSQ fields; we assume here that
+ * these form the last part of the description, and contain no spaces
+ */
+ String desc = csqInfo.getDescription();
+ int spacePos = desc.lastIndexOf(" ");
+ desc = desc.substring(spacePos + 1);
+
+ if (desc != null)
+ {
+ String[] format = desc.split(PIPE_REGEX);
+ int index = 0;
+ for (String field : format)
+ {
+ if (ALLELE_NUM_KEY.equals(field))
+ {
+ csqAlleleNumberFieldIndex = index;
+ }
+ if (ALLELE_KEY.equals(field))
+ {
+ csqAlleleFieldIndex = index;
+ }
+ if (FEATURE_KEY.equals(field))
+ {
+ csqFeatureFieldIndex = index;
+ }
+
+ if (isFieldWanted(field, vepFieldFilters))
+ {
+ vepFieldsOfInterest.put(index, field);
+ }
+
+ index++;
+ }
+ }
+ }
+
+ /**
+ * Reads the Preference value for the given key, with default specified if no
+ * preference set. The value is interpreted as a comma-separated list of
+ * regular expressions, and converted into a list of compiled patterns ready
+ * for matching. Patterns are forced to upper-case for non-case-sensitive
+ * matching.
+ * <p>
+ * This supports user-defined filters for fields of interest to capture while
+ * processing data. For example, VCF_FIELDS = AF,AC* would mean that VCF INFO
+ * fields with an ID of AF, or starting with AC, would be matched.
+ *
+ * @param key
+ * @param def
+ * @return
+ */
+ private List<Pattern> getFieldMatchers(String key, String def)
+ {
+ String pref = Cache.getDefault(key, def);
+ List<Pattern> patterns = new ArrayList<>();
+ String[] tokens = pref.split(",");
+ for (String token : tokens)
+ {
+ try
+ {
+ patterns.add(Pattern.compile(token.toUpperCase()));
+ } catch (PatternSyntaxException e)
+ {
+ System.err.println("Invalid pattern ignored: " + token);
+ }
+ }
+ return patterns;
+ }
+
+ /**
+ * Transfers VCF features to sequences to which this sequence has a mapping.
+ * If the mapping is 3:1, computes peptide variants from nucleotide variants.
+ *
+ * @param seq
+ */
+ protected void transferAddedFeatures(SequenceI seq)
+ {
+ DBRefEntry[] dbrefs = seq.getDBRefs();
+ if (dbrefs == null)
+ {
+ return;
+ }
+ for (DBRefEntry dbref : dbrefs)
+ {
+ Mapping mapping = dbref.getMap();
+ if (mapping == null || mapping.getTo() == null)
+ {
+ continue;
+ }
+
+ SequenceI mapTo = mapping.getTo();
+ MapList map = mapping.getMap();
+ if (map.getFromRatio() == 3)
+ {
+ /*
+ * dna-to-peptide product mapping
+ */
+ AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
+ }
+ else
+ {
+ /*
+ * nucleotide-to-nucleotide mapping e.g. transcript to CDS
+ */
+ List<SequenceFeature> features = seq.getFeatures()
+ .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
+ for (SequenceFeature sf : features)
+ {
+ if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup()))
+ {
+ transferFeature(sf, mapTo, map);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Tries to add overlapping variants read from a VCF file to the given
+ * sequence, and returns the number of variant features added. Note that this
+ * requires the sequence to hold information as to its species, chromosomal
+ * positions and reference assembly, in order to be able to map the VCF
+ * variants to the sequence (or not)
+ *
+ * @param seq
+ * @param reader
+ * @param vcfAssembly
+ * @return
+ */
+ protected int loadSequenceVCF(SequenceI seq, VCFReader reader,
+ String vcfAssembly)
+ {
+ VCFMap vcfMap = getVcfMap(seq, vcfAssembly);
+ if (vcfMap == null)
+ {
+ return 0;
+ }
+
+ return addVcfVariants(seq, reader, vcfMap, vcfAssembly);
+ }
+
+ /**
+ * Answers a map from sequence coordinates to VCF chromosome ranges
+ *
+ * @param seq
+ * @param vcfAssembly
+ * @return
+ */
+ private VCFMap getVcfMap(SequenceI seq, String vcfAssembly)
+ {
+ /*
+ * simplest case: sequence has id and length matching a VCF contig
+ */
+ GeneLociI seqCoords = seq.getGeneLoci();
+ if (seqCoords == null)
+ {
+ VCFMap map = getContigMap(seq);
+ if (map == null)
+ {
+ Cache.log.warn(String.format(
+ "Can't query VCF for %s as chromosome coordinates not known",
+ seq.getName()));
+ }
+ return map;
+ }
+
+ String species = seqCoords.getSpeciesId();
+ String chromosome = seqCoords.getChromosomeId();
+ String seqRef = seqCoords.getAssemblyId();
+ MapList map = seqCoords.getMap();
+
+ if (!vcfSpeciesMatchesSequence(vcfAssembly, species))
+ {
+ return null;
+ }
+
+ if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef))
+ {
+ return new VCFMap(chromosome, map);
+ }
+
+ if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl
+ || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
+ {
+ return null;
+ }
+
+ /*
+ * map chromosomal coordinates from sequence to VCF if the VCF
+ * data has a different reference assembly to the sequence
+ */
+ // TODO generalise for cases other than GRCh38 -> GRCh37 !
+ // - or get the user to choose in a dialog
+
+ List<int[]> toVcfRanges = new ArrayList<>();
+ List<int[]> fromSequenceRanges = new ArrayList<>();
+ String toRef = "GRCh37";
+
+ for (int[] range : map.getToRanges())
+ {
+ int[] fromRange = map.locateInFrom(range[0], range[1]);
+ if (fromRange == null)
+ {
+ // corrupted map?!?
+ continue;
+ }
+
+ int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef,
+ toRef);
+ if (newRange == null)
+ {
+ Cache.log.error(
+ String.format("Failed to map %s:%s:%s:%d:%d to %s", species,
+ chromosome, seqRef, range[0], range[1], toRef));
+ continue;
+ }
+ else
+ {
+ toVcfRanges.add(newRange);
+ fromSequenceRanges.add(fromRange);
+ }
+ }
+
+ return new VCFMap(chromosome,
+ new MapList(fromSequenceRanges, toVcfRanges, 1, 1));
+ }
+
+ /**
+ * If the sequence id matches a contig declared in the VCF file, and the
+ * sequence matches the contig length, then returns a 1:1 map of the sequence to
+ * the contig, else returns null
+ *
+ * @param seq
+ * @return
+ */
+ private VCFMap getContigMap(SequenceI seq)
+ {
+ String id = seq.getName();
+ for (VCFContigHeaderLine contig : header.getContigLines())
+ {
+ if (contig.getID().equals(id))
+ {
+ /*
+ * have to construct a SAMSequenceRecord to
+ * read the contig 'length' field!
+ */
+ int len = seq.getLength();
+ SAMSequenceRecord ssr = contig.getSAMSequenceRecord();
+ if (len == ssr.getSequenceLength())
+ {
+ MapList map = new MapList(new int[] { 1, len },
+ new int[]
+ { 1, len }, 1, 1);
+ return new VCFMap(id, map);
+ }
+ }
+
+ }
+ return null;
+ }
+
+ /**
+ * Answers true if we determine that the VCF data uses the same reference
+ * assembly as the sequence, else false
+ *
+ * @param vcfAssembly
+ * @param seqRef
+ * @return
+ */
+ private boolean vcfAssemblyMatchesSequence(String vcfAssembly,
+ String seqRef)
+ {
+ // TODO improve on this stub, which handles gnomAD and
+ // hopes for the best for other cases
+
+ if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl
+ && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
+ {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Answers true if the species inferred from the VCF reference identifier
+ * matches that for the sequence
+ *
+ * @param vcfAssembly
+ * @param speciesId
+ * @return
+ */
+ boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId)
+ {
+ // PROBLEM 1
+ // there are many aliases for species - how to equate one with another?
+ // PROBLEM 2
+ // VCF ##reference header is an unstructured URI - how to extract species?
+ // perhaps check if ref includes any (Ensembl) alias of speciesId??
+ // TODO ask the user to confirm this??
+
+ if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example
+ && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id
+ {
+ return true;
+ }
+
+ if (vcfAssembly.contains("c_elegans") // VEP VCF response example
+ && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl
+ {
+ return true;
+ }
+
+ // this is not a sustainable solution...
+
+ return false;
+ }
+
+ /**
+ * Queries the VCF reader for any variants that overlap the mapped chromosome
+ * ranges of the sequence, and adds as variant features. Returns the number of
+ * overlapping variants found.
+ *
+ * @param seq
+ * @param reader
+ * @param map
+ * mapping from sequence to VCF coordinates
+ * @param vcfAssembly
+ * the '##reference' identifier for the VCF reference assembly
+ * @return
+ */
+ protected int addVcfVariants(SequenceI seq, VCFReader reader,
+ VCFMap map, String vcfAssembly)
+ {
+ boolean forwardStrand = map.map.isToForwardStrand();
+
+ /*
+ * query the VCF for overlaps of each contiguous chromosomal region
+ */
+ int count = 0;
+
+ for (int[] range : map.map.getToRanges())
+ {
+ int vcfStart = Math.min(range[0], range[1]);
+ int vcfEnd = Math.max(range[0], range[1]);
+ CloseableIterator<VariantContext> variants = reader
+ .query(map.chromosome, vcfStart, vcfEnd);
+ while (variants.hasNext())
+ {
+ VariantContext variant = variants.next();
+
+ int[] featureRange = map.map.locateInFrom(variant.getStart(),
+ variant.getEnd());
+
+ if (featureRange != null)
+ {
+ int featureStart = Math.min(featureRange[0], featureRange[1]);
+ int featureEnd = Math.max(featureRange[0], featureRange[1]);
+ count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
+ forwardStrand);
+ }
+ }
+ variants.close();
+ }
+
+ return count;
+ }
+
+ /**
+ * A convenience method to get the AF value for the given alternate allele
+ * index
+ *
+ * @param variant
+ * @param alleleIndex
+ * @return
+ */
+ protected float getAlleleFrequency(VariantContext variant, int alleleIndex)
+ {
+ float score = 0f;
+ String attributeValue = getAttributeValue(variant,
+ ALLELE_FREQUENCY_KEY, alleleIndex);
+ if (attributeValue != null)
+ {
+ try
+ {
+ score = Float.parseFloat(attributeValue);
+ } catch (NumberFormatException e)
+ {
+ // leave as 0
+ }
+ }
+
+ return score;
+ }
+
+ /**
+ * A convenience method to get an attribute value for an alternate allele
+ *
+ * @param variant
+ * @param attributeName
+ * @param alleleIndex
+ * @return
+ */
+ protected String getAttributeValue(VariantContext variant,
+ String attributeName, int alleleIndex)
+ {
+ Object att = variant.getAttribute(attributeName);
+
+ if (att instanceof String)
+ {
+ return (String) att;
+ }
+ else if (att instanceof ArrayList)
+ {
+ return ((List<String>) att).get(alleleIndex);
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds one variant feature for each allele in the VCF variant record, and
+ * returns the number of features added.
+ *
+ * @param seq
+ * @param variant
+ * @param featureStart
+ * @param featureEnd
+ * @param forwardStrand
+ * @return
+ */
+ protected int addAlleleFeatures(SequenceI seq, VariantContext variant,
+ int featureStart, int featureEnd, boolean forwardStrand)
+ {
+ int added = 0;
+
+ /*
+ * Javadoc says getAlternateAlleles() imposes no order on the list returned
+ * so we proceed defensively to get them in strict order
+ */
+ int altAlleleCount = variant.getAlternateAlleles().size();
+ for (int i = 0; i < altAlleleCount; i++)
+ {
+ added += addAlleleFeature(seq, variant, i, featureStart, featureEnd,
+ forwardStrand);
+ }
+ return added;
+ }
+
+ /**
+ * Inspects one allele and attempts to add a variant feature for it to the
+ * sequence. We extract as much as possible of the additional data associated
+ * with this allele to store in the feature's key-value map. Answers the
+ * number of features added (0 or 1).
+ *
+ * @param seq
+ * @param variant
+ * @param altAlleleIndex
+ * (0, 1..)
+ * @param featureStart
+ * @param featureEnd
+ * @param forwardStrand
+ * @return
+ */
+ protected int addAlleleFeature(SequenceI seq, VariantContext variant,
+ int altAlleleIndex, int featureStart, int featureEnd,
+ boolean forwardStrand)
+ {
+ String reference = variant.getReference().getBaseString();
+ Allele alt = variant.getAlternateAllele(altAlleleIndex);
+ String allele = alt.getBaseString();
+
+ /*
+ * build the ref,alt allele description e.g. "G,A", using the base
+ * complement if the sequence is on the reverse strand
+ */
+ // TODO check how structural variants are shown on reverse strand
+ StringBuilder sb = new StringBuilder();
+ sb.append(forwardStrand ? reference : Dna.reverseComplement(reference));
+ sb.append(COMMA);
+ sb.append(forwardStrand ? allele : Dna.reverseComplement(allele));
+ String alleles = sb.toString(); // e.g. G,A
+
+ String type = SequenceOntologyI.SEQUENCE_VARIANT;
+ float score = getAlleleFrequency(variant, altAlleleIndex);
+
+ SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
+ featureEnd, score, FEATURE_GROUP_VCF);
+ sf.setSource(sourceId);
+
+ sf.setValue(Gff3Helper.ALLELES, alleles);
+
+ addAlleleProperties(variant, seq, sf, altAlleleIndex);
+
+ seq.addSequenceFeature(sf);
+
+ return 1;
+ }
+
+ /**
+ * Add any allele-specific VCF key-value data to the sequence feature
+ *
+ * @param variant
+ * @param seq
+ * @param sf
+ * @param altAlelleIndex
+ * (0, 1..)
+ */
+ protected void addAlleleProperties(VariantContext variant, SequenceI seq,
+ SequenceFeature sf, final int altAlelleIndex)
+ {
+ Map<String, Object> atts = variant.getAttributes();
+
+ for (Entry<String, Object> att : atts.entrySet())
+ {
+ String key = att.getKey();
+
+ /*
+ * extract Consequence data (if present) that we are able to
+ * associated with the allele for this variant feature
+ */
+ if (CSQ_FIELD.equals(key))
+ {
+ addConsequences(variant, seq, sf, altAlelleIndex);
+ continue;
+ }
+
+ /*
+ * filter out fields we don't want to capture
+ */
+ if (!vcfFieldsOfInterest.contains(key))
+ {
+ continue;
+ }
+
+ /*
+ * we extract values for other data which are allele-specific;
+ * these may be per alternate allele (INFO[key].Number = 'A')
+ * or per allele including reference (INFO[key].Number = 'R')
+ */
+ VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(key);
+ if (infoHeader == null)
+ {
+ /*
+ * can't be sure what data belongs to this allele, so
+ * play safe and don't take any
+ */
+ continue;
+ }
+
+ VCFHeaderLineCount number = infoHeader.getCountType();
+ int index = altAlelleIndex;
+ if (number == VCFHeaderLineCount.R)
+ {
+ /*
+ * one value per allele including reference, so bump index
+ * e.g. the 3rd value is for the 2nd alternate allele
+ */
+ index++;
+ }
+ else if (number != VCFHeaderLineCount.A)
+ {
+ /*
+ * don't save other values as not allele-related
+ */
+ continue;
+ }
+
+ /*
+ * take the index'th value
+ */
+ String value = getAttributeValue(variant, key, index);
+ if (value != null)
+ {
+ sf.setValue(key, value);
+ }
+ }
+ }
+
+ /**
+ * Inspects CSQ data blocks (consequences) and adds attributes on the sequence
+ * feature for the current allele (and transcript if applicable)
+ * <p>
+ * Allele matching: if field ALLELE_NUM is present, it must match
+ * altAlleleIndex. If not present, then field Allele value must match the VCF
+ * Allele.
+ * <p>
+ * Transcript matching: if sequence name can be identified to at least one of
+ * the consequences' Feature values, then select only consequences that match
+ * the value (i.e. consequences for the current transcript sequence). If not,
+ * take all consequences (this is the case when adding features to the gene
+ * sequence).
+ *
+ * @param variant
+ * @param seq
+ * @param sf
+ * @param altAlelleIndex
+ * (0, 1..)
+ */
+ protected void addConsequences(VariantContext variant, SequenceI seq,
+ SequenceFeature sf, int altAlelleIndex)
+ {
+ Object value = variant.getAttribute(CSQ_FIELD);
+
+ if (value == null || !(value instanceof ArrayList<?>))
+ {
+ return;
+ }
+
+ List<String> consequences = (List<String>) value;
+
+ /*
+ * if CSQ data includes 'Feature', and any value matches the sequence name,
+ * then restrict consequence data to only the matching value (transcript)
+ * i.e. just pick out consequences for the transcript the variant feature is on
+ */
+ String seqName = seq.getName()== null ? "" : seq.getName().toLowerCase();
+ String matchFeature = null;
+ if (csqFeatureFieldIndex > -1)
+ {
+ for (String consequence : consequences)
+ {
+ String[] csqFields = consequence.split(PIPE_REGEX);
+ if (csqFields.length > csqFeatureFieldIndex)
+ {
+ String featureIdentifier = csqFields[csqFeatureFieldIndex];
+ if (featureIdentifier.length() > 4
+ && seqName.indexOf(featureIdentifier.toLowerCase()) > -1)
+ {
+ matchFeature = featureIdentifier;
+ }
+ }
+ }
+ }
+
+ /*
+ * inspect CSQ consequences; where possible restrict to the consequence
+ * associated with the current transcript (Feature)
+ */
+ SortedMap<String, String> csqValues = new TreeMap<>(
+ String.CASE_INSENSITIVE_ORDER);
+
+ for (String consequence : consequences)
+ {
+ String[] csqFields = consequence.split(PIPE_REGEX);
+
+ if (includeConsequence(csqFields, matchFeature, variant,
+ altAlelleIndex))
+ {
+ /*
+ * inspect individual fields of this consequence, copying non-null
+ * values which are 'fields of interest'
+ */
+ int i = 0;
+ for (String field : csqFields)
+ {
+ if (field != null && field.length() > 0)
+ {
+ String id = vepFieldsOfInterest.get(i);
+ if (id != null)
+ {
+ csqValues.put(id, field);
+ }
+ }
+ i++;
+ }
+ }
+ }
+
+ if (!csqValues.isEmpty())
+ {
+ sf.setValue(CSQ_FIELD, csqValues);
+ }
+ }
+
+ /**
+ * Answers true if we want to associate this block of consequence data with
+ * the specified alternate allele of the VCF variant.
+ * <p>
+ * If consequence data includes the ALLELE_NUM field, then this has to match
+ * altAlleleIndex. Otherwise the Allele field of the consequence data has to
+ * match the allele value.
+ * <p>
+ * Optionally (if matchFeature is not null), restrict to only include
+ * consequences whose Feature value matches. This allows us to attach
+ * consequences to their respective transcripts.
+ *
+ * @param csqFields
+ * @param matchFeature
+ * @param variant
+ * @param altAlelleIndex
+ * (0, 1..)
+ * @return
+ */
+ protected boolean includeConsequence(String[] csqFields,
+ String matchFeature, VariantContext variant, int altAlelleIndex)
+ {
+ /*
+ * check consequence is for the current transcript
+ */
+ if (matchFeature != null)
+ {
+ if (csqFields.length <= csqFeatureFieldIndex)
+ {
+ return false;
+ }
+ String featureIdentifier = csqFields[csqFeatureFieldIndex];
+ if (!featureIdentifier.equals(matchFeature))
+ {
+ return false; // consequence is for a different transcript
+ }
+ }
+
+ /*
+ * if ALLELE_NUM is present, it must match altAlleleIndex
+ * NB first alternate allele is 1 for ALLELE_NUM, 0 for altAlleleIndex
+ */
+ if (csqAlleleNumberFieldIndex > -1)
+ {
+ if (csqFields.length <= csqAlleleNumberFieldIndex)
+ {
+ return false;
+ }
+ String alleleNum = csqFields[csqAlleleNumberFieldIndex];
+ return String.valueOf(altAlelleIndex + 1).equals(alleleNum);
+ }
+
+ /*
+ * else consequence allele must match variant allele
+ */
+ if (csqAlleleFieldIndex > -1 && csqFields.length > csqAlleleFieldIndex)
+ {
+ String csqAllele = csqFields[csqAlleleFieldIndex];
+ String vcfAllele = variant.getAlternateAllele(altAlelleIndex)
+ .getBaseString();
+ return csqAllele.equals(vcfAllele);
+ }
+
+ return false;
+ }
+
+ /**
+ * A convenience method to complement a dna base and return the string value
+ * of its complement
+ *
+ * @param reference
+ * @return
+ */
+ protected String complement(byte[] reference)
+ {
+ return String.valueOf(Dna.getComplement((char) reference[0]));
+ }
+
+ /**
+ * Determines the location of the query range (chromosome positions) in a
+ * different reference assembly.
+ * <p>
+ * If the range is just a subregion of one for which we already have a mapping
+ * (for example, an exon sub-region of a gene), then the mapping is just
+ * computed arithmetically.
+ * <p>
+ * Otherwise, calls the Ensembl REST service that maps from one assembly
+ * reference's coordinates to another's
+ *
+ * @param queryRange
+ * start-end chromosomal range in 'fromRef' coordinates
+ * @param chromosome
+ * @param species
+ * @param fromRef
+ * assembly reference for the query coordinates
+ * @param toRef
+ * assembly reference we wish to translate to
+ * @return the start-end range in 'toRef' coordinates
+ */
+ protected int[] mapReferenceRange(int[] queryRange, String chromosome,
+ String species, String fromRef, String toRef)
+ {
+ /*
+ * first try shorcut of computing the mapping as a subregion of one
+ * we already have (e.g. for an exon, if we have the gene mapping)
+ */
+ int[] mappedRange = findSubsumedRangeMapping(queryRange, chromosome,
+ species, fromRef, toRef);
+ if (mappedRange != null)
+ {
+ return mappedRange;
+ }
+
+ /*
+ * call (e.g.) http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37
+ */
+ EnsemblMap mapper = new EnsemblMap();
+ int[] mapping = mapper.getAssemblyMapping(species, chromosome, fromRef,
+ toRef, queryRange);
+
+ if (mapping == null)
+ {
+ // mapping service failure
+ return null;
+ }
+
+ /*
+ * save mapping for possible future re-use
+ */
+ String key = makeRangesKey(chromosome, species, fromRef, toRef);
+ if (!assemblyMappings.containsKey(key))
+ {
+ assemblyMappings.put(key, new HashMap<int[], int[]>());
+ }
+
+ assemblyMappings.get(key).put(queryRange, mapping);
+
+ return mapping;
+ }
+
+ /**
+ * If we already have a 1:1 contiguous mapping which subsumes the given query
+ * range, this method just calculates and returns the subset of that mapping,
+ * else it returns null. In practical terms, if a gene has a contiguous
+ * mapping between (for example) GRCh37 and GRCh38, then we assume that its
+ * subsidiary exons occupy unchanged relative positions, and just compute
+ * these as offsets, rather than do another lookup of the mapping.
+ * <p>
+ * If in future these assumptions prove invalid (e.g. for bacterial dna?!),
+ * simply remove this method or let it always return null.
+ * <p>
+ * Warning: many rapid calls to the /map service map result in a 429 overload
+ * error response
+ *
+ * @param queryRange
+ * @param chromosome
+ * @param species
+ * @param fromRef
+ * @param toRef
+ * @return
+ */
+ protected int[] findSubsumedRangeMapping(int[] queryRange, String chromosome,
+ String species, String fromRef, String toRef)
+ {
+ String key = makeRangesKey(chromosome, species, fromRef, toRef);
+ if (assemblyMappings.containsKey(key))
+ {
+ Map<int[], int[]> mappedRanges = assemblyMappings.get(key);
+ for (Entry<int[], int[]> mappedRange : mappedRanges.entrySet())
+ {
+ int[] fromRange = mappedRange.getKey();
+ int[] toRange = mappedRange.getValue();
+ if (fromRange[1] - fromRange[0] == toRange[1] - toRange[0])
+ {
+ /*
+ * mapping is 1:1 in length, so we trust it to have no discontinuities
+ */
+ if (MappingUtils.rangeContains(fromRange, queryRange))
+ {
+ /*
+ * fromRange subsumes our query range
+ */
+ int offset = queryRange[0] - fromRange[0];
+ int mappedRangeFrom = toRange[0] + offset;
+ int mappedRangeTo = mappedRangeFrom + (queryRange[1] - queryRange[0]);
+ return new int[] { mappedRangeFrom, mappedRangeTo };
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Transfers the sequence feature to the target sequence, locating its start
+ * and end range based on the mapping. Features which do not overlap the
+ * target sequence are ignored.
+ *
+ * @param sf
+ * @param targetSequence
+ * @param mapping
+ * mapping from the feature's coordinates to the target sequence
+ */
+ protected void transferFeature(SequenceFeature sf,
+ SequenceI targetSequence, MapList mapping)
+ {
+ int[] mappedRange = mapping.locateInTo(sf.getBegin(), sf.getEnd());
+
+ if (mappedRange != null)
+ {
+ String group = sf.getFeatureGroup();
+ int newBegin = Math.min(mappedRange[0], mappedRange[1]);
+ int newEnd = Math.max(mappedRange[0], mappedRange[1]);
+ SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+ group, sf.getScore());
+ targetSequence.addSequenceFeature(copy);
+ }
+ }
+
+ /**
+ * Formats a ranges map lookup key
+ *
+ * @param chromosome
+ * @param species
+ * @param fromRef
+ * @param toRef
+ * @return
+ */
+ protected static String makeRangesKey(String chromosome, String species,
+ String fromRef, String toRef)
+ {
+ return species + EXCL + chromosome + EXCL + fromRef + EXCL
+ + toRef;
+ }
+}
protected JMenuItem runGroovy = new JMenuItem();
+ protected JMenuItem loadVcf;
+
protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem();
associatedData_actionPerformed(e);
}
});
+ loadVcf = new JMenuItem(MessageManager.getString("label.load_vcf_file"));
+ loadVcf.setToolTipText(MessageManager.getString("label.load_vcf"));
+ loadVcf.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ loadVcf_actionPerformed();
+ }
+ });
autoCalculate.setText(
MessageManager.getString("label.autocalculate_consensus"));
autoCalculate.setState(
fileMenu.add(exportAnnotations);
fileMenu.add(loadTreeMenuItem);
fileMenu.add(associatedData);
+ fileMenu.add(loadVcf);
fileMenu.addSeparator();
fileMenu.add(closeMenuItem);
// selectMenu.add(listenToViewSelections);
}
+ protected void loadVcf_actionPerformed()
+ {
+ }
+
/**
* Constructs the entries on the Colour menu (but does not add them to the
* menu).
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
+import javax.swing.text.EditorKit;
+import javax.swing.text.html.HTMLEditorKit;
/**
* DOCUMENT ME!
{
try
{
+ textarea.setEditorKit(new HTMLEditorKit());
setJMenuBar(editMenubar);
jbInit();
} catch (Exception e)
{
}
+
+ /**
+ * Adds the given stylesheet rule to the Html editor. However note that CSS
+ * support is limited.
+ *
+ * @param rule
+ * @see javax.swing.text.html.CSS
+ */
+ public void addStylesheetRule(String rule)
+ {
+ EditorKit editorKit = textarea.getEditorKit();
+ if (editorKit != null)
+ {
+ ((HTMLEditorKit) editorKit).getStyleSheet().addRule(rule);
+ }
+ }
}
List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
visiblePositions.getBegin(), visiblePositions.getEnd(), type);
- filterFeaturesForDisplay(overlaps, fc);
+ if (fc.isSimpleColour())
+ {
+ filterFeaturesForDisplay(overlaps);
+ }
for (SequenceFeature sf : overlaps)
{
- Color featureColour = fc.getColor(sf);
+ Color featureColour = getColor(sf, fc);
if (featureColour == null)
{
- // score feature outwith threshold for colouring
+ /*
+ * feature excluded by visibility settings, filters, or colour threshold
+ */
continue;
}
import java.util.StringTokenizer;
/**
- * A class that wraps either a simple colour or a graduated colour
+ * A class that represents a colour scheme for a feature type. Options supported
+ * are currently
+ * <ul>
+ * <li>a simple colour e.g. Red</li>
+ * <li>colour by label - a colour is generated from the feature description</li>
+ * <li>graduated colour by feature score</li>
+ * <ul>
+ * <li>minimum and maximum score range must be provided</li>
+ * <li>minimum and maximum value colours should be specified</li>
+ * <li>a colour for 'no value' may optionally be provided</li>
+ * <li>colours for intermediate scores are interpolated RGB values</li>
+ * <li>there is an optional threshold above/below which to colour values</li>
+ * <li>the range may be the full value range, or may be limited by the threshold
+ * value</li>
+ * </ul>
+ * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
+ * (numeric) value of a named attribute</li> </ul>
*/
public class FeatureColour implements FeatureColourI
{
+ static final Color DEFAULT_NO_COLOUR = null;
+
private static final String BAR = "|";
final private Color colour;
final private Color maxColour;
+ /*
+ * colour to use for colour by attribute when the
+ * attribute value is absent
+ */
+ final private Color noColour;
+
+ /*
+ * if true, then colour has a gradient based on a numerical
+ * range (either feature score, or an attribute value)
+ */
private boolean graduatedColour;
+ /*
+ * if true, colour values are generated from a text string,
+ * either feature description, or an attribute value
+ */
private boolean colourByLabel;
+ /*
+ * if not null, the value of [attribute, [sub-attribute] ...]
+ * is used for colourByLabel or graduatedColour
+ */
+ private String[] attributeName;
+
private float threshold;
private float base;
private boolean aboveThreshold;
- private boolean thresholdIsMinOrMax;
-
private boolean isHighToLow;
private boolean autoScaled;
{
minColour = Color.WHITE;
maxColour = Color.BLACK;
+ noColour = DEFAULT_NO_COLOUR;
minRed = 0f;
minGreen = 0f;
minBlue = 0f;
}
/**
- * Constructor given a colour range and a score range
+ * Constructor given a colour range and a score range, defaulting 'no value
+ * colour' to be the same as minimum colour
*
* @param low
* @param high
*/
public FeatureColour(Color low, Color high, float min, float max)
{
- if (low == null)
- {
- low = Color.white;
- }
- if (high == null)
- {
- high = Color.black;
- }
- graduatedColour = true;
- colour = null;
- minColour = low;
- maxColour = high;
- threshold = Float.NaN;
- isHighToLow = min >= max;
- minRed = low.getRed() / 255f;
- minGreen = low.getGreen() / 255f;
- minBlue = low.getBlue() / 255f;
- deltaRed = (high.getRed() / 255f) - minRed;
- deltaGreen = (high.getGreen() / 255f) - minGreen;
- deltaBlue = (high.getBlue() / 255f) - minBlue;
- if (isHighToLow)
- {
- base = max;
- range = min - max;
- }
- else
- {
- base = min;
- range = max - min;
- }
+ this(low, high, low, min, max);
}
/**
colour = fc.colour;
minColour = fc.minColour;
maxColour = fc.maxColour;
+ noColour = fc.noColour;
minRed = fc.minRed;
minGreen = fc.minGreen;
minBlue = fc.minBlue;
base = fc.base;
range = fc.range;
isHighToLow = fc.isHighToLow;
+ attributeName = fc.attributeName;
setAboveThreshold(fc.isAboveThreshold());
setBelowThreshold(fc.isBelowThreshold());
setThreshold(fc.getThreshold());
public FeatureColour(FeatureColour fc, float min, float max)
{
this(fc);
- graduatedColour = true;
updateBounds(min, max);
}
+ /**
+ * Constructor for a graduated colour
+ *
+ * @param low
+ * @param high
+ * @param noValueColour
+ * @param min
+ * @param max
+ */
+ public FeatureColour(Color low, Color high, Color noValueColour,
+ float min, float max)
+ {
+ if (low == null)
+ {
+ low = Color.white;
+ }
+ if (high == null)
+ {
+ high = Color.black;
+ }
+ graduatedColour = true;
+ colour = null;
+ minColour = low;
+ maxColour = high;
+ noColour = noValueColour;
+ threshold = Float.NaN;
+ isHighToLow = min >= max;
+ minRed = low.getRed() / 255f;
+ minGreen = low.getGreen() / 255f;
+ minBlue = low.getBlue() / 255f;
+ deltaRed = (high.getRed() / 255f) - minRed;
+ deltaGreen = (high.getGreen() / 255f) - minGreen;
+ deltaBlue = (high.getBlue() / 255f) - minBlue;
+ if (isHighToLow)
+ {
+ base = max;
+ range = min - max;
+ }
+ else
+ {
+ base = min;
+ range = max - min;
+ }
+ }
+
@Override
public boolean isGraduatedColour()
{
}
@Override
+ public Color getNoColour()
+ {
+ return noColour;
+ }
+
+ @Override
public boolean isColourByLabel()
{
return colourByLabel;
}
@Override
- public boolean isThresholdMinMax()
- {
- return thresholdIsMinOrMax;
- }
-
- @Override
- public void setThresholdMinMax(boolean b)
- {
- thresholdIsMinOrMax = b;
- }
-
- @Override
public float getThreshold()
{
return threshold;
}
/**
- * Updates the base and range appropriately for the given minmax range
- *
- * @param min
- * @param max
+ * {@inheritDoc}
*/
@Override
public void updateBounds(float min, float max)
{
if (isColourByLabel())
{
- return ColorUtils.createColourFromName(feature.getDescription());
+ String label = attributeName == null ? feature.getDescription()
+ : feature.getValueAsString(attributeName);
+ return label == null ? noColour : ColorUtils
+ .createColourFromName(label);
}
if (!isGraduatedColour())
/*
* graduated colour case, optionally with threshold
- * Float.NaN is assigned minimum visible score colour
+ * may be based on feature score on an attribute value
+ * Float.NaN, or no value, is assigned the 'no value' colour
*/
float scr = feature.getScore();
+ if (attributeName != null)
+ {
+ try
+ {
+ String attVal = feature.getValueAsString(attributeName);
+ scr = Float.valueOf(attVal);
+ } catch (Throwable e)
+ {
+ scr = Float.NaN;
+ }
+ }
if (Float.isNaN(scr))
{
- return getMinColour();
+ return noColour;
}
+
if (isAboveThreshold() && scr <= threshold)
{
return null;
}
+
if (isBelowThreshold() && scr >= threshold)
{
return null;
return String.format("%s\t%s", featureType, colourString);
}
+ @Override
+ public boolean isColourByAttribute()
+ {
+ return attributeName != null;
+ }
+
+ @Override
+ public String[] getAttributeName()
+ {
+ return attributeName;
+ }
+
+ @Override
+ public void setAttributeName(String... name)
+ {
+ attributeName = name;
+ }
+
}
package jalview.util;
import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Random;
public class ColorUtils
{
+ private static final int MAX_CACHE_SIZE = 1729;
+ /*
+ * a cache for colours generated from text strings
+ */
+ static Map<String, Color> myColours = new HashMap<>();
/**
* Generates a random color, will mix with input color. Code taken from
{
return Color.white;
}
+ if (myColours.containsKey(name))
+ {
+ return myColours.get(name);
+ }
int lsize = name.length();
int start = 0;
int end = lsize / 3;
Color color = new Color(r, g, b);
+ if (myColours.size() < MAX_CACHE_SIZE)
+ {
+ myColours.put(name, color);
+ }
+
return color;
}
*/
public MapList()
{
- fromShifts = new ArrayList<int[]>();
- toShifts = new ArrayList<int[]>();
+ fromShifts = new ArrayList<>();
+ toShifts = new ArrayList<>();
}
/**
}
boolean changed = false;
- List<int[]> merged = new ArrayList<int[]>();
+ List<int[]> merged = new ArrayList<>();
int[] lastRange = ranges.get(0);
int lastDirection = lastRange[1] >= lastRange[0] ? 1 : -1;
lastRange = new int[] { lastRange[0], lastRange[1] };
{
return null;
}
- List<int[]> ranges = new ArrayList<int[]>();
+ List<int[]> ranges = new ArrayList<>();
if (fs <= fe)
{
intv = fs;
*/
public boolean isFromForwardStrand()
{
+ return isForwardStrand(getFromRanges());
+ }
+
+ /**
+ * Returns true if mapping is to forward strand, false if to reverse strand.
+ * Result is just based on the first 'to' range that is not a single position.
+ * Default is true unless proven to be false. Behaviour is not well defined if
+ * the mapping has a mixture of forward and reverse ranges.
+ *
+ * @return
+ */
+ public boolean isToForwardStrand()
+ {
+ return isForwardStrand(getToRanges());
+ }
+
+ /**
+ * A helper method that returns true unless at least one range has start > end.
+ * Behaviour is undefined for a mixture of forward and reverse ranges.
+ *
+ * @param ranges
+ * @return
+ */
+ private boolean isForwardStrand(List<int[]> ranges)
+ {
boolean forwardStrand = true;
- for (int[] range : getFromRanges())
+ for (int[] range : ranges)
{
if (range[1] > range[0])
{
|| (fromRatio == 3 && toRatio == 1);
}
+ /**
+ * Returns a map which is the composite of this one and the input map. That
+ * is, the output map has the fromRanges of this map, and its toRanges are the
+ * toRanges of this map as transformed by the input map.
+ * <p>
+ * Returns null if the mappings cannot be traversed (not all toRanges of this
+ * map correspond to fromRanges of the input), or if this.toRatio does not
+ * match map.fromRatio.
+ *
+ * <pre>
+ * Example 1:
+ * this: from [1-100] to [501-600]
+ * input: from [10-40] to [60-90]
+ * output: from [10-40] to [560-590]
+ * Example 2 ('reverse strand exons'):
+ * this: from [1-100] to [2000-1951], [1000-951] // transcript to loci
+ * input: from [1-50] to [41-90] // CDS to transcript
+ * output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
+ * </pre>
+ *
+ * @param map
+ * @return
+ */
+ public MapList traverse(MapList map)
+ {
+ if (map == null)
+ {
+ return null;
+ }
+
+ /*
+ * compound the ratios by this rule:
+ * A:B with M:N gives A*M:B*N
+ * reduced by greatest common divisor
+ * so 1:3 with 3:3 is 3:9 or 1:3
+ * 1:3 with 3:1 is 3:3 or 1:1
+ * 1:3 with 1:3 is 1:9
+ * 2:5 with 3:7 is 6:35
+ */
+ int outFromRatio = getFromRatio() * map.getFromRatio();
+ int outToRatio = getToRatio() * map.getToRatio();
+ int gcd = MathUtils.gcd(outFromRatio, outToRatio);
+ outFromRatio /= gcd;
+ outToRatio /= gcd;
+
+ List<int[]> toRanges = new ArrayList<>();
+ for (int[] range : getToRanges())
+ {
+ int[] transferred = map.locateInTo(range[0], range[1]);
+ if (transferred == null)
+ {
+ return null;
+ }
+ toRanges.add(transferred);
+ }
+
+ return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
+ }
+
}
}
/**
+ * 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.
--- /dev/null
+package jalview.util;
+
+public class MathUtils
+{
+
+ /**
+ * Returns the greatest common divisor of two integers
+ *
+ * @param a
+ * @param b
+ * @return
+ */
+ public static int gcd(int a, int b)
+ {
+ if (b == 0)
+ {
+ return Math.abs(a);
+ }
+ return gcd(b, a % b);
+ }
+
+}
}
return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
}
+
+ /**
+ * A helper method that strips off any leading or trailing html and body tags.
+ * If no html tag is found, then also html-encodes angle bracket characters.
+ *
+ * @param text
+ * @return
+ */
+ public static String stripHtmlTags(String text)
+ {
+ if (text == null)
+ {
+ return null;
+ }
+ String tmp2up = text.toUpperCase();
+ int startTag = tmp2up.indexOf("<HTML>");
+ if (startTag > -1)
+ {
+ text = text.substring(startTag + 6);
+ tmp2up = tmp2up.substring(startTag + 6);
+ }
+ // is omission of "<BODY>" intentional here??
+ int endTag = tmp2up.indexOf("</BODY>");
+ if (endTag > -1)
+ {
+ text = text.substring(0, endTag);
+ tmp2up = tmp2up.substring(0, endTag);
+ }
+ endTag = tmp2up.indexOf("</HTML>");
+ if (endTag > -1)
+ {
+ text = text.substring(0, endTag);
+ }
+
+ if (startTag == -1 && (text.contains("<") || text.contains(">")))
+ {
+ text = text.replaceAll("<", "<");
+ text = text.replaceAll(">", ">");
+ }
+ return text;
+ }
}
--- /dev/null
+package jalview.util.matcher;
+
+import jalview.util.MessageManager;
+
+/**
+ * An enumeration for binary conditions that a user might choose from when
+ * setting filter or match conditions for values
+ */
+public enum Condition
+{
+ Contains(false, true), NotContains(false, true), Matches(false, true),
+ NotMatches(false, true), Present(false, false), NotPresent(false, false),
+ EQ(true, true), NE(true, true), LT(true, true), LE(true, true),
+ GT(true, true), GE(true, true);
+
+ private boolean numeric;
+
+ private boolean needsAPattern;
+
+ Condition(boolean isNumeric, boolean needsPattern)
+ {
+ numeric = isNumeric;
+ needsAPattern = needsPattern;
+ }
+
+ /**
+ * Answers true if the condition does a numerical comparison, else false
+ * (string comparison)
+ *
+ * @return
+ */
+ public boolean isNumeric()
+ {
+ return numeric;
+ }
+
+ /**
+ * Answers true if the condition requires a pattern to compare against, else
+ * false
+ *
+ * @return
+ */
+ public boolean needsAPattern()
+ {
+ return needsAPattern;
+ }
+
+ /**
+ * Answers a display name for the match condition, suitable for showing in
+ * drop-down menus. The value may be internationalized using the resource key
+ * "label.matchCondition_" with the enum name appended.
+ *
+ * @return
+ */
+ @Override
+ public String toString()
+ {
+ return MessageManager.getStringOrReturn("label.matchCondition_",
+ name());
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * A bean to describe one attribute-based filter
+ */
+public class Matcher implements MatcherI
+{
+ /*
+ * the comparison condition
+ */
+ Condition condition;
+
+ /*
+ * the string pattern as entered, or the regex, to compare to
+ * also holds the string form of float value if a numeric condition
+ */
+ String pattern;
+
+ /*
+ * the pattern in upper case, for non-case-sensitive matching
+ */
+ String uppercasePattern;
+
+ /*
+ * the compiled regex if using a pattern match condition
+ * (reserved for possible future enhancement)
+ */
+ Pattern regexPattern;
+
+ /*
+ * the value to compare to for a numerical condition
+ */
+ float value;
+
+ /**
+ * Constructor
+ *
+ * @param cond
+ * @param compareTo
+ * @return
+ * @throws NumberFormatException
+ * if a numerical condition is specified with a non-numeric comparison
+ * value
+ * @throws NullPointerException
+ * if a null condition or comparison string is specified
+ */
+ public Matcher(Condition cond, String compareTo)
+ {
+ Objects.requireNonNull(cond);
+ condition = cond;
+ if (cond.isNumeric())
+ {
+ value = Float.valueOf(compareTo);
+ pattern = String.valueOf(value);
+ uppercasePattern = pattern;
+ }
+ else
+ {
+ pattern = compareTo;
+ if (pattern != null)
+ {
+ uppercasePattern = pattern.toUpperCase();
+ }
+ }
+
+ // if we add regex conditions (e.g. matchesPattern), then
+ // pattern should hold the raw regex, and
+ // regexPattern = Pattern.compile(compareTo);
+ }
+
+ /**
+ * Constructor for a numerical match condition. Note that if a string
+ * comparison condition is specified, this will be converted to a comparison
+ * with the float value as string
+ *
+ * @param cond
+ * @param compareTo
+ */
+ public Matcher(Condition cond, float compareTo)
+ {
+ this(cond, String.valueOf(compareTo));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("incomplete-switch")
+ @Override
+ public boolean matches(String val)
+ {
+ if (condition.isNumeric())
+ {
+ try
+ {
+ /*
+ * treat a null value (no such attribute) as
+ * failing any numerical filter condition
+ */
+ return val == null ? false : matches(Float.valueOf(val));
+ } catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+
+ /*
+ * a null value matches a negative condition, fails a positive test
+ */
+ if (val == null)
+ {
+ return condition == Condition.NotContains
+ || condition == Condition.NotMatches
+ || condition == Condition.NotPresent;
+ }
+
+ String upper = val.toUpperCase().trim();
+ boolean matched = false;
+ switch(condition) {
+ case Matches:
+ matched = upper.equals(uppercasePattern);
+ break;
+ case NotMatches:
+ matched = !upper.equals(uppercasePattern);
+ break;
+ case Contains:
+ matched = upper.indexOf(uppercasePattern) > -1;
+ break;
+ case NotContains:
+ matched = upper.indexOf(uppercasePattern) == -1;
+ break;
+ case Present:
+ matched = true;
+ break;
+ default:
+ break;
+ }
+ return matched;
+ }
+
+ /**
+ * Applies a numerical comparison match condition
+ *
+ * @param f
+ * @return
+ */
+ @SuppressWarnings("incomplete-switch")
+ boolean matches(float f)
+ {
+ if (!condition.isNumeric())
+ {
+ return matches(String.valueOf(f));
+ }
+
+ boolean matched = false;
+ switch (condition) {
+ case LT:
+ matched = f < value;
+ break;
+ case LE:
+ matched = f <= value;
+ break;
+ case EQ:
+ matched = f == value;
+ break;
+ case NE:
+ matched = f != value;
+ break;
+ case GT:
+ matched = f > value;
+ break;
+ case GE:
+ matched = f >= value;
+ break;
+ default:
+ break;
+ }
+
+ return matched;
+ }
+
+ /**
+ * A simple hash function that guarantees that when two objects are equal,
+ * they have the same hashcode
+ */
+ @Override
+ public int hashCode()
+ {
+ return pattern.hashCode() + condition.hashCode() + (int) value;
+ }
+
+ /**
+ * equals is overridden so that we can safely remove Matcher objects from
+ * collections (e.g. delete an attribute match condition for a feature colour)
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj == null || !(obj instanceof Matcher))
+ {
+ return false;
+ }
+ Matcher m = (Matcher) obj;
+ if (condition != m.condition || value != m.value)
+ {
+ return false;
+ }
+ if (pattern == null)
+ {
+ return m.pattern == null;
+ }
+ return uppercasePattern.equals(m.uppercasePattern);
+ }
+
+ @Override
+ public Condition getCondition()
+ {
+ return condition;
+ }
+
+ @Override
+ public String getPattern()
+ {
+ return pattern;
+ }
+
+ @Override
+ public float getFloatValue()
+ {
+ return value;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(condition.toString()).append(" ");
+ if (condition.isNumeric())
+ {
+ sb.append(pattern);
+ }
+ else
+ {
+ sb.append("'").append(pattern).append("'");
+ }
+
+ return sb.toString();
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+public interface MatcherI
+{
+ /**
+ * Answers true if the given value is matched, else false
+ *
+ * @param s
+ * @return
+ */
+ boolean matches(String s);
+
+ Condition getCondition();
+
+ String getPattern();
+
+ float getFloatValue();
+}
import jalview.datamodel.AlignmentI;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
import jalview.datamodel.features.SequenceFeatures;
import jalview.renderer.seqfeatures.FeatureRenderer;
import jalview.schemes.FeatureColour;
public abstract class FeatureRendererModel
implements jalview.api.FeatureRenderer
{
+ /*
+ * a data bean to hold one row of feature settings from the gui
+ */
+ public static class FeatureSettingsBean
+ {
+ public final String featureType;
- /**
+ public final FeatureColourI featureColour;
+
+ public final FeatureMatcherSetI filter;
+
+ public final Boolean show;
+
+ public FeatureSettingsBean(String type, FeatureColourI colour,
+ FeatureMatcherSetI theFilter, Boolean isShown)
+ {
+ featureType = type;
+ featureColour = colour;
+ filter = theFilter;
+ show = isShown;
+ }
+ }
+
+ /*
* global transparency for feature
*/
protected float transparency = 1.0f;
- protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+ /*
+ * colour scheme for each feature type
+ */
+ protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<>();
- protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<String, Boolean>();
+ /*
+ * visibility flag for each feature group
+ */
+ protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
+
+ /*
+ * filters for each feature type
+ */
+ protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
protected String[] renderOrder;
this.renderOrder = frs.renderOrder;
this.featureGroups = frs.featureGroups;
this.featureColours = frs.featureColours;
+ this.featureFilters = frs.featureFilters;
this.transparency = frs.transparency;
this.featureOrder = frs.featureOrder;
if (av != null && av != fr.getViewport())
{
av.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
}
- List<String> nft = new ArrayList<String>();
+ List<String> nft = new ArrayList<>();
for (String featureType : featureTypes)
{
if (!fdi.isRegistered(featureType))
renderOrder = neworder;
}
- protected Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
+ protected Map<String, float[][]> minmax = new Hashtable<>();
public Map<String, float[][]> getMinMax()
{
* include features at the position provided their feature type is
* displayed, and feature group is null or marked for display
*/
- List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+ List<SequenceFeature> result = new ArrayList<>();
if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
{
return result;
List<SequenceFeature> features = sequence.findFeatures(column, column,
visibleTypes);
+ /*
+ * include features unless their feature group is not displayed, or
+ * they are hidden (have no colour) based on a filter or colour threshold
+ */
for (SequenceFeature sf : features)
{
- if (!featureGroupNotShown(sf))
+ if (!featureGroupNotShown(sf) && getColour(sf) != null)
{
result.add(sf);
}
}
FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
- Set<String> oldfeatures = new HashSet<String>();
+ Set<String> oldfeatures = new HashSet<>();
if (renderOrder != null)
{
for (int i = 0; i < renderOrder.length; i++)
}
AlignmentI alignment = av.getAlignment();
- List<String> allfeatures = new ArrayList<String>();
+ List<String> allfeatures = new ArrayList<>();
for (int i = 0; i < alignment.getHeight(); i++)
{
*/
if (minmax == null)
{
- minmax = new Hashtable<String, float[][]>();
+ minmax = new Hashtable<>();
}
synchronized (minmax)
{
*/
private void updateRenderOrder(List<String> allFeatures)
{
- List<String> allfeatures = new ArrayList<String>(allFeatures);
+ List<String> allfeatures = new ArrayList<>(allFeatures);
String[] oldRender = renderOrder;
renderOrder = new String[allfeatures.size()];
boolean initOrders = (featureOrder == null);
if (mmrange != null)
{
FeatureColourI fc = featureColours.get(oldRender[j]);
- if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+ if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+ && !fc.isColourByAttribute())
{
fc.updateBounds(mmrange[0][0], mmrange[0][1]);
}
if (mmrange != null)
{
FeatureColourI fc = featureColours.get(newf[i]);
- if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+ if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+ && !fc.isColourByAttribute())
{
fc.updateBounds(mmrange[0][0], mmrange[0][1]);
}
return fc;
}
- /**
- * Returns the configured colour for a particular feature instance. This
- * includes calculation of 'colour by label', or of a graduated score colour,
- * if applicable. It does not take into account feature visibility or colour
- * transparency. Returns null for a score feature whose score value lies
- * outside any colour threshold.
- *
- * @param feature
- * @return
- */
+ @Override
public Color getColour(SequenceFeature feature)
{
FeatureColourI fc = getFeatureStyle(feature.getType());
- return fc.getColor(feature);
+ return getColor(feature, fc);
}
/**
*/
protected boolean showFeatureOfType(String type)
{
- return type == null ? false : av.getFeaturesDisplayed().isVisible(type);
+ return type == null ? false : (av.getFeaturesDisplayed() == null ? true
+ : av.getFeaturesDisplayed().isVisible(type));
}
@Override
{
if (featureOrder == null)
{
- featureOrder = new Hashtable<String, Float>();
+ featureOrder = new Hashtable<>();
}
featureOrder.put(type, new Float(position));
return position;
* Replace current ordering with new ordering
*
* @param data
- * { String(Type), Colour(Type), Boolean(Displayed) }
+ * an array of { Type, Colour, Filter, Boolean }
* @return true if any visible features have been reordered, else false
*/
- public boolean setFeaturePriority(Object[][] data)
+ public boolean setFeaturePriority(FeatureSettingsBean[] data)
{
return setFeaturePriority(data, true);
}
/**
- * Sets the priority order for features, with the highest priority (displayed
- * on top) at the start of the data array
+ * Sets the priority order for features, with the highest priority (displayed on
+ * top) at the start of the data array
*
* @param data
- * { String(Type), Colour(Type), Boolean(Displayed) }
+ * an array of { Type, Colour, Filter, Boolean }
* @param visibleNew
* when true current featureDisplay list will be cleared
- * @return true if any visible features have been reordered or recoloured,
- * else false (i.e. no need to repaint)
+ * @return true if any visible features have been reordered or recoloured, else
+ * false (i.e. no need to repaint)
*/
- public boolean setFeaturePriority(Object[][] data, boolean visibleNew)
+ public boolean setFeaturePriority(FeatureSettingsBean[] data,
+ boolean visibleNew)
{
/*
* note visible feature ordering and colours before update
*/
List<String> visibleFeatures = getDisplayedFeatureTypes();
- Map<String, FeatureColourI> visibleColours = new HashMap<String, FeatureColourI>(
+ Map<String, FeatureColourI> visibleColours = new HashMap<>(
getFeatureColours());
FeaturesDisplayedI av_featuresdisplayed = null;
{
for (int i = 0; i < data.length; i++)
{
- String type = data[i][0].toString();
- setColour(type, (FeatureColourI) data[i][1]);
- if (((Boolean) data[i][2]).booleanValue())
+ String type = data[i].featureType;
+ setColour(type, data[i].featureColour);
+ if (data[i].show)
{
av_featuresdisplayed.setVisible(type);
}
{
if (featureGroups != null)
{
- List<String> gp = new ArrayList<String>();
+ List<String> gp = new ArrayList<>();
for (String grp : featureGroups.keySet())
{
@Override
public Map<String, FeatureColourI> getDisplayedFeatureCols()
{
- Map<String, FeatureColourI> fcols = new Hashtable<String, FeatureColourI>();
+ Map<String, FeatureColourI> fcols = new Hashtable<>();
if (getViewport().getFeaturesDisplayed() == null)
{
return fcols;
public List<String> getDisplayedFeatureTypes()
{
List<String> typ = getRenderOrder();
- List<String> displayed = new ArrayList<String>();
+ List<String> displayed = new ArrayList<>();
FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed();
if (feature_disp != null)
{
@Override
public List<String> getDisplayedFeatureGroups()
{
- List<String> _gps = new ArrayList<String>();
+ List<String> _gps = new ArrayList<>();
for (String gp : getFeatureGroups())
{
if (checkGroupVisibility(gp, false))
public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
int resNo)
{
- List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+ List<SequenceFeature> result = new ArrayList<>();
if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
{
return result;
for (SequenceFeature sf : features)
{
- if (!featureGroupNotShown(sf))
+ if (!featureGroupNotShown(sf) && getColour(sf) != null)
{
result.add(sf);
}
}
/**
- * Removes from the list of features any that have a feature group that is not
- * displayed, or duplicate the location of a feature of the same type (unless
- * a graduated colour scheme or colour by label is applied). Should be used
- * only for features of the same feature colour (which normally implies the
- * same feature type).
+ * Removes from the list of features any that duplicate the location of a
+ * feature of the same type. Should be used only for features of the same,
+ * simple, feature colour (which normally implies the same feature type). Does
+ * not check visibility settings for feature type or feature group.
*
* @param features
- * @param fc
*/
- public void filterFeaturesForDisplay(List<SequenceFeature> features,
- FeatureColourI fc)
+ public void filterFeaturesForDisplay(List<SequenceFeature> features)
{
if (features.isEmpty())
{
return;
}
SequenceFeatures.sortFeatures(features, true);
- boolean simpleColour = fc == null || fc.isSimpleColour();
SequenceFeature lastFeature = null;
Iterator<SequenceFeature> it = features.iterator();
while (it.hasNext())
{
SequenceFeature sf = it.next();
- if (featureGroupNotShown(sf))
- {
- it.remove();
- continue;
- }
/*
* a feature is redundant for rendering purposes if it has the
* (checking type and isContactFeature as a fail-safe here, although
* currently they are guaranteed to match in this context)
*/
- if (simpleColour)
+ if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+ && sf.getEnd() == lastFeature.getEnd()
+ && sf.isContactFeature() == lastFeature.isContactFeature()
+ && sf.getType().equals(lastFeature.getType()))
{
- if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
- && sf.getEnd() == lastFeature.getEnd()
- && sf.isContactFeature() == lastFeature.isContactFeature()
- && sf.getType().equals(lastFeature.getType()))
- {
- it.remove();
- }
+ it.remove();
}
lastFeature = sf;
}
}
+ @Override
+ public Map<String, FeatureMatcherSetI> getFeatureFilters()
+ {
+ return new HashMap<>(featureFilters);
+ }
+
+ @Override
+ public void setFeatureFilters(Map<String, FeatureMatcherSetI> filters)
+ {
+ featureFilters = filters;
+ }
+
+ @Override
+ public FeatureMatcherSetI getFeatureFilter(String featureType)
+ {
+ return featureFilters.get(featureType);
+ }
+
+ @Override
+ public void setFeatureFilter(String featureType, FeatureMatcherSetI filter)
+ {
+ if (filter == null || filter.isEmpty())
+ {
+ featureFilters.remove(featureType);
+ }
+ else
+ {
+ featureFilters.put(featureType, filter);
+ }
+ }
+
+ /**
+ * Answers the colour for the feature, or null if the feature is excluded by
+ * feature type or group visibility, by filters, or by colour threshold
+ * settings
+ *
+ * @param sf
+ * @param fc
+ * @return
+ */
+ public Color getColor(SequenceFeature sf, FeatureColourI fc)
+ {
+ /*
+ * is the feature type displayed?
+ */
+ if (!showFeatureOfType(sf.getType()))
+ {
+ return null;
+ }
+
+ /*
+ * is the feature group displayed?
+ */
+ if (featureGroupNotShown(sf))
+ {
+ return null;
+ }
+
+ /*
+ * does the feature pass filters?
+ */
+ if (!featureMatchesFilters(sf))
+ {
+ return null;
+ }
+
+ return fc.getColor(sf);
+ }
+
+ /**
+ * Answers true if there no are filters defined for the feature type, or this
+ * feature matches the filters. Answers false if the feature fails to match
+ * filters.
+ *
+ * @param sf
+ * @return
+ */
+ protected boolean featureMatchesFilters(SequenceFeature sf)
+ {
+ FeatureMatcherSetI filter = featureFilters.get(sf.getType());
+ return filter == null ? true : filter.matches(sf);
+ }
+
}
package jalview.viewmodel.seqfeatures;
import jalview.api.FeatureColourI;
+import jalview.datamodel.features.FeatureMatcherSetI;
import jalview.schemes.FeatureColour;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
*/
Map<String, FeatureColourI> featureColours;
+ /*
+ * map of {featureType, filters}
+ */
+ Map<String, FeatureMatcherSetI> featureFilters;
+
float transparency;
Map<String, Float> featureOrder;
renderOrder = null;
featureGroups = new ConcurrentHashMap<String, Boolean>();
featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+ featureFilters = new HashMap<>();
featureOrder = new ConcurrentHashMap<String, Float>();
+
if (fr.renderOrder != null)
{
this.renderOrder = new String[fr.renderOrder.length];
featureColours.put(next, new FeatureColour((FeatureColour) val));
}
}
+
+ if (fr.featureFilters != null)
+ {
+ this.featureFilters.putAll(fr.featureFilters);
+ }
+
this.transparency = fr.transparency;
if (fr.featureOrder != null)
{
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;
public class AlignmentUtilsTests
{
+ private static Sequence ts = new Sequence("short",
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
@BeforeClass(alwaysRun = true)
public void setUpJvOptionPane()
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)
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
*/
assertEquals("[[3, 3], [8, 12]]",
Arrays.deepToString(ml.getFromRanges().toArray()));
}
-
}
import jalview.analysis.Finder;
import jalview.api.AlignViewControllerI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.Alignment;
import jalview.datamodel.SearchResults;
import jalview.datamodel.SearchResultsI;
import jalview.datamodel.Sequence;
import jalview.gui.JvOptionPane;
import jalview.io.DataSourceType;
import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
+import java.awt.Color;
import java.util.Arrays;
import java.util.BitSet;
null));
seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f,
null));
- seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f,
+ seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10,
+ 10f,
null));
seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15,
- 0f, null));
+ 10f, null));
// disulfide bond is a 'contact feature' - only select its 'start' and 'end'
- seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", 8, 12,
- 0f, null));
+ seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc",
+ 8, 12, 0f, null));
/*
* select the first five columns --> Metal in seq1 cols 4-5
sg.addSequence(seq3, false);
sg.addSequence(seq4, false);
+ /*
+ * set features visible on a viewport as only visible features are selected
+ */
+ AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1,
+ seq2, seq3, seq4 }), 100, 100);
+ af.getFeatureRenderer().findAllFeatures(true);
+
+ AlignViewController avc = new AlignViewController(af, af.getViewport(),
+ af.alignPanel);
+
BitSet bs = new BitSet();
- int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg,
- bs);
+ int seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
assertEquals(1, seqCount);
assertEquals(2, bs.cardinality());
assertTrue(bs.get(3)); // base 0
*/
sg.setEndRes(6);
bs.clear();
- seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+ seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
assertEquals(2, seqCount);
assertEquals(4, bs.cardinality());
assertTrue(bs.get(3));
sg.setStartRes(13);
sg.setEndRes(13);
bs.clear();
- seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+ seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
assertEquals(1, seqCount);
assertEquals(1, bs.cardinality());
assertTrue(bs.get(13));
sg.setStartRes(17);
sg.setEndRes(19);
bs.clear();
- seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+ seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
assertEquals(0, seqCount);
assertEquals(0, bs.cardinality());
/*
+ * threshold Metal to hide where score < 5
+ * seq1 feature in columns 4-6 is hidden
+ * seq2 feature in columns 6-7 is shown
+ */
+ FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+ fc.setAboveThreshold(true);
+ fc.setThreshold(5f);
+ af.getFeatureRenderer().setColour("Metal", fc);
+ sg.setStartRes(0);
+ sg.setEndRes(6);
+ bs.clear();
+ seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+ assertEquals(1, seqCount);
+ assertEquals(2, bs.cardinality());
+ assertTrue(bs.get(5));
+ assertTrue(bs.get(6));
+
+ /*
* columns 11-13 should not match disulfide bond at 8/12
*/
sg.setStartRes(10);
sg.setEndRes(12);
bs.clear();
- seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
- sg, bs);
+ seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
assertEquals(0, seqCount);
assertEquals(0, bs.cardinality());
sg.setStartRes(5);
sg.setEndRes(17);
bs.clear();
- seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
- sg, bs);
+ seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
assertEquals(1, seqCount);
assertEquals(2, bs.cardinality());
assertTrue(bs.get(8));
sg.setStartRes(0);
sg.setEndRes(19);
bs.clear();
- seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs);
+ seqCount = avc.findColumnsWithFeature("Pfam", sg, bs);
assertEquals(0, seqCount);
assertEquals(0, bs.cardinality());
}
"group");
assertTrue(sf.isContactFeature());
}
+
+ @Test(groups = { "Functional" })
+ public void testGetDetailsReport()
+ {
+ // single locus, no group, no score
+ SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null);
+ String expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+ + "<tr><td>Start/end</td><td>22</td><td></td></tr>"
+ + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
+ assertEquals(expected, sf.getDetailsReport());
+
+ // contact feature
+ sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
+ null);
+ expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
+ + "<tr><td>Start/end</td><td>28:31</td><td></td></tr>"
+ + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
+ assertEquals(expected, sf.getDetailsReport());
+
+ sf = new SequenceFeature("variant", "G,C", 22, 33,
+ 12.5f, "group");
+ sf.setValue("Parent", "ENSG001");
+ sf.setValue("Child", "ENSP002");
+ expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+ + "<tr><td>Start/end</td><td>22-33</td><td></td></tr>"
+ + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
+ + "<tr><td>Score</td><td>12.5</td><td></td></tr>"
+ + "<tr><td>Group</td><td>group</td><td></td></tr>"
+ + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
+ + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
+ assertEquals(expected, sf.getDetailsReport());
+
+ /*
+ * feature with embedded html link in description
+ */
+ String desc = "<html>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></html>";
+ sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot");
+ expected = "<br><table><tr><td>Type</td><td>Pfam</td><td></td></tr>"
+ + "<tr><td>Start/end</td><td>8-83</td><td></td></tr>"
+ + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
+ + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
+ assertEquals(expected, sf.getDetailsReport());
+ }
}
--- /dev/null
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureAttributes.Datatype;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class FeatureAttributesTest
+{
+
+ /**
+ * clear down attributes map after tests
+ */
+ @AfterMethod
+ public void tearDown()
+ {
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ ((Map<?, ?>) PA.getValue(fa, "attributes")).clear();
+ }
+
+ /**
+ * Test the method that keeps attribute names in non-case-sensitive order,
+ * including handling of 'compound' names
+ */
+ @Test(groups="Functional")
+ public void testAttributeNameComparator()
+ {
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ Comparator<String[]> comp = (Comparator<String[]>) PA.getValue(fa,
+ "comparator");
+
+ assertEquals(
+ comp.compare(new String[] { "CSQ" }, new String[] { "csq" }), 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ", "a" },
+ new String[] { "csq" }) > 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ" }, new String[] { "csq",
+ "b" }) < 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ", "AF" }, new String[] {
+ "csq", "ac" }) > 0);
+
+ assertTrue(comp.compare(new String[] { "CSQ", "ac" }, new String[] {
+ "csq", "AF" }) < 0);
+ }
+
+ @Test
+ public void testGetMinMax()
+ {
+ SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20,
+ "group");
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ assertNull(fa.getMinMax("Pfam", "kd"));
+ sf.setValue("domain", "xyz");
+ assertNull(fa.getMinMax("Pfam", "kd"));
+ sf.setValue("kd", "some text");
+ assertNull(fa.getMinMax("Pfam", "kd"));
+ sf.setValue("kd", "1.3");
+ assertEquals(fa.getMinMax("Pfam", "kd"), new float[] { 1.3f, 1.3f });
+ sf.setValue("kd", "-2.6");
+ assertEquals(fa.getMinMax("Pfam", "kd"), new float[] { -2.6f, 1.3f });
+ Map<String, String> csq = new HashMap<>();
+ csq.put("AF", "-3");
+ sf.setValue("CSQ", csq);
+ assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"),
+ new float[]
+ { -3f, -3f });
+ csq.put("AF", "4");
+ sf.setValue("CSQ", csq);
+ assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"),
+ new float[]
+ { -3f, 4f });
+ }
+
+ /**
+ * Test the method that returns an attribute description, provided it is
+ * recorded and unique
+ */
+ @Test
+ public void testGetDescription()
+ {
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ // with no description returns null
+ assertNull(fa.getDescription("Pfam", "kd"));
+ // with a unique description, returns that value
+ fa.addDescription("Pfam", "desc1", "kd");
+ assertEquals(fa.getDescription("Pfam", "kd"), "desc1");
+ // with ambiguous description, returns null
+ fa.addDescription("Pfam", "desc2", "kd");
+ assertNull(fa.getDescription("Pfam", "kd"));
+ }
+
+ @Test
+ public void testDatatype()
+ {
+ FeatureAttributes fa = FeatureAttributes.getInstance();
+ assertNull(fa.getDatatype("Pfam", "kd"));
+ SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20,
+ "group");
+ sf.setValue("kd", "-1");
+ sf.setValue("domain", "Metal");
+ sf.setValue("phase", "1");
+ sf.setValue("phase", "reverse");
+ assertEquals(fa.getDatatype("Pfam", "kd"), Datatype.Number);
+ assertEquals(fa.getDatatype("Pfam", "domain"), Datatype.Character);
+ assertEquals(fa.getDatatype("Pfam", "phase"), Datatype.Mixed);
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherSetTest
+{
+ @Test(groups = "Functional")
+ public void testMatches_byAttribute()
+ {
+ /*
+ * a numeric matcher - MatcherTest covers more conditions
+ */
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "AF");
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ fms.and(fm);
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "foobar");
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "-2");
+ assertTrue(fms.matches(sf));
+ sf.setValue("AF", "-1");
+ assertTrue(fms.matches(sf));
+ sf.setValue("AF", "-3");
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "");
+ assertFalse(fms.matches(sf));
+
+ /*
+ * a string pattern matcher
+ */
+ fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+ fms = new FeatureMatcherSet();
+ fms.and(fm);
+ assertFalse(fms.matches(sf));
+ sf.setValue("AF", "raining cats and dogs");
+ assertTrue(fms.matches(sf));
+ }
+
+ @Test(groups = "Functional")
+ public void testAnd()
+ {
+ // condition1: AF value contains "dog" (matches)
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+ "dog", "AF");
+ // condition 2: CSQ value does not contain "how" (does not match)
+ FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+ "how", "CSQ");
+
+ SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12,
+ 6.2f, "grp");
+ sf.setValue("AF", "raining cats and dogs");
+ sf.setValue("CSQ", "showers");
+
+ assertTrue(fm1.matches(sf));
+ assertFalse(fm2.matches(sf));
+
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+ fms.and(fm1);
+ assertTrue(fms.matches(sf));
+ fms.and(fm2);
+ assertFalse(fms.matches(sf));
+
+ /*
+ * OR a failed attribute condition with a matched label condition
+ */
+ fms = new FeatureMatcherSet();
+ fms.and(fm2);
+ assertFalse(fms.matches(sf));
+ FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains,
+ "Helix");
+ fms.or(byLabelPass);
+ assertTrue(fms.matches(sf));
+
+ /*
+ * OR a failed attribute condition with a failed score condition
+ */
+ fms = new FeatureMatcherSet();
+ fms.and(fm2);
+ assertFalse(fms.matches(sf));
+ FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT,
+ "5.9");
+ fms.or(byScoreFail);
+ assertFalse(fms.matches(sf));
+
+ /*
+ * OR failed attribute and score conditions with matched label condition
+ */
+ fms = new FeatureMatcherSet();
+ fms.or(fm2).or(byScoreFail);
+ assertFalse(fms.matches(sf));
+ fms.or(byLabelPass);
+ assertTrue(fms.matches(sf));
+ }
+
+ @Test(groups = "Functional")
+ public void testToString()
+ {
+ Locale.setDefault(Locale.ENGLISH);
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+ "AF");
+ assertEquals(fm1.toString(), "AF < 1.2");
+
+ FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+ "path", "CLIN_SIG");
+ assertEquals(fm2.toString(), "CLIN_SIG does not contain 'path'");
+
+ /*
+ * AND them
+ */
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertEquals(fms.toString(), "");
+ fms.and(fm1);
+ assertEquals(fms.toString(), "(AF < 1.2)");
+ fms.and(fm2);
+ assertEquals(fms.toString(),
+ "(AF < 1.2) and (CLIN_SIG does not contain 'path')");
+
+ /*
+ * OR them
+ */
+ fms = new FeatureMatcherSet();
+ assertEquals(fms.toString(), "");
+ fms.or(fm1);
+ assertEquals(fms.toString(), "(AF < 1.2)");
+ fms.or(fm2);
+ assertEquals(fms.toString(),
+ "(AF < 1.2) or (CLIN_SIG does not contain 'path')");
+
+ try
+ {
+ fms.and(fm1);
+ fail("Expected exception");
+ } catch (IllegalStateException e)
+ {
+ // expected
+ }
+ }
+
+ @Test(groups = "Functional")
+ public void testOr()
+ {
+ // condition1: AF value contains "dog" (matches)
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+ "dog", "AF");
+ // condition 2: CSQ value does not contain "how" (does not match)
+ FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+ "how", "CSQ");
+
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+ sf.setValue("AF", "raining cats and dogs");
+ sf.setValue("CSQ", "showers");
+
+ assertTrue(fm1.matches(sf));
+ assertFalse(fm2.matches(sf));
+
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+ fms.or(fm1);
+ assertTrue(fms.matches(sf));
+ fms.or(fm2);
+ assertTrue(fms.matches(sf)); // true or false makes true
+
+ fms = new FeatureMatcherSet();
+ fms.or(fm2);
+ assertFalse(fms.matches(sf));
+ fms.or(fm1);
+ assertTrue(fms.matches(sf)); // false or true makes true
+
+ try
+ {
+ fms.and(fm2);
+ fail("Expected exception");
+ } catch (IllegalStateException e)
+ {
+ // expected
+ }
+ }
+
+ @Test(groups = "Functional")
+ public void testIsEmpty()
+ {
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0",
+ "AF");
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ assertTrue(fms.isEmpty());
+ fms.and(fm);
+ assertFalse(fms.isEmpty());
+ }
+
+ @Test(groups = "Functional")
+ public void testGetMatchers()
+ {
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+
+ /*
+ * empty iterable:
+ */
+ Iterator<FeatureMatcherI> iterator = fms.getMatchers().iterator();
+ assertFalse(iterator.hasNext());
+
+ /*
+ * one matcher:
+ */
+ FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "AF");
+ fms.and(fm1);
+ iterator = fms.getMatchers().iterator();
+ assertSame(fm1, iterator.next());
+ assertFalse(iterator.hasNext());
+
+ /*
+ * two matchers:
+ */
+ FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.LT, "8f",
+ "AF");
+ fms.and(fm2);
+ iterator = fms.getMatchers().iterator();
+ assertSame(fm1, iterator.next());
+ assertSame(fm2, iterator.next());
+ assertFalse(iterator.hasNext());
+ }
+
+ /**
+ * Tests for the 'compound attribute' key i.e. where first key's value is a map
+ * from which we take the value for the second key, e.g. CSQ : Consequence
+ */
+ @Test(groups = "Functional")
+ public void testMatches_compoundKey()
+ {
+ /*
+ * a numeric matcher - MatcherTest covers more conditions
+ */
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "CSQ", "Consequence");
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 10, "grp");
+ FeatureMatcherSetI fms = new FeatureMatcherSet();
+ fms.and(fm);
+ assertFalse(fms.matches(sf));
+ Map<String, String> csq = new HashMap<>();
+ sf.setValue("CSQ", csq);
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "-2");
+ assertTrue(fms.matches(sf));
+ csq.put("Consequence", "-1");
+ assertTrue(fms.matches(sf));
+ csq.put("Consequence", "-3");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "junk");
+ assertFalse(fms.matches(sf));
+
+ /*
+ * a string pattern matcher
+ */
+ fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ",
+ "Consequence");
+ fms = new FeatureMatcherSet();
+ fms.and(fm);
+ assertFalse(fms.matches(sf));
+ csq.put("PolyPhen", "damaging");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "damaging");
+ assertFalse(fms.matches(sf));
+ csq.put("Consequence", "Catastrophic");
+ assertTrue(fms.matches(sf));
+ }
+}
--- /dev/null
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherTest
+{
+ @Test
+ public void testMatches_byLabel()
+ {
+ SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+ 12, "grp");
+
+ /*
+ * contains - not case sensitive
+ */
+ assertTrue(
+ FeatureMatcher.byLabel(Condition.Contains, "IS").matches(sf));
+ assertTrue(FeatureMatcher.byLabel(Condition.Contains, "").matches(sf));
+ assertFalse(
+ FeatureMatcher.byLabel(Condition.Contains, "ISNT").matches(sf));
+
+ /*
+ * does not contain
+ */
+ assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "isnt")
+ .matches(sf));
+ assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "is")
+ .matches(sf));
+
+ /*
+ * matches
+ */
+ assertTrue(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY label")
+ .matches(sf));
+ assertFalse(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY")
+ .matches(sf));
+
+ /*
+ * does not match
+ */
+ assertFalse(FeatureMatcher
+ .byLabel(Condition.NotMatches, "THIS is MY label").matches(sf));
+ assertTrue(FeatureMatcher.byLabel(Condition.NotMatches, "THIS is MY")
+ .matches(sf));
+
+ /*
+ * is present / not present
+ */
+ assertTrue(FeatureMatcher.byLabel(Condition.Present, "").matches(sf));
+ assertFalse(
+ FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf));
+ }
+
+ @Test
+ public void testMatches_byScore()
+ {
+ SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+ 12, 3.2f, "grp");
+
+ assertTrue(FeatureMatcher.byScore(Condition.LT, "3.3").matches(sf));
+ assertFalse(FeatureMatcher.byScore(Condition.LT, "3.2").matches(sf));
+ assertFalse(FeatureMatcher.byScore(Condition.LT, "2.2").matches(sf));
+
+ assertTrue(FeatureMatcher.byScore(Condition.LE, "3.3").matches(sf));
+ assertTrue(FeatureMatcher.byScore(Condition.LE, "3.2").matches(sf));
+ assertFalse(FeatureMatcher.byScore(Condition.LE, "2.2").matches(sf));
+
+ assertFalse(FeatureMatcher.byScore(Condition.EQ, "3.3").matches(sf));
+ assertTrue(FeatureMatcher.byScore(Condition.EQ, "3.2").matches(sf));
+
+ assertFalse(FeatureMatcher.byScore(Condition.GE, "3.3").matches(sf));
+ assertTrue(FeatureMatcher.byScore(Condition.GE, "3.2").matches(sf));
+ assertTrue(FeatureMatcher.byScore(Condition.GE, "2.2").matches(sf));
+
+ assertFalse(FeatureMatcher.byScore(Condition.GT, "3.3").matches(sf));
+ assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf));
+ assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf));
+ }
+ @Test
+ public void testMatches_byAttribute()
+ {
+ /*
+ * a numeric matcher - MatcherTest covers more conditions
+ */
+ FeatureMatcherI fm = FeatureMatcher
+ .byAttribute(Condition.GE, "-2", "AF");
+ SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+ assertFalse(fm.matches(sf));
+ sf.setValue("AF", "foobar");
+ assertFalse(fm.matches(sf));
+ sf.setValue("AF", "-2");
+ assertTrue(fm.matches(sf));
+ sf.setValue("AF", "-1");
+ assertTrue(fm.matches(sf));
+ sf.setValue("AF", "-3");
+ assertFalse(fm.matches(sf));
+ sf.setValue("AF", "");
+ assertFalse(fm.matches(sf));
+
+ /*
+ * a string pattern matcher
+ */
+ fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+ assertFalse(fm.matches(sf));
+ sf.setValue("AF", "raining cats and dogs");
+ assertTrue(fm.matches(sf));
+
+ fm = FeatureMatcher.byAttribute(Condition.Present, "", "AC");
+ assertFalse(fm.matches(sf));
+ sf.setValue("AC", "21");
+ assertTrue(fm.matches(sf));
+
+ fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AC_Females");
+ assertTrue(fm.matches(sf));
+ sf.setValue("AC_Females", "21");
+ assertFalse(fm.matches(sf));
+ }
+
+ @Test
+ public void testToString()
+ {
+ Locale.setDefault(Locale.ENGLISH);
+
+ /*
+ * toString uses the i18n translation of the enum conditions
+ */
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+ "AF");
+ assertEquals(fm.toString(), "AF < 1.2");
+
+ /*
+ * Present / NotPresent omit the value pattern
+ */
+ fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF");
+ assertEquals(fm.toString(), "AF is present");
+ fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF");
+ assertEquals(fm.toString(), "AF is not present");
+
+ /*
+ * by Label
+ */
+ fm = FeatureMatcher.byLabel(Condition.Matches, "foobar");
+ assertEquals(fm.toString(),
+ MessageManager.getString("label.label") + " matches 'foobar'");
+
+ /*
+ * by Score
+ */
+ fm = FeatureMatcher.byScore(Condition.GE, "12.2");
+ assertEquals(fm.toString(),
+ MessageManager.getString("label.score") + " >= 12.2");
+ }
+
+ @Test
+ public void testGetAttribute()
+ {
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+ "AF");
+ assertEquals(fm.getAttribute(), new String[] { "AF" });
+
+ /*
+ * compound key (attribute / subattribute)
+ */
+ fm = FeatureMatcher.byAttribute(Condition.GE, "-2F", "CSQ",
+ "Consequence");
+ assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" });
+
+ /*
+ * answers null if match is by Label or by Score
+ */
+ assertNull(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+ .getAttribute());
+ assertNull(FeatureMatcher.byScore(Condition.LE, "-1").getAttribute());
+ }
+
+ @Test
+ public void testIsByLabel()
+ {
+ assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+ .isByLabel());
+ assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByLabel());
+ assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+ .isByLabel());
+ }
+
+ @Test
+ public void testIsByScore()
+ {
+ assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+ .isByScore());
+ assertTrue(FeatureMatcher.byScore(Condition.LE, "-1").isByScore());
+ assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+ .isByScore());
+ }
+
+ @Test
+ public void testGetMatcher()
+ {
+ FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f",
+ "AF");
+ assertEquals(fm.getMatcher().getCondition(), Condition.GE);
+ assertEquals(fm.getMatcher().getFloatValue(), -2F);
+ assertEquals(fm.getMatcher().getPattern(), "-2.0");
+ }
+}
--- /dev/null
+package jalview.ext.htsjdk;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class VCFReaderTest
+{
+ private static final String[] VCF = new String[] {
+ "##fileformat=VCFv4.2",
+ "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+ "20\t3\t.\tC\tG\t.\tPASS\tDP=100", // SNP C/G
+ "20\t7\t.\tG\tGA\t.\tPASS\tDP=100", // insertion G/GA
+ "18\t2\t.\tACG\tA\t.\tPASS\tDP=100" }; // deletion ACG/A
+
+ // gnomAD exome variant dataset
+ private static final String VCF_PATH = "/Volumes/gjb/smacgowan/NOBACK/resources/gnomad/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+ // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+ /**
+ * A test to exercise some basic functionality of the htsjdk VCF reader,
+ * reading from a non-index VCF file
+ *
+ * @throws IOException
+ */
+ @Test(groups = "Functional")
+ public void testReadVcf_plain() throws IOException
+ {
+ File f = writeVcfFile();
+ VCFReader reader = new VCFReader(f.getAbsolutePath());
+ CloseableIterator<VariantContext> variants = reader.iterator();
+
+ /*
+ * SNP C/G variant
+ */
+ VariantContext vc = variants.next();
+ assertTrue(vc.isSNP());
+ Allele ref = vc.getReference();
+ assertEquals(ref.getBaseString(), "C");
+ List<Allele> alleles = vc.getAlleles();
+ assertEquals(alleles.size(), 2);
+ assertTrue(alleles.get(0).isReference());
+ assertEquals(alleles.get(0).getBaseString(), "C");
+ assertFalse(alleles.get(1).isReference());
+ assertEquals(alleles.get(1).getBaseString(), "G");
+
+ /*
+ * Insertion G -> GA
+ */
+ vc = variants.next();
+ assertFalse(vc.isSNP());
+ assertTrue(vc.isSimpleInsertion());
+ ref = vc.getReference();
+ assertEquals(ref.getBaseString(), "G");
+ alleles = vc.getAlleles();
+ assertEquals(alleles.size(), 2);
+ assertTrue(alleles.get(0).isReference());
+ assertEquals(alleles.get(0).getBaseString(), "G");
+ assertFalse(alleles.get(1).isReference());
+ assertEquals(alleles.get(1).getBaseString(), "GA");
+
+ /*
+ * Deletion ACG -> A
+ */
+ vc = variants.next();
+ assertFalse(vc.isSNP());
+ assertTrue(vc.isSimpleDeletion());
+ ref = vc.getReference();
+ assertEquals(ref.getBaseString(), "ACG");
+ alleles = vc.getAlleles();
+ assertEquals(alleles.size(), 2);
+ assertTrue(alleles.get(0).isReference());
+ assertEquals(alleles.get(0).getBaseString(), "ACG");
+ assertFalse(alleles.get(1).isReference());
+ assertEquals(alleles.get(1).getBaseString(), "A");
+
+ assertFalse(variants.hasNext());
+
+ variants.close();
+ reader.close();
+ }
+
+ /**
+ * Creates a temporary file to be read by the htsjdk VCF reader
+ *
+ * @return
+ * @throws IOException
+ */
+ protected File writeVcfFile() throws IOException
+ {
+ File f = File.createTempFile("Test", "vcf");
+ f.deleteOnExit();
+ PrintWriter pw = new PrintWriter(f);
+ for (String vcfLine : VCF) {
+ pw.println(vcfLine);
+ }
+ pw.close();
+ return f;
+ }
+
+ /**
+ * A 'test' that demonstrates querying an indexed VCF file for features in a
+ * specified interval
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testQuery_indexed() throws IOException
+ {
+ /*
+ * if not specified, assumes index file is filename.tbi
+ */
+ VCFReader reader = new VCFReader(VCF_PATH);
+
+ /*
+ * gene NMT1 (human) is on chromosome 17
+ * GCHR38 (Ensembl): 45051610-45109016
+ * GCHR37 (gnoMAD): 43128978-43186384
+ * CDS begins at offset 9720, first CDS variant at offset 9724
+ */
+ CloseableIterator<VariantContext> features = reader.query("17",
+ 43128978 + 9724, 43128978 + 9734); // first 11 CDS positions
+
+ assertEquals(printNext(features), 43138702);
+ assertEquals(printNext(features), 43138704);
+ assertEquals(printNext(features), 43138707);
+ assertEquals(printNext(features), 43138708);
+ assertEquals(printNext(features), 43138710);
+ assertEquals(printNext(features), 43138711);
+ assertFalse(features.hasNext());
+
+ features.close();
+ reader.close();
+ }
+
+ /**
+ * Prints the toString value of the next variant, and returns its start
+ * location
+ *
+ * @param features
+ * @return
+ */
+ protected int printNext(CloseableIterator<VariantContext> features)
+ {
+ VariantContext next = features.next();
+ System.out.println(next.toString());
+ return next.getStart();
+ }
+
+ // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+ /**
+ * Test the query method that wraps a non-indexed VCF file
+ *
+ * @throws IOException
+ */
+ @Test(groups = "Functional")
+ public void testQuery_plain() throws IOException
+ {
+ File f = writeVcfFile();
+ VCFReader reader = new VCFReader(f.getAbsolutePath());
+
+ /*
+ * query for overlap of 5-8 - should find variant at 7
+ */
+ CloseableIterator<VariantContext> variants = reader.query("20", 5, 8);
+
+ /*
+ * INDEL G/GA variant
+ */
+ VariantContext vc = variants.next();
+ assertTrue(vc.isIndel());
+ assertEquals(vc.getStart(), 7);
+ assertEquals(vc.getEnd(), 7);
+ Allele ref = vc.getReference();
+ assertEquals(ref.getBaseString(), "G");
+ List<Allele> alleles = vc.getAlleles();
+ assertEquals(alleles.size(), 2);
+ assertTrue(alleles.get(0).isReference());
+ assertEquals(alleles.get(0).getBaseString(), "G");
+ assertFalse(alleles.get(1).isReference());
+ assertEquals(alleles.get(1).getBaseString(), "GA");
+
+ assertFalse(variants.hasNext());
+
+ variants.close();
+ reader.close();
+ }
+}
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.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;
{
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());
+ .getHiddenColumnsCopy().isEmpty());
assertFalse(alignFrame.hideFeatureColumns("exon", false));
assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
- .getHiddenColumnsCopy()
- .isEmpty());
+ .getHiddenColumnsCopy().isEmpty());
/*
* 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();
+ .getHiddenColumns().getHiddenColumnsCopy();
assertTrue(hidden.isEmpty());
/*
+ * 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);
+
+ /*
* 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();
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.DBRefEntry;
import jalview.datamodel.DBRefSource;
-import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
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.List;
import javax.swing.JMenu;
@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);
// 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]);
}
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());
import org.testng.annotations.Test;
-import sun.swing.SwingUtilities2;
-
public class SeqCanvasTest
{
/**
av.setScaleAboveWrapped(true);
av.setScaleLeftWrapped(true);
av.setScaleRightWrapped(true);
- FontMetrics fm = SwingUtilities2.getFontMetrics(testee, av.getFont());
+ FontMetrics fm = testee.getFontMetrics(av.getFont());
int labelWidth = fm.stringWidth("000") + charWidth;
assertEquals(labelWidth, 39); // 3 x 9 + charWidth
av.setScaleAboveWrapped(true);
av.setScaleLeftWrapped(true);
av.setScaleRightWrapped(true);
- FontMetrics fm = SwingUtilities2.getFontMetrics(testee, av.getFont());
+ FontMetrics fm = testee.getFontMetrics(av.getFont());
int labelWidth = fm.stringWidth("000") + charWidth;
assertEquals(labelWidth, 39); // 3 x 9 + charWidth
int annotationHeight = testee.getAnnotationHeight();
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+
+import junit.extensions.PA;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
// . codonframes
//
//
- HashMap<String, String> dbtoviewBit = new HashMap<>();
+ Map<String, String> dbtoviewBit = new HashMap<>();
List<String> keyseq = new ArrayList<>();
- HashMap<String, File> savedProjects = new HashMap<>();
+ Map<String, File> savedProjects = new HashMap<>();
for (String[] did : new String[][] { { "UNIPROT", "P00338" } })
{
if (pass2 == 0)
{ // retrieve and show cross-refs in this thread
- cra = new CrossRefAction(af, seqs, dna, db);
+ cra = CrossRefAction.getHandlerFor(seqs, dna, db, af);
cra.run();
- if (cra.getXrefViews().size() == 0)
+ cra_views = (List<AlignmentViewPanel>) PA.getValue(cra,
+ "xrefViews");
+ if (cra_views.size() == 0)
{
failedXrefMenuItems.add("No crossrefs retrieved for "
+ first + " -> " + db);
continue;
}
- cra_views = cra.getXrefViews();
assertNucleotide(cra_views.get(0),
"Nucleotide panel included proteins for " + first
+ " -> " + db);
if (pass3 == 0)
{
-
SequenceI[] xrseqs = avp.getAlignment()
.getSequencesArray();
AlignFrame nextaf = Desktop.getAlignFrameFor(avp
.getAlignViewport());
- cra = new CrossRefAction(nextaf, xrseqs, avp
- .getAlignViewport().isNucleotide(), xrefdb);
+ cra = CrossRefAction.getHandlerFor(xrseqs, avp
+ .getAlignViewport().isNucleotide(), xrefdb,
+ nextaf);
cra.run();
- if (cra.getXrefViews().size() == 0)
+ cra_views2 = (List<AlignmentViewPanel>) PA.getValue(
+ cra, "xrefViews");
+ if (cra_views2.size() == 0)
{
failedXrefMenuItems
.add("No crossrefs retrieved for '"
+ " via '" + nextaf.getTitle() + "'");
continue;
}
- cra_views2 = cra.getXrefViews();
assertNucleotide(cra_views2.get(0),
"Nucleotide panel included proteins for '"
+ nextxref + "' to " + xrefdb
* viewpanel needs to be called with a distinct xrefpath to ensure
* each one's strings are compared)
*/
- private void stringify(HashMap<String, String> dbtoviewBit,
- HashMap<String, File> savedProjects, String xrefpath,
+ private void stringify(Map<String, String> dbtoviewBit,
+ Map<String, File> savedProjects, String xrefpath,
AlignmentViewPanel avp)
{
if (savedProjects != null)
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
+import jalview.api.FeatureColourI;
import jalview.datamodel.DBRefEntry;
import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
import jalview.gui.JvOptionPane;
import jalview.io.gff.GffConstants;
+import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
-import java.util.HashMap;
-import java.util.Hashtable;
+import java.awt.Color;
import java.util.Map;
import junit.extensions.PA;
SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f,
"group");
- Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
- sar.appendFeature(sb, 1, minmax, sf);
+ FeatureRendererModel fr = new FeatureRenderer(null);
+ Map<String, float[][]> minmax = fr.getMinMax();
+ sar.appendFeature(sb, 1, fr, sf);
/*
* map has no entry for this feature type - score is not shown:
*/
* map has entry for this feature type - score is shown:
*/
minmax.put("METAL", new float[][] { { 0f, 1f }, null });
- sar.appendFeature(sb, 1, minmax, sf);
+ sar.appendFeature(sb, 1, fr, sf);
// <br> is appended to a buffer > 6 in length
assertEquals("METAL 1 3; Fe2-S<br>METAL 1 3; Fe2-S Score=1.3",
sb.toString());
*/
minmax.put("METAL", new float[][] { { 2f, 2f }, null });
sb.setLength(0);
- sar.appendFeature(sb, 1, minmax, sf);
+ sar.appendFeature(sb, 1, fr, sf);
assertEquals("METAL 1 3; Fe2-S", sb.toString());
}
assertEquals("METAL 1 3; Fe2-S", sb.toString());
}
+ /**
+ * A specific attribute value is included if it is used to colour the feature
+ */
@Test(groups = "Functional")
- public void testAppendFeature_clinicalSignificance()
+ public void testAppendFeature_colouredByAttribute()
{
SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
StringBuilder sb = new StringBuilder();
Float.NaN, "group");
sf.setValue("clinical_significance", "Benign");
- sar.appendFeature(sb, 1, null, sf);
- assertEquals("METAL 1 3; Fe2-S; Benign", sb.toString());
+ /*
+ * first with no colour by attribute
+ */
+ FeatureRendererModel fr = new FeatureRenderer(null);
+ sar.appendFeature(sb, 1, fr, sf);
+ assertEquals("METAL 1 3; Fe2-S", sb.toString());
+
+ /*
+ * then with colour by an attribute the feature lacks
+ */
+ FeatureColourI fc = new FeatureColour(Color.white, Color.black, 5, 10);
+ fc.setAttributeName("Pfam");
+ fr.setColour("METAL", fc);
+ sb.setLength(0);
+ sar.appendFeature(sb, 1, fr, sf);
+ assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
+
+ /*
+ * then with colour by an attribute the feature has
+ */
+ fc.setAttributeName("clinical_significance");
+ sb.setLength(0);
+ sar.appendFeature(sb, 1, fr, sf);
+ assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
+ sb.toString());
}
@Test(groups = "Functional")
- public void testAppendFeature_withScoreStatusClinicalSignificance()
+ public void testAppendFeature_withScoreStatusAttribute()
{
SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
StringBuilder sb = new StringBuilder();
"group");
sf.setStatus("Confirmed");
sf.setValue("clinical_significance", "Benign");
- Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
+
+ FeatureRendererModel fr = new FeatureRenderer(null);
+ Map<String, float[][]> minmax = fr.getMinMax();
+ FeatureColourI fc = new FeatureColour(Color.white, Color.blue, 12, 22);
+ fc.setAttributeName("clinical_significance");
+ fr.setColour("METAL", fc);
minmax.put("METAL", new float[][] { { 0f, 1f }, null });
- sar.appendFeature(sb, 1, minmax, sf);
+ sar.appendFeature(sb, 1, fr, sf);
- assertEquals("METAL 1 3; Fe2-S Score=1.3; (Confirmed); Benign",
+ assertEquals(
+ "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
sb.toString());
}
null));
sb.setLength(0);
sar.createSequenceAnnotationReport(sb, seq, true, true, null);
- String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos</i>";
+ String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos Score=1.0</i>";
assertEquals(expected, sb.toString());
/*
*/
seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f,
null));
- Map<String, float[][]> minmax = new HashMap<String, float[][]>();
+
+ FeatureRendererModel fr = new FeatureRenderer(null);
+ Map<String, float[][]> minmax = fr.getMinMax();
minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+
sb.setLength(0);
- sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+ sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
assertEquals(expected, sb.toString());
sf.setValue("linkonly", Boolean.TRUE);
seq.addSequenceFeature(sf);
sb.setLength(0);
- sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+ sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
assertEquals(expected, sb.toString()); // unchanged!
/*
- * 'clinical_significance' currently being specially included
+ * 'clinical_significance' attribute only included when
+ * used for feature colouring
*/
SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0,
5f, null);
sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign");
seq.addSequenceFeature(sf2);
sb.setLength(0);
- sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
- expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+ sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+ expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana</i>";
assertEquals(expected, sb.toString());
/*
*/
seq.addDBRef(new DBRefEntry("PDB", "0", "3iu1"));
seq.addDBRef(new DBRefEntry("Uniprot", "1", "P30419"));
+
// with showDbRefs = false
sb.setLength(0);
- sar.createSequenceAnnotationReport(sb, seq, false, true, minmax);
+ sar.createSequenceAnnotationReport(sb, seq, false, true, fr);
assertEquals(expected, sb.toString()); // unchanged
- // with showDbRefs = true
+
+ // with showDbRefs = true, colour Variant features by clinical_significance
sb.setLength(0);
- sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
- expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+ FeatureColourI fc = new FeatureColour(Color.green, Color.pink, 2, 3);
+ fc.setAttributeName("clinical_significance");
+ fr.setColour("Variant", fc);
+ sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+ expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>"
+ + "Type1 ; Nonpos<br>Variant ; Havana; clinical_significance=benign</i>";
assertEquals(expected, sb.toString());
// with showNonPositionalFeatures = false
sb.setLength(0);
- sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+ sar.createSequenceAnnotationReport(sb, seq, true, false, fr);
expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
assertEquals(expected, sb.toString());
--- /dev/null
+package jalview.io.vcf;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class VCFLoaderTest
+{
+ private static final float DELTA = 0.00001f;
+
+ // columns 9717- of gene P30419 from Ensembl (much modified)
+ private static final String FASTA = ""
+ +
+ /*
+ * forward strand 'gene' and 'transcript' with two exons
+ */
+ ">gene1/1-25 chromosome:GRCh38:17:45051610:45051634:1\n"
+ + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+ + ">transcript1/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+
+ /*
+ * reverse strand gene and transcript (reverse complement alleles!)
+ */
+ + ">gene2/1-25 chromosome:GRCh38:17:45051610:45051634:-1\n"
+ + "TGTCACACTCTCGTCCGCCAGCTTG\n"
+ + ">transcript2/1-18\n" + "-GTCACACTCT----CGCCAGCT--\n"
+
+ /*
+ * 'gene' on chromosome 5 with two transcripts
+ */
+ + ">gene3/1-25 chromosome:GRCh38:5:45051610:45051634:1\n"
+ + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+ + ">transcript3/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+ + ">transcript4/1-18\n-----TGG-GGACGAGAGTGTGA-A\n";
+
+ private static final String[] VCF = { "##fileformat=VCFv4.2",
+ "##INFO=<ID=AF,Number=A,Type=Float,Description=\"Allele Frequency, for each ALT allele, in the same order as listed\">",
+ "##reference=Homo_sapiens/GRCh38",
+ "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+ // A/T,C variants in position 2 of gene sequence (precedes transcript)
+ // should create 2 variant features with respective scores
+ "17\t45051611\t.\tA\tT,C\t1666.64\tRF\tAC=15;AF=5.0e-03,4.0e-03",
+ // SNP G/C in position 4 of gene sequence, position 2 of transcript
+ // insertion G/GA is transferred to nucleotide but not to peptide
+ "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.0e-03,2.0e-03" };
+
+ @BeforeClass
+ public void setUp()
+ {
+ /*
+ * configure to capture all available VCF and VEP (CSQ) fields
+ */
+ Cache.loadProperties("test/jalview/io/testProps.jvprops");
+ Cache.setProperty("VCF_FIELDS", ".*");
+ Cache.setProperty("VEP_FIELDS", ".*");
+ }
+
+ @Test(groups = "Functional")
+ public void testDoLoad() throws IOException
+ {
+ AlignmentI al = buildAlignment();
+ VCFLoader loader = new VCFLoader(al);
+
+ File f = makeVcf();
+
+ loader.doLoad(f.getPath(), null);
+
+ /*
+ * verify variant feature(s) added to gene
+ * NB alleles at a locus may not be processed, and features added,
+ * in the order in which they appear in the VCF record as method
+ * VariantContext.getAlternateAlleles() does not guarantee order
+ * - order of assertions here matches what we find (is not important)
+ */
+ List<SequenceFeature> geneFeatures = al.getSequenceAt(0)
+ .getSequenceFeatures();
+ SequenceFeatures.sortFeatures(geneFeatures, true);
+ assertEquals(geneFeatures.size(), 4);
+ SequenceFeature sf = geneFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 2);
+ assertEquals(sf.getEnd(), 2);
+ assertEquals(sf.getScore(), 4.0e-03, DELTA);
+ assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,C");
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ sf = geneFeatures.get(1);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 2);
+ assertEquals(sf.getEnd(), 2);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 5.0e-03, DELTA);
+ assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,T");
+
+ sf = geneFeatures.get(2);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 4);
+ assertEquals(sf.getEnd(), 4);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 2.0e-03, DELTA);
+ assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+
+ sf = geneFeatures.get(3);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 4);
+ assertEquals(sf.getEnd(), 4);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 3.0e-03, DELTA);
+ assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+
+ /*
+ * verify variant feature(s) added to transcript
+ */
+ List<SequenceFeature> transcriptFeatures = al.getSequenceAt(1)
+ .getSequenceFeatures();
+ assertEquals(transcriptFeatures.size(), 2);
+ sf = transcriptFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 2);
+ assertEquals(sf.getEnd(), 2);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 2.0e-03, DELTA);
+ assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+ sf = transcriptFeatures.get(1);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 2);
+ assertEquals(sf.getEnd(), 2);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 3.0e-03, DELTA);
+ assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+
+ /*
+ * verify SNP variant feature(s) computed and added to protein
+ * first codon AGC varies to ACC giving S/T
+ */
+ DBRefEntry[] dbRefs = al.getSequenceAt(1).getDBRefs();
+ SequenceI peptide = null;
+ for (DBRefEntry dbref : dbRefs)
+ {
+ if (dbref.getMap().getMap().getFromRatio() == 3)
+ {
+ peptide = dbref.getMap().getTo();
+ }
+ }
+ List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+ assertEquals(proteinFeatures.size(), 1);
+ sf = proteinFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 1);
+ assertEquals(sf.getEnd(), 1);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getDescription(), "p.Ser1Thr");
+ }
+
+ private File makeVcf() throws IOException
+ {
+ File f = File.createTempFile("Test", ".vcf");
+ f.deleteOnExit();
+ PrintWriter pw = new PrintWriter(f);
+ for (String vcfLine : VCF)
+ {
+ pw.println(vcfLine);
+ }
+ pw.close();
+ return f;
+ }
+
+ /**
+ * Make a simple alignment with one 'gene' and one 'transcript'
+ *
+ * @return
+ */
+ private AlignmentI buildAlignment()
+ {
+ AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(FASTA,
+ DataSourceType.PASTE);
+
+ /*
+ * map gene1 sequence to chromosome (normally done when the sequence is fetched
+ * from Ensembl and transcripts computed)
+ */
+ AlignmentI alignment = af.getViewport().getAlignment();
+ SequenceI gene1 = alignment.findName("gene1");
+ int[] to = new int[] { 45051610, 45051634 };
+ int[] from = new int[] { gene1.getStart(), gene1.getEnd() };
+ gene1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to,
+ 1, 1));
+
+ /*
+ * map 'transcript1' to chromosome via 'gene1'
+ * transcript1/1-18 is gene1/3-10,15-24
+ * which is chromosome 45051612-45051619,45051624-45051633
+ */
+ to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+ SequenceI transcript1 = alignment.findName("transcript1");
+ from = new int[] { transcript1.getStart(), transcript1.getEnd() };
+ transcript1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(
+ from, to,
+ 1, 1));
+
+ /*
+ * map gene2 to chromosome reverse strand
+ */
+ SequenceI gene2 = alignment.findName("gene2");
+ to = new int[] { 45051634, 45051610 };
+ from = new int[] { gene2.getStart(), gene2.getEnd() };
+ gene2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to,
+ 1, 1));
+
+ /*
+ * map 'transcript2' to chromosome via 'gene2'
+ * transcript2/1-18 is gene2/2-11,16-23
+ * which is chromosome 45051633-45051624,45051619-45051612
+ */
+ to = new int[] { 45051633, 45051624, 45051619, 45051612 };
+ SequenceI transcript2 = alignment.findName("transcript2");
+ from = new int[] { transcript2.getStart(), transcript2.getEnd() };
+ transcript2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(
+ from, to,
+ 1, 1));
+
+ /*
+ * add a protein product as a DBRef on transcript1
+ */
+ SequenceI peptide1 = new Sequence("ENSP001", "SWRECD");
+ MapList mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 },
+ 3, 1);
+ Mapping map = new Mapping(peptide1, mapList);
+ DBRefEntry product = new DBRefEntry("", "", "ENSP001", map);
+ transcript1.addDBRef(product);
+
+ /*
+ * add a protein product as a DBRef on transcript2
+ */
+ SequenceI peptide2 = new Sequence("ENSP002", "VTLSPA");
+ mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+ map = new Mapping(peptide2, mapList);
+ product = new DBRefEntry("", "", "ENSP002", map);
+ transcript2.addDBRef(product);
+
+ /*
+ * map gene3 to chromosome
+ */
+ SequenceI gene3 = alignment.findName("gene3");
+ to = new int[] { 45051610, 45051634 };
+ from = new int[] { gene3.getStart(), gene3.getEnd() };
+ gene3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(from, to,
+ 1, 1));
+
+ /*
+ * map 'transcript3' to chromosome
+ */
+ SequenceI transcript3 = alignment.findName("transcript3");
+ to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+ from = new int[] { transcript3.getStart(), transcript3.getEnd() };
+ transcript3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(
+ from, to,
+ 1, 1));
+
+ /*
+ * map 'transcript4' to chromosome
+ */
+ SequenceI transcript4 = alignment.findName("transcript4");
+ to = new int[] { 45051615, 45051617, 45051619, 45051632, 45051634,
+ 45051634 };
+ from = new int[] { transcript4.getStart(), transcript4.getEnd() };
+ transcript4.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(
+ from, to,
+ 1, 1));
+
+ /*
+ * add a protein product as a DBRef on transcript3
+ */
+ SequenceI peptide3 = new Sequence("ENSP003", "SWRECD");
+ mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+ map = new Mapping(peptide3, mapList);
+ product = new DBRefEntry("", "", "ENSP003", map);
+ transcript3.addDBRef(product);
+
+ return alignment;
+ }
+
+ /**
+ * Test with 'gene' and 'transcript' mapped to the reverse strand of the
+ * chromosome. The VCF variant positions (in forward coordinates) should get
+ * correctly located on sequence positions.
+ *
+ * @throws IOException
+ */
+ @Test(groups = "Functional")
+ public void testDoLoad_reverseStrand() throws IOException
+ {
+ AlignmentI al = buildAlignment();
+
+ VCFLoader loader = new VCFLoader(al);
+
+ File f = makeVcf();
+
+ loader.doLoad(f.getPath(), null);
+
+ /*
+ * verify variant feature(s) added to gene2
+ * gene/1-25 maps to chromosome 45051634- reverse strand
+ * variants A/T, A/C at 45051611 and G/GA,G/C at 45051613 map to
+ * T/A, T/G and C/TC,C/G at gene positions 24 and 22 respectively
+ */
+ List<SequenceFeature> geneFeatures = al.getSequenceAt(2)
+ .getSequenceFeatures();
+ SequenceFeatures.sortFeatures(geneFeatures, true);
+ assertEquals(geneFeatures.size(), 4);
+ SequenceFeature sf = geneFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 22);
+ assertEquals(sf.getEnd(), 22);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 2.0e-03, DELTA);
+ assertEquals("C,G", sf.getValue(Gff3Helper.ALLELES));
+
+ sf = geneFeatures.get(1);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 22);
+ assertEquals(sf.getEnd(), 22);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 3.0e-03, DELTA);
+ assertEquals("C,TC", sf.getValue(Gff3Helper.ALLELES));
+
+ sf = geneFeatures.get(2);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 24);
+ assertEquals(sf.getEnd(), 24);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 4.0e-03, DELTA);
+ assertEquals("T,G", sf.getValue(Gff3Helper.ALLELES));
+
+ sf = geneFeatures.get(3);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 24);
+ assertEquals(sf.getEnd(), 24);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 5.0e-03, DELTA);
+ assertEquals("T,A", sf.getValue(Gff3Helper.ALLELES));
+
+ /*
+ * verify variant feature(s) added to transcript2
+ * variants G/GA,G/C at position 22 of gene overlap and map to
+ * C/TC,C/G at position 17 of transcript
+ */
+ List<SequenceFeature> transcriptFeatures = al.getSequenceAt(3)
+ .getSequenceFeatures();
+ assertEquals(transcriptFeatures.size(), 2);
+ sf = transcriptFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 17);
+ assertEquals(sf.getEnd(), 17);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 2.0e-03, DELTA);
+ assertEquals("C,G", sf.getValue(Gff3Helper.ALLELES));
+
+ sf = transcriptFeatures.get(1);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 17);
+ assertEquals(sf.getEnd(), 17);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getScore(), 3.0e-03, DELTA);
+ assertEquals("C,TC", sf.getValue(Gff3Helper.ALLELES));
+
+ /*
+ * verify variant feature(s) computed and added to protein
+ * last codon GCT varies to GGT giving A/G in the last peptide position
+ */
+ DBRefEntry[] dbRefs = al.getSequenceAt(3).getDBRefs();
+ SequenceI peptide = null;
+ for (DBRefEntry dbref : dbRefs)
+ {
+ if (dbref.getMap().getMap().getFromRatio() == 3)
+ {
+ peptide = dbref.getMap().getTo();
+ }
+ }
+ List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+ assertEquals(proteinFeatures.size(), 1);
+ sf = proteinFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 6);
+ assertEquals(sf.getEnd(), 6);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getDescription(), "p.Ala6Gly");
+ }
+
+ /**
+ * Tests that if VEP consequence (CSQ) data is present in the VCF data, then
+ * it is added to the variant feature, but restricted where possible to the
+ * consequences for a specific transcript
+ *
+ * @throws IOException
+ */
+ @Test(groups = "Functional")
+ public void testDoLoad_vepCsq() throws IOException
+ {
+ AlignmentI al = buildAlignment();
+
+ VCFLoader loader = new VCFLoader(al);
+
+ /*
+ * VCF data file with variants at gene3 positions
+ * 1 C/A
+ * 5 C/T
+ * 9 CGT/C (deletion)
+ * 13 C/G, C/T
+ * 17 A/AC (insertion), A/G
+ */
+ loader.doLoad("test/jalview/io/vcf/testVcf.dat", null);
+
+ /*
+ * verify variant feature(s) added to gene3
+ */
+ List<SequenceFeature> geneFeatures = al.findName("gene3")
+ .getSequenceFeatures();
+ SequenceFeatures.sortFeatures(geneFeatures, true);
+ assertEquals(geneFeatures.size(), 7);
+ SequenceFeature sf = geneFeatures.get(0);
+ assertEquals(sf.getBegin(), 1);
+ assertEquals(sf.getEnd(), 1);
+ assertEquals(sf.getScore(), 0.1f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,A");
+ // gene features include Consequence for all transcripts
+ Map map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ sf = geneFeatures.get(1);
+ assertEquals(sf.getBegin(), 5);
+ assertEquals(sf.getEnd(), 5);
+ assertEquals(sf.getScore(), 0.2f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,T");
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ sf = geneFeatures.get(2);
+ assertEquals(sf.getBegin(), 9);
+ assertEquals(sf.getEnd(), 11); // deletion over 3 positions
+ assertEquals(sf.getScore(), 0.3f, DELTA);
+ assertEquals(sf.getValue("alleles"), "CGG,C");
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ sf = geneFeatures.get(3);
+ assertEquals(sf.getBegin(), 13);
+ assertEquals(sf.getEnd(), 13);
+ assertEquals(sf.getScore(), 0.5f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,T");
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ sf = geneFeatures.get(4);
+ assertEquals(sf.getBegin(), 13);
+ assertEquals(sf.getEnd(), 13);
+ assertEquals(sf.getScore(), 0.4f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,G");
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ sf = geneFeatures.get(5);
+ assertEquals(sf.getBegin(), 17);
+ assertEquals(sf.getEnd(), 17);
+ assertEquals(sf.getScore(), 0.7f, DELTA);
+ assertEquals(sf.getValue("alleles"), "A,G");
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ sf = geneFeatures.get(6);
+ assertEquals(sf.getBegin(), 17);
+ assertEquals(sf.getEnd(), 17); // insertion
+ assertEquals(sf.getScore(), 0.6f, DELTA);
+ assertEquals(sf.getValue("alleles"), "A,AC");
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+
+ /*
+ * verify variant feature(s) added to transcript3
+ * at columns 5 (1), 17 (2), positions 3, 11
+ * note the deletion at columns 9-11 is not transferred since col 11
+ * has no mapping to transcript 3
+ */
+ List<SequenceFeature> transcriptFeatures = al.findName("transcript3")
+ .getSequenceFeatures();
+ SequenceFeatures.sortFeatures(transcriptFeatures, true);
+ assertEquals(transcriptFeatures.size(), 3);
+ sf = transcriptFeatures.get(0);
+ assertEquals(sf.getBegin(), 3);
+ assertEquals(sf.getEnd(), 3);
+ assertEquals(sf.getScore(), 0.2f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,T");
+ // transcript features only have Consequence for that transcripts
+ map = (Map) sf.getValue("CSQ");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+ sf = transcriptFeatures.get(1);
+ assertEquals(sf.getBegin(), 11);
+ assertEquals(sf.getEnd(), 11);
+ assertEquals(sf.getScore(), 0.7f, DELTA);
+ assertEquals(sf.getValue("alleles"), "A,G");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+ sf = transcriptFeatures.get(2);
+ assertEquals(sf.getBegin(), 11);
+ assertEquals(sf.getEnd(), 11);
+ assertEquals(sf.getScore(), 0.6f, DELTA);
+ assertEquals(sf.getValue("alleles"), "A,AC");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+ /*
+ * verify variants computed on protein product for transcript3
+ * peptide is SWRECD
+ * codon variants are AGC/AGT position 1 which is synonymous
+ * and GAG/GGG which is E/G in position 4
+ * the insertion variant is not transferred to the peptide
+ */
+ DBRefEntry[] dbRefs = al.findName("transcript3").getDBRefs();
+ SequenceI peptide = null;
+ for (DBRefEntry dbref : dbRefs)
+ {
+ if (dbref.getMap().getMap().getFromRatio() == 3)
+ {
+ peptide = dbref.getMap().getTo();
+ }
+ }
+ List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+ assertEquals(proteinFeatures.size(), 1);
+ sf = proteinFeatures.get(0);
+ assertEquals(sf.getFeatureGroup(), "VCF");
+ assertEquals(sf.getBegin(), 4);
+ assertEquals(sf.getEnd(), 4);
+ assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+ assertEquals(sf.getDescription(), "p.Glu4Gly");
+
+ /*
+ * verify variant feature(s) added to transcript4
+ * at columns 13 (2) and 17 (2), positions 7 and 11
+ */
+ transcriptFeatures = al.findName("transcript4").getSequenceFeatures();
+ SequenceFeatures.sortFeatures(transcriptFeatures, true);
+ assertEquals(transcriptFeatures.size(), 4);
+ sf = transcriptFeatures.get(0);
+ assertEquals(sf.getBegin(), 7);
+ assertEquals(sf.getEnd(), 7);
+ assertEquals(sf.getScore(), 0.5f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,T");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+ sf = transcriptFeatures.get(1);
+ assertEquals(sf.getBegin(), 7);
+ assertEquals(sf.getEnd(), 7);
+ assertEquals(sf.getScore(), 0.4f, DELTA);
+ assertEquals(sf.getValue("alleles"), "C,G");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+ sf = transcriptFeatures.get(2);
+ assertEquals(sf.getBegin(), 11);
+ assertEquals(sf.getEnd(), 11);
+ assertEquals(sf.getScore(), 0.7f, DELTA);
+ assertEquals(sf.getValue("alleles"), "A,G");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+ sf = transcriptFeatures.get(3);
+ assertEquals(sf.getBegin(), 11);
+ assertEquals(sf.getEnd(), 11);
+ assertEquals(sf.getScore(), 0.6f, DELTA);
+ assertEquals(sf.getValue("alleles"), "A,AC");
+ assertEquals(map.size(), 9);
+ assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+ }
+}
--- /dev/null
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF_Female,Number=R,Type=Float,Description="Allele Frequency among Female genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AN,Number=1,Type=Integer,Description="Total number of alleles in called genotypes">
+##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence annotations from Ensembl VEP. Format: Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|PolyPhen">
+##reference=/Homo_sapiens/GRCh38
+#CHROM POS ID REF ALT QUAL FILTER INFO
+5 45051610 . C A 81.96 RF;AC0 AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5 45051614 . C T 1666.64 RF AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5 45051618 . CGG C 41.94 AC0 AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5 45051622 . C G,T 224.23 RF;AC0 AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5 45051626 . A AC,G 433.35 RF;AC0 AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
import jalview.io.DataSourceType;
import jalview.io.FileLoader;
import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
import java.awt.Color;
import java.util.List;
* - currently no way other than mimicking reordering of
* table in Feature Settings
*/
- Object[][] data = new Object[2][];
- data[0] = new Object[] { "Metal", red, true };
- data[1] = new Object[] { "Domain", green, true };
+ FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+ data[0] = new FeatureSettingsBean("Metal", red, null, true);
+ data[1] = new FeatureSettingsBean("Domain", green, null, true);
fr.setFeaturePriority(data);
c = finder.findFeatureColour(Color.blue, seq, 10);
assertEquals(c, Color.red);
/*
* ..and turn off display of Metal
*/
- data[0][2] = false;
+ data[0] = new FeatureSettingsBean("Metal", red, null, false);
fr.setFeaturePriority(data);
c = finder.findFeatureColour(Color.blue, seq, 10);
assertEquals(c, Color.green);
/*
* turn off display of Metal - is this the easiest way to do it??
*/
- Object[][] data = new Object[1][];
- data[0] = new Object[] { "Metal", red, false };
+ FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+ data[0] = new FeatureSettingsBean("Metal", red, null, false);
fr.setFeaturePriority(data);
c = finder.findFeatureColour(Color.blue, seq, 10);
assertEquals(c, Color.blue);
/*
* turn display of Metal back on
*/
- data[0] = new Object[] { "Metal", red, true };
+ data[0] = new FeatureSettingsBean("Metal", red, null, true);
fr.setFeaturePriority(data);
c = finder.findFeatureColour(Color.blue, seq, 10);
assertEquals(c, Color.red);
* 1) 0.6 * green(0, 255, 0) + 0.4 * cyan(0, 255, 255) = (0, 255, 102)
* 2) 0.6* red(255, 0, 0) + 0.4 * (0, 255, 102) = (153, 102, 41) rounded
*/
- Object[][] data = new Object[2][];
- data[0] = new Object[] { "Metal", red, true };
- data[1] = new Object[] { "Domain", green, true };
+ FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+ data[0] = new FeatureSettingsBean("Metal", red, null, true);
+ data[1] = new FeatureSettingsBean("Domain", green, null, true);
fr.setFeaturePriority(data);
c = finder.findFeatureColour(Color.cyan, seq, 10);
assertEquals(c, new Color(153, 102, 41));
* Domain (green) above background (pink)
* 0.6 * green(0, 255, 0) + 0.4 * pink(255, 175, 175) = (102, 223, 70)
*/
- data[0][2] = false;
+ data[0] = new FeatureSettingsBean("Metal", red, null, false);
fr.setFeaturePriority(data);
c = finder.findFeatureColour(Color.pink, seq, 10);
assertEquals(c, new Color(102, 223, 70));
/*
* turn off display of Metal
*/
- Object[][] data = new Object[1][];
- data[0] = new Object[] { "Metal", red, false };
+ FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+ data[0] = new FeatureSettingsBean("Metal", red, null, false);
fr.setFeaturePriority(data);
assertTrue(finder.noFeaturesDisplayed());
/*
* render order is kd above Metal
*/
- Object[][] data = new Object[2][];
- data[0] = new Object[] { kdFeature, fc, true };
- data[1] = new Object[] { metalFeature, green, true };
+ FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+ data[0] = new FeatureSettingsBean(kdFeature, fc, null, true);
+ data[1] = new FeatureSettingsBean(metalFeature, green, null, true);
fr.setFeaturePriority(data);
av.setShowSequenceFeatures(true);
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;
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));
+ }
+
+ @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);
/*
- * hide group 2, show group 3 - sf2 is removed, sf3 is retained
+ * simple colour, feature type and group displayed
*/
- fr.setGroupVisibility("group2", false);
- fr.setGroupVisibility("group3", true);
- features = seq.getSequenceFeatures();
- fr.filterFeaturesForDisplay(features, null);
- assertEquals(features.size(), 3);
- assertTrue(features.contains(sf1) || features.contains(sf4));
- assertFalse(features.contains(sf1) && features.contains(sf4));
- assertFalse(features.contains(sf2));
- assertTrue(features.contains(sf3));
- assertTrue(features.contains(sf5));
+ 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);
/*
- * no filtering of co-located features with graduated colour scheme
- * filterFeaturesForDisplay does _not_ check colour threshold
- * sf2 is removed as its group is hidden
+ * hide feature type, then unhide
*/
- features = seq.getSequenceFeatures();
- fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
- Color.white, 0f, 1f));
- assertEquals(features.size(), 4);
- assertTrue(features.contains(sf1));
- assertTrue(features.contains(sf3));
- assertTrue(features.contains(sf4));
- assertTrue(features.contains(sf5));
+ FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+ data[0] = new FeatureSettingsBean("Cath", fc, null, false);
+ fr.setFeaturePriority(data);
+ assertNull(fr.getColour(sf1));
+ data[0] = new FeatureSettingsBean("Cath", fc, null, true);
+ fr.setFeaturePriority(data);
+ assertEquals(fr.getColour(sf1), Color.red);
/*
- * co-located features with colour by label
- * should not get filtered
+ * hide feature group, then unhide
*/
- 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));
+ 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);
}
}
* </ul>
* <ul>
*/
- @Test
+ @Test(groups = "Functional")
public void testFindColour()
{
ColourSchemeI blosum = new Blosum62ColourScheme();
import static org.testng.AssertJUnit.assertNull;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
import jalview.datamodel.SequenceFeature;
import jalview.gui.JvOptionPane;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import junit.extensions.PA;
+
public class FeatureColourTest
{
assertTrue(fc1.getColour().equals(Color.RED));
assertFalse(fc1.isGraduatedColour());
assertFalse(fc1.isColourByLabel());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
/*
* min-max colour
assertTrue(fc1.isGraduatedColour());
assertFalse(fc1.isColourByLabel());
assertTrue(fc1.isAboveThreshold());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
+ assertEquals(12f, fc1.getThreshold());
+ assertEquals(Color.gray, fc1.getMinColour());
+ assertEquals(Color.black, fc1.getMaxColour());
+ assertEquals(Color.gray, fc1.getNoColour());
+ assertEquals(10f, fc1.getMin());
+ assertEquals(20f, fc1.getMax());
+
+ /*
+ * min-max-noValue colour
+ */
+ fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+ fc.setAboveThreshold(true);
+ fc.setThreshold(12f);
+ fc1 = new FeatureColour(fc);
+ assertTrue(fc1.isGraduatedColour());
+ assertFalse(fc1.isColourByLabel());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
+ assertTrue(fc1.isAboveThreshold());
assertEquals(12f, fc1.getThreshold());
assertEquals(Color.gray, fc1.getMinColour());
assertEquals(Color.black, fc1.getMaxColour());
+ assertEquals(Color.green, fc1.getNoColour());
assertEquals(10f, fc1.getMin());
assertEquals(20f, fc1.getMax());
fc1 = new FeatureColour(fc);
assertTrue(fc1.isColourByLabel());
assertFalse(fc1.isGraduatedColour());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
+
+ /*
+ * colour by attribute (label)
+ */
+ fc = new FeatureColour();
+ fc.setColourByLabel(true);
+ fc.setAttributeName("AF");
+ fc1 = new FeatureColour(fc);
+ assertTrue(fc1.isColourByLabel());
+ assertFalse(fc1.isGraduatedColour());
+ assertTrue(fc1.isColourByAttribute());
+ assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+
+ /*
+ * colour by attribute (value)
+ */
+ fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+ fc.setAboveThreshold(true);
+ fc.setThreshold(12f);
+ fc.setAttributeName("AF");
+ fc1 = new FeatureColour(fc);
+ assertTrue(fc1.isGraduatedColour());
+ assertFalse(fc1.isColourByLabel());
+ assertTrue(fc1.isColourByAttribute());
+ assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+ assertTrue(fc1.isAboveThreshold());
+ assertEquals(12f, fc1.getThreshold());
+ assertEquals(Color.gray, fc1.getMinColour());
+ assertEquals(Color.black, fc1.getMaxColour());
+ assertEquals(Color.green, fc1.getNoColour());
+ assertEquals(10f, fc1.getMin());
+ assertEquals(20f, fc1.getMax());
+ }
+
+ @Test(groups = { "Functional" })
+ public void testCopyConstructor_minMax()
+ {
+ /*
+ * graduated colour
+ */
+ FeatureColour fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+ assertTrue(fc.isGraduatedColour());
+ assertFalse(fc.isColourByLabel());
+ assertFalse(fc.isColourByAttribute());
+ assertNull(fc.getAttributeName());
+ assertEquals(1f, fc.getMin());
+ assertEquals(5f, fc.getMax());
+
+ /*
+ * update min-max bounds
+ */
+ FeatureColour fc1 = new FeatureColour(fc, 2f, 6f);
+ assertTrue(fc1.isGraduatedColour());
+ assertFalse(fc1.isColourByLabel());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
+ assertEquals(2f, fc1.getMin());
+ assertEquals(6f, fc1.getMax());
+ assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+ /*
+ * update min-max bounds - high to low
+ */
+ fc1 = new FeatureColour(fc, 23f, 16f);
+ assertTrue(fc1.isGraduatedColour());
+ assertFalse(fc1.isColourByLabel());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
+ assertEquals(23f, fc1.getMin());
+ assertEquals(16f, fc1.getMax());
+ assertTrue((boolean) PA.getValue(fc1, "isHighToLow"));
+
+ /*
+ * graduated colour by attribute
+ */
+ fc1.setAttributeName("AF");
+ fc1 = new FeatureColour(fc1, 13f, 36f);
+ assertTrue(fc1.isGraduatedColour());
+ assertFalse(fc1.isColourByLabel());
+ assertTrue(fc1.isColourByAttribute());
+ assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+ assertEquals(13f, fc1.getMin());
+ assertEquals(36f, fc1.getMax());
+ assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+ /*
+ * colour by label
+ */
+ fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+ fc.setColourByLabel(true);
+ assertFalse(fc.isGraduatedColour());
+ assertTrue(fc.isColourByLabel());
+ assertFalse(fc.isColourByAttribute());
+ assertNull(fc.getAttributeName());
+ assertEquals(1f, fc.getMin());
+ assertEquals(5f, fc.getMax());
+
+ /*
+ * update min-max bounds
+ */
+ fc1 = new FeatureColour(fc, 2f, 6f);
+ assertFalse(fc1.isGraduatedColour());
+ assertTrue(fc1.isColourByLabel());
+ assertFalse(fc1.isColourByAttribute());
+ assertNull(fc1.getAttributeName());
+ assertEquals(2f, fc1.getMin());
+ assertEquals(6f, fc1.getMax());
+
+ /*
+ * colour by attribute text
+ */
+ fc1.setAttributeName("AC");
+ fc1 = new FeatureColour(fc1, 13f, 36f);
+ assertFalse(fc1.isGraduatedColour());
+ assertTrue(fc1.isColourByLabel());
+ assertTrue(fc1.isColourByAttribute());
+ assertArrayEquals(new String[] { "AC" }, fc1.getAttributeName());
+ assertEquals(13f, fc1.getMin());
+ assertEquals(36f, fc1.getMax());
}
@Test(groups = { "Functional" })
@Test(groups = { "Functional" })
public void testGetColor_Graduated()
{
- // graduated colour from score 0 to 100, gray(128, 128, 128) to red(255, 0,
- // 0)
+ /*
+ * graduated colour from
+ * score 0 to 100
+ * gray(128, 128, 128) to red(255, 0, 0)
+ */
FeatureColour fc = new FeatureColour(Color.GRAY, Color.RED, 0f, 100f);
// feature score is 75 which is 3/4 of the way from GRAY to RED
SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
fc = FeatureColour.parseJalviewFeatureColour(descriptor);
assertTrue(fc.isGraduatedColour());
}
+
+ @Test(groups = { "Functional" })
+ public void testGetColor_colourByAttributeText()
+ {
+ FeatureColour fc = new FeatureColour();
+ fc.setColourByLabel(true);
+ fc.setAttributeName("consequence");
+ SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
+ null);
+
+ /*
+ * if feature has no such attribute, use 'no value' colour
+ */
+ assertEquals(FeatureColour.DEFAULT_NO_COLOUR, fc.getColor(sf));
+
+ /*
+ * if feature has attribute, generate colour from value
+ */
+ sf.setValue("consequence", "benign");
+ Color expected = ColorUtils.createColourFromName("benign");
+ assertEquals(expected, fc.getColor(sf));
+ }
+
+ @Test(groups = { "Functional" })
+ public void testGetColor_GraduatedByAttributeValue()
+ {
+ /*
+ * graduated colour based on attribute value for AF
+ * given a min-max range of 0-100
+ */
+ FeatureColour fc = new FeatureColour(new Color(50, 100, 150),
+ new Color(150, 200, 250), Color.yellow, 0f, 100f);
+ String attName = "AF";
+ fc.setAttributeName(attName);
+
+ /*
+ * first case: feature lacks the attribute - use 'no value' colour
+ */
+ SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
+ null);
+ assertEquals(Color.yellow, fc.getColor(sf));
+
+ /*
+ * second case: attribute present but not numeric - treat as if absent
+ */
+ sf.setValue(attName, "twelve");
+ assertEquals(Color.yellow, fc.getColor(sf));
+
+ /*
+ * third case: valid attribute value
+ */
+ sf.setValue(attName, "20.0");
+ Color expected = new Color(70, 120, 170);
+ assertEquals(expected, fc.getColor(sf));
+ }
}
@Test(groups = { "Functional" })
public void testGetRanges()
{
- List<int[]> ranges = new ArrayList<int[]>();
+ List<int[]> ranges = new ArrayList<>();
ranges.add(new int[] { 2, 3 });
ranges.add(new int[] { 5, 6 });
assertEquals("[2, 3, 5, 6]", Arrays.toString(MapList.getRanges(ranges)));
public void testAddRange()
{
int[] range = { 1, 5 };
- List<int[]> ranges = new ArrayList<int[]>();
+ List<int[]> ranges = new ArrayList<>();
// add to empty list:
MapList.addRange(range, ranges);
public void testCoalesceRanges()
{
assertNull(MapList.coalesceRanges(null));
- List<int[]> ranges = new ArrayList<int[]>();
+ List<int[]> ranges = new ArrayList<>();
assertSame(ranges, MapList.coalesceRanges(ranges));
ranges.add(new int[] { 1, 3 });
assertSame(ranges, MapList.coalesceRanges(ranges));
@Test(groups = { "Functional" })
public void testCoalesceRanges_withOverlap()
{
- List<int[]> ranges = new ArrayList<int[]>();
+ List<int[]> ranges = new ArrayList<>();
ranges.add(new int[] { 1, 3 });
ranges.add(new int[] { 2, 5 });
assertEquals(1, merged.size());
assertArrayEquals(new int[] { 9, 0 }, merged.get(0));
}
+
+ /**
+ * Test the method that compounds ('traverses') two mappings
+ */
+ @Test(groups = "Functional")
+ public void testTraverse()
+ {
+ /*
+ * simple 1:1 plus 1:1 forwards
+ */
+ MapList ml1 = new MapList(new int[] { 3, 4, 8, 12 }, new int[] { 5, 8,
+ 11, 13 }, 1, 1);
+ MapList ml2 = new MapList(new int[] { 1, 50 }, new int[] { 40, 45, 70,
+ 75, 90, 127 }, 1, 1);
+ MapList compound = ml1.traverse(ml2);
+
+ assertEquals(compound.getFromRatio(), 1);
+ assertEquals(compound.getToRatio(), 1);
+ List<int[]> fromRanges = compound.getFromRanges();
+ assertEquals(fromRanges.size(), 2);
+ assertArrayEquals(new int[] { 3, 4 }, fromRanges.get(0));
+ assertArrayEquals(new int[] { 8, 12 }, fromRanges.get(1));
+ List<int[]> toRanges = compound.getToRanges();
+ assertEquals(toRanges.size(), 2);
+ // 5-8 maps to 44-45,70-71
+ // 11-13 maps to 74-75,90
+ assertArrayEquals(new int[] { 44, 45, 70, 71 }, toRanges.get(0));
+ assertArrayEquals(new int[] { 74, 75, 90, 90 }, toRanges.get(1));
+
+ /*
+ * 1:1 over 1:1 backwards ('reverse strand')
+ */
+ ml1 = new MapList(new int[] { 1, 50 }, new int[] { 70, 119 }, 1, 1);
+ ml2 = new MapList(new int[] { 1, 500 },
+ new int[] { 1000, 901, 600, 201 }, 1, 1);
+ compound = ml1.traverse(ml2);
+
+ assertEquals(compound.getFromRatio(), 1);
+ assertEquals(compound.getToRatio(), 1);
+ fromRanges = compound.getFromRanges();
+ assertEquals(fromRanges.size(), 1);
+ assertArrayEquals(new int[] { 1, 50 }, fromRanges.get(0));
+ toRanges = compound.getToRanges();
+ assertEquals(toRanges.size(), 1);
+ assertArrayEquals(new int[] { 931, 901, 600, 582 }, toRanges.get(0));
+
+ /*
+ * 1:1 plus 1:3 should result in 1:3
+ */
+ ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 40 }, 1, 1);
+ ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 50, 91, 340 },
+ 1, 3);
+ compound = ml1.traverse(ml2);
+
+ assertEquals(compound.getFromRatio(), 1);
+ assertEquals(compound.getToRatio(), 3);
+ fromRanges = compound.getFromRanges();
+ assertEquals(fromRanges.size(), 1);
+ assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+ // 11-40 maps to 31-50,91-160
+ toRanges = compound.getToRanges();
+ assertEquals(toRanges.size(), 1);
+ assertArrayEquals(new int[] { 31, 50, 91, 160 }, toRanges.get(0));
+
+ /*
+ * 3:1 plus 1:1 should result in 3:1
+ */
+ ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 20 }, 3, 1);
+ ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 15, 91, 175 },
+ 1, 1);
+ compound = ml1.traverse(ml2);
+
+ assertEquals(compound.getFromRatio(), 3);
+ assertEquals(compound.getToRatio(), 1);
+ fromRanges = compound.getFromRanges();
+ assertEquals(fromRanges.size(), 1);
+ assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+ // 11-20 maps to 11-15, 91-95
+ toRanges = compound.getToRanges();
+ assertEquals(toRanges.size(), 1);
+ assertArrayEquals(new int[] { 11, 15, 91, 95 }, toRanges.get(0));
+
+ /*
+ * 1:3 plus 3:1 should result in 1:1
+ */
+ ml1 = new MapList(new int[] { 21, 40 }, new int[] { 13, 72 }, 1, 3);
+ ml2 = new MapList(new int[] { 1, 300 }, new int[] { 51, 70, 121, 200 },
+ 3, 1);
+ compound = ml1.traverse(ml2);
+
+ assertEquals(compound.getFromRatio(), 1);
+ assertEquals(compound.getToRatio(), 1);
+ fromRanges = compound.getFromRanges();
+ assertEquals(fromRanges.size(), 1);
+ assertArrayEquals(new int[] { 21, 40 }, fromRanges.get(0));
+ // 13-72 maps 3:1 to 55-70, 121-124
+ toRanges = compound.getToRanges();
+ assertEquals(toRanges.size(), 1);
+ assertArrayEquals(new int[] { 55, 70, 121, 124 }, toRanges.get(0));
+
+ /*
+ * 3:1 plus 1:3 should result in 1:1
+ */
+ ml1 = new MapList(new int[] { 31, 90 }, new int[] { 13, 32 }, 3, 1);
+ ml2 = new MapList(new int[] { 11, 40 }, new int[] { 41, 50, 71, 150 },
+ 1, 3);
+ compound = ml1.traverse(ml2);
+
+ assertEquals(compound.getFromRatio(), 1);
+ assertEquals(compound.getToRatio(), 1);
+ fromRanges = compound.getFromRanges();
+ assertEquals(fromRanges.size(), 1);
+ assertArrayEquals(new int[] { 31, 90 }, fromRanges.get(0));
+ // 13-32 maps to 47-50,71-126
+ toRanges = compound.getToRanges();
+ assertEquals(toRanges.size(), 1);
+ assertArrayEquals(new int[] { 47, 50, 71, 126 }, toRanges.get(0));
+
+ /*
+ * method returns null if not all regions are mapped through
+ */
+ ml1 = new MapList(new int[] { 1, 50 }, new int[] { 101, 150 }, 1, 1);
+ ml2 = new MapList(new int[] { 131, 180 }, new int[] { 201, 250 }, 1, 3);
+ compound = ml1.traverse(ml2);
+ assertNull(compound);
+ }
+
+ /**
+ * Test that method that inspects for the (first) forward or reverse 'to' range.
+ * Single position ranges are ignored.
+ */
+ @Test(groups = { "Functional" })
+ public void testIsToForwardsStrand()
+ {
+ // [3-9] declares forward strand
+ MapList ml = new MapList(new int[] { 20, 11 },
+ new int[]
+ { 2, 2, 3, 9, 12, 11 }, 1, 1);
+ assertTrue(ml.isToForwardStrand());
+
+ // [11-5] declares reverse strand ([13-14] is ignored)
+ ml = new MapList(new int[] { 20, 11 },
+ new int[]
+ { 2, 2, 11, 5, 13, 14 }, 1, 1);
+ assertFalse(ml.isToForwardStrand());
+
+ // all single position ranges - defaults to forward strand
+ ml = new MapList(new int[] { 3, 1 }, new int[] { 2, 2, 4, 4, 6, 6 }, 1,
+ 1);
+ assertTrue(ml.isToForwardStrand());
+ }
}
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()
{
--- /dev/null
+package jalview.util;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class MathUtilsTest
+{
+ @Test(groups = "Functional")
+ public void testGcd()
+ {
+ assertEquals(MathUtils.gcd(0, 0), 0);
+ assertEquals(MathUtils.gcd(0, 1), 1);
+ assertEquals(MathUtils.gcd(1, 0), 1);
+ assertEquals(MathUtils.gcd(1, 1), 1);
+ assertEquals(MathUtils.gcd(1, -1), 1);
+ assertEquals(MathUtils.gcd(-1, 1), 1);
+ assertEquals(MathUtils.gcd(2, 3), 1);
+ assertEquals(MathUtils.gcd(4, 2), 2);
+ assertEquals(MathUtils.gcd(2, 4), 2);
+ assertEquals(MathUtils.gcd(2, -4), 2);
+ assertEquals(MathUtils.gcd(-2, 4), 2);
+ assertEquals(MathUtils.gcd(-2, -4), 2);
+ assertEquals(MathUtils.gcd(2 * 3 * 5 * 7 * 11, 3 * 7 * 13 * 17), 3 * 7);
+ }
+}
assertEquals("", StringUtils.toSentenceCase(""));
assertNull(StringUtils.toSentenceCase(null));
}
+
+ @Test(groups = { "Functional" })
+ public void testStripHtmlTags()
+ {
+ assertNull(StringUtils.stripHtmlTags(null));
+ assertEquals("", StringUtils.stripHtmlTags(""));
+ assertEquals(
+ "<a href=\"something\">label</href>",
+ StringUtils
+ .stripHtmlTags("<html><a href=\"something\">label</href></html>"));
+
+ // if no "<html>" tag, < and > get html-encoded (not sure why)
+ assertEquals("<a href=\"something\">label</href>",
+ StringUtils.stripHtmlTags("<a href=\"something\">label</href>"));
+
+ // </body> gets removed but not <body> (is this intentional?)
+ assertEquals("<body><p>hello",
+ StringUtils.stripHtmlTags("<html><body><p>hello</body></html>"));
+
+ assertEquals("kdHydro < 12.53",
+ StringUtils.stripHtmlTags("kdHydro < 12.53"));
+ }
}
--- /dev/null
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class ConditionTest
+{
+ @Test(groups = "Functional")
+ public void testToString()
+ {
+ Locale.setDefault(Locale.UK);
+ assertEquals(Condition.Contains.toString(), "Contains");
+ assertEquals(Condition.NotContains.toString(), "Does not contain");
+ assertEquals(Condition.Matches.toString(), "Matches");
+ assertEquals(Condition.NotMatches.toString(), "Does not match");
+ assertEquals(Condition.Present.toString(), "Is present");
+ assertEquals(Condition.NotPresent.toString(), "Is not present");
+ assertEquals(Condition.LT.toString(), "<");
+ assertEquals(Condition.LE.toString(), "<=");
+ assertEquals(Condition.GT.toString(), ">");
+ assertEquals(Condition.GE.toString(), ">=");
+ assertEquals(Condition.EQ.toString(), "=");
+ assertEquals(Condition.NE.toString(), "not =");
+
+ /*
+ * repeat call to get coverage of value caching
+ */
+ assertEquals(Condition.NE.toString(), "not =");
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class MatcherTest
+{
+ @Test(groups = "Functional")
+ public void testConstructor()
+ {
+ MatcherI m = new Matcher(Condition.Contains, "foo");
+ assertEquals(m.getCondition(), Condition.Contains);
+ assertEquals(m.getPattern(), "foo");
+ assertEquals(PA.getValue(m, "uppercasePattern"), "FOO");
+ assertEquals(m.getFloatValue(), 0f);
+
+ m = new Matcher(Condition.GT, -2.1f);
+ assertEquals(m.getCondition(), Condition.GT);
+ assertEquals(m.getPattern(), "-2.1");
+ assertEquals(m.getFloatValue(), -2.1f);
+
+ m = new Matcher(Condition.NotContains, "-1.2f");
+ assertEquals(m.getCondition(), Condition.NotContains);
+ assertEquals(m.getPattern(), "-1.2f");
+ assertEquals(m.getFloatValue(), 0f);
+
+ m = new Matcher(Condition.GE, "-1.2f");
+ assertEquals(m.getCondition(), Condition.GE);
+ assertEquals(m.getPattern(), "-1.2");
+ assertEquals(m.getFloatValue(), -1.2f);
+
+ try
+ {
+ new Matcher(null, 0f);
+ fail("Expected exception");
+ } catch (NullPointerException e)
+ {
+ // expected
+ }
+
+ try
+ {
+ new Matcher(Condition.LT, "123,456");
+ fail("Expected exception");
+ } catch (NumberFormatException e)
+ {
+ // expected
+ }
+ }
+
+ /**
+ * Tests for float comparison conditions
+ */
+ @Test(groups = "Functional")
+ public void testMatches_float()
+ {
+ /*
+ * EQUALS test
+ */
+ MatcherI m = new Matcher(Condition.EQ, 2f);
+ assertTrue(m.matches("2"));
+ assertTrue(m.matches("2.0"));
+ assertFalse(m.matches("2.01"));
+
+ /*
+ * NOT EQUALS test
+ */
+ m = new Matcher(Condition.NE, 2f);
+ assertFalse(m.matches("2"));
+ assertFalse(m.matches("2.0"));
+ assertTrue(m.matches("2.01"));
+
+ /*
+ * >= test
+ */
+ m = new Matcher(Condition.GE, 2f);
+ assertTrue(m.matches("2"));
+ assertTrue(m.matches("2.1"));
+ assertFalse(m.matches("1.9"));
+
+ /*
+ * > test
+ */
+ m = new Matcher(Condition.GT, 2f);
+ assertFalse(m.matches("2"));
+ assertTrue(m.matches("2.1"));
+ assertFalse(m.matches("1.9"));
+
+ /*
+ * <= test
+ */
+ m = new Matcher(Condition.LE, 2f);
+ assertTrue(m.matches("2"));
+ assertFalse(m.matches("2.1"));
+ assertTrue(m.matches("1.9"));
+
+ /*
+ * < test
+ */
+ m = new Matcher(Condition.LT, 2f);
+ assertFalse(m.matches("2"));
+ assertFalse(m.matches("2.1"));
+ assertTrue(m.matches("1.9"));
+ }
+
+ @Test(groups = "Functional")
+ public void testMatches_floatNullOrInvalid()
+ {
+ for (Condition cond : Condition.values())
+ {
+ if (cond.isNumeric())
+ {
+ MatcherI m = new Matcher(cond, 2f);
+ assertFalse(m.matches(null));
+ assertFalse(m.matches(""));
+ assertFalse(m.matches("two"));
+ }
+ }
+ }
+
+ /**
+ * Tests for string comparison conditions
+ */
+ @Test(groups = "Functional")
+ public void testMatches_pattern()
+ {
+ /*
+ * Contains
+ */
+ MatcherI m = new Matcher(Condition.Contains, "benign");
+ assertTrue(m.matches("benign"));
+ assertTrue(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+ assertFalse(m.matches("pathogenic"));
+ assertFalse(m.matches(null));
+
+ /*
+ * does not contain
+ */
+ m = new Matcher(Condition.NotContains, "benign");
+ assertFalse(m.matches("benign"));
+ assertFalse(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+ assertTrue(m.matches("pathogenic"));
+ assertTrue(m.matches(null)); // null value passes this condition
+
+ /*
+ * matches
+ */
+ m = new Matcher(Condition.Matches, "benign");
+ assertTrue(m.matches("benign"));
+ assertTrue(m.matches(" Benign ")); // trim before testing
+ assertFalse(m.matches("MOSTLY BENIGN"));
+ assertFalse(m.matches("pathogenic"));
+ assertFalse(m.matches(null));
+
+ /*
+ * does not match
+ */
+ m = new Matcher(Condition.NotMatches, "benign");
+ assertFalse(m.matches("benign"));
+ assertFalse(m.matches(" Benign ")); // trim before testing
+ assertTrue(m.matches("MOSTLY BENIGN"));
+ assertTrue(m.matches("pathogenic"));
+ assertTrue(m.matches(null));
+
+ /*
+ * value is present (is not null)
+ */
+ m = new Matcher(Condition.Present, null);
+ assertTrue(m.matches("benign"));
+ assertTrue(m.matches(""));
+ assertFalse(m.matches(null));
+
+ /*
+ * value is not present (is null)
+ */
+ m = new Matcher(Condition.NotPresent, null);
+ assertFalse(m.matches("benign"));
+ assertFalse(m.matches(""));
+ assertTrue(m.matches(null));
+
+ /*
+ * a float with a string match condition will be treated as string
+ */
+ Matcher m1 = new Matcher(Condition.Contains, "32");
+ assertFalse(m1.matches(-203f));
+ assertTrue(m1.matches(-4321.0f));
+ }
+
+ /**
+ * If a float is passed with a string condition it gets converted to a string
+ */
+ @Test(groups = "Functional")
+ public void testMatches_floatWithStringCondition()
+ {
+ MatcherI m = new Matcher(Condition.Contains, 1.2e-6f);
+ assertTrue(m.matches("1.2e-6"));
+
+ m = new Matcher(Condition.Contains, 0.0000001f);
+ assertTrue(m.matches("1.0e-7"));
+ assertTrue(m.matches("1.0E-7"));
+ assertFalse(m.matches("0.0000001f"));
+ }
+
+ @Test(groups = "Functional")
+ public void testToString()
+ {
+ Locale.setDefault(Locale.ENGLISH);
+
+ MatcherI m = new Matcher(Condition.LT, 1.2e-6f);
+ assertEquals(m.toString(), "< 1.2E-6");
+
+ m = new Matcher(Condition.NotMatches, "ABC");
+ assertEquals(m.toString(), "Does not match 'ABC'");
+
+ m = new Matcher(Condition.Contains, -1.2f);
+ assertEquals(m.toString(), "Contains '-1.2'");
+ }
+
+ @Test(groups = "Functional")
+ public void testEquals()
+ {
+ /*
+ * string condition
+ */
+ MatcherI m = new Matcher(Condition.NotMatches, "ABC");
+ assertFalse(m.equals(null));
+ assertFalse(m.equals("foo"));
+ assertTrue(m.equals(m));
+ assertTrue(m.equals(new Matcher(Condition.NotMatches, "ABC")));
+ // not case-sensitive:
+ assertTrue(m.equals(new Matcher(Condition.NotMatches, "abc")));
+ assertFalse(m.equals(new Matcher(Condition.Matches, "ABC")));
+ assertFalse(m.equals(new Matcher(Condition.NotMatches, "def")));
+
+ /*
+ * numeric conditions
+ */
+ m = new Matcher(Condition.LT, -1f);
+ assertFalse(m.equals(null));
+ assertFalse(m.equals("foo"));
+ assertTrue(m.equals(m));
+ assertTrue(m.equals(new Matcher(Condition.LT, -1f)));
+ assertTrue(m.equals(new Matcher(Condition.LT, "-1f")));
+ assertTrue(m.equals(new Matcher(Condition.LT, "-1.00f")));
+ assertFalse(m.equals(new Matcher(Condition.LE, -1f)));
+ assertFalse(m.equals(new Matcher(Condition.GE, -1f)));
+ assertFalse(m.equals(new Matcher(Condition.NE, -1f)));
+ assertFalse(m.equals(new Matcher(Condition.LT, 1f)));
+ assertFalse(m.equals(new Matcher(Condition.LT, -1.1f)));
+ }
+
+ @Test(groups = "Functional")
+ public void testHashCode()
+ {
+ MatcherI m1 = new Matcher(Condition.NotMatches, "ABC");
+ MatcherI m2 = new Matcher(Condition.NotMatches, "ABC");
+ MatcherI m3 = new Matcher(Condition.NotMatches, "AB");
+ MatcherI m4 = new Matcher(Condition.Matches, "ABC");
+ assertEquals(m1.hashCode(), m2.hashCode());
+ assertNotEquals(m1.hashCode(), m3.hashCode());
+ assertNotEquals(m1.hashCode(), m4.hashCode());
+ assertNotEquals(m3.hashCode(), m4.hashCode());
+ }
+}
import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
+import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
private int javaCount;
- private HashSet<String> invalidKeys;
+ private Set<String> invalidKeys;
+
+ private Set<String> dynamicKeys;
/**
* Runs the scan given the path to the root of Java source directories
private void doMain(String srcPath) throws IOException
{
System.out.println("Scanning " + srcPath
- + " for calls to MessageManager");
+ + " for calls to MessageManager\n");
sourcePath = srcPath;
loadMessages();
File dir = new File(srcPath);
System.out.println(srcPath + " not found");
return;
}
- invalidKeys = new HashSet<String>();
+
+ invalidKeys = new HashSet<>();
+ dynamicKeys = new HashSet<>();
+
if (dir.isDirectory())
{
scanDirectory(dir);
private void reportResults()
{
System.out.println("\nScanned " + javaCount + " source files");
- System.out.println("Message.properties has " + messages.size()
+ System.out.println(
+ "Messages.properties has " + messages.size()
+ " keys");
- System.out.println("Found " + invalidKeys.size()
- + " possibly invalid parameter calls");
+ if (!invalidKeys.isEmpty())
+ {
+ System.out.println("Found " + invalidKeys.size()
+ + " possibly invalid parameter call"
+ + (invalidKeys.size() > 1 ? "s" : ""));
+ }
- System.out.println(messageKeys.size()
- + " keys not found, either unused or constructed dynamically");
+ System.out.println("Keys not found, assumed constructed dynamically:");
+ int dynamicCount = 0;
for (String key : messageKeys)
{
- System.out.println(" " + key);
+ if (isDynamic(key))
+ {
+ System.out.println(" " + key);
+ dynamicCount++;
+ }
+ }
+
+ if (dynamicCount < messageKeys.size())
+ {
+ System.out.println((messageKeys.size() - dynamicCount)
+ + " keys not found, possibly unused");
+ for (String key : messageKeys)
+ {
+ if (!isDynamic(key))
+ {
+ System.out.println(" " + key);
+ }
+ }
+ }
+ System.out
+ .println("(Run i18nAnt.xml to compare other message bundles)");
+ }
+
+ /**
+ * Answers true if the key starts with one of the recorded dynamic key stubs,
+ * else false
+ *
+ * @param key
+ * @return
+ */
+ private boolean isDynamic(String key)
+ {
+ for (String dynamic : dynamicKeys)
+ {
+ if (key.startsWith(dynamic))
+ {
+ return true;
+ }
}
+ return false;
}
/**
continue;
}
+ String messageKey = getMessageKey(method, methodArgs);
+
if (METHOD3 == method)
{
System.out.println(String.format("Dynamic key at %s line %s %s",
path.substring(sourcePath.length()), lineNos, line));
+ String key = messageKey.substring(1, messageKey.length() - 1);
+ dynamicKeys.add(key);
continue;
}
- String messageKey = getMessageKey(method, methodArgs);
if (messageKey == null)
{
System.out.println(String.format("Trouble parsing %s line %s %s",
messages.load(reader);
reader.close();
- messageKeys = new TreeSet<String>();
+ messageKeys = new TreeSet<>();
for (Object key : messages.keySet())
{
messageKeys.add((String) key);