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