From: gmungoc Date: Fri, 2 Feb 2018 15:55:11 +0000 (+0000) Subject: Merge branch 'feature/JAL-984splitAdjuster' into features/JAL-1793VCF X-Git-Tag: Release_2_11_0~63 X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=537da9c65518c52985308b67c499fe6a60a607ce;hp=c1abd9108f6a78bc4e900746ba2a4f3307a218fc;p=jalview.git Merge branch 'feature/JAL-984splitAdjuster' into features/JAL-1793VCF --- diff --git a/.classpath b/.classpath index d704f10..c85feaf 100644 --- a/.classpath +++ b/.classpath @@ -48,11 +48,9 @@ - - @@ -68,6 +66,8 @@ + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 8a5e7a7..5908bb2 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,15 +1,15 @@ 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 diff --git a/build.xml b/build.xml index 4931cfb..89c3d24 100755 --- a/build.xml +++ b/build.xml @@ -106,8 +106,8 @@ - - + + @@ -425,7 +425,7 @@ - + @@ -451,10 +451,9 @@ - - j2se version="1.7+" - - + + j2se version="1.8+" + @@ -624,7 +623,7 @@ - + diff --git a/examples/exampleFeatures.txt b/examples/exampleFeatures.txt index 9e65534..99af214 100755 --- a/examples/exampleFeatures.txt +++ b/examples/exampleFeatures.txt @@ -26,6 +26,11 @@ BETA-TURN-IIL 8b5b50 ST-MOTIF ac25a1 kdHydrophobicity ccffcc|333300|-3.9|4.5|above|-2.0 +STARTFILTERS +GAMMA-TURN-INVERSE Label Contains PDB +kdHydrophobicity (Score LT 1.5) OR (Score GE 2.8) +ENDFILTERS + STARTGROUP uniprot Pfam family FER_CAPAA -1 0 0 Pfam Iron-sulfur (2Fe-2S) FER_CAPAA -1 39 39 METAL diff --git a/lib/htsjdk-2.12.0.jar b/lib/htsjdk-2.12.0.jar new file mode 100644 index 0000000..1df12b2 Binary files /dev/null and b/lib/htsjdk-2.12.0.jar differ diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index f526699..3f5aa94 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -242,7 +242,6 @@ label.documentation = Documentation label.about = About... label.show_sequence_limits = Show Sequence Limits action.feature_settings = Feature Settings... -label.feature_settings = Feature Settings label.all_columns = All Columns label.all_sequences = All Sequences label.selected_columns = Selected Columns @@ -274,6 +273,7 @@ label.chimera_missing = Chimera structure viewer not found.
Please enter the label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure label.min_colour = Minimum Colour label.max_colour = Maximum Colour +label.no_colour = No Colour label.use_original_colours = Use Original Colours label.threshold_minmax = Threshold is min/max label.represent_group_with = Represent Group with {0} @@ -281,9 +281,9 @@ label.selection = Selection label.group_colour = Group Colour label.sequence = Sequence label.view_pdb_structure = View PDB Structure -label.min = Min: -label.max = Max: -label.colour_by_label = Colour by label +label.min_value = Min value +label.max_value = Max value +label.no_value = No value label.new_feature = New Feature label.match_case = Match Case label.view_alignment_editor = View in alignment editor @@ -368,6 +368,8 @@ label.optimise_order = Optimise Order label.seq_sort_by_score = Sequence sort by Score label.load_colours = Load Colours label.save_colours = Save Colours +label.load_colours_tooltip = Load feature colours and filters from file +label.save_colours_tooltip = Save feature colours and filters to file label.fetch_das_features = Fetch DAS Features label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} label.database_param = Database: {0} @@ -490,6 +492,10 @@ label.settings_for_type = Settings for {0} label.view_full_application = View in Full Application label.load_associated_tree = Load Associated Tree... label.load_features_annotations = Load Features/Annotations... +label.load_vcf = Load SNP variants from plain text or indexed VCF data +label.load_vcf_file = Load VCF File +label.searching_vcf = Loading VCF variants... +label.added_vcf = Added {0} VCF variants to {1} sequence(s) label.export_features = Export Features... label.export_annotations = Export Annotations... label.to_upper_case = To Upper Case @@ -528,7 +534,6 @@ label.threshold_feature_above_threshold = Above Threshold label.threshold_feature_below_threshold = Below Threshold label.adjust_threshold = Adjust threshold label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold. -label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features) label.select_colour_minimum_value = Select Colour for Minimum Value label.select_colour_maximum_value = Select Colour for Maximum Value label.open_url_param = Open URL {0} @@ -780,7 +785,7 @@ label.pairwise_aligned_sequences = Pairwise Aligned Sequences label.original_data_for_params = Original Data for {0} label.points_for_params = Points for {0} label.transformed_points_for_params = Transformed points for {0} -label.graduated_color_for_params = Graduated Feature Colour for {0} +label.variable_color_for = Variable Feature Colour for {0} label.select_background_colour = Select Background Colour label.invalid_font = Invalid Font label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";" @@ -867,7 +872,7 @@ label.msa_service_is_unknown = The Multiple Sequence Alignment Service named {0} label.service_called_is_not_seq_search_service = The Service called \n{0}\nis not a \nSequence Search Service\! label.seq_search_service_is_unknown = The Sequence Search Service named {0} is unknown label.feature_type = Feature Type -label.display = Display +label.show = Show label.service_url = Service URL label.copied_sequences = Copied sequences label.cut_sequences = Cut Sequences @@ -1320,9 +1325,41 @@ label.select_hidden_colour = Select hidden colour label.overview = Overview label.reset_to_defaults = Reset to defaults label.oview_calc = Recalculating overview... +label.feature_details = Feature details +label.matchCondition_contains = Contains +label.matchCondition_notcontains = Does not contain +label.matchCondition_matches = Matches +label.matchCondition_notmatches = Does not match +label.matchCondition_present = Is present +label.matchCondition_notpresent = Is not present +label.matchCondition_eq = = +label.matchCondition_ne = not = +label.matchCondition_lt = < +label.matchCondition_le = <= +label.matchCondition_gt = > +label.matchCondition_ge = >= +label.numeric_required = The value should be numeric +label.filter = Filter +label.filters = Filters +label.join_conditions = Join conditions with +label.score = Score +label.colour_by_label = Colour by label +label.variable_colour = Variable colour... +label.select_colour = Select colour option.enable_disable_autosearch = When ticked, search is performed automatically option.autosearch = Autosearch label.retrieve_ids = Retrieve IDs +label.display_settings_for = Display settings for {0} features +label.simple = Simple +label.simple_colour = Simple Colour +label.colour_by_text = Colour by text +label.graduated_colour = Graduated Colour +label.by_text_of = By text of +label.by_range_of = By range of +label.filters_tooltip = Click to set or amend filters +label.or = Or +label.and = And +label.sequence_feature_colours = Sequence Feature Colours label.best_quality = Best Quality label.best_resolution = Best Resolution label.most_protein_chain = Most Protein Chain diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index 77f053e..e42d6b8 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -226,7 +226,6 @@ label.automatic_scrolling = Desplazamiento autom label.documentation = Documentación label.about = Acerca de... label.show_sequence_limits = Mostrar los límites de la secuencia -label.feature_settings = Ajustar funciones... label.all_columns = Todas las columnas label.all_sequences = Todas las secuencias label.selected_columns = Columnas seleccionadas @@ -243,6 +242,7 @@ label.apply_all_groups = Aplicar a todos los grupos label.autocalculated_annotation = Anotación autocalculada label.min_colour = Color mínimo label.max_colour = Color máximo +label.no_colour = Sin color label.use_original_colours = Usar colores originales label.threshold_minmax = El umbral es mín/máx label.represent_group_with = Representar al grupo con @@ -250,8 +250,9 @@ label.selection = Seleccionar label.group_colour = Color del grupo label.sequence = Secuencia label.view_pdb_structure = Ver estructura PDB -label.min = Mín: -label.max = Máx: +label.max_value = Valor máximo +label.min_value = Valor mínimo +label.no_value = Sin valor label.colour_by_label = Color por etiquetas label.new_feature = Nueva función label.match_case = Hacer corresponder mayúsculas y minúsculas @@ -336,6 +337,8 @@ label.optimise_order = Optimizar orden label.seq_sort_by_score = Ordenar las secuencias por puntuación label.load_colours = Cargar colores label.save_colours = Guardar colores +label.load_colours_tooltip = Cargar colores y filtros desde fichero +label.save_colours_tooltip = Guardar colores y filtros en fichero label.fetch_das_features = Recuperar funciones DAS label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para buscar de {2} label.database_param = Base de datos: {0} @@ -456,6 +459,10 @@ label.settings_for_type = Ajustes para {0} label.view_full_application = Ver en la aplicación completa label.load_associated_tree = Cargar árbol asociado ... label.load_features_annotations = Cargar características/anotaciones ... +label.load_vcf = Cargar variantes SNP desde fichero VCF texto o tab-indexado +label.load_vcf_file = Cargar fichero VCF +label.searching_vcf = Cargando variantes VCF... +label.added_vcf= {0} variantes VCF añadidas a {1} secuencia(s) label.export_features = Exportar características... label.export_annotations = Exportar anotaciones ... label.to_upper_case = Pasar a mayúsculas @@ -489,7 +496,6 @@ label.threshold_feature_above_threshold = Por encima del umbral label.threshold_feature_below_threshold = Por debajo del umbral label.adjust_threshold = Ajustar umbral label.toggle_absolute_relative_display_threshold = Cambiar entre mostrar el umbral absoluto y el relativo. -label.display_features_same_type_different_label_using_different_colour = Mostrar las características del mismo tipo con una etiqueta diferente y empleando un color distinto (p.e. características del dominio) label.select_colour_minimum_value = Seleccionar el color para el valor mínimo label.select_colour_maximum_value = Seleccionar el color para el valor máximo label.open_url_param = Abrir URL {0} @@ -709,7 +715,7 @@ label.pairwise_aligned_sequences = Secuencias alineadas a pares label.original_data_for_params = Datos originales de {0} label.points_for_params = Puntos de {0} label.transformed_points_for_params = Puntos transformados de {0} -label.graduated_color_for_params = Color graduado para la característica de {0} +label.variable_color_for = Color variable para la característica de {0} label.select_background_colour = Seleccionar color de fondo label.invalid_font = Fuente no válida label.separate_multiple_accession_ids = Separar los accession id con un punto y coma ";" @@ -792,7 +798,7 @@ label.msa_service_is_unknown = El Servicio de Alineamiento M label.service_called_is_not_seq_search_service = El Servicio llamando \n{0}\nno es un \nServicio de B\u00FAsqueda de Secuencias\! label.seq_search_service_is_unknown = El Servicio de Búsqueda de Sencuencias llamado {0} es desconocido label.feature_type = Tipo de característisca -label.display = Representación +label.show = Mostrar label.service_url = URL del servicio label.copied_sequences = Secuencias copiadas label.cut_sequences = Cortar secuencias @@ -1320,9 +1326,41 @@ label.select_hidden_colour = Seleccionar color de las regiones ocultas label.overview = Resumen label.reset_to_defaults = Restablecen a los predeterminados label.oview_calc = Recalculando resumen +label.feature_details = Detalles de característica +label.matchCondition_contains = Contiene +label.matchCondition_notcontains = No contiene +label.matchCondition_matches = Es igual a +label.matchCondition_notmatches = No es igual a +label.matchCondition_present = Está presente +label.matchCondition_notpresent = No está presente +label.matchCondition_eq = = +label.matchCondition_ne = not = +label.matchCondition_lt = < +label.matchCondition_le = <= +label.matchCondition_gt = > +label.matchCondition_ge = >= +label.numeric_required = Valor numérico requerido +label.filter = Filtro +label.filters = Filtros +label.join_conditions = Combinar condiciones con +label.score = Puntuación +label.colour_by_label = Colorear por texto +label.variable_colour = Color variable... +label.select_colour = Seleccionar color option.enable_disable_autosearch = Marcar para buscar automáticamente option.autosearch = Auto búsqueda label.retrieve_ids = Recuperar IDs +label.display_settings_for = Visualización de características {0} +label.simple = Simple +label.simple_colour = Color simple +label.colour_by_text = Colorear por texto +label.graduated_colour = Color graduado +label.by_text_of = Por texto de +label.by_range_of = Por rango de +label.filters_tooltip = Haga clic para configurar o modificar los filtros +label.or = O +label.and = Y +label.sequence_feature_colours = Colores de características de las secuencias label.best_quality = Mejor Calidad label.best_resolution = Mejor Resolución label.most_protein_chain = Más Cadena de Proteína diff --git a/schemas/JalviewUserColours.xsd b/schemas/JalviewUserColours.xsd index bd43e9d..3934d66 100755 --- a/schemas/JalviewUserColours.xsd +++ b/schemas/JalviewUserColours.xsd @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with Jalview. If not, see . --> - - + @@ -29,13 +28,29 @@ - + + + + name of feature attribute to colour by, or attribute and sub-attribute + + + + + + Single letter residue code for an alignment colour scheme, or feature type for a feature colour scheme + + - - - loosely specified enumeration: NONE,ABOVE, or BELOW - + + + + + + + + + @@ -44,7 +59,68 @@ + + + + + + + + + + + + A feature match condition, which may be simple or compound + + + + + + + + + + + If true, matchers are AND-ed, if false they are OR-ed + + + + + + + + + + + + name of feature attribute to filter on, or attribute and sub-attribute + + + + + + + + + + + + + + + + + + + Graduated feature colour if no score (or attribute) value + + + + + + + diff --git a/schemas/jalview.xsd b/schemas/jalview.xsd index f0bd638..48824e7 100755 --- a/schemas/jalview.xsd +++ b/schemas/jalview.xsd @@ -500,6 +500,18 @@ + + + + name of feature attribute to colour by, or attribute and sub-attribute + + + + + optional filter(s) applied to the feature type + + + + @@ -559,6 +572,11 @@ + + + key2 may be used for a sub-attribute of key + + diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index 90d9197..bdc5fc1 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -29,6 +29,7 @@ import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefEntry; +import jalview.datamodel.GeneLociI; import jalview.datamodel.IncompleteCodonException; import jalview.datamodel.Mapping; import jalview.datamodel.Sequence; @@ -36,6 +37,7 @@ import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.datamodel.features.SequenceFeatures; +import jalview.io.gff.Gff3Helper; import jalview.io.gff.SequenceOntologyI; import jalview.schemes.ResidueProperties; import jalview.util.Comparison; @@ -105,6 +107,15 @@ public class AlignmentUtils { return variant == null ? null : variant.getFeatureGroup(); } + + /** + * toString for aid in the debugger only + */ + @Override + public String toString() + { + return base + ":" + (variant == null ? "" : variant.getDescription()); + } } /** @@ -117,7 +128,7 @@ public class AlignmentUtils */ public static AlignmentI expandContext(AlignmentI core, int flankSize) { - List sq = new ArrayList(); + List sq = new ArrayList<>(); int maxoffset = 0; for (SequenceI s : core.getSequences()) { @@ -247,7 +258,7 @@ public class AlignmentUtils public static Map> getSequencesByName( AlignmentI al) { - Map> theMap = new LinkedHashMap>(); + Map> theMap = new LinkedHashMap<>(); for (SequenceI seq : al.getSequences()) { String name = seq.getName(); @@ -256,7 +267,7 @@ public class AlignmentUtils List seqs = theMap.get(name); if (seqs == null) { - seqs = new ArrayList(); + seqs = new ArrayList<>(); theMap.put(name, seqs); } seqs.add(seq); @@ -283,8 +294,8 @@ public class AlignmentUtils return false; } - Set mappedDna = new HashSet(); - Set mappedProtein = new HashSet(); + Set mappedDna = new HashSet<>(); + Set mappedProtein = new HashSet<>(); /* * First pass - map sequences where cross-references exist. This include @@ -384,7 +395,7 @@ public class AlignmentUtils * Answers true if the mappings include one between the given (dataset) * sequences. */ - public static boolean mappingExists(List mappings, + protected static boolean mappingExists(List mappings, SequenceI aaSeq, SequenceI cdnaSeq) { if (mappings != null) @@ -454,7 +465,7 @@ public class AlignmentUtils { String lastCodon = String.valueOf(cdnaSeqChars, cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase(); - for (String stop : ResidueProperties.STOP) + for (String stop : ResidueProperties.STOP_CODONS) { if (lastCodon.equals(stop)) { @@ -525,7 +536,8 @@ public class AlignmentUtils * allow * in protein to match untranslatable in dna */ final char aaRes = aaSeqChars[aaPos]; - if ((translated == null || "STOP".equals(translated)) && aaRes == '*') + if ((translated == null || ResidueProperties.STOP.equals(translated)) + && aaRes == '*') { continue; } @@ -557,7 +569,8 @@ public class AlignmentUtils if (dnaPos == cdnaSeqChars.length - CODON_LENGTH) { String codon = String.valueOf(cdnaSeqChars, dnaPos, CODON_LENGTH); - if ("STOP".equals(ResidueProperties.codonTranslate(codon))) + if (ResidueProperties.STOP + .equals(ResidueProperties.codonTranslate(codon))) { return true; } @@ -870,7 +883,7 @@ public class AlignmentUtils System.err.println("Wrong alignment type in alignProteinAsDna"); return 0; } - List unmappedProtein = new ArrayList(); + List unmappedProtein = new ArrayList<>(); Map> alignedCodons = buildCodonColumnsMap( protein, dna, unmappedProtein); return alignProteinAs(protein, alignedCodons, unmappedProtein); @@ -1081,7 +1094,7 @@ public class AlignmentUtils * {dnaSequence, {proteinSequence, codonProduct}} at that position. The * comparator keeps the codon positions ordered. */ - Map> alignedCodons = new TreeMap>( + Map> alignedCodons = new TreeMap<>( new CodonComparator()); for (SequenceI dnaSeq : dna.getSequences()) @@ -1127,9 +1140,9 @@ public class AlignmentUtils // TODO delete this ugly hack once JAL-2022 is resolved // i.e. we can model startPhase > 0 (incomplete start codon) - List sequencesChecked = new ArrayList(); + List sequencesChecked = new ArrayList<>(); AlignedCodon lastCodon = null; - Map toAdd = new HashMap(); + Map toAdd = new HashMap<>(); for (Entry> entry : alignedCodons .entrySet()) @@ -1308,7 +1321,7 @@ public class AlignmentUtils Map seqProduct = alignedCodons.get(codon); if (seqProduct == null) { - seqProduct = new HashMap(); + seqProduct = new HashMap<>(); alignedCodons.put(codon, seqProduct); } seqProduct.put(protein, codon); @@ -1445,7 +1458,7 @@ public class AlignmentUtils { continue; } - final List result = new ArrayList(); + final List result = new ArrayList<>(); for (AlignmentAnnotation dsann : datasetAnnotations) { /* @@ -1627,17 +1640,17 @@ public class AlignmentUtils throw new IllegalArgumentException( "IMPLEMENTATION ERROR: dataset.getDataset() must be null!"); } - List foundSeqs = new ArrayList(); - List cdsSeqs = new ArrayList(); + List foundSeqs = new ArrayList<>(); + List cdsSeqs = new ArrayList<>(); List mappings = dataset.getCodonFrames(); HashSet productSeqs = null; if (products != null) { - productSeqs = new HashSet(); + productSeqs = new HashSet<>(); for (SequenceI seq : products) { - productSeqs.add(seq.getDatasetSequence() == null ? seq - : seq.getDatasetSequence()); + productSeqs.add(seq.getDatasetSequence() == null ? seq : seq + .getDatasetSequence()); } } @@ -1730,9 +1743,8 @@ public class AlignmentUtils /* * add a mapping from CDS to the (unchanged) mapped to range */ - List cdsRange = Collections - .singletonList(new int[] - { 1, cdsSeq.getLength() }); + List cdsRange = Collections.singletonList(new int[] { 1, + cdsSeq.getLength() }); MapList cdsToProteinMap = new MapList(cdsRange, mapList.getToRanges(), mapList.getFromRatio(), mapList.getToRatio()); @@ -1754,7 +1766,7 @@ public class AlignmentUtils * add another mapping from original 'from' range to CDS */ AlignedCodonFrame dnaToCdsMapping = new AlignedCodonFrame(); - MapList dnaToCdsMap = new MapList(mapList.getFromRanges(), + final MapList dnaToCdsMap = new MapList(mapList.getFromRanges(), cdsRange, 1, 1); dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeqDss, dnaToCdsMap); @@ -1764,6 +1776,13 @@ public class AlignmentUtils } /* + * transfer dna chromosomal loci (if known) to the CDS + * sequence (via the mapping) + */ + final MapList cdsToDnaMap = dnaToCdsMap.getInverse(); + transferGeneLoci(dnaSeq, cdsToDnaMap, cdsSeq); + + /* * add DBRef with mapping from protein to CDS * (this enables Get Cross-References from protein alignment) * This is tricky because we can't have two DBRefs with the @@ -1782,26 +1801,30 @@ public class AlignmentUtils for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs()) { - // creates a complementary cross-reference to the source sequence's - // primary reference. - - DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(), - primRef.getSource() + ":" + primRef.getVersion(), - primRef.getAccessionId()); - cdsCrossRef - .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap))); + /* + * create a cross-reference from CDS to the source sequence's + * primary reference and vice versa + */ + String source = primRef.getSource(); + String version = primRef.getVersion(); + DBRefEntry cdsCrossRef = new DBRefEntry(source, source + ":" + + version, primRef.getAccessionId()); + cdsCrossRef.setMap(new Mapping(dnaDss, new MapList(cdsToDnaMap))); cdsSeqDss.addDBRef(cdsCrossRef); + dnaSeq.addDBRef(new DBRefEntry(source, version, cdsSeq + .getName(), new Mapping(cdsSeqDss, dnaToCdsMap))); + // problem here is that the cross-reference is synthesized - // cdsSeq.getName() may be like 'CDS|dnaaccession' or // 'CDS|emblcdsacc' // assuming cds version same as dna ?!? - DBRefEntry proteinToCdsRef = new DBRefEntry(primRef.getSource(), - primRef.getVersion(), cdsSeq.getName()); + DBRefEntry proteinToCdsRef = new DBRefEntry(source, version, + cdsSeq.getName()); // - proteinToCdsRef.setMap( - new Mapping(cdsSeqDss, cdsToProteinMap.getInverse())); + proteinToCdsRef.setMap(new Mapping(cdsSeqDss, cdsToProteinMap + .getInverse())); proteinProduct.addDBRef(proteinToCdsRef); } @@ -1814,14 +1837,46 @@ public class AlignmentUtils } } - AlignmentI cds = new Alignment( - cdsSeqs.toArray(new SequenceI[cdsSeqs.size()])); + AlignmentI cds = new Alignment(cdsSeqs.toArray(new SequenceI[cdsSeqs + .size()])); cds.setDataset(dataset); return cds; } /** + * Tries to transfer gene loci (dbref to chromosome positions) from fromSeq to + * toSeq, mediated by the given mapping between the sequences + * + * @param fromSeq + * @param targetToFrom + * Map + * @param targetSeq + */ + protected static void transferGeneLoci(SequenceI fromSeq, + MapList targetToFrom, SequenceI targetSeq) + { + if (targetSeq.getGeneLoci() != null) + { + // already have - don't override + return; + } + GeneLociI fromLoci = fromSeq.getGeneLoci(); + if (fromLoci == null) + { + return; + } + + MapList newMap = targetToFrom.traverse(fromLoci.getMap()); + + if (newMap != null) + { + targetSeq.setGeneLoci(fromLoci.getSpeciesId(), + fromLoci.getAssemblyId(), fromLoci.getChromosomeId(), newMap); + } + } + + /** * A helper method that finds a CDS sequence in the alignment dataset that is * mapped to the given protein sequence, and either is, or has a mapping from, * the given dna sequence. @@ -1989,21 +2044,21 @@ public class AlignmentUtils } /** - * add any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to + * Adds any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to * the given mapping. * * @param cdsSeq * @param contig + * @param proteinProduct * @param mapping - * @return list of DBRefEntrys added. + * @return list of DBRefEntrys added */ - public static List propagateDBRefsToCDS(SequenceI cdsSeq, + protected static List propagateDBRefsToCDS(SequenceI cdsSeq, SequenceI contig, SequenceI proteinProduct, Mapping mapping) { - - // gather direct refs from contig congrent with mapping - List direct = new ArrayList(); - HashSet directSources = new HashSet(); + // gather direct refs from contig congruent with mapping + List direct = new ArrayList<>(); + HashSet directSources = new HashSet<>(); if (contig.getDBRefs() != null) { for (DBRefEntry dbr : contig.getDBRefs()) @@ -2023,7 +2078,7 @@ public class AlignmentUtils DBRefEntry[] onSource = DBRefUtils.selectRefs( proteinProduct.getDBRefs(), directSources.toArray(new String[0])); - List propagated = new ArrayList(); + List propagated = new ArrayList<>(); // and generate appropriate mappings for (DBRefEntry cdsref : direct) @@ -2081,7 +2136,7 @@ public class AlignmentUtils * subtypes in the Sequence Ontology) * @param omitting */ - public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq, + protected static int transferFeatures(SequenceI fromSeq, SequenceI toSeq, MapList mapping, String select, String... omitting) { SequenceI copyTo = toSeq; @@ -2202,7 +2257,7 @@ public class AlignmentUtils proteinStart++; proteinLength--; } - List proteinRange = new ArrayList(); + List proteinRange = new ArrayList<>(); /* * dna length should map to protein (or protein plus stop codon) @@ -2235,9 +2290,9 @@ public class AlignmentUtils * @param dnaSeq * @return */ - public static List findCdsPositions(SequenceI dnaSeq) + protected static List findCdsPositions(SequenceI dnaSeq) { - List result = new ArrayList(); + List result = new ArrayList<>(); List sfs = dnaSeq.getFeatures().getFeaturesByOntology( SequenceOntologyI.CDS); @@ -2370,15 +2425,19 @@ public class AlignmentUtils { if (var.variant != null) { - String alleles = (String) var.variant.getValue("alleles"); + String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES); if (alleles != null) { for (String base : alleles.split(",")) { - String codon = base + base2 + base3; - if (addPeptideVariant(peptide, peptidePos, residue, var, codon)) + if (!base1.equals(base)) { - count++; + String codon = base + base2 + base3; + if (addPeptideVariant(peptide, peptidePos, residue, var, + codon)) + { + count++; + } } } } @@ -2392,15 +2451,19 @@ public class AlignmentUtils { if (var.variant != null) { - String alleles = (String) var.variant.getValue("alleles"); + String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES); if (alleles != null) { for (String base : alleles.split(",")) { - String codon = base1 + base + base3; - if (addPeptideVariant(peptide, peptidePos, residue, var, codon)) + if (!base2.equals(base)) { - count++; + String codon = base1 + base + base3; + if (addPeptideVariant(peptide, peptidePos, residue, var, + codon)) + { + count++; + } } } } @@ -2414,15 +2477,19 @@ public class AlignmentUtils { if (var.variant != null) { - String alleles = (String) var.variant.getValue("alleles"); + String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES); if (alleles != null) { for (String base : alleles.split(",")) { - String codon = base1 + base2 + base; - if (addPeptideVariant(peptide, peptidePos, residue, var, codon)) + if (!base3.equals(base)) { - count++; + String codon = base1 + base2 + base; + if (addPeptideVariant(peptide, peptidePos, residue, var, + codon)) + { + count++; + } } } } @@ -2454,62 +2521,79 @@ public class AlignmentUtils * e.g. multibase variants or HGMD_MUTATION etc * are currently ignored here */ - String trans = codon.contains("-") ? "-" + String trans = codon.contains("-") ? null : (codon.length() > CODON_LENGTH ? null : ResidueProperties.codonTranslate(codon)); - if (trans != null && !trans.equals(residue)) + if (trans == null) + { + return false; + } + String desc = codon; + String featureType = ""; + if (trans.equals(residue)) + { + featureType = SequenceOntologyI.SYNONYMOUS_VARIANT; + } + else if (ResidueProperties.STOP.equals(trans)) + { + featureType = SequenceOntologyI.STOP_GAINED; + } + else { String residue3Char = StringUtils .toSentenceCase(ResidueProperties.aa2Triplet.get(residue)); String trans3Char = StringUtils .toSentenceCase(ResidueProperties.aa2Triplet.get(trans)); - String desc = "p." + residue3Char + peptidePos + trans3Char; - SequenceFeature sf = new SequenceFeature( - SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos, - peptidePos, var.getSource()); - StringBuilder attributes = new StringBuilder(32); - String id = (String) var.variant.getValue(ID); - if (id != null) - { - if (id.startsWith(SEQUENCE_VARIANT)) - { - id = id.substring(SEQUENCE_VARIANT.length()); - } - sf.setValue(ID, id); - attributes.append(ID).append("=").append(id); - // TODO handle other species variants JAL-2064 - StringBuilder link = new StringBuilder(32); - try - { - link.append(desc).append(" ").append(id).append( - "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=") - .append(URLEncoder.encode(id, "UTF-8")); - sf.addLink(link.toString()); - } catch (UnsupportedEncodingException e) - { - // as if - } - } - String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE); - if (clinSig != null) + desc = "p." + residue3Char + peptidePos + trans3Char; + featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT; + } + SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos, + peptidePos, var.getSource()); + + StringBuilder attributes = new StringBuilder(32); + String id = (String) var.variant.getValue(ID); + if (id != null) + { + if (id.startsWith(SEQUENCE_VARIANT)) { - sf.setValue(CLINICAL_SIGNIFICANCE, clinSig); - attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=") - .append(clinSig); + id = id.substring(SEQUENCE_VARIANT.length()); } - peptide.addSequenceFeature(sf); - if (attributes.length() > 0) + sf.setValue(ID, id); + attributes.append(ID).append("=").append(id); + // TODO handle other species variants JAL-2064 + StringBuilder link = new StringBuilder(32); + try + { + link.append(desc).append(" ").append(id).append( + "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=") + .append(URLEncoder.encode(id, "UTF-8")); + sf.addLink(link.toString()); + } catch (UnsupportedEncodingException e) { - sf.setAttributes(attributes.toString()); + // as if } - return true; } - return false; + String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE); + if (clinSig != null) + { + sf.setValue(CLINICAL_SIGNIFICANCE, clinSig); + attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=") + .append(clinSig); + } + peptide.addSequenceFeature(sf); + if (attributes.length() > 0) + { + sf.setAttributes(attributes.toString()); + } + return true; } /** * Builds a map whose key is position in the protein sequence, and value is a - * list of the base and all variants for each corresponding codon position + * list of the base and all variants for each corresponding codon position. + *

+ * This depends on dna variants being held as a comma-separated list as + * property "alleles" on variant features. * * @param dnaSeq * @param dnaToProtein @@ -2523,7 +2607,7 @@ public class AlignmentUtils * map from peptide position to all variants of the codon which codes for it * LinkedHashMap ensures we keep the peptide features in sequence order */ - LinkedHashMap[]> variants = new LinkedHashMap[]>(); + LinkedHashMap[]> variants = new LinkedHashMap<>(); List dnaFeatures = dnaSeq.getFeatures() .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT); @@ -2547,6 +2631,30 @@ public class AlignmentUtils // not handling multi-locus variant features continue; } + + /* + * ignore variant if not a SNP + */ + String alls = (String) sf.getValue(Gff3Helper.ALLELES); + if (alls == null) + { + continue; // non-SNP VCF variant perhaps - can't process this + } + + String[] alleles = alls.toUpperCase().split(","); + boolean isSnp = true; + for (String allele : alleles) + { + if (allele.trim().length() > 1) + { + isSnp = false; + } + } + if (!isSnp) + { + continue; + } + int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol); if (mapsTo == null) { @@ -2558,28 +2666,13 @@ public class AlignmentUtils if (codonVariants == null) { codonVariants = new ArrayList[CODON_LENGTH]; - codonVariants[0] = new ArrayList(); - codonVariants[1] = new ArrayList(); - codonVariants[2] = new ArrayList(); + codonVariants[0] = new ArrayList<>(); + codonVariants[1] = new ArrayList<>(); + codonVariants[2] = new ArrayList<>(); variants.put(peptidePosition, codonVariants); } /* - * 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 @@ -2699,7 +2792,7 @@ public class AlignmentUtils /* * fancy case - aligning via mappings between sequences */ - List unmapped = new ArrayList(); + List unmapped = new ArrayList<>(); Map> columnMap = buildMappedColumnsMap( unaligned, aligned, unmapped); int width = columnMap.size(); @@ -2774,7 +2867,7 @@ public class AlignmentUtils } // map from dataset sequence to alignment sequence(s) - Map> alignedDatasets = new HashMap>(); + Map> alignedDatasets = new HashMap<>(); for (SequenceI seq : aligned.getSequences()) { SequenceI ds = seq.getDatasetSequence(); @@ -2837,7 +2930,7 @@ public class AlignmentUtils * {unalignedSequence, characterPerSequence} at that position. * TreeMap keeps the entries in ascending column order. */ - SortedMap> map = new TreeMap>(); + SortedMap> map = new TreeMap<>(); /* * record any sequences that have no mapping so can't be realigned @@ -2942,7 +3035,7 @@ public class AlignmentUtils Map seqsMap = map.get(fromCol); if (seqsMap == null) { - seqsMap = new HashMap(); + seqsMap = new HashMap<>(); map.put(fromCol, seqsMap); } seqsMap.put(seq, seq.getCharAt(mappedCharPos - toStart)); diff --git a/src/jalview/analysis/Dna.java b/src/jalview/analysis/Dna.java index a10b037..ef05a58 100644 --- a/src/jalview/analysis/Dna.java +++ b/src/jalview/analysis/Dna.java @@ -161,7 +161,7 @@ public class Dna int s; int sSize = selection.size(); - List pepseqs = new ArrayList(); + List pepseqs = new ArrayList<>(); for (s = 0; s < sSize; s++) { SequenceI newseq = translateCodingRegion(selection.get(s), @@ -213,7 +213,7 @@ public class Dna if (dnarefs != null) { // intersect with pep - List mappedrefs = new ArrayList(); + List mappedrefs = new ArrayList<>(); DBRefEntry[] refs = dna.getDBRefs(); for (int d = 0; d < refs.length; d++) { @@ -391,7 +391,7 @@ public class Dna String seqstring, AlignedCodonFrame acf, List proteinSeqs) { - List skip = new ArrayList(); + List skip = new ArrayList<>(); int skipint[] = null; ShiftList vismapping = new ShiftList(); // map from viscontigs to seqstring // intervals @@ -544,7 +544,7 @@ public class Dna skip.add(skipint); skipint = null; } - if (aa.equals("STOP")) + if (aa.equals(ResidueProperties.STOP)) { aa = STOP_ASTERIX; } @@ -800,7 +800,7 @@ public class Dna public AlignmentI reverseCdna(boolean complement) { int sSize = selection.size(); - List reversed = new ArrayList(); + List reversed = new ArrayList<>(); for (int s = 0; s < sSize; s++) { SequenceI newseq = reverseSequence(selection.get(s).getName(), @@ -851,6 +851,23 @@ public class Dna } /** + * Answers the reverse complement of the input string + * + * @see #getComplement(char) + * @param s + * @return + */ + public static String reverseComplement(String s) + { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = s.length() - 1; i >= 0; i--) + { + sb.append(Dna.getComplement(s.charAt(i))); + } + return sb.toString(); + } + + /** * Returns dna complement (preserving case) for aAcCgGtTuU. Ambiguity codes * are treated as on http://reverse-complement.com/. Anything else is left * unchanged. diff --git a/src/jalview/api/FeatureColourI.java b/src/jalview/api/FeatureColourI.java index 0ded079..4dbb1bb 100644 --- a/src/jalview/api/FeatureColourI.java +++ b/src/jalview/api/FeatureColourI.java @@ -56,6 +56,14 @@ public interface FeatureColourI Color getMaxColour(); /** + * Returns the 'no value' colour (used when a feature lacks score, or the + * attribute, being used for colouring) + * + * @return + */ + Color getNoColour(); + + /** * Answers true if the feature has a single colour, i.e. if isColourByLabel() * and isGraduatedColour() both answer false * @@ -64,7 +72,8 @@ public interface FeatureColourI boolean isSimpleColour(); /** - * Answers true if the feature is coloured by label (description) + * Answers true if the feature is coloured by label (description) or by text + * value of an attribute * * @return */ @@ -93,18 +102,6 @@ public interface FeatureColourI void setAboveThreshold(boolean b); /** - * Answers true if the threshold is the minimum value (when - * isAboveThreshold()) or maximum value (when isBelowThreshold()) of the - * colour range; only applicable when isGraduatedColour and either - * isAboveThreshold() or isBelowThreshold() answers true - * - * @return - */ - boolean isThresholdMinMax(); - - void setThresholdMinMax(boolean b); - - /** * Returns the threshold value (if any), else zero * * @return @@ -156,7 +153,10 @@ public interface FeatureColourI Color getColor(SequenceFeature feature); /** - * Update the min-max range for a graduated colour scheme + * Update the min-max range for a graduated colour scheme. Note that the + * colour scheme may be configured to colour by feature score, or a + * (numeric-valued) attribute - the caller should ensure that the correct + * range is being set. * * @param min * @param max @@ -169,4 +169,27 @@ public interface FeatureColourI * @return */ String toJalviewFormat(String featureType); + + /** + * Answers true if colour is by attribute text or numerical value + * + * @return + */ + boolean isColourByAttribute(); + + /** + * Answers the name of the attribute (and optional sub-attribute...) used for + * colouring if any, or null + * + * @return + */ + String[] getAttributeName(); + + /** + * Sets the name of the attribute (and optional sub-attribute...) used for + * colouring if any, or null to remove this property + * + * @return + */ + void setAttributeName(String... name); } diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 9d2d7f4..cf3c8da 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -22,6 +22,7 @@ package jalview.api; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; import java.awt.Color; import java.awt.Graphics; @@ -132,7 +133,7 @@ public interface FeatureRenderer List getGroups(boolean visible); /** - * change visibility for a range of groups + * Set visibility for a list of groups * * @param toset * @param visible @@ -140,7 +141,7 @@ public interface FeatureRenderer void setGroupVisibility(List toset, boolean visible); /** - * change visibiilty of given group + * Set visibility of the given feature group * * @param group * @param visible @@ -148,9 +149,9 @@ public interface FeatureRenderer void setGroupVisibility(String group, boolean visible); /** - * Returns features at the specified aligned column on the given sequence. - * Non-positional features are not included. If the column has a gap, then - * enclosing features are included (but not contact features). + * Returns visible features at the specified aligned column on the given + * sequence. Non-positional features are not included. If the column has a gap, + * then enclosing features are included (but not contact features). * * @param sequence * @param column @@ -215,4 +216,53 @@ public interface FeatureRenderer */ float getTransparency(); + /** + * Answers the filters applied to the given feature type, or null if none is + * set + * + * @param featureType + * @return + */ + FeatureMatcherSetI getFeatureFilter(String featureType); + + /** + * Answers the feature filters map + * + * @return + */ + public Map 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 filters); + + /** + * Returns the colour for a particular feature instance. This includes + * calculation of 'colour by label', or of a graduated score colour, if + * applicable. + *

+ * Returns null if + *

    + *
  • feature type is not visible, or
  • + *
  • feature group is not visible, or
  • + *
  • feature values lie outside any colour threshold, or
  • + *
  • feature is excluded by filter conditions
  • + *
+ * + * @param feature + * @return + */ + Color getColour(SequenceFeature feature); } diff --git a/src/jalview/appletgui/APopupMenu.java b/src/jalview/appletgui/APopupMenu.java index 46bd4fd..76f2705 100644 --- a/src/jalview/appletgui/APopupMenu.java +++ b/src/jalview/appletgui/APopupMenu.java @@ -901,10 +901,7 @@ public class APopupMenu extends java.awt.PopupMenu .formatMessage("label.annotation_for_displayid", new Object[] { seq.getDisplayId(true) })); new SequenceAnnotationReport(null).createSequenceAnnotationReport( - contents, seq, true, true, - (ap.seqPanel.seqCanvas.fr != null) - ? ap.seqPanel.seqCanvas.fr.getMinMax() - : null); + contents, seq, true, true, ap.seqPanel.seqCanvas.fr); contents.append("

"); } Frame frame = new Frame(); diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index ef87671..fe6b8d9 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -1445,9 +1445,10 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener, FeaturesFile formatter = new FeaturesFile(); if (format.equalsIgnoreCase("Jalview")) { - features = formatter.printJalviewFormat(viewport.getAlignment() - .getSequencesArray(), getDisplayedFeatureCols(), - getDisplayedFeatureGroups(), true); + features = formatter.printJalviewFormat( + viewport.getAlignment().getSequencesArray(), + getDisplayedFeatureCols(), null, getDisplayedFeatureGroups(), + true); } else { diff --git a/src/jalview/appletgui/FeatureColourChooser.java b/src/jalview/appletgui/FeatureColourChooser.java index 5a073c6..d9eae11 100644 --- a/src/jalview/appletgui/FeatureColourChooser.java +++ b/src/jalview/appletgui/FeatureColourChooser.java @@ -58,6 +58,8 @@ public class FeatureColourChooser extends Panel implements ActionListener, */ private static final int SCALE_FACTOR_1K = 1000; + private static final String COLON = ":"; + private JVDialog frame; private Frame owner; @@ -167,9 +169,9 @@ public class FeatureColourChooser extends Panel implements ActionListener, slider.addAdjustmentListener(this); slider.addMouseListener(this); owner = (af != null) ? af : fs.frame; - frame = new JVDialog(owner, MessageManager - .formatMessage("label.graduated_color_for_params", new String[] - { type }), true, 480, 248); + frame = new JVDialog(owner, MessageManager.formatMessage( + "label.variable_color_for", new String[] { type }), true, 480, + 248); frame.setMainPanel(this); validate(); frame.setVisible(true); @@ -198,8 +200,10 @@ public class FeatureColourChooser extends Panel implements ActionListener, private void jbInit() throws Exception { - Label minLabel = new Label(MessageManager.getString("label.min")), - maxLabel = new Label(MessageManager.getString("label.max")); + Label minLabel = new Label( + MessageManager.getString("label.min_value") + COLON); + Label maxLabel = new Label( + MessageManager.getString("label.max_value") + COLON); minLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11)); maxLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11)); // minColour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11)); diff --git a/src/jalview/appletgui/FeatureSettings.java b/src/jalview/appletgui/FeatureSettings.java index 9a67499..ad04171 100755 --- a/src/jalview/appletgui/FeatureSettings.java +++ b/src/jalview/appletgui/FeatureSettings.java @@ -25,6 +25,7 @@ import jalview.api.FeatureSettingsControllerI; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; import jalview.util.MessageManager; +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; import java.awt.BorderLayout; import java.awt.Button; @@ -583,22 +584,19 @@ public class FeatureSettings extends Panel { Component[] comps = featurePanel.getComponents(); int cSize = comps.length; - - Object[][] tmp = new Object[cSize][3]; - int tmpSize = 0; - for (int i = 0; i < cSize; i++) - { - MyCheckbox check = (MyCheckbox) comps[i]; - tmp[tmpSize][0] = check.type; - tmp[tmpSize][1] = fr.getFeatureStyle(check.type); - tmp[tmpSize][2] = new Boolean(check.getState()); - tmpSize++; + FeatureSettingsBean[] rowData = new FeatureSettingsBean[cSize]; + int i = 0; + for (Component comp : comps) + { + MyCheckbox check = (MyCheckbox) comp; + // feature filter set to null as not (yet) offered in applet + FeatureColourI colour = fr.getFeatureStyle(check.type); + rowData[i] = new FeatureSettingsBean(check.type, colour, null, + check.getState()); + i++; } - Object[][] data = new Object[tmpSize][3]; - System.arraycopy(tmp, 0, data, 0, tmpSize); - - fr.setFeaturePriority(data); + fr.setFeaturePriority(rowData); ap.paintAlignment(updateOverview, updateOverview); } diff --git a/src/jalview/binding/Colour.java b/src/jalview/binding/Colour.java index 25cf9bf..f51e9af 100644 --- a/src/jalview/binding/Colour.java +++ b/src/jalview/binding/Colour.java @@ -27,7 +27,8 @@ public class Colour implements java.io.Serializable // --------------------------/ /** - * Field _name. + * Single letter residue code for an alignment colour scheme, or feature type + * for a feature colour scheme */ private java.lang.String _name; @@ -42,9 +43,15 @@ public class Colour implements java.io.Serializable private java.lang.String _minRGB; /** - * loosely specified enumeration: NONE,ABOVE, or BELOW + * Field _noValueColour. */ - private java.lang.String _threshType; + private jalview.binding.types.NoValueColour _noValueColour = jalview.binding.types.NoValueColour + .valueOf("Min"); + + /** + * Field _threshType. + */ + private jalview.binding.types.ColourThreshTypeType _threshType; /** * Field _threshold. @@ -96,6 +103,11 @@ public class Colour implements java.io.Serializable */ private boolean _has_autoScale; + /** + * name of feature attribute to colour by, or attribute and sub-attribute + */ + private java.util.Vector _attributeNameList; + // ----------------/ // - Constructors -/ // ----------------/ @@ -103,6 +115,8 @@ public class Colour implements java.io.Serializable public Colour() { super(); + setNoValueColour(jalview.binding.types.NoValueColour.valueOf("Min")); + this._attributeNameList = new java.util.Vector(); } // -----------/ @@ -110,41 +124,140 @@ public class Colour implements java.io.Serializable // -----------/ /** - */ + * + * + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.addElement(vAttributeName); + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.add(index, vAttributeName); + } + + /** + */ public void deleteAutoScale() { this._has_autoScale = false; } /** - */ + */ public void deleteColourByLabel() { this._has_colourByLabel = false; } /** - */ + */ public void deleteMax() { this._has_max = false; } /** - */ + */ public void deleteMin() { this._has_min = false; } /** - */ + */ public void deleteThreshold() { this._has_threshold = false; } /** + * Method enumerateAttributeName. + * + * @return an Enumeration over all java.lang.String elements + */ + public java.util.Enumeration enumerateAttributeName() + { + return this._attributeNameList.elements(); + } + + /** + * Method getAttributeName. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the java.lang.String at the given index + */ + public java.lang.String getAttributeName(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("getAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + return (java.lang.String) _attributeNameList.get(index); + } + + /** + * Method getAttributeName.Returns the contents of the collection in an Array. + *

+ * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public java.lang.String[] getAttributeName() + { + java.lang.String[] array = new java.lang.String[0]; + return (java.lang.String[]) this._attributeNameList.toArray(array); + } + + /** + * Method getAttributeNameCount. + * + * @return the size of this collection + */ + public int getAttributeNameCount() + { + return this._attributeNameList.size(); + } + + /** * Returns the value of field 'autoScale'. * * @return the value of field 'AutoScale'. @@ -195,7 +308,9 @@ public class Colour implements java.io.Serializable } /** - * Returns the value of field 'name'. + * Returns the value of field 'name'. The field 'name' has the following + * description: Single letter residue code for an alignment colour scheme, or + * feature type for a feature colour scheme * * @return the value of field 'Name'. */ @@ -205,6 +320,16 @@ public class Colour implements java.io.Serializable } /** + * Returns the value of field 'noValueColour'. + * + * @return the value of field 'NoValueColour'. + */ + public jalview.binding.types.NoValueColour getNoValueColour() + { + return this._noValueColour; + } + + /** * Returns the value of field 'RGB'. * * @return the value of field 'RGB'. @@ -215,12 +340,11 @@ public class Colour implements java.io.Serializable } /** - * Returns the value of field 'threshType'. The field 'threshType' has the - * following description: loosely specified enumeration: NONE,ABOVE, or BELOW + * Returns the value of field 'threshType'. * * @return the value of field 'ThreshType'. */ - public java.lang.String getThreshType() + public jalview.binding.types.ColourThreshTypeType getThreshType() { return this._threshType; } @@ -360,6 +484,76 @@ public class Colour implements java.io.Serializable } /** + */ + public void removeAllAttributeName() + { + this._attributeNameList.clear(); + } + + /** + * Method removeAttributeName. + * + * @param vAttributeName + * @return true if the object was removed from the collection. + */ + public boolean removeAttributeName(final java.lang.String vAttributeName) + { + boolean removed = _attributeNameList.remove(vAttributeName); + return removed; + } + + /** + * Method removeAttributeNameAt. + * + * @param index + * @return the element removed from the collection + */ + public java.lang.String removeAttributeNameAt(final int index) + { + java.lang.Object obj = this._attributeNameList.remove(index); + return (java.lang.String) obj; + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("setAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + this._attributeNameList.set(index, vAttributeName); + } + + /** + * + * + * @param vAttributeNameArray + */ + public void setAttributeName(final java.lang.String[] vAttributeNameArray) + { + // -- copy array + _attributeNameList.clear(); + + for (int i = 0; i < vAttributeNameArray.length; i++) + { + this._attributeNameList.add(vAttributeNameArray[i]); + } + } + + /** * Sets the value of field 'autoScale'. * * @param autoScale @@ -419,7 +613,9 @@ public class Colour implements java.io.Serializable } /** - * Sets the value of field 'name'. + * Sets the value of field 'name'. The field 'name' has the following + * description: Single letter residue code for an alignment colour scheme, or + * feature type for a feature colour scheme * * @param name * the value of field 'name'. @@ -430,6 +626,18 @@ public class Colour implements java.io.Serializable } /** + * Sets the value of field 'noValueColour'. + * + * @param noValueColour + * the value of field 'noValueColour'. + */ + public void setNoValueColour( + final jalview.binding.types.NoValueColour noValueColour) + { + this._noValueColour = noValueColour; + } + + /** * Sets the value of field 'RGB'. * * @param RGB @@ -441,13 +649,13 @@ public class Colour implements java.io.Serializable } /** - * Sets the value of field 'threshType'. The field 'threshType' has the - * following description: loosely specified enumeration: NONE,ABOVE, or BELOW + * Sets the value of field 'threshType'. * * @param threshType * the value of field 'threshType'. */ - public void setThreshType(final java.lang.String threshType) + public void setThreshType( + final jalview.binding.types.ColourThreshTypeType threshType) { this._threshType = threshType; } diff --git a/src/jalview/binding/CompoundMatcher.java b/src/jalview/binding/CompoundMatcher.java new file mode 100644 index 0000000..a2d1048 --- /dev/null +++ b/src/jalview/binding/CompoundMatcher.java @@ -0,0 +1,368 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class CompoundMatcher. + * + * @version $Revision$ $Date$ + */ +public class CompoundMatcher implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * If true, matchers are AND-ed, if false they are OR-ed + */ + private boolean _and; + + /** + * keeps track of state for field: _and + */ + private boolean _has_and; + + /** + * Field _matcherSetList. + */ + private java.util.Vector _matcherSetList; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public CompoundMatcher() + { + super(); + this._matcherSetList = new java.util.Vector(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * + * + * @param vMatcherSet + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addMatcherSet(final jalview.binding.MatcherSet vMatcherSet) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._matcherSetList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addMatcherSet has a maximum of 2"); + } + + this._matcherSetList.addElement(vMatcherSet); + } + + /** + * + * + * @param index + * @param vMatcherSet + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addMatcherSet(final int index, + final jalview.binding.MatcherSet vMatcherSet) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._matcherSetList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addMatcherSet has a maximum of 2"); + } + + this._matcherSetList.add(index, vMatcherSet); + } + + /** + */ + public void deleteAnd() + { + this._has_and = false; + } + + /** + * Method enumerateMatcherSet. + * + * @return an Enumeration over all jalview.binding.MatcherSet elements + */ + public java.util.Enumeration enumerateMatcherSet() + { + return this._matcherSetList.elements(); + } + + /** + * Returns the value of field 'and'. The field 'and' has the following + * description: If true, matchers are AND-ed, if false they are OR-ed + * + * @return the value of field 'And'. + */ + public boolean getAnd() + { + return this._and; + } + + /** + * Method getMatcherSet. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the jalview.binding.MatcherSet at the given index + */ + public jalview.binding.MatcherSet getMatcherSet(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._matcherSetList.size()) + { + throw new IndexOutOfBoundsException( + "getMatcherSet: Index value '" + index + "' not in range [0.." + + (this._matcherSetList.size() - 1) + "]"); + } + + return (jalview.binding.MatcherSet) _matcherSetList.get(index); + } + + /** + * Method getMatcherSet.Returns the contents of the collection in an Array. + *

+ * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public jalview.binding.MatcherSet[] getMatcherSet() + { + jalview.binding.MatcherSet[] array = new jalview.binding.MatcherSet[0]; + return (jalview.binding.MatcherSet[]) this._matcherSetList + .toArray(array); + } + + /** + * Method getMatcherSetCount. + * + * @return the size of this collection + */ + public int getMatcherSetCount() + { + return this._matcherSetList.size(); + } + + /** + * Method hasAnd. + * + * @return true if at least one And has been added + */ + public boolean hasAnd() + { + return this._has_and; + } + + /** + * Returns the value of field 'and'. The field 'and' has the following + * description: If true, matchers are AND-ed, if false they are OR-ed + * + * @return the value of field 'And'. + */ + public boolean isAnd() + { + return this._and; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + */ + public void removeAllMatcherSet() + { + this._matcherSetList.clear(); + } + + /** + * Method removeMatcherSet. + * + * @param vMatcherSet + * @return true if the object was removed from the collection. + */ + public boolean removeMatcherSet( + final jalview.binding.MatcherSet vMatcherSet) + { + boolean removed = _matcherSetList.remove(vMatcherSet); + return removed; + } + + /** + * Method removeMatcherSetAt. + * + * @param index + * @return the element removed from the collection + */ + public jalview.binding.MatcherSet removeMatcherSetAt(final int index) + { + java.lang.Object obj = this._matcherSetList.remove(index); + return (jalview.binding.MatcherSet) obj; + } + + /** + * Sets the value of field 'and'. The field 'and' has the following + * description: If true, matchers are AND-ed, if false they are OR-ed + * + * @param and + * the value of field 'and'. + */ + public void setAnd(final boolean and) + { + this._and = and; + this._has_and = true; + } + + /** + * + * + * @param index + * @param vMatcherSet + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setMatcherSet(final int index, + final jalview.binding.MatcherSet vMatcherSet) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._matcherSetList.size()) + { + throw new IndexOutOfBoundsException( + "setMatcherSet: Index value '" + index + "' not in range [0.." + + (this._matcherSetList.size() - 1) + "]"); + } + + this._matcherSetList.set(index, vMatcherSet); + } + + /** + * + * + * @param vMatcherSetArray + */ + public void setMatcherSet( + final jalview.binding.MatcherSet[] vMatcherSetArray) + { + // -- copy array + _matcherSetList.clear(); + + for (int i = 0; i < vMatcherSetArray.length; i++) + { + this._matcherSetList.add(vMatcherSetArray[i]); + } + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.binding.CompoundMatcher + */ + public static jalview.binding.CompoundMatcher unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.binding.CompoundMatcher) Unmarshaller + .unmarshal(jalview.binding.CompoundMatcher.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/binding/FeatureMatcher.java b/src/jalview/binding/FeatureMatcher.java new file mode 100644 index 0000000..e4e52fb --- /dev/null +++ b/src/jalview/binding/FeatureMatcher.java @@ -0,0 +1,381 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class FeatureMatcher. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcher implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _by. + */ + private jalview.binding.types.FeatureMatcherByType _by; + + /** + * name of feature attribute to filter on, or attribute and sub-attribute + */ + private java.util.Vector _attributeNameList; + + /** + * Field _condition. + */ + private java.lang.String _condition; + + /** + * Field _value. + */ + private java.lang.String _value; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FeatureMatcher() + { + super(); + this._attributeNameList = new java.util.Vector(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * + * + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.addElement(vAttributeName); + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.add(index, vAttributeName); + } + + /** + * Method enumerateAttributeName. + * + * @return an Enumeration over all java.lang.String elements + */ + public java.util.Enumeration enumerateAttributeName() + { + return this._attributeNameList.elements(); + } + + /** + * Method getAttributeName. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the java.lang.String at the given index + */ + public java.lang.String getAttributeName(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("getAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + return (java.lang.String) _attributeNameList.get(index); + } + + /** + * Method getAttributeName.Returns the contents of the collection in an Array. + *

+ * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public java.lang.String[] getAttributeName() + { + java.lang.String[] array = new java.lang.String[0]; + return (java.lang.String[]) this._attributeNameList.toArray(array); + } + + /** + * Method getAttributeNameCount. + * + * @return the size of this collection + */ + public int getAttributeNameCount() + { + return this._attributeNameList.size(); + } + + /** + * Returns the value of field 'by'. + * + * @return the value of field 'By'. + */ + public jalview.binding.types.FeatureMatcherByType getBy() + { + return this._by; + } + + /** + * Returns the value of field 'condition'. + * + * @return the value of field 'Condition'. + */ + public java.lang.String getCondition() + { + return this._condition; + } + + /** + * Returns the value of field 'value'. + * + * @return the value of field 'Value'. + */ + public java.lang.String getValue() + { + return this._value; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + */ + public void removeAllAttributeName() + { + this._attributeNameList.clear(); + } + + /** + * Method removeAttributeName. + * + * @param vAttributeName + * @return true if the object was removed from the collection. + */ + public boolean removeAttributeName(final java.lang.String vAttributeName) + { + boolean removed = _attributeNameList.remove(vAttributeName); + return removed; + } + + /** + * Method removeAttributeNameAt. + * + * @param index + * @return the element removed from the collection + */ + public java.lang.String removeAttributeNameAt(final int index) + { + java.lang.Object obj = this._attributeNameList.remove(index); + return (java.lang.String) obj; + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("setAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + this._attributeNameList.set(index, vAttributeName); + } + + /** + * + * + * @param vAttributeNameArray + */ + public void setAttributeName(final java.lang.String[] vAttributeNameArray) + { + // -- copy array + _attributeNameList.clear(); + + for (int i = 0; i < vAttributeNameArray.length; i++) + { + this._attributeNameList.add(vAttributeNameArray[i]); + } + } + + /** + * Sets the value of field 'by'. + * + * @param by + * the value of field 'by'. + */ + public void setBy(final jalview.binding.types.FeatureMatcherByType by) + { + this._by = by; + } + + /** + * Sets the value of field 'condition'. + * + * @param condition + * the value of field 'condition'. + */ + public void setCondition(final java.lang.String condition) + { + this._condition = condition; + } + + /** + * Sets the value of field 'value'. + * + * @param value + * the value of field 'value'. + */ + public void setValue(final java.lang.String value) + { + this._value = value; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.binding.FeatureMatcher + */ + public static jalview.binding.FeatureMatcher unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.binding.FeatureMatcher) Unmarshaller + .unmarshal(jalview.binding.FeatureMatcher.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/binding/FeatureMatcherSet.java b/src/jalview/binding/FeatureMatcherSet.java new file mode 100644 index 0000000..7ba5f0e --- /dev/null +++ b/src/jalview/binding/FeatureMatcherSet.java @@ -0,0 +1,200 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * A feature match condition, which may be simple or compound + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherSet implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Internal choice value storage + */ + private java.lang.Object _choiceValue; + + /** + * Field _matchCondition. + */ + private jalview.binding.MatchCondition _matchCondition; + + /** + * Field _compoundMatcher. + */ + private jalview.binding.CompoundMatcher _compoundMatcher; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FeatureMatcherSet() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Returns the value of field 'choiceValue'. The field 'choiceValue' has the + * following description: Internal choice value storage + * + * @return the value of field 'ChoiceValue'. + */ + public java.lang.Object getChoiceValue() + { + return this._choiceValue; + } + + /** + * Returns the value of field 'compoundMatcher'. + * + * @return the value of field 'CompoundMatcher'. + */ + public jalview.binding.CompoundMatcher getCompoundMatcher() + { + return this._compoundMatcher; + } + + /** + * Returns the value of field 'matchCondition'. + * + * @return the value of field 'MatchCondition'. + */ + public jalview.binding.MatchCondition getMatchCondition() + { + return this._matchCondition; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Sets the value of field 'compoundMatcher'. + * + * @param compoundMatcher + * the value of field 'compoundMatcher'. + */ + public void setCompoundMatcher( + final jalview.binding.CompoundMatcher compoundMatcher) + { + this._compoundMatcher = compoundMatcher; + this._choiceValue = compoundMatcher; + } + + /** + * Sets the value of field 'matchCondition'. + * + * @param matchCondition + * the value of field 'matchCondition'. + */ + public void setMatchCondition( + final jalview.binding.MatchCondition matchCondition) + { + this._matchCondition = matchCondition; + this._choiceValue = matchCondition; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.binding.FeatureMatcherSet + */ + public static jalview.binding.FeatureMatcherSet unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.binding.FeatureMatcherSet) Unmarshaller + .unmarshal(jalview.binding.FeatureMatcherSet.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/binding/Filter.java b/src/jalview/binding/Filter.java new file mode 100644 index 0000000..687ae91 --- /dev/null +++ b/src/jalview/binding/Filter.java @@ -0,0 +1,180 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class Filter. + * + * @version $Revision$ $Date$ + */ +public class Filter implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _featureType. + */ + private java.lang.String _featureType; + + /** + * Field _matcherSet. + */ + private jalview.binding.MatcherSet _matcherSet; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public Filter() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Returns the value of field 'featureType'. + * + * @return the value of field 'FeatureType'. + */ + public java.lang.String getFeatureType() + { + return this._featureType; + } + + /** + * Returns the value of field 'matcherSet'. + * + * @return the value of field 'MatcherSet'. + */ + public jalview.binding.MatcherSet getMatcherSet() + { + return this._matcherSet; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Sets the value of field 'featureType'. + * + * @param featureType + * the value of field 'featureType'. + */ + public void setFeatureType(final java.lang.String featureType) + { + this._featureType = featureType; + } + + /** + * Sets the value of field 'matcherSet'. + * + * @param matcherSet + * the value of field 'matcherSet'. + */ + public void setMatcherSet(final jalview.binding.MatcherSet matcherSet) + { + this._matcherSet = matcherSet; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.binding.Filter + */ + public static jalview.binding.Filter unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.binding.Filter) Unmarshaller + .unmarshal(jalview.binding.Filter.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/binding/JalviewUserColours.java b/src/jalview/binding/JalviewUserColours.java index 6709487..67ee5a2 100644 --- a/src/jalview/binding/JalviewUserColours.java +++ b/src/jalview/binding/JalviewUserColours.java @@ -42,6 +42,11 @@ public class JalviewUserColours implements java.io.Serializable */ private java.util.Vector _colourList; + /** + * Field _filterList. + */ + private java.util.Vector _filterList; + // ----------------/ // - Constructors -/ // ----------------/ @@ -50,6 +55,7 @@ public class JalviewUserColours implements java.io.Serializable { super(); this._colourList = new java.util.Vector(); + this._filterList = new java.util.Vector(); } // -----------/ @@ -84,6 +90,33 @@ public class JalviewUserColours implements java.io.Serializable } /** + * + * + * @param vFilter + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addFilter(final Filter vFilter) + throws java.lang.IndexOutOfBoundsException + { + this._filterList.addElement(vFilter); + } + + /** + * + * + * @param index + * @param vFilter + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addFilter(final int index, final Filter vFilter) + throws java.lang.IndexOutOfBoundsException + { + this._filterList.add(index, vFilter); + } + + /** * Method enumerateColour. * * @return an Enumeration over all Colour elements @@ -94,6 +127,16 @@ public class JalviewUserColours implements java.io.Serializable } /** + * Method enumerateFilter. + * + * @return an Enumeration over all Filter elements + */ + public java.util.Enumeration enumerateFilter() + { + return this._filterList.elements(); + } + + /** * Method getColour. * * @param index @@ -141,6 +184,53 @@ public class JalviewUserColours implements java.io.Serializable } /** + * Method getFilter. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the Filter at the given index + */ + public Filter getFilter(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._filterList.size()) + { + throw new IndexOutOfBoundsException( + "getFilter: Index value '" + index + "' not in range [0.." + + (this._filterList.size() - 1) + "]"); + } + + return (Filter) _filterList.get(index); + } + + /** + * Method getFilter.Returns the contents of the collection in an Array. + *

+ * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public Filter[] getFilter() + { + Filter[] array = new Filter[0]; + return (Filter[]) this._filterList.toArray(array); + } + + /** + * Method getFilterCount. + * + * @return the size of this collection + */ + public int getFilterCount() + { + return this._filterList.size(); + } + + /** * Returns the value of field 'schemeName'. * * @return the value of field 'SchemeName'. @@ -217,13 +307,20 @@ public class JalviewUserColours implements java.io.Serializable } /** - */ + */ public void removeAllColour() { this._colourList.clear(); } /** + */ + public void removeAllFilter() + { + this._filterList.clear(); + } + + /** * Method removeColour. * * @param vColour @@ -248,6 +345,30 @@ public class JalviewUserColours implements java.io.Serializable } /** + * Method removeFilter. + * + * @param vFilter + * @return true if the object was removed from the collection. + */ + public boolean removeFilter(final Filter vFilter) + { + boolean removed = _filterList.remove(vFilter); + return removed; + } + + /** + * Method removeFilterAt. + * + * @param index + * @return the element removed from the collection + */ + public Filter removeFilterAt(final int index) + { + java.lang.Object obj = this._filterList.remove(index); + return (Filter) obj; + } + + /** * * * @param index @@ -286,6 +407,44 @@ public class JalviewUserColours implements java.io.Serializable } /** + * + * + * @param index + * @param vFilter + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setFilter(final int index, final Filter vFilter) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._filterList.size()) + { + throw new IndexOutOfBoundsException( + "setFilter: Index value '" + index + "' not in range [0.." + + (this._filterList.size() - 1) + "]"); + } + + this._filterList.set(index, vFilter); + } + + /** + * + * + * @param vFilterArray + */ + public void setFilter(final Filter[] vFilterArray) + { + // -- copy array + _filterList.clear(); + + for (int i = 0; i < vFilterArray.length; i++) + { + this._filterList.add(vFilterArray[i]); + } + } + + /** * Sets the value of field 'schemeName'. * * @param schemeName diff --git a/src/jalview/binding/MatchCondition.java b/src/jalview/binding/MatchCondition.java new file mode 100644 index 0000000..44a3d3e --- /dev/null +++ b/src/jalview/binding/MatchCondition.java @@ -0,0 +1,125 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class MatchCondition. + * + * @version $Revision$ $Date$ + */ +public class MatchCondition extends FeatureMatcher + implements java.io.Serializable +{ + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public MatchCondition() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.binding.FeatureMatcher + */ + public static jalview.binding.FeatureMatcher unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.binding.FeatureMatcher) Unmarshaller + .unmarshal(jalview.binding.MatchCondition.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/binding/MatcherSet.java b/src/jalview/binding/MatcherSet.java new file mode 100644 index 0000000..756d93a --- /dev/null +++ b/src/jalview/binding/MatcherSet.java @@ -0,0 +1,125 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class MatcherSet. + * + * @version $Revision$ $Date$ + */ +public class MatcherSet extends FeatureMatcherSet + implements java.io.Serializable +{ + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public MatcherSet() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.binding.FeatureMatcherSet + */ + public static jalview.binding.FeatureMatcherSet unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.binding.FeatureMatcherSet) Unmarshaller + .unmarshal(jalview.binding.MatcherSet.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/binding/types/ColourThreshTypeType.java b/src/jalview/binding/types/ColourThreshTypeType.java new file mode 100644 index 0000000..024f2c0 --- /dev/null +++ b/src/jalview/binding/types/ColourThreshTypeType.java @@ -0,0 +1,168 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding.types; + + //---------------------------------/ + //- Imported classes and packages -/ +//---------------------------------/ + +import java.util.Hashtable; + +/** + * Class ColourThreshTypeType. + * + * @version $Revision$ $Date$ + */ +public class ColourThreshTypeType implements java.io.Serializable { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * The NONE type + */ + public static final int NONE_TYPE = 0; + + /** + * The instance of the NONE type + */ + public static final ColourThreshTypeType NONE = new ColourThreshTypeType(NONE_TYPE, "NONE"); + + /** + * The ABOVE type + */ + public static final int ABOVE_TYPE = 1; + + /** + * The instance of the ABOVE type + */ + public static final ColourThreshTypeType ABOVE = new ColourThreshTypeType(ABOVE_TYPE, "ABOVE"); + + /** + * The BELOW type + */ + public static final int BELOW_TYPE = 2; + + /** + * The instance of the BELOW type + */ + public static final ColourThreshTypeType BELOW = new ColourThreshTypeType(BELOW_TYPE, "BELOW"); + + /** + * Field _memberTable. + */ + private static java.util.Hashtable _memberTable = init(); + + /** + * Field type. + */ + private int type = -1; + + /** + * Field stringValue. + */ + private java.lang.String stringValue = null; + + + //----------------/ + //- Constructors -/ + //----------------/ + + private ColourThreshTypeType(final int type, final java.lang.String value) { + super(); + this.type = type; + this.stringValue = value; + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Method enumerate.Returns an enumeration of all possible + * instances of ColourThreshTypeType + * + * @return an Enumeration over all possible instances of + * ColourThreshTypeType + */ + public static java.util.Enumeration enumerate( + ) { + return _memberTable.elements(); + } + + /** + * Method getType.Returns the type of this ColourThreshTypeType + * + * @return the type of this ColourThreshTypeType + */ + public int getType( + ) { + return this.type; + } + + /** + * Method init. + * + * @return the initialized Hashtable for the member table + */ + private static java.util.Hashtable init( + ) { + Hashtable members = new Hashtable(); + members.put("NONE", NONE); + members.put("ABOVE", ABOVE); + members.put("BELOW", BELOW); + return members; + } + + /** + * Method readResolve. will be called during deserialization to + * replace the deserialized object with the correct constant + * instance. + * + * @return this deserialized object + */ + private java.lang.Object readResolve( + ) { + return valueOf(this.stringValue); + } + + /** + * Method toString.Returns the String representation of this + * ColourThreshTypeType + * + * @return the String representation of this ColourThreshTypeTyp + */ + public java.lang.String toString( + ) { + return this.stringValue; + } + + /** + * Method valueOf.Returns a new ColourThreshTypeType based on + * the given String value. + * + * @param string + * @return the ColourThreshTypeType value of parameter 'string' + */ + public static jalview.binding.types.ColourThreshTypeType valueOf( + final java.lang.String string) { + java.lang.Object obj = null; + if (string != null) { + obj = _memberTable.get(string); + } + if (obj == null) { + String err = "" + string + " is not a valid ColourThreshTypeType"; + throw new IllegalArgumentException(err); + } + return (ColourThreshTypeType) obj; + } + +} diff --git a/src/jalview/binding/types/FeatureMatcherByType.java b/src/jalview/binding/types/FeatureMatcherByType.java new file mode 100644 index 0000000..2185bba --- /dev/null +++ b/src/jalview/binding/types/FeatureMatcherByType.java @@ -0,0 +1,168 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding.types; + + //---------------------------------/ + //- Imported classes and packages -/ +//---------------------------------/ + +import java.util.Hashtable; + +/** + * Class FeatureMatcherByType. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherByType implements java.io.Serializable { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * The byLabel type + */ + public static final int BYLABEL_TYPE = 0; + + /** + * The instance of the byLabel type + */ + public static final FeatureMatcherByType BYLABEL = new FeatureMatcherByType(BYLABEL_TYPE, "byLabel"); + + /** + * The byScore type + */ + public static final int BYSCORE_TYPE = 1; + + /** + * The instance of the byScore type + */ + public static final FeatureMatcherByType BYSCORE = new FeatureMatcherByType(BYSCORE_TYPE, "byScore"); + + /** + * The byAttribute type + */ + public static final int BYATTRIBUTE_TYPE = 2; + + /** + * The instance of the byAttribute type + */ + public static final FeatureMatcherByType BYATTRIBUTE = new FeatureMatcherByType(BYATTRIBUTE_TYPE, "byAttribute"); + + /** + * Field _memberTable. + */ + private static java.util.Hashtable _memberTable = init(); + + /** + * Field type. + */ + private int type = -1; + + /** + * Field stringValue. + */ + private java.lang.String stringValue = null; + + + //----------------/ + //- Constructors -/ + //----------------/ + + private FeatureMatcherByType(final int type, final java.lang.String value) { + super(); + this.type = type; + this.stringValue = value; + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Method enumerate.Returns an enumeration of all possible + * instances of FeatureMatcherByType + * + * @return an Enumeration over all possible instances of + * FeatureMatcherByType + */ + public static java.util.Enumeration enumerate( + ) { + return _memberTable.elements(); + } + + /** + * Method getType.Returns the type of this FeatureMatcherByType + * + * @return the type of this FeatureMatcherByType + */ + public int getType( + ) { + return this.type; + } + + /** + * Method init. + * + * @return the initialized Hashtable for the member table + */ + private static java.util.Hashtable init( + ) { + Hashtable members = new Hashtable(); + members.put("byLabel", BYLABEL); + members.put("byScore", BYSCORE); + members.put("byAttribute", BYATTRIBUTE); + return members; + } + + /** + * Method readResolve. will be called during deserialization to + * replace the deserialized object with the correct constant + * instance. + * + * @return this deserialized object + */ + private java.lang.Object readResolve( + ) { + return valueOf(this.stringValue); + } + + /** + * Method toString.Returns the String representation of this + * FeatureMatcherByType + * + * @return the String representation of this FeatureMatcherByTyp + */ + public java.lang.String toString( + ) { + return this.stringValue; + } + + /** + * Method valueOf.Returns a new FeatureMatcherByType based on + * the given String value. + * + * @param string + * @return the FeatureMatcherByType value of parameter 'string' + */ + public static jalview.binding.types.FeatureMatcherByType valueOf( + final java.lang.String string) { + java.lang.Object obj = null; + if (string != null) { + obj = _memberTable.get(string); + } + if (obj == null) { + String err = "" + string + " is not a valid FeatureMatcherByType"; + throw new IllegalArgumentException(err); + } + return (FeatureMatcherByType) obj; + } + +} diff --git a/src/jalview/binding/types/NoValueColour.java b/src/jalview/binding/types/NoValueColour.java new file mode 100644 index 0000000..c1540f6 --- /dev/null +++ b/src/jalview/binding/types/NoValueColour.java @@ -0,0 +1,169 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.binding.types; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import java.util.Hashtable; + +/** + * Graduated feature colour if no score (or attribute) value + * + * @version $Revision$ $Date$ + */ +public class NoValueColour implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * The None type + */ + public static final int NONE_TYPE = 0; + + /** + * The instance of the None type + */ + public static final NoValueColour NONE = new NoValueColour(NONE_TYPE, + "None"); + + /** + * The Min type + */ + public static final int MIN_TYPE = 1; + + /** + * The instance of the Min type + */ + public static final NoValueColour MIN = new NoValueColour(MIN_TYPE, + "Min"); + + /** + * The Max type + */ + public static final int MAX_TYPE = 2; + + /** + * The instance of the Max type + */ + public static final NoValueColour MAX = new NoValueColour(MAX_TYPE, + "Max"); + + /** + * Field _memberTable. + */ + private static java.util.Hashtable _memberTable = init(); + + /** + * Field type. + */ + private int type = -1; + + /** + * Field stringValue. + */ + private java.lang.String stringValue = null; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + private NoValueColour(final int type, final java.lang.String value) + { + super(); + this.type = type; + this.stringValue = value; + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method enumerate.Returns an enumeration of all possible instances of + * NoValueColour + * + * @return an Enumeration over all possible instances of NoValueColour + */ + public static java.util.Enumeration enumerate() + { + return _memberTable.elements(); + } + + /** + * Method getType.Returns the type of this NoValueColour + * + * @return the type of this NoValueColour + */ + public int getType() + { + return this.type; + } + + /** + * Method init. + * + * @return the initialized Hashtable for the member table + */ + private static java.util.Hashtable init() + { + Hashtable members = new Hashtable(); + members.put("None", NONE); + members.put("Min", MIN); + members.put("Max", MAX); + return members; + } + + /** + * Method readResolve. will be called during deserialization to replace the + * deserialized object with the correct constant instance. + * + * @return this deserialized object + */ + private java.lang.Object readResolve() + { + return valueOf(this.stringValue); + } + + /** + * Method toString.Returns the String representation of this NoValueColour + * + * @return the String representation of this NoValueColour + */ + public java.lang.String toString() + { + return this.stringValue; + } + + /** + * Method valueOf.Returns a new NoValueColour based on the given String value. + * + * @param string + * @return the NoValueColour value of parameter 'string' + */ + public static jalview.binding.types.NoValueColour valueOf( + final java.lang.String string) + { + java.lang.Object obj = null; + if (string != null) + { + obj = _memberTable.get(string); + } + if (obj == null) + { + String err = "" + string + " is not a valid NoValueColour"; + throw new IllegalArgumentException(err); + } + return (NoValueColour) obj; + } + +} diff --git a/src/jalview/controller/AlignViewController.java b/src/jalview/controller/AlignViewController.java index 460c2b3..d992e4e 100644 --- a/src/jalview/controller/AlignViewController.java +++ b/src/jalview/controller/AlignViewController.java @@ -53,20 +53,19 @@ public class AlignViewController implements AlignViewControllerI private AlignViewControllerGuiI avcg; public AlignViewController(AlignViewControllerGuiI alignFrame, - AlignViewportI viewport, AlignmentViewPanel alignPanel) + AlignViewportI vp, AlignmentViewPanel ap) { this.avcg = alignFrame; - this.viewport = viewport; - this.alignPanel = alignPanel; + this.viewport = vp; + this.alignPanel = ap; } @Override - public void setViewportAndAlignmentPanel(AlignViewportI viewport, - AlignmentViewPanel alignPanel) + public void setViewportAndAlignmentPanel(AlignViewportI vp, + AlignmentViewPanel ap) { - this.alignPanel = alignPanel; - this.viewport = viewport; - + this.alignPanel = ap; + this.viewport = vp; } @Override @@ -215,17 +214,21 @@ public class AlignViewController implements AlignViewControllerI /** * Sets a bit in the BitSet for each column (base 0) in the sequence - * collection which includes the specified feature type. Returns the number of - * sequences which have the feature in the selected range. + * collection which includes a visible feature of the specified feature type. + * Returns the number of sequences which have the feature visible in the + * selected range. * * @param featureType * @param sqcol * @param bs * @return */ - static int findColumnsWithFeature(String featureType, + int findColumnsWithFeature(String featureType, SequenceCollectionI sqcol, BitSet bs) { + FeatureRenderer fr = alignPanel == null ? null : alignPanel + .getFeatureRenderer(); + final int startColumn = sqcol.getStartRes() + 1; // converted to base 1 final int endColumn = sqcol.getEndRes() + 1; List seqs = sqcol.getSequences(); @@ -238,13 +241,19 @@ public class AlignViewController implements AlignViewControllerI List sfs = sq.findFeatures(startColumn, endColumn, featureType); - if (!sfs.isEmpty()) - { - nseq++; - } - + boolean found = false; for (SequenceFeature sf : sfs) { + if (fr.getColour(sf) == null) + { + continue; + } + if (!found) + { + nseq++; + } + found = true; + int sfStartCol = sq.findIndex(sf.getBegin()); int sfEndCol = sq.findIndex(sf.getEnd()); @@ -343,25 +352,25 @@ public class AlignViewController implements AlignViewControllerI public boolean parseFeaturesFile(String file, DataSourceType protocol, boolean relaxedIdMatching) { - boolean featuresFile = false; + boolean featuresAdded = false; + FeatureRenderer fr = alignPanel.getFeatureRenderer(); try { - featuresFile = new FeaturesFile(false, file, protocol).parse( - viewport.getAlignment().getDataset(), - alignPanel.getFeatureRenderer().getFeatureColours(), false, - relaxedIdMatching); + featuresAdded = new FeaturesFile(false, file, protocol).parse( + viewport.getAlignment().getDataset(), fr.getFeatureColours(), + fr.getFeatureFilters(), false, relaxedIdMatching); } catch (Exception ex) { ex.printStackTrace(); } - if (featuresFile) + if (featuresAdded) { avcg.refreshFeatureUI(true); - if (alignPanel.getFeatureRenderer() != null) + if (fr != null) { // update the min/max ranges where necessary - alignPanel.getFeatureRenderer().findAllFeatures(true); + fr.findAllFeatures(true); } if (avcg.getFeatureSettingsUI() != null) { @@ -370,7 +379,7 @@ public class AlignViewController implements AlignViewControllerI alignPanel.paintAlignment(true, true); } - return featuresFile; + return featuresAdded; } diff --git a/src/jalview/datamodel/DBRefEntry.java b/src/jalview/datamodel/DBRefEntry.java index f7837f7..98868ce 100755 --- a/src/jalview/datamodel/DBRefEntry.java +++ b/src/jalview/datamodel/DBRefEntry.java @@ -27,7 +27,20 @@ import java.util.List; public class DBRefEntry implements DBRefEntryI { - String source = "", version = "", accessionId = ""; + /* + * the mapping to chromosome (genome) is held as an instance with + * source = speciesId + * version = assemblyId + * accessionId = "chromosome:" + chromosomeId + * map = mapping from sequence to reference assembly + */ + public static final String CHROMOSOME = "chromosome"; + + String source = ""; + + String version = ""; + + String accessionId = ""; /** * maps from associated sequence to the database sequence's coordinate system @@ -331,4 +344,14 @@ public class DBRefEntry implements DBRefEntryI } return true; } + + /** + * Mappings to chromosome are held with accessionId as "chromosome:id" + * + * @return + */ + public boolean isChromosome() + { + return accessionId != null && accessionId.startsWith(CHROMOSOME + ":"); + } } diff --git a/src/jalview/datamodel/GeneLociI.java b/src/jalview/datamodel/GeneLociI.java new file mode 100644 index 0000000..f8c7ec5 --- /dev/null +++ b/src/jalview/datamodel/GeneLociI.java @@ -0,0 +1,38 @@ +package jalview.datamodel; + +import jalview.util.MapList; + +/** + * An interface to model one or more contiguous regions on one chromosome + */ +public interface GeneLociI +{ + /** + * Answers the species identifier + * + * @return + */ + String getSpeciesId(); + + /** + * Answers the reference assembly identifier + * + * @return + */ + String getAssemblyId(); + + /** + * Answers the chromosome identifier e.g. "2", "Y", "II" + * + * @return + */ + String getChromosomeId(); + + /** + * Answers the mapping from sequence to chromosome loci. For a reverse strand + * mapping, the chromosomal ranges will have start > end. + * + * @return + */ + MapList getMap(); +} diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index 15d1378..441d8d0 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -657,10 +657,10 @@ public class Sequence extends ASequence implements SequenceI } /** - * DOCUMENT ME! + * Sets the sequence description, and also parses out any special formats of + * interest * * @param desc - * DOCUMENT ME! */ @Override public void setDescription(String desc) @@ -668,10 +668,67 @@ public class Sequence extends ASequence implements SequenceI this.description = desc; } + @Override + public void setGeneLoci(String speciesId, String assemblyId, + String chromosomeId, MapList map) + { + addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME + + ":" + chromosomeId, new Mapping(map))); + } + /** - * DOCUMENT ME! + * Returns the gene loci mapping for the sequence (may be null) * - * @return DOCUMENT ME! + * @return + */ + @Override + public GeneLociI getGeneLoci() + { + DBRefEntry[] refs = getDBRefs(); + if (refs != null) + { + for (final DBRefEntry ref : refs) + { + if (ref.isChromosome()) + { + return new GeneLociI() + { + @Override + public String getSpeciesId() + { + return ref.getSource(); + } + + @Override + public String getAssemblyId() + { + return ref.getVersion(); + } + + @Override + public String getChromosomeId() + { + // strip off "chromosome:" prefix to chrId + return ref.getAccessionId().substring( + DBRefEntry.CHROMOSOME.length() + 1); + } + + @Override + public MapList getMap() + { + return ref.getMap().getMap(); + } + }; + } + } + } + return null; + } + + /** + * Answers the description + * + * @return */ @Override public String getDescription() diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 9c4087e..34565c6 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -20,18 +20,24 @@ */ 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.SortedMap; +import java.util.TreeMap; import java.util.Vector; /** - * DOCUMENT ME! - * - * @author $author$ - * @version $Revision$ + * A class that models a single contiguous feature on a sequence. If flag + * 'contactFeature' is true, the start and end positions are interpreted instead + * as two contact points. */ public class SequenceFeature implements FeatureLocationI { @@ -51,6 +57,8 @@ public class SequenceFeature implements FeatureLocationI // private key for ENA location designed not to conflict with real GFF data private static final String LOCATION = "!Location"; + private static final String ROW_DATA = "%s%s%s"; + /* * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as * name1=value1;name2=value2,value3;...etc @@ -84,6 +92,12 @@ public class SequenceFeature implements FeatureLocationI public Vector 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 @@ -155,9 +169,11 @@ public class SequenceFeature implements FeatureLocationI this(newType, sf.getDescription(), newBegin, newEnd, newScore, newGroup); + this.source = sf.source; + if (sf.otherDetails != null) { - otherDetails = new HashMap(); + otherDetails = new HashMap<>(); for (Entry entry : sf.otherDetails.entrySet()) { otherDetails.put(entry.getKey(), entry.getValue()); @@ -165,7 +181,7 @@ public class SequenceFeature implements FeatureLocationI } if (sf.links != null && sf.links.size() > 0) { - links = new Vector(); + links = new Vector<>(); for (int i = 0, iSize = sf.links.size(); i < iSize; i++) { links.addElement(sf.links.elementAt(i)); @@ -332,7 +348,7 @@ public class SequenceFeature implements FeatureLocationI { if (links == null) { - links = new Vector(); + links = new Vector<>(); } if (!links.contains(labelLink)) @@ -366,6 +382,30 @@ public class SequenceFeature implements FeatureLocationI } /** + * Answers the value of the specified attribute as string, or null if no such + * value. If more than one attribute name is provided, tries to resolve as keys + * to nested maps. For example, if attribute "CSQ" holds a map of key-value + * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele" + * in that map. + * + * @param key + * @return + */ + public String getValueAsString(String... key) + { + if (otherDetails == null) + { + return null; + } + Object value = otherDetails.get(key[0]); + if (key.length > 1 && value instanceof Map) + { + value = ((Map) value).get(key[1]); + } + return value == null ? null : value.toString(); + } + + /** * Returns a property value for the given key if known, else the specified * default value * @@ -394,13 +434,35 @@ public class SequenceFeature implements FeatureLocationI { if (otherDetails == null) { - otherDetails = new HashMap(); + 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. @@ -535,4 +597,142 @@ public class SequenceFeature implements FeatureLocationI { return begin == 0 && end == 0; } + + /** + * Answers an html-formatted report of feature details + * + * @return + */ + public String getDetailsReport() + { + FeatureSourceI metadata = FeatureSources.getInstance() + .getSource(source); + + StringBuilder sb = new StringBuilder(128); + sb.append("
"); + sb.append(""); + 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 ordered = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + ordered.putAll(otherDetails); + + for (Entry entry : ordered.entrySet()) + { + String key = entry.getKey(); + if (ATTRIBUTES.equals(key)) + { + continue; // to avoid double reporting + } + + Object value = entry.getValue(); + if (value instanceof Map) + { + /* + * expand values in a Map attribute across separate lines + * copy to a TreeMap for alphabetical ordering + */ + Map values = (Map) value; + SortedMap sm = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + sm.putAll(values); + for (Entry e : sm.entrySet()) + { + sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e + .getValue().toString())); + } + } + else + { + // tried
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("
"); + + 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; + } } diff --git a/src/jalview/datamodel/SequenceI.java b/src/jalview/datamodel/SequenceI.java index 2f3e925..fb723e6 100755 --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@ -21,6 +21,7 @@ package jalview.datamodel; import jalview.datamodel.features.SequenceFeaturesI; +import jalview.util.MapList; import java.util.BitSet; import java.util.List; @@ -524,4 +525,22 @@ public interface SequenceI extends ASequenceI * @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); } diff --git a/src/jalview/datamodel/features/FeatureAttributeType.java b/src/jalview/datamodel/features/FeatureAttributeType.java new file mode 100644 index 0000000..fd3069d --- /dev/null +++ b/src/jalview/datamodel/features/FeatureAttributeType.java @@ -0,0 +1,12 @@ +package jalview.datamodel.features; + +/** + * A class to model the datatype of feature attributes. + * + * @author gmcarstairs + * + */ +public enum FeatureAttributeType +{ + String, Integer, Float, Character, Flag; +} diff --git a/src/jalview/datamodel/features/FeatureAttributes.java b/src/jalview/datamodel/features/FeatureAttributes.java new file mode 100644 index 0000000..e359b62 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureAttributes.java @@ -0,0 +1,360 @@ +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> attributes; + + /* + * a case-insensitive comparator so that attributes are ordered e.g. + * AC + * af + * CSQ:AFR_MAF + * CSQ:Allele + */ + private Comparator comparator = new Comparator() + { + @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 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 getAttributes(String featureType) + { + if (!attributes.containsKey(featureType)) + { + return Collections. 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 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 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 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 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 atts = attributes.get(featureType); + if (atts != null) + { + AttributeData attData = atts.get(attName); + if (attData != null) + { + return attData.getType(); + } + } + return null; + } +} diff --git a/src/jalview/datamodel/features/FeatureMatcher.java b/src/jalview/datamodel/features/FeatureMatcher.java new file mode 100644 index 0000000..f844141 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureMatcher.java @@ -0,0 +1,417 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; +import jalview.util.MessageManager; +import jalview.util.matcher.Condition; +import jalview.util.matcher.Matcher; +import jalview.util.matcher.MatcherI; + +/** + * An immutable class that models one or more match conditions, each of which is + * applied to the value obtained by lookup given the match key. + *

+ * For example, the value provider could be a SequenceFeature's attributes map, + * and the conditions might be + *

    + *
  • CSQ contains "pathological"
  • + *
  • AND
  • + *
  • AF <= 1.0e-5
  • + *
+ * + * @author gmcarstairs + * + */ +public class FeatureMatcher implements FeatureMatcherI +{ + private static final String SCORE = "Score"; + + private static final String LABEL = "Label"; + + private static final String SPACE = " "; + + private static final String QUOTE = "'"; + + /* + * a dummy matcher that comes in useful for the 'add a filter' gui row + */ + public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher + .byLabel(Condition.values()[0], ""); + + private static final String COLON = ":"; + + /* + * if true, match is against feature description + */ + final private boolean byLabel; + + /* + * if true, match is against feature score + */ + final private boolean byScore; + + /* + * if not null, match is against feature attribute [sub-attribute] + */ + final private String[] key; + + final private MatcherI matcher; + + /** + * A helper method that converts a 'compound' attribute name from its display + * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" } + * + * @param attribute + * @return + */ + public static String[] fromAttributeDisplayName(String attribute) + { + return attribute == null ? null : attribute.split(COLON); + } + + /** + * A helper method that converts a 'compound' attribute name to its display + * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" } + * + * @param attName + * @return + */ + public static String toAttributeDisplayName(String[] attName) + { + return attName == null ? "" : String.join(COLON, attName); + } + + /** + * A factory constructor that converts a stringified object (as output by + * toStableString) to an object instance. Returns null if parsing fails. + *

+ * Leniency in parsing (for manually created feature files): + *

    + *
  • keywords Score and Label, and the condition, are not + * case-sensitive
  • + *
  • quotes around value and pattern are optional if string does not include + * a space
  • + *
+ * + * @param descriptor + * @return + */ + public static FeatureMatcher fromString(final String descriptor) + { + String invalidFormat = "Invalid matcher format: " + descriptor; + + /* + * expect + * value condition pattern + * where value is Label or Space or attributeName or attName1:attName2 + * and pattern is a float value as string, or a text string + * attribute names or patterns may be quoted (must be if include space) + */ + String attName = null; + boolean byScore = false; + boolean byLabel = false; + Condition cond = null; + String pattern = null; + + /* + * parse first field (Label / Score / attribute) + * optionally in quotes (required if attName includes space) + */ + String leftToParse = descriptor; + String firstField = null; + + if (descriptor.startsWith(QUOTE)) + { + // 'Label' / 'Score' / 'attName' + int nextQuotePos = descriptor.indexOf(QUOTE, 1); + if (nextQuotePos == -1) + { + System.err.println(invalidFormat); + return null; + } + firstField = descriptor.substring(1, nextQuotePos); + leftToParse = descriptor.substring(nextQuotePos + 1).trim(); + } + else + { + // Label / Score / attName (unquoted) + int nextSpacePos = descriptor.indexOf(SPACE); + if (nextSpacePos == -1) + { + System.err.println(invalidFormat); + return null; + } + firstField = descriptor.substring(0, nextSpacePos); + leftToParse = descriptor.substring(nextSpacePos + 1).trim(); + } + String lower = firstField.toLowerCase(); + if (lower.startsWith(LABEL.toLowerCase())) + { + byLabel = true; + } + else if (lower.startsWith(SCORE.toLowerCase())) + { + byScore = true; + } + else + { + attName = firstField; + } + + /* + * next field is the comparison condition + * most conditions require a following pattern (optionally quoted) + * although some conditions e.g. Present do not + */ + int nextSpacePos = leftToParse.indexOf(SPACE); + if (nextSpacePos == -1) + { + /* + * no value following condition - only valid for some conditions + */ + cond = Condition.fromString(leftToParse); + if (cond == null || cond.needsAPattern()) + { + System.err.println(invalidFormat); + return null; + } + } + else + { + /* + * condition and pattern + */ + cond = Condition.fromString(leftToParse.substring(0, nextSpacePos)); + leftToParse = leftToParse.substring(nextSpacePos + 1).trim(); + if (leftToParse.startsWith(QUOTE)) + { + // pattern in quotes + if (leftToParse.endsWith(QUOTE)) + { + pattern = leftToParse.substring(1, leftToParse.length() - 1); + } + else + { + // unbalanced quote + System.err.println(invalidFormat); + return null; + } + } + else + { + // unquoted pattern + pattern = leftToParse; + } + } + + /* + * we have parsed out value, condition and pattern + * so can now make the FeatureMatcher + */ + try + { + if (byLabel) + { + return FeatureMatcher.byLabel(cond, pattern); + } + else if (byScore) + { + return FeatureMatcher.byScore(cond, pattern); + } + else + { + String[] attNames = FeatureMatcher + .fromAttributeDisplayName(attName); + return FeatureMatcher.byAttribute(cond, pattern, attNames); + } + } catch (NumberFormatException e) + { + // numeric condition with non-numeric pattern + return null; + } + } + + /** + * A factory constructor method for a matcher that applies its match condition + * to the feature label (description) + * + * @param cond + * @param pattern + * @return + * @throws NumberFormatException + * if an invalid numeric pattern is supplied + */ + public static FeatureMatcher byLabel(Condition cond, String pattern) + { + return new FeatureMatcher(new Matcher(cond, pattern), true, false, + null); + } + + /** + * A factory constructor method for a matcher that applies its match condition + * to the feature score + * + * @param cond + * @param pattern + * @return + * @throws NumberFormatException + * if an invalid numeric pattern is supplied + */ + public static FeatureMatcher byScore(Condition cond, String pattern) + { + return new FeatureMatcher(new Matcher(cond, pattern), false, true, + null); + } + + /** + * A factory constructor method for a matcher that applies its match condition + * to the named feature attribute [and optional sub-attribute] + * + * @param cond + * @param pattern + * @param attName + * @return + * @throws NumberFormatException + * if an invalid numeric pattern is supplied + */ + public static FeatureMatcher byAttribute(Condition cond, String pattern, + String... attName) + { + return new FeatureMatcher(new Matcher(cond, pattern), false, false, + attName); + } + + private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore, + String[] theKey) + { + key = theKey; + matcher = m; + byLabel = forLabel; + byScore = forScore; + } + @Override + public boolean matches(SequenceFeature feature) + { + String value = byLabel ? feature.getDescription() + : (byScore ? String.valueOf(feature.getScore()) + : feature.getValueAsString(key)); + return matcher.matches(value); + } + + @Override + public String[] getAttribute() + { + return key; + } + + @Override + public MatcherI getMatcher() + { + return matcher; + } + + /** + * Answers a string description of this matcher, suitable for display, debugging + * or logging. The format may change in future. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if (byLabel) + { + sb.append(MessageManager.getString("label.label")); + } + else if (byScore) + { + sb.append(MessageManager.getString("label.score")); + } + else + { + sb.append(String.join(COLON, key)); + } + + Condition condition = matcher.getCondition(); + sb.append(SPACE).append(condition.toString().toLowerCase()); + if (condition.isNumeric()) + { + sb.append(SPACE).append(matcher.getPattern()); + } + else if (condition.needsAPattern()) + { + sb.append(" '").append(matcher.getPattern()).append(QUOTE); + } + + return sb.toString(); + } + + @Override + public boolean isByLabel() + { + return byLabel; + } + + @Override + public boolean isByScore() + { + return byScore; + } + + @Override + public boolean isByAttribute() + { + return getAttribute() != null; + } + + /** + * {@inheritDoc} The output of this method should be parseable by method + * fromString to restore the original object. + */ + @Override + public String toStableString() + { + StringBuilder sb = new StringBuilder(); + if (byLabel) + { + sb.append(LABEL); // no i18n here unlike toString() ! + } + else if (byScore) + { + sb.append(SCORE); + } + else + { + /* + * enclose attribute name in quotes if it includes space + */ + String displayName = toAttributeDisplayName(key); + if (displayName.contains(SPACE)) + { + sb.append(QUOTE).append(displayName).append(QUOTE); + } + else + { + sb.append(displayName); + } + } + + Condition condition = matcher.getCondition(); + sb.append(SPACE).append(condition.getStableName()); + String pattern = matcher.getPattern(); + if (condition.needsAPattern()) + { + /* + * enclose pattern in quotes if it includes space + */ + if (pattern.contains(SPACE)) + { + sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE); + } + else + { + sb.append(SPACE).append(pattern); + } + } + + return sb.toString(); + } +} diff --git a/src/jalview/datamodel/features/FeatureMatcherI.java b/src/jalview/datamodel/features/FeatureMatcherI.java new file mode 100644 index 0000000..f1f8585 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureMatcherI.java @@ -0,0 +1,65 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; +import jalview.util.matcher.MatcherI; + +/** + * An interface for an object that can apply a match condition to a + * SequenceFeature object + * + * @author gmcarstairs + */ +public interface FeatureMatcherI +{ + /** + * Answers true if the value provided for this matcher's key passes this + * matcher's match condition + * + * @param feature + * @return + */ + boolean matches(SequenceFeature feature); + + /** + * Answers the attribute key this matcher operates on (or null if match is by + * Label or Score) + * + * @return + */ + String[] getAttribute(); + + /** + * Answers true if match is against feature label (description), else false + * + * @return + */ + boolean isByLabel(); + + /** + * Answers true if match is against feature score, else false + * + * @return + */ + boolean isByScore(); + + /** + * Answers true if match is against a feature attribute (text or range) + * + * @return + */ + boolean isByAttribute(); + + /** + * Answers the match condition that is applied + * + * @return + */ + MatcherI getMatcher(); + + /** + * Answers a string representation of this object suitable for use when + * persisting data, in a format that can be reliably read back. Any changes to + * the format should be backwards compatible. + */ + String toStableString(); +} diff --git a/src/jalview/datamodel/features/FeatureMatcherSet.java b/src/jalview/datamodel/features/FeatureMatcherSet.java new file mode 100644 index 0000000..b51f2f0 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureMatcherSet.java @@ -0,0 +1,294 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; +import jalview.util.MessageManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * A class that models one or more match conditions, which may be combined with + * AND or OR (but not a mixture) + * + * @author gmcarstairs + */ +public class FeatureMatcherSet implements FeatureMatcherSetI +{ + private static final String OR = "OR"; + + private static final String AND = "AND"; + + private static final String SPACE = " "; + + private static final String CLOSE_BRACKET = ")"; + + private static final String OPEN_BRACKET = "("; + + private static final String OR_I18N = MessageManager + .getString("label.or"); + + private static final String AND_18N = MessageManager + .getString("label.and"); + + List matchConditions; + + boolean andConditions; + + /** + * A factory constructor that converts a stringified object (as output by + * toStableString) to an object instance. + * + * Format: + *
    + *
  • (condition1) AND (condition2) AND (condition3)
  • + *
  • or
  • + *
  • (condition1) OR (condition2) OR (condition3)
  • + *
+ * where OR and AND are not case-sensitive, and may not be mixed. Brackets are + * optional if there is only one condition. + * + * @param descriptor + * @return + * @see FeatureMatcher#fromString(String) + */ + public static FeatureMatcherSet fromString(final String descriptor) + { + String invalid = "Invalid descriptor: " + descriptor; + boolean firstCondition = true; + FeatureMatcherSet result = new FeatureMatcherSet(); + + String leftToParse = descriptor.trim(); + + while (leftToParse.length() > 0) + { + /* + * inspect AND or OR condition, check not mixed + */ + boolean and = true; + if (!firstCondition) + { + int spacePos = leftToParse.indexOf(SPACE); + if (spacePos == -1) + { + // trailing junk after a match condition + System.err.println(invalid); + return null; + } + String conjunction = leftToParse.substring(0, spacePos); + leftToParse = leftToParse.substring(spacePos + 1).trim(); + if (conjunction.equalsIgnoreCase(AND)) + { + and = true; + } + else if (conjunction.equalsIgnoreCase(OR)) + { + and = false; + } + else + { + // not an AND or an OR - invalid + System.err.println(invalid); + return null; + } + } + + /* + * now extract the next condition and AND or OR it + */ + String nextCondition = leftToParse; + if (leftToParse.startsWith(OPEN_BRACKET)) + { + int closePos = leftToParse.indexOf(CLOSE_BRACKET); + if (closePos == -1) + { + System.err.println(invalid); + return null; + } + nextCondition = leftToParse.substring(1, closePos); + leftToParse = leftToParse.substring(closePos + 1).trim(); + } + else + { + leftToParse = ""; + } + + FeatureMatcher fm = FeatureMatcher.fromString(nextCondition); + if (fm == null) + { + System.err.println(invalid); + return null; + } + try + { + if (and) + { + result.and(fm); + } + else + { + result.or(fm); + } + firstCondition = false; + } catch (IllegalStateException e) + { + // thrown if OR and AND are mixed + System.err.println(invalid); + return null; + } + + } + return result; + } + + /** + * Constructor + */ + public FeatureMatcherSet() + { + matchConditions = new ArrayList<>(); + } + + @Override + public boolean matches(SequenceFeature feature) + { + /* + * no conditions matches anything + */ + if (matchConditions.isEmpty()) + { + return true; + } + + /* + * AND until failure + */ + if (andConditions) + { + for (FeatureMatcherI m : matchConditions) + { + if (!m.matches(feature)) + { + return false; + } + } + return true; + } + + /* + * OR until match + */ + for (FeatureMatcherI m : matchConditions) + { + if (m.matches(feature)) + { + return true; + } + } + return false; + } + + @Override + public void and(FeatureMatcherI m) + { + if (!andConditions && matchConditions.size() > 1) + { + throw new IllegalStateException("Can't add an AND to OR conditions"); + } + matchConditions.add(m); + andConditions = true; + } + + @Override + public void or(FeatureMatcherI m) + { + if (andConditions && matchConditions.size() > 1) + { + throw new IllegalStateException("Can't add an OR to AND conditions"); + } + matchConditions.add(m); + andConditions = false; + } + + @Override + public boolean isAnded() + { + return andConditions; + } + + @Override + public Iterable getMatchers() + { + return matchConditions; + } + + /** + * Answers a string representation of this object suitable for display, and + * possibly internationalized. The format is not guaranteed stable and may + * change in future. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + boolean first = true; + boolean multiple = matchConditions.size() > 1; + for (FeatureMatcherI matcher : matchConditions) + { + if (!first) + { + String joiner = andConditions ? AND_18N : OR_I18N; + sb.append(SPACE).append(joiner.toLowerCase()).append(SPACE); + } + first = false; + if (multiple) + { + sb.append(OPEN_BRACKET).append(matcher.toString()) + .append(CLOSE_BRACKET); + } + else + { + sb.append(matcher.toString()); + } + } + return sb.toString(); + } + + @Override + public boolean isEmpty() + { + return matchConditions == null || matchConditions.isEmpty(); + } + + /** + * {@inheritDoc} The output of this method should be parseable by method + * fromString to restore the original object. + */ + @Override + public String toStableString() + { + StringBuilder sb = new StringBuilder(); + boolean moreThanOne = matchConditions.size() > 1; + boolean first = true; + + for (FeatureMatcherI matcher : matchConditions) + { + if (!first) + { + String joiner = andConditions ? AND : OR; + sb.append(SPACE).append(joiner).append(SPACE); + } + first = false; + if (moreThanOne) + { + sb.append(OPEN_BRACKET).append(matcher.toStableString()) + .append(CLOSE_BRACKET); + } + else + { + sb.append(matcher.toStableString()); + } + } + return sb.toString(); + } + +} diff --git a/src/jalview/datamodel/features/FeatureMatcherSetI.java b/src/jalview/datamodel/features/FeatureMatcherSetI.java new file mode 100644 index 0000000..90c2986 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureMatcherSetI.java @@ -0,0 +1,68 @@ +package jalview.datamodel.features; + +import jalview.datamodel.SequenceFeature; + +/** + * An interface to describe a set of one or more feature matchers, where all + * matchers are combined with either AND or OR + * + * @author gmcarstairs + * + */ +public interface FeatureMatcherSetI +{ + /** + * Answers true if the feature provided passes this matcher's match condition + * + * @param feature + * @return + */ + boolean matches(SequenceFeature feature); + + /** + * Adds (ANDs) match condition m to this object's matcher set + * + * @param m + * @throws IllegalStateException + * if an attempt is made to AND to existing OR-ed conditions + */ + void and(FeatureMatcherI m); + + /** + * Answers true if any second condition is AND-ed with this one, false if it + * is OR-ed + * + * @return + */ + boolean isAnded(); + + /** + * Adds (ORs) the given condition to this object's match conditions + * + * @param m + * @throws IllegalStateException + * if an attempt is made to OR to existing AND-ed conditions + */ + void or(FeatureMatcherI m); + + /** + * Answers an iterator over the combined match conditions + * + * @return + */ + Iterable getMatchers(); + + /** + * Answers true if this object contains no conditions + * + * @return + */ + boolean isEmpty(); + + /** + * Answers a string representation of this object suitable for use when + * persisting data, in a format that can be reliably read back. Any changes to + * the format should be backwards compatible. + */ + String toStableString(); +} diff --git a/src/jalview/datamodel/features/FeatureSource.java b/src/jalview/datamodel/features/FeatureSource.java new file mode 100644 index 0000000..a1be1dc --- /dev/null +++ b/src/jalview/datamodel/features/FeatureSource.java @@ -0,0 +1,78 @@ +package jalview.datamodel.features; + +import java.util.HashMap; +import java.util.Map; + +/** + * A class to model one source of feature data, including metadata about + * attributes of features + * + * @author gmcarstairs + * + */ +public class FeatureSource implements FeatureSourceI +{ + private String name; + + private Map attributeNames; + + private Map attributeTypes; + + /** + * Constructor + * + * @param theName + */ + public FeatureSource(String theName) + { + this.name = theName; + attributeNames = new HashMap<>(); + attributeTypes = new HashMap<>(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() + { + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public String getAttributeName(String attributeId) + { + return attributeNames.get(attributeId); + } + + /** + * {@inheritDoc} + */ + @Override + public FeatureAttributeType getAttributeType(String attributeId) + { + return attributeTypes.get(attributeId); + } + + /** + * {@inheritDoc} + */ + @Override + public void setAttributeName(String id, String attName) + { + attributeNames.put(id, attName); + } + + /** + * {@inheritDoc} + */ + @Override + public void setAttributeType(String id, FeatureAttributeType type) + { + attributeTypes.put(id, type); + } + +} diff --git a/src/jalview/datamodel/features/FeatureSourceI.java b/src/jalview/datamodel/features/FeatureSourceI.java new file mode 100644 index 0000000..c873593 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureSourceI.java @@ -0,0 +1,45 @@ +package jalview.datamodel.features; + +public interface FeatureSourceI +{ + /** + * Answers a name for the feature source (not necessarily unique) + * + * @return + */ + String getName(); + + /** + * Answers the 'long name' of an attribute given its id (short name or + * abbreviation), or null if not known + * + * @param attributeId + * @return + */ + String getAttributeName(String attributeId); + + /** + * Sets the 'long name' of an attribute given its id (short name or + * abbreviation). + * + * @param id + * @param name + */ + void setAttributeName(String id, String name); + + /** + * Answers the datatype of the attribute with given id, or null if not known + * + * @param attributeId + * @return + */ + FeatureAttributeType getAttributeType(String attributeId); + + /** + * Sets the datatype of the attribute with given id + * + * @param id + * @param type + */ + void setAttributeType(String id, FeatureAttributeType type); +} diff --git a/src/jalview/datamodel/features/FeatureSources.java b/src/jalview/datamodel/features/FeatureSources.java new file mode 100644 index 0000000..1be1b82 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureSources.java @@ -0,0 +1,58 @@ +package jalview.datamodel.features; + +import java.util.HashMap; +import java.util.Map; + +/** + * A singleton to hold metadata about feature attributes, keyed by a unique + * feature source identifier + * + * @author gmcarstairs + * + */ +public class FeatureSources +{ + private static FeatureSources instance = new FeatureSources(); + + private Map 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); + } +} diff --git a/src/jalview/ext/ensembl/EnsemblData.java b/src/jalview/ext/ensembl/EnsemblData.java new file mode 100644 index 0000000..47fe0fc --- /dev/null +++ b/src/jalview/ext/ensembl/EnsemblData.java @@ -0,0 +1,91 @@ +/* + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors + * + * This file is part of Jalview. + * + * Jalview is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Jalview is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Jalview. If not, see . + * 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; + } + +} diff --git a/src/jalview/ext/ensembl/EnsemblGene.java b/src/jalview/ext/ensembl/EnsemblGene.java index 0d5fc26..7e6f653 100644 --- a/src/jalview/ext/ensembl/EnsemblGene.java +++ b/src/jalview/ext/ensembl/EnsemblGene.java @@ -23,6 +23,8 @@ package jalview.ext.ensembl; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsModelI; import jalview.datamodel.AlignmentI; +import jalview.datamodel.DBRefEntry; +import jalview.datamodel.GeneLociI; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; @@ -150,10 +152,14 @@ public class EnsemblGene extends EnsemblSeqProxy { continue; } + if (geneAlignment.getHeight() == 1) { // ensure id has 'correct' case for the Ensembl identifier geneId = geneAlignment.getSequenceAt(0).getName(); + + findGeneLoci(geneAlignment.getSequenceAt(0), geneId); + getTranscripts(geneAlignment, geneId); } if (al == null) @@ -169,6 +175,67 @@ public class EnsemblGene extends EnsemblSeqProxy } /** + * Calls the /lookup/id REST service, parses the response for gene + * coordinates, and if successful, adds these to the sequence. If this fails, + * fall back on trying to parse the sequence description in case it is in + * Ensembl-gene format e.g. chromosome:GRCh38:17:45051610:45109016:1. + * + * @param seq + * @param geneId + */ + void findGeneLoci(SequenceI seq, String geneId) + { + GeneLociI geneLoci = new EnsemblLookup(getDomain()).getGeneLoci(geneId); + if (geneLoci != null) + { + seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(), + geneLoci.getChromosomeId(), geneLoci.getMap()); + } + else + { + parseChromosomeLocations(seq); + } + } + + /** + * Parses and saves fields of an Ensembl-style description e.g. + * chromosome:GRCh38:17:45051610:45109016:1 + * + * @param seq + */ + boolean parseChromosomeLocations(SequenceI seq) + { + String description = seq.getDescription(); + if (description == null) + { + return false; + } + String[] tokens = description.split(":"); + if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME)) + { + String ref = tokens[1]; + String chrom = tokens[2]; + try + { + int chStart = Integer.parseInt(tokens[3]); + int chEnd = Integer.parseInt(tokens[4]); + boolean forwardStrand = "1".equals(tokens[5]); + String species = ""; // not known here + int[] from = new int[] { seq.getStart(), seq.getEnd() }; + int[] to = new int[] { forwardStrand ? chStart : chEnd, + forwardStrand ? chEnd : chStart }; + MapList map = new MapList(from, to, 1, 1); + seq.setGeneLoci(species, ref, chrom, map); + return true; + } catch (NumberFormatException e) + { + System.err.println("Bad integers in description " + description); + } + } + return false; + } + + /** * Converts a query, which may contain one or more gene, transcript, or * external (to Ensembl) identifiers, into a non-redundant list of gene * identifiers. @@ -362,6 +429,8 @@ public class EnsemblGene extends EnsemblSeqProxy cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(), transcript.getDatasetSequence(), mapping, parentId); + mapTranscriptToChromosome(transcript, gene, mapping); + /* * fetch and save cross-references */ @@ -376,6 +445,42 @@ public class EnsemblGene extends EnsemblSeqProxy } /** + * If the gene has a mapping to chromosome coordinates, derive the transcript + * chromosome regions and save on the transcript sequence + * + * @param transcript + * @param gene + * @param mapping + * the mapping from gene to transcript positions + */ + protected void mapTranscriptToChromosome(SequenceI transcript, + SequenceI gene, MapList mapping) + { + GeneLociI loci = gene.getGeneLoci(); + if (loci == null) + { + return; + } + + MapList geneMapping = loci.getMap(); + + List exons = mapping.getFromRanges(); + List transcriptLoci = new ArrayList<>(); + + for (int[] exon : exons) + { + transcriptLoci.add(geneMapping.locateInTo(exon[0], exon[1])); + } + + List 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 diff --git a/src/jalview/ext/ensembl/EnsemblInfo.java b/src/jalview/ext/ensembl/EnsemblInfo.java index 7668941..de55a53 100644 --- a/src/jalview/ext/ensembl/EnsemblInfo.java +++ b/src/jalview/ext/ensembl/EnsemblInfo.java @@ -1,86 +1,185 @@ -/* - * 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 . - * 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 + *
+   * { 
+   *  { "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" }
+   *  }
+   * 
+ * 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 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 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 getDivisions() { + if (divisions == null) + { + fetchDivisions(); + } + + return divisions.keySet(); + } } diff --git a/src/jalview/ext/ensembl/EnsemblLookup.java b/src/jalview/ext/ensembl/EnsemblLookup.java index 92763a1..f6b3a47 100644 --- a/src/jalview/ext/ensembl/EnsemblLookup.java +++ b/src/jalview/ext/ensembl/EnsemblLookup.java @@ -20,13 +20,17 @@ */ package jalview.ext.ensembl; +import jalview.bin.Cache; import jalview.datamodel.AlignmentI; +import jalview.datamodel.GeneLociI; +import jalview.util.MapList; import java.io.BufferedReader; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.json.simple.JSONObject; @@ -34,13 +38,16 @@ import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; /** - * A client for the Ensembl lookup REST endpoint, used to find the gene - * identifier given a gene, transcript or protein identifier. + * A client for the Ensembl /lookup REST endpoint, used to find the gene + * identifier given a gene, transcript or protein identifier, or to extract the + * species or chromosomal coordinates from the same service response * * @author gmcarstairs */ public class EnsemblLookup extends EnsemblRestClient { + private static final String SPECIES = "species"; + /** * Default constructor (to use rest.ensembl.org) */ @@ -123,27 +130,49 @@ public class EnsemblLookup extends EnsemblRestClient } /** - * Returns the gene id related to the given identifier, which may be for a - * gene, transcript or protein + * Returns the gene id related to the given identifier (which may be for a + * gene, transcript or protein) * * @param identifier * @return */ public String getGeneId(String identifier) { - return getGeneId(identifier, null); + return parseGeneId(getResult(identifier, null)); } /** - * Calls the Ensembl lookup REST endpoint and retrieves the 'Parent' for the + * Calls the Ensembl lookup REST endpoint and retrieves the 'species' for the * given identifier, or null if not found * * @param identifier + * @return + */ + public String getSpecies(String identifier) + { + String species = null; + JSONObject json = getResult(identifier, null); + if (json != null) + { + Object o = json.get(SPECIES); + if (o != null) + { + species = o.toString(); + } + } + return species; + } + + /** + * Calls the /lookup/id rest service and returns the response as a JSONObject, + * or null if any error + * + * @param identifier * @param objectType * (optional) * @return */ - public String getGeneId(String identifier, String objectType) + protected JSONObject getResult(String identifier, String objectType) { List ids = Arrays.asList(new String[] { identifier }); @@ -155,10 +184,11 @@ public class EnsemblLookup extends EnsemblRestClient { br = getHttpResponse(url, ids); } - return br == null ? null : parseResponse(br); - } catch (IOException e) + return br == null ? null : (JSONObject) (new JSONParser().parse(br)); + } catch (IOException | ParseException e) { - // ignore + System.err.println("Error parsing " + identifier + " lookup response " + + e.getMessage()); return null; } finally { @@ -179,41 +209,119 @@ public class EnsemblLookup extends EnsemblRestClient * Parses the JSON response and returns the gene identifier, or null if not * found. If the returned object_type is Gene, returns the id, if Transcript * returns the Parent. If it is Translation (peptide identifier), then the - * Parent is the transcript identifier, so we redo the search with this value. + * Parent is the transcript identifier, so we redo the search with this value, + * specifying that object_type should be Transcript. * - * @param br + * @param jsonObject * @return - * @throws IOException */ - protected String parseResponse(BufferedReader br) throws IOException + protected String parseGeneId(JSONObject json) { + if (json == null) + { + // e.g. lookup failed with 404 not found + return null; + } + String geneId = null; - JSONParser jp = new JSONParser(); + String type = json.get(OBJECT_TYPE).toString(); + if (OBJECT_TYPE_GENE.equalsIgnoreCase(type)) + { + // got the gene - just returns its id + geneId = json.get(JSON_ID).toString(); + } + else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type)) + { + // got the transcript - return its (Gene) Parent + geneId = json.get(PARENT).toString(); + } + else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type)) + { + // got the protein - look up its Parent, restricted to type Transcript + String transcriptId = json.get(PARENT).toString(); + geneId = parseGeneId(getResult(transcriptId, OBJECT_TYPE_TRANSCRIPT)); + } + + return geneId; + } + + /** + * Calls the /lookup/id rest service for the given id, and if successful, + * parses and returns the gene's chromosomal coordinates + * + * @param geneId + * @return + */ + public GeneLociI getGeneLoci(String geneId) + { + return parseGeneLoci(getResult(geneId, OBJECT_TYPE_GENE)); + } + + /** + * Parses the /lookup/id response for species, asssembly_name, + * seq_region_name, start, end and returns an object that wraps them, or null + * if unsuccessful + * + * @param json + * @return + */ + GeneLociI parseGeneLoci(JSONObject json) + { + if (json == null) + { + return null; + } + try { - JSONObject val = (JSONObject) jp.parse(br); - String type = val.get(OBJECT_TYPE).toString(); - if (OBJECT_TYPE_GENE.equalsIgnoreCase(type)) - { - // got the gene - just returns its id - geneId = val.get(ID).toString(); - } - else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type)) - { - // got the transcript - return its (Gene) Parent - geneId = val.get(PARENT).toString(); - } - else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type)) + final String species = json.get("species").toString(); + final String assembly = json.get("assembly_name").toString(); + final String chromosome = json.get("seq_region_name").toString(); + String strand = json.get("strand").toString(); + int start = Integer.parseInt(json.get("start").toString()); + int end = Integer.parseInt(json.get("end").toString()); + int fromEnd = end - start + 1; + boolean reverseStrand = "-1".equals(strand); + int toStart = reverseStrand ? end : start; + int toEnd = reverseStrand ? start : end; + List fromRange = Collections.singletonList(new int[] { 1, + fromEnd }); + List toRange = Collections.singletonList(new int[] { toStart, + toEnd }); + final MapList map = new MapList(fromRange, toRange, 1, 1); + return new GeneLociI() { - // got the protein - get its Parent, restricted to type Transcript - String transcriptId = val.get(PARENT).toString(); - geneId = getGeneId(transcriptId, OBJECT_TYPE_TRANSCRIPT); - } - } catch (ParseException e) + + @Override + public String getSpeciesId() + { + return species == null ? "" : species; + } + + @Override + public String getAssemblyId() + { + return assembly; + } + + @Override + public String getChromosomeId() + { + return chromosome; + } + + @Override + public MapList getMap() + { + return map; + } + }; + } catch (NullPointerException | NumberFormatException e) { - // ignore + Cache.log.error("Error looking up gene loci: " + e.getMessage()); + e.printStackTrace(); } - return geneId; + return null; } } diff --git a/src/jalview/ext/ensembl/EnsemblMap.java b/src/jalview/ext/ensembl/EnsemblMap.java new file mode 100644 index 0000000..56657e0 --- /dev/null +++ b/src/jalview/ext/ensembl/EnsemblMap.java @@ -0,0 +1,422 @@ +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 + * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json + * + * + * @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 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) + * + *
+   *  {"mappings": 
+   *    [{
+   *       "original": {"end":45109016,"start":45051610},
+   *       "mapped"  : {"end":43186384,"start":43128978} 
+   *  }] }
+   * 
+ * + * @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/ or /map/cdna/ 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 + * + *
+   * {"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}
+   *   ]
+   * }
+   * 
+ * + * @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 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 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; + } + +} diff --git a/src/jalview/ext/ensembl/EnsemblRestClient.java b/src/jalview/ext/ensembl/EnsemblRestClient.java index b1bc8e5..e3d1215 100644 --- a/src/jalview/ext/ensembl/EnsemblRestClient.java +++ b/src/jalview/ext/ensembl/EnsemblRestClient.java @@ -72,7 +72,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log"; - private static Map domainData; + private static Map domainData; // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats private static final String PING_URL = "http://rest.ensembl.org/info/ping.json"; @@ -87,8 +87,8 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher { 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)); } @@ -381,7 +381,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher */ protected boolean isEnsemblAvailable() { - EnsemblInfo info = domainData.get(getDomain()); + EnsemblData info = domainData.get(getDomain()); long now = System.currentTimeMillis(); @@ -455,7 +455,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher */ private void checkEnsemblRestVersion() { - EnsemblInfo info = domainData.get(getDomain()); + EnsemblData info = domainData.get(getDomain()); JSONParser jp = new JSONParser(); URL url = null; diff --git a/src/jalview/ext/ensembl/EnsemblSeqProxy.java b/src/jalview/ext/ensembl/EnsemblSeqProxy.java index b2ebb1a..9229379 100644 --- a/src/jalview/ext/ensembl/EnsemblSeqProxy.java +++ b/src/jalview/ext/ensembl/EnsemblSeqProxy.java @@ -34,6 +34,7 @@ import jalview.datamodel.features.SequenceFeatures; 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; @@ -57,8 +58,6 @@ import java.util.List; */ public abstract class EnsemblSeqProxy extends EnsemblRestClient { - private static final String ALLELES = "alleles"; - protected static final String NAME = "Name"; protected static final String DESCRIPTION = "description"; @@ -708,7 +707,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient */ static void reverseComplementAlleles(SequenceFeature sf) { - final String alleles = (String) sf.getValue(ALLELES); + final String alleles = (String) sf.getValue(Gff3Helper.ALLELES); if (alleles == null) { return; @@ -719,7 +718,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient reverseComplementAllele(complement, allele); } String comp = complement.toString(); - sf.setValue(ALLELES, comp); + sf.setValue(Gff3Helper.ALLELES, comp); sf.setDescription(comp); /* @@ -729,7 +728,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient String atts = sf.getAttributes(); if (atts != null) { - atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp); + atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles, + Gff3Helper.ALLELES + "=" + comp); sf.setAttributes(atts); } } diff --git a/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java b/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java index c4abb20..cfb3c6d 100644 --- a/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java +++ b/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java @@ -53,7 +53,7 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl protected static final String PARENT = "Parent"; - protected static final String ID = "id"; + protected static final String JSON_ID = "id"; protected static final String OBJECT_TYPE = "object_type"; diff --git a/src/jalview/ext/ensembl/EnsemblSymbol.java b/src/jalview/ext/ensembl/EnsemblSymbol.java index e3b6c93..40d6cad 100644 --- a/src/jalview/ext/ensembl/EnsemblSymbol.java +++ b/src/jalview/ext/ensembl/EnsemblSymbol.java @@ -75,7 +75,7 @@ public class EnsemblSymbol extends EnsemblXref while (rvals.hasNext()) { JSONObject val = (JSONObject) rvals.next(); - String id = val.get(ID).toString(); + String id = val.get(JSON_ID).toString(); String type = val.get(TYPE).toString(); if (id != null && GENE.equals(type)) { @@ -150,7 +150,6 @@ public class EnsemblSymbol extends EnsemblXref if (br != null) { String geneId = parseSymbolResponse(br); - System.out.println(url + " returned " + geneId); if (geneId != null && !result.contains(geneId)) { result.add(geneId); diff --git a/src/jalview/ext/htsjdk/HtsContigDb.java b/src/jalview/ext/htsjdk/HtsContigDb.java index 37ce625..73d1674 100644 --- a/src/jalview/ext/htsjdk/HtsContigDb.java +++ b/src/jalview/ext/htsjdk/HtsContigDb.java @@ -20,17 +20,13 @@ */ package jalview.ext.htsjdk; -import htsjdk.samtools.SAMSequenceDictionary; -import htsjdk.samtools.SAMSequenceRecord; -import htsjdk.samtools.reference.ReferenceSequence; -import htsjdk.samtools.reference.ReferenceSequenceFile; -import htsjdk.samtools.reference.ReferenceSequenceFileFactory; -import htsjdk.samtools.util.StringUtil; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; import java.io.File; +import java.io.IOException; import java.math.BigInteger; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -38,6 +34,15 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import htsjdk.samtools.SAMException; +import htsjdk.samtools.SAMSequenceDictionary; +import htsjdk.samtools.SAMSequenceRecord; +import htsjdk.samtools.reference.FastaSequenceIndexCreator; +import htsjdk.samtools.reference.ReferenceSequence; +import htsjdk.samtools.reference.ReferenceSequenceFile; +import htsjdk.samtools.reference.ReferenceSequenceFileFactory; +import htsjdk.samtools.util.StringUtil; + /** * a source of sequence data accessed via the HTSJDK * @@ -46,14 +51,25 @@ import java.util.Set; */ public class HtsContigDb { - private String name; private File dbLocation; private htsjdk.samtools.reference.ReferenceSequenceFile refFile = null; - public HtsContigDb(String name, File descriptor) throws Exception + public static void createFastaSequenceIndex(Path path, boolean overwrite) + throws IOException + { + try + { + FastaSequenceIndexCreator.create(path, overwrite); + } catch (SAMException e) + { + throw new IOException(e.getMessage()); + } + } + + public HtsContigDb(String name, File descriptor) { if (descriptor.isFile()) { @@ -63,7 +79,21 @@ public class HtsContigDb initSource(); } - private void initSource() throws Exception + public void close() + { + if (refFile != null) + { + try + { + refFile.close(); + } catch (IOException e) + { + // ignore + } + } + } + + private void initSource() { if (refFile != null) { @@ -142,8 +172,8 @@ public class HtsContigDb final ReferenceSequenceFile refSeqFile = ReferenceSequenceFileFactory .getReferenceSequenceFile(f, truncate); ReferenceSequence refSeq; - List ret = new ArrayList(); - Set sequenceNames = new HashSet(); + List ret = new ArrayList<>(); + Set sequenceNames = new HashSet<>(); for (int numSequences = 0; (refSeq = refSeqFile .nextSequence()) != null; ++numSequences) { @@ -220,14 +250,29 @@ public class HtsContigDb // ///// end of hts bits. - SequenceI getSequenceProxy(String id) + /** + * Reads the contig with the given id and returns as a Jalview SequenceI object. + * Note the database must be indexed for this operation to succeed. + * + * @param id + * @return + */ + public SequenceI getSequenceProxy(String id) { - if (!isValid()) + if (!isValid() || !refFile.isIndexed()) { + System.err.println( + "Cannot read contig as file is invalid or not indexed"); return null; } ReferenceSequence sseq = refFile.getSequence(id); return new Sequence(sseq.getName(), new String(sseq.getBases())); } + + public boolean isIndexed() + { + return refFile != null && refFile.isIndexed(); + } + } diff --git a/src/jalview/ext/htsjdk/VCFReader.java b/src/jalview/ext/htsjdk/VCFReader.java new file mode 100644 index 0000000..14c057f --- /dev/null +++ b/src/jalview/ext/htsjdk/VCFReader.java @@ -0,0 +1,214 @@ +package jalview.ext.htsjdk; + +import htsjdk.samtools.util.CloseableIterator; +import htsjdk.variant.variantcontext.VariantContext; +import htsjdk.variant.vcf.VCFFileReader; +import htsjdk.variant.vcf.VCFHeader; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; + +/** + * A thin wrapper for htsjdk classes to read either plain, or compressed, or + * compressed and indexed VCF files + */ +public class VCFReader implements Closeable, Iterable +{ + 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 + *

+ * 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 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. + *

+ * 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 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 queryUnindexed( + final String chrom, final int start, final int end) + { + final CloseableIterator it = reader.iterator(); + + return new CloseableIterator() + { + 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; + } +} diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 298688b..4c9360e 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -81,6 +81,7 @@ import jalview.io.JnetAnnotationMaker; import jalview.io.NewickFile; import jalview.io.ScoreMatrixFile; import jalview.io.TCoffeeScoreFile; +import jalview.io.vcf.VCFLoader; import jalview.jbgui.GAlignFrame; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemes; @@ -839,6 +840,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, AlignmentI al = getViewport().getAlignment(); boolean nucleotide = al.isNucleotide(); + loadVcf.setVisible(nucleotide); showTranslation.setVisible(nucleotide); showReverse.setVisible(nucleotide); showReverseComplement.setVisible(nucleotide); @@ -1390,13 +1392,13 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, @Override public void exportFeatures_actionPerformed(ActionEvent e) { - new AnnotationExporter().exportFeatures(alignPanel); + new AnnotationExporter(alignPanel).exportFeatures(); } @Override public void exportAnnotations_actionPerformed(ActionEvent e) { - new AnnotationExporter().exportAnnotations(alignPanel); + new AnnotationExporter(alignPanel).exportAnnotations(); } @Override @@ -4258,7 +4260,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, protected void showProductsFor(final SequenceI[] sel, final boolean _odna, final String source) { - new Thread(CrossRefAction.showProductsFor(sel, _odna, source, this)) + new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this)) .start(); } @@ -5585,6 +5587,27 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, new CalculationChooser(AlignFrame.this); } } + + @Override + protected void loadVcf_actionPerformed() + { + JalviewFileChooser chooser = new JalviewFileChooser( + Cache.getProperty("LAST_DIRECTORY")); + chooser.setFileView(new JalviewFileView()); + chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file")); + chooser.setToolTipText(MessageManager.getString("label.load_vcf_file")); + + int value = chooser.showOpenDialog(null); + + if (value == JalviewFileChooser.APPROVE_OPTION) + { + String choice = chooser.getSelectedFile().getPath(); + Cache.setProperty("LAST_DIRECTORY", choice); + SequenceI[] seqs = viewport.getAlignment().getSequencesArray(); + new VCFLoader(choice).loadVCF(seqs, this); + } + + } } class PrintThread extends Thread diff --git a/src/jalview/gui/AnnotationExporter.java b/src/jalview/gui/AnnotationExporter.java index a619997..6fefbd0 100644 --- a/src/jalview/gui/AnnotationExporter.java +++ b/src/jalview/gui/AnnotationExporter.java @@ -21,8 +21,10 @@ package jalview.gui; import jalview.api.FeatureColourI; +import jalview.bin.Cache; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.io.AnnotationFile; import jalview.io.FeaturesFile; import jalview.io.JalviewFileChooser; @@ -34,6 +36,8 @@ import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.FileWriter; +import java.io.PrintWriter; import java.util.List; import java.util.Map; @@ -57,18 +61,22 @@ import javax.swing.SwingConstants; */ public class AnnotationExporter extends JPanel { - JInternalFrame frame; + private JInternalFrame frame; - AlignmentPanel ap; + private AlignmentPanel ap; - boolean features = true; + /* + * true if exporting features, false if exporting annotations + */ + private boolean exportFeatures = true; private AlignmentAnnotation[] annotations; private boolean wholeView; - public AnnotationExporter() + public AnnotationExporter(AlignmentPanel panel) { + this.ap = panel; try { jbInit(); @@ -84,47 +92,54 @@ public class AnnotationExporter extends JPanel frame.getPreferredSize().height); } - public void exportFeatures(AlignmentPanel ap) + /** + * Configures the diglog for options to export visible features + */ + public void exportFeatures() { - this.ap = ap; - features = true; + exportFeatures = true; CSVFormat.setVisible(false); frame.setTitle(MessageManager.getString("label.export_features")); } - public void exportAnnotations(AlignmentPanel ap) + /** + * Configures the dialog for options to export all visible annotations + */ + public void exportAnnotations() { - this.ap = ap; - annotations = ap.av.isShowAnnotation() ? null - : ap.av.getAlignment().getAlignmentAnnotation(); - wholeView = true; - startExportAnnotation(); + boolean showAnnotation = ap.av.isShowAnnotation(); + exportAnnotation(showAnnotation ? null + : ap.av.getAlignment().getAlignmentAnnotation(), true); } - public void exportAnnotations(AlignmentPanel alp, - AlignmentAnnotation[] toExport) + /** + * Configures the dialog for options to export the given annotation row + * + * @param toExport + */ + public void exportAnnotation(AlignmentAnnotation toExport) { - ap = alp; - annotations = toExport; - wholeView = false; - startExportAnnotation(); + exportAnnotation(new AlignmentAnnotation[] { toExport }, false); } - private void startExportAnnotation() + private void exportAnnotation(AlignmentAnnotation[] toExport, + boolean forWholeView) { - features = false; + wholeView = forWholeView; + annotations = toExport; + exportFeatures = false; GFFFormat.setVisible(false); CSVFormat.setVisible(true); frame.setTitle(MessageManager.getString("label.export_annotations")); } - public void toFile_actionPerformed(ActionEvent e) + private void toFile_actionPerformed() { JalviewFileChooser chooser = new JalviewFileChooser( - jalview.bin.Cache.getProperty("LAST_DIRECTORY")); + Cache.getProperty("LAST_DIRECTORY")); chooser.setFileView(new JalviewFileView()); - chooser.setDialogTitle(features + chooser.setDialogTitle(exportFeatures ? MessageManager.getString("label.save_features_to_file") : MessageManager.getString("label.save_annotation_to_file")); chooser.setToolTipText(MessageManager.getString("action.save")); @@ -133,13 +148,12 @@ public class AnnotationExporter extends JPanel if (value == JalviewFileChooser.APPROVE_OPTION) { - String text = getFileContents(); + String text = getText(); try { - java.io.PrintWriter out = new java.io.PrintWriter( - new java.io.FileWriter(chooser.getSelectedFile())); - + PrintWriter out = new PrintWriter( + new FileWriter(chooser.getSelectedFile())); out.print(text); out.close(); } catch (Exception ex) @@ -148,64 +162,89 @@ public class AnnotationExporter extends JPanel } } - close_actionPerformed(null); + close_actionPerformed(); + } + + /** + * Answers the text to output for either Features (in GFF or Jalview format) or + * Annotations (in CSV or Jalview format) + * + * @return + */ + private String getText() + { + return exportFeatures ? getFeaturesText() : getAnnotationsText(); } - private String getFileContents() + /** + * Returns the text contents for output of annotations in either CSV or Jalview + * format + * + * @return + */ + private String getAnnotationsText() { - String text = MessageManager - .getString("label.no_features_on_alignment"); - if (features) + String text; + if (CSVFormat.isSelected()) { - FeaturesFile formatter = new FeaturesFile(); - SequenceI[] sequences = ap.av.getAlignment().getSequencesArray(); - Map featureColours = ap.getFeatureRenderer() - .getDisplayedFeatureCols(); - List featureGroups = ap.getFeatureRenderer() - .getDisplayedFeatureGroups(); - boolean includeNonPositional = ap.av.isShowNPFeats(); - if (GFFFormat.isSelected()) - { - text = formatter.printGffFormat(sequences, featureColours, - featureGroups, includeNonPositional); - } - else - { - text = formatter.printJalviewFormat(sequences, featureColours, - featureGroups, includeNonPositional); - } + text = new AnnotationFile().printCSVAnnotations(annotations); } else { - if (CSVFormat.isSelected()) + if (wholeView) { - text = new AnnotationFile().printCSVAnnotations(annotations); + text = new AnnotationFile().printAnnotationsForView(ap.av); } else { - if (wholeView) - { - text = new AnnotationFile().printAnnotationsForView(ap.av); - } - else - { - text = new AnnotationFile().printAnnotations(annotations, null, - null); - } + text = new AnnotationFile().printAnnotations(annotations, null, + null); } } return text; } - public void toTextbox_actionPerformed(ActionEvent e) + /** + * Returns the text contents for output of features in either GFF or Jalview + * format + * + * @return + */ + private String getFeaturesText() + { + String text; + SequenceI[] sequences = ap.av.getAlignment().getSequencesArray(); + Map featureColours = ap.getFeatureRenderer() + .getDisplayedFeatureCols(); + Map featureFilters = ap.getFeatureRenderer() + .getFeatureFilters(); + List featureGroups = ap.getFeatureRenderer() + .getDisplayedFeatureGroups(); + boolean includeNonPositional = ap.av.isShowNPFeats(); + + FeaturesFile formatter = new FeaturesFile(); + if (GFFFormat.isSelected()) + { + text = formatter.printGffFormat(sequences, featureColours, + featureGroups, includeNonPositional); + } + else + { + text = formatter.printJalviewFormat(sequences, featureColours, + featureFilters, featureGroups, includeNonPositional); + } + return text; + } + + private void toTextbox_actionPerformed() { CutAndPasteTransfer cap = new CutAndPasteTransfer(); try { - String text = getFileContents(); + String text = getText(); cap.setText(text); - Desktop.addInternalFrame(cap, (features ? MessageManager + Desktop.addInternalFrame(cap, (exportFeatures ? MessageManager .formatMessage("label.features_for_params", new String[] { ap.alignFrame.getTitle() }) : MessageManager.formatMessage("label.annotations_for_params", @@ -214,7 +253,7 @@ public class AnnotationExporter extends JPanel 600, 500); } catch (OutOfMemoryError oom) { - new OOMWarning((features ? MessageManager.formatMessage( + new OOMWarning((exportFeatures ? MessageManager.formatMessage( "label.generating_features_for_params", new String[] { ap.alignFrame.getTitle() }) : MessageManager.formatMessage( @@ -225,10 +264,10 @@ public class AnnotationExporter extends JPanel cap.dispose(); } - close_actionPerformed(null); + close_actionPerformed(); } - public void close_actionPerformed(ActionEvent e) + private void close_actionPerformed() { try { @@ -248,7 +287,7 @@ public class AnnotationExporter extends JPanel @Override public void actionPerformed(ActionEvent e) { - toFile_actionPerformed(e); + toFile_actionPerformed(); } }); toTextbox.setText(MessageManager.getString("label.to_textbox")); @@ -257,7 +296,7 @@ public class AnnotationExporter extends JPanel @Override public void actionPerformed(ActionEvent e) { - toTextbox_actionPerformed(e); + toTextbox_actionPerformed(); } }); close.setText(MessageManager.getString("action.close")); @@ -266,7 +305,7 @@ public class AnnotationExporter extends JPanel @Override public void actionPerformed(ActionEvent e) { - close_actionPerformed(e); + close_actionPerformed(); } }); jalviewFormat.setOpaque(false); diff --git a/src/jalview/gui/AnnotationLabels.java b/src/jalview/gui/AnnotationLabels.java index 77b4ea9..4e181b3 100755 --- a/src/jalview/gui/AnnotationLabels.java +++ b/src/jalview/gui/AnnotationLabels.java @@ -290,9 +290,7 @@ public class AnnotationLabels extends JPanel } else if (evt.getActionCommand().equals(OUTPUT_TEXT)) { - new AnnotationExporter().exportAnnotations(ap, - new AlignmentAnnotation[] - { aa[selectedRow] }); + new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]); } else if (evt.getActionCommand().equals(COPYCONS_SEQ)) { diff --git a/src/jalview/gui/AquaInternalFrameManager.java b/src/jalview/gui/AquaInternalFrameManager.java index ea809eb..829135b 100644 --- a/src/jalview/gui/AquaInternalFrameManager.java +++ b/src/jalview/gui/AquaInternalFrameManager.java @@ -60,7 +60,6 @@ import javax.swing.JInternalFrame; * 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 { diff --git a/src/jalview/gui/CalculationChooser.java b/src/jalview/gui/CalculationChooser.java index e403dba..f674c7e 100644 --- a/src/jalview/gui/CalculationChooser.java +++ b/src/jalview/gui/CalculationChooser.java @@ -169,8 +169,8 @@ public class CalculationChooser extends JPanel JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); treePanel.setOpaque(false); - treePanel.setBorder(BorderFactory - .createTitledBorder(MessageManager.getString("label.tree"))); + JvSwingUtils.createTitledBorder(treePanel, + MessageManager.getString("label.tree"), true); // then copy the inset dimensions for the border-less PCA panel JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT)); diff --git a/src/jalview/gui/CrossRefAction.java b/src/jalview/gui/CrossRefAction.java index 2d1dfd4..85f2498 100644 --- a/src/jalview/gui/CrossRefAction.java +++ b/src/jalview/gui/CrossRefAction.java @@ -27,17 +27,25 @@ import jalview.api.FeatureSettingsModelI; import jalview.bin.Cache; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; +import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; +import jalview.datamodel.GeneLociI; import jalview.datamodel.SequenceI; +import jalview.ext.ensembl.EnsemblInfo; +import jalview.ext.ensembl.EnsemblMap; import jalview.io.gff.SequenceOntologyI; import jalview.structure.StructureSelectionManager; +import jalview.util.DBRefUtils; +import jalview.util.MapList; +import jalview.util.MappingUtils; import jalview.util.MessageManager; import jalview.ws.SequenceFetcher; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; - -import javax.swing.JOptionPane; +import java.util.Map; +import java.util.Set; /** * Factory constructor and runnable for discovering and displaying @@ -52,13 +60,13 @@ public class CrossRefAction implements Runnable private SequenceI[] sel; - private boolean _odna; + private final boolean _odna; private String source; - List xrefViews = new ArrayList(); + List xrefViews = new ArrayList<>(); - public List getXrefViews() + List getXrefViews() { return xrefViews; } @@ -90,6 +98,13 @@ public class CrossRefAction implements Runnable { return; } + + /* + * try to look up chromosomal coordinates for nucleotide + * sequences (if not already retrieved) + */ + findGeneLoci(xrefs.getSequences()); + /* * get display scheme (if any) to apply to features */ @@ -113,75 +128,14 @@ public class CrossRefAction implements Runnable if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true)) { - boolean copyAlignmentIsAligned = false; - if (dna) - { - copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset, - xrefsAlignment.getSequencesArray()); - if (copyAlignment.getHeight() == 0) - { - JvOptionPane.showMessageDialog(alignFrame, - MessageManager.getString("label.cant_map_cds"), - MessageManager.getString("label.operation_failed"), - JvOptionPane.OK_OPTION); - System.err.println("Failed to make CDS alignment"); - } - - /* - * pending getting Embl transcripts to 'align', - * we are only doing this for Ensembl - */ - // TODO proper criteria for 'can align as cdna' - if (DBRefSource.ENSEMBL.equalsIgnoreCase(source) - || AlignmentUtils.looksLikeEnsembl(alignment)) - { - copyAlignment.alignAs(alignment); - copyAlignmentIsAligned = true; - } - } - else + copyAlignment = copyAlignmentForSplitFrame(alignment, dataset, dna, + xrefs, xrefsAlignment); + if (copyAlignment == null) { - copyAlignment = AlignmentUtils.makeCopyAlignment(sel, - xrefs.getSequencesArray(), dataset); - } - copyAlignment - .setGapCharacter(alignFrame.viewport.getGapCharacter()); - - StructureSelectionManager ssm = StructureSelectionManager - .getStructureSelectionManager(Desktop.instance); - - /* - * register any new mappings for sequence mouseover etc - * (will not duplicate any previously registered mappings) - */ - ssm.registerMappings(dataset.getCodonFrames()); - - if (copyAlignment.getHeight() <= 0) - { - System.err.println( - "No Sequences generated for xRef type " + source); - return; - } - /* - * align protein to dna - */ - if (dna && copyAlignmentIsAligned) - { - xrefsAlignment.alignAs(copyAlignment); - } - else - { - /* - * align cdna to protein - currently only if - * fetching and aligning Ensembl transcripts! - */ - // TODO: generalise for other sources of locus/transcript/cds data - if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source)) - { - copyAlignment.alignAs(xrefsAlignment); - } + return; // failed } } + /* * build AlignFrame(s) according to available alignment data */ @@ -207,6 +161,7 @@ public class CrossRefAction implements Runnable xrefViews.add(newFrame.alignPanel); return; // via finally clause } + AlignFrame copyThis = new AlignFrame(copyAlignment, AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); copyThis.setTitle(alignFrame.getTitle()); @@ -221,10 +176,14 @@ public class CrossRefAction implements Runnable /* * copy feature rendering settings to split frame */ - newFrame.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer() - .transferSettings(myFeatureStyling); - copyThis.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer() - .transferSettings(myFeatureStyling); + FeatureRenderer fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas + .getFeatureRenderer(); + fr1.transferSettings(myFeatureStyling); + fr1.findAllFeatures(true); + FeatureRenderer fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas + .getFeatureRenderer(); + fr2.transferSettings(myFeatureStyling); + fr2.findAllFeatures(true); /* * apply 'database source' feature configuration @@ -263,6 +222,260 @@ public class CrossRefAction implements Runnable } /** + * Tries to add chromosomal coordinates to any nucleotide sequence which does + * not already have them. Coordinates are retrieved from Ensembl given an + * Ensembl identifier, either on the sequence itself or on a peptide sequence + * it has a reference to. + * + *

+   * 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
+   * 
+ * + * @param sequences + */ + public static void findGeneLoci(List sequences) + { + Map 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 retrievedLoci) + { + /* + * don't replace any existing chromosomal coordinates + */ + if (seq == null || seq.isProtein() || seq.getGeneLoci() != null + || seq.getDBRefs() == null) + { + return; + } + + Set 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 retrievedLoci) + { + String accession = dbref.getAccessionId(); + String division = dbref.getSource(); + + /* + * hack: ignore cross-references to Ensembl protein ids + * (or use map/translation perhaps?) + * todo: is there an equivalent in EnsemblGenomes? + */ + if (accession.startsWith("ENSP")) + { + return false; + } + EnsemblMap mapper = new EnsemblMap(); + + /* + * try CDS mapping first + */ + GeneLociI geneLoci = mapper.getCdsMapping(division, accession, 1, + seq.getLength()); + if (geneLoci != null) + { + MapList map = geneLoci.getMap(); + int mappedFromLength = MappingUtils.getLength(map.getFromRanges()); + if (mappedFromLength == seq.getLength()) + { + seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(), + geneLoci.getChromosomeId(), geneLoci.getMap()); + retrievedLoci.put(dbref, geneLoci); + return true; + } + } + + /* + * else try CDNA mapping + */ + geneLoci = mapper.getCdnaMapping(division, accession, 1, + seq.getLength()); + if (geneLoci != null) + { + MapList map = geneLoci.getMap(); + int mappedFromLength = MappingUtils.getLength(map.getFromRanges()); + if (mappedFromLength == seq.getLength()) + { + seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(), + geneLoci.getChromosomeId(), geneLoci.getMap()); + retrievedLoci.put(dbref, geneLoci); + return true; + } + } + + return false; + } + + /** + * @param alignment + * @param dataset + * @param dna + * @param xrefs + * @param xrefsAlignment + * @return + */ + protected AlignmentI copyAlignmentForSplitFrame(AlignmentI alignment, + AlignmentI dataset, boolean dna, AlignmentI xrefs, + AlignmentI xrefsAlignment) + { + AlignmentI copyAlignment; + boolean copyAlignmentIsAligned = false; + if (dna) + { + copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset, + xrefsAlignment.getSequencesArray()); + if (copyAlignment.getHeight() == 0) + { + JvOptionPane.showMessageDialog(alignFrame, + MessageManager.getString("label.cant_map_cds"), + MessageManager.getString("label.operation_failed"), + JvOptionPane.OK_OPTION); + System.err.println("Failed to make CDS alignment"); + return null; + } + + /* + * pending getting Embl transcripts to 'align', + * we are only doing this for Ensembl + */ + // TODO proper criteria for 'can align as cdna' + if (DBRefSource.ENSEMBL.equalsIgnoreCase(source) + || AlignmentUtils.looksLikeEnsembl(alignment)) + { + copyAlignment.alignAs(alignment); + copyAlignmentIsAligned = true; + } + } + else + { + copyAlignment = AlignmentUtils.makeCopyAlignment(sel, + xrefs.getSequencesArray(), dataset); + } + copyAlignment + .setGapCharacter(alignFrame.viewport.getGapCharacter()); + + StructureSelectionManager ssm = StructureSelectionManager + .getStructureSelectionManager(Desktop.instance); + + /* + * register any new mappings for sequence mouseover etc + * (will not duplicate any previously registered mappings) + */ + ssm.registerMappings(dataset.getCodonFrames()); + + if (copyAlignment.getHeight() <= 0) + { + System.err.println( + "No Sequences generated for xRef type " + source); + return null; + } + + /* + * align protein to dna + */ + if (dna && copyAlignmentIsAligned) + { + xrefsAlignment.alignAs(copyAlignment); + } + else + { + /* + * align cdna to protein - currently only if + * fetching and aligning Ensembl transcripts! + */ + // TODO: generalise for other sources of locus/transcript/cds data + if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source)) + { + copyAlignment.alignAs(xrefsAlignment); + } + } + + return copyAlignment; + } + + /** * Makes an alignment containing the given sequences, and adds them to the * given dataset, which is also set as the dataset for the new alignment * @@ -291,20 +504,28 @@ public class CrossRefAction implements Runnable return al; } - public CrossRefAction(AlignFrame alignFrame, SequenceI[] sel, - boolean _odna, String source) + /** + * Constructor + * + * @param af + * @param seqs + * @param fromDna + * @param dbSource + */ + CrossRefAction(AlignFrame af, SequenceI[] seqs, boolean fromDna, + String dbSource) { - this.alignFrame = alignFrame; - this.sel = sel; - this._odna = _odna; - this.source = source; + this.alignFrame = af; + this.sel = seqs; + this._odna = fromDna; + this.source = dbSource; } - public static CrossRefAction showProductsFor(final SequenceI[] sel, - final boolean _odna, final String source, + public static CrossRefAction getHandlerFor(final SequenceI[] sel, + final boolean fromDna, final String source, final AlignFrame alignFrame) { - return new CrossRefAction(alignFrame, sel, _odna, source); + return new CrossRefAction(alignFrame, sel, fromDna, source); } } diff --git a/src/jalview/gui/CutAndPasteHtmlTransfer.java b/src/jalview/gui/CutAndPasteHtmlTransfer.java index 71a1520..2e51bce 100644 --- a/src/jalview/gui/CutAndPasteHtmlTransfer.java +++ b/src/jalview/gui/CutAndPasteHtmlTransfer.java @@ -141,6 +141,7 @@ public class CutAndPasteHtmlTransfer extends GCutAndPasteHtmlTransfer */ public void setText(String text) { + textarea.setDocument(textarea.getEditorKit().createDefaultDocument()); textarea.setText(text); } diff --git a/src/jalview/gui/FeatureColourChooser.java b/src/jalview/gui/FeatureColourChooser.java deleted file mode 100644 index d8db546..0000000 --- a/src/jalview/gui/FeatureColourChooser.java +++ /dev/null @@ -1,632 +0,0 @@ -/* - * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) - * Copyright (C) $$Year-Rel$$ The Jalview Authors - * - * This file is part of Jalview. - * - * Jalview is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 - * of the License, or (at your option) any later version. - * - * Jalview is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty - * of MERCHANTABILITY or FITNESS FOR A PARTICULAR - * PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Jalview. If not, see . - * 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 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; - } - -} diff --git a/src/jalview/gui/FeatureRenderer.java b/src/jalview/gui/FeatureRenderer.java index 9c4b009..46f574e 100644 --- a/src/jalview/gui/FeatureRenderer.java +++ b/src/jalview/gui/FeatureRenderer.java @@ -180,15 +180,15 @@ public class FeatureRenderer final JSpinner end = new JSpinner(); start.setPreferredSize(new Dimension(80, 20)); end.setPreferredSize(new Dimension(80, 20)); - final FeatureRenderer me = this; final JLabel colour = new JLabel(); colour.setOpaque(true); // colour.setBorder(BorderFactory.createEtchedBorder()); colour.setMaximumSize(new Dimension(30, 16)); colour.addMouseListener(new MouseAdapter() { - FeatureColourChooser fcc = null; - + /* + * open colour chooser on click in colour panel + */ @Override public void mousePressed(MouseEvent evt) { @@ -205,28 +205,26 @@ public class FeatureRenderer } else { - if (fcc == null) + /* + * variable colour dialog - on OK, refetch the updated + * feature colour and update this display + */ + final String ft = features.get(featureIndex).getType(); + final String type = ft == null ? lastFeatureAdded : ft; + FeatureTypeSettings fcc = new FeatureTypeSettings( + FeatureRenderer.this, type); + fcc.setRequestFocusEnabled(true); + fcc.requestFocus(); + fcc.addActionListener(new ActionListener() { - final String ft = features.get(featureIndex).getType(); - final String type = ft == null ? lastFeatureAdded : ft; - fcc = new FeatureColourChooser(me, type); - fcc.setRequestFocusEnabled(true); - fcc.requestFocus(); - - fcc.addActionListener(new ActionListener() + @Override + public void actionPerformed(ActionEvent e) { - - @Override - public void actionPerformed(ActionEvent e) - { - fcol = fcc.getLastColour(); - fcc = null; - setColour(type, fcol); - updateColourButton(mainPanel, colour, fcol); - } - }); - - } + fcol = FeatureRenderer.this.getFeatureStyle(ft); + setColour(type, fcol); + updateColourButton(mainPanel, colour, fcol); + } + }); } } }); diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index 3f1d9c7..1dbc9b8 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -22,19 +22,23 @@ package jalview.gui; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsControllerI; -import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.Help.HelpId; import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; +import jalview.schemabinding.version2.Filter; import jalview.schemabinding.version2.JalviewUserColours; +import jalview.schemabinding.version2.MatcherSet; import jalview.schemes.FeatureColour; -import jalview.util.Format; import jalview.util.MessageManager; import jalview.util.Platform; -import jalview.util.QuickSort; import jalview.viewmodel.AlignmentViewport; +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; +import jalview.ws.DasSequenceFeatureFetcher; import jalview.ws.dbsources.das.api.jalviewSourceI; import java.awt.BorderLayout; @@ -44,6 +48,7 @@ import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.GridLayout; +import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -61,6 +66,8 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; @@ -86,7 +93,6 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSlider; -import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; @@ -96,15 +102,34 @@ import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; public class FeatureSettings extends JPanel implements FeatureSettingsControllerI { - DasSourceBrowser dassourceBrowser; + private static final String SEQUENCE_FEATURE_COLOURS = MessageManager + .getString("label.sequence_feature_colours"); + + /* + * column indices of fields in Feature Settings table + */ + static final int TYPE_COLUMN = 0; + + static final int COLOUR_COLUMN = 1; + + static final int FILTER_COLUMN = 2; + + static final int SHOW_COLUMN = 3; + + private static final int COLUMN_COUNT = 4; - jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher; + private static final int MIN_WIDTH = 400; + + private static final int MIN_HEIGHT = 400; - JPanel settingsPane = new JPanel(); + DasSourceBrowser dassourceBrowser; + + DasSequenceFeatureFetcher dasFeatureFetcher; JPanel dasSettingsPane = new JPanel(); @@ -112,10 +137,15 @@ public class FeatureSettings extends JPanel public final AlignFrame af; + /* + * 'original' fields hold settings to restore on Cancel + */ Object[][] originalData; private float originalTransparency; + private Map originalFilters; + final JInternalFrame frame; JScrollPane scrollPane = new JScrollPane(); @@ -126,29 +156,47 @@ public class FeatureSettings extends JPanel JSlider transparency = new JSlider(); - JPanel transPanel = new JPanel(new GridLayout(1, 2)); - - private static final int MIN_WIDTH = 400; - - private static final int MIN_HEIGHT = 400; - - /** + /* * when true, constructor is still executing - so ignore UI events */ protected volatile boolean inConstruction = true; + int selectedRow = -1; + + JButton fetchDAS = new JButton(); + + JButton saveDAS = new JButton(); + + JButton cancelDAS = new JButton(); + + boolean resettingTable = false; + + /* + * true when Feature Settings are updating from feature renderer + */ + private boolean handlingUpdate = false; + + /* + * holds {featureCount, totalExtent} for each feature type + */ + Map typeWidth = null; + /** * Constructor * * @param af */ - public FeatureSettings(AlignFrame af) + public FeatureSettings(AlignFrame alignFrame) { - this.af = af; + this.af = alignFrame; fr = af.getFeatureRenderer(); - // allow transparency to be recovered - transparency.setMaximum(100 - - (int) ((originalTransparency = fr.getTransparency()) * 100)); + + // save transparency for restore on Cancel + originalTransparency = fr.getTransparency(); + int originalTransparencyAsPercent = (int) (originalTransparency * 100); + transparency.setMaximum(100 - originalTransparencyAsPercent); + + originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy try { @@ -163,25 +211,48 @@ public class FeatureSettings extends JPanel @Override public String getToolTipText(MouseEvent e) { - if (table.columnAtPoint(e.getPoint()) == 0) + String tip = null; + int column = table.columnAtPoint(e.getPoint()); + switch (column) { - /* - * Tooltip for feature name only - */ - return JvSwingUtils.wrapTooltip(true, MessageManager + case TYPE_COLUMN: + tip = JvSwingUtils.wrapTooltip(true, MessageManager .getString("label.feature_settings_click_drag")); + break; + case FILTER_COLUMN: + int row = table.rowAtPoint(e.getPoint()); + FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row, + column); + tip = o.isEmpty() + ? MessageManager.getString("label.filters_tooltip") + : o.toString(); + break; + default: + break; } - return null; + return tip; } }; table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12)); table.setFont(new Font("Verdana", Font.PLAIN, 12)); - table.setDefaultRenderer(Color.class, new ColorRenderer()); - - table.setDefaultEditor(Color.class, new ColorEditor(this)); + // table.setDefaultRenderer(Color.class, new ColorRenderer()); + // table.setDefaultEditor(Color.class, new ColorEditor(this)); + // table.setDefaultEditor(FeatureColour.class, new ColorEditor(this)); table.setDefaultRenderer(FeatureColour.class, new ColorRenderer()); + + table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this)); + table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer()); + + TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75, + new ColorRenderer(), new ColorEditor(this)); + table.addColumn(colourColumn); + + TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75, + new FilterRenderer(), new FilterEditor(this)); + table.addColumn(filterColumn); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.addMouseListener(new MouseAdapter() @@ -190,11 +261,12 @@ public class FeatureSettings extends JPanel public void mousePressed(MouseEvent evt) { selectedRow = table.rowAtPoint(evt.getPoint()); + String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN); if (evt.isPopupTrigger()) { - popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0), - table.getValueAt(selectedRow, 1), fr.getMinMax(), - evt.getX(), evt.getY()); + Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN); + popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(), + evt.getY()); } else if (evt.getClickCount() == 2) { @@ -202,8 +274,7 @@ public class FeatureSettings extends JPanel boolean toggleSelection = Platform.isControlDown(evt); boolean extendSelection = evt.isShiftDown(); fr.ap.alignFrame.avc.markColumnsContainingFeatures( - invertSelection, extendSelection, toggleSelection, - (String) table.getValueAt(selectedRow, 0)); + invertSelection, extendSelection, toggleSelection, type); } } @@ -214,9 +285,10 @@ public class FeatureSettings extends JPanel selectedRow = table.rowAtPoint(evt.getPoint()); if (evt.isPopupTrigger()) { - popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0), - table.getValueAt(selectedRow, 1), fr.getMinMax(), - evt.getX(), evt.getY()); + String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN); + Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN); + popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(), + evt.getY()); } } }); @@ -272,8 +344,8 @@ public class FeatureSettings extends JPanel if (!fs.resettingTable && !fs.handlingUpdate) { fs.handlingUpdate = true; - fs.resetTable(null); // new groups may be added with new seuqence - // feature types only + fs.resetTable(null); + // new groups may be added with new sequence feature types only fs.handlingUpdate = false; } } @@ -286,13 +358,13 @@ public class FeatureSettings extends JPanel { Desktop.addInternalFrame(frame, MessageManager.getString("label.sequence_feature_settings"), - 475, 480); + 600, 480); } else { Desktop.addInternalFrame(frame, MessageManager.getString("label.sequence_feature_settings"), - 400, 450); + 600, 450); } frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT)); @@ -311,7 +383,7 @@ public class FeatureSettings extends JPanel 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 minmax, int x, int y) { @@ -351,84 +423,70 @@ public class FeatureSettings extends JPanel }); men.add(dens); - if (minmax != null) + + /* + * variable colour options include colour by label, by score, + * by selected attribute text, or attribute value + */ + final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem( + MessageManager.getString("label.variable_colour")); + mxcol.setSelected(!featureColour.isSimpleColour()); + men.add(mxcol); + mxcol.addActionListener(new ActionListener() { - final float[][] typeMinMax = minmax.get(type); - /* - * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); // - * this is broken at the moment and isn't that useful anyway! - * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new - * ActionListener() { - * - * public void actionPerformed(ActionEvent e) { - * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type, - * null); } else { minmax.put(type, typeMinMax); } } - * - * }); - * - * men.add(chb); - */ - if (typeMinMax != null && typeMinMax[0] != null) - { - // if (table.getValueAt(row, column)); - // graduated colourschemes for those where minmax exists for the - // positional features - final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem( - "Graduated Colour"); - mxcol.setSelected(!featureColour.isSimpleColour()); - men.add(mxcol); - mxcol.addActionListener(new ActionListener() - { - JColorChooser colorChooser; + JColorChooser colorChooser; - @Override - public void actionPerformed(ActionEvent e) + @Override + public void actionPerformed(ActionEvent e) + { + if (e.getSource() == mxcol) + { + if (featureColour.isSimpleColour()) { - if (e.getSource() == mxcol) - { - if (featureColour.isSimpleColour()) - { - FeatureColourChooser fc = new FeatureColourChooser(me.fr, - type); - fc.addActionListener(this); - } - else - { - // bring up simple color chooser - colorChooser = new JColorChooser(); - JDialog dialog = JColorChooser.createDialog(me, - "Select new Colour", true, // modal - colorChooser, this, // OK button handler - null); // no CANCEL button handler - colorChooser.setColor(featureColour.getMaxColour()); - dialog.setVisible(true); - } - } - else - { - if (e.getSource() instanceof FeatureColourChooser) - { - FeatureColourChooser fc = (FeatureColourChooser) e - .getSource(); - table.setValueAt(fc.getLastColour(), selectedRow, 1); - table.validate(); - } - else - { - // probably the color chooser! - table.setValueAt(new FeatureColour(colorChooser.getColor()), - selectedRow, 1); - table.validate(); - me.updateFeatureRenderer( - ((FeatureTableModel) table.getModel()).getData(), - false); - } - } + FeatureTypeSettings fc = new FeatureTypeSettings(me.fr, type); + fc.addActionListener(this); } - - }); + else + { + // bring up simple color chooser + colorChooser = new JColorChooser(); + String title = MessageManager + .getString("label.select_colour"); + JDialog dialog = JColorChooser.createDialog(me, + title, true, // modal + colorChooser, this, // OK button handler + null); // no CANCEL button handler + colorChooser.setColor(featureColour.getMaxColour()); + dialog.setVisible(true); + } + } + else + { + if (e.getSource() instanceof FeatureTypeSettings) + { + /* + * update after OK in feature colour dialog; the updated + * colour will have already been set in the FeatureRenderer + */ + FeatureColourI fci = fr.getFeatureColours().get(type); + table.setValueAt(fci, rowSelected, 1); + table.validate(); + } + else + { + // probably the color chooser! + table.setValueAt(new FeatureColour(colorChooser.getColor()), + rowSelected, 1); + table.validate(); + me.updateFeatureRenderer( + ((FeatureTableModel) table.getModel()).getData(), + false); + } + } } - } + + }); + JMenuItem selCols = new JMenuItem( MessageManager.getString("label.select_columns_containing")); selCols.addActionListener(new ActionListener() @@ -478,16 +536,6 @@ public class FeatureSettings extends JPanel men.show(table, x, y); } - /** - * true when Feature Settings are updating from feature renderer - */ - private boolean handlingUpdate = false; - - /** - * holds {featureCount, totalExtent} for each feature type - */ - Map typeWidth = null; - @Override synchronized public void discoverAllFeatureData() { @@ -549,8 +597,6 @@ public class FeatureSettings extends JPanel return visible; } - boolean resettingTable = false; - synchronized void resetTable(String[] groupChanged) { if (resettingTable) @@ -613,7 +659,7 @@ public class FeatureSettings extends JPanel } } - Object[][] data = new Object[displayableTypes.size()][3]; + Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT]; int dataIndex = 0; if (fr.hasRenderOrder()) @@ -636,9 +682,13 @@ public class FeatureSettings extends JPanel continue; } - data[dataIndex][0] = type; - data[dataIndex][1] = fr.getFeatureStyle(type); - data[dataIndex][2] = new Boolean( + data[dataIndex][TYPE_COLUMN] = type; + data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type); + FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type); + data[dataIndex][FILTER_COLUMN] = featureFilter == null + ? new FeatureMatcherSet() + : featureFilter; + data[dataIndex][SHOW_COLUMN] = new Boolean( af.getViewport().getFeaturesDisplayed().isVisible(type)); dataIndex++; displayableTypes.remove(type); @@ -652,27 +702,30 @@ public class FeatureSettings extends JPanel while (!displayableTypes.isEmpty()) { String type = displayableTypes.iterator().next(); - data[dataIndex][0] = type; + data[dataIndex][TYPE_COLUMN] = type; - data[dataIndex][1] = fr.getFeatureStyle(type); - if (data[dataIndex][1] == null) + data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type); + if (data[dataIndex][COLOUR_COLUMN] == null) { // "Colour has been updated in another view!!" fr.clearRenderOrder(); return; } - - data[dataIndex][2] = new Boolean(true); + FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type); + data[dataIndex][FILTER_COLUMN] = featureFilter == null + ? new FeatureMatcherSet() + : featureFilter; + data[dataIndex][SHOW_COLUMN] = new Boolean(true); dataIndex++; displayableTypes.remove(type); } if (originalData == null) { - originalData = new Object[data.length][3]; + originalData = new Object[data.length][COLUMN_COUNT]; for (int i = 0; i < data.length; i++) { - System.arraycopy(data[i], 0, originalData[i], 0, 3); + System.arraycopy(data[i], 0, originalData[i], 0, COLUMN_COUNT); } } else @@ -693,8 +746,8 @@ public class FeatureSettings extends JPanel } /** - * Updates 'originalData' (used for restore on Cancel) if we detect that - * changes have been made outwith this dialog + * Updates 'originalData' (used for restore on Cancel) if we detect that changes + * have been made outwith this dialog *
    *
  • a new feature type added (and made visible)
  • *
  • a feature colour changed (in the Amend Features dialog)
  • @@ -710,27 +763,27 @@ public class FeatureSettings extends JPanel .getData(); for (Object[] row : foundData) { - String type = (String) row[0]; + String type = (String) row[TYPE_COLUMN]; boolean found = false; for (Object[] current : currentData) { - if (type.equals(current[0])) + if (type.equals(current[TYPE_COLUMN])) { found = true; /* * currently dependent on object equality here; * really need an equals method on FeatureColour */ - if (!row[1].equals(current[1])) + if (!row[COLOUR_COLUMN].equals(current[COLOUR_COLUMN])) { /* * feature colour has changed externally - update originalData */ for (Object[] original : originalData) { - if (type.equals(original[0])) + if (type.equals(original[TYPE_COLUMN])) { - original[1] = row[1]; + original[COLOUR_COLUMN] = row[COLOUR_COLUMN]; break; } } @@ -743,10 +796,12 @@ public class FeatureSettings extends JPanel /* * new feature detected - add to original data (on top) */ - Object[][] newData = new Object[originalData.length + 1][3]; + Object[][] newData = new Object[originalData.length + + 1][COLUMN_COUNT]; for (int i = 0; i < originalData.length; i++) { - System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3); + System.arraycopy(originalData[i], 0, newData[i + 1], 0, + COLUMN_COUNT); } newData[0] = row; originalData = newData; @@ -756,8 +811,8 @@ public class FeatureSettings extends JPanel /** * Remove from the groups panel any checkboxes for groups that are not in the - * foundGroups set. This enables removing a group from the display when the - * last feature in that group is deleted. + * foundGroups set. This enables removing a group from the display when the last + * feature in that group is deleted. * * @param foundGroups */ @@ -800,10 +855,14 @@ public class FeatureSettings extends JPanel } } + /** + * Offers a file chooser dialog, and then loads the feature colours and + * filters from file in XML format and unmarshals to Jalview feature settings + */ void load() { JalviewFileChooser chooser = new JalviewFileChooser("fc", - "Sequence Feature Colours"); + SEQUENCE_FEATURE_COLOURS); chooser.setFileView(new JalviewFileView()); chooser.setDialogTitle( MessageManager.getString("label.load_feature_colours")); @@ -814,88 +873,78 @@ public class FeatureSettings extends JPanel if (value == JalviewFileChooser.APPROVE_OPTION) { File file = chooser.getSelectedFile(); + load(file); + } + } - try - { - InputStreamReader in = new InputStreamReader( - new FileInputStream(file), "UTF-8"); + /** + * Loads feature colours and filters from XML stored in the given file + * + * @param file + */ + void load(File file) + { + try + { + InputStreamReader in = new InputStreamReader( + new FileInputStream(file), "UTF-8"); - JalviewUserColours jucs = JalviewUserColours.unmarshal(in); + JalviewUserColours jucs = JalviewUserColours.unmarshal(in); - for (int i = jucs.getColourCount() - 1; i >= 0; i--) - { - String name; - jalview.schemabinding.version2.Colour newcol = jucs.getColour(i); - if (newcol.hasMax()) - { - Color mincol = null, maxcol = null; - try - { - mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16)); - maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16)); + /* + * load feature colours + */ + for (int i = jucs.getColourCount() - 1; i >= 0; i--) + { + jalview.schemabinding.version2.Colour newcol = jucs.getColour(i); + FeatureColourI colour = Jalview2XML.unmarshalColour(newcol); + fr.setColour(newcol.getName(), colour); + fr.setOrder(newcol.getName(), i / (float) jucs.getColourCount()); + } - } catch (Exception e) - { - Cache.log.warn("Couldn't parse out graduated feature color.", - e); - } - FeatureColourI gcol = new FeatureColour(mincol, maxcol, - newcol.getMin(), newcol.getMax()); - if (newcol.hasAutoScale()) - { - gcol.setAutoScaled(newcol.getAutoScale()); - } - if (newcol.hasColourByLabel()) - { - gcol.setColourByLabel(newcol.getColourByLabel()); - } - if (newcol.hasThreshold()) - { - gcol.setThreshold(newcol.getThreshold()); - } - if (newcol.getThreshType().length() > 0) - { - String ttyp = newcol.getThreshType(); - if (ttyp.equalsIgnoreCase("ABOVE")) - { - gcol.setAboveThreshold(true); - } - if (ttyp.equalsIgnoreCase("BELOW")) - { - gcol.setBelowThreshold(true); - } - } - fr.setColour(name = newcol.getName(), gcol); - } - else - { - Color color = new Color( - Integer.parseInt(jucs.getColour(i).getRGB(), 16)); - fr.setColour(name = jucs.getColour(i).getName(), - new FeatureColour(color)); - } - fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount()); - } - if (table != null) + /* + * load feature filters; loaded filters will replace any that are + * currently defined, other defined filters are left unchanged + */ + for (int i = 0; i < jucs.getFilterCount(); i++) + { + jalview.schemabinding.version2.Filter filterModel = jucs + .getFilter(i); + String featureType = filterModel.getFeatureType(); + FeatureMatcherSetI filter = Jalview2XML.unmarshalFilter(featureType, + filterModel.getMatcherSet()); + if (!filter.isEmpty()) { - resetTable(null); - Object[][] data = ((FeatureTableModel) table.getModel()) - .getData(); - ensureOrder(data); - updateFeatureRenderer(data, false); - table.repaint(); + fr.setFeatureFilter(featureType, filter); } - } catch (Exception ex) - { - System.out.println("Error loading User Colour File\n" + ex); } + + /* + * update feature settings table + */ + if (table != null) + { + resetTable(null); + Object[][] data = ((FeatureTableModel) table.getModel()) + .getData(); + ensureOrder(data); + updateFeatureRenderer(data, false); + table.repaint(); + } + } catch (Exception ex) + { + System.out.println("Error loading User Colour File\n" + ex); } } + /** + * Offers a file chooser dialog, and then saves the current feature colours + * and any filters to the selected file in XML format + */ void save() { JalviewFileChooser chooser = new JalviewFileChooser("fc", - "Sequence Feature Colours"); + SEQUENCE_FEATURE_COLOURS); chooser.setFileView(new JalviewFileView()); chooser.setDialogTitle( MessageManager.getString("label.save_feature_colours")); @@ -905,57 +954,75 @@ public class FeatureSettings extends JPanel if (value == JalviewFileChooser.APPROVE_OPTION) { - String choice = chooser.getSelectedFile().getPath(); - jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours(); - ucs.setSchemeName("Sequence Features"); - try - { - PrintWriter out = new PrintWriter(new OutputStreamWriter( - new FileOutputStream(choice), "UTF-8")); + save(chooser.getSelectedFile()); + } + } - Set fr_colours = fr.getAllFeatureColours(); - Iterator e = fr_colours.iterator(); - float[] sortOrder = new float[fr_colours.size()]; - String[] sortTypes = new String[fr_colours.size()]; - int i = 0; - while (e.hasNext()) + /** + * Saves feature colours and filters to the given file + * + * @param file + */ + void save(File file) + { + JalviewUserColours ucs = new JalviewUserColours(); + ucs.setSchemeName("Sequence Features"); + try + { + PrintWriter out = new PrintWriter(new OutputStreamWriter( + new FileOutputStream(file), "UTF-8")); + + /* + * sort feature types by colour order, from 0 (highest) + * to 1 (lowest) + */ + Set fr_colours = fr.getAllFeatureColours(); + String[] sortedTypes = fr_colours + .toArray(new String[fr_colours.size()]); + Arrays.sort(sortedTypes, new Comparator() + { + @Override + public int compare(String type1, String type2) { - sortTypes[i] = e.next(); - sortOrder[i] = fr.getOrder(sortTypes[i]); - i++; + return Float.compare(fr.getOrder(type1), fr.getOrder(type2)); } - QuickSort.sort(sortOrder, sortTypes); - sortOrder = null; - for (i = 0; i < sortTypes.length; i++) + }); + + /* + * save feature colours + */ + for (String featureType : sortedTypes) + { + FeatureColourI fcol = fr.getFeatureStyle(featureType); + jalview.schemabinding.version2.Colour col = Jalview2XML.marshalColour( + featureType, fcol); + ucs.addColour(col); + } + + /* + * save any feature filters + */ + for (String featureType : sortedTypes) + { + FeatureMatcherSetI filter = fr.getFeatureFilter(featureType); + if (filter != null && !filter.isEmpty()) { - jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour(); - col.setName(sortTypes[i]); - FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]); - if (fcol.isSimpleColour()) - { - col.setRGB(Format.getHexString(fcol.getColour())); - } - else - { - col.setRGB(Format.getHexString(fcol.getMaxColour())); - col.setMin(fcol.getMin()); - col.setMax(fcol.getMax()); - col.setMinRGB( - jalview.util.Format.getHexString(fcol.getMinColour())); - col.setAutoScale(fcol.isAutoScaled()); - col.setThreshold(fcol.getThreshold()); - col.setColourByLabel(fcol.isColourByLabel()); - col.setThreshType(fcol.isAboveThreshold() ? "ABOVE" - : (fcol.isBelowThreshold() ? "BELOW" : "NONE")); - } - ucs.addColour(col); + Iterator iterator = filter.getMatchers().iterator(); + FeatureMatcherI firstMatcher = iterator.next(); + MatcherSet ms = Jalview2XML.marshalFilter(firstMatcher, iterator, + filter.isAnded()); + Filter filterModel = new Filter(); + filterModel.setFeatureType(featureType); + filterModel.setMatcherSet(ms); + ucs.addFilter(filterModel); } - ucs.marshal(out); - out.close(); - } catch (Exception ex) - { - ex.printStackTrace(); } + + ucs.marshal(out); + out.close(); + } catch (Exception ex) + { + ex.printStackTrace(); } } @@ -963,9 +1030,9 @@ public class FeatureSettings extends JPanel { 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); } } @@ -979,17 +1046,16 @@ public class FeatureSettings extends JPanel float[] width = new float[data.length]; float[] awidth; float max = 0; - int num = 0; + for (int i = 0; i < data.length; i++) { - awidth = typeWidth.get(data[i][0]); + awidth = typeWidth.get(data[i][TYPE_COLUMN]); if (awidth[0] > 0) { width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better // weight - but have to make per // sequence, too (awidth[2]) // if (width[i]==1) // hack to distinguish single width sequences. - num++; } else { @@ -1006,16 +1072,17 @@ public class FeatureSettings extends JPanel // awidth = (float[]) typeWidth.get(data[i][0]); if (width[i] == 0) { - width[i] = fr.getOrder(data[i][0].toString()); + width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString()); if (width[i] < 0) { - width[i] = fr.setOrder(data[i][0].toString(), i / data.length); + width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(), + i / data.length); } } else { width[i] /= max; // normalize - fr.setOrder(data[i][0].toString(), width[i]); // store for later + fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for later } if (i > 0) { @@ -1049,76 +1116,58 @@ public class FeatureSettings extends JPanel } /** - * Update the priority order of features; only repaint if this changed the - * order of visible features + * Update the priority order of features; only repaint if this changed the order + * of visible features * * @param data * @param visibleNew */ private void updateFeatureRenderer(Object[][] data, boolean visibleNew) { - if (fr.setFeaturePriority(data, visibleNew)) + FeatureSettingsBean[] rowData = getTableAsBeans(data); + + if (fr.setFeaturePriority(rowData, visibleNew)) { af.alignPanel.paintAlignment(true, true); } } - int selectedRow = -1; - - JTabbedPane tabbedPane = new JTabbedPane(); - - BorderLayout borderLayout1 = new BorderLayout(); - - BorderLayout borderLayout2 = new BorderLayout(); - - BorderLayout borderLayout3 = new BorderLayout(); - - JPanel bigPanel = new JPanel(); - - BorderLayout borderLayout4 = new BorderLayout(); - - JButton invert = new JButton(); - - JPanel buttonPanel = new JPanel(); - - JButton cancel = new JButton(); - - JButton ok = new JButton(); - - JButton loadColours = new JButton(); - - JButton saveColours = new JButton(); - - JPanel dasButtonPanel = new JPanel(); - - JButton fetchDAS = new JButton(); - - JButton saveDAS = new JButton(); - - JButton cancelDAS = new JButton(); - - JButton optimizeOrder = new JButton(); - - JButton sortByScore = new JButton(); + /** + * 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 @@ -1127,8 +1176,10 @@ public class FeatureSettings extends JPanel invertSelection(); } }); + + JButton optimizeOrder = new JButton( + MessageManager.getString("label.optimise_order")); optimizeOrder.setFont(JvSwingUtils.getLabelFont()); - optimizeOrder.setText(MessageManager.getString("label.optimise_order")); optimizeOrder.addActionListener(new ActionListener() { @Override @@ -1137,9 +1188,10 @@ public class FeatureSettings extends JPanel orderByAvWidth(); } }); + + JButton sortByScore = new JButton( + MessageManager.getString("label.seq_sort_by_score")); sortByScore.setFont(JvSwingUtils.getLabelFont()); - sortByScore - .setText(MessageManager.getString("label.seq_sort_by_score")); sortByScore.addActionListener(new ActionListener() { @Override @@ -1148,9 +1200,9 @@ public class FeatureSettings extends JPanel af.avc.sortAlignmentByFeatureScore(null); } }); - sortByDens.setFont(JvSwingUtils.getLabelFont()); - sortByDens.setText( + JButton sortByDens = new JButton( MessageManager.getString("label.sequence_sort_by_density")); + sortByDens.setFont(JvSwingUtils.getLabelFont()); sortByDens.addActionListener(new ActionListener() { @Override @@ -1159,8 +1211,9 @@ public class FeatureSettings extends JPanel af.avc.sortAlignmentByFeatureDensity(null); } }); + + JButton help = new JButton(MessageManager.getString("action.help")); help.setFont(JvSwingUtils.getLabelFont()); - help.setText(MessageManager.getString("action.help")); help.addActionListener(new ActionListener() { @Override @@ -1191,20 +1244,23 @@ public class FeatureSettings extends JPanel } } }); + + JButton cancel = new JButton(MessageManager.getString("action.cancel")); cancel.setFont(JvSwingUtils.getLabelFont()); - cancel.setText(MessageManager.getString("action.cancel")); cancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { fr.setTransparency(originalTransparency); + fr.setFeatureFilters(originalFilters); updateFeatureRenderer(originalData); close(); } }); + + JButton ok = new JButton(MessageManager.getString("action.ok")); ok.setFont(JvSwingUtils.getLabelFont()); - ok.setText(MessageManager.getString("action.ok")); ok.addActionListener(new ActionListener() { @Override @@ -1213,8 +1269,12 @@ public class FeatureSettings extends JPanel close(); } }); + + JButton loadColours = new JButton( + MessageManager.getString("label.load_colours")); loadColours.setFont(JvSwingUtils.getLabelFont()); - loadColours.setText(MessageManager.getString("label.load_colours")); + loadColours.setToolTipText( + MessageManager.getString("label.load_colours_tooltip")); loadColours.addActionListener(new ActionListener() { @Override @@ -1223,8 +1283,12 @@ public class FeatureSettings extends JPanel load(); } }); + + JButton saveColours = new JButton( + MessageManager.getString("label.save_colours")); saveColours.setFont(JvSwingUtils.getLabelFont()); - saveColours.setText(MessageManager.getString("label.save_colours")); + saveColours.setToolTipText( + MessageManager.getString("label.save_colours_tooltip")); saveColours.addActionListener(new ActionListener() { @Override @@ -1241,7 +1305,7 @@ public class FeatureSettings extends JPanel if (!inConstruction) { fr.setTransparency((100 - transparency.getValue()) / 100f); - af.alignPanel.paintAlignment(true,true); + af.alignPanel.paintAlignment(true, true); } } }); @@ -1267,6 +1331,8 @@ public class FeatureSettings extends JPanel saveDAS_actionPerformed(e); } }); + + JPanel dasButtonPanel = new JPanel(); dasButtonPanel.setBorder(BorderFactory.createEtchedBorder()); dasSettingsPane.setBorder(null); cancelDAS.setEnabled(false); @@ -1279,32 +1345,32 @@ public class FeatureSettings extends JPanel 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) @@ -1469,18 +1535,19 @@ public class FeatureSettings extends JPanel // /////////////////////////////////////////////////////////////////////// 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; @@ -1520,10 +1587,14 @@ public class FeatureSettings extends JPanel return data[row][col]; } + /** + * Answers the class of the object in column c of the first row of the table + */ @Override - public Class getColumnClass(int c) + public Class getColumnClass(int c) { - return getValueAt(0, c).getClass(); + Object v = getValueAt(0, c); + return v == null ? null : v.getClass(); } @Override @@ -1562,12 +1633,7 @@ public class FeatureSettings extends JPanel boolean isSelected, boolean hasFocus, int row, int column) { FeatureColourI cellColour = (FeatureColourI) color; - // JLabel comp = new JLabel(); - // comp. setOpaque(true); - // comp. - // setBounds(getBounds()); - Color newColor; setToolTipText(baseTT); setBackground(tbl.getBackground()); if (!cellColour.isSimpleColour()) @@ -1575,14 +1641,12 @@ public class FeatureSettings extends JPanel Rectangle cr = tbl.getCellRect(row, column, false); FeatureSettings.renderGraduatedColor(this, cellColour, (int) cr.getWidth(), (int) cr.getHeight()); - } else { this.setText(""); this.setIcon(null); - newColor = cellColour.getColour(); - setBackground(newColor); + setBackground(cellColour.getColour()); } if (isSelected) { @@ -1607,6 +1671,54 @@ public class FeatureSettings extends JPanel } } + 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 * @@ -1633,28 +1745,43 @@ public class FeatureSettings extends JPanel int w, int h) { boolean thr = false; - String tt = ""; - String tx = ""; + StringBuilder tt = new StringBuilder(); + StringBuilder tx = new StringBuilder(); + + if (gcol.isColourByAttribute()) + { + tx.append(String.join(":", gcol.getAttributeName())); + } + else if (!gcol.isColourByLabel()) + { + tx.append(MessageManager.getString("label.score")); + } + tx.append(" "); if (gcol.isAboveThreshold()) { thr = true; - tx += ">"; - tt += "Thresholded (Above " + gcol.getThreshold() + ") "; + tx.append(">"); + tt.append("Thresholded (Above ").append(gcol.getThreshold()) + .append(") "); } if (gcol.isBelowThreshold()) { thr = true; - tx += "<"; - tt += "Thresholded (Below " + gcol.getThreshold() + ") "; + tx.append("<"); + tt.append("Thresholded (Below ").append(gcol.getThreshold()) + .append(") "); } if (gcol.isColourByLabel()) { - tt = "Coloured by label text. " + tt; + tt.append("Coloured by label text. ").append(tt); if (thr) { - tx += " "; + tx.append(" "); + } + if (!gcol.isColourByAttribute()) + { + tx.append("Label"); } - tx += "Label"; comp.setIcon(null); } else @@ -1670,19 +1797,259 @@ public class FeatureSettings extends JPanel // + ", " + minCol.getBlue() + ")"); } comp.setHorizontalAlignment(SwingConstants.CENTER); - comp.setText(tx); + comp.setText(tx.toString()); if (tt.length() > 0) { if (comp.getToolTipText() == null) { - comp.setToolTipText(tt); + comp.setToolTipText(tt.toString()); } else { - comp.setToolTipText(tt + " " + comp.getToolTipText()); + comp.setToolTipText( + tt.append(" ").append(comp.getToolTipText()).toString()); } } } + + class ColorEditor extends AbstractCellEditor + implements TableCellEditor, ActionListener + { + FeatureSettings me; + + FeatureColourI currentColor; + + FeatureTypeSettings chooser; + + String type; + + JButton button; + + JColorChooser colorChooser; + + JDialog dialog; + + protected static final String EDIT = "edit"; + + int rowSelected = 0; + + public ColorEditor(FeatureSettings me) + { + this.me = me; + // Set up the editor (from the table's point of view), + // which is a button. + // This button brings up the color chooser dialog, + // which is the editor from the user's point of view. + button = new JButton(); + button.setActionCommand(EDIT); + button.addActionListener(this); + button.setBorderPainted(false); + // Set up the dialog that the button brings up. + colorChooser = new JColorChooser(); + dialog = JColorChooser.createDialog(button, + MessageManager.getString("label.select_colour"), true, // modal + colorChooser, this, // OK button handler + null); // no CANCEL button handler + } + + /** + * Handles events from the editor button and from the dialog's OK button. + */ + @Override + public void actionPerformed(ActionEvent e) + { + // todo test e.getSource() instead here + if (EDIT.equals(e.getActionCommand())) + { + // The user has clicked the cell, so + // bring up the dialog. + if (currentColor.isSimpleColour()) + { + // bring up simple color chooser + button.setBackground(currentColor.getColour()); + colorChooser.setColor(currentColor.getColour()); + dialog.setVisible(true); + } + else + { + // bring up graduated chooser. + chooser = new FeatureTypeSettings(me.fr, type); + chooser.setRequestFocusEnabled(true); + chooser.requestFocus(); + chooser.addActionListener(this); + chooser.showTab(true); + } + // Make the renderer reappear. + fireEditingStopped(); + + } + else + { + if (currentColor.isSimpleColour()) + { + /* + * read off colour picked in colour chooser after OK pressed + */ + currentColor = new FeatureColour(colorChooser.getColor()); + me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN); + } + else + { + /* + * after OK in variable colour dialog, any changes to colour + * (or filters!) are already set in FeatureRenderer, so just + * update table data without triggering updateFeatureRenderer + */ + currentColor = fr.getFeatureColours().get(type); + FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type); + if (currentFilter == null) + { + currentFilter = new FeatureMatcherSet(); + } + Object[] data = ((FeatureTableModel) table.getModel()) + .getData()[rowSelected]; + data[COLOUR_COLUMN] = currentColor; + data[FILTER_COLUMN] = currentFilter; + } + fireEditingStopped(); + me.table.validate(); + } + } + + // Implement the one CellEditor method that AbstractCellEditor doesn't. + @Override + public Object getCellEditorValue() + { + return currentColor; + } + + // Implement the one method defined by TableCellEditor. + @Override + public Component getTableCellEditorComponent(JTable theTable, Object value, + boolean isSelected, int row, int column) + { + currentColor = (FeatureColourI) value; + this.rowSelected = row; + type = me.table.getValueAt(row, TYPE_COLUMN).toString(); + button.setOpaque(true); + button.setBackground(me.getBackground()); + if (!currentColor.isSimpleColour()) + { + JLabel btn = new JLabel(); + btn.setSize(button.getSize()); + FeatureSettings.renderGraduatedColor(btn, currentColor); + button.setBackground(btn.getBackground()); + button.setIcon(btn.getIcon()); + button.setText(btn.getText()); + } + else + { + button.setText(""); + button.setIcon(null); + button.setBackground(currentColor.getColour()); + } + return button; + } + } + + /** + * The cell editor for the Filter column. It displays the text of any filters + * for the feature type in that row (in full as a tooltip, possible abbreviated + * as display text). On click in the cell, opens the Feature Display Settings + * dialog at the Filters tab. + */ + class FilterEditor extends AbstractCellEditor + implements TableCellEditor, ActionListener + { + FeatureSettings me; + + FeatureMatcherSetI currentFilter; + + Point lastLocation; + + String type; + + JButton button; + + protected static final String EDIT = "edit"; + + int rowSelected = 0; + + public FilterEditor(FeatureSettings me) + { + this.me = me; + button = new JButton(); + button.setActionCommand(EDIT); + button.addActionListener(this); + button.setBorderPainted(false); + } + + /** + * Handles events from the editor button + */ + @Override + public void actionPerformed(ActionEvent e) + { + if (button == e.getSource()) + { + FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type); + chooser.addActionListener(this); + chooser.setRequestFocusEnabled(true); + chooser.requestFocus(); + if (lastLocation != null) + { + // todo open at its last position on screen + chooser.setBounds(lastLocation.x, lastLocation.y, + chooser.getWidth(), chooser.getHeight()); + chooser.validate(); + } + chooser.showTab(false); + fireEditingStopped(); + } + else if (e.getSource() instanceof Component) + { + + /* + * after OK in variable colour dialog, any changes to filter + * (or colours!) are already set in FeatureRenderer, so just + * update table data without triggering updateFeatureRenderer + */ + FeatureColourI currentColor = fr.getFeatureColours().get(type); + currentFilter = me.fr.getFeatureFilter(type); + if (currentFilter == null) + { + currentFilter = new FeatureMatcherSet(); + } + Object[] data = ((FeatureTableModel) table.getModel()) + .getData()[rowSelected]; + data[COLOUR_COLUMN] = currentColor; + data[FILTER_COLUMN] = currentFilter; + fireEditingStopped(); + me.table.validate(); + } + } + + @Override + public Object getCellEditorValue() + { + return currentFilter; + } + + @Override + public Component getTableCellEditorComponent(JTable theTable, Object value, + boolean isSelected, int row, int column) + { + currentFilter = (FeatureMatcherSetI) value; + this.rowSelected = row; + type = me.table.getValueAt(row, TYPE_COLUMN).toString(); + button.setOpaque(true); + button.setBackground(me.getBackground()); + button.setText(currentFilter.toString()); + button.setToolTipText(currentFilter.toString()); + button.setIcon(null); + return button; + } + } } class FeatureIcon implements Icon @@ -1766,124 +2133,3 @@ class FeatureIcon implements Icon } } } - -class ColorEditor extends AbstractCellEditor - implements TableCellEditor, ActionListener -{ - FeatureSettings me; - - FeatureColourI currentColor; - - FeatureColourChooser chooser; - - String type; - - JButton button; - - JColorChooser colorChooser; - - JDialog dialog; - - protected static final String EDIT = "edit"; - - int selectedRow = 0; - - public ColorEditor(FeatureSettings me) - { - this.me = me; - // Set up the editor (from the table's point of view), - // which is a button. - // This button brings up the color chooser dialog, - // which is the editor from the user's point of view. - button = new JButton(); - button.setActionCommand(EDIT); - button.addActionListener(this); - button.setBorderPainted(false); - // Set up the dialog that the button brings up. - colorChooser = new JColorChooser(); - dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal - colorChooser, this, // OK button handler - null); // no CANCEL button handler - } - - /** - * Handles events from the editor button and from the dialog's OK button. - */ - @Override - public void actionPerformed(ActionEvent e) - { - - if (EDIT.equals(e.getActionCommand())) - { - // The user has clicked the cell, so - // bring up the dialog. - if (currentColor.isSimpleColour()) - { - // bring up simple color chooser - button.setBackground(currentColor.getColour()); - colorChooser.setColor(currentColor.getColour()); - dialog.setVisible(true); - } - else - { - // bring up graduated chooser. - chooser = new FeatureColourChooser(me.fr, type); - chooser.setRequestFocusEnabled(true); - chooser.requestFocus(); - chooser.addActionListener(this); - } - // Make the renderer reappear. - fireEditingStopped(); - - } - else - { // User pressed dialog's "OK" button. - if (currentColor.isSimpleColour()) - { - currentColor = new FeatureColour(colorChooser.getColor()); - } - else - { - currentColor = chooser.getLastColour(); - } - me.table.setValueAt(getCellEditorValue(), selectedRow, 1); - fireEditingStopped(); - me.table.validate(); - } - } - - // Implement the one CellEditor method that AbstractCellEditor doesn't. - @Override - public Object getCellEditorValue() - { - return currentColor; - } - - // Implement the one method defined by TableCellEditor. - @Override - public Component getTableCellEditorComponent(JTable table, Object value, - boolean isSelected, int row, int column) - { - currentColor = (FeatureColourI) value; - this.selectedRow = row; - type = me.table.getValueAt(row, 0).toString(); - button.setOpaque(true); - button.setBackground(me.getBackground()); - if (!currentColor.isSimpleColour()) - { - JLabel btn = new JLabel(); - btn.setSize(button.getSize()); - FeatureSettings.renderGraduatedColor(btn, currentColor); - button.setBackground(btn.getBackground()); - button.setIcon(btn.getIcon()); - button.setText(btn.getText()); - } - else - { - button.setText(""); - button.setIcon(null); - button.setBackground(currentColor.getColour()); - } - return button; - } -} diff --git a/src/jalview/gui/FeatureTypeSettings.java b/src/jalview/gui/FeatureTypeSettings.java new file mode 100644 index 0000000..5480f5d --- /dev/null +++ b/src/jalview/gui/FeatureTypeSettings.java @@ -0,0 +1,1739 @@ +/* + * 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 . + * 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 + *

    + * (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 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 noValueCombo; + + /* + * choice of what to colour by text (Label or attribute) + */ + private JComboBox colourByTextCombo; + + /* + * choice of what to colour by range (Score or attribute) + */ + private JComboBox colourByRangeCombo; + + private JRadioButton andFilters; + + private JRadioButton orFilters; + + /* + * filters for the currently selected feature type + */ + private List filters; + + // set white normally, black to debug layout + private Color debugBorderColour = Color.white; + + private JPanel chooseFiltersPanel; + + private JTabbedPane tabbedPane; + + /** + * Constructor + * + * @param frender + * @param theType + */ + public FeatureTypeSettings(FeatureRenderer frender, String theType) + { + this(frender, false, theType); + } + + /** + * Constructor, with option to make a blocking dialog (has to complete in the + * AWT event queue thread). Currently this option is always set to false. + * + * @param frender + * @param blocking + * @param theType + */ + FeatureTypeSettings(FeatureRenderer frender, boolean blocking, + String theType) + { + this.fr = frender; + this.featureType = theType; + ap = fr.ap; + originalFilter = fr.getFeatureFilter(theType); + originalColour = fr.getFeatureColours().get(theType); + + adjusting = true; + + try + { + initialise(); + } catch (Exception ex) + { + ex.printStackTrace(); + return; + } + + updateColoursTab(); + + updateFiltersTab(); + + adjusting = false; + + colourChanged(false); + + String title = MessageManager + .formatMessage("label.display_settings_for", new String[] + { theType }); + initDialogFrame(this, true, blocking, title, 600, 360); + + waitForInput(); + } + + /** + * Configures the widgets on the Colours tab according to the current feature + * colour scheme + */ + private void updateColoursTab() + { + FeatureColourI fc = fr.getFeatureColours().get(featureType); + + /* + * suppress action handling while updating values programmatically + */ + adjusting = true; + try + { + /* + * single colour + */ + if (fc.isSimpleColour()) + { + simpleColour.setSelected(true); + singleColour.setBackground(fc.getColour()); + singleColour.setForeground(fc.getColour()); + } + + /* + * colour by text (Label or attribute text) + */ + if (fc.isColourByLabel()) + { + byCategory.setSelected(true); + colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1); + if (fc.isColourByAttribute()) + { + String[] attributeName = fc.getAttributeName(); + colourByTextCombo.setSelectedItem( + FeatureMatcher.toAttributeDisplayName(attributeName)); + } + else + { + colourByTextCombo.setSelectedItem(LABEL_18N); + } + } + else + { + colourByTextCombo.setEnabled(false); + } + + if (!fc.isGraduatedColour()) + { + colourByRangeCombo.setEnabled(false); + minColour.setEnabled(false); + maxColour.setEnabled(false); + noValueCombo.setEnabled(false); + threshold.setEnabled(false); + slider.setEnabled(false); + thresholdValue.setEnabled(false); + thresholdIsMin.setEnabled(false); + return; + } + + /* + * Graduated colour, by score or attribute value range + */ + graduatedColour.setSelected(true); + updateColourMinMax(); // ensure min, max are set + colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1); + minColour.setEnabled(true); + maxColour.setEnabled(true); + noValueCombo.setEnabled(true); + threshold.setEnabled(true); + minColour.setBackground(fc.getMinColour()); + maxColour.setBackground(fc.getMaxColour()); + + if (fc.isColourByAttribute()) + { + String[] attributeName = fc.getAttributeName(); + colourByRangeCombo.setSelectedItem( + FeatureMatcher.toAttributeDisplayName(attributeName)); + } + else + { + colourByRangeCombo.setSelectedItem(SCORE_18N); + } + Color noColour = fc.getNoColour(); + if (noColour == null) + { + noValueCombo.setSelectedIndex(NO_COLOUR_OPTION); + } + else if (noColour.equals(fc.getMinColour())) + { + noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION); + } + else if (noColour.equals(fc.getMaxColour())) + { + noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION); + } + + /* + * update min-max scaling if there is a range to work with, + * else disable the widgets (this shouldn't happen if only + * valid options are offered in the combo box) + */ + scaleFactor = (max == min) ? 1f : 100f / (max - min); + float range = (max - min) * scaleFactor; + slider.setMinimum((int) (min * scaleFactor)); + slider.setMaximum((int) (max * scaleFactor)); + slider.setMajorTickSpacing((int) (range / 10f)); + + threshline = new GraphLine((max - min) / 2f, "Threshold", + Color.black); + threshline.value = fc.getThreshold(); + + if (fc.hasThreshold()) + { + threshold.setSelectedIndex( + fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION + : BELOW_THRESHOLD_OPTION); + slider.setEnabled(true); + slider.setValue((int) (fc.getThreshold() * scaleFactor)); + thresholdValue.setText(String.valueOf(getRoundedSliderValue())); + thresholdValue.setEnabled(true); + thresholdIsMin.setEnabled(true); + } + else + { + slider.setEnabled(false); + thresholdValue.setEnabled(false); + thresholdIsMin.setEnabled(false); + } + thresholdIsMin.setSelected(!fc.isAutoScaled()); + } finally + { + adjusting = false; + } + } + + /** + * Configures the initial layout + */ + private void initialise() + { + this.setLayout(new BorderLayout()); + tabbedPane = new JTabbedPane(); + this.add(tabbedPane, BorderLayout.CENTER); + + /* + * an ActionListener that applies colour changes + */ + changeColourAction = new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + colourChanged(true); + } + }; + + /* + * first tab: colour options + */ + JPanel coloursPanel = initialiseColoursPanel(); + tabbedPane.addTab(MessageManager.getString("action.colour"), + coloursPanel); + + /* + * second tab: filter options + */ + JPanel filtersPanel = initialiseFiltersPanel(); + tabbedPane.addTab(MessageManager.getString("label.filters"), + filtersPanel); + + JPanel okCancelPanel = initialiseOkCancelPanel(); + + this.add(okCancelPanel, BorderLayout.SOUTH); + } + + /** + * Updates the min-max range if Colour By selected item is Score, or an + * attribute, with a min-max range + */ + protected void updateColourMinMax() + { + if (!graduatedColour.isSelected()) + { + return; + } + + String colourBy = (String) colourByRangeCombo.getSelectedItem(); + float[] minMax = getMinMax(colourBy); + + if (minMax != null) + { + min = minMax[0]; + max = minMax[1]; + } + } + + /** + * Retrieves the min-max range: + *

      + *
    • of feature score, if colour or filter is by Score
    • + *
    • else of the selected attribute
    • + *
    + * + * @param attName + * @return + */ + private float[] getMinMax(String attName) + { + float[] minMax = null; + if (SCORE_18N.equals(attName)) + { + minMax = fr.getMinMax().get(featureType)[0]; + } + else + { + // colour by attribute range + minMax = FeatureAttributes.getInstance().getMinMax(featureType, + FeatureMatcher.fromAttributeDisplayName(attName)); + } + return minMax; + } + + /** + * Lay out fields for graduated colour (by score or attribute value) + * + * @return + */ + private JPanel initialiseGraduatedColourPanel() + { + JPanel graduatedColourPanel = new JPanel(); + graduatedColourPanel.setLayout( + new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS)); + JvSwingUtils.createTitledBorder(graduatedColourPanel, + MessageManager.getString("label.graduated_colour"), true); + graduatedColourPanel.setBackground(Color.white); + + /* + * first row: graduated colour radio button, score/attribute drop-down + */ + JPanel graduatedChoicePanel = new JPanel( + new FlowLayout(FlowLayout.LEFT)); + graduatedChoicePanel.setBackground(Color.white); + graduatedColour = new JRadioButton( + MessageManager.getString("label.by_range_of") + COLON); + graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20)); + graduatedColour.addItemListener(new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + if (graduatedColour.isSelected()) + { + colourChanged(true); + } + } + }); + graduatedChoicePanel.add(graduatedColour); + + List 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 + *
      + *
    • plain colour, with colour picker
    • + *
    • colour by text, with choice of Label or other attribute
    • + *
    • colour by range, of score or other attribute, when available
    • + *
    + * + * @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 attNames = FeatureAttributes.getInstance() + .getAttributes(featureType); + colourByTextCombo = populateAttributesDropdown(attNames, false, true); + colourByTextCombo.addItemListener(new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + colourChanged(true); + } + }); + byTextPanel.add(colourByTextCombo); + + /* + * graduated colour panel + */ + JPanel graduatedColourPanel = initialiseGraduatedColourPanel(); + colourByPanel.add(graduatedColourPanel); + + /* + * 3 radio buttons select between simple colour, + * by category (text), or graduated + */ + ButtonGroup bg = new ButtonGroup(); + bg.add(simpleColour); + bg.add(byCategory); + bg.add(graduatedColour); + + return colourByPanel; + } + + private void showColourChooser(JPanel colourPanel, String key) + { + Color col = JColorChooser.showDialog(this, + MessageManager.getString(key), colourPanel.getBackground()); + if (col != null) + { + colourPanel.setBackground(col); + colourPanel.setForeground(col); + } + colourPanel.repaint(); + colourChanged(true); + } + + /** + * Constructs and sets the selected colour options as the colour for the feature + * type, and repaints the alignment, and optionally the Overview and/or + * structure viewer if open + * + * @param updateStructsAndOverview + */ + void colourChanged(boolean updateStructsAndOverview) + { + if (adjusting) + { + /* + * ignore action handlers while setting values programmatically + */ + return; + } + + /* + * ensure min-max range is for the latest choice of + * 'graduated colour by' + */ + updateColourMinMax(); + + FeatureColourI acg = makeColourFromInputs(); + + /* + * save the colour, and repaint stuff + */ + fr.setColour(featureType, acg); + ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview); + + updateColoursTab(); + } + + /** + * Converts the input values into an instance of FeatureColour + * + * @return + */ + private FeatureColourI makeColourFromInputs() + { + /* + * easiest case - a single colour + */ + if (simpleColour.isSelected()) + { + return new FeatureColour(singleColour.getBackground()); + } + + /* + * next easiest case - colour by Label, or attribute text + */ + if (byCategory.isSelected()) + { + Color c = this.getBackground(); + FeatureColourI fc = new FeatureColour(c, c, null, 0f, 0f); + fc.setColourByLabel(true); + String byWhat = (String) colourByTextCombo.getSelectedItem(); + if (!LABEL_18N.equals(byWhat)) + { + fc.setAttributeName( + FeatureMatcher.fromAttributeDisplayName(byWhat)); + } + return fc; + } + + /* + * remaining case - graduated colour by score, or attribute value + */ + Color noColour = null; + if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION) + { + noColour = minColour.getBackground(); + } + else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION) + { + noColour = maxColour.getBackground(); + } + + float thresh = 0f; + try + { + thresh = Float.valueOf(thresholdValue.getText()); + } catch (NumberFormatException e) + { + // invalid inputs are already handled on entry + } + + /* + * min-max range is to (or from) threshold value if + * 'threshold is min/max' is selected + */ + float minValue = min; + float maxValue = max; + final int thresholdOption = threshold.getSelectedIndex(); + if (thresholdIsMin.isSelected() + && thresholdOption == ABOVE_THRESHOLD_OPTION) + { + minValue = thresh; + } + if (thresholdIsMin.isSelected() + && thresholdOption == BELOW_THRESHOLD_OPTION) + { + maxValue = thresh; + } + + /* + * make the graduated colour + */ + FeatureColourI fc = new FeatureColour(minColour.getBackground(), + maxColour.getBackground(), noColour, minValue, maxValue); + + /* + * set attribute to colour by if selected + */ + String byWhat = (String) colourByRangeCombo.getSelectedItem(); + if (!SCORE_18N.equals(byWhat)) + { + fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat)); + } + + /* + * set threshold options and 'autoscaled' which is + * false if 'threshold is min/max' is selected + * else true (colour range is on actual range of values) + */ + fc.setThreshold(thresh); + fc.setAutoScaled(!thresholdIsMin.isSelected()); + fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION); + fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION); + + if (threshline == null) + { + /* + * todo not yet implemented: visual indication of feature threshold + */ + threshline = new GraphLine((max - min) / 2f, "Threshold", + Color.black); + } + + return fc; + } + + @Override + protected void raiseClosed() + { + if (this.featureSettings != null) + { + featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED")); + } + } + + /** + * Action on OK is just to dismiss the dialog - any changes have already been + * applied + */ + @Override + public void okPressed() + { + } + + /** + * Action on Cancel is to restore colour scheme and filters as they were when + * the dialog was opened + */ + @Override + public void cancelPressed() + { + fr.setColour(featureType, originalColour); + fr.setFeatureFilter(featureType, originalFilter); + ap.paintAlignment(true, true); + } + + /** + * Action on text entry of a threshold value + */ + protected void thresholdValue_actionPerformed() + { + try + { + adjusting = true; + float f = Float.parseFloat(thresholdValue.getText()); + slider.setValue((int) (f * scaleFactor)); + threshline.value = f; + thresholdValue.setBackground(Color.white); // ok + + /* + * force repaint of any Overview window or structure + */ + ap.paintAlignment(true, true); + } catch (NumberFormatException ex) + { + thresholdValue.setBackground(Color.red); // not ok + } finally + { + adjusting = false; + } + } + + /** + * Action on change of threshold slider value. This may be done interactively + * (by moving the slider), or programmatically (to update the slider after + * manual input of a threshold value). + */ + protected void sliderValueChanged() + { + threshline.value = getRoundedSliderValue(); + + /* + * repaint alignment, but not Overview or structure, + * to avoid overload while dragging the slider + */ + colourChanged(false); + } + + /** + * Converts the slider value to its absolute value by dividing by the + * scaleFactor. Rounding errors are squashed by forcing min/max of slider range + * to the actual min/max of feature score range + * + * @return + */ + private float getRoundedSliderValue() + { + int value = slider.getValue(); + float f = value == slider.getMaximum() ? max + : (value == slider.getMinimum() ? min : value / scaleFactor); + return f; + } + + void addActionListener(ActionListener listener) + { + if (featureSettings != null) + { + System.err.println( + "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser"); + } + featureSettings = listener; + } + + /** + * A helper method to build the drop-down choice of attributes for a feature. If + * 'withRange' is true, then Score, and any attributes with a min-max range, are + * added. If 'withText' is true, Label and any known attributes are added. This + * allows 'categorical numerical' attributes e.g. codon position to be coloured + * by text. + *

    + * Where metadata is available with a description for an attribute, that is + * added as a tooltip. + *

    + * 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. + *

    + * This method does not add any ActionListener to the JComboBox. + * + * @param attNames + * @param withRange + * @param withText + */ + protected JComboBox populateAttributesDropdown( + List attNames, boolean withRange, boolean withText) + { + List displayAtts = new ArrayList<>(); + List tooltips = new ArrayList<>(); + + if (withText) + { + displayAtts.add(LABEL_18N); + tooltips.add(MessageManager.getString("label.description")); + } + if (withRange) + { + float[][] minMax = fr.getMinMax().get(featureType); + if (minMax != null && minMax[0][0] != minMax[0][1]) + { + displayAtts.add(SCORE_18N); + tooltips.add(SCORE_18N); + } + } + + FeatureAttributes fa = FeatureAttributes.getInstance(); + for (String[] attName : attNames) + { + float[] minMax = fa.getMinMax(featureType, attName); + boolean hasRange = minMax != null && minMax[0] != minMax[1]; + if (!withText && !hasRange) + { + continue; + } + displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName)); + String desc = fa.getDescription(featureType, attName); + if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH) + { + desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "..."; + } + tooltips.add(desc == null ? "" : desc); + } + + JComboBox 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 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: + *

      + *
    • a drop-down list of Label, Score and attribute names to choose from
    • + *
    • a drop-down list of conditions to choose from
    • + *
    • a text field for input of a match pattern
    • + *
    • optionally, a 'remove' button
    • + *
    + * The filter values are set as defaults for the input fields. The 'remove' + * button is added unless the pattern is empty (incomplete filter condition). + *

    + * Action handlers on these fields provide for + *

      + *
    • validate pattern field - should be numeric if condition is numeric
    • + *
    • save filters and refresh display on any (valid) change
    • + *
    • remove filter and refresh on 'Remove'
    • + *
    • update conditions list on change of Label/Score/Attribute
    • + *
    • refresh value field tooltip with min-max range on change of + * attribute
    • + *
    + * + * @param filter + * @param attNames + * @param filterIndex + * @return + */ + protected JPanel addFilter(FeatureMatcherI filter, + List 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 attCombo = populateAttributesDropdown(attNames, + true, true); + String filterBy = setSelectedAttribute(attCombo, filter); + + JComboBox condCombo = new JComboBox<>(); + + JTextField patternField = new JTextField(8); + patternField.setText(pattern); + + /* + * action handlers that validate and (if valid) apply changes + */ + ActionListener actionListener = new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + if (validateFilter(patternField, condCombo)) + { + if (updateFilter(attCombo, condCombo, patternField, filterIndex)) + { + filtersChanged(); + } + } + } + }; + ItemListener itemListener = new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + actionListener.actionPerformed(null); + } + }; + + if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row + { + attCombo.setSelectedIndex(0); + } + else + { + attCombo.setSelectedItem( + FeatureMatcher.toAttributeDisplayName(attName)); + } + attCombo.addItemListener(new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + /* + * on change of attribute, refresh the conditions list to + * ensure it is appropriate for the attribute datatype + */ + populateConditions((String) attCombo.getSelectedItem(), + (Condition) condCombo.getSelectedItem(), condCombo, + patternField); + actionListener.actionPerformed(null); + } + }); + + filterRow.add(attCombo); + + /* + * drop-down choice of test condition + */ + populateConditions(filterBy, cond, condCombo, patternField); + condCombo.setPreferredSize(new Dimension(150, 20)); + condCombo.addItemListener(itemListener); + filterRow.add(condCombo); + + /* + * pattern to match against + */ + patternField.addActionListener(actionListener); + patternField.addFocusListener(new FocusAdapter() + { + @Override + public void focusLost(FocusEvent e) + { + actionListener.actionPerformed(null); + } + }); + filterRow.add(patternField); + + /* + * disable pattern field for condition 'Present / NotPresent' + */ + Condition selectedCondition = (Condition) condCombo.getSelectedItem(); + 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 attCombo, + FeatureMatcherI filter) + { + String item = null; + if (filter.isByScore()) + { + item = SCORE_18N; + } + else if (filter.isByLabel()) + { + item = LABEL_18N; + } + else + { + item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute()); + } + attCombo.setSelectedItem(item); + return item; + } + + /** + * If a numeric comparison condition is selected, 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 condCombo, JTextField patternField) + { + Datatype type = FeatureAttributes.getInstance().getDatatype(featureType, + FeatureMatcher.fromAttributeDisplayName(attName)); + if (LABEL_18N.equals(attName)) + { + type = Datatype.Character; + } + else if (SCORE_18N.equals(attName)) + { + type = Datatype.Number; + } + + /* + * remove itemListener before starting + */ + ItemListener listener = condCombo.getItemListeners()[0]; + condCombo.removeItemListener(listener); + boolean condIsValid = false; + condCombo.removeAllItems(); + for (Condition c : Condition.values()) + { + if ((c.isNumeric() && type != Datatype.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. + *

    + * 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 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. + *

    + * 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 attCombo, + JComboBox 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, + FeatureMatcher.fromAttributeDisplayName(attName)); + } + + filters.set(filterIndex, km); + + return true; + } + + /** + * Makes the dialog visible, at the Feature Colour tab or at the Filters tab + * + * @param coloursTab + */ + public void showTab(boolean coloursTab) + { + setVisible(true); + tabbedPane.setSelectedIndex(coloursTab ? 0 : 1); + } + + /** + * Action on any change to feature filtering, namely + *

      + *
    • change of selected attribute
    • + *
    • change of selected condition
    • + *
    • change of match pattern
    • + *
    • removal of a condition
    • + *
    + * 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(); + } +} diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index 1f2a3ad..a1726f1 100755 --- a/src/jalview/gui/IdPanel.java +++ b/src/jalview/gui/IdPanel.java @@ -108,8 +108,7 @@ public class IdPanel extends JPanel SequenceI sequence = av.getAlignment().getSequenceAt(seq); StringBuilder tip = new StringBuilder(64); seqAnnotReport.createTooltipAnnotationReport(tip, sequence, - av.isShowDBRefs(), av.isShowNPFeats(), - sp.seqCanvas.fr.getMinMax()); + av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr); setToolTipText(JvSwingUtils.wrapTooltip(true, sequence.getDisplayId(true) + " " + tip.toString())); } @@ -331,7 +330,8 @@ public class IdPanel extends JPanel * and any non-positional features */ List nlinks = Preferences.sequenceUrlLinks.getLinksForMenu(); - for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures()) + List features = sq.getFeatures().getNonPositionalFeatures(); + for (SequenceFeature sf : features) { if (sf.links != null) { @@ -342,7 +342,7 @@ public class IdPanel extends JPanel } } - PopupMenu pop = new PopupMenu(alignPanel, sq, nlinks, + PopupMenu pop = new PopupMenu(alignPanel, sq, features, Preferences.getGroupURLLinks()); pop.show(this, e.getX(), e.getY()); } diff --git a/src/jalview/gui/Jalview2XML.java b/src/jalview/gui/Jalview2XML.java index 4a15024..fdc2847 100644 --- a/src/jalview/gui/Jalview2XML.java +++ b/src/jalview/gui/Jalview2XML.java @@ -37,6 +37,10 @@ import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.datamodel.StructureViewerModel; import jalview.datamodel.StructureViewerModel.StructureData; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.ext.varna.RnaModel; import jalview.gui.StructureViewer.ViewerType; import jalview.io.DataSourceType; @@ -48,6 +52,8 @@ import jalview.schemabinding.version2.Annotation; import jalview.schemabinding.version2.AnnotationColours; import jalview.schemabinding.version2.AnnotationElement; import jalview.schemabinding.version2.CalcIdParam; +import jalview.schemabinding.version2.Colour; +import jalview.schemabinding.version2.CompoundMatcher; import jalview.schemabinding.version2.DBRef; import jalview.schemabinding.version2.Features; import jalview.schemabinding.version2.Group; @@ -60,6 +66,8 @@ import jalview.schemabinding.version2.MapListFrom; import jalview.schemabinding.version2.MapListTo; import jalview.schemabinding.version2.Mapping; import jalview.schemabinding.version2.MappingChoice; +import jalview.schemabinding.version2.MatchCondition; +import jalview.schemabinding.version2.MatcherSet; import jalview.schemabinding.version2.OtherData; import jalview.schemabinding.version2.PdbentryItem; import jalview.schemabinding.version2.Pdbids; @@ -75,6 +83,9 @@ import jalview.schemabinding.version2.ThresholdLine; import jalview.schemabinding.version2.Tree; import jalview.schemabinding.version2.UserColours; import jalview.schemabinding.version2.Viewport; +import jalview.schemabinding.version2.types.ColourThreshTypeType; +import jalview.schemabinding.version2.types.FeatureMatcherByType; +import jalview.schemabinding.version2.types.NoValueColour; import jalview.schemes.AnnotationColourGradient; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemeProperty; @@ -83,10 +94,12 @@ import jalview.schemes.ResidueProperties; import jalview.schemes.UserColourScheme; import jalview.structure.StructureSelectionManager; import jalview.structures.models.AAStructureBindingModel; +import jalview.util.Format; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.util.StringUtils; import jalview.util.jarInputStreamProvider; +import jalview.util.matcher.Condition; import jalview.viewmodel.AlignmentViewport; import jalview.viewmodel.ViewportRanges; import jalview.viewmodel.seqfeatures.FeatureRendererSettings; @@ -115,6 +128,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -879,15 +893,33 @@ public class Jalview2XML } if (sf.otherDetails != null) { - String key; - Iterator keys = sf.otherDetails.keySet().iterator(); - while (keys.hasNext()) + /* + * save feature attributes, which may be simple strings or + * map valued (have sub-attributes) + */ + for (Entry entry : sf.otherDetails.entrySet()) { - key = keys.next(); - OtherData keyValue = new OtherData(); - keyValue.setKey(key); - keyValue.setValue(sf.otherDetails.get(key).toString()); - features.addOtherData(keyValue); + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof Map) + { + for (Entry subAttribute : ((Map) value) + .entrySet()) + { + OtherData otherData = new OtherData(); + otherData.setKey(key); + otherData.setKey2(subAttribute.getKey()); + otherData.setValue(subAttribute.getValue().toString()); + features.addOtherData(otherData); + } + } + else + { + OtherData otherData = new OtherData(); + otherData.setKey(key); + otherData.setValue(value.toString()); + features.addOtherData(otherData); + } } } @@ -1313,19 +1345,33 @@ public class Jalview2XML { jalview.schemabinding.version2.FeatureSettings fs = new jalview.schemabinding.version2.FeatureSettings(); - String[] renderOrder = ap.getSeqPanel().seqCanvas - .getFeatureRenderer().getRenderOrder() - .toArray(new String[0]); + FeatureRenderer fr = ap.getSeqPanel().seqCanvas + .getFeatureRenderer(); + String[] renderOrder = fr.getRenderOrder().toArray(new String[0]); Vector settingsAdded = new Vector<>(); if (renderOrder != null) { for (String featureType : renderOrder) { - FeatureColourI fcol = ap.getSeqPanel().seqCanvas - .getFeatureRenderer().getFeatureStyle(featureType); Setting setting = new Setting(); setting.setType(featureType); + + /* + * save any filter for the feature type + */ + FeatureMatcherSetI filter = fr.getFeatureFilter(featureType); + if (filter != null) { + Iterator filters = filter.getMatchers().iterator(); + FeatureMatcherI firstFilter = filters.next(); + setting.setMatcherSet(Jalview2XML.marshalFilter( + firstFilter, filters, filter.isAnded())); + } + + /* + * save colour scheme for the feature type + */ + FeatureColourI fcol = fr.getFeatureStyle(featureType); if (!fcol.isSimpleColour()) { setting.setColour(fcol.getMaxColour().getRGB()); @@ -1333,8 +1379,25 @@ public class Jalview2XML setting.setMin(fcol.getMin()); setting.setMax(fcol.getMax()); setting.setColourByLabel(fcol.isColourByLabel()); + if (fcol.isColourByAttribute()) + { + setting.setAttributeName(fcol.getAttributeName()); + } setting.setAutoScale(fcol.isAutoScaled()); setting.setThreshold(fcol.getThreshold()); + Color noColour = fcol.getNoColour(); + if (noColour == null) + { + setting.setNoValueColour(NoValueColour.NONE); + } + else if (noColour.equals(fcol.getMaxColour())) + { + setting.setNoValueColour(NoValueColour.MAX); + } + else + { + setting.setNoValueColour(NoValueColour.MIN); + } // -1 = No threshold, 0 = Below, 1 = Above setting.setThreshstate(fcol.isAboveThreshold() ? 1 : (fcol.isBelowThreshold() ? 0 : -1)); @@ -1346,7 +1409,7 @@ public class Jalview2XML setting.setDisplay( av.getFeaturesDisplayed().isVisible(featureType)); - float rorder = ap.getSeqPanel().seqCanvas.getFeatureRenderer() + float rorder = fr .getOrder(featureType); if (rorder > -1) { @@ -1358,8 +1421,7 @@ public class Jalview2XML } // is groups actually supposed to be a map here ? - Iterator en = ap.getSeqPanel().seqCanvas - .getFeatureRenderer().getFeatureGroups().iterator(); + Iterator en = fr.getFeatureGroups().iterator(); Vector groupsAdded = new Vector<>(); while (en.hasNext()) { @@ -1370,8 +1432,7 @@ public class Jalview2XML } Group g = new Group(); g.setName(grp); - g.setDisplay(((Boolean) ap.getSeqPanel().seqCanvas - .getFeatureRenderer().checkGroupVisibility(grp, false)) + g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false)) .booleanValue()); fs.addGroup(g); groupsAdded.addElement(grp); @@ -2962,19 +3023,46 @@ public class Jalview2XML features[f].getEnd(), features[f].getScore(), features[f].getFeatureGroup()); sf.setStatus(features[f].getStatus()); + + /* + * load any feature attributes - include map-valued attributes + */ + Map> mapAttributes = new HashMap<>(); for (int od = 0; od < features[f].getOtherDataCount(); od++) { OtherData keyValue = features[f].getOtherData(od); - if (keyValue.getKey().startsWith("LINK")) + String attributeName = keyValue.getKey(); + String attributeValue = keyValue.getValue(); + if (attributeName.startsWith("LINK")) { - sf.addLink(keyValue.getValue()); + sf.addLink(attributeValue); } else { - sf.setValue(keyValue.getKey(), keyValue.getValue()); + String subAttribute = keyValue.getKey2(); + if (subAttribute == null) + { + // simple string-valued attribute + sf.setValue(attributeName, attributeValue); + } + else + { + // attribute 'key' has sub-attribute 'key2' + if (!mapAttributes.containsKey(attributeName)) + { + mapAttributes.put(attributeName, new HashMap<>()); + } + mapAttributes.get(attributeName).put(subAttribute, + attributeValue); + } } - } + for (Entry> mapAttribute : mapAttributes + .entrySet()) + { + sf.setValue(mapAttribute.getKey(), mapAttribute.getValue()); + } + // adds feature to datasequence's feature set (since Jalview 2.10) al.getSequenceAt(i).addSequenceFeature(sf); } @@ -4550,9 +4638,11 @@ public class Jalview2XML af.viewport.setShowGroupConservation(false); } - // recover featre settings + // recover feature settings if (jms.getFeatureSettings() != null) { + FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas + .getFeatureRenderer(); FeaturesDisplayed fdi; af.viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed()); String[] renderOrder = new String[jms.getFeatureSettings() @@ -4564,14 +4654,51 @@ public class Jalview2XML .getSettingCount(); fs++) { Setting setting = jms.getFeatureSettings().getSetting(fs); + String featureType = setting.getType(); + + /* + * restore feature filters (if any) + */ + MatcherSet filters = setting.getMatcherSet(); + if (filters != null) + { + FeatureMatcherSetI filter = Jalview2XML + .unmarshalFilter(featureType, filters); + if (!filter.isEmpty()) + { + fr.setFeatureFilter(featureType, filter); + } + } + + /* + * restore feature colour scheme + */ + Color maxColour = new Color(setting.getColour()); if (setting.hasMincolour()) { - FeatureColourI gc = setting.hasMin() - ? new FeatureColour(new Color(setting.getMincolour()), - new Color(setting.getColour()), setting.getMin(), - setting.getMax()) - : new FeatureColour(new Color(setting.getMincolour()), - new Color(setting.getColour()), 0, 1); + /* + * minColour is always set unless a simple colour + * (including for colour by label though it doesn't use it) + */ + Color minColour = new Color(setting.getMincolour()); + Color noValueColour = minColour; + NoValueColour noColour = setting.getNoValueColour(); + if (noColour == NoValueColour.NONE) + { + noValueColour = null; + } + else if (noColour == NoValueColour.MAX) + { + noValueColour = maxColour; + } + float min = setting.hasMin() ? setting.getMin() : 0f; + float max = setting.hasMin() ? setting.getMax() : 1f; + FeatureColourI gc = new FeatureColour(minColour, maxColour, + noValueColour, min, max); + if (setting.getAttributeNameCount() > 0) + { + gc.setAttributeName(setting.getAttributeName()); + } if (setting.hasThreshold()) { gc.setThreshold(setting.getThreshold()); @@ -4596,26 +4723,26 @@ public class Jalview2XML gc.setColourByLabel(setting.getColourByLabel()); } // and put in the feature colour table. - featureColours.put(setting.getType(), gc); + featureColours.put(featureType, gc); } else { - featureColours.put(setting.getType(), - new FeatureColour(new Color(setting.getColour()))); + featureColours.put(featureType, + new FeatureColour(maxColour)); } - renderOrder[fs] = setting.getType(); + renderOrder[fs] = featureType; if (setting.hasOrder()) { - featureOrder.put(setting.getType(), setting.getOrder()); + featureOrder.put(featureType, setting.getOrder()); } else { - featureOrder.put(setting.getType(), new Float( + featureOrder.put(featureType, new Float( fs / jms.getFeatureSettings().getSettingCount())); } if (setting.getDisplay()) { - fdi.setVisible(setting.getType()); + fdi.setVisible(featureType); } } Map fgtable = new Hashtable<>(); @@ -4629,9 +4756,7 @@ public class Jalview2XML // jms.getFeatureSettings().getTransparency() : 0.0, featureOrder); FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder, fgtable, featureColours, 1.0f, featureOrder); - af.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer() - .transferSettings(frs); - + fr.transferSettings(frs); } if (view.getHiddenColumnsCount() > 0) @@ -5333,7 +5458,7 @@ public class Jalview2XML if (this.frefedSequence == null) { - frefedSequence = new Vector(); + frefedSequence = new Vector<>(); } viewportsAdded.clear(); @@ -5585,4 +5710,289 @@ public class Jalview2XML { return counter++; } + + /** + * Populates an XML model of the feature colour scheme for one feature type + * + * @param featureType + * @param fcol + * @return + */ + protected static jalview.schemabinding.version2.Colour marshalColour( + String featureType, FeatureColourI fcol) + { + jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour(); + if (fcol.isSimpleColour()) + { + col.setRGB(Format.getHexString(fcol.getColour())); + } + else + { + col.setRGB(Format.getHexString(fcol.getMaxColour())); + col.setMin(fcol.getMin()); + col.setMax(fcol.getMax()); + col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour())); + col.setAutoScale(fcol.isAutoScaled()); + col.setThreshold(fcol.getThreshold()); + col.setColourByLabel(fcol.isColourByLabel()); + col.setThreshType(fcol.isAboveThreshold() ? ColourThreshTypeType.ABOVE + : (fcol.isBelowThreshold() ? ColourThreshTypeType.BELOW + : ColourThreshTypeType.NONE)); + if (fcol.isColourByAttribute()) + { + col.setAttributeName(fcol.getAttributeName()); + } + Color noColour = fcol.getNoColour(); + if (noColour == null) + { + col.setNoValueColour(NoValueColour.NONE); + } + else if (noColour == fcol.getMaxColour()) + { + col.setNoValueColour(NoValueColour.MAX); + } + else + { + col.setNoValueColour(NoValueColour.MIN); + } + } + col.setName(featureType); + return col; + } + + /** + * Populates an XML model of the feature filter(s) for one feature type + * + * @param firstMatcher + * the first (or only) match condition) + * @param filter + * remaining match conditions (if any) + * @param and + * if true, conditions are and-ed, else or-ed + */ + protected static MatcherSet marshalFilter(FeatureMatcherI firstMatcher, + Iterator filters, boolean and) + { + MatcherSet result = new MatcherSet(); + + if (filters.hasNext()) + { + /* + * compound matcher + */ + CompoundMatcher compound = new CompoundMatcher(); + compound.setAnd(and); + MatcherSet matcher1 = marshalFilter(firstMatcher, + Collections.emptyIterator(), and); + compound.addMatcherSet(matcher1); + FeatureMatcherI nextMatcher = filters.next(); + MatcherSet matcher2 = marshalFilter(nextMatcher, filters, and); + compound.addMatcherSet(matcher2); + result.setCompoundMatcher(compound); + } + else + { + /* + * single condition matcher + */ + MatchCondition matcherModel = new MatchCondition(); + matcherModel.setCondition( + firstMatcher.getMatcher().getCondition().getStableName()); + matcherModel.setValue(firstMatcher.getMatcher().getPattern()); + if (firstMatcher.isByAttribute()) + { + matcherModel.setBy(FeatureMatcherByType.BYATTRIBUTE); + matcherModel.setAttributeName(firstMatcher.getAttribute()); + } + else if (firstMatcher.isByLabel()) + { + matcherModel.setBy(FeatureMatcherByType.BYLABEL); + } + else if (firstMatcher.isByScore()) + { + matcherModel.setBy(FeatureMatcherByType.BYSCORE); + } + result.setMatchCondition(matcherModel); + } + + return result; + } + + /** + * Loads one XML model of a feature filter to a Jalview object + * + * @param featureType + * @param matcherSetModel + * @return + */ + protected static FeatureMatcherSetI unmarshalFilter( + String featureType, MatcherSet matcherSetModel) + { + FeatureMatcherSetI result = new FeatureMatcherSet(); + try + { + unmarshalFilterConditions(result, matcherSetModel, true); + } catch (IllegalStateException e) + { + // mixing AND and OR conditions perhaps + System.err.println( + String.format("Error reading filter conditions for '%s': %s", + featureType, e.getMessage())); + // return as much as was parsed up to the error + } + + return result; + } + + /** + * Adds feature match conditions to matcherSet as unmarshalled from XML + * (possibly recursively for compound conditions) + * + * @param matcherSet + * @param matcherSetModel + * @param and + * if true, multiple conditions are AND-ed, else they are OR-ed + * @throws IllegalStateException + * if AND and OR conditions are mixed + */ + protected static void unmarshalFilterConditions( + FeatureMatcherSetI matcherSet, MatcherSet matcherSetModel, + boolean and) + { + MatchCondition mc = matcherSetModel.getMatchCondition(); + if (mc != null) + { + /* + * single condition + */ + FeatureMatcherByType filterBy = mc.getBy(); + Condition cond = Condition.fromString(mc.getCondition()); + String pattern = mc.getValue(); + FeatureMatcherI matchCondition = null; + if (filterBy == FeatureMatcherByType.BYLABEL) + { + matchCondition = FeatureMatcher.byLabel(cond, pattern); + } + else if (filterBy == FeatureMatcherByType.BYSCORE) + { + matchCondition = FeatureMatcher.byScore(cond, pattern); + + } + else if (filterBy == FeatureMatcherByType.BYATTRIBUTE) + { + String[] attNames = mc.getAttributeName(); + matchCondition = FeatureMatcher.byAttribute(cond, pattern, + attNames); + } + + /* + * note this throws IllegalStateException if AND-ing to a + * previously OR-ed compound condition, or vice versa + */ + if (and) + { + matcherSet.and(matchCondition); + } + else + { + matcherSet.or(matchCondition); + } + } + else + { + /* + * compound condition + */ + MatcherSet[] matchers = matcherSetModel.getCompoundMatcher() + .getMatcherSet(); + boolean anded = matcherSetModel.getCompoundMatcher().getAnd(); + if (matchers.length == 2) + { + unmarshalFilterConditions(matcherSet, matchers[0], anded); + unmarshalFilterConditions(matcherSet, matchers[1], anded); + } + else + { + System.err.println("Malformed compound filter condition"); + } + } + } + + /** + * Loads one XML model of a feature colour to a Jalview object + * + * @param colourModel + * @return + */ + protected static FeatureColourI unmarshalColour( + jalview.schemabinding.version2.Colour colourModel) + { + FeatureColourI colour = null; + + if (colourModel.hasMax()) + { + Color mincol = null; + Color maxcol = null; + Color noValueColour = null; + + try + { + mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16)); + maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16)); + } catch (Exception e) + { + Cache.log.warn("Couldn't parse out graduated feature color.", e); + } + + NoValueColour noCol = colourModel.getNoValueColour(); + if (noCol == NoValueColour.MIN) + { + noValueColour = mincol; + } + else if (noCol == NoValueColour.MAX) + { + noValueColour = maxcol; + } + + colour = new FeatureColour(mincol, maxcol, noValueColour, + colourModel.getMin(), + colourModel.getMax()); + String[] attributes = colourModel.getAttributeName(); + if (attributes != null && attributes.length > 0) + { + colour.setAttributeName(attributes); + } + if (colourModel.hasAutoScale()) + { + colour.setAutoScaled(colourModel.getAutoScale()); + } + if (colourModel.hasColourByLabel()) + { + colour.setColourByLabel(colourModel.getColourByLabel()); + } + if (colourModel.hasThreshold()) + { + colour.setThreshold(colourModel.getThreshold()); + } + ColourThreshTypeType ttyp = colourModel.getThreshType(); + if (ttyp != null) + { + if (ttyp == ColourThreshTypeType.ABOVE) + { + colour.setAboveThreshold(true); + } + else if (ttyp == ColourThreshTypeType.BELOW) + { + colour.setBelowThreshold(true); + } + } + } + else + { + Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16)); + colour = new FeatureColour(color); + } + + return colour; + } } diff --git a/src/jalview/gui/JalviewDialog.java b/src/jalview/gui/JalviewDialog.java index 05f5ffc..1d7bf3d 100644 --- a/src/jalview/gui/JalviewDialog.java +++ b/src/jalview/gui/JalviewDialog.java @@ -27,8 +27,8 @@ import java.awt.Dimension; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.awt.event.WindowListener; import javax.swing.JButton; import javax.swing.JDialog; @@ -118,55 +118,14 @@ public abstract class JalviewDialog extends JPanel closeDialog(); } }); - frame.addWindowListener(new WindowListener() + frame.addWindowListener(new WindowAdapter() { - - @Override - public void windowOpened(WindowEvent e) - { - // TODO Auto-generated method stub - - } - - @Override - public void windowIconified(WindowEvent e) - { - // TODO Auto-generated method stub - - } - - @Override - public void windowDeiconified(WindowEvent e) - { - // TODO Auto-generated method stub - - } - - @Override - public void windowDeactivated(WindowEvent e) - { - // TODO Auto-generated method stub - - } - @Override public void windowClosing(WindowEvent e) { // user has cancelled the dialog closeDialog(); } - - @Override - public void windowClosed(WindowEvent e) - { - } - - @Override - public void windowActivated(WindowEvent e) - { - // TODO Auto-generated method stub - - } }); } @@ -177,8 +136,8 @@ public abstract class JalviewDialog extends JPanel { try { - frame.dispose(); raiseClosed(); + frame.dispose(); } catch (Exception ex) { } diff --git a/src/jalview/gui/JvSwingUtils.java b/src/jalview/gui/JvSwingUtils.java index 0a765cb..4658668 100644 --- a/src/jalview/gui/JvSwingUtils.java +++ b/src/jalview/gui/JvSwingUtils.java @@ -24,14 +24,20 @@ import jalview.util.MessageManager; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Component; import java.awt.Font; import java.awt.GridLayout; import java.awt.Rectangle; import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; import java.util.Objects; import javax.swing.AbstractButton; +import javax.swing.BorderFactory; import javax.swing.JButton; +import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenu; @@ -39,6 +45,8 @@ import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.SwingConstants; +import javax.swing.border.Border; +import javax.swing.border.TitledBorder; /** * useful functions for building Swing GUIs @@ -304,4 +312,71 @@ public final class JvSwingUtils comp.setFont(JvSwingUtils.getLabelFont()); } + /** + * A helper method to build a drop-down choice of values, with tooltips for + * the entries + * + * @param entries + * @param tooltips + */ + public static JComboBox buildComboWithTooltips( + List entries, List tooltips) + { + JComboBox 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; + } + } diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 850a09a..0cf7ef4 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -34,7 +34,6 @@ import jalview.datamodel.Annotation; import jalview.datamodel.DBRefEntry; import jalview.datamodel.HiddenColumns; import jalview.datamodel.PDBEntry; -import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; @@ -50,6 +49,7 @@ import jalview.schemes.PIDColourScheme; import jalview.util.GroupUrlLink; import jalview.util.GroupUrlLink.UrlStringTooLongException; import jalview.util.MessageManager; +import jalview.util.StringUtils; import jalview.util.UrlLink; import java.awt.Color; @@ -176,25 +176,31 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * Creates a new PopupMenu object. * * @param ap - * DOCUMENT ME! * @param seq - * DOCUMENT ME! + * @param features + * non-positional features (for seq not null), or positional features + * at residue (for seq equal to null) */ - public PopupMenu(final AlignmentPanel ap, Sequence seq, - List links) + public PopupMenu(final AlignmentPanel ap, SequenceI seq, + List 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 links, List groupLinks) + public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq, + List features, List groupLinks) { // ///////////////////////////////////////////////////////// // If this is activated from the sequence panel, the user may want to @@ -202,7 +208,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener // // If from the IDPanel, we must display the sequence menu // //////////////////////////////////////////////////////// - this.ap = ap; + this.ap = alignPanel; sequence = seq; for (String ff : FileFormats.getInstance().getWritableFormats(true)) @@ -237,9 +243,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener /* * And repeat for the current selection group (if there is one): */ - final List selectedGroup = (ap.av.getSelectionGroup() == null + final List selectedGroup = (alignPanel.av.getSelectionGroup() == null ? Collections. emptyList() - : ap.av.getSelectionGroup().getSequences()); + : alignPanel.av.getSelectionGroup().getSequences()); buildAnnotationTypesMenus(groupShowAnnotationsMenu, groupHideAnnotationsMenu, selectedGroup); configureReferenceAnnotationsMenu(groupAddReferenceAnnotations, @@ -257,7 +263,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener if (seq != null) { sequenceMenu.setText(sequence.getName()); - if (seq == ap.av.getAlignment().getSeqrep()) + if (seq == alignPanel.av.getAlignment().getSeqrep()) { makeReferenceSeq.setText( MessageManager.getString("action.unmark_as_reference")); @@ -268,7 +274,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener MessageManager.getString("action.set_as_reference")); } - if (!ap.av.getAlignment().isNucleotide()) + if (!alignPanel.av.getAlignment().isNucleotide()) { remove(rnaStructureMenu); } @@ -279,7 +285,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * add menu items to 2D-render any alignment or sequence secondary * structure annotation */ - AlignmentAnnotation[] aas = ap.av.getAlignment() + AlignmentAnnotation[] aas = alignPanel.av.getAlignment() .getAlignmentAnnotation(); if (aas != null) { @@ -299,7 +305,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener @Override public void actionPerformed(ActionEvent e) { - new AppVarna(seq, aa, ap); + new AppVarna(seq, aa, alignPanel); } }); rnaStructureMenu.add(menuItem); @@ -328,7 +334,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener public void actionPerformed(ActionEvent e) { // TODO: VARNA does'nt print gaps in the sequence - new AppVarna(seq, aa, ap); + new AppVarna(seq, aa, alignPanel); } }); rnaStructureMenu.add(menuItem); @@ -353,8 +359,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener }); add(menuItem); - if (ap.av.getSelectionGroup() != null - && ap.av.getSelectionGroup().getSize() > 1) + if (alignPanel.av.getSelectionGroup() != null + && alignPanel.av.getSelectionGroup().getSize() > 1) { menuItem = new JMenuItem(MessageManager .formatMessage("label.represent_group_with", new Object[] @@ -370,12 +376,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener sequenceMenu.add(menuItem); } - if (ap.av.hasHiddenRows()) + if (alignPanel.av.hasHiddenRows()) { - final int index = ap.av.getAlignment().findIndex(seq); + final int index = alignPanel.av.getAlignment().findIndex(seq); - if (ap.av.adjustForHiddenSeqs(index) - - ap.av.adjustForHiddenSeqs(index - 1) > 1) + if (alignPanel.av.adjustForHiddenSeqs(index) + - alignPanel.av.adjustForHiddenSeqs(index - 1) > 1) { menuItem = new JMenuItem( MessageManager.getString("action.reveal_sequences")); @@ -384,10 +390,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener @Override public void actionPerformed(ActionEvent e) { - ap.av.showSequence(index); - if (ap.overviewPanel != null) + alignPanel.av.showSequence(index); + if (alignPanel.overviewPanel != null) { - ap.overviewPanel.updateOverviewImage(); + alignPanel.overviewPanel.updateOverviewImage(); } } }); @@ -396,7 +402,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } } // for the case when no sequences are even visible - if (ap.av.hasHiddenRows()) + if (alignPanel.av.hasHiddenRows()) { { menuItem = new JMenuItem( @@ -406,10 +412,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener @Override public void actionPerformed(ActionEvent e) { - ap.av.showAllHiddenSeqs(); - if (ap.overviewPanel != null) + alignPanel.av.showAllHiddenSeqs(); + if (alignPanel.overviewPanel != null) { - ap.overviewPanel.updateOverviewImage(); + alignPanel.overviewPanel.updateOverviewImage(); } } }); @@ -418,9 +424,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } } - SequenceGroup sg = ap.av.getSelectionGroup(); + SequenceGroup sg = alignPanel.av.getSelectionGroup(); boolean isDefinedGroup = (sg != null) - ? ap.av.getAlignment().getGroups().contains(sg) + ? alignPanel.av.getAlignment().getGroups().contains(sg) : false; if (sg != null && sg.getSize() > 0) @@ -458,7 +464,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener Hashtable pdbe = new Hashtable<>(), reppdb = new Hashtable<>(); SequenceI sqass = null; - for (SequenceI sq : ap.av.getSequenceSelection()) + for (SequenceI sq : alignPanel.av.getSequenceSelection()) { Vector pes = sq.getDatasetSequence().getAllPDBEntries(); if (pes != null && pes.size() > 0) @@ -508,24 +514,133 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener rnaStructureMenu.setVisible(false); } - if (links != null && links.size() > 0) + addLinks(seq, features); + + if (seq == null) + { + addFeatureDetails(features); + } + } + + /** + * Add a link to show feature details for each sequence feature + * + * @param features + */ + protected void addFeatureDetails(List features) + { + if (features == null || features.isEmpty()) + { + return; + } + JMenu details = new JMenu( + MessageManager.getString("label.feature_details")); + add(details); + + for (final SequenceFeature sf : features) { - addFeatureLinks(seq, links); + int start = sf.getBegin(); + int end = sf.getEnd(); + String desc = null; + if (start == end) + { + desc = String.format("%s %d", sf.getType(), start); + } + else + { + desc = String.format("%s %d-%d", sf.getType(), start, end); + } + String tooltip = desc; + String description = sf.getDescription(); + if (description != null) + { + description = StringUtils.stripHtmlTags(description); + if (description.length() > 12) + { + desc = desc + " " + description.substring(0, 12) + ".."; + } + else + { + desc = desc + " " + description; + } + tooltip = tooltip + " " + description; + } + if (sf.getFeatureGroup() != null) + { + tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")"); + } + JMenuItem item = new JMenuItem(desc); + item.setToolTipText(tooltip); + item.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + showFeatureDetails(sf); + } + }); + details.add(item); } } /** + * Opens a panel showing a text report of feature dteails + * + * @param sf + */ + protected void showFeatureDetails(SequenceFeature sf) + { + CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer(); + // it appears Java's CSS does not support border-collaps :-( + cap.addStylesheetRule("table { border-collapse: collapse;}"); + cap.addStylesheetRule("table, td, th {border: 1px solid black;}"); + cap.setText(sf.getDetailsReport()); + + Desktop.addInternalFrame(cap, + MessageManager.getString("label.feature_details"), 500, 500); + } + + /** * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided. + * When seq is not null, these are links for the sequence id, which may be to + * external web sites for the sequence accession, and/or links embedded in + * non-positional features. When seq is null, only links embedded in the + * provided features are added. * * @param seq - * @param links + * @param features */ - void addFeatureLinks(final SequenceI seq, List links) + void addLinks(final SequenceI seq, List features) { JMenu linkMenu = new JMenu(MessageManager.getString("action.link")); + + List 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> linkset = new LinkedHashMap<>(); - for (String link : links) + for (String link : nlinks) { UrlLink urlLink = null; try @@ -548,25 +663,18 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener addshowLinks(linkMenu, linkset.values()); - // disable link menu if there are no valid entries + // only add link menu if it has entries if (linkMenu.getItemCount() > 0) { - linkMenu.setEnabled(true); - } - else - { - linkMenu.setEnabled(false); - } - - if (sequence != null) - { - sequenceMenu.add(linkMenu); - } - else - { - add(linkMenu); + if (sequence != null) + { + sequenceMenu.add(linkMenu); + } + else + { + add(linkMenu); + } } - } /** @@ -1530,10 +1638,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener new Object[] { seq.getDisplayId(true) }) + "

    "); 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("

    "); } cap.setText("" + contents.toString() + ""); diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index 61cac46..fb6efe5 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -59,7 +59,6 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -76,12 +75,11 @@ import javax.swing.ToolTipManager; public class SeqPanel extends JPanel implements MouseListener, MouseMotionListener, MouseWheelListener, SequenceListener, SelectionListener - { - /** DOCUMENT ME!! */ + private static final int MAX_TOOLTIP_LENGTH = 300; + public SeqCanvas seqCanvas; - /** DOCUMENT ME!! */ public AlignmentPanel ap; /* @@ -148,35 +146,33 @@ public class SeqPanel extends JPanel SearchResultsI lastSearchResults; /** - * Creates a new SeqPanel object. + * Creates a new SeqPanel object * - * @param avp - * DOCUMENT ME! - * @param p - * DOCUMENT ME! + * @param viewport + * @param alignPanel */ - public SeqPanel(AlignViewport av, AlignmentPanel ap) + public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel) { linkImageURL = getClass().getResource("/images/link.gif"); seqARep = new SequenceAnnotationReport(linkImageURL.toString()); ToolTipManager.sharedInstance().registerComponent(this); ToolTipManager.sharedInstance().setInitialDelay(0); ToolTipManager.sharedInstance().setDismissDelay(10000); - this.av = av; + this.av = viewport; setBackground(Color.white); - seqCanvas = new SeqCanvas(ap); + seqCanvas = new SeqCanvas(alignPanel); setLayout(new BorderLayout()); add(seqCanvas, BorderLayout.CENTER); - this.ap = ap; + this.ap = alignPanel; - if (!av.isDataset()) + if (!viewport.isDataset()) { addMouseMotionListener(this); addMouseListener(this); addMouseWheelListener(this); - ssm = av.getStructureSelectionManager(); + ssm = viewport.getStructureSelectionManager(); ssm.addStructureViewerListener(this); ssm.addSelectionListener(this); } @@ -838,7 +834,7 @@ public class SeqPanel extends JPanel List 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) // { @@ -847,6 +843,11 @@ public class SeqPanel extends JPanel } else { + if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant + { + tooltipText.setLength(MAX_TOOLTIP_LENGTH); + tooltipText.append("..."); + } String textString = tooltipText.toString(); if (lastTooltip == null || !lastTooltip.equals(textString)) { @@ -1825,21 +1826,10 @@ public class SeqPanel extends JPanel final int column = findColumn(evt); final int seq = findSeq(evt); SequenceI sequence = av.getAlignment().getSequenceAt(seq); - List allFeatures = ap.getFeatureRenderer() + List features = ap.getFeatureRenderer() .findFeaturesAtColumn(sequence, column + 1); - List 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()); } diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index d2282b1..99663c8 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -31,6 +31,8 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.io.gff.GffHelperBase; import jalview.io.gff.GffHelperFactory; import jalview.io.gff.GffHelperI; @@ -68,6 +70,16 @@ import java.util.Map.Entry; */ public class FeaturesFile extends AlignFile implements FeaturesSourceI { + private static final String TAB_REGEX = "\\t"; + + private static final String STARTGROUP = "STARTGROUP"; + + private static final String ENDGROUP = "ENDGROUP"; + + private static final String STARTFILTERS = "STARTFILTERS"; + + private static final String ENDFILTERS = "ENDFILTERS"; + private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED"; private static final String NOTE = "Note"; @@ -169,7 +181,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param align * - alignment/dataset containing sequences that are to be annotated * @param colours - * - hashtable to store feature colour definitions + * - map to store feature colour definitions * @param removeHTML * - process html strings into plain text * @param relaxedIdmatching @@ -180,11 +192,34 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI Map colours, boolean removeHTML, boolean relaxedIdmatching) { - Map gffProps = new HashMap(); + return parse(align, colours, null, removeHTML, relaxedIdmatching); + } + + /** + * Parse GFF or Jalview format sequence features file + * + * @param align + * - alignment/dataset containing sequences that are to be annotated + * @param colours + * - map to store feature colour definitions + * @param filters + * - map to store feature filter definitions + * @param removeHTML + * - process html strings into plain text + * @param relaxedIdmatching + * - when true, ID matches to compound sequence IDs are allowed + * @return true if features were added + */ + public boolean parse(AlignmentI align, + Map colours, + Map filters, boolean removeHTML, + boolean relaxedIdmatching) + { + Map gffProps = new HashMap<>(); /* * keep track of any sequences we try to create from the data */ - List newseqs = new ArrayList(); + List newseqs = new ArrayList<>(); String line = null; try @@ -204,7 +239,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI continue; } - gffColumns = line.split("\\t"); // tab as regex + gffColumns = line.split(TAB_REGEX); if (gffColumns.length == 1) { if (line.trim().equalsIgnoreCase("GFF")) @@ -218,18 +253,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - if (gffColumns.length > 1 && gffColumns.length < 4) + if (gffColumns.length > 0 && gffColumns.length < 4) { /* * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or * a feature type colour specification */ String ft = gffColumns[0]; - if (ft.equalsIgnoreCase("startgroup")) + if (ft.equalsIgnoreCase(STARTFILTERS)) + { + parseFilters(filters); + continue; + } + if (ft.equalsIgnoreCase(STARTGROUP)) { featureGroup = gffColumns[1]; } - else if (ft.equalsIgnoreCase("endgroup")) + else if (ft.equalsIgnoreCase(ENDGROUP)) { // We should check whether this is the current group, // but at present there's no way of showing more than 1 group @@ -290,6 +330,43 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type + * filter to the map for each line parsed. After exit from this method, + * nextLine() should return the line after ENDFILTERS (or we are already at + * end of file if ENDFILTERS was missing). + * + * @param filters + * @throws IOException + */ + protected void parseFilters(Map filters) + throws IOException + { + String line; + while ((line = nextLine()) != null) + { + if (line.toUpperCase().startsWith(ENDFILTERS)) + { + return; + } + String[] tokens = line.split(TAB_REGEX); + if (tokens.length != 2) + { + System.err.println(String.format("Invalid token count %d for %d", + tokens.length, line)); + } + else + { + String featureType = tokens[0]; + FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]); + if (fm != null && filters != null) + { + filters.put(featureType, fm); + } + } + } + } + + /** * Try to parse a Jalview format feature specification and add it as a * sequence feature to any matching sequences in the alignment. Returns true * if successful (a feature was added), or false if not. @@ -487,15 +564,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Returns contents of a Jalview format features file, for visible features, - * as filtered by type and group. Features with a null group are displayed if - * their feature type is visible. Non-positional features may optionally be - * included (with no check on type or group). + * Returns contents of a Jalview format features file, for visible features, as + * filtered by type and group. Features with a null group are displayed if their + * feature type is visible. Non-positional features may optionally be included + * (with no check on type or group). * * @param sequences * source of features * @param visible * map of colour for each visible feature type + * @param featureFilters * @param visibleFeatureGroups * @param includeNonPositional * if true, include non-positional features (regardless of group or @@ -504,6 +582,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI */ public String printJalviewFormat(SequenceI[] sequences, Map visible, + Map featureFilters, List visibleFeatureGroups, boolean includeNonPositional) { if (!includeNonPositional && (visible == null || visible.isEmpty())) @@ -531,10 +610,15 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI .toArray(new String[visible.keySet().size()]); /* + * feature filters if any + */ + outputFeatureFilters(out, visible, featureFilters); + + /* * sort groups alphabetically, and ensure that features with a * null or empty group are output after those in named groups */ - List sortedGroups = new ArrayList(visibleFeatureGroups); + List sortedGroups = new ArrayList<>(visibleFeatureGroups); sortedGroups.remove(null); sortedGroups.remove(""); Collections.sort(sortedGroups); @@ -560,13 +644,76 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - for (String group : sortedGroups) + /* + * positional features within groups + */ + foundSome |= outputFeaturesByGroup(out, sortedGroups, types, sequences); + + return foundSome ? out.toString() : "No Features Visible"; + } + + /** + * Outputs any feature filters defined for visible feature types, sandwiched by + * STARTFILTERS and ENDFILTERS lines + * + * @param out + * @param visible + * @param featureFilters + */ + void outputFeatureFilters(StringBuilder out, + Map visible, + Map featureFilters) + { + if (visible == null || featureFilters == null + || featureFilters.isEmpty()) + { + return; + } + + boolean first = true; + for (String featureType : visible.keySet()) + { + FeatureMatcherSetI filter = featureFilters.get(featureType); + if (filter != null) + { + if (first) + { + first = false; + out.append(newline).append(STARTFILTERS).append(newline); + } + out.append(featureType).append(TAB).append(filter.toStableString()) + .append(newline); + } + } + if (!first) + { + out.append(ENDFILTERS).append(newline).append(newline); + } + + } + + /** + * Appends output of sequence features within feature groups to the output + * buffer. Groups other than the null or empty group are sandwiched by + * STARTGROUP and ENDGROUP lines. + * + * @param out + * @param groups + * @param featureTypes + * @param sequences + * @return + */ + private boolean outputFeaturesByGroup(StringBuilder out, + List groups, String[] featureTypes, SequenceI[] sequences) + { + boolean foundSome = false; + for (String group : groups) { boolean isNamedGroup = (group != null && !"".equals(group)); if (isNamedGroup) { out.append(newline); - out.append("STARTGROUP").append(TAB); + out.append(STARTGROUP).append(TAB); out.append(group); out.append(newline); } @@ -577,11 +724,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI for (int i = 0; i < sequences.length; i++) { String sequenceName = sequences[i].getName(); - List features = new ArrayList(); - if (types.length > 0) + List features = new ArrayList<>(); + if (featureTypes.length > 0) { features.addAll(sequences[i].getFeatures().getFeaturesForGroup( - true, group, types)); + true, group, featureTypes)); } for (SequenceFeature sequenceFeature : features) @@ -593,13 +740,12 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI if (isNamedGroup) { - out.append("ENDGROUP").append(TAB); + out.append(ENDGROUP).append(TAB); out.append(group); out.append(newline); } } - - return foundSome ? out.toString() : "No Features Visible"; + return foundSome; } /** @@ -688,7 +834,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI dataset = new Alignment(new SequenceI[] {}); } - Map featureColours = new HashMap(); + Map featureColours = new HashMap<>(); boolean parseResult = parse(dataset, featureColours, false, true); if (!parseResult) { @@ -748,7 +894,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI for (SequenceI seq : sequences) { - List features = new ArrayList(); + List features = new ArrayList<>(); if (includeNonPositionalFeatures) { features.addAll(seq.getFeatures().getNonPositionalFeatures()); diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index f1ebcac..6b82671 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -20,13 +20,15 @@ */ package jalview.io; +import jalview.api.FeatureColourI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.io.gff.GffConstants; import jalview.util.MessageManager; +import jalview.util.StringUtils; import jalview.util.UrlLink; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.util.Arrays; import java.util.Collection; @@ -58,7 +60,7 @@ public class SequenceAnnotationReport /* * Comparator to order DBRefEntry by Source + accession id (case-insensitive), - * with 'Primary' sources placed before others + * with 'Primary' sources placed before others, and 'chromosome' first of all */ private static Comparator comparator = new Comparator() { @@ -66,6 +68,14 @@ public class SequenceAnnotationReport @Override public int compare(DBRefEntry ref1, DBRefEntry ref2) { + if (ref1.isChromosome()) + { + return -1; + } + if (ref2.isChromosome()) + { + return 1; + } String s1 = ref1.getSource(); String s2 = ref2.getSource(); boolean s1Primary = isPrimarySource(s1); @@ -78,14 +88,14 @@ public class SequenceAnnotationReport { return 1; } - int comp = s1 == null ? -1 - : (s2 == null ? 1 : s1.compareToIgnoreCase(s2)); + int comp = s1 == null ? -1 : (s2 == null ? 1 : s1 + .compareToIgnoreCase(s2)); if (comp == 0) { String a1 = ref1.getAccessionId(); String a2 = ref2.getAccessionId(); - comp = a1 == null ? -1 - : (a2 == null ? 1 : a1.compareToIgnoreCase(a2)); + comp = a1 == null ? -1 : (a2 == null ? 1 : a1 + .compareToIgnoreCase(a2)); } return comp; } @@ -106,9 +116,9 @@ public class SequenceAnnotationReport } }; - public SequenceAnnotationReport(String linkImageURL) + public SequenceAnnotationReport(String linkURL) { - this.linkImageURL = linkImageURL; + this.linkImageURL = linkURL; } /** @@ -120,13 +130,13 @@ public class SequenceAnnotationReport * @param minmax */ public void appendFeatures(final StringBuilder sb, int rpos, - List features, Map minmax) + List features, FeatureRendererModel fr) { if (features != null) { for (SequenceFeature feature : features) { - appendFeature(sb, rpos, minmax, feature); + appendFeature(sb, rpos, fr, feature); } } } @@ -140,7 +150,7 @@ public class SequenceAnnotationReport * @param feature */ void appendFeature(final StringBuilder sb, int rpos, - Map minmax, SequenceFeature feature) + FeatureRendererModel fr, SequenceFeature feature) { if (feature.isContactFeature()) { @@ -153,99 +163,92 @@ public class SequenceAnnotationReport sb.append(feature.getType()).append(" ").append(feature.getBegin()) .append(":").append(feature.getEnd()); } + return; } - else + + if (sb.length() > 6) { - if (sb.length() > 6) + sb.append("
    "); + } + // 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("
    "); + // 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(""); - if (startTag > -1) - { - tmpString = tmpString.substring(startTag + 6); - tmp2up = tmp2up.substring(startTag + 6); - } - int endTag = tmp2up.indexOf(""); - if (endTag > -1) - { - tmpString = tmpString.substring(0, endTag); - tmp2up = tmp2up.substring(0, endTag); - } - endTag = tmp2up.indexOf(""); - 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 * @@ -268,19 +271,20 @@ public class SequenceAnnotationReport { for (List urllink : createLinksFrom(null, urlstring)) { - sb.append("
    " + sb.append("
    " + (urllink.get(0).toLowerCase() - .equals(urllink.get(1).toLowerCase()) - ? urllink.get(0) - : (urllink.get(0) + ":" - + urllink.get(1))) - + "
    "); + .equals(urllink.get(1).toLowerCase()) ? urllink + .get(0) : (urllink.get(0) + ":" + urllink + .get(1))) + "
    "); } } catch (Exception x) { - System.err.println( - "problem when creating links from " + urlstring); + System.err.println("problem when creating links from " + + urlstring); x.printStackTrace(); } } @@ -298,7 +302,7 @@ public class SequenceAnnotationReport */ Collection> createLinksFrom(SequenceI seq, String link) { - Map> urlSets = new LinkedHashMap>(); + Map> urlSets = new LinkedHashMap<>(); UrlLink urlLink = new UrlLink(link); if (!urlLink.isValid()) { @@ -313,10 +317,10 @@ public class SequenceAnnotationReport public void createSequenceAnnotationReport(final StringBuilder tip, SequenceI sequence, boolean showDbRefs, boolean showNpFeats, - Map minmax) + FeatureRendererModel fr) { createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats, - minmax, false); + fr, false); } /** @@ -331,13 +335,13 @@ public class SequenceAnnotationReport * whether to include database references for the sequence * @param showNpFeats * whether to include non-positional sequence features - * @param minmax + * @param fr * @param summary * @return */ int createSequenceAnnotationReport(final StringBuilder sb, SequenceI sequence, boolean showDbRefs, boolean showNpFeats, - Map minmax, boolean summary) + FeatureRendererModel fr, boolean summary) { String tmp; sb.append(""); @@ -354,7 +358,7 @@ public class SequenceAnnotationReport { ds = ds.getDatasetSequence(); } - + if (showDbRefs) { maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary)); @@ -369,7 +373,7 @@ public class SequenceAnnotationReport .getNonPositionalFeatures()) { int sz = -sb.length(); - appendFeature(sb, 0, minmax, sf); + appendFeature(sb, 0, fr, sf); sz += sb.length(); maxWidth = Math.max(maxWidth, sz); } @@ -458,8 +462,7 @@ public class SequenceAnnotationReport } if (moreSources) { - sb.append("
    ").append(source) - .append(COMMA).append(ELLIPSIS); + sb.append("
    ").append(source).append(COMMA).append(ELLIPSIS); } if (ellipsis) { @@ -473,10 +476,10 @@ public class SequenceAnnotationReport public void createTooltipAnnotationReport(final StringBuilder tip, SequenceI sequence, boolean showDbRefs, boolean showNpFeats, - Map minmax) + FeatureRendererModel fr) { - int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs, - showNpFeats, minmax, true); + int maxWidth = createSequenceAnnotationReport(tip, sequence, + showDbRefs, showNpFeats, fr, true); if (maxWidth > 60) { diff --git a/src/jalview/io/gff/Gff3Helper.java b/src/jalview/io/gff/Gff3Helper.java index c7e1d7a..a25a014 100644 --- a/src/jalview/io/gff/Gff3Helper.java +++ b/src/jalview/io/gff/Gff3Helper.java @@ -39,6 +39,8 @@ import java.util.Map; */ public class Gff3Helper extends GffHelperBase { + public static final String ALLELES = "alleles"; + protected static final String TARGET = "Target"; protected static final String ID = "ID"; @@ -399,7 +401,7 @@ public class Gff3Helper extends GffHelperBase /* * Ensembl returns dna variants as 'alleles' */ - desc = StringUtils.listToDelimitedString(attributes.get("alleles"), + desc = StringUtils.listToDelimitedString(attributes.get(ALLELES), ","); } diff --git a/src/jalview/io/gff/SequenceOntologyI.java b/src/jalview/io/gff/SequenceOntologyI.java index c0570e0..307e1d1 100644 --- a/src/jalview/io/gff/SequenceOntologyI.java +++ b/src/jalview/io/gff/SequenceOntologyI.java @@ -42,6 +42,15 @@ public interface SequenceOntologyI // SO:0001060 public static final String SEQUENCE_VARIANT = "sequence_variant"; + // SO:0001819 + public static final String SYNONYMOUS_VARIANT = "synonymous_variant"; + + // SO:0001992 + public static final String NONSYNONYMOUS_VARIANT = "nonsynonymous_variant"; + + // SO:0001587 + public static final String STOP_GAINED = "stop_gained"; + // SO:0000147 public static final String EXON = "exon"; diff --git a/src/jalview/io/gff/SequenceOntologyLite.java b/src/jalview/io/gff/SequenceOntologyLite.java index f989f7b..72e906c 100644 --- a/src/jalview/io/gff/SequenceOntologyLite.java +++ b/src/jalview/io/gff/SequenceOntologyLite.java @@ -44,7 +44,7 @@ public class SequenceOntologyLite implements SequenceOntologyI * initial selection of types of interest when processing Ensembl features * NB unlike the full SequenceOntology we don't traverse indirect * child-parent relationships here so e.g. need to list every sub-type - * of gene (direct or indirect) that is of interest + * (direct or indirect) that is of interest */ // @formatter:off private final String[][] TERMS = new String[][] { @@ -75,16 +75,26 @@ public class SequenceOntologyLite implements SequenceOntologyI // there are many more sub-types of ncRNA... /* - * sequence_variant sub-types: + * sequence_variant sub-types */ { "sequence_variant", "sequence_variant" }, + { "structural_variant", "sequence_variant" }, { "feature_variant", "sequence_variant" }, { "gene_variant", "sequence_variant" }, + { "transcript_variant", "sequence_variant" }, // NB Ensembl uses NMD_transcript_variant as if a 'transcript' // but we model it here correctly as per the SO { "NMD_transcript_variant", "sequence_variant" }, - { "transcript_variant", "sequence_variant" }, - { "structural_variant", "sequence_variant" }, + { "missense_variant", "sequence_variant" }, + { "synonymous_variant", "sequence_variant" }, + { "frameshift_variant", "sequence_variant" }, + { "5_prime_UTR_variant", "sequence_variant" }, + { "3_prime_UTR_variant", "sequence_variant" }, + { "stop_gained", "sequence_variant" }, + { "stop_lost", "sequence_variant" }, + { "inframe_deletion", "sequence_variant" }, + { "inframe_insertion", "sequence_variant" }, + { "splice_region_variant", "sequence_variant" }, /* * no sub-types of exon or CDS yet seen in Ensembl @@ -121,8 +131,8 @@ public class SequenceOntologyLite implements SequenceOntologyI public SequenceOntologyLite() { - termsFound = new ArrayList(); - termsNotFound = new ArrayList(); + termsFound = new ArrayList<>(); + termsNotFound = new ArrayList<>(); loadStaticData(); } @@ -131,13 +141,13 @@ public class SequenceOntologyLite implements SequenceOntologyI */ private void loadStaticData() { - parents = new HashMap>(); + parents = new HashMap<>(); for (String[] pair : TERMS) { List p = parents.get(pair[0]); if (p == null) { - p = new ArrayList(); + p = new ArrayList<>(); parents.put(pair[0], p); } p.add(pair[1]); diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java new file mode 100644 index 0000000..de2f18a --- /dev/null +++ b/src/jalview/io/vcf/VCFLoader.java @@ -0,0 +1,1474 @@ +package jalview.io.vcf; + +import jalview.analysis.AlignmentUtils; +import jalview.analysis.Dna; +import jalview.api.AlignViewControllerGuiI; +import jalview.bin.Cache; +import jalview.datamodel.DBRefEntry; +import jalview.datamodel.GeneLociI; +import jalview.datamodel.Mapping; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureAttributeType; +import jalview.datamodel.features.FeatureSource; +import jalview.datamodel.features.FeatureSources; +import jalview.ext.ensembl.EnsemblMap; +import jalview.ext.htsjdk.HtsContigDb; +import jalview.ext.htsjdk.VCFReader; +import jalview.io.gff.Gff3Helper; +import jalview.io.gff.SequenceOntologyI; +import jalview.util.MapList; +import jalview.util.MappingUtils; +import jalview.util.MessageManager; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import htsjdk.samtools.SAMException; +import htsjdk.samtools.SAMSequenceDictionary; +import htsjdk.samtools.SAMSequenceRecord; +import htsjdk.samtools.util.CloseableIterator; +import htsjdk.variant.variantcontext.Allele; +import htsjdk.variant.variantcontext.VariantContext; +import htsjdk.variant.vcf.VCFHeader; +import htsjdk.variant.vcf.VCFHeaderLine; +import htsjdk.variant.vcf.VCFHeaderLineCount; +import htsjdk.variant.vcf.VCFHeaderLineType; +import htsjdk.variant.vcf.VCFInfoHeaderLine; + +/** + * A class to read VCF data (using the htsjdk) and add variants as sequence + * features on dna and any related protein product sequences + * + * @author gmcarstairs + */ +public class VCFLoader +{ + /** + * A class to model the mapping from sequence to VCF coordinates. Cases include + *
      + *
    • a direct 1:1 mapping where the sequence is one of the VCF contigs
    • + *
    • a mapping of sequence to chromosomal coordinates, where sequence and VCF + * use the same reference assembly
    • + *
    • a modified mapping of sequence to chromosomal coordinates, where sequence + * and VCF use different reference assembles
    • + *
    + */ + class VCFMap + { + final String chromosome; + + final MapList map; + + VCFMap(String chr, MapList m) + { + chromosome = chr; + map = m; + } + + @Override + public String toString() + { + return chromosome + ":" + map.toString(); + } + } + + /* + * Lookup keys, and default values, for Preference entries that describe + * patterns for VCF and VEP fields to capture + */ + private static final String VEP_FIELDS_PREF = "VEP_FIELDS"; + + private static final String VCF_FIELDS_PREF = "VCF_FIELDS"; + + private static final String DEFAULT_VCF_FIELDS = ".*"; + + private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG"; + + /* + * keys to fields of VEP CSQ consequence data + * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html + */ + private static final String CSQ_CONSEQUENCE_KEY = "Consequence"; + private static final String CSQ_ALLELE_KEY = "Allele"; + private static final String CSQ_ALLELE_NUM_KEY = "ALLELE_NUM"; // 0 (ref), 1... + private static final String CSQ_FEATURE_KEY = "Feature"; // Ensembl stable id + + /* + * default VCF INFO key for VEP consequence data + * NB this can be overridden running VEP with --vcf_info_field + * - we don't handle this case (require identifier to be CSQ) + */ + private static final String CSQ_FIELD = "CSQ"; + + /* + * separator for fields in consequence data is '|' + */ + private static final String PIPE_REGEX = "\\|"; + + /* + * key for Allele Frequency output by VEP + * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html + */ + private static final String ALLELE_FREQUENCY_KEY = "AF"; + + /* + * delimiter that separates multiple consequence data blocks + */ + private static final String COMMA = ","; + + /* + * the feature group assigned to a VCF variant in Jalview + */ + private static final String FEATURE_GROUP_VCF = "VCF"; + + /* + * internal delimiter used to build keys for assemblyMappings + * + */ + private static final String EXCL = "!"; + + /* + * the VCF file we are processing + */ + protected String vcfFilePath; + + /* + * mappings between VCF and sequence reference assembly regions, as + * key = "species!chromosome!fromAssembly!toAssembly + * value = Map{fromRange, toRange} + */ + private Map> assemblyMappings; + + private VCFReader reader; + + /* + * holds details of the VCF header lines (metadata) + */ + private VCFHeader header; + + /* + * a Dictionary of contigs (if present) referenced in the VCF file + */ + private SAMSequenceDictionary dictionary; + + /* + * the position (0...) of field in each block of + * CSQ (consequence) data (if declared in the VCF INFO header for CSQ) + * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html + */ + private int csqConsequenceFieldIndex = -1; + private int csqAlleleFieldIndex = -1; + private int csqAlleleNumberFieldIndex = -1; + private int csqFeatureFieldIndex = -1; + + // todo the same fields for SnpEff ANN data if wanted + // see http://snpeff.sourceforge.net/SnpEff_manual.html#input + + /* + * a unique identifier under which to save metadata about feature + * attributes (selected INFO field data) + */ + private String sourceId; + + /* + * The INFO IDs of data that is both present in the VCF file, and + * also matched by any filters for data of interest + */ + List 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 vepFieldsOfInterest; + + /** + * Constructor given a VCF file + * + * @param alignment + */ + public VCFLoader(String vcfFile) + { + try + { + initialise(vcfFile); + } catch (IOException e) + { + System.err.println("Error opening VCF file: " + e.getMessage()); + } + + // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange} + assemblyMappings = new HashMap<>(); + } + + /** + * Starts a new thread to query and load VCF variant data on to the given + * sequences + *

    + * This method is not thread safe - concurrent threads should use separate + * instances of this class. + * + * @param seqs + * @param gui + */ + public void loadVCF(SequenceI[] seqs, final AlignViewControllerGuiI gui) + { + if (gui != null) + { + gui.setStatus(MessageManager.getString("label.searching_vcf")); + } + + new Thread() + { + @Override + public void run() + { + VCFLoader.this.doLoad(seqs, gui); + } + }.start(); + } + + /** + * Reads the specified contig sequence and adds its VCF variants to it + * + * @param contig + * the id of a single sequence (contig) to load + * @return + */ + public SequenceI loadVCFContig(String contig) + { + String ref = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY) + .getValue(); + if (ref.startsWith("file://")) + { + ref = ref.substring(7); + } + + SequenceI seq = null; + File dbFile = new File(ref); + + if (dbFile.exists()) + { + HtsContigDb db = new HtsContigDb("", dbFile); + seq = db.getSequenceProxy(contig); + loadSequenceVCF(seq, ref); + db.close(); + } + else + { + System.err.println("VCF reference not found: " + ref); + } + + return seq; + } + + /** + * Loads VCF on to one or more sequences + * + * @param seqs + * @param gui + * optional callback handler for messages + */ + protected void doLoad(SequenceI[] seqs, AlignViewControllerGuiI gui) + { + try + { + VCFHeaderLine ref = header + .getOtherHeaderLine(VCFHeader.REFERENCE_KEY); + String vcfAssembly = ref.getValue(); + + int varCount = 0; + int seqCount = 0; + + /* + * query for VCF overlapping each sequence in turn + */ + for (SequenceI seq : seqs) + { + int added = loadSequenceVCF(seq, vcfAssembly); + if (added > 0) + { + seqCount++; + varCount += added; + transferAddedFeatures(seq); + } + } + if (gui != null) + { + String msg = MessageManager.formatMessage("label.added_vcf", + varCount, seqCount); + gui.setStatus(msg); + if (gui.getFeatureSettingsUI() != null) + { + gui.getFeatureSettingsUI().discoverAllFeatureData(); + } + } + } catch (Throwable e) + { + System.err.println("Error processing VCF: " + e.getMessage()); + e.printStackTrace(); + if (gui != null) + { + gui.setStatus("Error occurred - see console for details"); + } + } finally + { + if (reader != null) + { + try + { + reader.close(); + } catch (IOException e) + { + // ignore + } + } + header = null; + dictionary = null; + } + } + + /** + * Opens the VCF file and parses header data + * + * @param filePath + * @throws IOException + */ + private void initialise(String filePath) throws IOException + { + vcfFilePath = filePath; + + reader = new VCFReader(filePath); + + header = reader.getFileHeader(); + + try + { + dictionary = header.getSequenceDictionary(); + } catch (SAMException e) + { + // ignore - thrown if any contig line lacks length info + } + + sourceId = filePath; + + saveMetadata(sourceId); + + /* + * get offset of CSQ ALLELE_NUM and Feature if declared + */ + parseCsqHeader(); + } + + /** + * Reads metadata (such as INFO field descriptions and datatypes) and saves + * them for future reference + * + * @param theSourceId + */ + void saveMetadata(String theSourceId) + { + List 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 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. + *

    + * CSQ fields are declared in the CSQ INFO Description e.g. + *

    + * Description="Consequence ...from ... VEP. Format: Allele|Consequence|... + */ + protected void parseCsqHeader() + { + List vepFieldFilters = getFieldMatchers(VEP_FIELDS_PREF, + DEFAULT_VEP_FIELDS); + vepFieldsOfInterest = new HashMap<>(); + + VCFInfoHeaderLine csqInfo = header.getInfoHeaderLine(CSQ_FIELD); + if (csqInfo == null) + { + return; + } + + /* + * parse out the pipe-separated list of CSQ fields; we assume here that + * these form the last part of the description, and contain no spaces + */ + String desc = csqInfo.getDescription(); + int spacePos = desc.lastIndexOf(" "); + desc = desc.substring(spacePos + 1); + + if (desc != null) + { + String[] format = desc.split(PIPE_REGEX); + int index = 0; + for (String field : format) + { + if (CSQ_CONSEQUENCE_KEY.equals(field)) + { + csqConsequenceFieldIndex = index; + } + if (CSQ_ALLELE_NUM_KEY.equals(field)) + { + csqAlleleNumberFieldIndex = index; + } + if (CSQ_ALLELE_KEY.equals(field)) + { + csqAlleleFieldIndex = index; + } + if (CSQ_FEATURE_KEY.equals(field)) + { + csqFeatureFieldIndex = index; + } + + if (isFieldWanted(field, vepFieldFilters)) + { + vepFieldsOfInterest.put(index, field); + } + + index++; + } + } + } + + /** + * Reads the Preference value for the given key, with default specified if no + * preference set. The value is interpreted as a comma-separated list of + * regular expressions, and converted into a list of compiled patterns ready + * for matching. Patterns are forced to upper-case for non-case-sensitive + * matching. + *

    + * 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 getFieldMatchers(String key, String def) + { + String pref = Cache.getDefault(key, def); + List 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 features = seq.getFeatures() + .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT); + for (SequenceFeature sf : features) + { + if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup())) + { + transferFeature(sf, mapTo, map); + } + } + } + } + } + + /** + * Tries to add overlapping variants read from a VCF file to the given sequence, + * and returns the number of variant features added + * + * @param seq + * @param vcfAssembly + * @return + */ + protected int loadSequenceVCF(SequenceI seq, String vcfAssembly) + { + VCFMap vcfMap = getVcfMap(seq, vcfAssembly); + if (vcfMap == null) + { + return 0; + } + + /* + * work with the dataset sequence here + */ + SequenceI dss = seq.getDatasetSequence(); + if (dss == null) + { + dss = seq; + } + return addVcfVariants(dss, vcfMap); + } + + /** + * Answers a map from sequence coordinates to VCF chromosome ranges + * + * @param seq + * @param vcfAssembly + * @return + */ + private VCFMap getVcfMap(SequenceI seq, String vcfAssembly) + { + /* + * simplest case: sequence has id and length matching a VCF contig + */ + VCFMap vcfMap = null; + if (dictionary != null) + { + vcfMap = getContigMap(seq); + } + if (vcfMap != null) + { + return vcfMap; + } + + /* + * otherwise, map to VCF from chromosomal coordinates + * of the sequence (if known) + */ + GeneLociI seqCoords = seq.getGeneLoci(); + if (seqCoords == null) + { + Cache.log.warn(String.format( + "Can't query VCF for %s as chromosome coordinates not known", + seq.getName())); + return null; + } + + String species = seqCoords.getSpeciesId(); + String chromosome = seqCoords.getChromosomeId(); + String seqRef = seqCoords.getAssemblyId(); + MapList map = seqCoords.getMap(); + + if (!vcfSpeciesMatchesSequence(vcfAssembly, species)) + { + return null; + } + + if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef)) + { + return new VCFMap(chromosome, map); + } + + if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl + || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD + { + return null; + } + + /* + * map chromosomal coordinates from sequence to VCF if the VCF + * data has a different reference assembly to the sequence + */ + // TODO generalise for cases other than GRCh38 -> GRCh37 ! + // - or get the user to choose in a dialog + + List toVcfRanges = new ArrayList<>(); + List fromSequenceRanges = new ArrayList<>(); + String toRef = "GRCh37"; + + for (int[] range : map.getToRanges()) + { + int[] fromRange = map.locateInFrom(range[0], range[1]); + if (fromRange == null) + { + // corrupted map?!? + continue; + } + + int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef, + toRef); + if (newRange == null) + { + Cache.log.error( + String.format("Failed to map %s:%s:%s:%d:%d to %s", species, + chromosome, seqRef, range[0], range[1], toRef)); + continue; + } + else + { + toVcfRanges.add(newRange); + fromSequenceRanges.add(fromRange); + } + } + + return new VCFMap(chromosome, + new MapList(fromSequenceRanges, toVcfRanges, 1, 1)); + } + + /** + * If the sequence id matches a contig declared in the VCF file, and the + * sequence length matches the contig length, then returns a 1:1 map of the + * sequence to the contig, else returns null + * + * @param seq + * @return + */ + private VCFMap getContigMap(SequenceI seq) + { + String id = seq.getName(); + SAMSequenceRecord contig = dictionary.getSequence(id); + if (contig != null) + { + int len = seq.getLength(); + if (len == contig.getSequenceLength()) + { + MapList map = new MapList(new int[] { 1, len }, + new int[] + { 1, len }, 1, 1); + return new VCFMap(id, map); + } + } + return null; + } + + /** + * Answers true if we determine that the VCF data uses the same reference + * assembly as the sequence, else false + * + * @param vcfAssembly + * @param seqRef + * @return + */ + private boolean vcfAssemblyMatchesSequence(String vcfAssembly, + String seqRef) + { + // TODO improve on this stub, which handles gnomAD and + // hopes for the best for other cases + + if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl + && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD + { + return false; + } + return true; + } + + /** + * Answers true if the species inferred from the VCF reference identifier + * matches that for the sequence + * + * @param vcfAssembly + * @param speciesId + * @return + */ + boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId) + { + // PROBLEM 1 + // there are many aliases for species - how to equate one with another? + // PROBLEM 2 + // VCF ##reference header is an unstructured URI - how to extract species? + // perhaps check if ref includes any (Ensembl) alias of speciesId?? + // TODO ask the user to confirm this?? + + if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example + && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id + { + return true; + } + + if (vcfAssembly.contains("c_elegans") // VEP VCF response example + && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl + { + return true; + } + + // this is not a sustainable solution... + + return false; + } + + /** + * Queries the VCF reader for any variants that overlap the mapped chromosome + * ranges of the sequence, and adds as variant features. Returns the number of + * overlapping variants found. + * + * @param seq + * @param map + * mapping from sequence to VCF coordinates + * @return + */ + protected int addVcfVariants(SequenceI seq, VCFMap map) + { + boolean forwardStrand = map.map.isToForwardStrand(); + + /* + * query the VCF for overlaps of each contiguous chromosomal region + */ + int count = 0; + + for (int[] range : map.map.getToRanges()) + { + int vcfStart = Math.min(range[0], range[1]); + int vcfEnd = Math.max(range[0], range[1]); + CloseableIterator 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) att).get(alleleIndex); + } + + return null; + } + + /** + * Adds one variant feature for each allele in the VCF variant record, and + * returns the number of features added. + * + * @param seq + * @param variant + * @param featureStart + * @param featureEnd + * @param forwardStrand + * @return + */ + protected int addAlleleFeatures(SequenceI seq, VariantContext variant, + int featureStart, int featureEnd, boolean forwardStrand) + { + int added = 0; + + /* + * Javadoc says getAlternateAlleles() imposes no order on the list returned + * so we proceed defensively to get them in strict order + */ + int altAlleleCount = variant.getAlternateAlleles().size(); + for (int i = 0; i < altAlleleCount; i++) + { + added += addAlleleFeature(seq, variant, i, featureStart, featureEnd, + forwardStrand); + } + return added; + } + + /** + * Inspects one allele and attempts to add a variant feature for it to the + * sequence. The additional data associated with this allele is extracted to + * store in the feature's key-value map. Answers the number of features added (0 + * or 1). + * + * @param seq + * @param variant + * @param altAlleleIndex + * (0, 1..) + * @param featureStart + * @param featureEnd + * @param forwardStrand + * @return + */ + protected int addAlleleFeature(SequenceI seq, VariantContext variant, + int altAlleleIndex, int featureStart, int featureEnd, + boolean forwardStrand) + { + String reference = variant.getReference().getBaseString(); + Allele alt = variant.getAlternateAllele(altAlleleIndex); + String allele = alt.getBaseString(); + + /* + * insertion after a genomic base, if on reverse strand, has to be + * converted to insertion of complement after the preceding position + */ + int referenceLength = reference.length(); + if (!forwardStrand && allele.length() > referenceLength + && allele.startsWith(reference)) + { + featureStart -= referenceLength; + featureEnd = featureStart; + char insertAfter = seq.getCharAt(featureStart - seq.getStart()); + reference = Dna.reverseComplement(String.valueOf(insertAfter)); + allele = allele.substring(referenceLength) + reference; + } + + /* + * build the ref,alt allele description e.g. "G,A", using the base + * complement if the sequence is on the reverse strand + */ + StringBuilder sb = new StringBuilder(); + sb.append(forwardStrand ? reference : Dna.reverseComplement(reference)); + sb.append(COMMA); + sb.append(forwardStrand ? allele : Dna.reverseComplement(allele)); + String alleles = sb.toString(); // e.g. G,A + + /* + * pick out the consequence data (if any) that is for the current allele + * and feature (transcript) that matches the current sequence + */ + String consequence = getConsequenceForAlleleAndFeature(variant, CSQ_FIELD, + altAlleleIndex, csqAlleleFieldIndex, + csqAlleleNumberFieldIndex, seq.getName().toLowerCase(), + csqFeatureFieldIndex); + + /* + * pick out the ontology term for the consequence type + */ + String type = SequenceOntologyI.SEQUENCE_VARIANT; + if (consequence != null) + { + type = getOntologyTerm(consequence); + } + + float score = getAlleleFrequency(variant, altAlleleIndex); + + SequenceFeature sf = new SequenceFeature(type, alleles, featureStart, + featureEnd, score, FEATURE_GROUP_VCF); + sf.setSource(sourceId); + + sf.setValue(Gff3Helper.ALLELES, alleles); + + addAlleleProperties(variant, sf, altAlleleIndex, consequence); + + seq.addSequenceFeature(sf); + + return 1; + } + + /** + * Determines the Sequence Ontology term to use for the variant feature type in + * Jalview. The default is 'sequence_variant', but a more specific term is used + * if: + *

      + *
    • VEP (or SnpEff) Consequence annotation is included in the VCF
    • + *
    • sequence id can be matched to VEP Feature (or SnpEff Feature_ID)
    • + *
    + * + * @param consequence + * @return + * @see http://www.sequenceontology.org/browser/current_svn/term/SO:0001060 + */ + String getOntologyTerm(String consequence) + { + String type = SequenceOntologyI.SEQUENCE_VARIANT; + + /* + * could we associate Consequence data with this allele and feature (transcript)? + * if so, prefer the consequence term from that data + */ + if (csqAlleleFieldIndex == -1) // && snpEffAlleleFieldIndex == -1 + { + /* + * no Consequence data so we can't refine the ontology term + */ + return type; + } + + if (consequence != null) + { + String[] csqFields = consequence.split(PIPE_REGEX); + if (csqFields.length > csqConsequenceFieldIndex) + { + type = csqFields[csqConsequenceFieldIndex]; + } + } + else + { + // todo the same for SnpEff consequence data matching if wanted + } + + /* + * if of the form (e.g.) missense_variant&splice_region_variant, + * just take the first ('most severe') consequence + */ + if (type != null) + { + int pos = type.indexOf('&'); + if (pos > 0) + { + type = type.substring(0, pos); + } + } + return type; + } + + /** + * Returns matched consequence data if it can be found, else null. + *
      + *
    • inspects the VCF data for key 'vcfInfoId'
    • + *
    • splits this on comma (to distinct consequences)
    • + *
    • returns the first consequence (if any) where
    • + *
        + *
      • the allele matches the altAlleleIndex'th allele of variant
      • + *
      • the feature matches the sequence name (e.g. transcript id)
      • + *
      + *
    + * If matched, the consequence is returned (as pipe-delimited fields). + * + * @param variant + * @param vcfInfoId + * @param altAlleleIndex + * @param alleleFieldIndex + * @param alleleNumberFieldIndex + * @param seqName + * @param featureFieldIndex + * @return + */ + private String getConsequenceForAlleleAndFeature(VariantContext variant, + String vcfInfoId, int altAlleleIndex, int alleleFieldIndex, + int alleleNumberFieldIndex, + String seqName, int featureFieldIndex) + { + if (alleleFieldIndex == -1 || featureFieldIndex == -1) + { + return null; + } + Object value = variant.getAttribute(vcfInfoId); + + if (value == null || !(value instanceof List)) + { + return null; + } + + /* + * inspect each consequence in turn (comma-separated blocks + * extracted by htsjdk) + */ + List consequences = (List) value; + + for (String consequence : consequences) + { + String[] csqFields = consequence.split(PIPE_REGEX); + if (csqFields.length > featureFieldIndex) + { + String featureIdentifier = csqFields[featureFieldIndex]; + if (featureIdentifier.length() > 4 + && seqName.indexOf(featureIdentifier.toLowerCase()) > -1) + { + /* + * feature (transcript) matched - now check for allele match + */ + if (matchAllele(variant, altAlleleIndex, csqFields, + alleleFieldIndex, alleleNumberFieldIndex)) + { + return consequence; + } + } + } + } + return null; + } + + private boolean matchAllele(VariantContext variant, int altAlleleIndex, + String[] csqFields, int alleleFieldIndex, + int alleleNumberFieldIndex) + { + /* + * if ALLELE_NUM is present, it must match altAlleleIndex + * NB first alternate allele is 1 for ALLELE_NUM, 0 for altAlleleIndex + */ + if (alleleNumberFieldIndex > -1) + { + if (csqFields.length <= alleleNumberFieldIndex) + { + return false; + } + String alleleNum = csqFields[alleleNumberFieldIndex]; + return String.valueOf(altAlleleIndex + 1).equals(alleleNum); + } + + /* + * else consequence allele must match variant allele + */ + if (alleleFieldIndex > -1 && csqFields.length > alleleFieldIndex) + { + String csqAllele = csqFields[alleleFieldIndex]; + String vcfAllele = variant.getAlternateAllele(altAlleleIndex) + .getBaseString(); + return csqAllele.equals(vcfAllele); + } + return false; + } + + /** + * Add any allele-specific VCF key-value data to the sequence feature + * + * @param variant + * @param sf + * @param altAlelleIndex + * (0, 1..) + * @param consequence + * if not null, the consequence specific to this sequence (transcript + * feature) and allele + */ + protected void addAlleleProperties(VariantContext variant, + SequenceFeature sf, final int altAlelleIndex, String consequence) + { + Map atts = variant.getAttributes(); + + for (Entry att : atts.entrySet()) + { + String key = att.getKey(); + + /* + * extract Consequence data (if present) that we are able to + * associated with the allele for this variant feature + */ + if (CSQ_FIELD.equals(key)) + { + addConsequences(variant, sf, consequence); + continue; + } + + /* + * filter out fields we don't want to capture + */ + if (!vcfFieldsOfInterest.contains(key)) + { + continue; + } + + /* + * filter out fields we don't want to capture + */ + if (!vcfFieldsOfInterest.contains(key)) + { + continue; + } + + /* + * we extract values for other data which are allele-specific; + * these may be per alternate allele (INFO[key].Number = 'A') + * or per allele including reference (INFO[key].Number = 'R') + */ + VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(key); + if (infoHeader == null) + { + /* + * can't be sure what data belongs to this allele, so + * play safe and don't take any + */ + continue; + } + + VCFHeaderLineCount number = infoHeader.getCountType(); + int index = altAlelleIndex; + if (number == VCFHeaderLineCount.R) + { + /* + * one value per allele including reference, so bump index + * e.g. the 3rd value is for the 2nd alternate allele + */ + index++; + } + else if (number != VCFHeaderLineCount.A) + { + /* + * don't save other values as not allele-related + */ + continue; + } + + /* + * take the index'th value + */ + String value = getAttributeValue(variant, key, index); + if (value != null) + { + sf.setValue(key, value); + } + } + } + + /** + * Inspects CSQ data blocks (consequences) and adds attributes on the sequence + * feature. + *

    + * If myConsequence is not null, then this is the specific + * consequence data (pipe-delimited fields) that is for the current allele and + * transcript (sequence) being processed) + * + * @param variant + * @param sf + * @param myConsequence + */ + protected void addConsequences(VariantContext variant, SequenceFeature sf, + String myConsequence) + { + Object value = variant.getAttribute(CSQ_FIELD); + + if (value == null || !(value instanceof List)) + { + return; + } + + List consequences = (List) value; + + /* + * inspect CSQ consequences; restrict to the consequence + * associated with the current transcript (Feature) + */ + Map csqValues = new HashMap<>(); + + for (String consequence : consequences) + { + if (myConsequence == null || myConsequence.equals(consequence)) + { + String[] csqFields = consequence.split(PIPE_REGEX); + + /* + * inspect individual fields of this consequence, copying non-null + * values which are 'fields of interest' + */ + int i = 0; + for (String field : csqFields) + { + if (field != null && field.length() > 0) + { + String id = vepFieldsOfInterest.get(i); + if (id != null) + { + csqValues.put(id, field); + } + } + i++; + } + } + } + + if (!csqValues.isEmpty()) + { + sf.setValue(CSQ_FIELD, csqValues); + } + } + + /** + * A convenience method to complement a dna base and return the string value + * of its complement + * + * @param reference + * @return + */ + protected String complement(byte[] reference) + { + return String.valueOf(Dna.getComplement((char) reference[0])); + } + + /** + * Determines the location of the query range (chromosome positions) in a + * different reference assembly. + *

    + * 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. + *

    + * 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()); + } + + 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. + *

    + * If in future these assumptions prove invalid (e.g. for bacterial dna?!), + * simply remove this method or let it always return null. + *

    + * 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 mappedRanges = assemblyMappings.get(key); + for (Entry 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; + } +} diff --git a/src/jalview/jbgui/GAlignFrame.java b/src/jalview/jbgui/GAlignFrame.java index 86d0c85..1cf482d 100755 --- a/src/jalview/jbgui/GAlignFrame.java +++ b/src/jalview/jbgui/GAlignFrame.java @@ -147,6 +147,8 @@ public class GAlignFrame extends JInternalFrame protected JMenuItem runGroovy = new JMenuItem(); + protected JMenuItem loadVcf; + protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem(); protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem(); @@ -1308,6 +1310,16 @@ public class GAlignFrame extends JInternalFrame associatedData_actionPerformed(e); } }); + loadVcf = new JMenuItem(MessageManager.getString("label.load_vcf_file")); + loadVcf.setToolTipText(MessageManager.getString("label.load_vcf")); + loadVcf.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + loadVcf_actionPerformed(); + } + }); autoCalculate.setText( MessageManager.getString("label.autocalculate_consensus")); autoCalculate.setState( @@ -1710,6 +1722,7 @@ public class GAlignFrame extends JInternalFrame fileMenu.add(exportAnnotations); fileMenu.add(loadTreeMenuItem); fileMenu.add(associatedData); + fileMenu.add(loadVcf); fileMenu.addSeparator(); fileMenu.add(closeMenuItem); @@ -1855,6 +1868,10 @@ public class GAlignFrame extends JInternalFrame // selectMenu.add(listenToViewSelections); } + protected void loadVcf_actionPerformed() + { + } + /** * Constructs the entries on the Colour menu (but does not add them to the * menu). diff --git a/src/jalview/jbgui/GCutAndPasteHtmlTransfer.java b/src/jalview/jbgui/GCutAndPasteHtmlTransfer.java index abc0b3d..a6e0ace 100644 --- a/src/jalview/jbgui/GCutAndPasteHtmlTransfer.java +++ b/src/jalview/jbgui/GCutAndPasteHtmlTransfer.java @@ -39,6 +39,8 @@ import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; +import javax.swing.text.EditorKit; +import javax.swing.text.html.HTMLEditorKit; /** * DOCUMENT ME! @@ -85,6 +87,7 @@ public class GCutAndPasteHtmlTransfer extends JInternalFrame { try { + textarea.setEditorKit(new HTMLEditorKit()); setJMenuBar(editMenubar); jbInit(); } catch (Exception e) @@ -272,4 +275,20 @@ public class GCutAndPasteHtmlTransfer extends JInternalFrame { } + + /** + * Adds the given stylesheet rule to the Html editor. However note that CSS + * support is limited. + * + * @param rule + * @see javax.swing.text.html.CSS + */ + public void addStylesheetRule(String rule) + { + EditorKit editorKit = textarea.getEditorKit(); + if (editorKit != null) + { + ((HTMLEditorKit) editorKit).getStyleSheet().addRule(rule); + } + } } diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 1f47da3..795cd36 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -304,14 +304,19 @@ public class FeatureRenderer extends FeatureRendererModel List 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; } diff --git a/src/jalview/schemabinding/version2/.castor.cdr b/src/jalview/schemabinding/version2/.castor.cdr index 0a01103..e1100a8 100644 --- a/src/jalview/schemabinding/version2/.castor.cdr +++ b/src/jalview/schemabinding/version2/.castor.cdr @@ -1,4 +1,4 @@ -#Mon Jun 20 15:44:52 BST 2016 +#Thu Dec 14 09:10:14 GMT 2017 jalview.schemabinding.version2.ThresholdLine=jalview.schemabinding.version2.descriptors.ThresholdLineDescriptor jalview.schemabinding.version2.SequenceSetProperties=jalview.schemabinding.version2.descriptors.SequenceSetPropertiesDescriptor jalview.schemabinding.version2.StructureState=jalview.schemabinding.version2.descriptors.StructureStateDescriptor @@ -10,7 +10,9 @@ jalview.schemabinding.version2.OtherData=jalview.schemabinding.version2.descript jalview.schemabinding.version2.Setting=jalview.schemabinding.version2.descriptors.SettingDescriptor jalview.schemabinding.version2.AlcodonFrame=jalview.schemabinding.version2.descriptors.AlcodonFrameDescriptor jalview.schemabinding.version2.AnnotationElement=jalview.schemabinding.version2.descriptors.AnnotationElementDescriptor +jalview.schemabinding.version2.FeatureMatcherSet=jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor jalview.schemabinding.version2.SecondaryStructure=jalview.schemabinding.version2.descriptors.SecondaryStructureDescriptor +jalview.schemabinding.version2.MatchCondition=jalview.schemabinding.version2.descriptors.MatchConditionDescriptor jalview.schemabinding.version2.SequenceSet=jalview.schemabinding.version2.descriptors.SequenceSetDescriptor jalview.schemabinding.version2.Viewport=jalview.schemabinding.version2.descriptors.ViewportDescriptor jalview.schemabinding.version2.RnaViewer=jalview.schemabinding.version2.descriptors.RnaViewerDescriptor @@ -20,31 +22,32 @@ jalview.schemabinding.version2.UserColourScheme=jalview.schemabinding.version2.d jalview.schemabinding.version2.DBRef=jalview.schemabinding.version2.descriptors.DBRefDescriptor jalview.schemabinding.version2.AlcodMap=jalview.schemabinding.version2.descriptors.AlcodMapDescriptor jalview.schemabinding.version2.Annotation=jalview.schemabinding.version2.descriptors.AnnotationDescriptor -jalview.schemabinding.version2.Wsparameters=jalview.schemabinding.version2.descriptors.WsparametersDescriptor jalview.schemabinding.version2.JSeq=jalview.schemabinding.version2.descriptors.JSeqDescriptor +jalview.schemabinding.version2.MatcherSet=jalview.schemabinding.version2.descriptors.MatcherSetDescriptor jalview.schemabinding.version2.Sequence=jalview.schemabinding.version2.descriptors.SequenceDescriptor jalview.schemabinding.version2.WebServiceParameterSet=jalview.schemabinding.version2.descriptors.WebServiceParameterSetDescriptor jalview.schemabinding.version2.Alcodon=jalview.schemabinding.version2.descriptors.AlcodonDescriptor +jalview.schemabinding.version2.Filter=jalview.schemabinding.version2.descriptors.FilterDescriptor jalview.schemabinding.version2.AnnotationColours=jalview.schemabinding.version2.descriptors.AnnotationColoursDescriptor jalview.schemabinding.version2.Pdbids=jalview.schemabinding.version2.descriptors.PdbidsDescriptor jalview.schemabinding.version2.AnnotationColourScheme=jalview.schemabinding.version2.descriptors.AnnotationColourSchemeDescriptor jalview.schemabinding.version2.Mapping=jalview.schemabinding.version2.descriptors.MappingDescriptor -jalview.schemabinding.version2.MappingChoice=jalview.schemabinding.version2.descriptors.MappingChoiceDescriptor +jalview.schemabinding.version2.CompoundMatcher=jalview.schemabinding.version2.descriptors.CompoundMatcherDescriptor +jalview.schemabinding.version2.JalviewModelSequence=jalview.schemabinding.version2.descriptors.JalviewModelSequenceDescriptor jalview.schemabinding.version2.Group=jalview.schemabinding.version2.descriptors.GroupDescriptor +jalview.schemabinding.version2.MappingChoice=jalview.schemabinding.version2.descriptors.MappingChoiceDescriptor jalview.schemabinding.version2.Feature=jalview.schemabinding.version2.descriptors.FeatureDescriptor -jalview.schemabinding.version2.JalviewModelSequence=jalview.schemabinding.version2.descriptors.JalviewModelSequenceDescriptor jalview.schemabinding.version2.UserColours=jalview.schemabinding.version2.descriptors.UserColoursDescriptor jalview.schemabinding.version2.Colour=jalview.schemabinding.version2.descriptors.ColourDescriptor -jalview.schemabinding.version2.MapListFrom=jalview.schemabinding.version2.descriptors.MapListFromDescriptor jalview.schemabinding.version2.PdbentryItem=jalview.schemabinding.version2.descriptors.PdbentryItemDescriptor -jalview.schemabinding.version2.JGroup=jalview.schemabinding.version2.descriptors.JGroupDescriptor +jalview.schemabinding.version2.MapListFrom=jalview.schemabinding.version2.descriptors.MapListFromDescriptor jalview.schemabinding.version2.FeatureSettings=jalview.schemabinding.version2.descriptors.FeatureSettingsDescriptor -jalview.schemabinding.version2.VamsasModel=jalview.schemabinding.version2.descriptors.VamsasModelDescriptor -jalview.schemabinding.version2.JalviewUserColours=jalview.schemabinding.version2.descriptors.JalviewUserColoursDescriptor +jalview.schemabinding.version2.JGroup=jalview.schemabinding.version2.descriptors.JGroupDescriptor jalview.schemabinding.version2.MapListTo=jalview.schemabinding.version2.descriptors.MapListToDescriptor +jalview.schemabinding.version2.JalviewUserColours=jalview.schemabinding.version2.descriptors.JalviewUserColoursDescriptor +jalview.schemabinding.version2.VamsasModel=jalview.schemabinding.version2.descriptors.VamsasModelDescriptor jalview.schemabinding.version2.Pdbentry=jalview.schemabinding.version2.descriptors.PdbentryDescriptor jalview.schemabinding.version2.HiddenColumns=jalview.schemabinding.version2.descriptors.HiddenColumnsDescriptor jalview.schemabinding.version2.Features=jalview.schemabinding.version2.descriptors.FeaturesDescriptor -jalview.schemabinding.version2.DseqFor=jalview.schemabinding.version2.descriptors.DseqForDescriptor jalview.schemabinding.version2.VAMSAS=jalview.schemabinding.version2.descriptors.VAMSASDescriptor -jalview.schemabinding.version2.MappingChoiceItem=jalview.schemabinding.version2.descriptors.MappingChoiceItemDescriptor +jalview.schemabinding.version2.FeatureMatcher=jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor diff --git a/src/jalview/schemabinding/version2/Colour.java b/src/jalview/schemabinding/version2/Colour.java index 9d5a916..d1c7297 100644 --- a/src/jalview/schemabinding/version2/Colour.java +++ b/src/jalview/schemabinding/version2/Colour.java @@ -27,7 +27,8 @@ public class Colour implements java.io.Serializable // --------------------------/ /** - * Field _name. + * Single letter residue code for an alignment colour scheme, or feature type + * for a feature colour scheme */ private java.lang.String _name; @@ -42,9 +43,15 @@ public class Colour implements java.io.Serializable private java.lang.String _minRGB; /** - * loosely specified enumeration: NONE,ABOVE, or BELOW + * Field _noValueColour. */ - private java.lang.String _threshType; + private jalview.schemabinding.version2.types.NoValueColour _noValueColour = jalview.schemabinding.version2.types.NoValueColour + .valueOf("Min"); + + /** + * Field _threshType. + */ + private jalview.schemabinding.version2.types.ColourThreshTypeType _threshType; /** * Field _threshold. @@ -96,6 +103,11 @@ public class Colour implements java.io.Serializable */ private boolean _has_autoScale; + /** + * name of feature attribute to colour by, or attribute and sub-attribute + */ + private java.util.Vector _attributeNameList; + // ----------------/ // - Constructors -/ // ----------------/ @@ -103,6 +115,9 @@ public class Colour implements java.io.Serializable public Colour() { super(); + setNoValueColour(jalview.schemabinding.version2.types.NoValueColour + .valueOf("Min")); + this._attributeNameList = new java.util.Vector(); } // -----------/ @@ -110,41 +125,140 @@ public class Colour implements java.io.Serializable // -----------/ /** - */ + * + * + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.addElement(vAttributeName); + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.add(index, vAttributeName); + } + + /** + */ public void deleteAutoScale() { this._has_autoScale = false; } /** - */ + */ public void deleteColourByLabel() { this._has_colourByLabel = false; } /** - */ + */ public void deleteMax() { this._has_max = false; } /** - */ + */ public void deleteMin() { this._has_min = false; } /** - */ + */ public void deleteThreshold() { this._has_threshold = false; } /** + * Method enumerateAttributeName. + * + * @return an Enumeration over all java.lang.String elements + */ + public java.util.Enumeration enumerateAttributeName() + { + return this._attributeNameList.elements(); + } + + /** + * Method getAttributeName. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the java.lang.String at the given index + */ + public java.lang.String getAttributeName(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("getAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + return (java.lang.String) _attributeNameList.get(index); + } + + /** + * Method getAttributeName.Returns the contents of the collection in an Array. + *

    + * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public java.lang.String[] getAttributeName() + { + java.lang.String[] array = new java.lang.String[0]; + return (java.lang.String[]) this._attributeNameList.toArray(array); + } + + /** + * Method getAttributeNameCount. + * + * @return the size of this collection + */ + public int getAttributeNameCount() + { + return this._attributeNameList.size(); + } + + /** * Returns the value of field 'autoScale'. * * @return the value of field 'AutoScale'. @@ -195,7 +309,9 @@ public class Colour implements java.io.Serializable } /** - * Returns the value of field 'name'. + * Returns the value of field 'name'. The field 'name' has the following + * description: Single letter residue code for an alignment colour scheme, or + * feature type for a feature colour scheme * * @return the value of field 'Name'. */ @@ -205,6 +321,16 @@ public class Colour implements java.io.Serializable } /** + * Returns the value of field 'noValueColour'. + * + * @return the value of field 'NoValueColour'. + */ + public jalview.schemabinding.version2.types.NoValueColour getNoValueColour() + { + return this._noValueColour; + } + + /** * Returns the value of field 'RGB'. * * @return the value of field 'RGB'. @@ -215,12 +341,11 @@ public class Colour implements java.io.Serializable } /** - * Returns the value of field 'threshType'. The field 'threshType' has the - * following description: loosely specified enumeration: NONE,ABOVE, or BELOW + * Returns the value of field 'threshType'. * * @return the value of field 'ThreshType'. */ - public java.lang.String getThreshType() + public jalview.schemabinding.version2.types.ColourThreshTypeType getThreshType() { return this._threshType; } @@ -360,6 +485,76 @@ public class Colour implements java.io.Serializable } /** + */ + public void removeAllAttributeName() + { + this._attributeNameList.clear(); + } + + /** + * Method removeAttributeName. + * + * @param vAttributeName + * @return true if the object was removed from the collection. + */ + public boolean removeAttributeName(final java.lang.String vAttributeName) + { + boolean removed = _attributeNameList.remove(vAttributeName); + return removed; + } + + /** + * Method removeAttributeNameAt. + * + * @param index + * @return the element removed from the collection + */ + public java.lang.String removeAttributeNameAt(final int index) + { + java.lang.Object obj = this._attributeNameList.remove(index); + return (java.lang.String) obj; + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("setAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + this._attributeNameList.set(index, vAttributeName); + } + + /** + * + * + * @param vAttributeNameArray + */ + public void setAttributeName(final java.lang.String[] vAttributeNameArray) + { + // -- copy array + _attributeNameList.clear(); + + for (int i = 0; i < vAttributeNameArray.length; i++) + { + this._attributeNameList.add(vAttributeNameArray[i]); + } + } + + /** * Sets the value of field 'autoScale'. * * @param autoScale @@ -419,7 +614,9 @@ public class Colour implements java.io.Serializable } /** - * Sets the value of field 'name'. + * Sets the value of field 'name'. The field 'name' has the following + * description: Single letter residue code for an alignment colour scheme, or + * feature type for a feature colour scheme * * @param name * the value of field 'name'. @@ -430,6 +627,18 @@ public class Colour implements java.io.Serializable } /** + * Sets the value of field 'noValueColour'. + * + * @param noValueColour + * the value of field 'noValueColour'. + */ + public void setNoValueColour( + final jalview.schemabinding.version2.types.NoValueColour noValueColour) + { + this._noValueColour = noValueColour; + } + + /** * Sets the value of field 'RGB'. * * @param RGB @@ -441,13 +650,13 @@ public class Colour implements java.io.Serializable } /** - * Sets the value of field 'threshType'. The field 'threshType' has the - * following description: loosely specified enumeration: NONE,ABOVE, or BELOW + * Sets the value of field 'threshType'. * * @param threshType * the value of field 'threshType'. */ - public void setThreshType(final java.lang.String threshType) + public void setThreshType( + final jalview.schemabinding.version2.types.ColourThreshTypeType threshType) { this._threshType = threshType; } @@ -480,8 +689,8 @@ public class Colour implements java.io.Serializable throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException { - return (jalview.schemabinding.version2.Colour) Unmarshaller.unmarshal( - jalview.schemabinding.version2.Colour.class, reader); + return (jalview.schemabinding.version2.Colour) Unmarshaller + .unmarshal(jalview.schemabinding.version2.Colour.class, reader); } /** diff --git a/src/jalview/schemabinding/version2/CompoundMatcher.java b/src/jalview/schemabinding/version2/CompoundMatcher.java new file mode 100644 index 0000000..27714e2 --- /dev/null +++ b/src/jalview/schemabinding/version2/CompoundMatcher.java @@ -0,0 +1,374 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class CompoundMatcher. + * + * @version $Revision$ $Date$ + */ +public class CompoundMatcher implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * If true, matchers are AND-ed, if false they are OR-ed + */ + private boolean _and; + + /** + * keeps track of state for field: _and + */ + private boolean _has_and; + + /** + * Field _matcherSetList. + */ + private java.util.Vector _matcherSetList; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public CompoundMatcher() + { + super(); + this._matcherSetList = new java.util.Vector(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * + * + * @param vMatcherSet + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addMatcherSet( + final jalview.schemabinding.version2.MatcherSet vMatcherSet) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._matcherSetList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addMatcherSet has a maximum of 2"); + } + + this._matcherSetList.addElement(vMatcherSet); + } + + /** + * + * + * @param index + * @param vMatcherSet + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addMatcherSet(final int index, + final jalview.schemabinding.version2.MatcherSet vMatcherSet) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._matcherSetList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addMatcherSet has a maximum of 2"); + } + + this._matcherSetList.add(index, vMatcherSet); + } + + /** + */ + public void deleteAnd() + { + this._has_and = false; + } + + /** + * Method enumerateMatcherSet. + * + * @return an Enumeration over all jalview.schemabinding.version2.MatcherSet + * elements + */ + public java.util.Enumeration enumerateMatcherSet() + { + return this._matcherSetList.elements(); + } + + /** + * Returns the value of field 'and'. The field 'and' has the following + * description: If true, matchers are AND-ed, if false they are OR-ed + * + * @return the value of field 'And'. + */ + public boolean getAnd() + { + return this._and; + } + + /** + * Method getMatcherSet. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the jalview.schemabinding.version2.MatcherSet at the + * given index + */ + public jalview.schemabinding.version2.MatcherSet getMatcherSet( + final int index) throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._matcherSetList.size()) + { + throw new IndexOutOfBoundsException( + "getMatcherSet: Index value '" + index + "' not in range [0.." + + (this._matcherSetList.size() - 1) + "]"); + } + + return (jalview.schemabinding.version2.MatcherSet) _matcherSetList + .get(index); + } + + /** + * Method getMatcherSet.Returns the contents of the collection in an Array. + *

    + * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public jalview.schemabinding.version2.MatcherSet[] getMatcherSet() + { + jalview.schemabinding.version2.MatcherSet[] array = new jalview.schemabinding.version2.MatcherSet[0]; + return (jalview.schemabinding.version2.MatcherSet[]) this._matcherSetList + .toArray(array); + } + + /** + * Method getMatcherSetCount. + * + * @return the size of this collection + */ + public int getMatcherSetCount() + { + return this._matcherSetList.size(); + } + + /** + * Method hasAnd. + * + * @return true if at least one And has been added + */ + public boolean hasAnd() + { + return this._has_and; + } + + /** + * Returns the value of field 'and'. The field 'and' has the following + * description: If true, matchers are AND-ed, if false they are OR-ed + * + * @return the value of field 'And'. + */ + public boolean isAnd() + { + return this._and; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + */ + public void removeAllMatcherSet() + { + this._matcherSetList.clear(); + } + + /** + * Method removeMatcherSet. + * + * @param vMatcherSet + * @return true if the object was removed from the collection. + */ + public boolean removeMatcherSet( + final jalview.schemabinding.version2.MatcherSet vMatcherSet) + { + boolean removed = _matcherSetList.remove(vMatcherSet); + return removed; + } + + /** + * Method removeMatcherSetAt. + * + * @param index + * @return the element removed from the collection + */ + public jalview.schemabinding.version2.MatcherSet removeMatcherSetAt( + final int index) + { + java.lang.Object obj = this._matcherSetList.remove(index); + return (jalview.schemabinding.version2.MatcherSet) obj; + } + + /** + * Sets the value of field 'and'. The field 'and' has the following + * description: If true, matchers are AND-ed, if false they are OR-ed + * + * @param and + * the value of field 'and'. + */ + public void setAnd(final boolean and) + { + this._and = and; + this._has_and = true; + } + + /** + * + * + * @param index + * @param vMatcherSet + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setMatcherSet(final int index, + final jalview.schemabinding.version2.MatcherSet vMatcherSet) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._matcherSetList.size()) + { + throw new IndexOutOfBoundsException( + "setMatcherSet: Index value '" + index + "' not in range [0.." + + (this._matcherSetList.size() - 1) + "]"); + } + + this._matcherSetList.set(index, vMatcherSet); + } + + /** + * + * + * @param vMatcherSetArray + */ + public void setMatcherSet( + final jalview.schemabinding.version2.MatcherSet[] vMatcherSetArray) + { + // -- copy array + _matcherSetList.clear(); + + for (int i = 0; i < vMatcherSetArray.length; i++) + { + this._matcherSetList.add(vMatcherSetArray[i]); + } + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.schemabinding.version2.CompoundMatcher + */ + public static jalview.schemabinding.version2.CompoundMatcher unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.schemabinding.version2.CompoundMatcher) Unmarshaller + .unmarshal(jalview.schemabinding.version2.CompoundMatcher.class, + reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/schemabinding/version2/FeatureMatcher.java b/src/jalview/schemabinding/version2/FeatureMatcher.java new file mode 100644 index 0000000..4d29cab --- /dev/null +++ b/src/jalview/schemabinding/version2/FeatureMatcher.java @@ -0,0 +1,383 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class FeatureMatcher. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcher implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _by. + */ + private jalview.schemabinding.version2.types.FeatureMatcherByType _by; + + /** + * name of feature attribute to filter on, or attribute and sub-attribute + */ + private java.util.Vector _attributeNameList; + + /** + * Field _condition. + */ + private java.lang.String _condition; + + /** + * Field _value. + */ + private java.lang.String _value; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FeatureMatcher() + { + super(); + this._attributeNameList = new java.util.Vector(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * + * + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.addElement(vAttributeName); + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.add(index, vAttributeName); + } + + /** + * Method enumerateAttributeName. + * + * @return an Enumeration over all java.lang.String elements + */ + public java.util.Enumeration enumerateAttributeName() + { + return this._attributeNameList.elements(); + } + + /** + * Method getAttributeName. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the java.lang.String at the given index + */ + public java.lang.String getAttributeName(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("getAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + return (java.lang.String) _attributeNameList.get(index); + } + + /** + * Method getAttributeName.Returns the contents of the collection in an Array. + *

    + * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public java.lang.String[] getAttributeName() + { + java.lang.String[] array = new java.lang.String[0]; + return (java.lang.String[]) this._attributeNameList.toArray(array); + } + + /** + * Method getAttributeNameCount. + * + * @return the size of this collection + */ + public int getAttributeNameCount() + { + return this._attributeNameList.size(); + } + + /** + * Returns the value of field 'by'. + * + * @return the value of field 'By'. + */ + public jalview.schemabinding.version2.types.FeatureMatcherByType getBy() + { + return this._by; + } + + /** + * Returns the value of field 'condition'. + * + * @return the value of field 'Condition'. + */ + public java.lang.String getCondition() + { + return this._condition; + } + + /** + * Returns the value of field 'value'. + * + * @return the value of field 'Value'. + */ + public java.lang.String getValue() + { + return this._value; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + */ + public void removeAllAttributeName() + { + this._attributeNameList.clear(); + } + + /** + * Method removeAttributeName. + * + * @param vAttributeName + * @return true if the object was removed from the collection. + */ + public boolean removeAttributeName(final java.lang.String vAttributeName) + { + boolean removed = _attributeNameList.remove(vAttributeName); + return removed; + } + + /** + * Method removeAttributeNameAt. + * + * @param index + * @return the element removed from the collection + */ + public java.lang.String removeAttributeNameAt(final int index) + { + java.lang.Object obj = this._attributeNameList.remove(index); + return (java.lang.String) obj; + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("setAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + this._attributeNameList.set(index, vAttributeName); + } + + /** + * + * + * @param vAttributeNameArray + */ + public void setAttributeName(final java.lang.String[] vAttributeNameArray) + { + // -- copy array + _attributeNameList.clear(); + + for (int i = 0; i < vAttributeNameArray.length; i++) + { + this._attributeNameList.add(vAttributeNameArray[i]); + } + } + + /** + * Sets the value of field 'by'. + * + * @param by + * the value of field 'by'. + */ + public void setBy( + final jalview.schemabinding.version2.types.FeatureMatcherByType by) + { + this._by = by; + } + + /** + * Sets the value of field 'condition'. + * + * @param condition + * the value of field 'condition'. + */ + public void setCondition(final java.lang.String condition) + { + this._condition = condition; + } + + /** + * Sets the value of field 'value'. + * + * @param value + * the value of field 'value'. + */ + public void setValue(final java.lang.String value) + { + this._value = value; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcher + */ + public static jalview.schemabinding.version2.FeatureMatcher unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.schemabinding.version2.FeatureMatcher) Unmarshaller + .unmarshal(jalview.schemabinding.version2.FeatureMatcher.class, + reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/schemabinding/version2/FeatureMatcherSet.java b/src/jalview/schemabinding/version2/FeatureMatcherSet.java new file mode 100644 index 0000000..2d79a98 --- /dev/null +++ b/src/jalview/schemabinding/version2/FeatureMatcherSet.java @@ -0,0 +1,200 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * A feature match condition, which may be simple or compound + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherSet implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Internal choice value storage + */ + private java.lang.Object _choiceValue; + + /** + * Field _matchCondition. + */ + private MatchCondition _matchCondition; + + /** + * Field _compoundMatcher. + */ + private CompoundMatcher _compoundMatcher; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FeatureMatcherSet() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Returns the value of field 'choiceValue'. The field 'choiceValue' has the + * following description: Internal choice value storage + * + * @return the value of field 'ChoiceValue'. + */ + public java.lang.Object getChoiceValue() + { + return this._choiceValue; + } + + /** + * Returns the value of field 'compoundMatcher'. + * + * @return the value of field 'CompoundMatcher'. + */ + public CompoundMatcher getCompoundMatcher() + { + return this._compoundMatcher; + } + + /** + * Returns the value of field 'matchCondition'. + * + * @return the value of field 'MatchCondition'. + */ + public MatchCondition getMatchCondition() + { + return this._matchCondition; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Sets the value of field 'compoundMatcher'. + * + * @param compoundMatcher + * the value of field 'compoundMatcher'. + */ + public void setCompoundMatcher(final CompoundMatcher compoundMatcher) + { + this._compoundMatcher = compoundMatcher; + this._choiceValue = compoundMatcher; + } + + /** + * Sets the value of field 'matchCondition'. + * + * @param matchCondition + * the value of field 'matchCondition'. + */ + public void setMatchCondition(final MatchCondition matchCondition) + { + this._matchCondition = matchCondition; + this._choiceValue = matchCondition; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcherSet + */ + public static jalview.schemabinding.version2.FeatureMatcherSet unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.schemabinding.version2.FeatureMatcherSet) Unmarshaller + .unmarshal( + jalview.schemabinding.version2.FeatureMatcherSet.class, + reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/schemabinding/version2/Filter.java b/src/jalview/schemabinding/version2/Filter.java new file mode 100644 index 0000000..45323a7 --- /dev/null +++ b/src/jalview/schemabinding/version2/Filter.java @@ -0,0 +1,181 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class Filter. + * + * @version $Revision$ $Date$ + */ +public class Filter implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _featureType. + */ + private java.lang.String _featureType; + + /** + * Field _matcherSet. + */ + private jalview.schemabinding.version2.MatcherSet _matcherSet; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public Filter() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Returns the value of field 'featureType'. + * + * @return the value of field 'FeatureType'. + */ + public java.lang.String getFeatureType() + { + return this._featureType; + } + + /** + * Returns the value of field 'matcherSet'. + * + * @return the value of field 'MatcherSet'. + */ + public jalview.schemabinding.version2.MatcherSet getMatcherSet() + { + return this._matcherSet; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Sets the value of field 'featureType'. + * + * @param featureType + * the value of field 'featureType'. + */ + public void setFeatureType(final java.lang.String featureType) + { + this._featureType = featureType; + } + + /** + * Sets the value of field 'matcherSet'. + * + * @param matcherSet + * the value of field 'matcherSet'. + */ + public void setMatcherSet( + final jalview.schemabinding.version2.MatcherSet matcherSet) + { + this._matcherSet = matcherSet; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.schemabinding.version2.Filter + */ + public static jalview.schemabinding.version2.Filter unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.schemabinding.version2.Filter) Unmarshaller + .unmarshal(jalview.schemabinding.version2.Filter.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/schemabinding/version2/JalviewUserColours.java b/src/jalview/schemabinding/version2/JalviewUserColours.java index 042f092..c8d52ac 100644 --- a/src/jalview/schemabinding/version2/JalviewUserColours.java +++ b/src/jalview/schemabinding/version2/JalviewUserColours.java @@ -42,6 +42,11 @@ public class JalviewUserColours implements java.io.Serializable */ private java.util.Vector _colourList; + /** + * Field _filterList. + */ + private java.util.Vector _filterList; + // ----------------/ // - Constructors -/ // ----------------/ @@ -50,6 +55,7 @@ public class JalviewUserColours implements java.io.Serializable { super(); this._colourList = new java.util.Vector(); + this._filterList = new java.util.Vector(); } // -----------/ @@ -84,6 +90,33 @@ public class JalviewUserColours implements java.io.Serializable } /** + * + * + * @param vFilter + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addFilter(final Filter vFilter) + throws java.lang.IndexOutOfBoundsException + { + this._filterList.addElement(vFilter); + } + + /** + * + * + * @param index + * @param vFilter + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addFilter(final int index, final Filter vFilter) + throws java.lang.IndexOutOfBoundsException + { + this._filterList.add(index, vFilter); + } + + /** * Method enumerateColour. * * @return an Enumeration over all Colour elements @@ -94,6 +127,16 @@ public class JalviewUserColours implements java.io.Serializable } /** + * Method enumerateFilter. + * + * @return an Enumeration over all Filter elements + */ + public java.util.Enumeration enumerateFilter() + { + return this._filterList.elements(); + } + + /** * Method getColour. * * @param index @@ -107,9 +150,9 @@ public class JalviewUserColours implements java.io.Serializable // check bounds for index if (index < 0 || index >= this._colourList.size()) { - throw new IndexOutOfBoundsException("getColour: Index value '" - + index + "' not in range [0.." - + (this._colourList.size() - 1) + "]"); + throw new IndexOutOfBoundsException( + "getColour: Index value '" + index + "' not in range [0.." + + (this._colourList.size() - 1) + "]"); } return (Colour) _colourList.get(index); @@ -141,6 +184,53 @@ public class JalviewUserColours implements java.io.Serializable } /** + * Method getFilter. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the Filter at the given index + */ + public Filter getFilter(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._filterList.size()) + { + throw new IndexOutOfBoundsException( + "getFilter: Index value '" + index + "' not in range [0.." + + (this._filterList.size() - 1) + "]"); + } + + return (Filter) _filterList.get(index); + } + + /** + * Method getFilter.Returns the contents of the collection in an Array. + *

    + * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public Filter[] getFilter() + { + Filter[] array = new Filter[0]; + return (Filter[]) this._filterList.toArray(array); + } + + /** + * Method getFilterCount. + * + * @return the size of this collection + */ + public int getFilterCount() + { + return this._filterList.size(); + } + + /** * Returns the value of field 'schemeName'. * * @return the value of field 'SchemeName'. @@ -217,13 +307,20 @@ public class JalviewUserColours implements java.io.Serializable } /** - */ + */ public void removeAllColour() { this._colourList.clear(); } /** + */ + public void removeAllFilter() + { + this._filterList.clear(); + } + + /** * Method removeColour. * * @param vColour @@ -248,6 +345,30 @@ public class JalviewUserColours implements java.io.Serializable } /** + * Method removeFilter. + * + * @param vFilter + * @return true if the object was removed from the collection. + */ + public boolean removeFilter(final Filter vFilter) + { + boolean removed = _filterList.remove(vFilter); + return removed; + } + + /** + * Method removeFilterAt. + * + * @param index + * @return the element removed from the collection + */ + public Filter removeFilterAt(final int index) + { + java.lang.Object obj = this._filterList.remove(index); + return (Filter) obj; + } + + /** * * * @param index @@ -261,9 +382,9 @@ public class JalviewUserColours implements java.io.Serializable // check bounds for index if (index < 0 || index >= this._colourList.size()) { - throw new IndexOutOfBoundsException("setColour: Index value '" - + index + "' not in range [0.." - + (this._colourList.size() - 1) + "]"); + throw new IndexOutOfBoundsException( + "setColour: Index value '" + index + "' not in range [0.." + + (this._colourList.size() - 1) + "]"); } this._colourList.set(index, vColour); @@ -286,6 +407,44 @@ public class JalviewUserColours implements java.io.Serializable } /** + * + * + * @param index + * @param vFilter + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setFilter(final int index, final Filter vFilter) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._filterList.size()) + { + throw new IndexOutOfBoundsException( + "setFilter: Index value '" + index + "' not in range [0.." + + (this._filterList.size() - 1) + "]"); + } + + this._filterList.set(index, vFilter); + } + + /** + * + * + * @param vFilterArray + */ + public void setFilter(final Filter[] vFilterArray) + { + // -- copy array + _filterList.clear(); + + for (int i = 0; i < vFilterArray.length; i++) + { + this._filterList.add(vFilterArray[i]); + } + } + + /** * Sets the value of field 'schemeName'. * * @param schemeName diff --git a/src/jalview/schemabinding/version2/MatchCondition.java b/src/jalview/schemabinding/version2/MatchCondition.java new file mode 100644 index 0000000..af2f3f5 --- /dev/null +++ b/src/jalview/schemabinding/version2/MatchCondition.java @@ -0,0 +1,126 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * Class MatchCondition. + * + * @version $Revision$ $Date$ + */ +public class MatchCondition extends FeatureMatcher + implements java.io.Serializable +{ + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public MatchCondition() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcher + */ + public static jalview.schemabinding.version2.FeatureMatcher unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.schemabinding.version2.FeatureMatcher) Unmarshaller + .unmarshal(jalview.schemabinding.version2.MatchCondition.class, + reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/schemabinding/version2/MatcherSet.java b/src/jalview/schemabinding/version2/MatcherSet.java new file mode 100644 index 0000000..6fde9e4 --- /dev/null +++ b/src/jalview/schemabinding/version2/MatcherSet.java @@ -0,0 +1,126 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import org.exolab.castor.xml.Marshaller; +import org.exolab.castor.xml.Unmarshaller; + +/** + * optional filter(s) applied to the feature type + * + * @version $Revision$ $Date$ + */ +public class MatcherSet extends FeatureMatcherSet + implements java.io.Serializable +{ + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public MatcherSet() + { + super(); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid() + { + try + { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) + { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void marshal(final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException + * if an IOException occurs during marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + */ + public void marshal(final org.xml.sax.ContentHandler handler) + throws java.io.IOException, + org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + Marshaller.marshal(this, handler); + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException + * if object is null or if any SAXException is thrown during + * marshaling + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcherSet + */ + public static jalview.schemabinding.version2.FeatureMatcherSet unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, + org.exolab.castor.xml.ValidationException + { + return (jalview.schemabinding.version2.FeatureMatcherSet) Unmarshaller + .unmarshal(jalview.schemabinding.version2.MatcherSet.class, + reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException + * if this object is an invalid instance according to the schema + */ + public void validate() throws org.exolab.castor.xml.ValidationException + { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); + } + +} diff --git a/src/jalview/schemabinding/version2/OtherData.java b/src/jalview/schemabinding/version2/OtherData.java index fb6b276..31797fe 100644 --- a/src/jalview/schemabinding/version2/OtherData.java +++ b/src/jalview/schemabinding/version2/OtherData.java @@ -7,8 +7,8 @@ package jalview.schemabinding.version2; -//---------------------------------/ -//- Imported classes and packages -/ + //---------------------------------/ + //- Imported classes and packages -/ //---------------------------------/ import org.exolab.castor.xml.Marshaller; @@ -19,163 +19,181 @@ import org.exolab.castor.xml.Unmarshaller; * * @version $Revision$ $Date$ */ -public class OtherData implements java.io.Serializable -{ - - // --------------------------/ - // - Class/Member Variables -/ - // --------------------------/ - - /** - * Field _key. - */ - private java.lang.String _key; - - /** - * Field _value. - */ - private java.lang.String _value; - - // ----------------/ - // - Constructors -/ - // ----------------/ - - public OtherData() - { - super(); - } - - // -----------/ - // - Methods -/ - // -----------/ - - /** - * Returns the value of field 'key'. - * - * @return the value of field 'Key'. - */ - public java.lang.String getKey() - { - return this._key; - } - - /** - * Returns the value of field 'value'. - * - * @return the value of field 'Value'. - */ - public java.lang.String getValue() - { - return this._value; - } - - /** - * Method isValid. - * - * @return true if this object is valid according to the schema - */ - public boolean isValid() - { - try - { - validate(); - } catch (org.exolab.castor.xml.ValidationException vex) - { - return false; +public class OtherData implements java.io.Serializable { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * Field _key. + */ + private java.lang.String _key; + + /** + * key2 may be used for a sub-attribute of key + */ + private java.lang.String _key2; + + /** + * Field _value. + */ + private java.lang.String _value; + + + //----------------/ + //- Constructors -/ + //----------------/ + + public OtherData() { + super(); + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Returns the value of field 'key'. + * + * @return the value of field 'Key'. + */ + public java.lang.String getKey( + ) { + return this._key; + } + + /** + * Returns the value of field 'key2'. The field 'key2' has the + * following description: key2 may be used for a sub-attribute + * of key + * + * @return the value of field 'Key2'. + */ + public java.lang.String getKey2( + ) { + return this._key2; + } + + /** + * Returns the value of field 'value'. + * + * @return the value of field 'Value'. + */ + public java.lang.String getValue( + ) { + return this._value; + } + + /** + * Method isValid. + * + * @return true if this object is valid according to the schema + */ + public boolean isValid( + ) { + try { + validate(); + } catch (org.exolab.castor.xml.ValidationException vex) { + return false; + } + return true; + } + + /** + * + * + * @param out + * @throws org.exolab.castor.xml.MarshalException if object is + * null or if any SAXException is thrown during marshaling + * @throws org.exolab.castor.xml.ValidationException if this + * object is an invalid instance according to the schema + */ + public void marshal( + final java.io.Writer out) + throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException { + Marshaller.marshal(this, out); + } + + /** + * + * + * @param handler + * @throws java.io.IOException if an IOException occurs during + * marshaling + * @throws org.exolab.castor.xml.ValidationException if this + * object is an invalid instance according to the schema + * @throws org.exolab.castor.xml.MarshalException if object is + * null or if any SAXException is thrown during marshaling + */ + public void marshal( + final org.xml.sax.ContentHandler handler) + throws java.io.IOException, org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException { + Marshaller.marshal(this, handler); + } + + /** + * Sets the value of field 'key'. + * + * @param key the value of field 'key'. + */ + public void setKey( + final java.lang.String key) { + this._key = key; + } + + /** + * Sets the value of field 'key2'. The field 'key2' has the + * following description: key2 may be used for a sub-attribute + * of key + * + * @param key2 the value of field 'key2'. + */ + public void setKey2( + final java.lang.String key2) { + this._key2 = key2; + } + + /** + * Sets the value of field 'value'. + * + * @param value the value of field 'value'. + */ + public void setValue( + final java.lang.String value) { + this._value = value; + } + + /** + * Method unmarshal. + * + * @param reader + * @throws org.exolab.castor.xml.MarshalException if object is + * null or if any SAXException is thrown during marshaling + * @throws org.exolab.castor.xml.ValidationException if this + * object is an invalid instance according to the schema + * @return the unmarshaled + * jalview.schemabinding.version2.OtherData + */ + public static jalview.schemabinding.version2.OtherData unmarshal( + final java.io.Reader reader) + throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException { + return (jalview.schemabinding.version2.OtherData) Unmarshaller.unmarshal(jalview.schemabinding.version2.OtherData.class, reader); + } + + /** + * + * + * @throws org.exolab.castor.xml.ValidationException if this + * object is an invalid instance according to the schema + */ + public void validate( + ) + throws org.exolab.castor.xml.ValidationException { + org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); + validator.validate(this); } - return true; - } - - /** - * - * - * @param out - * @throws org.exolab.castor.xml.MarshalException - * if object is null or if any SAXException is thrown during - * marshaling - * @throws org.exolab.castor.xml.ValidationException - * if this object is an invalid instance according to the schema - */ - public void marshal(final java.io.Writer out) - throws org.exolab.castor.xml.MarshalException, - org.exolab.castor.xml.ValidationException - { - Marshaller.marshal(this, out); - } - - /** - * - * - * @param handler - * @throws java.io.IOException - * if an IOException occurs during marshaling - * @throws org.exolab.castor.xml.ValidationException - * if this object is an invalid instance according to the schema - * @throws org.exolab.castor.xml.MarshalException - * if object is null or if any SAXException is thrown during - * marshaling - */ - public void marshal(final org.xml.sax.ContentHandler handler) - throws java.io.IOException, - org.exolab.castor.xml.MarshalException, - org.exolab.castor.xml.ValidationException - { - Marshaller.marshal(this, handler); - } - - /** - * Sets the value of field 'key'. - * - * @param key - * the value of field 'key'. - */ - public void setKey(final java.lang.String key) - { - this._key = key; - } - - /** - * Sets the value of field 'value'. - * - * @param value - * the value of field 'value'. - */ - public void setValue(final java.lang.String value) - { - this._value = value; - } - - /** - * Method unmarshal. - * - * @param reader - * @throws org.exolab.castor.xml.MarshalException - * if object is null or if any SAXException is thrown during - * marshaling - * @throws org.exolab.castor.xml.ValidationException - * if this object is an invalid instance according to the schema - * @return the unmarshaled jalview.schemabinding.version2.OtherData - */ - public static jalview.schemabinding.version2.OtherData unmarshal( - final java.io.Reader reader) - throws org.exolab.castor.xml.MarshalException, - org.exolab.castor.xml.ValidationException - { - return (jalview.schemabinding.version2.OtherData) Unmarshaller - .unmarshal(jalview.schemabinding.version2.OtherData.class, - reader); - } - - /** - * - * - * @throws org.exolab.castor.xml.ValidationException - * if this object is an invalid instance according to the schema - */ - public void validate() throws org.exolab.castor.xml.ValidationException - { - org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator(); - validator.validate(this); - } } diff --git a/src/jalview/schemabinding/version2/Setting.java b/src/jalview/schemabinding/version2/Setting.java index c458971..59e9522 100644 --- a/src/jalview/schemabinding/version2/Setting.java +++ b/src/jalview/schemabinding/version2/Setting.java @@ -73,6 +73,12 @@ public class Setting implements java.io.Serializable private boolean _has_mincolour; /** + * Field _noValueColour. + */ + private jalview.schemabinding.version2.types.NoValueColour _noValueColour = jalview.schemabinding.version2.types.NoValueColour + .valueOf("Min"); + + /** * threshold value for graduated feature colour * */ @@ -134,6 +140,16 @@ public class Setting implements java.io.Serializable */ private boolean _has_autoScale; + /** + * name of feature attribute to colour by, or attribute and sub-attribute + */ + private java.util.Vector _attributeNameList; + + /** + * optional filter(s) applied to the feature type + */ + private jalview.schemabinding.version2.MatcherSet _matcherSet; + // ----------------/ // - Constructors -/ // ----------------/ @@ -141,6 +157,9 @@ public class Setting implements java.io.Serializable public Setting() { super(); + setNoValueColour(jalview.schemabinding.version2.types.NoValueColour + .valueOf("Min")); + this._attributeNameList = new java.util.Vector(); } // -----------/ @@ -148,76 +167,175 @@ public class Setting implements java.io.Serializable // -----------/ /** - */ + * + * + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.addElement(vAttributeName); + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void addAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check for the maximum size + if (this._attributeNameList.size() >= 2) + { + throw new IndexOutOfBoundsException( + "addAttributeName has a maximum of 2"); + } + + this._attributeNameList.add(index, vAttributeName); + } + + /** + */ public void deleteAutoScale() { this._has_autoScale = false; } /** - */ + */ public void deleteColour() { this._has_colour = false; } /** - */ + */ public void deleteColourByLabel() { this._has_colourByLabel = false; } /** - */ + */ public void deleteDisplay() { this._has_display = false; } /** - */ + */ public void deleteMax() { this._has_max = false; } /** - */ + */ public void deleteMin() { this._has_min = false; } /** - */ + */ public void deleteMincolour() { this._has_mincolour = false; } /** - */ + */ public void deleteOrder() { this._has_order = false; } /** - */ + */ public void deleteThreshold() { this._has_threshold = false; } /** - */ + */ public void deleteThreshstate() { this._has_threshstate = false; } /** + * Method enumerateAttributeName. + * + * @return an Enumeration over all java.lang.String elements + */ + public java.util.Enumeration enumerateAttributeName() + { + return this._attributeNameList.elements(); + } + + /** + * Method getAttributeName. + * + * @param index + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + * @return the value of the java.lang.String at the given index + */ + public java.lang.String getAttributeName(final int index) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("getAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + return (java.lang.String) _attributeNameList.get(index); + } + + /** + * Method getAttributeName.Returns the contents of the collection in an Array. + *

    + * Note: Just in case the collection contents are changing in another thread, + * we pass a 0-length Array of the correct type into the API call. This way we + * know that the Array returned is of exactly the correct length. + * + * @return this collection as an Array + */ + public java.lang.String[] getAttributeName() + { + java.lang.String[] array = new java.lang.String[0]; + return (java.lang.String[]) this._attributeNameList.toArray(array); + } + + /** + * Method getAttributeNameCount. + * + * @return the size of this collection + */ + public int getAttributeNameCount() + { + return this._attributeNameList.size(); + } + + /** * Returns the value of field 'autoScale'. * * @return the value of field 'AutoScale'. @@ -258,6 +376,17 @@ public class Setting implements java.io.Serializable } /** + * Returns the value of field 'matcherSet'. The field 'matcherSet' has the + * following description: optional filter(s) applied to the feature type + * + * @return the value of field 'MatcherSet'. + */ + public jalview.schemabinding.version2.MatcherSet getMatcherSet() + { + return this._matcherSet; + } + + /** * Returns the value of field 'max'. * * @return the value of field 'Max'. @@ -290,6 +419,16 @@ public class Setting implements java.io.Serializable } /** + * Returns the value of field 'noValueColour'. + * + * @return the value of field 'NoValueColour'. + */ + public jalview.schemabinding.version2.types.NoValueColour getNoValueColour() + { + return this._noValueColour; + } + + /** * Returns the value of field 'order'. * * @return the value of field 'Order'. @@ -518,6 +657,76 @@ public class Setting implements java.io.Serializable } /** + */ + public void removeAllAttributeName() + { + this._attributeNameList.clear(); + } + + /** + * Method removeAttributeName. + * + * @param vAttributeName + * @return true if the object was removed from the collection. + */ + public boolean removeAttributeName(final java.lang.String vAttributeName) + { + boolean removed = _attributeNameList.remove(vAttributeName); + return removed; + } + + /** + * Method removeAttributeNameAt. + * + * @param index + * @return the element removed from the collection + */ + public java.lang.String removeAttributeNameAt(final int index) + { + java.lang.Object obj = this._attributeNameList.remove(index); + return (java.lang.String) obj; + } + + /** + * + * + * @param index + * @param vAttributeName + * @throws java.lang.IndexOutOfBoundsException + * if the index given is outside the bounds of the collection + */ + public void setAttributeName(final int index, + final java.lang.String vAttributeName) + throws java.lang.IndexOutOfBoundsException + { + // check bounds for index + if (index < 0 || index >= this._attributeNameList.size()) + { + throw new IndexOutOfBoundsException("setAttributeName: Index value '" + + index + "' not in range [0.." + + (this._attributeNameList.size() - 1) + "]"); + } + + this._attributeNameList.set(index, vAttributeName); + } + + /** + * + * + * @param vAttributeNameArray + */ + public void setAttributeName(final java.lang.String[] vAttributeNameArray) + { + // -- copy array + _attributeNameList.clear(); + + for (int i = 0; i < vAttributeNameArray.length; i++) + { + this._attributeNameList.add(vAttributeNameArray[i]); + } + } + + /** * Sets the value of field 'autoScale'. * * @param autoScale @@ -566,6 +775,19 @@ public class Setting implements java.io.Serializable } /** + * Sets the value of field 'matcherSet'. The field 'matcherSet' has the + * following description: optional filter(s) applied to the feature type + * + * @param matcherSet + * the value of field 'matcherSet'. + */ + public void setMatcherSet( + final jalview.schemabinding.version2.MatcherSet matcherSet) + { + this._matcherSet = matcherSet; + } + + /** * Sets the value of field 'max'. * * @param max @@ -604,6 +826,18 @@ public class Setting implements java.io.Serializable } /** + * Sets the value of field 'noValueColour'. + * + * @param noValueColour + * the value of field 'noValueColour'. + */ + public void setNoValueColour( + final jalview.schemabinding.version2.types.NoValueColour noValueColour) + { + this._noValueColour = noValueColour; + } + + /** * Sets the value of field 'order'. * * @param order diff --git a/src/jalview/schemabinding/version2/descriptors/ColourDescriptor.java b/src/jalview/schemabinding/version2/descriptors/ColourDescriptor.java index 8b1ae9e..cca4ef1 100644 --- a/src/jalview/schemabinding/version2/descriptors/ColourDescriptor.java +++ b/src/jalview/schemabinding/version2/descriptors/ColourDescriptor.java @@ -18,8 +18,8 @@ import jalview.schemabinding.version2.Colour; * * @version $Revision$ $Date$ */ -public class ColourDescriptor extends - org.exolab.castor.xml.util.XMLClassDescriptorImpl +public class ColourDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl { // --------------------------/ @@ -55,6 +55,9 @@ public class ColourDescriptor extends super(); _xmlName = "colour"; _elementDefinition = true; + + // -- set grouping compositor + setCompositorAsSequence(); org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; org.exolab.castor.mapping.FieldHandler handler = null; org.exolab.castor.xml.FieldValidator fieldValidator = null; @@ -197,11 +200,57 @@ public class ColourDescriptor extends typeValidator.setWhiteSpace("preserve"); } desc.setValidator(fieldValidator); - // -- _threshType + // -- _noValueColour desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( - java.lang.String.class, "_threshType", "threshType", + jalview.schemabinding.version2.types.NoValueColour.class, + "_noValueColour", "noValueColour", org.exolab.castor.xml.NodeType.Attribute); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Colour target = (Colour) object; + return target.getNoValueColour(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Colour target = (Colour) object; + target.setNoValueColour( + (jalview.schemabinding.version2.types.NoValueColour) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + handler = new org.exolab.castor.xml.handlers.EnumFieldHandler( + jalview.schemabinding.version2.types.NoValueColour.class, + handler); desc.setImmutable(true); + desc.setHandler(handler); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _noValueColour + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + { // -- local scope + } + desc.setValidator(fieldValidator); + // -- _threshType + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + jalview.schemabinding.version2.types.ColourThreshTypeType.class, + "_threshType", "threshType", + org.exolab.castor.xml.NodeType.Attribute); handler = new org.exolab.castor.xml.XMLFieldHandler() { public java.lang.Object getValue(java.lang.Object object) @@ -217,7 +266,8 @@ public class ColourDescriptor extends try { Colour target = (Colour) object; - target.setThreshType((java.lang.String) value); + target.setThreshType( + (jalview.schemabinding.version2.types.ColourThreshTypeType) value); } catch (java.lang.Exception ex) { throw new IllegalStateException(ex.toString()); @@ -229,6 +279,10 @@ public class ColourDescriptor extends return null; } }; + handler = new org.exolab.castor.xml.handlers.EnumFieldHandler( + jalview.schemabinding.version2.types.ColourThreshTypeType.class, + handler); + desc.setImmutable(true); desc.setHandler(handler); desc.setMultivalued(false); addFieldDescriptor(desc); @@ -236,10 +290,6 @@ public class ColourDescriptor extends // -- validation code for: _threshType fieldValidator = new org.exolab.castor.xml.FieldValidator(); { // -- local scope - org.exolab.castor.xml.validators.StringValidator typeValidator; - typeValidator = new org.exolab.castor.xml.validators.StringValidator(); - fieldValidator.setValidator(typeValidator); - typeValidator.setWhiteSpace("preserve"); } desc.setValidator(fieldValidator); // -- _threshold @@ -437,8 +487,8 @@ public class ColourDescriptor extends target.deleteColourByLabel(); return; } - target.setColourByLabel(((java.lang.Boolean) value) - .booleanValue()); + target.setColourByLabel( + ((java.lang.Boolean) value).booleanValue()); } catch (java.lang.Exception ex) { throw new IllegalStateException(ex.toString()); @@ -518,6 +568,66 @@ public class ColourDescriptor extends desc.setValidator(fieldValidator); // -- initialize element descriptors + // -- _attributeNameList + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.String.class, "_attributeNameList", "attributeName", + org.exolab.castor.xml.NodeType.Element); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Colour target = (Colour) object; + return target.getAttributeName(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Colour target = (Colour) object; + target.addAttributeName((java.lang.String) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public void resetValue(Object object) + throws IllegalStateException, IllegalArgumentException + { + try + { + Colour target = (Colour) object; + target.removeAllAttributeName(); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setMultivalued(true); + addFieldDescriptor(desc); + + // -- validation code for: _attributeNameList + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(0); + fieldValidator.setMaxOccurs(2); + { // -- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); } // -----------/ diff --git a/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java b/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java new file mode 100644 index 0000000..2402d68 --- /dev/null +++ b/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java @@ -0,0 +1,270 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.descriptors; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.CompoundMatcher; + +/** + * Class CompoundMatcherDescriptor. + * + * @version $Revision$ $Date$ + */ +public class CompoundMatcherDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public CompoundMatcherDescriptor() + { + super(); + _xmlName = "compoundMatcher"; + _elementDefinition = true; + + // -- set grouping compositor + setCompositorAsSequence(); + org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; + org.exolab.castor.mapping.FieldHandler handler = null; + org.exolab.castor.xml.FieldValidator fieldValidator = null; + // -- initialize attribute descriptors + + // -- _and + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.Boolean.TYPE, "_and", "and", + org.exolab.castor.xml.NodeType.Attribute); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + CompoundMatcher target = (CompoundMatcher) object; + if (!target.hasAnd()) + { + return null; + } + return (target.getAnd() ? java.lang.Boolean.TRUE + : java.lang.Boolean.FALSE); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + CompoundMatcher target = (CompoundMatcher) object; + // ignore null values for non optional primitives + if (value == null) + { + return; + } + + target.setAnd(((java.lang.Boolean) value).booleanValue()); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _and + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + org.exolab.castor.xml.validators.BooleanValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.BooleanValidator(); + fieldValidator.setValidator(typeValidator); + } + desc.setValidator(fieldValidator); + // -- initialize element descriptors + + // -- _matcherSetList + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + jalview.schemabinding.version2.MatcherSet.class, + "_matcherSetList", "matcherSet", + org.exolab.castor.xml.NodeType.Element); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + CompoundMatcher target = (CompoundMatcher) object; + return target.getMatcherSet(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + CompoundMatcher target = (CompoundMatcher) object; + target.addMatcherSet( + (jalview.schemabinding.version2.MatcherSet) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public void resetValue(Object object) + throws IllegalStateException, IllegalArgumentException + { + try + { + CompoundMatcher target = (CompoundMatcher) object; + target.removeAllMatcherSet(); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return new jalview.schemabinding.version2.MatcherSet(); + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(true); + addFieldDescriptor(desc); + + // -- validation code for: _matcherSetList + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(2); + fieldValidator.setMaxOccurs(2); + { // -- local scope + } + desc.setValidator(fieldValidator); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.CompoundMatcher.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java new file mode 100644 index 0000000..2df2f5b --- /dev/null +++ b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java @@ -0,0 +1,356 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.descriptors; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.FeatureMatcher; + +/** + * Class FeatureMatcherDescriptor. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FeatureMatcherDescriptor() + { + super(); + _nsURI = "www.jalview.org/colours"; + _xmlName = "FeatureMatcher"; + _elementDefinition = false; + + // -- set grouping compositor + setCompositorAsSequence(); + org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; + org.exolab.castor.mapping.FieldHandler handler = null; + org.exolab.castor.xml.FieldValidator fieldValidator = null; + // -- initialize attribute descriptors + + // -- _by + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + jalview.schemabinding.version2.types.FeatureMatcherByType.class, + "_by", "by", org.exolab.castor.xml.NodeType.Attribute); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + FeatureMatcher target = (FeatureMatcher) object; + return target.getBy(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcher target = (FeatureMatcher) object; + target.setBy( + (jalview.schemabinding.version2.types.FeatureMatcherByType) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + handler = new org.exolab.castor.xml.handlers.EnumFieldHandler( + jalview.schemabinding.version2.types.FeatureMatcherByType.class, + handler); + desc.setImmutable(true); + desc.setHandler(handler); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _by + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + { // -- local scope + } + desc.setValidator(fieldValidator); + // -- initialize element descriptors + + // -- _attributeNameList + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.String.class, "_attributeNameList", "attributeName", + org.exolab.castor.xml.NodeType.Element); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + FeatureMatcher target = (FeatureMatcher) object; + return target.getAttributeName(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcher target = (FeatureMatcher) object; + target.addAttributeName((java.lang.String) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public void resetValue(Object object) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcher target = (FeatureMatcher) object; + target.removeAllAttributeName(); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setMultivalued(true); + addFieldDescriptor(desc); + + // -- validation code for: _attributeNameList + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(0); + fieldValidator.setMaxOccurs(2); + { // -- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + // -- _condition + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.String.class, "_condition", "condition", + org.exolab.castor.xml.NodeType.Element); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + FeatureMatcher target = (FeatureMatcher) object; + return target.getCondition(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcher target = (FeatureMatcher) object; + target.setCondition((java.lang.String) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _condition + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + // -- _value + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.String.class, "_value", "value", + org.exolab.castor.xml.NodeType.Element); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + FeatureMatcher target = (FeatureMatcher) object; + return target.getValue(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcher target = (FeatureMatcher) object; + target.setValue((java.lang.String) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _value + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.FeatureMatcher.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java new file mode 100644 index 0000000..b3d19bb --- /dev/null +++ b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java @@ -0,0 +1,258 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.descriptors; + +import jalview.schemabinding.version2.CompoundMatcher; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.FeatureMatcherSet; +import jalview.schemabinding.version2.MatchCondition; + +/** + * Class FeatureMatcherSetDescriptor. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherSetDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FeatureMatcherSetDescriptor() + { + super(); + _nsURI = "www.jalview.org/colours"; + _xmlName = "FeatureMatcherSet"; + _elementDefinition = false; + + // -- set grouping compositor + setCompositorAsChoice(); + org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; + org.exolab.castor.mapping.FieldHandler handler = null; + org.exolab.castor.xml.FieldValidator fieldValidator = null; + // -- initialize attribute descriptors + + // -- initialize element descriptors + + // -- _matchCondition + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + MatchCondition.class, "_matchCondition", "matchCondition", + org.exolab.castor.xml.NodeType.Element); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + @Override + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + FeatureMatcherSet target = (FeatureMatcherSet) object; + return target.getMatchCondition(); + } + + @Override + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcherSet target = (FeatureMatcherSet) object; + target.setMatchCondition((MatchCondition) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + @Override + public java.lang.Object newInstance(java.lang.Object parent) + { + return new MatchCondition(); + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _matchCondition + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + } + desc.setValidator(fieldValidator); + // -- _compoundMatcher + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + CompoundMatcher.class, "_compoundMatcher", "compoundMatcher", + org.exolab.castor.xml.NodeType.Element); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + @Override + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + FeatureMatcherSet target = (FeatureMatcherSet) object; + return target.getCompoundMatcher(); + } + + @Override + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + FeatureMatcherSet target = (FeatureMatcherSet) object; + target.setCompoundMatcher((CompoundMatcher) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + @Override + public java.lang.Object newInstance(java.lang.Object parent) + { + return new CompoundMatcher(); + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _compoundMatcher + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + } + desc.setValidator(fieldValidator); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + @Override + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + @Override + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + @Override + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.FeatureMatcherSet.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + @Override + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + @Override + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + @Override + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + @Override + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + @Override + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java new file mode 100644 index 0000000..f58f9ae --- /dev/null +++ b/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java @@ -0,0 +1,246 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.descriptors; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.Filter; + +/** + * Class FilterDescriptor. + * + * @version $Revision$ $Date$ + */ +public class FilterDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public FilterDescriptor() + { + super(); + _xmlName = "filter"; + _elementDefinition = true; + + // -- set grouping compositor + setCompositorAsSequence(); + org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; + org.exolab.castor.mapping.FieldHandler handler = null; + org.exolab.castor.xml.FieldValidator fieldValidator = null; + // -- initialize attribute descriptors + + // -- _featureType + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.String.class, "_featureType", "featureType", + org.exolab.castor.xml.NodeType.Attribute); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Filter target = (Filter) object; + return target.getFeatureType(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Filter target = (Filter) object; + target.setFeatureType((java.lang.String) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _featureType + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + // -- initialize element descriptors + + // -- _matcherSet + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + jalview.schemabinding.version2.MatcherSet.class, "_matcherSet", + "matcherSet", org.exolab.castor.xml.NodeType.Element); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Filter target = (Filter) object; + return target.getMatcherSet(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Filter target = (Filter) object; + target.setMatcherSet( + (jalview.schemabinding.version2.MatcherSet) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return new jalview.schemabinding.version2.MatcherSet(); + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _matcherSet + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { // -- local scope + } + desc.setValidator(fieldValidator); + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.Filter.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/descriptors/JalviewUserColoursDescriptor.java b/src/jalview/schemabinding/version2/descriptors/JalviewUserColoursDescriptor.java index d65de13..459d645 100644 --- a/src/jalview/schemabinding/version2/descriptors/JalviewUserColoursDescriptor.java +++ b/src/jalview/schemabinding/version2/descriptors/JalviewUserColoursDescriptor.java @@ -7,11 +7,13 @@ package jalview.schemabinding.version2.descriptors; +import jalview.schemabinding.version2.Colour; +import jalview.schemabinding.version2.Filter; + //---------------------------------/ //- Imported classes and packages -/ //---------------------------------/ -import jalview.schemabinding.version2.Colour; import jalview.schemabinding.version2.JalviewUserColours; /** @@ -19,8 +21,8 @@ import jalview.schemabinding.version2.JalviewUserColours; * * @version $Revision$ $Date$ */ -public class JalviewUserColoursDescriptor extends - org.exolab.castor.xml.util.XMLClassDescriptorImpl +public class JalviewUserColoursDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl { // --------------------------/ @@ -192,8 +194,8 @@ public class JalviewUserColoursDescriptor extends } @Override - public void resetValue(Object object) throws IllegalStateException, - IllegalArgumentException + public void resetValue(Object object) + throws IllegalStateException, IllegalArgumentException { try { @@ -221,6 +223,64 @@ public class JalviewUserColoursDescriptor extends { // -- local scope } desc.setValidator(fieldValidator); + // -- _filterList + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + Filter.class, "_filterList", "filter", + org.exolab.castor.xml.NodeType.Element); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + @Override + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + JalviewUserColours target = (JalviewUserColours) object; + return target.getFilter(); + } + + @Override + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + JalviewUserColours target = (JalviewUserColours) object; + target.addFilter((Filter) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + @Override + public void resetValue(Object object) + throws IllegalStateException, IllegalArgumentException + { + try + { + JalviewUserColours target = (JalviewUserColours) object; + target.removeAllFilter(); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + @Override + public java.lang.Object newInstance(java.lang.Object parent) + { + return new Filter(); + } + }; + desc.setHandler(handler); + desc.setMultivalued(true); + addFieldDescriptor(desc); + + // -- validation code for: _filterList + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(0); + { // -- local scope + } + desc.setValidator(fieldValidator); } // -----------/ diff --git a/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java b/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java new file mode 100644 index 0000000..8373421 --- /dev/null +++ b/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java @@ -0,0 +1,148 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.descriptors; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.MatchCondition; + +/** + * Class MatchConditionDescriptor. + * + * @version $Revision$ $Date$ + */ +public class MatchConditionDescriptor extends + jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public MatchConditionDescriptor() + { + super(); + setExtendsWithoutFlatten( + new jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor()); + _xmlName = "matchCondition"; + _elementDefinition = true; + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.MatchCondition.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java b/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java new file mode 100644 index 0000000..2807f92 --- /dev/null +++ b/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java @@ -0,0 +1,148 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.descriptors; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.MatcherSet; + +/** + * Class MatcherSetDescriptor. + * + * @version $Revision$ $Date$ + */ +public class MatcherSetDescriptor extends + jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public MatcherSetDescriptor() + { + super(); + setExtendsWithoutFlatten( + new jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor()); + _xmlName = "matcherSet"; + _elementDefinition = true; + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.MatcherSet.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/descriptors/OtherDataDescriptor.java b/src/jalview/schemabinding/version2/descriptors/OtherDataDescriptor.java index f582311..ab7a626 100644 --- a/src/jalview/schemabinding/version2/descriptors/OtherDataDescriptor.java +++ b/src/jalview/schemabinding/version2/descriptors/OtherDataDescriptor.java @@ -7,8 +7,8 @@ package jalview.schemabinding.version2.descriptors; -//---------------------------------/ -//- Imported classes and packages -/ + //---------------------------------/ + //- Imported classes and packages -/ //---------------------------------/ import jalview.schemabinding.version2.OtherData; @@ -18,231 +18,255 @@ import jalview.schemabinding.version2.OtherData; * * @version $Revision$ $Date$ */ -public class OtherDataDescriptor extends - org.exolab.castor.xml.util.XMLClassDescriptorImpl -{ - - // --------------------------/ - // - Class/Member Variables -/ - // --------------------------/ - - /** - * Field _elementDefinition. - */ - private boolean _elementDefinition; - - /** - * Field _nsPrefix. - */ - private java.lang.String _nsPrefix; - - /** - * Field _nsURI. - */ - private java.lang.String _nsURI; - - /** - * Field _xmlName. - */ - private java.lang.String _xmlName; - - // ----------------/ - // - Constructors -/ - // ----------------/ - - public OtherDataDescriptor() - { - super(); - _nsURI = "www.jalview.org"; - _xmlName = "otherData"; - _elementDefinition = true; - org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; - org.exolab.castor.mapping.FieldHandler handler = null; - org.exolab.castor.xml.FieldValidator fieldValidator = null; - // -- initialize attribute descriptors - - // -- _key - desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( - java.lang.String.class, "_key", "key", - org.exolab.castor.xml.NodeType.Attribute); - desc.setImmutable(true); - handler = new org.exolab.castor.xml.XMLFieldHandler() - { - public java.lang.Object getValue(java.lang.Object object) - throws IllegalStateException - { - OtherData target = (OtherData) object; - return target.getKey(); - } - - public void setValue(java.lang.Object object, java.lang.Object value) - throws IllegalStateException, IllegalArgumentException - { - try - { - OtherData target = (OtherData) object; - target.setKey((java.lang.String) value); - } catch (java.lang.Exception ex) - { - throw new IllegalStateException(ex.toString()); +public class OtherDataDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + + //----------------/ + //- Constructors -/ + //----------------/ + + public OtherDataDescriptor() { + super(); + _nsURI = "www.jalview.org"; + _xmlName = "otherData"; + _elementDefinition = true; + org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; + org.exolab.castor.mapping.FieldHandler handler = null; + org.exolab.castor.xml.FieldValidator fieldValidator = null; + //-- initialize attribute descriptors + + //-- _key + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_key", "key", org.exolab.castor.xml.NodeType.Attribute); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() { + public java.lang.Object getValue( java.lang.Object object ) + throws IllegalStateException + { + OtherData target = (OtherData) object; + return target.getKey(); + } + public void setValue( java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try { + OtherData target = (OtherData) object; + target.setKey( (java.lang.String) value); + } catch (java.lang.Exception ex) { + throw new IllegalStateException(ex.toString()); + } + } + public java.lang.Object newInstance(java.lang.Object parent) { + return null; + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + //-- validation code for: _key + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { //-- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + //-- _key2 + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_key2", "key2", org.exolab.castor.xml.NodeType.Attribute); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() { + public java.lang.Object getValue( java.lang.Object object ) + throws IllegalStateException + { + OtherData target = (OtherData) object; + return target.getKey2(); + } + public void setValue( java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try { + OtherData target = (OtherData) object; + target.setKey2( (java.lang.String) value); + } catch (java.lang.Exception ex) { + throw new IllegalStateException(ex.toString()); + } + } + public java.lang.Object newInstance(java.lang.Object parent) { + return null; + } + }; + desc.setHandler(handler); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + //-- validation code for: _key2 + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + { //-- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + //-- _value + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_value", "value", org.exolab.castor.xml.NodeType.Attribute); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() { + public java.lang.Object getValue( java.lang.Object object ) + throws IllegalStateException + { + OtherData target = (OtherData) object; + return target.getValue(); + } + public void setValue( java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try { + OtherData target = (OtherData) object; + target.setValue( (java.lang.String) value); + } catch (java.lang.Exception ex) { + throw new IllegalStateException(ex.toString()); + } + } + public java.lang.Object newInstance(java.lang.Object parent) { + return null; + } + }; + desc.setHandler(handler); + desc.setRequired(true); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + //-- validation code for: _value + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(1); + { //-- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); } - } + desc.setValidator(fieldValidator); + //-- initialize element descriptors + + } + + + //-----------/ + //- Methods -/ + //-----------/ - public java.lang.Object newInstance(java.lang.Object parent) - { + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode( + ) { return null; - } - }; - desc.setHandler(handler); - desc.setRequired(true); - desc.setMultivalued(false); - addFieldDescriptor(desc); - - // -- validation code for: _key - fieldValidator = new org.exolab.castor.xml.FieldValidator(); - fieldValidator.setMinOccurs(1); - { // -- local scope - org.exolab.castor.xml.validators.StringValidator typeValidator; - typeValidator = new org.exolab.castor.xml.validators.StringValidator(); - fieldValidator.setValidator(typeValidator); - typeValidator.setWhiteSpace("preserve"); } - desc.setValidator(fieldValidator); - // -- _value - desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( - java.lang.String.class, "_value", "value", - org.exolab.castor.xml.NodeType.Attribute); - desc.setImmutable(true); - handler = new org.exolab.castor.xml.XMLFieldHandler() - { - public java.lang.Object getValue(java.lang.Object object) - throws IllegalStateException - { - OtherData target = (OtherData) object; - return target.getValue(); - } - - public void setValue(java.lang.Object object, java.lang.Object value) - throws IllegalStateException, IllegalArgumentException - { - try - { - OtherData target = (OtherData) object; - target.setValue((java.lang.String) value); - } catch (java.lang.Exception ex) - { - throw new IllegalStateException(ex.toString()); - } - } - public java.lang.Object newInstance(java.lang.Object parent) - { - return null; - } - }; - desc.setHandler(handler); - desc.setRequired(true); - desc.setMultivalued(false); - addFieldDescriptor(desc); - - // -- validation code for: _value - fieldValidator = new org.exolab.castor.xml.FieldValidator(); - fieldValidator.setMinOccurs(1); - { // -- local scope - org.exolab.castor.xml.validators.StringValidator typeValidator; - typeValidator = new org.exolab.castor.xml.validators.StringValidator(); - fieldValidator.setValidator(typeValidator); - typeValidator.setWhiteSpace("preserve"); + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no + * identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity( + ) { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass( + ) { + return jalview.schemabinding.version2.OtherData.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix( + ) { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and + * unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI( + ) { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator( + ) { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName( + ) { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that + * of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition( + ) { + return _elementDefinition; } - desc.setValidator(fieldValidator); - // -- initialize element descriptors - - } - - // -----------/ - // - Methods -/ - // -----------/ - - /** - * Method getAccessMode. - * - * @return the access mode specified for this class. - */ - public org.exolab.castor.mapping.AccessMode getAccessMode() - { - return null; - } - - /** - * Method getIdentity. - * - * @return the identity field, null if this class has no identity. - */ - public org.exolab.castor.mapping.FieldDescriptor getIdentity() - { - return super.getIdentity(); - } - - /** - * Method getJavaClass. - * - * @return the Java class represented by this descriptor. - */ - public java.lang.Class getJavaClass() - { - return jalview.schemabinding.version2.OtherData.class; - } - - /** - * Method getNameSpacePrefix. - * - * @return the namespace prefix to use when marshaling as XML. - */ - public java.lang.String getNameSpacePrefix() - { - return _nsPrefix; - } - - /** - * Method getNameSpaceURI. - * - * @return the namespace URI used when marshaling and unmarshaling as XML. - */ - public java.lang.String getNameSpaceURI() - { - return _nsURI; - } - - /** - * Method getValidator. - * - * @return a specific validator for the class described by this - * ClassDescriptor. - */ - public org.exolab.castor.xml.TypeValidator getValidator() - { - return this; - } - - /** - * Method getXMLName. - * - * @return the XML Name for the Class being described. - */ - public java.lang.String getXMLName() - { - return _xmlName; - } - - /** - * Method isElementDefinition. - * - * @return true if XML schema definition of this Class is that of a global - * element or element with anonymous type definition. - */ - public boolean isElementDefinition() - { - return _elementDefinition; - } } diff --git a/src/jalview/schemabinding/version2/descriptors/SettingDescriptor.java b/src/jalview/schemabinding/version2/descriptors/SettingDescriptor.java index 4703f46..c816e43 100644 --- a/src/jalview/schemabinding/version2/descriptors/SettingDescriptor.java +++ b/src/jalview/schemabinding/version2/descriptors/SettingDescriptor.java @@ -18,8 +18,8 @@ import jalview.schemabinding.version2.Setting; * * @version $Revision$ $Date$ */ -public class SettingDescriptor extends - org.exolab.castor.xml.util.XMLClassDescriptorImpl +public class SettingDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl { // --------------------------/ @@ -56,6 +56,9 @@ public class SettingDescriptor extends _nsURI = "www.jalview.org"; _xmlName = "setting"; _elementDefinition = true; + + // -- set grouping compositor + setCompositorAsSequence(); org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null; org.exolab.castor.mapping.FieldHandler handler = null; org.exolab.castor.xml.FieldValidator fieldValidator = null; @@ -331,6 +334,52 @@ public class SettingDescriptor extends typeValidator.setMaxInclusive(2147483647); } desc.setValidator(fieldValidator); + // -- _noValueColour + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + jalview.schemabinding.version2.types.NoValueColour.class, + "_noValueColour", "noValueColour", + org.exolab.castor.xml.NodeType.Attribute); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Setting target = (Setting) object; + return target.getNoValueColour(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Setting target = (Setting) object; + target.setNoValueColour( + (jalview.schemabinding.version2.types.NoValueColour) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + handler = new org.exolab.castor.xml.handlers.EnumFieldHandler( + jalview.schemabinding.version2.types.NoValueColour.class, + handler); + desc.setImmutable(true); + desc.setHandler(handler); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _noValueColour + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + { // -- local scope + } + desc.setValidator(fieldValidator); // -- _threshold desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( java.lang.Float.TYPE, "_threshold", "threshold", @@ -581,8 +630,8 @@ public class SettingDescriptor extends target.deleteColourByLabel(); return; } - target.setColourByLabel(((java.lang.Boolean) value) - .booleanValue()); + target.setColourByLabel( + ((java.lang.Boolean) value).booleanValue()); } catch (java.lang.Exception ex) { throw new IllegalStateException(ex.toString()); @@ -662,6 +711,109 @@ public class SettingDescriptor extends desc.setValidator(fieldValidator); // -- initialize element descriptors + // -- _attributeNameList + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + java.lang.String.class, "_attributeNameList", "attributeName", + org.exolab.castor.xml.NodeType.Element); + desc.setImmutable(true); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Setting target = (Setting) object; + return target.getAttributeName(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Setting target = (Setting) object; + target.addAttributeName((java.lang.String) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public void resetValue(Object object) + throws IllegalStateException, IllegalArgumentException + { + try + { + Setting target = (Setting) object; + target.removeAllAttributeName(); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return null; + } + }; + desc.setHandler(handler); + desc.setNameSpaceURI("www.jalview.org"); + desc.setMultivalued(true); + addFieldDescriptor(desc); + + // -- validation code for: _attributeNameList + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + fieldValidator.setMinOccurs(0); + fieldValidator.setMaxOccurs(2); + { // -- local scope + org.exolab.castor.xml.validators.StringValidator typeValidator; + typeValidator = new org.exolab.castor.xml.validators.StringValidator(); + fieldValidator.setValidator(typeValidator); + typeValidator.setWhiteSpace("preserve"); + } + desc.setValidator(fieldValidator); + // -- _matcherSet + desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl( + jalview.schemabinding.version2.MatcherSet.class, "_matcherSet", + "matcherSet", org.exolab.castor.xml.NodeType.Element); + handler = new org.exolab.castor.xml.XMLFieldHandler() + { + public java.lang.Object getValue(java.lang.Object object) + throws IllegalStateException + { + Setting target = (Setting) object; + return target.getMatcherSet(); + } + + public void setValue(java.lang.Object object, java.lang.Object value) + throws IllegalStateException, IllegalArgumentException + { + try + { + Setting target = (Setting) object; + target.setMatcherSet( + (jalview.schemabinding.version2.MatcherSet) value); + } catch (java.lang.Exception ex) + { + throw new IllegalStateException(ex.toString()); + } + } + + public java.lang.Object newInstance(java.lang.Object parent) + { + return new jalview.schemabinding.version2.MatcherSet(); + } + }; + desc.setHandler(handler); + desc.setNameSpaceURI("www.jalview.org"); + desc.setMultivalued(false); + addFieldDescriptor(desc); + + // -- validation code for: _matcherSet + fieldValidator = new org.exolab.castor.xml.FieldValidator(); + { // -- local scope + } + desc.setValidator(fieldValidator); } // -----------/ diff --git a/src/jalview/schemabinding/version2/types/.castor.cdr b/src/jalview/schemabinding/version2/types/.castor.cdr new file mode 100644 index 0000000..d9874b6 --- /dev/null +++ b/src/jalview/schemabinding/version2/types/.castor.cdr @@ -0,0 +1,5 @@ +#Thu Dec 14 15:28:22 GMT 2017 +jalview.schemabinding.version2.types.ColourNoValueColourType=jalview.schemabinding.version2.types.descriptors.ColourNoValueColourTypeDescriptor +jalview.schemabinding.version2.types.FeatureMatcherByType=jalview.schemabinding.version2.types.descriptors.FeatureMatcherByTypeDescriptor +jalview.schemabinding.version2.types.NoValueColour=jalview.schemabinding.version2.types.descriptors.NoValueColourDescriptor +jalview.schemabinding.version2.types.ColourThreshTypeType=jalview.schemabinding.version2.types.descriptors.ColourThreshTypeTypeDescriptor diff --git a/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java b/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java new file mode 100644 index 0000000..0330411 --- /dev/null +++ b/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java @@ -0,0 +1,168 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.types; + + //---------------------------------/ + //- Imported classes and packages -/ +//---------------------------------/ + +import java.util.Hashtable; + +/** + * Class ColourThreshTypeType. + * + * @version $Revision$ $Date$ + */ +public class ColourThreshTypeType implements java.io.Serializable { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * The NONE type + */ + public static final int NONE_TYPE = 0; + + /** + * The instance of the NONE type + */ + public static final ColourThreshTypeType NONE = new ColourThreshTypeType(NONE_TYPE, "NONE"); + + /** + * The ABOVE type + */ + public static final int ABOVE_TYPE = 1; + + /** + * The instance of the ABOVE type + */ + public static final ColourThreshTypeType ABOVE = new ColourThreshTypeType(ABOVE_TYPE, "ABOVE"); + + /** + * The BELOW type + */ + public static final int BELOW_TYPE = 2; + + /** + * The instance of the BELOW type + */ + public static final ColourThreshTypeType BELOW = new ColourThreshTypeType(BELOW_TYPE, "BELOW"); + + /** + * Field _memberTable. + */ + private static java.util.Hashtable _memberTable = init(); + + /** + * Field type. + */ + private int type = -1; + + /** + * Field stringValue. + */ + private java.lang.String stringValue = null; + + + //----------------/ + //- Constructors -/ + //----------------/ + + private ColourThreshTypeType(final int type, final java.lang.String value) { + super(); + this.type = type; + this.stringValue = value; + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Method enumerate.Returns an enumeration of all possible + * instances of ColourThreshTypeType + * + * @return an Enumeration over all possible instances of + * ColourThreshTypeType + */ + public static java.util.Enumeration enumerate( + ) { + return _memberTable.elements(); + } + + /** + * Method getType.Returns the type of this ColourThreshTypeType + * + * @return the type of this ColourThreshTypeType + */ + public int getType( + ) { + return this.type; + } + + /** + * Method init. + * + * @return the initialized Hashtable for the member table + */ + private static java.util.Hashtable init( + ) { + Hashtable members = new Hashtable(); + members.put("NONE", NONE); + members.put("ABOVE", ABOVE); + members.put("BELOW", BELOW); + return members; + } + + /** + * Method readResolve. will be called during deserialization to + * replace the deserialized object with the correct constant + * instance. + * + * @return this deserialized object + */ + private java.lang.Object readResolve( + ) { + return valueOf(this.stringValue); + } + + /** + * Method toString.Returns the String representation of this + * ColourThreshTypeType + * + * @return the String representation of this ColourThreshTypeTyp + */ + public java.lang.String toString( + ) { + return this.stringValue; + } + + /** + * Method valueOf.Returns a new ColourThreshTypeType based on + * the given String value. + * + * @param string + * @return the ColourThreshTypeType value of parameter 'string' + */ + public static jalview.schemabinding.version2.types.ColourThreshTypeType valueOf( + final java.lang.String string) { + java.lang.Object obj = null; + if (string != null) { + obj = _memberTable.get(string); + } + if (obj == null) { + String err = "" + string + " is not a valid ColourThreshTypeType"; + throw new IllegalArgumentException(err); + } + return (ColourThreshTypeType) obj; + } + +} diff --git a/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java b/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java new file mode 100644 index 0000000..6e97332 --- /dev/null +++ b/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java @@ -0,0 +1,168 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.types; + + //---------------------------------/ + //- Imported classes and packages -/ +//---------------------------------/ + +import java.util.Hashtable; + +/** + * Class FeatureMatcherByType. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherByType implements java.io.Serializable { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * The byLabel type + */ + public static final int BYLABEL_TYPE = 0; + + /** + * The instance of the byLabel type + */ + public static final FeatureMatcherByType BYLABEL = new FeatureMatcherByType(BYLABEL_TYPE, "byLabel"); + + /** + * The byScore type + */ + public static final int BYSCORE_TYPE = 1; + + /** + * The instance of the byScore type + */ + public static final FeatureMatcherByType BYSCORE = new FeatureMatcherByType(BYSCORE_TYPE, "byScore"); + + /** + * The byAttribute type + */ + public static final int BYATTRIBUTE_TYPE = 2; + + /** + * The instance of the byAttribute type + */ + public static final FeatureMatcherByType BYATTRIBUTE = new FeatureMatcherByType(BYATTRIBUTE_TYPE, "byAttribute"); + + /** + * Field _memberTable. + */ + private static java.util.Hashtable _memberTable = init(); + + /** + * Field type. + */ + private int type = -1; + + /** + * Field stringValue. + */ + private java.lang.String stringValue = null; + + + //----------------/ + //- Constructors -/ + //----------------/ + + private FeatureMatcherByType(final int type, final java.lang.String value) { + super(); + this.type = type; + this.stringValue = value; + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Method enumerate.Returns an enumeration of all possible + * instances of FeatureMatcherByType + * + * @return an Enumeration over all possible instances of + * FeatureMatcherByType + */ + public static java.util.Enumeration enumerate( + ) { + return _memberTable.elements(); + } + + /** + * Method getType.Returns the type of this FeatureMatcherByType + * + * @return the type of this FeatureMatcherByType + */ + public int getType( + ) { + return this.type; + } + + /** + * Method init. + * + * @return the initialized Hashtable for the member table + */ + private static java.util.Hashtable init( + ) { + Hashtable members = new Hashtable(); + members.put("byLabel", BYLABEL); + members.put("byScore", BYSCORE); + members.put("byAttribute", BYATTRIBUTE); + return members; + } + + /** + * Method readResolve. will be called during deserialization to + * replace the deserialized object with the correct constant + * instance. + * + * @return this deserialized object + */ + private java.lang.Object readResolve( + ) { + return valueOf(this.stringValue); + } + + /** + * Method toString.Returns the String representation of this + * FeatureMatcherByType + * + * @return the String representation of this FeatureMatcherByTyp + */ + public java.lang.String toString( + ) { + return this.stringValue; + } + + /** + * Method valueOf.Returns a new FeatureMatcherByType based on + * the given String value. + * + * @param string + * @return the FeatureMatcherByType value of parameter 'string' + */ + public static jalview.schemabinding.version2.types.FeatureMatcherByType valueOf( + final java.lang.String string) { + java.lang.Object obj = null; + if (string != null) { + obj = _memberTable.get(string); + } + if (obj == null) { + String err = "" + string + " is not a valid FeatureMatcherByType"; + throw new IllegalArgumentException(err); + } + return (FeatureMatcherByType) obj; + } + +} diff --git a/src/jalview/schemabinding/version2/types/NoValueColour.java b/src/jalview/schemabinding/version2/types/NoValueColour.java new file mode 100644 index 0000000..bbef3d7 --- /dev/null +++ b/src/jalview/schemabinding/version2/types/NoValueColour.java @@ -0,0 +1,169 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.types; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import java.util.Hashtable; + +/** + * Graduated feature colour if no score (or attribute) value + * + * @version $Revision$ $Date$ + */ +public class NoValueColour implements java.io.Serializable +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * The None type + */ + public static final int NONE_TYPE = 0; + + /** + * The instance of the None type + */ + public static final NoValueColour NONE = new NoValueColour(NONE_TYPE, + "None"); + + /** + * The Min type + */ + public static final int MIN_TYPE = 1; + + /** + * The instance of the Min type + */ + public static final NoValueColour MIN = new NoValueColour(MIN_TYPE, + "Min"); + + /** + * The Max type + */ + public static final int MAX_TYPE = 2; + + /** + * The instance of the Max type + */ + public static final NoValueColour MAX = new NoValueColour(MAX_TYPE, + "Max"); + + /** + * Field _memberTable. + */ + private static java.util.Hashtable _memberTable = init(); + + /** + * Field type. + */ + private int type = -1; + + /** + * Field stringValue. + */ + private java.lang.String stringValue = null; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + private NoValueColour(final int type, final java.lang.String value) + { + super(); + this.type = type; + this.stringValue = value; + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method enumerate.Returns an enumeration of all possible instances of + * NoValueColour + * + * @return an Enumeration over all possible instances of NoValueColour + */ + public static java.util.Enumeration enumerate() + { + return _memberTable.elements(); + } + + /** + * Method getType.Returns the type of this NoValueColour + * + * @return the type of this NoValueColour + */ + public int getType() + { + return this.type; + } + + /** + * Method init. + * + * @return the initialized Hashtable for the member table + */ + private static java.util.Hashtable init() + { + Hashtable members = new Hashtable(); + members.put("None", NONE); + members.put("Min", MIN); + members.put("Max", MAX); + return members; + } + + /** + * Method readResolve. will be called during deserialization to replace the + * deserialized object with the correct constant instance. + * + * @return this deserialized object + */ + private java.lang.Object readResolve() + { + return valueOf(this.stringValue); + } + + /** + * Method toString.Returns the String representation of this NoValueColour + * + * @return the String representation of this NoValueColour + */ + public java.lang.String toString() + { + return this.stringValue; + } + + /** + * Method valueOf.Returns a new NoValueColour based on the given String value. + * + * @param string + * @return the NoValueColour value of parameter 'string' + */ + public static jalview.schemabinding.version2.types.NoValueColour valueOf( + final java.lang.String string) + { + java.lang.Object obj = null; + if (string != null) + { + obj = _memberTable.get(string); + } + if (obj == null) + { + String err = "" + string + " is not a valid NoValueColour"; + throw new IllegalArgumentException(err); + } + return (NoValueColour) obj; + } + +} diff --git a/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java new file mode 100644 index 0000000..f978363 --- /dev/null +++ b/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java @@ -0,0 +1,150 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.types.descriptors; + + //---------------------------------/ + //- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.types.ColourThreshTypeType; + +/** + * Class ColourThreshTypeTypeDescriptor. + * + * @version $Revision$ $Date$ + */ +public class ColourThreshTypeTypeDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + + //----------------/ + //- Constructors -/ + //----------------/ + + public ColourThreshTypeTypeDescriptor() { + super(); + _nsURI = "www.jalview.org/colours"; + _xmlName = "ColourThreshTypeType"; + _elementDefinition = false; + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode( + ) { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no + * identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity( + ) { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass( + ) { + return jalview.schemabinding.version2.types.ColourThreshTypeType.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix( + ) { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and + * unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI( + ) { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator( + ) { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName( + ) { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that + * of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition( + ) { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java new file mode 100644 index 0000000..e392e76 --- /dev/null +++ b/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java @@ -0,0 +1,150 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.types.descriptors; + + //---------------------------------/ + //- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.types.FeatureMatcherByType; + +/** + * Class FeatureMatcherByTypeDescriptor. + * + * @version $Revision$ $Date$ + */ +public class FeatureMatcherByTypeDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl { + + + //--------------------------/ + //- Class/Member Variables -/ + //--------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + + //----------------/ + //- Constructors -/ + //----------------/ + + public FeatureMatcherByTypeDescriptor() { + super(); + _nsURI = "www.jalview.org/colours"; + _xmlName = "FeatureMatcherByType"; + _elementDefinition = false; + } + + + //-----------/ + //- Methods -/ + //-----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode( + ) { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no + * identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity( + ) { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass( + ) { + return jalview.schemabinding.version2.types.FeatureMatcherByType.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix( + ) { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and + * unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI( + ) { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator( + ) { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName( + ) { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that + * of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition( + ) { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java new file mode 100644 index 0000000..14c58ed --- /dev/null +++ b/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java @@ -0,0 +1,147 @@ +/* + * This class was automatically generated with + * Castor 1.1, using an XML + * Schema. + * $Id$ + */ + +package jalview.schemabinding.version2.types.descriptors; + +//---------------------------------/ +//- Imported classes and packages -/ +//---------------------------------/ + +import jalview.schemabinding.version2.types.NoValueColour; + +/** + * Class NoValueColourDescriptor. + * + * @version $Revision$ $Date$ + */ +public class NoValueColourDescriptor + extends org.exolab.castor.xml.util.XMLClassDescriptorImpl +{ + + // --------------------------/ + // - Class/Member Variables -/ + // --------------------------/ + + /** + * Field _elementDefinition. + */ + private boolean _elementDefinition; + + /** + * Field _nsPrefix. + */ + private java.lang.String _nsPrefix; + + /** + * Field _nsURI. + */ + private java.lang.String _nsURI; + + /** + * Field _xmlName. + */ + private java.lang.String _xmlName; + + // ----------------/ + // - Constructors -/ + // ----------------/ + + public NoValueColourDescriptor() + { + super(); + _nsURI = "www.jalview.org/colours"; + _xmlName = "NoValueColour"; + _elementDefinition = false; + } + + // -----------/ + // - Methods -/ + // -----------/ + + /** + * Method getAccessMode. + * + * @return the access mode specified for this class. + */ + public org.exolab.castor.mapping.AccessMode getAccessMode() + { + return null; + } + + /** + * Method getIdentity. + * + * @return the identity field, null if this class has no identity. + */ + public org.exolab.castor.mapping.FieldDescriptor getIdentity() + { + return super.getIdentity(); + } + + /** + * Method getJavaClass. + * + * @return the Java class represented by this descriptor. + */ + public java.lang.Class getJavaClass() + { + return jalview.schemabinding.version2.types.NoValueColour.class; + } + + /** + * Method getNameSpacePrefix. + * + * @return the namespace prefix to use when marshaling as XML. + */ + public java.lang.String getNameSpacePrefix() + { + return _nsPrefix; + } + + /** + * Method getNameSpaceURI. + * + * @return the namespace URI used when marshaling and unmarshaling as XML. + */ + public java.lang.String getNameSpaceURI() + { + return _nsURI; + } + + /** + * Method getValidator. + * + * @return a specific validator for the class described by this + * ClassDescriptor. + */ + public org.exolab.castor.xml.TypeValidator getValidator() + { + return this; + } + + /** + * Method getXMLName. + * + * @return the XML Name for the Class being described. + */ + public java.lang.String getXMLName() + { + return _xmlName; + } + + /** + * Method isElementDefinition. + * + * @return true if XML schema definition of this Class is that of a global + * element or element with anonymous type definition. + */ + public boolean isElementDefinition() + { + return _elementDefinition; + } + +} diff --git a/src/jalview/schemes/FeatureColour.java b/src/jalview/schemes/FeatureColour.java index 54d1c6c..7d14662 100644 --- a/src/jalview/schemes/FeatureColour.java +++ b/src/jalview/schemes/FeatureColour.java @@ -22,6 +22,7 @@ package jalview.schemes; import jalview.api.FeatureColourI; import jalview.datamodel.SequenceFeature; +import jalview.datamodel.features.FeatureMatcher; import jalview.util.ColorUtils; import jalview.util.Format; @@ -29,10 +30,49 @@ import java.awt.Color; import java.util.StringTokenizer; /** - * A class that wraps either a simple colour or a graduated colour + * A class that represents a colour scheme for a feature type. Options supported + * are currently + *

      + *
    • a simple colour e.g. Red
    • + *
    • colour by label - a colour is generated from the feature description
    • + *
    • graduated colour by feature score
    • + *
        + *
      • minimum and maximum score range must be provided
      • + *
      • minimum and maximum value colours should be specified
      • + *
      • a colour for 'no value' may optionally be provided
      • + *
      • colours for intermediate scores are interpolated RGB values
      • + *
      • there is an optional threshold above/below which to colour values
      • + *
      • the range may be the full value range, or may be limited by the threshold + * value
      • + *
      + *
    • colour by (text) value of a named attribute
    • graduated colour by + * (numeric) value of a named attribute
    */ public class FeatureColour implements FeatureColourI { + private static final String ABSOLUTE = "abso"; + + private static final String ABOVE = "above"; + + private static final String BELOW = "below"; + + /* + * constants used to read or write a Jalview Features file + */ + private static final String LABEL = "label"; + + private static final String SCORE = "score"; + + private static final String ATTRIBUTE = "attribute"; + + private static final String NO_VALUE_MIN = "noValueMin"; + + private static final String NO_VALUE_MAX = "noValueMax"; + + private static final String NO_VALUE_NONE = "noValueNone"; + + static final Color DEFAULT_NO_COLOUR = null; + private static final String BAR = "|"; final private Color colour; @@ -41,10 +81,30 @@ public class FeatureColour implements FeatureColourI final private Color maxColour; + /* + * colour to use for colour by attribute when the + * attribute value is absent + */ + final private Color noColour; + + /* + * if true, then colour has a gradient based on a numerical + * range (either feature score, or an attribute value) + */ private boolean graduatedColour; + /* + * if true, colour values are generated from a text string, + * either feature description, or an attribute value + */ private boolean colourByLabel; + /* + * if not null, the value of [attribute, [sub-attribute] ...] + * is used for colourByLabel or graduatedColour + */ + private String[] attributeName; + private float threshold; private float base; @@ -55,8 +115,6 @@ public class FeatureColour implements FeatureColourI private boolean aboveThreshold; - private boolean thresholdIsMinOrMax; - private boolean isHighToLow; private boolean autoScaled; @@ -75,16 +133,29 @@ public class FeatureColour implements FeatureColourI /** * Parses a Jalview features file format colour descriptor - * [label|][mincolour|maxcolour - * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples: + *

    + * + * [label|score|[attribute|attributeName]|][mincolour|maxcolour| + * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue] + *

    + * 'Score' is optional (default) for a graduated colour. An attribute with + * sub-attribute should be written as (for example) CSQ:Consequence. + * noValueOption is one of noValueMin, noValueMax, noValueNone + * with default noValueMin. + *

    + * Examples: *

      *
    • red
    • *
    • a28bbb
    • *
    • 25,125,213
    • *
    • label
    • + *
    • attribute|CSQ:PolyPhen
    • *
    • label|||0.0|0.0|above|12.5
    • *
    • label|||0.0|0.0|below|12.5
    • *
    • red|green|12.0|26.0|none
    • + *
    • score|red|green|12.0|26.0|none
    • + *
    • attribute|AF|red|green|12.0|26.0|none
    • + *
    • attribute|AF|red|green|noValueNone|12.0|26.0|none
    • *
    • a28bbb|3eb555|12.0|26.0|above|12.5
    • *
    • a28bbb|3eb555|abso|12.0|26.0|below|12.5
    • *
    @@ -94,34 +165,71 @@ public class FeatureColour implements FeatureColourI * @throws IllegalArgumentException * if not parseable */ - public static FeatureColour parseJalviewFeatureColour(String descriptor) + public static FeatureColourI parseJalviewFeatureColour(String descriptor) { - StringTokenizer gcol = new StringTokenizer(descriptor, "|", true); + StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true); float min = Float.MIN_VALUE; float max = Float.MAX_VALUE; - boolean labelColour = false; + boolean byLabel = false; + boolean byAttribute = false; + String attName = null; + String mincol = null; + String maxcol = null; - String mincol = gcol.nextToken(); - if (mincol == "|") + /* + * first token should be 'label', or 'score', or an + * attribute name, or simple colour, or minimum colour + */ + String nextToken = gcol.nextToken(); + if (nextToken == BAR) { throw new IllegalArgumentException( "Expected either 'label' or a colour specification in the line: " + descriptor); } - String maxcol = null; - if (mincol.toLowerCase().indexOf("label") == 0) + if (nextToken.toLowerCase().startsWith(LABEL)) + { + byLabel = true; + // get the token after the next delimiter: + mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); + mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); + } + else if (nextToken.toLowerCase().startsWith(SCORE)) + { + mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); + mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); + } + else if (nextToken.toLowerCase().startsWith(ATTRIBUTE)) { - labelColour = true; + byAttribute = true; + attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null); + attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null); mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); - // skip '|' mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); } + else + { + mincol = nextToken; + } - if (!labelColour && !gcol.hasMoreTokens()) + /* + * if only one token, it can validly be label, attributeName, + * or a plain colour value + */ + if (!gcol.hasMoreTokens()) { - /* - * only a simple colour specification - parse it - */ + if (byLabel || byAttribute) + { + FeatureColourI fc = new FeatureColour(); + fc.setColourByLabel(true); + if (byAttribute) + { + fc.setAttributeName( + FeatureMatcher.fromAttributeDisplayName(attName)); + } + return fc; + } + Color colour = ColorUtils.parseColourString(descriptor); if (colour == null) { @@ -132,34 +240,64 @@ public class FeatureColour implements FeatureColourI } /* + * continue parsing for min/max/no colour (if graduated) + * and for threshold (colour by text or graduated) + */ + + /* * autoScaled == true: colours range over actual score range * autoScaled == false ('abso'): colours range over min/max range */ boolean autoScaled = true; String tok = null, minval, maxval; + String noValueColour = NO_VALUE_MIN; + if (mincol != null) { // at least four more tokens - if (mincol.equals("|")) + if (mincol.equals(BAR)) { - mincol = ""; + mincol = null; } else { gcol.nextToken(); // skip next '|' } maxcol = gcol.nextToken(); - if (maxcol.equals("|")) + if (maxcol.equals(BAR)) { - maxcol = ""; + maxcol = null; } else { gcol.nextToken(); // skip next '|' } tok = gcol.nextToken(); + + /* + * check for specifier for colour for no attribute value + * (new in 2.11, defaults to minColour if not specified) + */ + if (tok.equalsIgnoreCase(NO_VALUE_MIN)) + { + tok = gcol.nextToken(); + tok = gcol.nextToken(); + } + else if (tok.equalsIgnoreCase(NO_VALUE_MAX)) + { + noValueColour = NO_VALUE_MAX; + tok = gcol.nextToken(); + tok = gcol.nextToken(); + } + else if (tok.equalsIgnoreCase(NO_VALUE_NONE)) + { + noValueColour = NO_VALUE_NONE; + tok = gcol.nextToken(); + tok = gcol.nextToken(); + } + gcol.nextToken(); // skip next '|' - if (tok.toLowerCase().startsWith("abso")) + if (tok.toLowerCase().startsWith(ABSOLUTE)) { minval = gcol.nextToken(); gcol.nextToken(); // skip next '|' @@ -201,34 +339,45 @@ public class FeatureColour implements FeatureColourI } else { - // add in some dummy min/max colours for the label-only - // colourscheme. - mincol = "FFFFFF"; - maxcol = "000000"; + /* + * dummy min/max colours for colour by text + * (label or attribute value) + */ + mincol = "white"; + maxcol = "black"; + byLabel = true; } /* - * construct the FeatureColour + * construct the FeatureColour! */ FeatureColour featureColour; try { Color minColour = ColorUtils.parseColourString(mincol); Color maxColour = ColorUtils.parseColourString(maxcol); - featureColour = new FeatureColour(minColour, maxColour, min, max); - featureColour.setColourByLabel(labelColour); + Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour + : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour); + featureColour = new FeatureColour(minColour, maxColour, noColour, min, + max); + featureColour.setColourByLabel(minColour == null); featureColour.setAutoScaled(autoScaled); + if (byAttribute) + { + featureColour.setAttributeName( + FeatureMatcher.fromAttributeDisplayName(attName)); + } // add in any additional parameters String ttype = null, tval = null; if (gcol.hasMoreTokens()) { // threshold type and possibly a threshold value ttype = gcol.nextToken(); - if (ttype.toLowerCase().startsWith("below")) + if (ttype.toLowerCase().startsWith(BELOW)) { featureColour.setBelowThreshold(true); } - else if (ttype.toLowerCase().startsWith("above")) + else if (ttype.toLowerCase().startsWith(ABOVE)) { featureColour.setAboveThreshold(true); } @@ -260,7 +409,7 @@ public class FeatureColour implements FeatureColourI "Ignoring additional tokens in parameters in graduated colour specification\n"); while (gcol.hasMoreTokens()) { - System.err.println("|" + gcol.nextToken()); + System.err.println(BAR + gcol.nextToken()); } System.err.println("\n"); } @@ -288,6 +437,7 @@ public class FeatureColour implements FeatureColourI { minColour = Color.WHITE; maxColour = Color.BLACK; + noColour = DEFAULT_NO_COLOUR; minRed = 0f; minGreen = 0f; minBlue = 0f; @@ -298,7 +448,8 @@ public class FeatureColour implements FeatureColourI } /** - * Constructor given a colour range and a score range + * Constructor given a colour range and a score range, defaulting 'no value + * colour' to be the same as minimum colour * * @param low * @param high @@ -307,36 +458,7 @@ public class FeatureColour implements FeatureColourI */ public FeatureColour(Color low, Color high, float min, float max) { - if (low == null) - { - low = Color.white; - } - if (high == null) - { - high = Color.black; - } - graduatedColour = true; - colour = null; - minColour = low; - maxColour = high; - threshold = Float.NaN; - isHighToLow = min >= max; - minRed = low.getRed() / 255f; - minGreen = low.getGreen() / 255f; - minBlue = low.getBlue() / 255f; - deltaRed = (high.getRed() / 255f) - minRed; - deltaGreen = (high.getGreen() / 255f) - minGreen; - deltaBlue = (high.getBlue() / 255f) - minBlue; - if (isHighToLow) - { - base = max; - range = min - max; - } - else - { - base = min; - range = max - min; - } + this(low, high, low, min, max); } /** @@ -350,6 +472,7 @@ public class FeatureColour implements FeatureColourI colour = fc.colour; minColour = fc.minColour; maxColour = fc.maxColour; + noColour = fc.noColour; minRed = fc.minRed; minGreen = fc.minGreen; minBlue = fc.minBlue; @@ -359,6 +482,7 @@ public class FeatureColour implements FeatureColourI base = fc.base; range = fc.range; isHighToLow = fc.isHighToLow; + attributeName = fc.attributeName; setAboveThreshold(fc.isAboveThreshold()); setBelowThreshold(fc.isBelowThreshold()); setThreshold(fc.getThreshold()); @@ -376,10 +500,54 @@ public class FeatureColour implements FeatureColourI public FeatureColour(FeatureColour fc, float min, float max) { this(fc); - graduatedColour = true; updateBounds(min, max); } + /** + * Constructor for a graduated colour + * + * @param low + * @param high + * @param noValueColour + * @param min + * @param max + */ + public FeatureColour(Color low, Color high, Color noValueColour, + float min, float max) + { + if (low == null) + { + low = Color.white; + } + if (high == null) + { + high = Color.black; + } + graduatedColour = true; + colour = null; + minColour = low; + maxColour = high; + noColour = noValueColour; + threshold = Float.NaN; + isHighToLow = min >= max; + minRed = low.getRed() / 255f; + minGreen = low.getGreen() / 255f; + minBlue = low.getBlue() / 255f; + deltaRed = (high.getRed() / 255f) - minRed; + deltaGreen = (high.getGreen() / 255f) - minGreen; + deltaBlue = (high.getBlue() / 255f) - minBlue; + if (isHighToLow) + { + base = max; + range = min - max; + } + else + { + base = min; + range = max - min; + } + } + @Override public boolean isGraduatedColour() { @@ -418,6 +586,12 @@ public class FeatureColour implements FeatureColourI } @Override + public Color getNoColour() + { + return noColour; + } + + @Override public boolean isColourByLabel() { return colourByLabel; @@ -470,18 +644,6 @@ public class FeatureColour implements FeatureColourI } @Override - public boolean isThresholdMinMax() - { - return thresholdIsMinOrMax; - } - - @Override - public void setThresholdMinMax(boolean b) - { - thresholdIsMinOrMax = b; - } - - @Override public float getThreshold() { return threshold; @@ -506,10 +668,7 @@ public class FeatureColour implements FeatureColourI } /** - * Updates the base and range appropriately for the given minmax range - * - * @param min - * @param max + * {@inheritDoc} */ @Override public void updateBounds(float min, float max) @@ -542,7 +701,10 @@ public class FeatureColour implements FeatureColourI { if (isColourByLabel()) { - return ColorUtils.createColourFromName(feature.getDescription()); + String label = attributeName == null ? feature.getDescription() + : feature.getValueAsString(attributeName); + return label == null ? noColour : ColorUtils + .createColourFromName(label); } if (!isGraduatedColour()) @@ -552,17 +714,31 @@ public class FeatureColour implements FeatureColourI /* * graduated colour case, optionally with threshold - * Float.NaN is assigned minimum visible score colour + * may be based on feature score on an attribute value + * Float.NaN, or no value, is assigned the 'no value' colour */ float scr = feature.getScore(); + if (attributeName != null) + { + try + { + String attVal = feature.getValueAsString(attributeName); + scr = Float.valueOf(attVal); + } catch (Throwable e) + { + scr = Float.NaN; + } + } if (Float.isNaN(scr)) { - return getMinColour(); + return noColour; } + if (isAboveThreshold() && scr <= threshold) { return null; } + if (isBelowThreshold() && scr >= threshold) { return null; @@ -635,21 +811,43 @@ public class FeatureColour implements FeatureColourI else { StringBuilder sb = new StringBuilder(32); - if (isColourByLabel()) + if (isColourByAttribute()) { - sb.append("label"); - if (hasThreshold()) - { - sb.append(BAR).append(BAR).append(BAR); - } + sb.append(ATTRIBUTE).append(BAR); + sb.append( + FeatureMatcher.toAttributeDisplayName(getAttributeName())); + } + else if (isColourByLabel()) + { + sb.append(LABEL); + } + else + { + sb.append(SCORE); } if (isGraduatedColour()) { - sb.append(Format.getHexString(getMinColour())).append(BAR); + sb.append(BAR).append(Format.getHexString(getMinColour())) + .append(BAR); sb.append(Format.getHexString(getMaxColour())).append(BAR); + String noValue = minColour.equals(noColour) ? NO_VALUE_MIN + : (maxColour.equals(noColour) ? NO_VALUE_MAX + : NO_VALUE_NONE); + sb.append(noValue).append(BAR); if (!isAutoScaled()) { - sb.append("abso").append(BAR); + sb.append(ABSOLUTE).append(BAR); + } + } + else + { + /* + * colour by text with score threshold: empty fields for + * minColour and maxColour (not used) + */ + if (hasThreshold()) + { + sb.append(BAR).append(BAR).append(BAR); } } if (hasThreshold() || isGraduatedColour()) @@ -658,11 +856,11 @@ public class FeatureColour implements FeatureColourI sb.append(getMax()).append(BAR); if (isBelowThreshold()) { - sb.append("below").append(BAR).append(getThreshold()); + sb.append(BELOW).append(BAR).append(getThreshold()); } else if (isAboveThreshold()) { - sb.append("above").append(BAR).append(getThreshold()); + sb.append(ABOVE).append(BAR).append(getThreshold()); } else { @@ -674,4 +872,22 @@ public class FeatureColour implements FeatureColourI return String.format("%s\t%s", featureType, colourString); } + @Override + public boolean isColourByAttribute() + { + return attributeName != null; + } + + @Override + public String[] getAttributeName() + { + return attributeName; + } + + @Override + public void setAttributeName(String... name) + { + attributeName = name; + } + } diff --git a/src/jalview/schemes/ResidueProperties.java b/src/jalview/schemes/ResidueProperties.java index 55df1d1..a4e6480 100755 --- a/src/jalview/schemes/ResidueProperties.java +++ b/src/jalview/schemes/ResidueProperties.java @@ -39,14 +39,14 @@ public class ResidueProperties public static final int[] purinepyrimidineIndex; - public static final Map aa3Hash = new HashMap(); + public static final Map aa3Hash = new HashMap<>(); - public static final Map aa2Triplet = new HashMap(); + public static final Map aa2Triplet = new HashMap<>(); - public static final Map nucleotideName = new HashMap(); + public static final Map nucleotideName = new HashMap<>(); // lookup from modified amino acid (e.g. MSE) to canonical form (e.g. MET) - public static final Map modifications = new HashMap(); + public static final Map modifications = new HashMap<>(); static { @@ -496,25 +496,27 @@ public class ResidueProperties * Color.white, // R Color.white, // Y Color.white, // N Color.white, // Gap */ - public static List STOP = Arrays.asList("TGA", "TAA", "TAG"); + public static String STOP = "STOP"; + + public static List STOP_CODONS = Arrays.asList("TGA", "TAA", "TAG"); public static String START = "ATG"; /** * Nucleotide Ambiguity Codes */ - public static final Map ambiguityCodes = new Hashtable(); + public static final Map ambiguityCodes = new Hashtable<>(); /** * Codon triplets with additional symbols for unambiguous codons that include * ambiguity codes */ - public static final Hashtable codonHash2 = new Hashtable(); + public static final Hashtable codonHash2 = new Hashtable<>(); /** * all ambiguity codes for a given base */ - public final static Hashtable> _ambiguityCodes = new Hashtable>(); + public final static Hashtable> _ambiguityCodes = new Hashtable<>(); static { @@ -638,7 +640,7 @@ public class ResidueProperties List codesfor = _ambiguityCodes.get(r); if (codesfor == null) { - _ambiguityCodes.put(r, codesfor = new ArrayList()); + _ambiguityCodes.put(r, codesfor = new ArrayList<>()); } if (!codesfor.contains(acode.getKey())) { @@ -755,27 +757,27 @@ public class ResidueProperties } // Stores residue codes/names and colours and other things - public static Map> propHash = new Hashtable>(); + public static Map> propHash = new Hashtable<>(); - public static Map hydrophobic = new Hashtable(); + public static Map hydrophobic = new Hashtable<>(); - public static Map polar = new Hashtable(); + public static Map polar = new Hashtable<>(); - public static Map small = new Hashtable(); + public static Map small = new Hashtable<>(); - public static Map positive = new Hashtable(); + public static Map positive = new Hashtable<>(); - public static Map negative = new Hashtable(); + public static Map negative = new Hashtable<>(); - public static Map charged = new Hashtable(); + public static Map charged = new Hashtable<>(); - public static Map aromatic = new Hashtable(); + public static Map aromatic = new Hashtable<>(); - public static Map aliphatic = new Hashtable(); + public static Map aliphatic = new Hashtable<>(); - public static Map tiny = new Hashtable(); + public static Map tiny = new Hashtable<>(); - public static Map proline = new Hashtable(); + public static Map proline = new Hashtable<>(); static { @@ -1149,7 +1151,7 @@ public class ResidueProperties String cdn = codonHash2.get(lccodon.toUpperCase()); if ("*".equals(cdn)) { - return "STOP"; + return STOP; } return cdn; } @@ -1157,7 +1159,7 @@ public class ResidueProperties public static Hashtable toDssp3State; static { - toDssp3State = new Hashtable(); + toDssp3State = new Hashtable<>(); toDssp3State.put("H", "H"); toDssp3State.put("E", "E"); toDssp3State.put("C", " "); @@ -2525,7 +2527,7 @@ public class ResidueProperties // / cut here public static void main(String[] args) { - Hashtable> aaProps = new Hashtable>(); + Hashtable> aaProps = new Hashtable<>(); System.out.println("my %aa = {"); // invert property hashes for (String pname : propHash.keySet()) @@ -2536,7 +2538,7 @@ public class ResidueProperties Vector aprops = aaProps.get(rname); if (aprops == null) { - aprops = new Vector(); + aprops = new Vector<>(); aaProps.put(rname, aprops); } Integer hasprop = phash.get(rname); @@ -2578,7 +2580,7 @@ public class ResidueProperties public static List getResidues(boolean forNucleotide, boolean includeAmbiguous) { - List result = new ArrayList(); + List result = new ArrayList<>(); if (forNucleotide) { for (String nuc : nucleotideName.keySet()) diff --git a/src/jalview/util/ColorUtils.java b/src/jalview/util/ColorUtils.java index d4be322..60129fb 100644 --- a/src/jalview/util/ColorUtils.java +++ b/src/jalview/util/ColorUtils.java @@ -25,10 +25,17 @@ 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 myColours = new HashMap<>(); /** * Generates a random color, will mix with input color. Code taken from @@ -260,6 +267,10 @@ public class ColorUtils { return Color.white; } + if (myColours.containsKey(name)) + { + return myColours.get(name); + } int lsize = name.length(); int start = 0; int end = lsize / 3; @@ -291,6 +302,11 @@ public class ColorUtils Color color = new Color(r, g, b); + if (myColours.size() < MAX_CACHE_SIZE) + { + myColours.put(name, color); + } + return color; } diff --git a/src/jalview/util/MapList.java b/src/jalview/util/MapList.java index 4658724..c944345 100644 --- a/src/jalview/util/MapList.java +++ b/src/jalview/util/MapList.java @@ -77,8 +77,8 @@ public class MapList */ public MapList() { - fromShifts = new ArrayList(); - toShifts = new ArrayList(); + fromShifts = new ArrayList<>(); + toShifts = new ArrayList<>(); } /** @@ -347,7 +347,7 @@ public class MapList } boolean changed = false; - List merged = new ArrayList(); + List merged = new ArrayList<>(); int[] lastRange = ranges.get(0); int lastDirection = lastRange[1] >= lastRange[0] ? 1 : -1; lastRange = new int[] { lastRange[0], lastRange[1] }; @@ -803,7 +803,7 @@ public class MapList { return null; } - List ranges = new ArrayList(); + List ranges = new ArrayList<>(); if (fs <= fe) { intv = fs; @@ -1094,8 +1094,33 @@ public class MapList */ public boolean isFromForwardStrand() { + return isForwardStrand(getFromRanges()); + } + + /** + * Returns true if mapping is to forward strand, false if to reverse strand. + * Result is just based on the first 'to' range that is not a single position. + * Default is true unless proven to be false. Behaviour is not well defined if + * the mapping has a mixture of forward and reverse ranges. + * + * @return + */ + public boolean isToForwardStrand() + { + return isForwardStrand(getToRanges()); + } + + /** + * A helper method that returns true unless at least one range has start > end. + * Behaviour is undefined for a mixture of forward and reverse ranges. + * + * @param ranges + * @return + */ + private boolean isForwardStrand(List ranges) + { boolean forwardStrand = true; - for (int[] range : getFromRanges()) + for (int[] range : ranges) { if (range[1] > range[0]) { @@ -1120,4 +1145,63 @@ public class MapList || (fromRatio == 3 && toRatio == 1); } + /** + * Returns a map which is the composite of this one and the input map. That + * is, the output map has the fromRanges of this map, and its toRanges are the + * toRanges of this map as transformed by the input map. + *

    + * 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. + * + *

    +   * 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
    +   * 
    + * + * @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 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); + } + } diff --git a/src/jalview/util/MappingUtils.java b/src/jalview/util/MappingUtils.java index 9c5c109..f5dd883 100644 --- a/src/jalview/util/MappingUtils.java +++ b/src/jalview/util/MappingUtils.java @@ -941,6 +941,34 @@ public final class MappingUtils } /** + * Answers true if range's start-end positions include those of queryRange, + * where either range might be in reverse direction, else false + * + * @param range + * a start-end range + * @param queryRange + * a candidate subrange of range (start2-end2) + * @return + */ + public static boolean rangeContains(int[] range, int[] queryRange) + { + if (range == null || queryRange == null || range.length != 2 + || queryRange.length != 2) + { + /* + * invalid arguments + */ + return false; + } + + int min = Math.min(range[0], range[1]); + int max = Math.max(range[0], range[1]); + + return (min <= queryRange[0] && max >= queryRange[0] + && min <= queryRange[1] && max >= queryRange[1]); + } + + /** * Removes the specified number of positions from the given ranges. Provided * to allow a stop codon to be stripped from a CDS sequence so that it matches * the peptide translation length. diff --git a/src/jalview/util/MathUtils.java b/src/jalview/util/MathUtils.java new file mode 100644 index 0000000..72d46a2 --- /dev/null +++ b/src/jalview/util/MathUtils.java @@ -0,0 +1,22 @@ +package jalview.util; + +public class MathUtils +{ + + /** + * Returns the greatest common divisor of two integers + * + * @param a + * @param b + * @return + */ + public static int gcd(int a, int b) + { + if (b == 0) + { + return Math.abs(a); + } + return gcd(b, a % b); + } + +} diff --git a/src/jalview/util/StringUtils.java b/src/jalview/util/StringUtils.java index b3456aa..2e8ace8 100644 --- a/src/jalview/util/StringUtils.java +++ b/src/jalview/util/StringUtils.java @@ -403,4 +403,45 @@ public class StringUtils } return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); } + + /** + * A helper method that strips off any leading or trailing html and body tags. + * If no html tag is found, then also html-encodes angle bracket characters. + * + * @param text + * @return + */ + public static String stripHtmlTags(String text) + { + if (text == null) + { + return null; + } + String tmp2up = text.toUpperCase(); + int startTag = tmp2up.indexOf(""); + if (startTag > -1) + { + text = text.substring(startTag + 6); + tmp2up = tmp2up.substring(startTag + 6); + } + // is omission of "" intentional here?? + int endTag = tmp2up.indexOf(""); + if (endTag > -1) + { + text = text.substring(0, endTag); + tmp2up = tmp2up.substring(0, endTag); + } + endTag = tmp2up.indexOf(""); + if (endTag > -1) + { + text = text.substring(0, endTag); + } + + if (startTag == -1 && (text.contains("<") || text.contains(">"))) + { + text = text.replaceAll("<", "<"); + text = text.replaceAll(">", ">"); + } + return text; + } } diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java new file mode 100644 index 0000000..8816a7f --- /dev/null +++ b/src/jalview/util/matcher/Condition.java @@ -0,0 +1,102 @@ +package jalview.util.matcher; + +import jalview.util.MessageManager; + +/** + * An enumeration for binary conditions that a user might choose from when + * setting filter or match conditions for values + */ +public enum Condition +{ + Contains(false, true, "Contains"), + NotContains(false, true, "NotContains"), Matches(false, true, "Matches"), + NotMatches(false, true, "NotMatches"), Present(false, false, "Present"), + NotPresent(false, false, "NotPresent"), EQ(true, true, "EQ"), + NE(true, true, "NE"), LT(true, true, "LT"), LE(true, true, "LE"), + GT(true, true, "GT"), GE(true, true, "GE"); + + private boolean numeric; + + private boolean needsAPattern; + + /* + * value used to save a Condition to the + * Jalview project file or restore it from project; + * it should not be changed even if enum names change in future + */ + private String stableName; + + /** + * Answers the enum value whose 'stable name' matches the argument (not case + * sensitive), or null if no match + * + * @param stableName + * @return + */ + public static Condition fromString(String stableName) + { + for (Condition c : values()) + { + if (c.stableName.equalsIgnoreCase(stableName)) + { + return c; + } + } + return null; + } + + /** + * Constructor + * + * @param isNumeric + * @param needsPattern + * @param stablename + */ + Condition(boolean isNumeric, boolean needsPattern, String stablename) + { + numeric = isNumeric; + needsAPattern = needsPattern; + stableName = stablename; + } + + /** + * Answers true if the condition does a numerical comparison, else false + * (string comparison) + * + * @return + */ + public boolean isNumeric() + { + return numeric; + } + + /** + * Answers true if the condition requires a pattern to compare against, else + * false + * + * @return + */ + public boolean needsAPattern() + { + return needsAPattern; + } + + public String getStableName() + { + return stableName; + } + + /** + * Answers a display name for the match condition, suitable for showing in + * drop-down menus. The value may be internationalized using the resource key + * "label.matchCondition_" with the enum name appended. + * + * @return + */ + @Override + public String toString() + { + return MessageManager.getStringOrReturn("label.matchCondition_", + name()); + } +} diff --git a/src/jalview/util/matcher/Matcher.java b/src/jalview/util/matcher/Matcher.java new file mode 100644 index 0000000..0792509 --- /dev/null +++ b/src/jalview/util/matcher/Matcher.java @@ -0,0 +1,251 @@ +package jalview.util.matcher; + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A bean to describe one attribute-based filter + */ +public class Matcher implements MatcherI +{ + /* + * the comparison condition + */ + Condition condition; + + /* + * the string pattern as entered, or the regex, to compare to + * also holds the string form of float value if a numeric condition + */ + String pattern; + + /* + * the pattern in upper case, for non-case-sensitive matching + */ + String uppercasePattern; + + /* + * the compiled regex if using a pattern match condition + * (reserved for possible future enhancement) + */ + Pattern regexPattern; + + /* + * the value to compare to for a numerical condition + */ + float value; + + /** + * Constructor + * + * @param cond + * @param compareTo + * @return + * @throws NumberFormatException + * if a numerical condition is specified with a non-numeric + * comparison value + * @throws NullPointerException + * if a null condition or comparison string is specified + */ + public Matcher(Condition cond, String compareTo) + { + Objects.requireNonNull(cond); + condition = cond; + if (cond.isNumeric()) + { + value = Float.valueOf(compareTo); + pattern = String.valueOf(value); + uppercasePattern = pattern; + } + else + { + pattern = compareTo; + if (pattern != null) + { + uppercasePattern = pattern.toUpperCase(); + } + } + + // if we add regex conditions (e.g. matchesPattern), then + // pattern should hold the raw regex, and + // regexPattern = Pattern.compile(compareTo); + } + + /** + * Constructor for a numerical match condition. Note that if a string + * comparison condition is specified, this will be converted to a comparison + * with the float value as string + * + * @param cond + * @param compareTo + */ + public Matcher(Condition cond, float compareTo) + { + this(cond, String.valueOf(compareTo)); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("incomplete-switch") + @Override + public boolean matches(String val) + { + if (condition.isNumeric()) + { + try + { + /* + * treat a null value (no such attribute) as + * failing any numerical filter condition + */ + return val == null ? false : matches(Float.valueOf(val)); + } catch (NumberFormatException e) + { + return false; + } + } + + /* + * a null value matches a negative condition, fails a positive test + */ + if (val == null) + { + return condition == Condition.NotContains + || condition == Condition.NotMatches + || condition == Condition.NotPresent; + } + + String upper = val.toUpperCase().trim(); + boolean matched = false; + switch(condition) { + case Matches: + matched = upper.equals(uppercasePattern); + break; + case NotMatches: + matched = !upper.equals(uppercasePattern); + break; + case Contains: + matched = upper.indexOf(uppercasePattern) > -1; + break; + case NotContains: + matched = upper.indexOf(uppercasePattern) == -1; + break; + case Present: + matched = true; + break; + default: + break; + } + return matched; + } + + /** + * Applies a numerical comparison match condition + * + * @param f + * @return + */ + @SuppressWarnings("incomplete-switch") + boolean matches(float f) + { + if (!condition.isNumeric()) + { + return matches(String.valueOf(f)); + } + + boolean matched = false; + switch (condition) { + case LT: + matched = f < value; + break; + case LE: + matched = f <= value; + break; + case EQ: + matched = f == value; + break; + case NE: + matched = f != value; + break; + case GT: + matched = f > value; + break; + case GE: + matched = f >= value; + break; + default: + break; + } + + return matched; + } + + /** + * A simple hash function that guarantees that when two objects are equal, + * they have the same hashcode + */ + @Override + public int hashCode() + { + return pattern.hashCode() + condition.hashCode() + (int) value; + } + + /** + * equals is overridden so that we can safely remove Matcher objects from + * collections (e.g. delete an attribute match condition for a feature colour) + */ + @Override + public boolean equals(Object obj) + { + if (obj == null || !(obj instanceof Matcher)) + { + return false; + } + Matcher m = (Matcher) obj; + if (condition != m.condition || value != m.value) + { + return false; + } + if (pattern == null) + { + return m.pattern == null; + } + return uppercasePattern.equals(m.uppercasePattern); + } + + @Override + public Condition getCondition() + { + return condition; + } + + @Override + public String getPattern() + { + return pattern; + } + + @Override + public float getFloatValue() + { + return value; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(condition.toString()).append(" "); + if (condition.isNumeric()) + { + sb.append(pattern); + } + else + { + sb.append("'").append(pattern).append("'"); + } + + return sb.toString(); + } +} diff --git a/src/jalview/util/matcher/MatcherI.java b/src/jalview/util/matcher/MatcherI.java new file mode 100644 index 0000000..ca6d44c --- /dev/null +++ b/src/jalview/util/matcher/MatcherI.java @@ -0,0 +1,18 @@ +package jalview.util.matcher; + +public interface MatcherI +{ + /** + * Answers true if the given value is matched, else false + * + * @param s + * @return + */ + boolean matches(String s); + + Condition getCondition(); + + String getPattern(); + + float getFloatValue(); +} diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 2f30e94..e4d9d88 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -26,6 +26,7 @@ import jalview.api.FeaturesDisplayedI; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.datamodel.features.SequenceFeatures; import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; @@ -48,15 +49,48 @@ import java.util.concurrent.ConcurrentHashMap; public abstract class FeatureRendererModel implements jalview.api.FeatureRenderer { + /* + * a data bean to hold one row of feature settings from the gui + */ + public static class FeatureSettingsBean + { + public final String featureType; - /** + public final FeatureColourI featureColour; + + public final FeatureMatcherSetI filter; + + public final Boolean show; + + public FeatureSettingsBean(String type, FeatureColourI colour, + FeatureMatcherSetI theFilter, Boolean isShown) + { + featureType = type; + featureColour = colour; + filter = theFilter; + show = isShown; + } + } + + /* * global transparency for feature */ protected float transparency = 1.0f; - protected Map featureColours = new ConcurrentHashMap(); + /* + * colour scheme for each feature type + */ + protected Map featureColours = new ConcurrentHashMap<>(); - protected Map featureGroups = new ConcurrentHashMap(); + /* + * visibility flag for each feature group + */ + protected Map featureGroups = new ConcurrentHashMap<>(); + + /* + * filters for each feature type + */ + protected Map featureFilters = new HashMap<>(); protected String[] renderOrder; @@ -100,6 +134,7 @@ public abstract class FeatureRendererModel this.renderOrder = frs.renderOrder; this.featureGroups = frs.featureGroups; this.featureColours = frs.featureColours; + this.featureFilters = frs.featureFilters; this.transparency = frs.transparency; this.featureOrder = frs.featureOrder; if (av != null && av != fr.getViewport()) @@ -156,7 +191,7 @@ public abstract class FeatureRendererModel { av.setFeaturesDisplayed(fdi = new FeaturesDisplayed()); } - List nft = new ArrayList(); + List nft = new ArrayList<>(); for (String featureType : featureTypes) { if (!fdi.isRegistered(featureType)) @@ -192,7 +227,7 @@ public abstract class FeatureRendererModel renderOrder = neworder; } - protected Map minmax = new Hashtable(); + protected Map minmax = new Hashtable<>(); public Map getMinMax() { @@ -271,7 +306,7 @@ public abstract class FeatureRendererModel * include features at the position provided their feature type is * displayed, and feature group is null or marked for display */ - List result = new ArrayList(); + List result = new ArrayList<>(); if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) { return result; @@ -284,9 +319,13 @@ public abstract class FeatureRendererModel List 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); } @@ -320,7 +359,7 @@ public abstract class FeatureRendererModel } FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed(); - Set oldfeatures = new HashSet(); + Set oldfeatures = new HashSet<>(); if (renderOrder != null) { for (int i = 0; i < renderOrder.length; i++) @@ -333,7 +372,7 @@ public abstract class FeatureRendererModel } AlignmentI alignment = av.getAlignment(); - List allfeatures = new ArrayList(); + List allfeatures = new ArrayList<>(); for (int i = 0; i < alignment.getHeight(); i++) { @@ -413,7 +452,7 @@ public abstract class FeatureRendererModel */ if (minmax == null) { - minmax = new Hashtable(); + minmax = new Hashtable<>(); } synchronized (minmax) { @@ -450,7 +489,7 @@ public abstract class FeatureRendererModel */ private void updateRenderOrder(List allFeatures) { - List allfeatures = new ArrayList(allFeatures); + List allfeatures = new ArrayList<>(allFeatures); String[] oldRender = renderOrder; renderOrder = new String[allfeatures.size()]; boolean initOrders = (featureOrder == null); @@ -477,7 +516,8 @@ public abstract class FeatureRendererModel if (mmrange != null) { FeatureColourI fc = featureColours.get(oldRender[j]); - if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()) + if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled() + && !fc.isColourByAttribute()) { fc.updateBounds(mmrange[0][0], mmrange[0][1]); } @@ -507,7 +547,8 @@ public abstract class FeatureRendererModel if (mmrange != null) { FeatureColourI fc = featureColours.get(newf[i]); - if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()) + if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled() + && !fc.isColourByAttribute()) { fc.updateBounds(mmrange[0][0], mmrange[0][1]); } @@ -557,20 +598,11 @@ public abstract class FeatureRendererModel return fc; } - /** - * Returns the configured colour for a particular feature instance. This - * includes calculation of 'colour by label', or of a graduated score colour, - * if applicable. It does not take into account feature visibility or colour - * transparency. Returns null for a score feature whose score value lies - * outside any colour threshold. - * - * @param feature - * @return - */ + @Override public Color getColour(SequenceFeature feature) { FeatureColourI fc = getFeatureStyle(feature.getType()); - return fc.getColor(feature); + return getColor(feature, fc); } /** @@ -582,7 +614,8 @@ public abstract class FeatureRendererModel */ protected boolean showFeatureOfType(String type) { - return type == null ? false : av.getFeaturesDisplayed().isVisible(type); + return type == null ? false : (av.getFeaturesDisplayed() == null ? true + : av.getFeaturesDisplayed().isVisible(type)); } @Override @@ -617,7 +650,7 @@ public abstract class FeatureRendererModel { if (featureOrder == null) { - featureOrder = new Hashtable(); + featureOrder = new Hashtable<>(); } featureOrder.put(type, new Float(position)); return position; @@ -651,32 +684,33 @@ public abstract class FeatureRendererModel * Replace current ordering with new ordering * * @param data - * { String(Type), Colour(Type), Boolean(Displayed) } + * an array of { Type, Colour, Filter, Boolean } * @return true if any visible features have been reordered, else false */ - public boolean setFeaturePriority(Object[][] data) + public boolean setFeaturePriority(FeatureSettingsBean[] data) { return setFeaturePriority(data, true); } /** - * Sets the priority order for features, with the highest priority (displayed - * on top) at the start of the data array + * Sets the priority order for features, with the highest priority (displayed on + * top) at the start of the data array * * @param data - * { String(Type), Colour(Type), Boolean(Displayed) } + * an array of { Type, Colour, Filter, Boolean } * @param visibleNew * when true current featureDisplay list will be cleared - * @return true if any visible features have been reordered or recoloured, - * else false (i.e. no need to repaint) + * @return true if any visible features have been reordered or recoloured, else + * false (i.e. no need to repaint) */ - public boolean setFeaturePriority(Object[][] data, boolean visibleNew) + public boolean setFeaturePriority(FeatureSettingsBean[] data, + boolean visibleNew) { /* * note visible feature ordering and colours before update */ List visibleFeatures = getDisplayedFeatureTypes(); - Map visibleColours = new HashMap( + Map visibleColours = new HashMap<>( getFeatureColours()); FeaturesDisplayedI av_featuresdisplayed = null; @@ -709,9 +743,9 @@ public abstract class FeatureRendererModel { for (int i = 0; i < data.length; i++) { - String type = data[i][0].toString(); - setColour(type, (FeatureColourI) data[i][1]); - if (((Boolean) data[i][2]).booleanValue()) + String type = data[i].featureType; + setColour(type, data[i].featureColour); + if (data[i].show) { av_featuresdisplayed.setVisible(type); } @@ -836,7 +870,7 @@ public abstract class FeatureRendererModel { if (featureGroups != null) { - List gp = new ArrayList(); + List gp = new ArrayList<>(); for (String grp : featureGroups.keySet()) { @@ -882,7 +916,7 @@ public abstract class FeatureRendererModel @Override public Map getDisplayedFeatureCols() { - Map fcols = new Hashtable(); + Map fcols = new Hashtable<>(); if (getViewport().getFeaturesDisplayed() == null) { return fcols; @@ -910,7 +944,7 @@ public abstract class FeatureRendererModel public List getDisplayedFeatureTypes() { List typ = getRenderOrder(); - List displayed = new ArrayList(); + List displayed = new ArrayList<>(); FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed(); if (feature_disp != null) { @@ -931,7 +965,7 @@ public abstract class FeatureRendererModel @Override public List getDisplayedFeatureGroups() { - List _gps = new ArrayList(); + List _gps = new ArrayList<>(); for (String gp : getFeatureGroups()) { if (checkGroupVisibility(gp, false)) @@ -966,7 +1000,7 @@ public abstract class FeatureRendererModel public List findFeaturesAtResidue(SequenceI sequence, int resNo) { - List result = new ArrayList(); + List result = new ArrayList<>(); if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) { return result; @@ -986,7 +1020,7 @@ public abstract class FeatureRendererModel for (SequenceFeature sf : features) { - if (!featureGroupNotShown(sf)) + if (!featureGroupNotShown(sf) && getColour(sf) != null) { result.add(sf); } @@ -995,35 +1029,26 @@ public abstract class FeatureRendererModel } /** - * Removes from the list of features any that have a feature group that is not - * displayed, or duplicate the location of a feature of the same type (unless - * a graduated colour scheme or colour by label is applied). Should be used - * only for features of the same feature colour (which normally implies the - * same feature type). + * Removes from the list of features any that duplicate the location of a + * feature of the same type. Should be used only for features of the same, + * simple, feature colour (which normally implies the same feature type). Does + * not check visibility settings for feature type or feature group. * * @param features - * @param fc */ - public void filterFeaturesForDisplay(List features, - FeatureColourI fc) + public void filterFeaturesForDisplay(List features) { if (features.isEmpty()) { return; } SequenceFeatures.sortFeatures(features, true); - boolean simpleColour = fc == null || fc.isSimpleColour(); SequenceFeature lastFeature = null; Iterator 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 @@ -1031,18 +1056,90 @@ public abstract class FeatureRendererModel * (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 getFeatureFilters() + { + return featureFilters; + } + + @Override + public void setFeatureFilters(Map filters) + { + featureFilters = filters; + } + + @Override + public FeatureMatcherSetI getFeatureFilter(String featureType) + { + return featureFilters.get(featureType); + } + + @Override + public void setFeatureFilter(String featureType, FeatureMatcherSetI filter) + { + if (filter == null || filter.isEmpty()) + { + featureFilters.remove(featureType); + } + else + { + featureFilters.put(featureType, filter); + } + } + + /** + * Answers the colour for the feature, or null if the feature is excluded by + * feature group visibility, by filters, or by colour threshold settings. This + * method does not take feature visibility into account. + * + * @param sf + * @param fc + * @return + */ + public Color getColor(SequenceFeature sf, FeatureColourI fc) + { + /* + * is the feature group displayed? + */ + if (featureGroupNotShown(sf)) + { + return null; + } + + /* + * does the feature pass filters? + */ + if (!featureMatchesFilters(sf)) + { + return null; + } + + return fc.getColor(sf); + } + + /** + * Answers true if there no are filters defined for the feature type, or this + * feature matches the filters. Answers false if the feature fails to match + * filters. + * + * @param sf + * @return + */ + protected boolean featureMatchesFilters(SequenceFeature sf) + { + FeatureMatcherSetI filter = featureFilters.get(sf.getType()); + return filter == null ? true : filter.matches(sf); + } + } diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java index dc2ae11..f594453 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java @@ -21,9 +21,11 @@ package jalview.viewmodel.seqfeatures; import jalview.api.FeatureColourI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.schemes.FeatureColour; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -42,6 +44,11 @@ public class FeatureRendererSettings implements Cloneable */ Map featureColours; + /* + * map of {featureType, filters} + */ + Map featureFilters; + float transparency; Map featureOrder; @@ -72,7 +79,9 @@ public class FeatureRendererSettings implements Cloneable renderOrder = null; featureGroups = new ConcurrentHashMap(); featureColours = new ConcurrentHashMap(); + featureFilters = new HashMap<>(); featureOrder = new ConcurrentHashMap(); + if (fr.renderOrder != null) { this.renderOrder = new String[fr.renderOrder.length]; @@ -100,6 +109,12 @@ public class FeatureRendererSettings implements Cloneable featureColours.put(next, new FeatureColour((FeatureColour) val)); } } + + if (fr.featureFilters != null) + { + this.featureFilters.putAll(fr.featureFilters); + } + this.transparency = fr.transparency; if (fr.featureOrder != null) { diff --git a/test/jalview/analysis/AlignmentUtilsTests.java b/test/jalview/analysis/AlignmentUtilsTests.java index 06b51e6..be6ba60 100644 --- a/test/jalview/analysis/AlignmentUtilsTests.java +++ b/test/jalview/analysis/AlignmentUtilsTests.java @@ -34,6 +34,7 @@ import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.Annotation; import jalview.datamodel.DBRefEntry; +import jalview.datamodel.GeneLociI; import jalview.datamodel.Mapping; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResultsI; @@ -63,6 +64,8 @@ import org.testng.annotations.Test; public class AlignmentUtilsTests { + private static Sequence ts = new Sequence("short", + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm"); @BeforeClass(alwaysRun = true) public void setUpJvOptionPane() @@ -71,9 +74,6 @@ public class AlignmentUtilsTests JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION); } - public static Sequence ts = new Sequence("short", - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm"); - @Test(groups = { "Functional" }) public void testExpandContext() { @@ -263,14 +263,14 @@ public class AlignmentUtilsTests @Test(groups = { "Functional" }) public void testMapProteinAlignmentToCdna_noXrefs() throws IOException { - List protseqs = new ArrayList(); + List protseqs = new ArrayList<>(); protseqs.add(new Sequence("UNIPROT|V12345", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12346", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12347", "SAR")); AlignmentI protein = new Alignment(protseqs.toArray(new SequenceI[3])); protein.setDataset(null); - List dnaseqs = new ArrayList(); + List dnaseqs = new ArrayList<>(); dnaseqs.add(new Sequence("EMBL|A11111", "TCAGCACGC")); // = SAR dnaseqs.add(new Sequence("EMBL|A22222", "GAGATACAA")); // = EIQ dnaseqs.add(new Sequence("EMBL|A33333", "GAAATCCAG")); // = EIQ @@ -507,7 +507,7 @@ public class AlignmentUtilsTests acf.addMap(dna1.getDatasetSequence(), prot1.getDatasetSequence(), map); acf.addMap(dna2.getDatasetSequence(), prot2.getDatasetSequence(), map); acf.addMap(dna3.getDatasetSequence(), prot3.getDatasetSequence(), map); - ArrayList acfs = new ArrayList(); + ArrayList acfs = new ArrayList<>(); acfs.add(acf); protein.setCodonFrames(acfs); @@ -605,14 +605,14 @@ public class AlignmentUtilsTests public void testMapProteinAlignmentToCdna_withStartAndStopCodons() throws IOException { - List protseqs = new ArrayList(); + List protseqs = new ArrayList<>(); protseqs.add(new Sequence("UNIPROT|V12345", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12346", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12347", "SAR")); AlignmentI protein = new Alignment(protseqs.toArray(new SequenceI[3])); protein.setDataset(null); - List dnaseqs = new ArrayList(); + List dnaseqs = new ArrayList<>(); // start + SAR: dnaseqs.add(new Sequence("EMBL|A11111", "ATGTCAGCACGC")); // = EIQ + stop @@ -697,14 +697,14 @@ public class AlignmentUtilsTests @Test(groups = { "Functional" }) public void testMapProteinAlignmentToCdna_withXrefs() throws IOException { - List protseqs = new ArrayList(); + List protseqs = new ArrayList<>(); protseqs.add(new Sequence("UNIPROT|V12345", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12346", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12347", "SAR")); AlignmentI protein = new Alignment(protseqs.toArray(new SequenceI[3])); protein.setDataset(null); - List dnaseqs = new ArrayList(); + List dnaseqs = new ArrayList<>(); dnaseqs.add(new Sequence("EMBL|A11111", "TCAGCACGC")); // = SAR dnaseqs.add(new Sequence("EMBL|A22222", "ATGGAGATACAA")); // = start + EIQ dnaseqs.add(new Sequence("EMBL|A33333", "GAAATCCAG")); // = EIQ @@ -774,14 +774,14 @@ public class AlignmentUtilsTests public void testMapProteinAlignmentToCdna_prioritiseXrefs() throws IOException { - List protseqs = new ArrayList(); + List protseqs = new ArrayList<>(); protseqs.add(new Sequence("UNIPROT|V12345", "EIQ")); protseqs.add(new Sequence("UNIPROT|V12346", "EIQ")); AlignmentI protein = new Alignment( protseqs.toArray(new SequenceI[protseqs.size()])); protein.setDataset(null); - List dnaseqs = new ArrayList(); + List dnaseqs = new ArrayList<>(); dnaseqs.add(new Sequence("EMBL|A11111", "GAAATCCAG")); // = EIQ dnaseqs.add(new Sequence("EMBL|A22222", "GAAATTCAG")); // = EIQ AlignmentI cdna = new Alignment(dnaseqs.toArray(new SequenceI[dnaseqs @@ -848,8 +848,8 @@ public class AlignmentUtilsTests al.addAnnotation(ann4); // Temp for seq1 al.addAnnotation(ann5); // Temp for seq2 al.addAnnotation(ann6); // Temp for no sequence - List types = new ArrayList(); - List scope = new ArrayList(); + List types = new ArrayList<>(); + List scope = new ArrayList<>(); /* * Set all sequence related Structure to hidden (ann1, ann2) @@ -1044,14 +1044,18 @@ public class AlignmentUtilsTests dna.addCodonFrame(acf); /* - * In this case, mappings originally came from matching Uniprot accessions - so need an xref on dna involving those regions. These are normally constructed from CDS annotation + * In this case, mappings originally came from matching Uniprot accessions + * - so need an xref on dna involving those regions. + * These are normally constructed from CDS annotation */ DBRefEntry dna1xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep1", new Mapping(mapfordna1)); - dna1.getDatasetSequence().addDBRef(dna1xref); + dna1.addDBRef(dna1xref); + assertEquals(2, dna1.getDBRefs().length); // to self and to pep1 DBRefEntry dna2xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep2", new Mapping(mapfordna2)); - dna2.getDatasetSequence().addDBRef(dna2xref); + dna2.addDBRef(dna2xref); + assertEquals(2, dna2.getDBRefs().length); // to self and to pep2 /* * execute method under test: @@ -1106,6 +1110,38 @@ public class AlignmentUtilsTests assertEquals(cdsMapping.getInverse(), dbref.getMap().getMap()); /* + * verify cDNA has added a dbref with mapping to CDS + */ + assertEquals(3, dna1.getDBRefs().length); + DBRefEntry dbRefEntry = dna1.getDBRefs()[2]; + assertSame(cds1Dss, dbRefEntry.getMap().getTo()); + MapList dnaToCdsMapping = new MapList(new int[] { 4, 6, 10, 12 }, + new int[] { 1, 6 }, 1, 1); + assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap()); + assertEquals(3, dna2.getDBRefs().length); + dbRefEntry = dna2.getDBRefs()[2]; + assertSame(cds2Dss, dbRefEntry.getMap().getTo()); + dnaToCdsMapping = new MapList(new int[] { 1, 3, 7, 9, 13, 15 }, + new int[] { 1, 9 }, 1, 1); + assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap()); + + /* + * verify CDS has added a dbref with mapping to cDNA + */ + assertEquals(2, cds1Dss.getDBRefs().length); + dbRefEntry = cds1Dss.getDBRefs()[1]; + assertSame(dna1.getDatasetSequence(), dbRefEntry.getMap().getTo()); + MapList cdsToDnaMapping = new MapList(new int[] { 1, 6 }, new int[] { + 4, 6, 10, 12 }, 1, 1); + assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap()); + assertEquals(2, cds2Dss.getDBRefs().length); + dbRefEntry = cds2Dss.getDBRefs()[1]; + assertSame(dna2.getDatasetSequence(), dbRefEntry.getMap().getTo()); + cdsToDnaMapping = new MapList(new int[] { 1, 9 }, new int[] { 1, 3, 7, + 9, 13, 15 }, 1, 1); + assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap()); + + /* * Verify mappings from CDS to peptide, cDNA to CDS, and cDNA to peptide * the mappings are on the shared alignment dataset * 6 mappings, 2*(DNA->CDS), 2*(DNA->Pep), 2*(CDS->Pep) @@ -1747,7 +1783,7 @@ public class AlignmentUtilsTests map = new MapList(new int[] { 9, 11 }, new int[] { 2, 2 }, 3, 1); acf.addMap(dna3.getDatasetSequence(), prot3.getDatasetSequence(), map); - ArrayList acfs = new ArrayList(); + ArrayList acfs = new ArrayList<>(); acfs.add(acf); protein.setCodonFrames(acfs); @@ -1896,6 +1932,7 @@ public class AlignmentUtilsTests sf6.setValue("alleles", "g, a"); // should force to upper-case sf6.setValue("ID", "sequence_variant:rs758803216"); dna.addSequenceFeature(sf6); + SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15, 15, 0f, null); sf7.setValue("alleles", "A, T"); @@ -1985,6 +2022,7 @@ public class AlignmentUtilsTests * variants: * GAA -> E source: Ensembl * CAA -> Q source: dbSNP + * TAA -> STOP source: dnSNP * AAG synonymous source: COSMIC * AAT -> N source: Ensembl * ...TTC synonymous source: dbSNP @@ -2000,39 +2038,50 @@ public class AlignmentUtilsTests String ensembl = "Ensembl"; String dbSnp = "dbSNP"; String cosmic = "COSMIC"; + SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1, 0f, ensembl); - sf1.setValue("alleles", "A,G"); // GAA -> E + sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E sf1.setValue("ID", "var1.125A>G"); + SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1, 0f, dbSnp); - sf2.setValue("alleles", "A,C"); // CAA -> Q + sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q sf2.setValue("ID", "var2"); sf2.setValue("clinical_significance", "Dodgy"); - SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 3, 3, - 0f, cosmic); - sf3.setValue("alleles", "A,G"); // synonymous + + SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1, + 0f, dbSnp); + sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon sf3.setValue("ID", "var3"); - sf3.setValue("clinical_significance", "None"); + sf3.setValue("clinical_significance", "Bad"); + SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3, + 0f, cosmic); + sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous + sf4.setValue("ID", "var4"); + sf4.setValue("clinical_significance", "None"); + + SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3, 0f, ensembl); - sf4.setValue("alleles", "A,T"); // AAT -> N - sf4.setValue("ID", "sequence_variant:var4"); // prefix gets stripped off - sf4.setValue("clinical_significance", "Benign"); - SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 6, 6, + sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N + sf5.setValue("ID", "sequence_variant:var5"); // prefix gets stripped off + sf5.setValue("clinical_significance", "Benign"); + + SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6, 0f, dbSnp); - sf5.setValue("alleles", "T,C"); // synonymous - sf5.setValue("ID", "var5"); - sf5.setValue("clinical_significance", "Bad"); - SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 8, 8, - 0f, cosmic); - sf6.setValue("alleles", "C,A,G"); // CAC,CGC -> H,R + sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous sf6.setValue("ID", "var6"); - sf6.setValue("clinical_significance", "Good"); - List codon1Variants = new ArrayList(); - List codon2Variants = new ArrayList(); - List codon3Variants = new ArrayList(); + SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8, + 0f, cosmic); + sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R + sf7.setValue("ID", "var7"); + sf7.setValue("clinical_significance", "Good"); + + List codon1Variants = new ArrayList<>(); + List codon2Variants = new ArrayList<>(); + List codon3Variants = new ArrayList<>(); List codonVariants[] = new ArrayList[3]; codonVariants[0] = codon1Variants; codonVariants[1] = codon2Variants; @@ -2043,10 +2092,11 @@ public class AlignmentUtilsTests */ codon1Variants.add(new DnaVariant("A", sf1)); codon1Variants.add(new DnaVariant("A", sf2)); + codon1Variants.add(new DnaVariant("A", sf3)); codon2Variants.add(new DnaVariant("A")); - codon2Variants.add(new DnaVariant("A")); - codon3Variants.add(new DnaVariant("A", sf3)); + // codon2Variants.add(new DnaVariant("A")); codon3Variants.add(new DnaVariant("A", sf4)); + codon3Variants.add(new DnaVariant("A", sf5)); AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants); /* @@ -2057,7 +2107,7 @@ public class AlignmentUtilsTests codon3Variants.clear(); codon1Variants.add(new DnaVariant("T")); codon2Variants.add(new DnaVariant("T")); - codon3Variants.add(new DnaVariant("T", sf5)); + codon3Variants.add(new DnaVariant("T", sf6)); AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants); /* @@ -2067,7 +2117,7 @@ public class AlignmentUtilsTests codon2Variants.clear(); codon3Variants.clear(); codon1Variants.add(new DnaVariant("C")); - codon2Variants.add(new DnaVariant("C", sf6)); + codon2Variants.add(new DnaVariant("C", sf7)); codon3Variants.add(new DnaVariant("C")); AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants); @@ -2075,35 +2125,43 @@ public class AlignmentUtilsTests * verify added sequence features for * var1 K -> E Ensembl * var2 K -> Q dbSNP - * var4 K -> N Ensembl - * var6 P -> H COSMIC - * var6 P -> R COSMIC + * var3 K -> stop + * var4 synonymous + * var5 K -> N Ensembl + * var6 synonymous + * var7 P -> H COSMIC + * var8 P -> R COSMIC */ List sfs = peptide.getSequenceFeatures(); SequenceFeatures.sortFeatures(sfs, true); - assertEquals(5, sfs.size()); + assertEquals(8, sfs.size()); /* * features are sorted by start position ascending, but in no * particular order where start positions match; asserts here * simply match the data returned (the order is not important) */ + // AAA -> AAT -> K/N SequenceFeature sf = sfs.get(0); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); + assertEquals("nonsynonymous_variant", sf.getType()); assertEquals("p.Lys1Asn", sf.getDescription()); - assertEquals("var4", sf.getValue("ID")); + assertEquals("var5", sf.getValue("ID")); assertEquals("Benign", sf.getValue("clinical_significance")); - assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes()); + assertEquals("ID=var5;clinical_significance=Benign", + sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4", + "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5", sf.links.get(0)); assertEquals(ensembl, sf.getFeatureGroup()); + // AAA -> CAA -> K/Q sf = sfs.get(1); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); + assertEquals("nonsynonymous_variant", sf.getType()); assertEquals("p.Lys1Gln", sf.getDescription()); assertEquals("var2", sf.getValue("ID")); assertEquals("Dodgy", sf.getValue("clinical_significance")); @@ -2114,9 +2172,11 @@ public class AlignmentUtilsTests sf.links.get(0)); assertEquals(dbSnp, sf.getFeatureGroup()); + // AAA -> GAA -> K/E sf = sfs.get(2); assertEquals(1, sf.getBegin()); assertEquals(1, sf.getEnd()); + assertEquals("nonsynonymous_variant", sf.getType()); assertEquals("p.Lys1Glu", sf.getDescription()); assertEquals("var1.125A>G", sf.getValue("ID")); assertNull(sf.getValue("clinical_significance")); @@ -2128,30 +2188,79 @@ public class AlignmentUtilsTests sf.links.get(0)); assertEquals(ensembl, sf.getFeatureGroup()); + // AAA -> TAA -> stop codon sf = sfs.get(3); + assertEquals(1, sf.getBegin()); + assertEquals(1, sf.getEnd()); + assertEquals("stop_gained", sf.getType()); + assertEquals("TAA", sf.getDescription()); + assertEquals("var3", sf.getValue("ID")); + assertEquals("Bad", sf.getValue("clinical_significance")); + assertEquals("ID=var3;clinical_significance=Bad", sf.getAttributes()); + assertEquals(1, sf.links.size()); + assertEquals( + "TAA var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3", + sf.links.get(0)); + assertEquals(dbSnp, sf.getFeatureGroup()); + + // AAA -> AAG synonymous + sf = sfs.get(4); + assertEquals(1, sf.getBegin()); + assertEquals(1, sf.getEnd()); + assertEquals("synonymous_variant", sf.getType()); + assertEquals("AAG", sf.getDescription()); + assertEquals("var4", sf.getValue("ID")); + assertEquals("None", sf.getValue("clinical_significance")); + assertEquals("ID=var4;clinical_significance=None", sf.getAttributes()); + assertEquals(1, sf.links.size()); + assertEquals( + "AAG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4", + sf.links.get(0)); + assertEquals(cosmic, sf.getFeatureGroup()); + + // TTT -> TTC synonymous + sf = sfs.get(5); + assertEquals(2, sf.getBegin()); + assertEquals(2, sf.getEnd()); + assertEquals("synonymous_variant", sf.getType()); + assertEquals("TTC", sf.getDescription()); + assertEquals("var6", sf.getValue("ID")); + assertNull(sf.getValue("clinical_significance")); + assertEquals("ID=var6", sf.getAttributes()); + assertEquals(1, sf.links.size()); + assertEquals( + "TTC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", + sf.links.get(0)); + assertEquals(dbSnp, sf.getFeatureGroup()); + + // var7 generates two distinct protein variant features (two alleles) + // CCC -> CGC -> P/R + sf = sfs.get(6); assertEquals(3, sf.getBegin()); assertEquals(3, sf.getEnd()); + assertEquals("nonsynonymous_variant", sf.getType()); assertEquals("p.Pro3Arg", sf.getDescription()); - assertEquals("var6", sf.getValue("ID")); + assertEquals("var7", sf.getValue("ID")); assertEquals("Good", sf.getValue("clinical_significance")); - assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes()); + assertEquals("ID=var7;clinical_significance=Good", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", + "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", sf.links.get(0)); assertEquals(cosmic, sf.getFeatureGroup()); - // var5 generates two distinct protein variant features - sf = sfs.get(4); + // CCC -> CAC -> P/H + sf = sfs.get(7); assertEquals(3, sf.getBegin()); assertEquals(3, sf.getEnd()); + assertEquals("nonsynonymous_variant", sf.getType()); assertEquals("p.Pro3His", sf.getDescription()); - assertEquals("var6", sf.getValue("ID")); + assertEquals("var7", sf.getValue("ID")); assertEquals("Good", sf.getValue("clinical_significance")); - assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes()); + assertEquals("ID=var7;clinical_significance=Good", sf.getAttributes()); assertEquals(1, sf.links.size()); assertEquals( - "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", + "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", sf.links.get(0)); assertEquals(cosmic, sf.getFeatureGroup()); } @@ -2272,7 +2381,7 @@ public class AlignmentUtilsTests seq1.createDatasetSequence(); Mapping mapping = new Mapping(seq1, new MapList( new int[] { 3, 6, 9, 10 }, new int[] { 1, 6 }, 1, 1)); - Map> map = new TreeMap>(); + Map> map = new TreeMap<>(); AlignmentUtils.addMappedPositions(seq1, from, mapping, map); /* @@ -2304,7 +2413,7 @@ public class AlignmentUtilsTests seq1.createDatasetSequence(); Mapping mapping = new Mapping(seq1, new MapList( new int[] { 3, 6, 9, 10 }, new int[] { 1, 6 }, 1, 1)); - Map> map = new TreeMap>(); + Map> map = new TreeMap<>(); AlignmentUtils.addMappedPositions(seq1, from, mapping, map); /* @@ -2533,6 +2642,70 @@ public class AlignmentUtilsTests assertEquals(s_as3, uas3.getSequenceAsString()); } + @Test(groups = { "Functional" }) + public void testTransferGeneLoci() + { + SequenceI from = new Sequence("transcript", + "aaacccgggTTTAAACCCGGGtttaaacccgggttt"); + SequenceI to = new Sequence("CDS", "TTTAAACCCGGG"); + MapList map = new MapList(new int[] { 1, 12 }, new int[] { 10, 21 }, 1, + 1); + + /* + * first with nothing to transfer + */ + AlignmentUtils.transferGeneLoci(from, map, to); + assertNull(to.getGeneLoci()); + + /* + * next with gene loci set on 'from' sequence + */ + int[] exons = new int[] { 100, 105, 155, 164, 210, 229 }; + MapList geneMap = new MapList(new int[] { 1, 36 }, exons, 1, 1); + from.setGeneLoci("human", "GRCh38", "7", geneMap); + AlignmentUtils.transferGeneLoci(from, map, to); + + GeneLociI toLoci = to.getGeneLoci(); + assertNotNull(toLoci); + // DBRefEntry constructor upper-cases 'source' + assertEquals("HUMAN", toLoci.getSpeciesId()); + assertEquals("GRCh38", toLoci.getAssemblyId()); + assertEquals("7", toLoci.getChromosomeId()); + + /* + * transcript 'exons' are 1-6, 7-16, 17-36 + * CDS 1:12 is transcript 10-21 + * transcript 'CDS' is 10-16, 17-21 + * which is 'gene' 158-164, 210-214 + */ + MapList toMap = toLoci.getMap(); + assertEquals(1, toMap.getFromRanges().size()); + assertEquals(2, toMap.getFromRanges().get(0).length); + assertEquals(1, toMap.getFromRanges().get(0)[0]); + assertEquals(12, toMap.getFromRanges().get(0)[1]); + assertEquals(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 */ @@ -2599,5 +2772,4 @@ public class AlignmentUtilsTests assertEquals("[[3, 3], [8, 12]]", Arrays.deepToString(ml.getFromRanges().toArray())); } - } diff --git a/test/jalview/controller/AlignViewControllerTest.java b/test/jalview/controller/AlignViewControllerTest.java index 2e89b0e..efee93b 100644 --- a/test/jalview/controller/AlignViewControllerTest.java +++ b/test/jalview/controller/AlignViewControllerTest.java @@ -25,6 +25,8 @@ import static org.testng.AssertJUnit.assertTrue; import jalview.analysis.Finder; import jalview.api.AlignViewControllerI; +import jalview.api.FeatureColourI; +import jalview.datamodel.Alignment; import jalview.datamodel.SearchResults; import jalview.datamodel.SearchResultsI; import jalview.datamodel.Sequence; @@ -35,7 +37,9 @@ import jalview.gui.AlignFrame; import jalview.gui.JvOptionPane; import jalview.io.DataSourceType; import jalview.io.FileLoader; +import jalview.schemes.FeatureColour; +import java.awt.Color; import java.util.Arrays; import java.util.BitSet; @@ -67,13 +71,14 @@ public class AlignViewControllerTest null)); seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f, null)); - seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f, + seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, + 10f, null)); seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15, - 0f, null)); + 10f, null)); // disulfide bond is a 'contact feature' - only select its 'start' and 'end' - seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", 8, 12, - 0f, null)); + seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", + 8, 12, 0f, null)); /* * select the first five columns --> Metal in seq1 cols 4-5 @@ -86,9 +91,18 @@ public class AlignViewControllerTest sg.addSequence(seq3, false); sg.addSequence(seq4, false); + /* + * set features visible on a viewport as only visible features are selected + */ + AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1, + seq2, seq3, seq4 }), 100, 100); + af.getFeatureRenderer().findAllFeatures(true); + + AlignViewController avc = new AlignViewController(af, af.getViewport(), + af.alignPanel); + BitSet bs = new BitSet(); - int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, - bs); + int seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(1, seqCount); assertEquals(2, bs.cardinality()); assertTrue(bs.get(3)); // base 0 @@ -99,7 +113,7 @@ public class AlignViewControllerTest */ sg.setEndRes(6); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(2, seqCount); assertEquals(4, bs.cardinality()); assertTrue(bs.get(3)); @@ -113,7 +127,7 @@ public class AlignViewControllerTest sg.setStartRes(13); sg.setEndRes(13); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(1, seqCount); assertEquals(1, bs.cardinality()); assertTrue(bs.get(13)); @@ -124,18 +138,35 @@ public class AlignViewControllerTest sg.setStartRes(17); sg.setEndRes(19); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); assertEquals(0, seqCount); assertEquals(0, bs.cardinality()); /* + * threshold Metal to hide where score < 5 + * seq1 feature in columns 4-6 is hidden + * seq2 feature in columns 6-7 is shown + */ + FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f); + fc.setAboveThreshold(true); + fc.setThreshold(5f); + af.getFeatureRenderer().setColour("Metal", fc); + sg.setStartRes(0); + sg.setEndRes(6); + bs.clear(); + seqCount = avc.findColumnsWithFeature("Metal", sg, bs); + assertEquals(1, seqCount); + assertEquals(2, bs.cardinality()); + assertTrue(bs.get(5)); + assertTrue(bs.get(6)); + + /* * columns 11-13 should not match disulfide bond at 8/12 */ sg.setStartRes(10); sg.setEndRes(12); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("disulfide bond", - sg, bs); + seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs); assertEquals(0, seqCount); assertEquals(0, bs.cardinality()); @@ -145,8 +176,7 @@ public class AlignViewControllerTest sg.setStartRes(5); sg.setEndRes(17); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("disulfide bond", - sg, bs); + seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs); assertEquals(1, seqCount); assertEquals(2, bs.cardinality()); assertTrue(bs.get(8)); @@ -158,7 +188,7 @@ public class AlignViewControllerTest sg.setStartRes(0); sg.setEndRes(19); bs.clear(); - seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs); + seqCount = avc.findColumnsWithFeature("Pfam", sg, bs); assertEquals(0, seqCount); assertEquals(0, bs.cardinality()); } diff --git a/test/jalview/datamodel/SequenceFeatureTest.java b/test/jalview/datamodel/SequenceFeatureTest.java index fbeb365..c955979 100644 --- a/test/jalview/datamodel/SequenceFeatureTest.java +++ b/test/jalview/datamodel/SequenceFeatureTest.java @@ -273,4 +273,47 @@ public class SequenceFeatureTest "group"); assertTrue(sf.isContactFeature()); } + + @Test(groups = { "Functional" }) + public void testGetDetailsReport() + { + // single locus, no group, no score + SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null); + String expected = "
    " + + "" + + "
    Typevariant
    Start/end22
    DescriptionG,C
    "; + assertEquals(expected, sf.getDetailsReport()); + + // contact feature + sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31, + null); + expected = "
    " + + "" + + "
    TypeDisulphide Bond
    Start/end28:31
    Descriptiona description
    "; + assertEquals(expected, sf.getDetailsReport()); + + sf = new SequenceFeature("variant", "G,C", 22, 33, + 12.5f, "group"); + sf.setValue("Parent", "ENSG001"); + sf.setValue("Child", "ENSP002"); + expected = "
    " + + "" + + "" + + "" + + "" + + "" + + "
    Typevariant
    Start/end22-33
    DescriptionG,C
    Score12.5
    Groupgroup
    ChildENSP002
    ParentENSG001
    "; + assertEquals(expected, sf.getDetailsReport()); + + /* + * feature with embedded html link in description + */ + String desc = "Fer2 Status: True Positive Pfam 8_8"; + sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot"); + expected = "
    " + + "" + + "" + + "
    TypePfam
    Start/end8-83
    DescriptionFer2 Status: True Positive Pfam 8_8
    GroupUniprot
    "; + assertEquals(expected, sf.getDetailsReport()); + } } diff --git a/test/jalview/datamodel/features/FeatureAttributesTest.java b/test/jalview/datamodel/features/FeatureAttributesTest.java new file mode 100644 index 0000000..e47c787 --- /dev/null +++ b/test/jalview/datamodel/features/FeatureAttributesTest.java @@ -0,0 +1,131 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.features.FeatureAttributes.Datatype; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import junit.extensions.PA; + +public class FeatureAttributesTest +{ + + /** + * clear down attributes map before tests + */ + @BeforeClass + public void setUp() + { + FeatureAttributes fa = FeatureAttributes.getInstance(); + ((Map) PA.getValue(fa, "attributes")).clear(); + } + + /** + * 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 comp = (Comparator) PA.getValue(fa, + "comparator"); + + assertEquals( + comp.compare(new String[] { "CSQ" }, new String[] { "csq" }), 0); + + assertTrue(comp.compare(new String[] { "CSQ", "a" }, + new String[] { "csq" }) > 0); + + assertTrue(comp.compare(new String[] { "CSQ" }, new String[] { "csq", + "b" }) < 0); + + assertTrue(comp.compare(new String[] { "CSQ", "AF" }, new String[] { + "csq", "ac" }) > 0); + + assertTrue(comp.compare(new String[] { "CSQ", "ac" }, new String[] { + "csq", "AF" }) < 0); + } + + @Test(groups = "Functional") + public void testGetMinMax() + { + SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20, + "group"); + FeatureAttributes fa = FeatureAttributes.getInstance(); + assertNull(fa.getMinMax("Pfam", "kd")); + sf.setValue("domain", "xyz"); + assertNull(fa.getMinMax("Pfam", "kd")); + sf.setValue("kd", "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 csq = new HashMap<>(); + csq.put("AF", "-3"); + sf.setValue("CSQ", csq); + assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"), + new float[] + { -3f, -3f }); + csq.put("AF", "4"); + sf.setValue("CSQ", csq); + assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"), + new float[] + { -3f, 4f }); + } + + /** + * Test the method that returns an attribute description, provided it is + * recorded and unique + */ + @Test(groups = "Functional") + public void testGetDescription() + { + FeatureAttributes fa = FeatureAttributes.getInstance(); + // with no description returns null + assertNull(fa.getDescription("Pfam", "kd")); + // with a unique description, returns that value + fa.addDescription("Pfam", "desc1", "kd"); + assertEquals(fa.getDescription("Pfam", "kd"), "desc1"); + // with ambiguous description, returns null + fa.addDescription("Pfam", "desc2", "kd"); + assertNull(fa.getDescription("Pfam", "kd")); + } + + @Test(groups = "Functional") + public void testDatatype() + { + FeatureAttributes fa = FeatureAttributes.getInstance(); + assertNull(fa.getDatatype("Pfam", "kd")); + SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20, + "group"); + sf.setValue("kd", "-1"); + sf.setValue("domain", "Metal"); + sf.setValue("phase", "1"); + sf.setValue("phase", "reverse"); + assertEquals(fa.getDatatype("Pfam", "kd"), Datatype.Number); + assertEquals(fa.getDatatype("Pfam", "domain"), Datatype.Character); + assertEquals(fa.getDatatype("Pfam", "phase"), Datatype.Mixed); + } +} diff --git a/test/jalview/datamodel/features/FeatureMatcherSetTest.java b/test/jalview/datamodel/features/FeatureMatcherSetTest.java new file mode 100644 index 0000000..a2d2c9a --- /dev/null +++ b/test/jalview/datamodel/features/FeatureMatcherSetTest.java @@ -0,0 +1,419 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import jalview.datamodel.SequenceFeature; +import jalview.util.matcher.Condition; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import org.testng.annotations.Test; + +public class FeatureMatcherSetTest +{ + @Test(groups = "Functional") + public void testMatches_byAttribute() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2", + "AF"); + FeatureMatcherSetI fms = new FeatureMatcherSet(); + fms.and(fm); + SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp"); + assertFalse(fms.matches(sf)); + sf.setValue("AF", "foobar"); + assertFalse(fms.matches(sf)); + sf.setValue("AF", "-2"); + assertTrue(fms.matches(sf)); + sf.setValue("AF", "-1"); + assertTrue(fms.matches(sf)); + sf.setValue("AF", "-3"); + assertFalse(fms.matches(sf)); + sf.setValue("AF", ""); + assertFalse(fms.matches(sf)); + + /* + * a string pattern matcher + */ + fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF"); + fms = new FeatureMatcherSet(); + fms.and(fm); + assertFalse(fms.matches(sf)); + sf.setValue("AF", "raining cats and dogs"); + assertTrue(fms.matches(sf)); + } + + @Test(groups = "Functional") + public void testAnd() + { + // condition1: AF value contains "dog" (matches) + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains, + "dog", "AF"); + // condition 2: CSQ value does not contain "how" (does not match) + FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "how", "CSQ"); + + SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12, + 6.2f, "grp"); + sf.setValue("AF", "raining cats and dogs"); + sf.setValue("CSQ", "showers"); + + assertTrue(fm1.matches(sf)); + assertFalse(fm2.matches(sf)); + + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass + fms.and(fm1); + assertTrue(fms.matches(sf)); + fms.and(fm2); + assertFalse(fms.matches(sf)); + + /* + * OR a failed attribute condition with a matched label condition + */ + fms = new FeatureMatcherSet(); + fms.and(fm2); + assertFalse(fms.matches(sf)); + FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains, + "Helix"); + fms.or(byLabelPass); + assertTrue(fms.matches(sf)); + + /* + * OR a failed attribute condition with a failed score condition + */ + fms = new FeatureMatcherSet(); + fms.and(fm2); + assertFalse(fms.matches(sf)); + FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT, + "5.9"); + fms.or(byScoreFail); + assertFalse(fms.matches(sf)); + + /* + * OR failed attribute and score conditions with matched label condition + */ + fms = new FeatureMatcherSet(); + fms.or(fm2); + fms.or(byScoreFail); + assertFalse(fms.matches(sf)); + fms.or(byLabelPass); + assertTrue(fms.matches(sf)); + } + + @Test(groups = "Functional") + public void testToString() + { + Locale.setDefault(Locale.ENGLISH); + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm1.toString(), "AF < 1.2"); + + FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "path", "CLIN_SIG"); + assertEquals(fm2.toString(), "CLIN_SIG does not contain 'path'"); + + /* + * AND them + */ + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertEquals(fms.toString(), ""); + fms.and(fm1); + assertEquals(fms.toString(), "AF < 1.2"); + fms.and(fm2); + assertEquals(fms.toString(), + "(AF < 1.2) and (CLIN_SIG does not contain 'path')"); + + /* + * OR them + */ + fms = new FeatureMatcherSet(); + assertEquals(fms.toString(), ""); + fms.or(fm1); + assertEquals(fms.toString(), "AF < 1.2"); + fms.or(fm2); + assertEquals(fms.toString(), + "(AF < 1.2) or (CLIN_SIG does not contain 'path')"); + + try + { + fms.and(fm1); + fail("Expected exception"); + } catch (IllegalStateException e) + { + // expected + } + } + + @Test(groups = "Functional") + public void testOr() + { + // condition1: AF value contains "dog" (matches) + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains, + "dog", "AF"); + // condition 2: CSQ value does not contain "how" (does not match) + FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "how", "CSQ"); + + SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp"); + sf.setValue("AF", "raining cats and dogs"); + sf.setValue("CSQ", "showers"); + + assertTrue(fm1.matches(sf)); + assertFalse(fm2.matches(sf)); + + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass + fms.or(fm1); + assertTrue(fms.matches(sf)); + fms.or(fm2); + assertTrue(fms.matches(sf)); // true or false makes true + + fms = new FeatureMatcherSet(); + fms.or(fm2); + assertFalse(fms.matches(sf)); + fms.or(fm1); + assertTrue(fms.matches(sf)); // false or true makes true + + try + { + fms.and(fm2); + fail("Expected exception"); + } catch (IllegalStateException e) + { + // expected + } + } + + @Test(groups = "Functional") + public void testIsEmpty() + { + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0", + "AF"); + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertTrue(fms.isEmpty()); + fms.and(fm); + assertFalse(fms.isEmpty()); + } + + @Test(groups = "Functional") + public void testGetMatchers() + { + FeatureMatcherSetI fms = new FeatureMatcherSet(); + + /* + * empty iterable: + */ + Iterator 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 csq = new HashMap<>(); + sf.setValue("CSQ", csq); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "-2"); + assertTrue(fms.matches(sf)); + csq.put("Consequence", "-1"); + assertTrue(fms.matches(sf)); + csq.put("Consequence", "-3"); + assertFalse(fms.matches(sf)); + csq.put("Consequence", ""); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "junk"); + assertFalse(fms.matches(sf)); + + /* + * a string pattern matcher + */ + fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ", + "Consequence"); + fms = new FeatureMatcherSet(); + fms.and(fm); + assertFalse(fms.matches(sf)); + csq.put("PolyPhen", "damaging"); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "damaging"); + assertFalse(fms.matches(sf)); + csq.put("Consequence", "Catastrophic"); + assertTrue(fms.matches(sf)); + } + + /** + * Tests for toStableString which (unlike toString) does not i18n the + * conditions + * + * @see FeatureMatcherTest#testToStableString() + */ + @Test(groups = "Functional") + public void testToStableString() + { + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm1.toStableString(), "AF LT 1.2"); + + FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "path", "CLIN_SIG"); + assertEquals(fm2.toStableString(), "CLIN_SIG NotContains path"); + + /* + * AND them + */ + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertEquals(fms.toStableString(), ""); + fms.and(fm1); + // no brackets needed if a single condition + assertEquals(fms.toStableString(), "AF LT 1.2"); + // brackets if more than one condition + fms.and(fm2); + assertEquals(fms.toStableString(), + "(AF LT 1.2) AND (CLIN_SIG NotContains path)"); + + /* + * OR them + */ + fms = new FeatureMatcherSet(); + assertEquals(fms.toStableString(), ""); + fms.or(fm1); + assertEquals(fms.toStableString(), "AF LT 1.2"); + fms.or(fm2); + assertEquals(fms.toStableString(), + "(AF LT 1.2) OR (CLIN_SIG NotContains path)"); + + /* + * attribute or value including space is quoted + */ + FeatureMatcher fm3 = FeatureMatcher.byAttribute(Condition.NotMatches, + "foo bar", "CSQ", "Poly Phen"); + assertEquals(fm3.toStableString(), + "'CSQ:Poly Phen' NotMatches 'foo bar'"); + fms.or(fm3); + assertEquals(fms.toStableString(), + "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')"); + + try + { + fms.and(fm1); + fail("Expected exception"); + } catch (IllegalStateException e) + { + // expected + } + } + + /** + * Tests for parsing a string representation of a FeatureMatcherSet + * + * @see FeatureMatcherSetTest#testToStableString() + */ + @Test(groups = "Functional") + public void testFromString() + { + String descriptor = "AF LT 1.2"; + FeatureMatcherSetI fms = FeatureMatcherSet.fromString(descriptor); + + /* + * shortcut asserts by verifying a 'roundtrip', + * which we trust if other tests pass :-) + */ + assertEquals(fms.toStableString(), descriptor); + + // brackets optional, quotes optional, condition case insensitive + fms = FeatureMatcherSet.fromString("('AF' lt '1.2')"); + assertEquals(fms.toStableString(), descriptor); + + descriptor = "(AF LT 1.2) AND (CLIN_SIG NotContains path)"; + fms = FeatureMatcherSet.fromString(descriptor); + assertEquals(fms.toStableString(), descriptor); + + // AND is not case-sensitive + fms = FeatureMatcherSet + .fromString("(AF LT 1.2) and (CLIN_SIG NotContains path)"); + assertEquals(fms.toStableString(), descriptor); + + descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path)"; + fms = FeatureMatcherSet.fromString(descriptor); + assertEquals(fms.toStableString(), descriptor); + + // OR is not case-sensitive + fms = FeatureMatcherSet + .fromString("(AF LT 1.2) or (CLIN_SIG NotContains path)"); + assertEquals(fms.toStableString(), descriptor); + + // can get away without brackets on last match condition + fms = FeatureMatcherSet + .fromString("(AF LT 1.2) or CLIN_SIG NotContains path"); + assertEquals(fms.toStableString(), descriptor); + + descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')"; + fms = FeatureMatcherSet.fromString(descriptor); + assertEquals(fms.toStableString(), descriptor); + + // can't mix OR and AND + descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) AND ('CSQ:Poly Phen' NotMatches 'foo bar')"; + assertNull(FeatureMatcherSet.fromString(descriptor)); + + // can't mix AND and OR + descriptor = "(AF LT 1.2) and (CLIN_SIG NotContains path) or ('CSQ:Poly Phen' NotMatches 'foo bar')"; + assertNull(FeatureMatcherSet.fromString(descriptor)); + + // brackets missing + assertNull(FeatureMatcherSet + .fromString("AF LT 1.2 or CLIN_SIG NotContains path")); + + // invalid conjunction + assertNull(FeatureMatcherSet.fromString("(AF LT 1.2) but (AF GT -2)")); + + // unbalanced quote (1) + assertNull(FeatureMatcherSet.fromString("('AF lt '1.2')")); + + // unbalanced quote (2) + assertNull(FeatureMatcherSet.fromString("('AF' lt '1.2)")); + } +} diff --git a/test/jalview/datamodel/features/FeatureMatcherTest.java b/test/jalview/datamodel/features/FeatureMatcherTest.java new file mode 100644 index 0000000..4bd34cb --- /dev/null +++ b/test/jalview/datamodel/features/FeatureMatcherTest.java @@ -0,0 +1,352 @@ +package jalview.datamodel.features; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import jalview.datamodel.SequenceFeature; +import jalview.util.MessageManager; +import jalview.util.matcher.Condition; + +import java.util.Locale; + +import org.testng.annotations.Test; + +public class FeatureMatcherTest +{ + @Test(groups = "Functional") + public void testMatches_byLabel() + { + SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11, + 12, "grp"); + + /* + * contains - not case sensitive + */ + assertTrue( + FeatureMatcher.byLabel(Condition.Contains, "IS").matches(sf)); + assertTrue(FeatureMatcher.byLabel(Condition.Contains, "").matches(sf)); + assertFalse( + FeatureMatcher.byLabel(Condition.Contains, "ISNT").matches(sf)); + + /* + * does not contain + */ + assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "isnt") + .matches(sf)); + assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "is") + .matches(sf)); + + /* + * matches + */ + assertTrue(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY label") + .matches(sf)); + assertFalse(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY") + .matches(sf)); + + /* + * does not match + */ + assertFalse(FeatureMatcher + .byLabel(Condition.NotMatches, "THIS is MY label").matches(sf)); + assertTrue(FeatureMatcher.byLabel(Condition.NotMatches, "THIS is MY") + .matches(sf)); + + /* + * is present / not present + */ + assertTrue(FeatureMatcher.byLabel(Condition.Present, "").matches(sf)); + assertFalse( + FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf)); + } + + @Test(groups = "Functional") + public void testMatches_byScore() + { + SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11, + 12, 3.2f, "grp"); + + assertTrue(FeatureMatcher.byScore(Condition.LT, "3.3").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.LT, "3.2").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.LT, "2.2").matches(sf)); + + assertTrue(FeatureMatcher.byScore(Condition.LE, "3.3").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.LE, "3.2").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.LE, "2.2").matches(sf)); + + assertFalse(FeatureMatcher.byScore(Condition.EQ, "3.3").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.EQ, "3.2").matches(sf)); + + assertFalse(FeatureMatcher.byScore(Condition.GE, "3.3").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.GE, "3.2").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.GE, "2.2").matches(sf)); + + assertFalse(FeatureMatcher.byScore(Condition.GT, "3.3").matches(sf)); + assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf)); + assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf)); + } + + @Test(groups = "Functional") + public void testMatches_byAttribute() + { + /* + * a numeric matcher - MatcherTest covers more conditions + */ + FeatureMatcherI fm = FeatureMatcher + .byAttribute(Condition.GE, "-2", "AF"); + SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", "foobar"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", "-2"); + assertTrue(fm.matches(sf)); + sf.setValue("AF", "-1"); + assertTrue(fm.matches(sf)); + sf.setValue("AF", "-3"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", ""); + assertFalse(fm.matches(sf)); + + /* + * a string pattern matcher + */ + fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF"); + assertFalse(fm.matches(sf)); + sf.setValue("AF", "raining cats and dogs"); + assertTrue(fm.matches(sf)); + + fm = FeatureMatcher.byAttribute(Condition.Present, "", "AC"); + assertFalse(fm.matches(sf)); + sf.setValue("AC", "21"); + assertTrue(fm.matches(sf)); + + fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AC_Females"); + assertTrue(fm.matches(sf)); + sf.setValue("AC_Females", "21"); + assertFalse(fm.matches(sf)); + } + + @Test(groups = "Functional") + public void testToString() + { + Locale.setDefault(Locale.ENGLISH); + + /* + * toString uses the i18n translation of the enum conditions + */ + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm.toString(), "AF < 1.2"); + + /* + * Present / NotPresent omit the value pattern + */ + fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF"); + assertEquals(fm.toString(), "AF is present"); + fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF"); + assertEquals(fm.toString(), "AF is not present"); + + /* + * by Label + */ + fm = FeatureMatcher.byLabel(Condition.Matches, "foobar"); + assertEquals(fm.toString(), + MessageManager.getString("label.label") + " matches 'foobar'"); + + /* + * by Score + */ + fm = FeatureMatcher.byScore(Condition.GE, "12.2"); + assertEquals(fm.toString(), + MessageManager.getString("label.score") + " >= 12.2"); + } + + @Test(groups = "Functional") + public void testGetAttribute() + { + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2", + "AF"); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + + /* + * compound key (attribute / subattribute) + */ + fm = FeatureMatcher.byAttribute(Condition.GE, "-2F", "CSQ", + "Consequence"); + assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" }); + + /* + * answers null if match is by Label or by Score + */ + assertNull(FeatureMatcher.byLabel(Condition.NotContains, "foo") + .getAttribute()); + assertNull(FeatureMatcher.byScore(Condition.LE, "-1").getAttribute()); + } + + @Test(groups = "Functional") + public void testIsByAttribute() + { + assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo") + .isByAttribute()); + assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByAttribute()); + assertTrue(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC") + .isByAttribute()); + } + + @Test(groups = "Functional") + public void testIsByLabel() + { + assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "foo") + .isByLabel()); + assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByLabel()); + assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC") + .isByLabel()); + } + + @Test(groups = "Functional") + public void testIsByScore() + { + assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo") + .isByScore()); + assertTrue(FeatureMatcher.byScore(Condition.LE, "-1").isByScore()); + assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC") + .isByScore()); + } + + @Test(groups = "Functional") + public void testGetMatcher() + { + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f", + "AF"); + assertEquals(fm.getMatcher().getCondition(), Condition.GE); + assertEquals(fm.getMatcher().getFloatValue(), -2F); + assertEquals(fm.getMatcher().getPattern(), "-2.0"); + } + + @Test(groups = "Functional") + public void testFromString() + { + FeatureMatcherI fm = FeatureMatcher.fromString("'AF' LT 1.2"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + assertSame(Condition.LT, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getFloatValue(), 1.2f); + assertEquals(fm.getMatcher().getPattern(), "1.2"); + + // quotes are optional, condition is not case sensitive + fm = FeatureMatcher.fromString("AF lt '1.2'"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + assertSame(Condition.LT, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getFloatValue(), 1.2f); + assertEquals(fm.getMatcher().getPattern(), "1.2"); + + fm = FeatureMatcher.fromString("'AF' Present"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + assertSame(Condition.Present, fm.getMatcher().getCondition()); + + fm = FeatureMatcher.fromString("CSQ:Consequence contains damaging"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" }); + assertSame(Condition.Contains, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "damaging"); + + // keyword Label is not case sensitive + fm = FeatureMatcher.fromString("LABEL Matches 'foobar'"); + assertTrue(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.Matches, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "foobar"); + + fm = FeatureMatcher.fromString("'Label' matches 'foo bar'"); + assertTrue(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.Matches, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "foo bar"); + + // quotes optional on pattern + fm = FeatureMatcher.fromString("'Label' matches foo bar"); + assertTrue(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.Matches, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "foo bar"); + + fm = FeatureMatcher.fromString("Score GE 12.2"); + assertFalse(fm.isByLabel()); + assertTrue(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.GE, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "12.2"); + assertEquals(fm.getMatcher().getFloatValue(), 12.2f); + + // keyword Score is not case sensitive + fm = FeatureMatcher.fromString("'SCORE' ge '12.2'"); + assertFalse(fm.isByLabel()); + assertTrue(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.GE, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "12.2"); + assertEquals(fm.getMatcher().getFloatValue(), 12.2f); + + // invalid numeric pattern + assertNull(FeatureMatcher.fromString("Score eq twelve")); + // unbalanced opening quote + assertNull(FeatureMatcher.fromString("'Score ge 12.2")); + // unbalanced pattern quote + assertNull(FeatureMatcher.fromString("'Score' ge '12.2")); + // pattern missing + assertNull(FeatureMatcher.fromString("Score ge")); + // condition and pattern missing + assertNull(FeatureMatcher.fromString("Score")); + // everything missing + assertNull(FeatureMatcher.fromString("")); + } + + /** + * Tests for toStableString which (unlike toString) does not i18n the + * conditions + */ + @Test(groups = "Functional") + public void testToStableString() + { + // attribute name not quoted unless it contains space + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm.toStableString(), "AF LT 1.2"); + + /* + * Present / NotPresent omit the value pattern + */ + fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF"); + assertEquals(fm.toStableString(), "AF Present"); + fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF"); + assertEquals(fm.toStableString(), "AF NotPresent"); + + /* + * by Label + * pattern not quoted unless it contains space + */ + fm = FeatureMatcher.byLabel(Condition.Matches, "foobar"); + assertEquals(fm.toStableString(), "Label Matches foobar"); + + fm = FeatureMatcher.byLabel(Condition.Matches, "foo bar"); + assertEquals(fm.toStableString(), "Label Matches 'foo bar'"); + + /* + * by Score + */ + fm = FeatureMatcher.byScore(Condition.GE, "12.2"); + assertEquals(fm.toStableString(), "Score GE 12.2"); + } +} diff --git a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java index e2af26b..42afa82 100644 --- a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java +++ b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java @@ -21,9 +21,7 @@ package jalview.ext.ensembl; import static org.testng.AssertJUnit.assertEquals; -import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertSame; -import static org.testng.AssertJUnit.assertTrue; import jalview.datamodel.Alignment; import jalview.datamodel.SequenceFeature; diff --git a/test/jalview/ext/htsjdk/TestHtsContigDb.java b/test/jalview/ext/htsjdk/TestHtsContigDb.java index 350b599..28c5cf0 100644 --- a/test/jalview/ext/htsjdk/TestHtsContigDb.java +++ b/test/jalview/ext/htsjdk/TestHtsContigDb.java @@ -20,13 +20,19 @@ */ package jalview.ext.htsjdk; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + import jalview.datamodel.SequenceI; -import jalview.gui.JvOptionPane; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; /** @@ -35,25 +41,108 @@ import org.testng.annotations.Test; */ public class TestHtsContigDb { + @Test(groups = "Functional") + public final void testGetSequenceProxy() throws Exception + { + String pathname = "test/jalview/ext/htsjdk/pgmb.fasta"; + HtsContigDb db = new HtsContigDb("ADB", new File(pathname)); + + assertTrue(db.isValid()); + assertTrue(db.isIndexed()); // htsjdk opens the .fai file + + SequenceI sq = db.getSequenceProxy("Deminut"); + assertNotNull(sq); + assertEquals(sq.getLength(), 606); + + /* + * read a sequence earlier in the file + */ + sq = db.getSequenceProxy("PPL_06716"); + assertNotNull(sq); + assertEquals(sq.getLength(), 602); + + // dict = db.getDictionary(f, truncate)) + } - @BeforeClass(alwaysRun = true) - public void setUpJvOptionPane() + /** + * Trying to open a .fai file directly results in IllegalArgumentException - + * have to provide the unindexed file name instead + */ + @Test( + groups = "Functional", + expectedExceptions = java.lang.IllegalArgumentException.class) + public final void testGetSequenceProxy_indexed() { - JvOptionPane.setInteractiveMode(false); - JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION); + String pathname = "test/jalview/ext/htsjdk/pgmb.fasta.fai"; + new HtsContigDb("ADB", new File(pathname)); + fail("Expected exception opening .fai file"); } + /** + * Tests that exercise + *
      + *
    • opening an unindexed fasta file
    • + *
    • creating a .fai index
    • + *
    • opening the fasta file, now using the index
    • + *
    • error on creating index if overwrite not allowed
    • + *
    + * + * @throws IOException + */ @Test(groups = "Functional") - public final void testHTSReferenceSequence() throws Exception + public void testCreateFastaSequenceIndex() throws IOException { - HtsContigDb remmadb = new HtsContigDb("REEMADB", new File( - "test/jalview/ext/htsjdk/pgmb.fasta")); + File fasta = new File("test/jalview/ext/htsjdk/pgmb.fasta"); + + /* + * create .fai with no overwrite fails if it exists + */ + try { + HtsContigDb.createFastaSequenceIndex(fasta.toPath(), false); + fail("Expected exception"); + } catch (IOException e) + { + // expected + } - Assert.assertTrue(remmadb.isValid()); + /* + * create a copy of the .fasta (as a temp file) + */ + File copyFasta = File.createTempFile("copyFasta", ".fasta"); + copyFasta.deleteOnExit(); + assertTrue(copyFasta.exists()); + Files.copy(fasta.toPath(), copyFasta.toPath(), + StandardCopyOption.REPLACE_EXISTING); - SequenceI sq = remmadb.getSequenceProxy("Deminut"); - Assert.assertNotNull(sq); - Assert.assertNotEquals(0, sq.getLength()); + /* + * open the Fasta file - not indexed, as no .fai file yet exists + */ + HtsContigDb db = new HtsContigDb("ADB", copyFasta); + assertTrue(db.isValid()); + assertFalse(db.isIndexed()); + db.close(); + + /* + * create the .fai index, re-open the .fasta file - now indexed + */ + HtsContigDb.createFastaSequenceIndex(copyFasta.toPath(), true); + db = new HtsContigDb("ADB", copyFasta); + assertTrue(db.isValid()); + assertTrue(db.isIndexed()); + db.close(); } + /** + * A convenience 'test' that may be run to create a .fai file for any given + * fasta file + * + * @throws IOException + */ + @Test(enabled = false) + public void testCreateIndex() throws IOException + { + + File fasta = new File("test/jalview/io/vcf/contigs.fasta"); + HtsContigDb.createFastaSequenceIndex(fasta.toPath(), true); + } } diff --git a/test/jalview/ext/htsjdk/VCFReaderTest.java b/test/jalview/ext/htsjdk/VCFReaderTest.java new file mode 100644 index 0000000..bf617ae --- /dev/null +++ b/test/jalview/ext/htsjdk/VCFReaderTest.java @@ -0,0 +1,200 @@ +package jalview.ext.htsjdk; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import htsjdk.samtools.util.CloseableIterator; +import htsjdk.variant.variantcontext.Allele; +import htsjdk.variant.variantcontext.VariantContext; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import org.testng.annotations.Test; + +public class VCFReaderTest +{ + private static final String[] VCF = new String[] { + "##fileformat=VCFv4.2", + "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO", + "20\t3\t.\tC\tG\t.\tPASS\tDP=100", // SNP C/G + "20\t7\t.\tG\tGA\t.\tPASS\tDP=100", // insertion G/GA + "18\t2\t.\tACG\tA\t.\tPASS\tDP=100" }; // deletion ACG/A + + // gnomAD exome variant dataset + private static final String VCF_PATH = "/Volumes/gjb/smacgowan/NOBACK/resources/gnomad/gnomad.exomes.r2.0.1.sites.vcf.gz"; + + // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz"; + + /** + * A test to exercise some basic functionality of the htsjdk VCF reader, + * reading from a non-index VCF file + * + * @throws IOException + */ + @Test(groups = "Functional") + public void testReadVcf_plain() throws IOException + { + File f = writeVcfFile(); + VCFReader reader = new VCFReader(f.getAbsolutePath()); + CloseableIterator variants = reader.iterator(); + + /* + * SNP C/G variant + */ + VariantContext vc = variants.next(); + assertTrue(vc.isSNP()); + Allele ref = vc.getReference(); + assertEquals(ref.getBaseString(), "C"); + List 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 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 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 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 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(); + } +} diff --git a/test/jalview/ext/so/SequenceOntologyTest.java b/test/jalview/ext/so/SequenceOntologyTest.java index b76a295..31e1887 100644 --- a/test/jalview/ext/so/SequenceOntologyTest.java +++ b/test/jalview/ext/so/SequenceOntologyTest.java @@ -107,4 +107,29 @@ public class SequenceOntologyTest assertFalse(so.isA("CDS_region", "CDS"));// part_of assertFalse(so.isA("polypeptide", "CDS")); // derives_from } + + @Test(groups = "Functional") + public void testIsSequenceVariant() + { + assertFalse(so.isA("CDS", "sequence_variant")); + assertTrue(so.isA("sequence_variant", "sequence_variant")); + + /* + * these should all be sub-types of sequence_variant + */ + assertTrue(so.isA("structural_variant", "sequence_variant")); + assertTrue(so.isA("feature_variant", "sequence_variant")); + assertTrue(so.isA("gene_variant", "sequence_variant")); + assertTrue(so.isA("transcript_variant", "sequence_variant")); + assertTrue(so.isA("NMD_transcript_variant", "sequence_variant")); + assertTrue(so.isA("missense_variant", "sequence_variant")); + assertTrue(so.isA("synonymous_variant", "sequence_variant")); + assertTrue(so.isA("frameshift_variant", "sequence_variant")); + assertTrue(so.isA("5_prime_UTR_variant", "sequence_variant")); + assertTrue(so.isA("3_prime_UTR_variant", "sequence_variant")); + assertTrue(so.isA("stop_gained", "sequence_variant")); + assertTrue(so.isA("stop_lost", "sequence_variant")); + assertTrue(so.isA("inframe_deletion", "sequence_variant")); + assertTrue(so.isA("inframe_insertion", "sequence_variant")); + } } diff --git a/test/jalview/gui/AlignFrameTest.java b/test/jalview/gui/AlignFrameTest.java index af9c045..1ee25c7 100644 --- a/test/jalview/gui/AlignFrameTest.java +++ b/test/jalview/gui/AlignFrameTest.java @@ -26,6 +26,7 @@ import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; +import jalview.api.FeatureColourI; import jalview.bin.Cache; import jalview.bin.Jalview; import jalview.datamodel.Alignment; @@ -39,6 +40,7 @@ import jalview.io.FileLoader; import jalview.io.Jalview2xmlTests; import jalview.renderer.ResidueShaderI; import jalview.schemes.BuriedColourScheme; +import jalview.schemes.FeatureColour; import jalview.schemes.HelixColourScheme; import jalview.schemes.JalviewColourScheme; import jalview.schemes.StrandColourScheme; @@ -69,16 +71,21 @@ public class AlignFrameTest { SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ"); SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ"); - seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, - Float.NaN, null)); - seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, - Float.NaN, null)); + seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null)); + seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f, + null)); seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4, Float.NaN, null)); seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9, Float.NaN, null)); AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 }); - AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight()); + AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), + al.getHeight()); + + /* + * make all features visible (select feature columns checks visibility) + */ + alignFrame.getFeatureRenderer().findAllFeatures(true); /* * hiding a feature not present does nothing @@ -86,13 +93,11 @@ public class AlignFrameTest 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 @@ -100,15 +105,31 @@ public class AlignFrameTest assertFalse(alignFrame.hideFeatureColumns("Metal", true)); assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty()); List 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(); diff --git a/test/jalview/gui/FeatureSettingsTest.java b/test/jalview/gui/FeatureSettingsTest.java new file mode 100644 index 0000000..6ddebf8 --- /dev/null +++ b/test/jalview/gui/FeatureSettingsTest.java @@ -0,0 +1,191 @@ +package jalview.gui; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import jalview.api.FeatureColourI; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.io.DataSourceType; +import jalview.io.FileLoader; +import jalview.schemes.FeatureColour; +import jalview.util.matcher.Condition; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +import org.testng.annotations.Test; + +public class FeatureSettingsTest +{ + /** + * Test a roundtrip of save and reload of feature colours and filters as XML + * + * @throws IOException + */ + @Test(groups = "Functional") + public void testSaveLoad() throws IOException + { + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded( + ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE); + SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0); + + /* + * add some features to the sequence + */ + int score = 1; + addFeatures(seq1, "type1", score++); + addFeatures(seq1, "type2", score++); + addFeatures(seq1, "type3", score++); + addFeatures(seq1, "type4", score++); + addFeatures(seq1, "type5", score++); + + /* + * set colour schemes for features + */ + FeatureRenderer fr = af.getFeatureRenderer(); + + // type1: red + fr.setColour("type1", new FeatureColour(Color.red)); + + // type2: by label + FeatureColourI byLabel = new FeatureColour(); + byLabel.setColourByLabel(true); + fr.setColour("type2", byLabel); + + // type3: by score above threshold + FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1, + 10); + byScore.setAboveThreshold(true); + byScore.setThreshold(2f); + fr.setColour("type3", byScore); + + // type4: by attribute AF + FeatureColourI byAF = new FeatureColour(); + byAF.setColourByLabel(true); + byAF.setAttributeName("AF"); + fr.setColour("type4", byAF); + + // type5: by attribute CSQ:PolyPhen below threshold + FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE, + 1, 10); + byPolyPhen.setBelowThreshold(true); + byPolyPhen.setThreshold(3f); + byPolyPhen.setAttributeName("CSQ", "PolyPhen"); + fr.setColour("type5", byPolyPhen); + + /* + * set filters for feature types + */ + + // filter type1 features by (label contains "x") + FeatureMatcherSetI filterByX = new FeatureMatcherSet(); + filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x")); + fr.setFeatureFilter("type1", filterByX); + + // filter type2 features by (score <= 2.4 and score > 1.1) + FeatureMatcherSetI filterByScore = new FeatureMatcherSet(); + filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4")); + filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1")); + fr.setFeatureFilter("type2", filterByScore); + + // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0) + FeatureMatcherSetI filterByXY = new FeatureMatcherSet(); + filterByXY + .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF")); + filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ", + "PolyPhen")); + fr.setFeatureFilter("type3", filterByXY); + + /* + * save colours and filters to an XML file + */ + File coloursFile = File.createTempFile("testSaveLoad", ".fc"); + coloursFile.deleteOnExit(); + FeatureSettings fs = new FeatureSettings(af); + fs.save(coloursFile); + + /* + * change feature colours and filters + */ + FeatureColourI pink = new FeatureColour(Color.pink); + fr.setColour("type1", pink); + fr.setColour("type2", pink); + fr.setColour("type3", pink); + fr.setColour("type4", pink); + fr.setColour("type5", pink); + + FeatureMatcherSetI filter2 = new FeatureMatcherSet(); + filter2.and(FeatureMatcher.byLabel(Condition.NotContains, "y")); + fr.setFeatureFilter("type1", filter2); + fr.setFeatureFilter("type2", filter2); + fr.setFeatureFilter("type3", filter2); + fr.setFeatureFilter("type4", filter2); + fr.setFeatureFilter("type5", filter2); + + /* + * reload colours and filters from file and verify they are restored + */ + fs.load(coloursFile); + FeatureColourI fc = fr.getFeatureStyle("type1"); + assertTrue(fc.isSimpleColour()); + assertEquals(fc.getColour(), Color.red); + fc = fr.getFeatureStyle("type2"); + assertTrue(fc.isColourByLabel()); + fc = fr.getFeatureStyle("type3"); + assertTrue(fc.isGraduatedColour()); + assertNull(fc.getAttributeName()); + assertTrue(fc.isAboveThreshold()); + assertEquals(fc.getThreshold(), 2f); + fc = fr.getFeatureStyle("type4"); + assertTrue(fc.isColourByLabel()); + assertTrue(fc.isColourByAttribute()); + assertEquals(fc.getAttributeName(), new String[] { "AF" }); + fc = fr.getFeatureStyle("type5"); + assertTrue(fc.isGraduatedColour()); + assertTrue(fc.isColourByAttribute()); + assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" }); + assertTrue(fc.isBelowThreshold()); + assertEquals(fc.getThreshold(), 3f); + + assertEquals(fr.getFeatureFilter("type1").toStableString(), "Label Contains x"); + assertEquals(fr.getFeatureFilter("type2").toStableString(), + "(Score LE 2.4) AND (Score GT 1.1)"); + assertEquals(fr.getFeatureFilter("type3").toStableString(), + "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)"); + } + + /** + * Adds two features of the given type to the given sequence, also setting the + * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen" + * + * @param seq + * @param featureType + * @param score + */ + private void addFeatures(SequenceI seq, String featureType, int score) + { + addFeature(seq, featureType, score++); + addFeature(seq, featureType, score); + } + + private void addFeature(SequenceI seq, String featureType, int score) + { + SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2, + score, "grp"); + sf.setValue("AF", score); + sf.setValue("CSQ", new HashMap() + { + { + put("PolyPhen", Integer.toString(score)); + } + }); + seq.addSequenceFeature(sf); + } +} diff --git a/test/jalview/gui/PopupMenuTest.java b/test/jalview/gui/PopupMenuTest.java index 335240b..40e624d 100644 --- a/test/jalview/gui/PopupMenuTest.java +++ b/test/jalview/gui/PopupMenuTest.java @@ -26,21 +26,26 @@ import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; +import jalview.bin.Cache; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.Annotation; 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; @@ -80,6 +85,25 @@ public class PopupMenuTest @BeforeMethod(alwaysRun = true) public void setUp() throws IOException { + Cache.loadProperties("test/jalview/io/testProps.jvprops"); + String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$" + + SEQUENCE_ID + + "$" + + "|" + + "UNIPROT | http://www.uniprot.org/uniprot/$" + DB_ACCESSION + "$") + + "|" + + ("INTERPRO | http://www.ebi.ac.uk/interpro/entry/$" + + DB_ACCESSION + "$") + + "|" + + + // Gene3D entry tests for case (in)sensitivity + ("Gene3D | http://gene3d.biochem.ucl.ac.uk/Gene3D/search?sterm=$" + + DB_ACCESSION + "$&mode=protein"); + + UrlProviderFactoryI factory = new DesktopUrlProviderFactory( + UrlConstants.DEFAULT_LABEL, inMenuString, ""); + Preferences.sequenceUrlLinks = factory.createUrlProvider(); + alignment = new FormatAdapter().readFile(TEST_DATA, DataSourceType.PASTE, FileFormat.Fasta); AlignFrame af = new AlignFrame(alignment, 700, 500); @@ -495,17 +519,19 @@ public class PopupMenuTest // add all the dbrefs to the sequences: Uniprot 1 each, Interpro all 3 to // seq0, Gene3D to seq1 - seqs.get(0).addDBRef(refs.get(0)); + SequenceI seq = seqs.get(0); + seq.addDBRef(refs.get(0)); - seqs.get(0).addDBRef(refs.get(1)); - seqs.get(0).addDBRef(refs.get(2)); - seqs.get(0).addDBRef(refs.get(3)); + seq.addDBRef(refs.get(1)); + seq.addDBRef(refs.get(2)); + seq.addDBRef(refs.get(3)); seqs.get(1).addDBRef(refs.get(4)); seqs.get(1).addDBRef(refs.get(5)); // get the Popup Menu for first sequence - testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0), links); + List noFeatures = Collections. emptyList(); + testee = new PopupMenu(parentPanel, seq, noFeatures); Component[] seqItems = testee.sequenceMenu.getMenuComponents(); JMenu linkMenu = (JMenu) seqItems[6]; Component[] linkItems = linkMenu.getMenuComponents(); @@ -519,15 +545,18 @@ public class PopupMenuTest // sequence id for each link should match corresponding DB accession id for (int i = 1; i < 4; i++) { - assertEquals(refs.get(i - 1).getSource(), ((JMenuItem) linkItems[i]) + String msg = seq.getName() + " link[" + i + "]"; + assertEquals(msg, refs.get(i - 1).getSource(), + ((JMenuItem) linkItems[i]) .getText().split("\\|")[0]); - assertEquals(refs.get(i - 1).getAccessionId(), + assertEquals(msg, refs.get(i - 1).getAccessionId(), ((JMenuItem) linkItems[i]) .getText().split("\\|")[1]); } // get the Popup Menu for second sequence - testee = new PopupMenu(parentPanel, (Sequence) seqs.get(1), links); + seq = seqs.get(1); + testee = new PopupMenu(parentPanel, seq, noFeatures); seqItems = testee.sequenceMenu.getMenuComponents(); linkMenu = (JMenu) seqItems[6]; linkItems = linkMenu.getMenuComponents(); @@ -541,9 +570,11 @@ public class PopupMenuTest // sequence id for each link should match corresponding DB accession id for (int i = 1; i < 3; i++) { - assertEquals(refs.get(i + 3).getSource(), ((JMenuItem) linkItems[i]) + String msg = seq.getName() + " link[" + i + "]"; + assertEquals(msg, refs.get(i + 3).getSource(), + ((JMenuItem) linkItems[i]) .getText().split("\\|")[0].toUpperCase()); - assertEquals(refs.get(i + 3).getAccessionId(), + assertEquals(msg, refs.get(i + 3).getAccessionId(), ((JMenuItem) linkItems[i]).getText().split("\\|")[1]); } @@ -552,8 +583,7 @@ public class PopupMenuTest nomatchlinks.add("NOMATCH | http://www.uniprot.org/uniprot/$" + DB_ACCESSION + "$"); - testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0), - nomatchlinks); + testee = new PopupMenu(parentPanel, seq, noFeatures); seqItems = testee.sequenceMenu.getMenuComponents(); linkMenu = (JMenu) seqItems[6]; assertFalse(linkMenu.isEnabled()); diff --git a/test/jalview/gui/SeqCanvasTest.java b/test/jalview/gui/SeqCanvasTest.java index a27bc3f..05b9aea 100644 --- a/test/jalview/gui/SeqCanvasTest.java +++ b/test/jalview/gui/SeqCanvasTest.java @@ -13,8 +13,6 @@ import junit.extensions.PA; import org.testng.annotations.Test; -import sun.swing.SwingUtilities2; - public class SeqCanvasTest { /** @@ -48,7 +46,7 @@ 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 @@ -218,7 +216,7 @@ 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 int annotationHeight = testee.getAnnotationHeight(); diff --git a/test/jalview/io/CrossRef2xmlTests.java b/test/jalview/io/CrossRef2xmlTests.java index 0715857..b3db4de 100644 --- a/test/jalview/io/CrossRef2xmlTests.java +++ b/test/jalview/io/CrossRef2xmlTests.java @@ -39,6 +39,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; + +import junit.extensions.PA; import org.testng.Assert; import org.testng.annotations.BeforeClass; @@ -90,9 +93,9 @@ public class CrossRef2xmlTests extends Jalview2xmlBase // . codonframes // // - HashMap dbtoviewBit = new HashMap<>(); + Map dbtoviewBit = new HashMap<>(); List keyseq = new ArrayList<>(); - HashMap savedProjects = new HashMap<>(); + Map savedProjects = new HashMap<>(); for (String[] did : new String[][] { { "UNIPROT", "P00338" } }) { @@ -186,15 +189,16 @@ public class CrossRef2xmlTests extends Jalview2xmlBase if (pass2 == 0) { // retrieve and show cross-refs in this thread - cra = new CrossRefAction(af, seqs, dna, db); + cra = CrossRefAction.getHandlerFor(seqs, dna, db, af); cra.run(); - if (cra.getXrefViews().size() == 0) + cra_views = (List) 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); @@ -286,16 +290,18 @@ public class CrossRef2xmlTests extends Jalview2xmlBase if (pass3 == 0) { - SequenceI[] xrseqs = avp.getAlignment() .getSequencesArray(); AlignFrame nextaf = Desktop.getAlignFrameFor(avp .getAlignViewport()); - cra = new CrossRefAction(nextaf, xrseqs, avp - .getAlignViewport().isNucleotide(), xrefdb); + cra = CrossRefAction.getHandlerFor(xrseqs, avp + .getAlignViewport().isNucleotide(), xrefdb, + nextaf); cra.run(); - if (cra.getXrefViews().size() == 0) + cra_views2 = (List) PA.getValue( + cra, "xrefViews"); + if (cra_views2.size() == 0) { failedXrefMenuItems .add("No crossrefs retrieved for '" @@ -303,7 +309,6 @@ public class CrossRef2xmlTests extends Jalview2xmlBase + " via '" + nextaf.getTitle() + "'"); continue; } - cra_views2 = cra.getXrefViews(); assertNucleotide(cra_views2.get(0), "Nucleotide panel included proteins for '" + nextxref + "' to " + xrefdb @@ -541,8 +546,8 @@ public class CrossRef2xmlTests extends Jalview2xmlBase * viewpanel needs to be called with a distinct xrefpath to ensure * each one's strings are compared) */ - private void stringify(HashMap dbtoviewBit, - HashMap savedProjects, String xrefpath, + private void stringify(Map dbtoviewBit, + Map savedProjects, String xrefpath, AlignmentViewPanel avp) { if (savedProjects != null) diff --git a/test/jalview/io/FeaturesFileTest.java b/test/jalview/io/FeaturesFileTest.java index 152ab84..32ca841 100644 --- a/test/jalview/io/FeaturesFileTest.java +++ b/test/jalview/io/FeaturesFileTest.java @@ -23,7 +23,9 @@ package jalview.io; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertSame; import static org.testng.AssertJUnit.assertTrue; +import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals; import jalview.api.FeatureColourI; import jalview.api.FeatureRenderer; @@ -32,11 +34,17 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.datamodel.features.SequenceFeatures; import jalview.gui.AlignFrame; import jalview.gui.Desktop; import jalview.gui.JvOptionPane; +import jalview.schemes.FeatureColour; import jalview.structure.StructureSelectionManager; +import jalview.util.matcher.Condition; import java.awt.Color; import java.io.File; @@ -44,6 +52,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -467,10 +476,10 @@ public class FeaturesFileTest */ FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); Map visible = fr.getDisplayedFeatureCols(); - List visibleGroups = new ArrayList( + List visibleGroups = new ArrayList<>( Arrays.asList(new String[] {})); String exported = featuresFile.printJalviewFormat( - al.getSequencesArray(), visible, visibleGroups, false); + al.getSequencesArray(), visible, null, visibleGroups, false); String expected = "No Features Visible"; assertEquals(expected, exported); @@ -479,7 +488,7 @@ public class FeaturesFileTest */ visibleGroups.add("uniprot"); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible, visibleGroups, true); + visible, null, visibleGroups, true); expected = "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n" + "desc1\tFER_CAPAN\t-1\t0\t0\tPfam\t1.3\n" + "desc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n" // NaN is not output @@ -493,9 +502,9 @@ public class FeaturesFileTest fr.setVisible("GAMMA-TURN"); visible = fr.getDisplayedFeatureCols(); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible, visibleGroups, false); + visible, null, visibleGroups, false); expected = "METAL\tcc9900\n" - + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n" + + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" @@ -508,13 +517,13 @@ public class FeaturesFileTest fr.setVisible("Pfam"); visible = fr.getDisplayedFeatureCols(); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), - visible, visibleGroups, false); + visible, null, visibleGroups, false); /* * features are output within group, ordered by sequence and by type */ expected = "METAL\tcc9900\n" + "Pfam\tff0000\n" - + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n" + + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" @@ -539,8 +548,8 @@ public class FeaturesFileTest */ FeaturesFile featuresFile = new FeaturesFile(); FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); - Map visible = new HashMap(); - List visibleGroups = new ArrayList( + Map visible = new HashMap<>(); + List visibleGroups = new ArrayList<>( Arrays.asList(new String[] {})); String exported = featuresFile.printGffFormat(al.getSequencesArray(), visible, visibleGroups, false); @@ -623,4 +632,79 @@ public class FeaturesFileTest + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n"; assertEquals(expected, exported); } + + /** + * Test for parsing of feature filters as represented in a Jalview features + * file + * + * @throws Exception + */ + @Test(groups = { "Functional" }) + public void testParseFilters() throws Exception + { + Map filters = new HashMap<>(); + String text = "sequence_variant\tCSQ:PolyPhen NotContains 'damaging'\n" + + "missense_variant\t(label contains foobar) and (Score lt 1.3)"; + FeaturesFile featuresFile = new FeaturesFile(text, + DataSourceType.PASTE); + featuresFile.parseFilters(filters); + assertEquals(filters.size(), 2); + + FeatureMatcherSetI fm = filters.get("sequence_variant"); + assertNotNull(fm); + Iterator matchers = fm.getMatchers().iterator(); + FeatureMatcherI matcher = matchers.next(); + assertFalse(matchers.hasNext()); + String[] attributes = matcher.getAttribute(); + assertArrayEquals(attributes, new String[] { "CSQ", "PolyPhen" }); + assertSame(matcher.getMatcher().getCondition(), Condition.NotContains); + assertEquals(matcher.getMatcher().getPattern(), "damaging"); + + fm = filters.get("missense_variant"); + assertNotNull(fm); + matchers = fm.getMatchers().iterator(); + matcher = matchers.next(); + assertTrue(matcher.isByLabel()); + assertSame(matcher.getMatcher().getCondition(), Condition.Contains); + assertEquals(matcher.getMatcher().getPattern(), "foobar"); + matcher = matchers.next(); + assertTrue(matcher.isByScore()); + assertSame(matcher.getMatcher().getCondition(), Condition.LT); + assertEquals(matcher.getMatcher().getPattern(), "1.3"); + assertEquals(matcher.getMatcher().getFloatValue(), 1.3f); + + assertFalse(matchers.hasNext()); + } + + @Test(groups = { "Functional" }) + public void testOutputFeatureFilters() + { + FeaturesFile ff = new FeaturesFile(); + StringBuilder sb = new StringBuilder(); + Map visible = new HashMap<>(); + visible.put("pfam", new FeatureColour(Color.red)); + Map featureFilters = new HashMap<>(); + + // with no filters, nothing is output + ff.outputFeatureFilters(sb, visible, featureFilters); + assertEquals("", sb.toString()); + + // with filter for not visible features only, nothing is output + FeatureMatcherSet filter = new FeatureMatcherSet(); + filter.and(FeatureMatcher.byLabel(Condition.Present, null)); + featureFilters.put("foobar", filter); + ff.outputFeatureFilters(sb, visible, featureFilters); + assertEquals("", sb.toString()); + + // with filters for visible feature types + FeatureMatcherSet filter2 = new FeatureMatcherSet(); + filter2.and(FeatureMatcher.byAttribute(Condition.Present, null, "CSQ", + "PolyPhen")); + filter2.and(FeatureMatcher.byScore(Condition.LE, "-2.4")); + featureFilters.put("pfam", filter2); + visible.put("foobar", new FeatureColour(Color.blue)); + ff.outputFeatureFilters(sb, visible, featureFilters); + String expected = "\nSTARTFILTERS\nfoobar\tLabel Present\npfam\t(CSQ:PolyPhen Present) AND (Score LE -2.4)\nENDFILTERS\n\n"; + assertEquals(expected, sb.toString()); + } } diff --git a/test/jalview/io/Jalview2xmlTests.java b/test/jalview/io/Jalview2xmlTests.java index 6abb7e5..e9e0782 100644 --- a/test/jalview/io/Jalview2xmlTests.java +++ b/test/jalview/io/Jalview2xmlTests.java @@ -23,11 +23,13 @@ package jalview.io; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; +import jalview.api.FeatureColourI; import jalview.api.ViewStyleI; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; @@ -35,12 +37,17 @@ import jalview.datamodel.HiddenSequences; import jalview.datamodel.PDBEntry; import jalview.datamodel.PDBEntry.Type; import jalview.datamodel.SequenceCollectionI; +import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.AlignFrame; import jalview.gui.AlignViewport; import jalview.gui.AlignmentPanel; import jalview.gui.Desktop; +import jalview.gui.FeatureRenderer; import jalview.gui.Jalview2XML; import jalview.gui.JvOptionPane; import jalview.gui.PopupMenu; @@ -50,13 +57,16 @@ import jalview.schemes.AnnotationColourGradient; import jalview.schemes.BuriedColourScheme; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemeProperty; +import jalview.schemes.FeatureColour; import jalview.schemes.JalviewColourScheme; import jalview.schemes.RNAHelicesColour; import jalview.schemes.StrandColourScheme; import jalview.schemes.TCoffeeColourScheme; import jalview.structure.StructureImportSettings; +import jalview.util.matcher.Condition; import jalview.viewmodel.AlignmentViewport; +import java.awt.Color; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -413,7 +423,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase String afid = af.getViewport().getSequenceSetId(); // remember reference sequence for each panel - Map refseqs = new HashMap(); + Map refseqs = new HashMap<>(); /* * mark sequence 2, 3, 4.. in panels 1, 2, 3... @@ -551,8 +561,8 @@ public class Jalview2xmlTests extends Jalview2xmlBase * remember representative and hidden sequences marked * on each panel */ - Map repSeqs = new HashMap(); - Map> hiddenSeqNames = new HashMap>(); + Map repSeqs = new HashMap<>(); + Map> hiddenSeqNames = new HashMap<>(); /* * mark sequence 2, 3, 4.. in panels 1, 2, 3... @@ -568,7 +578,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase repIndex = Math.max(repIndex, 1); SequenceI repSeq = alignment.getSequenceAt(repIndex); repSeqs.put(ap.getViewName(), repSeq); - List hiddenNames = new ArrayList(); + List hiddenNames = new ArrayList<>(); hiddenSeqNames.put(ap.getViewName(), hiddenNames); /* @@ -841,4 +851,163 @@ public class Jalview2xmlTests extends Jalview2xmlBase assertTrue(rs.conservationApplied()); assertEquals(rs.getConservationInc(), 30); } + + /** + * Test save and reload of feature colour schemes and filter settings + * + * @throws IOException + */ + @Test(groups = { "Functional" }) + public void testSaveLoadFeatureColoursAndFilters() throws IOException + { + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded( + ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE); + SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0); + + /* + * add some features to the sequence + */ + int score = 1; + addFeatures(seq1, "type1", score++); + addFeatures(seq1, "type2", score++); + addFeatures(seq1, "type3", score++); + addFeatures(seq1, "type4", score++); + addFeatures(seq1, "type5", score++); + + /* + * set colour schemes for features + */ + FeatureRenderer fr = af.getFeatureRenderer(); + fr.findAllFeatures(true); + + // type1: red + fr.setColour("type1", new FeatureColour(Color.red)); + + // type2: by label + FeatureColourI byLabel = new FeatureColour(); + byLabel.setColourByLabel(true); + fr.setColour("type2", byLabel); + + // type3: by score above threshold + FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1, + 10); + byScore.setAboveThreshold(true); + byScore.setThreshold(2f); + fr.setColour("type3", byScore); + + // type4: by attribute AF + FeatureColourI byAF = new FeatureColour(); + byAF.setColourByLabel(true); + byAF.setAttributeName("AF"); + fr.setColour("type4", byAF); + + // type5: by attribute CSQ:PolyPhen below threshold + FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE, + 1, 10); + byPolyPhen.setBelowThreshold(true); + byPolyPhen.setThreshold(3f); + byPolyPhen.setAttributeName("CSQ", "PolyPhen"); + fr.setColour("type5", byPolyPhen); + + /* + * set filters for feature types + */ + + // filter type1 features by (label contains "x") + FeatureMatcherSetI filterByX = new FeatureMatcherSet(); + filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x")); + fr.setFeatureFilter("type1", filterByX); + + // filter type2 features by (score <= 2.4 and score > 1.1) + FeatureMatcherSetI filterByScore = new FeatureMatcherSet(); + filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4")); + filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1")); + fr.setFeatureFilter("type2", filterByScore); + + // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0) + FeatureMatcherSetI filterByXY = new FeatureMatcherSet(); + filterByXY + .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF")); + filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ", + "PolyPhen")); + fr.setFeatureFilter("type3", filterByXY); + + /* + * save as Jalview project + */ + File tfile = File.createTempFile("JalviewTest", ".jvp"); + tfile.deleteOnExit(); + String filePath = tfile.getAbsolutePath(); + assertTrue(af.saveAlignment(filePath, FileFormat.Jalview), + "Failed to store as a project."); + + /* + * close current alignment and load the saved project + */ + af.closeMenuItem_actionPerformed(true); + af = null; + af = new FileLoader() + .LoadFileWaitTillLoaded(filePath, DataSourceType.FILE); + assertNotNull(af, "Failed to import new project"); + + /* + * verify restored feature colour schemes and filters + */ + fr = af.getFeatureRenderer(); + FeatureColourI fc = fr.getFeatureStyle("type1"); + assertTrue(fc.isSimpleColour()); + assertEquals(fc.getColour(), Color.red); + fc = fr.getFeatureStyle("type2"); + assertTrue(fc.isColourByLabel()); + fc = fr.getFeatureStyle("type3"); + assertTrue(fc.isGraduatedColour()); + assertNull(fc.getAttributeName()); + assertTrue(fc.isAboveThreshold()); + assertEquals(fc.getThreshold(), 2f); + fc = fr.getFeatureStyle("type4"); + assertTrue(fc.isColourByLabel()); + assertTrue(fc.isColourByAttribute()); + assertEquals(fc.getAttributeName(), new String[] { "AF" }); + fc = fr.getFeatureStyle("type5"); + assertTrue(fc.isGraduatedColour()); + assertTrue(fc.isColourByAttribute()); + assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" }); + assertTrue(fc.isBelowThreshold()); + assertEquals(fc.getThreshold(), 3f); + + assertEquals(fr.getFeatureFilter("type1").toStableString(), + "Label Contains x"); + assertEquals(fr.getFeatureFilter("type2").toStableString(), + "(Score LE 2.4) AND (Score GT 1.1)"); + assertEquals(fr.getFeatureFilter("type3").toStableString(), + "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)"); + } + + private void addFeature(SequenceI seq, String featureType, int score) + { + SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2, + score, "grp"); + sf.setValue("AF", score); + sf.setValue("CSQ", new HashMap() + { + { + put("PolyPhen", Integer.toString(score)); + } + }); + seq.addSequenceFeature(sf); + } + + /** + * Adds two features of the given type to the given sequence, also setting the + * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen" + * + * @param seq + * @param featureType + * @param score + */ + private void addFeatures(SequenceI seq, String featureType, int score) + { + addFeature(seq, featureType, score++); + addFeature(seq, featureType, score); + } } diff --git a/test/jalview/io/SequenceAnnotationReportTest.java b/test/jalview/io/SequenceAnnotationReportTest.java index 9e61bec..87e35c7 100644 --- a/test/jalview/io/SequenceAnnotationReportTest.java +++ b/test/jalview/io/SequenceAnnotationReportTest.java @@ -23,15 +23,18 @@ package jalview.io; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; +import jalview.api.FeatureColourI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.gui.JvOptionPane; import jalview.io.gff.GffConstants; +import jalview.renderer.seqfeatures.FeatureRenderer; +import jalview.schemes.FeatureColour; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; -import java.util.HashMap; -import java.util.Hashtable; +import java.awt.Color; import java.util.Map; import junit.extensions.PA; @@ -95,8 +98,9 @@ public class SequenceAnnotationReportTest SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f, "group"); - Map minmax = new Hashtable(); - sar.appendFeature(sb, 1, minmax, sf); + FeatureRendererModel fr = new FeatureRenderer(null); + Map minmax = fr.getMinMax(); + sar.appendFeature(sb, 1, fr, sf); /* * map has no entry for this feature type - score is not shown: */ @@ -106,7 +110,7 @@ public class SequenceAnnotationReportTest * map has entry for this feature type - score is shown: */ minmax.put("METAL", new float[][] { { 0f, 1f }, null }); - sar.appendFeature(sb, 1, minmax, sf); + sar.appendFeature(sb, 1, fr, sf); //
    is appended to a buffer > 6 in length assertEquals("METAL 1 3; Fe2-S
    METAL 1 3; Fe2-S Score=1.3", sb.toString()); @@ -116,7 +120,7 @@ public class SequenceAnnotationReportTest */ minmax.put("METAL", new float[][] { { 2f, 2f }, null }); sb.setLength(0); - sar.appendFeature(sb, 1, minmax, sf); + sar.appendFeature(sb, 1, fr, sf); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @@ -132,8 +136,11 @@ public class SequenceAnnotationReportTest assertEquals("METAL 1 3; Fe2-S", sb.toString()); } + /** + * A specific attribute value is included if it is used to colour the feature + */ @Test(groups = "Functional") - public void testAppendFeature_clinicalSignificance() + public void testAppendFeature_colouredByAttribute() { SequenceAnnotationReport sar = new SequenceAnnotationReport(null); StringBuilder sb = new StringBuilder(); @@ -141,12 +148,35 @@ public class SequenceAnnotationReportTest Float.NaN, "group"); sf.setValue("clinical_significance", "Benign"); - sar.appendFeature(sb, 1, null, sf); - assertEquals("METAL 1 3; Fe2-S; Benign", sb.toString()); + /* + * first with no colour by attribute + */ + FeatureRendererModel fr = new FeatureRenderer(null); + sar.appendFeature(sb, 1, fr, sf); + assertEquals("METAL 1 3; Fe2-S", sb.toString()); + + /* + * then with colour by an attribute the feature lacks + */ + FeatureColourI fc = new FeatureColour(Color.white, Color.black, 5, 10); + fc.setAttributeName("Pfam"); + fr.setColour("METAL", fc); + sb.setLength(0); + sar.appendFeature(sb, 1, fr, sf); + assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change + + /* + * then with colour by an attribute the feature has + */ + fc.setAttributeName("clinical_significance"); + sb.setLength(0); + sar.appendFeature(sb, 1, fr, sf); + assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign", + sb.toString()); } @Test(groups = "Functional") - public void testAppendFeature_withScoreStatusClinicalSignificance() + public void testAppendFeature_withScoreStatusAttribute() { SequenceAnnotationReport sar = new SequenceAnnotationReport(null); StringBuilder sb = new StringBuilder(); @@ -154,11 +184,17 @@ public class SequenceAnnotationReportTest "group"); sf.setStatus("Confirmed"); sf.setValue("clinical_significance", "Benign"); - Map minmax = new Hashtable(); + + FeatureRendererModel fr = new FeatureRenderer(null); + Map minmax = fr.getMinMax(); + FeatureColourI fc = new FeatureColour(Color.white, Color.blue, 12, 22); + fc.setAttributeName("clinical_significance"); + fr.setColour("METAL", fc); minmax.put("METAL", new float[][] { { 0f, 1f }, null }); - sar.appendFeature(sb, 1, minmax, sf); + sar.appendFeature(sb, 1, fr, sf); - assertEquals("METAL 1 3; Fe2-S Score=1.3; (Confirmed); Benign", + assertEquals( + "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign", sb.toString()); } @@ -226,7 +262,7 @@ public class SequenceAnnotationReportTest null)); sb.setLength(0); sar.createSequenceAnnotationReport(sb, seq, true, true, null); - String expected = "
    SeqDesc
    Type1 ; Nonpos
    "; + String expected = "
    SeqDesc
    Type1 ; Nonpos Score=1.0
    "; assertEquals(expected, sb.toString()); /* @@ -244,10 +280,13 @@ public class SequenceAnnotationReportTest */ seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f, null)); - Map minmax = new HashMap(); + + FeatureRendererModel fr = new FeatureRenderer(null); + Map 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 = "
    SeqDesc
    Metal ; Desc
    Type1 ; Nonpos
    "; assertEquals(expected, sb.toString()); @@ -260,19 +299,20 @@ public class SequenceAnnotationReportTest sf.setValue("linkonly", Boolean.TRUE); seq.addSequenceFeature(sf); sb.setLength(0); - sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); + sar.createSequenceAnnotationReport(sb, seq, true, true, fr); assertEquals(expected, sb.toString()); // unchanged! /* - * 'clinical_significance' currently being specially included + * 'clinical_significance' attribute only included when + * used for feature colouring */ SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0, 5f, null); sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign"); seq.addSequenceFeature(sf2); sb.setLength(0); - sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); - expected = "
    SeqDesc
    Metal ; Desc
    Type1 ; Nonpos
    Variant ; Havana; benign
    "; + sar.createSequenceAnnotationReport(sb, seq, true, true, fr); + expected = "
    SeqDesc
    Metal ; Desc
    Type1 ; Nonpos
    Variant ; Havana
    "; assertEquals(expected, sb.toString()); /* @@ -280,18 +320,24 @@ public class SequenceAnnotationReportTest */ seq.addDBRef(new DBRefEntry("PDB", "0", "3iu1")); seq.addDBRef(new DBRefEntry("Uniprot", "1", "P30419")); + // with showDbRefs = false sb.setLength(0); - sar.createSequenceAnnotationReport(sb, seq, false, true, minmax); + sar.createSequenceAnnotationReport(sb, seq, false, true, fr); assertEquals(expected, sb.toString()); // unchanged - // with showDbRefs = true + + // with showDbRefs = true, colour Variant features by clinical_significance sb.setLength(0); - sar.createSequenceAnnotationReport(sb, seq, true, true, minmax); - expected = "
    SeqDesc
    UNIPROT P30419
    PDB 3iu1
    Metal ; Desc
    Type1 ; Nonpos
    Variant ; Havana; benign
    "; + 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 = "
    SeqDesc
    UNIPROT P30419
    PDB 3iu1
    Metal ; Desc
    " + + "Type1 ; Nonpos
    Variant ; Havana; clinical_significance=benign
    "; 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 = "
    SeqDesc
    UNIPROT P30419
    PDB 3iu1
    "; assertEquals(expected, sb.toString()); diff --git a/test/jalview/io/gff/SequenceOntologyLiteTest.java b/test/jalview/io/gff/SequenceOntologyLiteTest.java new file mode 100644 index 0000000..0766666 --- /dev/null +++ b/test/jalview/io/gff/SequenceOntologyLiteTest.java @@ -0,0 +1,37 @@ +package jalview.io.gff; + +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +import org.testng.annotations.Test; + +public class SequenceOntologyLiteTest +{ + @Test(groups = "Functional") + public void testIsA_sequenceVariant() + { + SequenceOntologyI so = new SequenceOntologyLite(); + + assertFalse(so.isA("CDS", "sequence_variant")); + assertTrue(so.isA("sequence_variant", "sequence_variant")); + + /* + * these should all be sub-types of sequence_variant + */ + assertTrue(so.isA("structural_variant", "sequence_variant")); + assertTrue(so.isA("feature_variant", "sequence_variant")); + assertTrue(so.isA("gene_variant", "sequence_variant")); + assertTrue(so.isA("transcript_variant", "sequence_variant")); + assertTrue(so.isA("NMD_transcript_variant", "sequence_variant")); + assertTrue(so.isA("missense_variant", "sequence_variant")); + assertTrue(so.isA("synonymous_variant", "sequence_variant")); + assertTrue(so.isA("frameshift_variant", "sequence_variant")); + assertTrue(so.isA("5_prime_UTR_variant", "sequence_variant")); + assertTrue(so.isA("3_prime_UTR_variant", "sequence_variant")); + assertTrue(so.isA("stop_gained", "sequence_variant")); + assertTrue(so.isA("stop_lost", "sequence_variant")); + assertTrue(so.isA("inframe_deletion", "sequence_variant")); + assertTrue(so.isA("inframe_insertion", "sequence_variant")); + assertTrue(so.isA("splice_region_variant", "sequence_variant")); + } +} diff --git a/test/jalview/io/vcf/VCFLoaderTest.java b/test/jalview/io/vcf/VCFLoaderTest.java new file mode 100644 index 0000000..7099282 --- /dev/null +++ b/test/jalview/io/vcf/VCFLoaderTest.java @@ -0,0 +1,681 @@ +package jalview.io.vcf; + +import static org.testng.Assert.assertEquals; + +import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.DBRefEntry; +import jalview.datamodel.Mapping; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.SequenceFeatures; +import jalview.gui.AlignFrame; +import jalview.io.DataSourceType; +import jalview.io.FileLoader; +import jalview.io.gff.Gff3Helper; +import jalview.io.gff.SequenceOntologyI; +import jalview.util.MapList; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class VCFLoaderTest +{ + private static final float DELTA = 0.00001f; + + // columns 9717- of gene P30419 from Ensembl (much modified) + private static final String FASTA = "" + + + /* + * forward strand 'gene' and 'transcript' with two exons + */ + ">gene1/1-25 chromosome:GRCh38:17:45051610:45051634:1\n" + + "CAAGCTGGCGGACGAGAGTGTGACA\n" + + ">transcript1/1-18\n--AGCTGGCG----AGAGTGTGAC-\n" + + /* + * reverse strand gene and transcript (reverse complement alleles!) + */ + + ">gene2/1-25 chromosome:GRCh38:17:45051610:45051634:-1\n" + + "TGTCACACTCTCGTCCGCCAGCTTG\n" + + ">transcript2/1-18\n" + "-GTCACACTCT----CGCCAGCT--\n" + + /* + * 'gene' on chromosome 5 with two transcripts + */ + + ">gene3/1-25 chromosome:GRCh38:5:45051610:45051634:1\n" + + "CAAGCTGGCGGACGAGAGTGTGACA\n" + + ">transcript3/1-18\n--AGCTGGCG----AGAGTGTGAC-\n" + + ">transcript4/1-18\n-----TGG-GGACGAGAGTGTGA-A\n"; + + private static final String[] VCF = { "##fileformat=VCFv4.2", + "##INFO=", + "##reference=Homo_sapiens/GRCh38", + "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO", + // A/T,C variants in position 2 of gene sequence (precedes transcript) + // should create 2 variant features with respective scores + "17\t45051611\t.\tA\tT,C\t1666.64\tRF\tAC=15;AF=5.0e-03,4.0e-03", + // SNP G/C in position 4 of gene sequence, position 2 of transcript + // insertion G/GA is transferred to nucleotide but not to peptide + "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.0e-03,2.0e-03" }; + + @BeforeClass + public void setUp() + { + /* + * configure to capture all available VCF and VEP (CSQ) fields + */ + Cache.loadProperties("test/jalview/io/testProps.jvprops"); + Cache.setProperty("VCF_FIELDS", ".*"); + Cache.setProperty("VEP_FIELDS", ".*"); + Cache.initLogger(); + } + + @Test(groups = "Functional") + public void testDoLoad() throws IOException + { + AlignmentI al = buildAlignment(); + + File f = makeVcf(); + VCFLoader loader = new VCFLoader(f.getPath()); + + loader.doLoad(al.getSequencesArray(), null); + + /* + * verify variant feature(s) added to gene + * NB alleles at a locus may not be processed, and features added, + * in the order in which they appear in the VCF record as method + * VariantContext.getAlternateAlleles() does not guarantee order + * - order of assertions here matches what we find (is not important) + */ + List 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 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 proteinFeatures = peptide.getSequenceFeatures(); + assertEquals(proteinFeatures.size(), 1); + sf = proteinFeatures.get(0); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 1); + assertEquals(sf.getEnd(), 1); + assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); + assertEquals(sf.getDescription(), "p.Ser1Thr"); + } + + private File makeVcf() throws IOException + { + File f = File.createTempFile("Test", ".vcf"); + f.deleteOnExit(); + PrintWriter pw = new PrintWriter(f); + for (String vcfLine : VCF) + { + pw.println(vcfLine); + } + pw.close(); + return f; + } + + /** + * Make a simple alignment with one 'gene' and one 'transcript' + * + * @return + */ + private AlignmentI buildAlignment() + { + AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(FASTA, + DataSourceType.PASTE); + + /* + * map gene1 sequence to chromosome (normally done when the sequence is fetched + * from Ensembl and transcripts computed) + */ + AlignmentI alignment = af.getViewport().getAlignment(); + SequenceI gene1 = alignment.findName("gene1"); + int[] to = new int[] { 45051610, 45051634 }; + int[] from = new int[] { gene1.getStart(), gene1.getEnd() }; + gene1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to, + 1, 1)); + + /* + * map 'transcript1' to chromosome via 'gene1' + * transcript1/1-18 is gene1/3-10,15-24 + * which is chromosome 45051612-45051619,45051624-45051633 + */ + to = new int[] { 45051612, 45051619, 45051624, 45051633 }; + SequenceI transcript1 = alignment.findName("transcript1"); + from = new int[] { transcript1.getStart(), transcript1.getEnd() }; + transcript1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList( + from, to, + 1, 1)); + + /* + * map gene2 to chromosome reverse strand + */ + SequenceI gene2 = alignment.findName("gene2"); + to = new int[] { 45051634, 45051610 }; + from = new int[] { gene2.getStart(), gene2.getEnd() }; + gene2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to, + 1, 1)); + + /* + * map 'transcript2' to chromosome via 'gene2' + * transcript2/1-18 is gene2/2-11,16-23 + * which is chromosome 45051633-45051624,45051619-45051612 + */ + to = new int[] { 45051633, 45051624, 45051619, 45051612 }; + SequenceI transcript2 = alignment.findName("transcript2"); + from = new int[] { transcript2.getStart(), transcript2.getEnd() }; + transcript2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList( + from, to, + 1, 1)); + + /* + * add a protein product as a DBRef on transcript1 + */ + SequenceI peptide1 = new Sequence("ENSP001", "SWRECD"); + MapList mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, + 3, 1); + Mapping map = new Mapping(peptide1, mapList); + DBRefEntry product = new DBRefEntry("", "", "ENSP001", map); + transcript1.addDBRef(product); + + /* + * add a protein product as a DBRef on transcript2 + */ + SequenceI peptide2 = new Sequence("ENSP002", "VTLSPA"); + mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1); + map = new Mapping(peptide2, mapList); + product = new DBRefEntry("", "", "ENSP002", map); + transcript2.addDBRef(product); + + /* + * map gene3 to chromosome + */ + SequenceI gene3 = alignment.findName("gene3"); + to = new int[] { 45051610, 45051634 }; + from = new int[] { gene3.getStart(), gene3.getEnd() }; + gene3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(from, to, + 1, 1)); + + /* + * map 'transcript3' to chromosome + */ + SequenceI transcript3 = alignment.findName("transcript3"); + to = new int[] { 45051612, 45051619, 45051624, 45051633 }; + from = new int[] { transcript3.getStart(), transcript3.getEnd() }; + transcript3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList( + from, to, + 1, 1)); + + /* + * map 'transcript4' to chromosome + */ + SequenceI transcript4 = alignment.findName("transcript4"); + to = new int[] { 45051615, 45051617, 45051619, 45051632, 45051634, + 45051634 }; + from = new int[] { transcript4.getStart(), transcript4.getEnd() }; + transcript4.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList( + from, to, + 1, 1)); + + /* + * add a protein product as a DBRef on transcript3 + */ + SequenceI peptide3 = new Sequence("ENSP003", "SWRECD"); + mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1); + map = new Mapping(peptide3, mapList); + product = new DBRefEntry("", "", "ENSP003", map); + transcript3.addDBRef(product); + + return alignment; + } + + /** + * Test with 'gene' and 'transcript' mapped to the reverse strand of the + * chromosome. The VCF variant positions (in forward coordinates) should get + * correctly located on sequence positions. + * + * @throws IOException + */ + @Test(groups = "Functional") + public void testDoLoad_reverseStrand() throws IOException + { + AlignmentI al = buildAlignment(); + + File f = makeVcf(); + + VCFLoader loader = new VCFLoader(f.getPath()); + + loader.doLoad(al.getSequencesArray(), null); + + /* + * verify variant feature(s) added to gene2 + * gene2/1-25 maps to chromosome 45051634- reverse strand + */ + List geneFeatures = al.getSequenceAt(2) + .getSequenceFeatures(); + SequenceFeatures.sortFeatures(geneFeatures, true); + assertEquals(geneFeatures.size(), 4); + + /* + * variant A/T at 45051611 maps to T/A at gene position 24 + */ + SequenceFeature sf = geneFeatures.get(3); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 24); + assertEquals(sf.getEnd(), 24); + assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT); + assertEquals(sf.getScore(), 5.0e-03, DELTA); + assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,A"); + + /* + * variant A/C at 45051611 maps to T/G at gene position 24 + */ + sf = geneFeatures.get(2); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 24); + assertEquals(sf.getEnd(), 24); + assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT); + assertEquals(sf.getScore(), 4.0e-03, DELTA); + assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,G"); + + /* + * variant G/C at 45051613 maps to C/G at gene position 22 + */ + sf = geneFeatures.get(1); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 22); + assertEquals(sf.getEnd(), 22); + assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT); + assertEquals(sf.getScore(), 2.0e-03, DELTA); + assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G"); + + /* + * insertion G/GA at 45051613 maps to an insertion at + * the preceding position (21) on reverse strand gene + * reference: CAAGC -> GCTTG/21-25 + * genomic variant: CAAGAC (G/GA) + * gene variant: GTCTTG (G/GT at 21) + */ + sf = geneFeatures.get(0); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 21); + assertEquals(sf.getEnd(), 21); + assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT); + assertEquals(sf.getScore(), 3.0e-03, DELTA); + assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT"); + + /* + * verify 2 variant features added to transcript2 + */ + List transcriptFeatures = al.getSequenceAt(3) + .getSequenceFeatures(); + assertEquals(transcriptFeatures.size(), 2); + + /* + * insertion G/GT at position 21 of gene maps to position 16 of transcript + */ + sf = transcriptFeatures.get(0); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 16); + assertEquals(sf.getEnd(), 16); + assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT); + assertEquals(sf.getScore(), 3.0e-03, DELTA); + assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT"); + + /* + * SNP C/G at position 22 of gene maps to position 17 of transcript + */ + sf = transcriptFeatures.get(1); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 17); + assertEquals(sf.getEnd(), 17); + assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT); + assertEquals(sf.getScore(), 2.0e-03, DELTA); + assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G"); + + /* + * verify variant feature(s) computed and added to protein + * last codon GCT varies to GGT giving A/G in the last peptide position + */ + DBRefEntry[] dbRefs = al.getSequenceAt(3).getDBRefs(); + SequenceI peptide = null; + for (DBRefEntry dbref : dbRefs) + { + if (dbref.getMap().getMap().getFromRatio() == 3) + { + peptide = dbref.getMap().getTo(); + } + } + List proteinFeatures = peptide.getSequenceFeatures(); + assertEquals(proteinFeatures.size(), 1); + sf = proteinFeatures.get(0); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 6); + assertEquals(sf.getEnd(), 6); + assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); + assertEquals(sf.getDescription(), "p.Ala6Gly"); + } + + /** + * Tests that if VEP consequence (CSQ) data is present in the VCF data, then + * it is added to the variant feature, but restricted where possible to the + * consequences for a specific transcript + * + * @throws IOException + */ + @Test(groups = "Functional") + public void testDoLoad_vepCsq() throws IOException + { + AlignmentI al = buildAlignment(); + + VCFLoader loader = new VCFLoader("test/jalview/io/vcf/testVcf.vcf"); + + /* + * VCF data file with variants at gene3 positions + * 1 C/A + * 5 C/T + * 9 CGT/C (deletion) + * 13 C/G, C/T + * 17 A/AC (insertion), A/G + */ + loader.doLoad(al.getSequencesArray(), null); + + /* + * verify variant feature(s) added to gene3 + */ + List 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 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 proteinFeatures = peptide.getSequenceFeatures(); + SequenceFeatures.sortFeatures(proteinFeatures, true); + assertEquals(proteinFeatures.size(), 2); + sf = proteinFeatures.get(0); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 1); + assertEquals(sf.getEnd(), 1); + assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT); + assertEquals(sf.getDescription(), "AGT"); + sf = proteinFeatures.get(1); + assertEquals(sf.getFeatureGroup(), "VCF"); + assertEquals(sf.getBegin(), 4); + assertEquals(sf.getEnd(), 4); + assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT); + assertEquals(sf.getDescription(), "p.Glu4Gly"); + + /* + * verify variant feature(s) added to transcript4 + * at columns 13 (2) and 17 (2), positions 7 and 11 + */ + transcriptFeatures = al.findName("transcript4").getSequenceFeatures(); + SequenceFeatures.sortFeatures(transcriptFeatures, true); + assertEquals(transcriptFeatures.size(), 4); + sf = transcriptFeatures.get(0); + assertEquals(sf.getBegin(), 7); + assertEquals(sf.getEnd(), 7); + assertEquals(sf.getScore(), 0.5f, DELTA); + assertEquals(sf.getValue("alleles"), "C,T"); + assertEquals(map.size(), 9); + assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4"); + + sf = transcriptFeatures.get(1); + assertEquals(sf.getBegin(), 7); + assertEquals(sf.getEnd(), 7); + assertEquals(sf.getScore(), 0.4f, DELTA); + assertEquals(sf.getValue("alleles"), "C,G"); + assertEquals(map.size(), 9); + assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4"); + + sf = transcriptFeatures.get(2); + assertEquals(sf.getBegin(), 11); + assertEquals(sf.getEnd(), 11); + assertEquals(sf.getScore(), 0.7f, DELTA); + assertEquals(sf.getValue("alleles"), "A,G"); + assertEquals(map.size(), 9); + assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4"); + + sf = transcriptFeatures.get(3); + assertEquals(sf.getBegin(), 11); + assertEquals(sf.getEnd(), 11); + assertEquals(sf.getScore(), 0.6f, DELTA); + assertEquals(sf.getValue("alleles"), "A,AC"); + assertEquals(map.size(), 9); + assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4"); + } + + /** + * A test that demonstrates loading a contig sequence from an indexed sequence + * database which is the reference for a VCF file + * + * @throws IOException + */ + @Test(groups = "Functional") + public void testLoadVCFContig() throws IOException + { + VCFLoader loader = new VCFLoader( + "test/jalview/io/vcf/testVcf2.vcf"); + + SequenceI seq = loader.loadVCFContig("contig123"); + assertEquals(seq.getLength(), 15); + assertEquals(seq.getSequenceAsString(), "AAAAACCCCCGGGGG"); + List features = seq.getSequenceFeatures(); + SequenceFeatures.sortFeatures(features, true); + assertEquals(features.size(), 2); + SequenceFeature sf = features.get(0); + assertEquals(sf.getBegin(), 8); + assertEquals(sf.getEnd(), 8); + assertEquals(sf.getDescription(), "C,A"); + sf = features.get(1); + assertEquals(sf.getBegin(), 12); + assertEquals(sf.getEnd(), 12); + assertEquals(sf.getDescription(), "G,T"); + + seq = loader.loadVCFContig("contig789"); + assertEquals(seq.getLength(), 25); + assertEquals(seq.getSequenceAsString(), "GGGGGTTTTTAAAAACCCCCGGGGG"); + features = seq.getSequenceFeatures(); + SequenceFeatures.sortFeatures(features, true); + assertEquals(features.size(), 2); + sf = features.get(0); + assertEquals(sf.getBegin(), 2); + assertEquals(sf.getEnd(), 2); + assertEquals(sf.getDescription(), "G,T"); + sf = features.get(1); + assertEquals(sf.getBegin(), 21); + assertEquals(sf.getEnd(), 21); + assertEquals(sf.getDescription(), "G,A"); + + seq = loader.loadVCFContig("contig456"); + assertEquals(seq.getLength(), 20); + assertEquals(seq.getSequenceAsString(), "CCCCCGGGGGTTTTTAAAAA"); + features = seq.getSequenceFeatures(); + SequenceFeatures.sortFeatures(features, true); + assertEquals(features.size(), 1); + sf = features.get(0); + assertEquals(sf.getBegin(), 15); + assertEquals(sf.getEnd(), 15); + assertEquals(sf.getDescription(), "T,C"); + } +} \ No newline at end of file diff --git a/test/jalview/io/vcf/contigs.fasta b/test/jalview/io/vcf/contigs.fasta new file mode 100644 index 0000000..ec839b6 --- /dev/null +++ b/test/jalview/io/vcf/contigs.fasta @@ -0,0 +1,6 @@ +>contig123 +AAAAACCCCCGGGGG +>contig456 +CCCCCGGGGGTTTTTAAAAA +>contig789 +GGGGGTTTTTAAAAACCCCCGGGGG diff --git a/test/jalview/io/vcf/contigs.fasta.fai b/test/jalview/io/vcf/contigs.fasta.fai new file mode 100644 index 0000000..e9f5067 --- /dev/null +++ b/test/jalview/io/vcf/contigs.fasta.fai @@ -0,0 +1,3 @@ +contig123 15 11 15 16 +contig456 20 38 20 21 +contig789 25 70 25 26 diff --git a/test/jalview/io/vcf/testVcf.dat b/test/jalview/io/vcf/testVcf.dat new file mode 100644 index 0000000..77e070c --- /dev/null +++ b/test/jalview/io/vcf/testVcf.dat @@ -0,0 +1,13 @@ +##fileformat=VCFv4.2 +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##reference=/Homo_sapiens/GRCh38 +#CHROM POS ID REF ALT QUAL FILTER INFO +5 45051610 . C A 81.96 RF;AC0 AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051614 . C T 1666.64 RF AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051618 . CGG C 41.94 AC0 AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051622 . C G,T 224.23 RF;AC0 AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051626 . A AC,G 433.35 RF;AC0 AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad diff --git a/test/jalview/io/vcf/testVcf.vcf b/test/jalview/io/vcf/testVcf.vcf new file mode 100644 index 0000000..77e070c --- /dev/null +++ b/test/jalview/io/vcf/testVcf.vcf @@ -0,0 +1,13 @@ +##fileformat=VCFv4.2 +##INFO= +##INFO= +##INFO= +##INFO= +##INFO= +##reference=/Homo_sapiens/GRCh38 +#CHROM POS ID REF ALT QUAL FILTER INFO +5 45051610 . C A 81.96 RF;AC0 AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051614 . C T 1666.64 RF AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051618 . CGG C 41.94 AC0 AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051622 . C G,T 224.23 RF;AC0 AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad +5 45051626 . A AC,G 433.35 RF;AC0 AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad diff --git a/test/jalview/io/vcf/testVcf2.vcf b/test/jalview/io/vcf/testVcf2.vcf new file mode 100644 index 0000000..aa3792a --- /dev/null +++ b/test/jalview/io/vcf/testVcf2.vcf @@ -0,0 +1,13 @@ +##fileformat=VCFv4.2 +##INFO= +##contig= +##contig= +##contig= +##INFO= +##reference=test/jalview/io/vcf/contigs.fasta +#CHROM POS ID REF ALT QUAL FILTER INFO +contig123 8 . C A 81.96 . AC=1;AF=0.1 +contig123 12 . G T 1666.64 . AC=1;AF=0.2 +contig456 15 . T C 41.94 . AC=1;AF=0.3 +contig789 2 . G T 224.23 . AC=1,2;AF=0 +contig789 21 . G A 433.35 . AC=3;AF=0.6 diff --git a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java index f6dfed6..d8b905e 100644 --- a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java +++ b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java @@ -15,6 +15,7 @@ import jalview.gui.FeatureRenderer; import jalview.io.DataSourceType; import jalview.io.FileLoader; import jalview.schemes.FeatureColour; +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; import java.awt.Color; import java.util.List; @@ -172,9 +173,9 @@ public class FeatureColourFinderTest * - currently no way other than mimicking reordering of * table in Feature Settings */ - Object[][] data = new Object[2][]; - data[0] = new Object[] { "Metal", red, true }; - data[1] = new Object[] { "Domain", green, true }; + FeatureSettingsBean[] data = new FeatureSettingsBean[2]; + data[0] = new FeatureSettingsBean("Metal", red, null, true); + data[1] = new FeatureSettingsBean("Domain", green, null, true); fr.setFeaturePriority(data); c = finder.findFeatureColour(Color.blue, seq, 10); assertEquals(c, Color.red); @@ -182,7 +183,7 @@ public class FeatureColourFinderTest /* * ..and turn off display of Metal */ - data[0][2] = false; + data[0] = new FeatureSettingsBean("Metal", red, null, false); fr.setFeaturePriority(data); c = finder.findFeatureColour(Color.blue, seq, 10); assertEquals(c, Color.green); @@ -216,8 +217,8 @@ public class FeatureColourFinderTest /* * turn off display of Metal - is this the easiest way to do it?? */ - Object[][] data = new Object[1][]; - data[0] = new Object[] { "Metal", red, false }; + FeatureSettingsBean[] data = new FeatureSettingsBean[1]; + data[0] = new FeatureSettingsBean("Metal", red, null, false); fr.setFeaturePriority(data); c = finder.findFeatureColour(Color.blue, seq, 10); assertEquals(c, Color.blue); @@ -225,7 +226,7 @@ public class FeatureColourFinderTest /* * turn display of Metal back on */ - data[0] = new Object[] { "Metal", red, true }; + data[0] = new FeatureSettingsBean("Metal", red, null, true); fr.setFeaturePriority(data); c = finder.findFeatureColour(Color.blue, seq, 10); assertEquals(c, Color.red); @@ -399,9 +400,9 @@ public class FeatureColourFinderTest * 1) 0.6 * green(0, 255, 0) + 0.4 * cyan(0, 255, 255) = (0, 255, 102) * 2) 0.6* red(255, 0, 0) + 0.4 * (0, 255, 102) = (153, 102, 41) rounded */ - Object[][] data = new Object[2][]; - data[0] = new Object[] { "Metal", red, true }; - data[1] = new Object[] { "Domain", green, true }; + FeatureSettingsBean[] data = new FeatureSettingsBean[2]; + data[0] = new FeatureSettingsBean("Metal", red, null, true); + data[1] = new FeatureSettingsBean("Domain", green, null, true); fr.setFeaturePriority(data); c = finder.findFeatureColour(Color.cyan, seq, 10); assertEquals(c, new Color(153, 102, 41)); @@ -411,7 +412,7 @@ public class FeatureColourFinderTest * Domain (green) above background (pink) * 0.6 * green(0, 255, 0) + 0.4 * pink(255, 175, 175) = (102, 223, 70) */ - data[0][2] = false; + data[0] = new FeatureSettingsBean("Metal", red, null, false); fr.setFeaturePriority(data); c = finder.findFeatureColour(Color.pink, seq, 10); assertEquals(c, new Color(102, 223, 70)); @@ -447,8 +448,8 @@ public class FeatureColourFinderTest /* * turn off display of Metal */ - Object[][] data = new Object[1][]; - data[0] = new Object[] { "Metal", red, false }; + FeatureSettingsBean[] data = new FeatureSettingsBean[1]; + data[0] = new FeatureSettingsBean("Metal", red, null, false); fr.setFeaturePriority(data); assertTrue(finder.noFeaturesDisplayed()); @@ -503,9 +504,9 @@ public class FeatureColourFinderTest /* * render order is kd above Metal */ - Object[][] data = new Object[2][]; - data[0] = new Object[] { kdFeature, fc, true }; - data[1] = new Object[] { metalFeature, green, true }; + FeatureSettingsBean[] data = new FeatureSettingsBean[2]; + data[0] = new FeatureSettingsBean(kdFeature, fc, null, true); + data[1] = new FeatureSettingsBean(metalFeature, green, null, true); fr.setFeaturePriority(data); av.setShowSequenceFeatures(true); diff --git a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java index d3cddf9..cebef11 100644 --- a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java +++ b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java @@ -2,20 +2,27 @@ package jalview.renderer.seqfeatures; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.gui.AlignFrame; import jalview.io.DataSourceType; import jalview.io.FileLoader; import jalview.schemes.FeatureColour; +import jalview.util.matcher.Condition; +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; import java.awt.Color; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,9 +68,8 @@ public class FeatureRendererTest seqs.get(2).addSequenceFeature( new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup")); // bug in findAllFeatures - group not checked for a known feature type - seqs.get(2).addSequenceFeature( - new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, - "RfamGroup")); + seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9, + Float.NaN, "RfamGroup")); // existing feature type with null group seqs.get(3).addSequenceFeature( new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null)); @@ -116,13 +122,14 @@ public class FeatureRendererTest * change render order (todo: an easier way) * nb here last comes first in the data array */ - Object[][] data = new Object[3][]; + FeatureSettingsBean[] data = new FeatureSettingsBean[3]; FeatureColourI colour = new FeatureColour(Color.RED); - data[0] = new Object[] { "Rfam", colour, true }; - data[1] = new Object[] { "Pfam", colour, false }; - data[2] = new Object[] { "Scop", colour, false }; + data[0] = new FeatureSettingsBean("Rfam", colour, null, true); + data[1] = new FeatureSettingsBean("Pfam", colour, null, false); + data[2] = new FeatureSettingsBean("Scop", colour, null, false); fr.setFeaturePriority(data); - assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam")); + assertEquals(fr.getRenderOrder(), + Arrays.asList("Scop", "Pfam", "Rfam")); assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam")); /* @@ -217,12 +224,13 @@ public class FeatureRendererTest /* * make "Type2" not displayed */ - Object[][] data = new Object[4][]; FeatureColourI colour = new FeatureColour(Color.RED); - data[0] = new Object[] { "Type1", colour, true }; - data[1] = new Object[] { "Type2", colour, false }; - data[2] = new Object[] { "Type3", colour, true }; - data[3] = new Object[] { "Disulphide Bond", colour, true }; + FeatureSettingsBean[] data = new FeatureSettingsBean[4]; + data[0] = new FeatureSettingsBean("Type1", colour, null, true); + data[1] = new FeatureSettingsBean("Type2", colour, null, false); + data[2] = new FeatureSettingsBean("Type3", colour, null, true); + data[3] = new FeatureSettingsBean("Disulphide Bond", colour, null, + true); fr.setFeaturePriority(data); features = fr.findFeaturesAtColumn(seq, 15); @@ -252,6 +260,37 @@ public class FeatureRendererTest features = fr.findFeaturesAtColumn(seq, 5); assertEquals(features.size(), 1); assertTrue(features.contains(sf8)); + + /* + * give "Type3" features a graduated colour scheme + * - first with no threshold + */ + FeatureColourI gc = new FeatureColour(Color.yellow, Color.red, null, 0f, + 10f); + fr.getFeatureColours().put("Type3", gc); + features = fr.findFeaturesAtColumn(seq, 8); + assertTrue(features.contains(sf4)); + // now with threshold > 2f - feature score of 1f is excluded + gc.setAboveThreshold(true); + gc.setThreshold(2f); + features = fr.findFeaturesAtColumn(seq, 8); + assertFalse(features.contains(sf4)); + + /* + * make "Type3" graduated colour by attribute "AF" + * - first with no attribute held - feature should be excluded + */ + gc.setAttributeName("AF"); + features = fr.findFeaturesAtColumn(seq, 8); + assertFalse(features.contains(sf4)); + // now with the attribute above threshold - should be included + sf4.setValue("AF", "2.4"); + features = fr.findFeaturesAtColumn(seq, 8); + assertTrue(features.contains(sf4)); + // now with the attribute below threshold - should be excluded + sf4.setValue("AF", "1.4"); + features = fr.findFeaturesAtColumn(seq, 8); + assertFalse(features.contains(sf4)); } @Test(groups = "Functional") @@ -264,7 +303,7 @@ public class FeatureRendererTest FeatureRenderer fr = new FeatureRenderer(av); List 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, @@ -297,7 +336,7 @@ public class FeatureRendererTest * filter out duplicate (co-located) features * note: which gets removed is not guaranteed */ - fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue)); + fr.filterFeaturesForDisplay(features); assertEquals(features.size(), 3); assertTrue(features.contains(sf1) || features.contains(sf4)); assertFalse(features.contains(sf1) && features.contains(sf4)); @@ -306,58 +345,166 @@ public class FeatureRendererTest assertTrue(features.contains(sf5)); /* - * hide group 3 - sf3 is removed, sf2 is retained + * hide groups 2 and 3 makes no difference to this method */ + fr.setGroupVisibility("group2", false); fr.setGroupVisibility("group3", false); features = seq.getSequenceFeatures(); - fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue)); + fr.filterFeaturesForDisplay(features); assertEquals(features.size(), 3); assertTrue(features.contains(sf1) || features.contains(sf4)); assertFalse(features.contains(sf1) && features.contains(sf4)); - assertTrue(features.contains(sf2)); - assertFalse(features.contains(sf3)); + assertTrue(features.contains(sf2) || features.contains(sf3)); + assertFalse(features.contains(sf2) && features.contains(sf3)); assertTrue(features.contains(sf5)); + } + + @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 + * - feature type visibility should not affect the result */ - 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); + assertEquals(fr.getColour(sf1), Color.red); + 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 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); } } diff --git a/test/jalview/schemes/Blosum62ColourSchemeTest.java b/test/jalview/schemes/Blosum62ColourSchemeTest.java index 0b5b6bd..030a90f 100644 --- a/test/jalview/schemes/Blosum62ColourSchemeTest.java +++ b/test/jalview/schemes/Blosum62ColourSchemeTest.java @@ -20,7 +20,7 @@ public class Blosum62ColourSchemeTest *
*