From: BobHanson Date: Thu, 9 Apr 2020 17:53:04 +0000 (-0500) Subject: 3253-omnibus save X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=6200addf078b7f7ace90597dc056dafc7fc602c1;p=jalview.git 3253-omnibus save --- diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index 2946ba2..0c40873 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -20,8 +20,7 @@ */ package jalview.analysis; -import static jalview.io.gff.GffConstants.CLINICAL_SIGNIFICANCE; - +import jalview.commands.RemoveGapColCommand; import jalview.datamodel.AlignedCodon; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping; @@ -37,7 +36,6 @@ import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.datamodel.features.SequenceFeatures; -import jalview.io.gff.Gff3Helper; import jalview.io.gff.SequenceOntologyI; import jalview.schemes.ResidueProperties; import jalview.util.Comparison; @@ -45,10 +43,7 @@ import jalview.util.DBRefUtils; import jalview.util.IntRangeComparator; import jalview.util.MapList; import jalview.util.MappingUtils; -import jalview.util.StringUtils; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -1607,7 +1602,7 @@ public class AlignmentUtils { for (int ix = 0, nx = xrefs.size(); ix < nx; ix++) { - DBRefEntry xref = xrefs.get(ix); + DBRefEntry xref = xrefs.get(ix); String xrefName = xref.getSource() + "|" + xref.getAccessionId(); // case-insensitive test, consistent with DBRefEntry.equalRef() if (xrefName.equalsIgnoreCase(name)) @@ -1737,31 +1732,36 @@ public class AlignmentUtils cdsSeqs.add(cdsSeq); - if (!dataset.getSequences().contains(cdsSeqDss)) - { - // check if this sequence is a newly created one - // so needs adding to the dataset - dataset.addSequence(cdsSeqDss); - } - /* - * add a mapping from CDS to the (unchanged) mapped to range + * build the mapping from CDS to protein */ - List cdsRange = Collections.singletonList(new int[] { 1, - cdsSeq.getLength() }); + List cdsRange = Collections + .singletonList(new int[] + { cdsSeq.getStart(), + cdsSeq.getLength() + cdsSeq.getStart() - 1 }); MapList cdsToProteinMap = new MapList(cdsRange, mapList.getToRanges(), mapList.getFromRatio(), mapList.getToRatio()); - AlignedCodonFrame cdsToProteinMapping = new AlignedCodonFrame(); - cdsToProteinMapping.addMap(cdsSeqDss, proteinProduct, - cdsToProteinMap); - /* - * guard against duplicating the mapping if repeating this action - */ - if (!mappings.contains(cdsToProteinMapping)) + if (!dataset.getSequences().contains(cdsSeqDss)) { - mappings.add(cdsToProteinMapping); + /* + * if this sequence is a newly created one, add it to the dataset + * and made a CDS to protein mapping (if sequence already exists, + * CDS-to-protein mapping _is_ the transcript-to-protein mapping) + */ + dataset.addSequence(cdsSeqDss); + AlignedCodonFrame cdsToProteinMapping = new AlignedCodonFrame(); + cdsToProteinMapping.addMap(cdsSeqDss, proteinProduct, + cdsToProteinMap); + + /* + * guard against duplicating the mapping if repeating this action + */ + if (!mappings.contains(cdsToProteinMapping)) + { + mappings.add(cdsToProteinMapping); + } } propagateDBRefsToCDS(cdsSeqDss, dnaSeq.getDatasetSequence(), @@ -1871,7 +1871,7 @@ public class AlignmentUtils return; } - MapList newMap = targetToFrom.traverse(fromLoci.getMap()); + MapList newMap = targetToFrom.traverse(fromLoci.getMapping()); if (newMap != null) { @@ -1985,39 +1985,61 @@ public class AlignmentUtils static SequenceI makeCdsSequence(SequenceI seq, Mapping mapping, AlignmentI dataset) { - char[] seqChars = seq.getSequence(); - List fromRanges = mapping.getMap().getFromRanges(); - int cdsWidth = MappingUtils.getLength(fromRanges); - char[] newSeqChars = new char[cdsWidth]; + /* + * construct CDS sequence name as "CDS|" with 'from id' held in the mapping + * if set (e.g. EMBL protein_id), else sequence name appended + */ + String mapFromId = mapping.getMappedFromId(); + final String seqId = "CDS|" + + (mapFromId != null ? mapFromId : seq.getName()); + + SequenceI newSeq = null; - int newPos = 0; - for (int[] range : fromRanges) + final MapList maplist = mapping.getMap(); + if (maplist.isContiguous() && maplist.isFromForwardStrand()) { - if (range[0] <= range[1]) - { - // forward strand mapping - just copy the range - int length = range[1] - range[0] + 1; - System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos, - length); - newPos += length; - } - else + /* + * just a subsequence, keep same dataset sequence + */ + int start = maplist.getFromLowest(); + int end = maplist.getFromHighest(); + newSeq = seq.getSubSequence(start - 1, end); + newSeq.setName(seqId); + } + else + { + /* + * construct by splicing mapped from ranges + */ + char[] seqChars = seq.getSequence(); + List fromRanges = maplist.getFromRanges(); + int cdsWidth = MappingUtils.getLength(fromRanges); + char[] newSeqChars = new char[cdsWidth]; + + int newPos = 0; + for (int[] range : fromRanges) { - // reverse strand mapping - copy and complement one by one - for (int i = range[0]; i >= range[1]; i--) + if (range[0] <= range[1]) { - newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]); + // forward strand mapping - just copy the range + int length = range[1] - range[0] + 1; + System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos, + length); + newPos += length; + } + else + { + // reverse strand mapping - copy and complement one by one + for (int i = range[0]; i >= range[1]; i--) + { + newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]); + } } } + + newSeq = new Sequence(seqId, newSeqChars, 1, newPos); } - /* - * assign 'from id' held in the mapping if set (e.g. EMBL protein_id), - * else generate a sequence name - */ - String mapFromId = mapping.getMappedFromId(); - String seqId = "CDS|" + (mapFromId != null ? mapFromId : seq.getName()); - SequenceI newSeq = new Sequence(seqId, newSeqChars, 1, newPos); if (dataset != null) { SequenceI[] matches = dataset.findSequenceMatch(newSeq.getName()); @@ -2080,8 +2102,8 @@ public class AlignmentUtils { for (int ib = 0, nb = refs.size(); ib < nb; ib++) { - DBRefEntry dbr = refs.get(ib); - MapList map; + DBRefEntry dbr = refs.get(ib); + MapList map; if (dbr.hasMap() && (map = dbr.getMap().getMap()).isTripletMap()) { // check if map is the CDS mapping @@ -2101,14 +2123,14 @@ public class AlignmentUtils // and generate appropriate mappings for (int ic = 0, nc = direct.size(); ic < nc; ic++) { - DBRefEntry cdsref = direct.get(ic); - Mapping m = cdsref.getMap(); + DBRefEntry cdsref = direct.get(ic); + Mapping m = cdsref.getMap(); // clone maplist and mapping MapList cdsposmap = new MapList( Arrays.asList(new int[][] { new int[] { cdsSeq.getStart(), cdsSeq.getEnd() } }), m.getMap().getToRanges(), 3, 1); - Mapping cdsmap = new Mapping(m.getTo(),m.getMap()); + Mapping cdsmap = new Mapping(m.getTo(), m.getMap()); // create dbref DBRefEntry newref = new DBRefEntry(cdsref.getSource(), @@ -2163,6 +2185,10 @@ public class AlignmentUtils { copyTo = copyTo.getDatasetSequence(); } + if (fromSeq == copyTo || fromSeq.getDatasetSequence() == copyTo) + { + return 0; // shared dataset sequence + } /* * get features, optionally restricted by an ontology term @@ -2334,7 +2360,7 @@ public class AlignmentUtils } } catch (NumberFormatException e) { - // SwingJS -- need to avoid these. + // leave as zero } /* * phase > 0 on first codon means 5' incomplete - skip to the start @@ -2368,393 +2394,6 @@ public class AlignmentUtils } /** - * Maps exon features from dna to protein, and computes variants in peptide - * product generated by variants in dna, and adds them as sequence_variant - * features on the protein sequence. Returns the number of variant features - * added. - * - * @param dnaSeq - * @param peptide - * @param dnaToProtein - */ - public static int computeProteinFeatures(SequenceI dnaSeq, - SequenceI peptide, MapList dnaToProtein) - { - while (dnaSeq.getDatasetSequence() != null) - { - dnaSeq = dnaSeq.getDatasetSequence(); - } - while (peptide.getDatasetSequence() != null) - { - peptide = peptide.getDatasetSequence(); - } - - transferFeatures(dnaSeq, peptide, dnaToProtein, SequenceOntologyI.EXON); - - /* - * compute protein variants from dna variants and codon mappings; - * NB - alternatively we could retrieve this using the REST service e.g. - * http://rest.ensembl.org/overlap/translation - * /ENSP00000288602?feature=transcript_variation;content-type=text/xml - * which would be a bit slower but possibly more reliable - */ - - /* - * build a map with codon variations for each potentially varying peptide - */ - LinkedHashMap[]> variants = buildDnaVariantsMap( - dnaSeq, dnaToProtein); - - /* - * scan codon variations, compute peptide variants and add to peptide sequence - */ - int count = 0; - for (Entry[]> variant : variants.entrySet()) - { - int peptidePos = variant.getKey(); - List[] codonVariants = variant.getValue(); - count += computePeptideVariants(peptide, peptidePos, codonVariants); - } - - return count; - } - - /** - * Computes non-synonymous peptide variants from codon variants and adds them - * as sequence_variant features on the protein sequence (one feature per - * allele variant). Selected attributes (variant id, clinical significance) - * are copied over to the new features. - * - * @param peptide - * the protein sequence - * @param peptidePos - * the position to compute peptide variants for - * @param codonVariants - * a list of dna variants per codon position - * @return the number of features added - */ - static int computePeptideVariants(SequenceI peptide, int peptidePos, - List[] codonVariants) - { - String residue = String.valueOf(peptide.getCharAt(peptidePos - 1)); - int count = 0; - String base1 = codonVariants[0].get(0).base; - String base2 = codonVariants[1].get(0).base; - String base3 = codonVariants[2].get(0).base; - - /* - * variants in first codon base - */ - for (DnaVariant dnavar : codonVariants[0]) - { - if (dnavar.variant != null) - { - String alleles = (String) dnavar.variant.getValue(Gff3Helper.ALLELES); - if (alleles != null) - { - for (String base : alleles.split(",")) - { - if (!base1.equalsIgnoreCase(base)) - { - String codon = base.toUpperCase() + base2.toLowerCase() - + base3.toLowerCase(); - String canonical = base1.toUpperCase() + base2.toLowerCase() - + base3.toLowerCase(); - if (addPeptideVariant(peptide, peptidePos, residue, dnavar, - codon, canonical)) - { - count++; - } - } - } - } - } - } - - /* - * variants in second codon base - */ - for (DnaVariant var : codonVariants[1]) - { - if (var.variant != null) - { - String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES); - if (alleles != null) - { - for (String base : alleles.split(",")) - { - if (!base2.equalsIgnoreCase(base)) - { - String codon = base1.toLowerCase() + base.toUpperCase() - + base3.toLowerCase(); - String canonical = base1.toLowerCase() + base2.toUpperCase() - + base3.toLowerCase(); - if (addPeptideVariant(peptide, peptidePos, residue, var, - codon, canonical)) - { - count++; - } - } - } - } - } - } - - /* - * variants in third codon base - */ - for (DnaVariant var : codonVariants[2]) - { - if (var.variant != null) - { - String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES); - if (alleles != null) - { - for (String base : alleles.split(",")) - { - if (!base3.equalsIgnoreCase(base)) - { - String codon = base1.toLowerCase() + base2.toLowerCase() - + base.toUpperCase(); - String canonical = base1.toLowerCase() + base2.toLowerCase() - + base3.toUpperCase(); - if (addPeptideVariant(peptide, peptidePos, residue, var, - codon, canonical)) - { - count++; - } - } - } - } - } - } - - return count; - } - - /** - * Helper method that adds a peptide variant feature. ID and - * clinical_significance attributes of the dna variant (if present) are copied - * to the new feature. - * - * @param peptide - * @param peptidePos - * @param residue - * @param var - * @param codon - * the variant codon e.g. aCg - * @param canonical - * the 'normal' codon e.g. aTg - * @return true if a feature was added, else false - */ - static boolean addPeptideVariant(SequenceI peptide, int peptidePos, - String residue, DnaVariant var, String codon, String canonical) - { - /* - * get peptide translation of codon e.g. GAT -> D - * note that variants which are not single alleles, - * e.g. multibase variants or HGMD_MUTATION etc - * are currently ignored here - */ - String trans = codon.contains("-") ? null - : (codon.length() > CODON_LENGTH ? null - : ResidueProperties.codonTranslate(codon)); - if (trans == null) - { - return false; - } - String desc = canonical + "/" + codon; - String featureType = ""; - if (trans.equals(residue)) - { - featureType = SequenceOntologyI.SYNONYMOUS_VARIANT; - } - else if (ResidueProperties.STOP.equals(trans)) - { - featureType = SequenceOntologyI.STOP_GAINED; - } - else - { - String residue3Char = StringUtils - .toSentenceCase(ResidueProperties.aa2Triplet.get(residue)); - String trans3Char = StringUtils - .toSentenceCase(ResidueProperties.aa2Triplet.get(trans)); - desc = "p." + residue3Char + peptidePos + trans3Char; - featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT; - } - SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos, - peptidePos, var.getSource()); - - StringBuilder attributes = new StringBuilder(32); - String id = (String) var.variant.getValue(VARIANT_ID); - if (id != null) - { - if (id.startsWith(SEQUENCE_VARIANT)) - { - id = id.substring(SEQUENCE_VARIANT.length()); - } - sf.setValue(VARIANT_ID, id); - attributes.append(VARIANT_ID).append("=").append(id); - // TODO handle other species variants JAL-2064 - StringBuilder link = new StringBuilder(32); - try - { - link.append(desc).append(" ").append(id).append( - "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=") - .append(URLEncoder.encode(id, "UTF-8")); - sf.addLink(link.toString()); - } catch (UnsupportedEncodingException e) - { - // as if - } - } - String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE); - if (clinSig != null) - { - sf.setValue(CLINICAL_SIGNIFICANCE, clinSig); - attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=") - .append(clinSig); - } - peptide.addSequenceFeature(sf); - if (attributes.length() > 0) - { - sf.setAttributes(attributes.toString()); - } - return true; - } - - /** - * Builds a map whose key is position in the protein sequence, and value is a - * list of the base and all variants for each corresponding codon position. - *

- * This depends on dna variants being held as a comma-separated list as - * property "alleles" on variant features. - * - * @param dnaSeq - * @param dnaToProtein - * @return - */ - @SuppressWarnings("unchecked") - static LinkedHashMap[]> buildDnaVariantsMap( - SequenceI dnaSeq, MapList dnaToProtein) - { - /* - * map from peptide position to all variants of the codon which codes for it - * LinkedHashMap ensures we keep the peptide features in sequence order - */ - LinkedHashMap[]> variants = new LinkedHashMap<>(); - - List dnaFeatures = dnaSeq.getFeatures() - .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT); - if (dnaFeatures.isEmpty()) - { - return variants; - } - - int dnaStart = dnaSeq.getStart(); - int[] lastCodon = null; - int lastPeptidePostion = 0; - - /* - * build a map of codon variations for peptides - */ - for (SequenceFeature sf : dnaFeatures) - { - int dnaCol = sf.getBegin(); - if (dnaCol != sf.getEnd()) - { - // not handling multi-locus variant features - continue; - } - - /* - * ignore variant if not a SNP - */ - String alls = (String) sf.getValue(Gff3Helper.ALLELES); - if (alls == null) - { - continue; // non-SNP VCF variant perhaps - can't process this - } - - String[] alleles = alls.toUpperCase().split(","); - boolean isSnp = true; - for (String allele : alleles) - { - if (allele.trim().length() > 1) - { - isSnp = false; - } - } - if (!isSnp) - { - continue; - } - - int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol); - if (mapsTo == null) - { - // feature doesn't lie within coding region - continue; - } - int peptidePosition = mapsTo[0]; - List[] codonVariants = variants.get(peptidePosition); - if (codonVariants == null) - { - codonVariants = new ArrayList[CODON_LENGTH]; - codonVariants[0] = new ArrayList<>(); - codonVariants[1] = new ArrayList<>(); - codonVariants[2] = new ArrayList<>(); - variants.put(peptidePosition, codonVariants); - } - - /* - * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10] - */ - int[] codon = peptidePosition == lastPeptidePostion ? lastCodon - : MappingUtils.flattenRanges(dnaToProtein.locateInFrom( - peptidePosition, peptidePosition)); - lastPeptidePostion = peptidePosition; - lastCodon = codon; - - /* - * save nucleotide (and any variant) for each codon position - */ - for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++) - { - String nucleotide = String.valueOf( - dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase(); - List codonVariant = codonVariants[codonPos]; - if (codon[codonPos] == dnaCol) - { - if (!codonVariant.isEmpty() - && codonVariant.get(0).variant == null) - { - /* - * already recorded base value, add this variant - */ - codonVariant.get(0).variant = sf; - } - else - { - /* - * add variant with base value - */ - codonVariant.add(new DnaVariant(nucleotide, sf)); - } - } - else if (codonVariant.isEmpty()) - { - /* - * record (possibly non-varying) base value - */ - codonVariant.add(new DnaVariant(nucleotide)); - } - } - } - return variants; - } - - /** * Makes an alignment with a copy of the given sequences, adding in any * non-redundant sequences which are mapped to by the cross-referenced * sequences. @@ -2774,7 +2413,7 @@ public class AlignmentUtils SequenceIdMatcher matcher = new SequenceIdMatcher(seqs); if (xrefs != null) { - // BH 2019.01.25 streamlined this triply nested loop to remove all iterators + // BH 2019.01.25 recoded to remove iterators for (int ix = 0, nx = xrefs.length; ix < nx; ix++) { @@ -2784,9 +2423,9 @@ public class AlignmentUtils { for (int ir = 0, nir = dbrefs.size(); ir < nir; ir++) { - DBRefEntry dbref = dbrefs.get(ir); - Mapping map = dbref.getMap(); - SequenceI mto; + DBRefEntry dbref = dbrefs.get(ir); + Mapping map = dbref.getMap(); + SequenceI mto; if (map == null || (mto = map.getTo()) == null || mto.isProtein() != isProtein) { @@ -2893,10 +2532,10 @@ public class AlignmentUtils * true; else returns false * * @param unaligned - * - sequences to be aligned based on aligned + * - sequences to be aligned based on aligned * @param aligned - * - 'guide' alignment containing sequences derived from same dataset - * as unaligned + * - 'guide' alignment containing sequences derived from same + * dataset as unaligned * @return */ static boolean alignAsSameSequences(AlignmentI unaligned, @@ -2920,15 +2559,22 @@ public class AlignmentUtils } /* - * first pass - check whether all sequences to be aligned share a dataset - * sequence with an aligned sequence + * first pass - check whether all sequences to be aligned share a + * dataset sequence with an aligned sequence; also note the leftmost + * ungapped column from which to copy */ + int leftmost = Integer.MAX_VALUE; for (SequenceI seq : unaligned.getSequences()) { - if (!alignedDatasets.containsKey(seq.getDatasetSequence())) + final SequenceI ds = seq.getDatasetSequence(); + if (!alignedDatasets.containsKey(ds)) { return false; } + SequenceI alignedSeq = alignedDatasets.get(ds) + .get(0); + int startCol = alignedSeq.findIndex(seq.getStart()); // 1.. + leftmost = Math.min(leftmost, startCol); } /* @@ -2936,13 +2582,32 @@ public class AlignmentUtils * heuristic rule: pair off sequences in order for the case where * more than one shares the same dataset sequence */ + final char gapCharacter = aligned.getGapCharacter(); for (SequenceI seq : unaligned.getSequences()) { List alignedSequences = alignedDatasets .get(seq.getDatasetSequence()); - // TODO: getSequenceAsString() will be deprecated in the future - // TODO: need to leave to SequenceI implementor to update gaps - seq.setSequence(alignedSequences.get(0).getSequenceAsString()); + if (alignedSequences.isEmpty()) + { + /* + * defensive check - shouldn't happen! (JAL-3536) + */ + continue; + } + SequenceI alignedSeq = alignedSequences.get(0); + + /* + * gap fill for leading (5') UTR if any + */ + // TODO this copies intron columns - wrong! + int startCol = alignedSeq.findIndex(seq.getStart()); // 1.. + int endCol = alignedSeq.findIndex(seq.getEnd()); + char[] seqchars = new char[endCol - leftmost + 1]; + Arrays.fill(seqchars, gapCharacter); + char[] toCopy = alignedSeq.getSequence(startCol - 1, endCol); + System.arraycopy(toCopy, 0, seqchars, startCol - leftmost, + toCopy.length); + seq.setSequence(String.valueOf(seqchars)); if (alignedSequences.size() > 0) { // pop off aligned sequences (except the last one) @@ -2950,6 +2615,12 @@ public class AlignmentUtils } } + /* + * finally remove gapped columns (e.g. introns) + */ + new RemoveGapColCommand("", unaligned.getSequencesArray(), 0, + unaligned.getWidth() - 1, unaligned); + return true; } diff --git a/src/jalview/analysis/CrossRef.java b/src/jalview/analysis/CrossRef.java index ecdd11e..61add1e 100644 --- a/src/jalview/analysis/CrossRef.java +++ b/src/jalview/analysis/CrossRef.java @@ -32,6 +32,7 @@ import jalview.datamodel.SequenceI; import jalview.util.DBRefUtils; import jalview.util.MapList; import jalview.ws.SequenceFetcher; +import jalview.ws.seqfetcher.ASequenceFetcher; import java.util.ArrayList; import java.util.Iterator; @@ -401,6 +402,7 @@ public class CrossRef private void retrieveCrossRef(List sourceRefs, SequenceI seq, List xrfs, boolean fromDna, AlignedCodonFrame cf) { + ASequenceFetcher sftch = SequenceFetcher.getInstance();// Factory.getSequenceFetcher(); SequenceI[] retrieved = null; SequenceI dss = seq.getDatasetSequence() == null ? seq : seq.getDatasetSequence(); @@ -416,8 +418,7 @@ public class CrossRef } try { - retrieved = SequenceFetcher.getInstance() - .getSequences(sourceRefs, !fromDna); + retrieved = sftch.getSequences(sourceRefs, !fromDna); } catch (Exception e) { System.err.println( @@ -640,7 +641,7 @@ public class CrossRef @Override public boolean equals(Object o) { - return equals((SequenceFeature) o, true); + return super.equals(o, true); } }; matched.addSequenceFeature(newFeature); @@ -928,7 +929,7 @@ public class CrossRef if (fromDna) { - AlignmentUtils.computeProteinFeatures(mapFrom, mapTo, mapping); + // AlignmentUtils.computeProteinFeatures(mapFrom, mapTo, mapping); mappings.addMap(mapFrom, mapTo, mapping); } else diff --git a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java index 38217e9..95ca1ba 100644 --- a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java +++ b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java @@ -188,7 +188,7 @@ public class FeatureDistanceModel extends DistanceScoreModel protected Map> findFeatureTypesAtColumn( SeqCigar[] seqs, int columnPosition) { - Map> sfap = new HashMap>(); + Map> sfap = new HashMap<>(); for (SeqCigar seq : seqs) { int spos = seq.findPosition(columnPosition); @@ -197,9 +197,9 @@ public class FeatureDistanceModel extends DistanceScoreModel /* * position is not a gap */ - Set types = new HashSet(); + Set types = new HashSet<>(); List sfs = fr.findFeaturesAtResidue( - seq.getRefSeq(), spos); + seq.getRefSeq(), spos, spos); for (SequenceFeature sf : sfs) { types.add(sf.getType()); diff --git a/src/jalview/api/AlignViewControllerGuiI.java b/src/jalview/api/AlignViewControllerGuiI.java index 86e61e7..6549d64 100644 --- a/src/jalview/api/AlignViewControllerGuiI.java +++ b/src/jalview/api/AlignViewControllerGuiI.java @@ -23,6 +23,8 @@ package jalview.api; import jalview.commands.CommandI; import jalview.schemes.ColourSchemeI; +import java.awt.Rectangle; + /** * Interface implemented by gui implementations managing a Jalview Alignment * View @@ -62,4 +64,25 @@ public interface AlignViewControllerGuiI * @return */ FeatureSettingsControllerI getFeatureSettingsUI(); + + /** + * displays the Feature Settigns control panel for the alignment view - if one + * exists it is closed and re-opened. + * + * @return the current feature settings controller + */ + FeatureSettingsControllerI showFeatureSettingsUI(); + + /** + * record the last position of a feature settings dialog before it was closed + * + * @param bounds + */ + void setFeatureSettingsGeometry(Rectangle bounds); + + /** + * + * @return last position of feature settings for this alignment view GUI + */ + Rectangle getFeatureSettingsGeometry(); } diff --git a/src/jalview/api/AlignViewportI.java b/src/jalview/api/AlignViewportI.java index 4016c75..065be75 100644 --- a/src/jalview/api/AlignViewportI.java +++ b/src/jalview/api/AlignViewportI.java @@ -199,7 +199,7 @@ public interface AlignViewportI extends ViewStyleI * Sets the colour scheme for the background alignment (as distinct from * sub-groups, which may have their own colour schemes). A null value is used * for no residue colour (white). - * + * * @param cs */ void setGlobalColourScheme(ColourSchemeI cs); @@ -433,9 +433,19 @@ public interface AlignViewportI extends ViewStyleI */ void setFollowHighlight(boolean b); + /** + * configure the feature renderer with predefined feature settings + * + * @param featureSettings + */ public void applyFeaturesStyle(FeatureSettingsModelI featureSettings); /** + * Apply the given feature settings on top of existing feature settings. + */ + public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings); + + /** * check if current selection group is defined on the view, or is simply a * temporary group. * diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index 404c497..8aa2858 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -20,6 +20,7 @@ */ package jalview.api; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSetI; @@ -161,15 +162,18 @@ public interface FeatureRenderer List findFeaturesAtColumn(SequenceI sequence, int column); /** - * Returns features at the specified residue position on the given sequence. - * Non-positional features are not included. + * Returns features at the specified residue positions on the given sequence. + * Non-positional features are not included. Features are returned in render + * order of their feature type (last is on top). Within feature type, ordering + * is undefined. * * @param sequence - * @param resNo - * residue position (start..) + * @param fromResNo + * @param toResNo * @return */ - List findFeaturesAtResidue(SequenceI sequence, int resNo); + List findFeaturesAtResidue(SequenceI sequence, + int fromResNo, int toResNo); /** * get current displayed types, in ordering of rendering (on top last) @@ -280,4 +284,17 @@ public interface FeatureRenderer * @return */ boolean isVisible(SequenceFeature feature); + + /** + * Answers a bean containing a mapping, and a list of visible features in this + * alignment at a position (or range) which is mappable from the given sequence + * residue position in a mapped alignment. Features are returned in render order + * of feature type (on top last), with order within feature type undefined. If + * no features or mapping are found, answers null. + * + * @param sequence + * @param pos + * @return + */ + MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, int pos); } diff --git a/src/jalview/api/SplitContainerI.java b/src/jalview/api/SplitContainerI.java index 46f5f44..6b037f5 100644 --- a/src/jalview/api/SplitContainerI.java +++ b/src/jalview/api/SplitContainerI.java @@ -20,6 +20,7 @@ */ package jalview.api; +import jalview.controller.FeatureSettingsControllerGuiI; import jalview.datamodel.AlignmentI; /** @@ -55,4 +56,41 @@ public interface SplitContainerI */ String getComplementTitle(Object af); + /** + * get the 'other' alignFrame in the SplitFrame + * + * @param alignFrame + * @return the complement alignFrame - or null if alignFrame wasn't held by this + * frame + */ + AlignViewControllerGuiI getComplementAlignFrame( + AlignViewControllerGuiI alignFrame); + + /** + * add the given UI to the splitframe's feature settings UI holder + * + * @param featureSettings + * @return + */ + void addFeatureSettingsUI( + FeatureSettingsControllerGuiI featureSettings); + + /** + * Request to close all feature settings originating from a particular panel. + * + * @param featureSettings + * @param closeContainingFrame + * - if false then the tab containing the feature + * settings will be 'reset' ready for a new + * feature settings + */ + void closeFeatureSettings(FeatureSettingsControllerI featureSettings, + boolean closeContainingFrame); + + /** + * + * @return true if a feature settings panel is currently open + */ + boolean isFeatureSettingsOpen(); + } diff --git a/src/jalview/api/ViewStyleI.java b/src/jalview/api/ViewStyleI.java index 2b554ea..a348300 100644 --- a/src/jalview/api/ViewStyleI.java +++ b/src/jalview/api/ViewStyleI.java @@ -24,6 +24,13 @@ import java.awt.Color; public interface ViewStyleI { + void setShowComplementFeatures(boolean b); + + boolean isShowComplementFeatures(); + + void setShowComplementFeaturesOnTop(boolean b); + + boolean isShowComplementFeaturesOnTop(); void setColourAppliesToAllGroups(boolean b); diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index 062dbf8..997f185 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -96,6 +96,7 @@ import java.awt.Menu; import java.awt.MenuBar; import java.awt.MenuItem; import java.awt.Panel; +import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -107,6 +108,7 @@ import java.awt.event.KeyListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.util.Arrays; @@ -119,9 +121,8 @@ import java.util.Vector; import org.jmol.viewer.Viewer; -public class AlignFrame extends EmbmenuFrame implements AlignFrameI, - ActionListener, - ItemListener, KeyListener, AlignViewControllerGuiI +public class AlignFrame extends EmbmenuFrame implements ActionListener, + ItemListener, KeyListener, AlignViewControllerGuiI, AlignFrameI { public AlignViewControllerI avc; @@ -1204,7 +1205,7 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI, } else if (source == featureSettings) { - new FeatureSettings(alignPanel); + showFeatureSettingsUI(); } else if (source == alProperties) { @@ -1449,12 +1450,13 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI, { features = formatter.printJalviewFormat( viewport.getAlignment().getSequencesArray(), - alignPanel.getFeatureRenderer(), true); + alignPanel.getFeatureRenderer(), true, false); } else { features = formatter.printGffFormat(viewport.getAlignment() - .getSequencesArray(), alignPanel.getFeatureRenderer(), true); + .getSequencesArray(), alignPanel.getFeatureRenderer(), true, + false); } if (displayTextbox) @@ -1571,7 +1573,7 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI, try { new URL(url); - url = URLEncoder.encode(url); + url = URLEncoder.encode(url, "UTF-8"); } /* * When we finally deprecate 1.1 compatibility, we can start to use @@ -1584,6 +1586,13 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI, { url = viewport.applet.getCodeBase() + url; } + catch (UnsupportedEncodingException ex) + { + System.err.println( + "WARNING = IMPLEMENTATION ERROR - UNSUPPORTED ENCODING EXCEPTION FOR " + + url); + ex.printStackTrace(); + } return url; } @@ -4332,4 +4341,24 @@ public class AlignFrame extends EmbmenuFrame implements AlignFrameI, return alignPanel.av.featureSettings; } + @Override + public FeatureSettingsControllerI showFeatureSettingsUI() + { + return new FeatureSettings(alignPanel); + } + + private Rectangle fs_bounds = null; + + @Override + public void setFeatureSettingsGeometry(Rectangle bounds) + { + fs_bounds = bounds; + } + + @Override + public Rectangle getFeatureSettingsGeometry() + { + return fs_bounds; + } + } diff --git a/src/jalview/appletgui/AlignViewport.java b/src/jalview/appletgui/AlignViewport.java index 2bead46..0324d8c 100644 --- a/src/jalview/appletgui/AlignViewport.java +++ b/src/jalview/appletgui/AlignViewport.java @@ -71,7 +71,7 @@ public class AlignViewport extends AlignmentViewport { try { - widthScale = new Float(param).floatValue(); + widthScale = Float.valueOf(param).floatValue(); } catch (Exception e) { } @@ -94,7 +94,7 @@ public class AlignViewport extends AlignmentViewport { try { - heightScale = new Float(param).floatValue(); + heightScale = Float.valueOf(param).floatValue(); } catch (Exception e) { } @@ -392,10 +392,10 @@ public class AlignViewport extends AlignmentViewport } /** - * Applies the supplied feature settings descriptor to currently known - * features. This supports an 'initial configuration' of feature colouring - * based on a preset or user favourite. This may then be modified in the usual - * way using the Feature Settings dialogue. + * Applies the supplied feature settings descriptor to currently known features. + * This supports an 'initial configuration' of feature colouring based on a + * preset or user favourite. This may then be modified in the usual way using + * the Feature Settings dialogue. NOT IMPLEMENTED FOR APPLET * * @param featureSettings */ @@ -405,4 +405,18 @@ public class AlignViewport extends AlignmentViewport // TODO implement for applet } + /** + * Merges the supplied feature settings descriptor with existing feature styles. + * This supports an 'initial configuration' of feature colouring based on a + * preset or user favourite. This may then be modified in the usual way using + * the Feature Settings dialogue. NOT IMPLEMENTED FOR APPLET + * + * @param featureSettings + */ + @Override + public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings) + { + // TODO Auto-generated method stub + + } } diff --git a/src/jalview/appletgui/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index ed3d8fa..776e9ad 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -500,7 +500,6 @@ public class SeqPanel extends Panel implements MouseMotionListener, // For now, ignore the mouseWheel font resizing on Macs // As the Button2_mask always seems to be true - if (Platform.isWinMiddleButton(evt)) { mouseWheelPressed = true; @@ -744,7 +743,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (av.isFollowHighlight()) { @@ -761,7 +760,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, } setStatusMessage(results); seqCanvas.highlightSearchResults(results); - + return null; } @Override @@ -820,8 +819,7 @@ public class SeqPanel extends Panel implements MouseMotionListener, { if (av.getAlignment().isNucleotide()) { - String base = ResidueProperties.nucleotideName - .get(String.valueOf(ch)); // BH 2020.04.07 just Eclipse warning + String base = ResidueProperties.nucleotideName.get(ch); text.append(" Nucleotide: ").append(base == null ? ch : base); } else @@ -1433,8 +1431,8 @@ public class SeqPanel extends Panel implements MouseMotionListener, } // DETECT RIGHT MOUSE BUTTON IN AWT - if ((evt.getModifiers() - & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK) + if ((evt.getModifiersEx() + & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK) { List allFeatures = findFeaturesAtColumn(sequence, sequence.findPosition(column + 1)); diff --git a/src/jalview/bin/Jalview.java b/src/jalview/bin/Jalview.java index 5d82015..0ec9ee0 100755 --- a/src/jalview/bin/Jalview.java +++ b/src/jalview/bin/Jalview.java @@ -598,7 +598,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi System.out.println("No files to open!"); System.exit(1); } - boolean haveImport = checkStartVamas(aparser); + boolean haveImport = false;// checkStartVamas(aparser); // Finally, deal with the remaining input data. long progress = -1; if (file == null && isJavaAppletTag) @@ -692,7 +692,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi System.out.println("CMD [nocalculation] executed successfully!"); } - AlignFrame af = new FileLoader(!headless).loadFileWaitTillLoaded(file, + AlignFrame af = new FileLoader(!headless).LoadFileWaitTillLoaded(file, protocol, format); if (af == null) { @@ -713,7 +713,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi // TODO ? } AlignFrame af2 = new FileLoader(!headless) - .loadFileWaitTillLoaded(file2, protocol, format); + .LoadFileWaitTillLoaded(file2, protocol, format); if (af2 == null) { System.out.println("error"); @@ -933,7 +933,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi } startUpAlframe = new FileLoader(!headless) - .loadFileWaitTillLoaded(file, protocol, format); + .LoadFileWaitTillLoaded(file, protocol, format); // extract groovy arguments before anything else. } @@ -965,99 +965,6 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi return null; } - private boolean checkStartVamas(ArgsParser aparser) - { - String vamsasImport = aparser.getValue(ArgsParser.VDOC); - String vamsasSession = aparser.getValue(ArgsParser.VSESS); - if (vamsasImport == null && vamsasSession == null) - { - return false; - } - if (desktop == null || headless) - { - System.out.println( - "Headless vamsas sessions not yet supported. Sorry."); - System.exit(1); - } - boolean haveImport = (vamsasImport != null); - if (haveImport) - { - // if we have a file, start a new session and import it. - boolean inSession = false; - try - { - DataSourceType viprotocol = AppletFormatAdapter - .checkProtocol(vamsasImport); - if (viprotocol == DataSourceType.FILE) - { - inSession = desktop.vamsasImport(new File(vamsasImport)); - } - else if (viprotocol == DataSourceType.URL) - { - inSession = desktop.vamsasImport(new URL(vamsasImport)); - } - - } catch (Exception e) - { - System.err.println("Exeption when importing " + vamsasImport - + " as a vamsas document."); - e.printStackTrace(); - } - if (!inSession) - { - System.err.println("Failed to import " + vamsasImport - + " as a vamsas document."); - } - else - { - System.out.println("Imported Successfully into new session " - + desktop.getVamsasApplication().getCurrentSession()); - } - } - if (vamsasSession != null) - { - if (vamsasImport != null) - { - // close the newly imported session and import the Jalview specific - // remnants into the new session later on. - desktop.vamsasStop_actionPerformed(null); - } - // now join the new session - try - { - if (desktop.joinVamsasSession(vamsasSession)) - { - System.out.println( - "Successfully joined vamsas session " + vamsasSession); - } - else - { - System.err.println("WARNING: Failed to join vamsas session " - + vamsasSession); - } - } catch (Exception e) - { - System.err.println( - "ERROR: Failed to join vamsas session " + vamsasSession); - e.printStackTrace(); - } - if (vamsasImport != null) - { - // the Jalview specific remnants can now be imported into the new - // session at the user's leisure. - Cache.log.info( - "Skipping Push for import of data into existing vamsas session."); - // TODO: - // enable - // this - // when - // debugged - // desktop.getVamsasApplication().push_update(); - } - } - return haveImport; - } - /** * Writes an output file for each format (if any) specified in the * command-line arguments. Supported formats are currently @@ -1387,23 +1294,6 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi } /** - * Get the SwingJS applet ID and combine that with the frameType - * - * @param frameType - * "alignment", "desktop", etc., or null - * @return - */ - public static String getAppID(String frameType) - { - String id = Cache.getProperty("Info.j2sAppletID"); - if (id == null) - { - id = "jalview"; - } - return id + (frameType == null ? "" : "-" + frameType); - } - - /** * Handle all JalviewLite applet parameters * * @param aparser @@ -1886,7 +1776,7 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi { alf = getCurrentAlignFrame(); } - return appLoader.getFeaturesFrom(alf, format); + return appLoader.getFeaturesFrom(alf, format, true, false); } @Override @@ -2266,4 +2156,21 @@ public class Jalview implements ApplicationSingletonI, JalviewJSApi } } + /** + * Get the SwingJS applet ID and combine that with the frameType + * + * @param frameType + * "alignment", "desktop", etc., or null + * @return + */ + public static String getAppID(String frameType) + { + String id = Cache.getProperty("Info.j2sAppletID"); + if (id == null) + { + id = "jalview"; + } + return id + (frameType == null ? "" : "-" + frameType); + } + } \ No newline at end of file diff --git a/src/jalview/bin/JalviewAppLoader.java b/src/jalview/bin/JalviewAppLoader.java index 8fe2cdc..834a30f 100644 --- a/src/jalview/bin/JalviewAppLoader.java +++ b/src/jalview/bin/JalviewAppLoader.java @@ -1370,7 +1370,8 @@ public class JalviewAppLoader return true; } - public String getFeaturesFrom(AlignFrameI alf, String format) + public String getFeaturesFrom(AlignFrameI alf, String format, + boolean includeNonpositionsFeatures, boolean includeComplement) { AlignFrame f = ((AlignFrame) alf); @@ -1380,13 +1381,15 @@ public class JalviewAppLoader { features = formatter.printJalviewFormat( f.getViewport().getAlignment().getSequencesArray(), - f.alignPanel.getFeatureRenderer(), true); + f.alignPanel.getFeatureRenderer(), + includeNonpositionsFeatures, includeComplement); } else { features = formatter.printGffFormat( f.getViewport().getAlignment().getSequencesArray(), - f.alignPanel.getFeatureRenderer(), true); + f.alignPanel.getFeatureRenderer(), + includeNonpositionsFeatures, includeComplement); } if (features == null) diff --git a/src/jalview/controller/AlignViewController.java b/src/jalview/controller/AlignViewController.java index 33699cc..ee46a89 100644 --- a/src/jalview/controller/AlignViewController.java +++ b/src/jalview/controller/AlignViewController.java @@ -166,9 +166,10 @@ public class AlignViewController implements AlignViewControllerI // JBPNote this routine could also mark rows, not just columns. // need a decent query structure to allow all types of feature searches BitSet bs = new BitSet(); - SequenceCollectionI sqcol = (viewport.getSelectionGroup() == null - || extendCurrent) ? viewport.getAlignment() - : viewport.getSelectionGroup(); + boolean searchSelection = viewport.getSelectionGroup() != null + && !extendCurrent; + SequenceCollectionI sqcol = searchSelection ? viewport + .getSelectionGroup() : viewport.getAlignment(); int nseq = findColumnsWithFeature(featureType, sqcol, bs); @@ -204,9 +205,10 @@ public class AlignViewController implements AlignViewControllerI } else { - avcg.setStatus(MessageManager - .formatMessage("label.no_feature_of_type_found", new String[] - { featureType })); + String key = searchSelection ? "label.no_feature_found_selection" + : "label.no_feature_of_type_found"; + avcg.setStatus(MessageManager.formatMessage(key, + new String[] { featureType })); if (!extendCurrent) { cs.clear(); @@ -308,16 +310,34 @@ public class AlignViewController implements AlignViewControllerI @Override public void sortAlignmentByFeatureDensity(List typ) { - sortBy(typ, "Sort by Density", AlignmentSorter.FEATURE_DENSITY); + String methodText = MessageManager.getString("label.sort_by_density"); + sortByFeatures(typ, methodText, AlignmentSorter.FEATURE_DENSITY); } - protected void sortBy(List typ, String methodText, + /** + * Sorts the alignment (or current selection) by either average score or density + * of the specified feature types, and adds to the command history. If + * {@code types} is null, all visible feature types are used for the sort. If no + * feature types apply, does nothing. + * + * @param types + * @param methodText + * - text shown in Undo/Redo command + * @param method + * - passed to + * jalview.analysis.AlignmentSorter.sortByFeatures() + */ + protected void sortByFeatures(List types, String methodText, final String method) { FeatureRenderer fr = alignPanel.getFeatureRenderer(); - if (typ == null && fr != null) + if (types == null && fr != null) + { + types = fr.getDisplayedFeatureTypes(); + } + if (types.isEmpty()) { - typ = fr.getDisplayedFeatureTypes(); + return; // nothing to do } List gps = null; if (fr != null) @@ -339,7 +359,7 @@ public class AlignViewController implements AlignViewControllerI stop = al.getWidth(); } SequenceI[] oldOrder = al.getSequencesArray(); - AlignmentSorter.sortByFeature(typ, gps, start, stop, al, method); + AlignmentSorter.sortByFeature(types, gps, start, stop, al, method); avcg.addHistoryItem(new OrderCommand(methodText, oldOrder, viewport.getAlignment())); alignPanel.paintAlignment(true, false); @@ -349,7 +369,8 @@ public class AlignViewController implements AlignViewControllerI @Override public void sortAlignmentByFeatureScore(List typ) { - sortBy(typ, "Sort by Feature Score", AlignmentSorter.FEATURE_SCORE); + String methodText = MessageManager.getString("label.sort_by_score"); + sortByFeatures(typ, methodText, AlignmentSorter.FEATURE_SCORE); } @Override diff --git a/src/jalview/controller/FeatureSettingsControllerGuiI.java b/src/jalview/controller/FeatureSettingsControllerGuiI.java index 728612e..92571bc 100644 --- a/src/jalview/controller/FeatureSettingsControllerGuiI.java +++ b/src/jalview/controller/FeatureSettingsControllerGuiI.java @@ -20,7 +20,19 @@ */ package jalview.controller; +import jalview.api.AlignViewControllerGuiI; + public interface FeatureSettingsControllerGuiI { + AlignViewControllerGuiI getAlignframe(); + + void featureSettings_isClosed(); + + /** + * undo any changes made to feature settings whilst the dialog has been visible, + * since the last 'apply' + */ + void revert(); + } diff --git a/src/jalview/datamodel/DBRefEntry.java b/src/jalview/datamodel/DBRefEntry.java index 0004abe..b4c7ea4 100755 --- a/src/jalview/datamodel/DBRefEntry.java +++ b/src/jalview/datamodel/DBRefEntry.java @@ -103,7 +103,8 @@ public class DBRefEntry implements DBRefEntryI : new String(entry.getVersion())), (entry.getAccessionId() == null ? "" : new String(entry.getAccessionId())), - (entry.getMap() == null ? null : new Mapping(entry.getMap()))); + (entry.getMap() == null ? null + : new Mapping(entry.getMap()))); } @Override diff --git a/src/jalview/datamodel/GeneLociI.java b/src/jalview/datamodel/GeneLociI.java index f8c7ec5..09db9d7 100644 --- a/src/jalview/datamodel/GeneLociI.java +++ b/src/jalview/datamodel/GeneLociI.java @@ -34,5 +34,5 @@ public interface GeneLociI * * @return */ - MapList getMap(); + MapList getMapping(); } diff --git a/src/jalview/datamodel/GeneLocus.java b/src/jalview/datamodel/GeneLocus.java new file mode 100644 index 0000000..f81348f --- /dev/null +++ b/src/jalview/datamodel/GeneLocus.java @@ -0,0 +1,91 @@ +package jalview.datamodel; + +import jalview.util.MapList; + +/** + * A specialisation of DBRefEntry used to hold the chromosomal coordinates for a + * (typically gene) sequence + *

    + *
  • field source is used to hold a species id e.g. human
  • + *
  • field version is used to hold assembly id e.g GRCh38
  • + *
  • field accession is used to hold the chromosome id
  • + *
  • field map is used to hold the mapping from sequence to + * chromosome coordinates
  • + *
+ * + * @author gmcarstairs + * + */ +public class GeneLocus extends DBRefEntry implements GeneLociI +{ + /** + * Constructor adapts species, assembly, chromosome to DBRefEntry source, + * version, accession, respectively, and saves the mapping of sequence to + * chromosomal coordinates + * + * @param speciesId + * @param assemblyId + * @param chromosomeId + * @param mapping + */ + public GeneLocus(String speciesId, String assemblyId, String chromosomeId, + Mapping mapping) + { + super(speciesId, assemblyId, chromosomeId, mapping); + } + + /** + * Constructor + * + * @param speciesId + * @param assemblyId + * @param chromosomeId + */ + public GeneLocus(String speciesId, String assemblyId, String chromosomeId) + { + this(speciesId, assemblyId, chromosomeId, null); + } + + @Override + public boolean equals(Object o) + { + return o instanceof GeneLocus && super.equals(o); + } + + @Override + public MapList getMapping() + { + return map == null ? null : map.getMap(); + } + + /** + * Answers the species identifier e.g. "human", stored as field source of + * DBRefEntry + */ + @Override + public String getSpeciesId() + { + return getSource(); + } + + /** + * Answers the genome assembly id e.g. "GRCh38", stored as field + * version of DBRefEntry + */ + @Override + public String getAssemblyId() + { + return getVersion(); + } + + /** + * Answers the chromosome identifier e.g. "X", stored as field + * accession of DBRefEntry + */ + @Override + public String getChromosomeId() + { + return getAccessionId(); + } + +} diff --git a/src/jalview/datamodel/MappedFeatures.java b/src/jalview/datamodel/MappedFeatures.java new file mode 100644 index 0000000..0fa03cf --- /dev/null +++ b/src/jalview/datamodel/MappedFeatures.java @@ -0,0 +1,236 @@ +package jalview.datamodel; + +import jalview.io.gff.Gff3Helper; +import jalview.schemes.ResidueProperties; +import jalview.util.MappingUtils; +import jalview.util.StringUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A data bean to hold a list of mapped sequence features (e.g. CDS features + * mapped from protein), and the mapping between the sequences. It also provides + * a method to derive peptide variants from codon variants. + * + * @author gmcarstairs + */ +public class MappedFeatures +{ + private static final String HGV_SP = "HGVSp"; + + private static final String CSQ = "CSQ"; + + /* + * the mapping from one sequence to another + */ + public final Mapping mapping; + + /** + * the sequence mapped from + */ + public final SequenceI fromSeq; + + /* + * features on the sequence mapped to that overlap the mapped positions + */ + public final List features; + + /* + * the residue position in the sequence mapped to + */ + private final int toPosition; + + /* + * the residue at toPosition + */ + private final char toResidue; + + /* + * if the mapping is 3:1 or 1:3 (peptide to CDS), this holds the + * mapped positions i.e. codon base positions in CDS; to + * support calculation of peptide variants from alleles + */ + private final int[] codonPos; + + private final char[] baseCodon; + + /** + * Constructor + * + * @param theMapping + * @param from + * the sequence mapped from (e.g. CDS) + * @param pos + * the residue position in the sequence mapped to + * @param res + * the residue character at position pos + * @param theFeatures + * list of mapped features found in the 'from' sequence at + * the mapped position(s) + */ + public MappedFeatures(Mapping theMapping, SequenceI from, int pos, + char res, List theFeatures) + { + mapping = theMapping; + fromSeq = from; + toPosition = pos; + toResidue = res; + features = theFeatures; + + /* + * determine codon positions and canonical codon + * for a peptide-to-CDS mapping + */ + int[] codonIntervals = mapping.getMap().locateInFrom(toPosition, toPosition); + int[] codonPositions = codonIntervals == null ? null + : MappingUtils.flattenRanges(codonIntervals); + if (codonPositions != null && codonPositions.length == 3) + { + codonPos = codonPositions; + baseCodon = new char[3]; + int cdsStart = fromSeq.getStart(); + baseCodon[0] = Character + .toUpperCase(fromSeq.getCharAt(codonPos[0] - cdsStart)); + baseCodon[1] = Character + .toUpperCase(fromSeq.getCharAt(codonPos[1] - cdsStart)); + baseCodon[2] = Character + .toUpperCase(fromSeq.getCharAt(codonPos[2] - cdsStart)); + } + else + { + codonPos = null; + baseCodon = null; + } + } + + /** + * Computes and returns comma-delimited HGVS notation peptide variants derived + * from codon allele variants. If no variants are found, answers an empty + * string. + * + * @param sf + * a sequence feature (which must be one of those held in this + * object) + * @return + */ + public String findProteinVariants(SequenceFeature sf) + { + if (!features.contains(sf) || baseCodon == null) + { + return ""; + } + + /* + * VCF data may already contain the protein consequence + */ + String hgvsp = sf.getValueAsString(CSQ, HGV_SP); + if (hgvsp != null) + { + int colonPos = hgvsp.lastIndexOf(':'); + if (colonPos >= 0) + { + String var = hgvsp.substring(colonPos + 1); + if (var.contains("p.")) // sanity check + { + return var; + } + } + } + + /* + * otherwise, compute codon and peptide variant + */ + int cdsPos = sf.getBegin(); + if (cdsPos != sf.getEnd()) + { + // not handling multi-locus variant features + return ""; + } + if (cdsPos != codonPos[0] && cdsPos != codonPos[1] + && cdsPos != codonPos[2]) + { + // e.g. feature on intron within spliced codon! + return ""; + } + + String alls = (String) sf.getValue(Gff3Helper.ALLELES); + if (alls == null) + { + return ""; + } + + String from3 = StringUtils.toSentenceCase( + ResidueProperties.aa2Triplet.get(String.valueOf(toResidue))); + + /* + * make a peptide variant for each SNP allele + * e.g. C,G,T gives variants G and T for base C + */ + Set variantPeptides = new HashSet<>(); + String[] alleles = alls.toUpperCase().split(","); + StringBuilder vars = new StringBuilder(); + + for (String allele : alleles) + { + allele = allele.trim().toUpperCase(); + if (allele.length() > 1 || "-".equals(allele)) + { + continue; // multi-locus variant + } + char[] variantCodon = new char[3]; + variantCodon[0] = baseCodon[0]; + variantCodon[1] = baseCodon[1]; + variantCodon[2] = baseCodon[2]; + + /* + * poke variant base into canonical codon; + * ignore first 'allele' (canonical base) + */ + final int i = cdsPos == codonPos[0] ? 0 + : (cdsPos == codonPos[1] ? 1 : 2); + variantCodon[i] = allele.toUpperCase().charAt(0); + if (variantCodon[i] == baseCodon[i]) + { + continue; + } + String codon = new String(variantCodon); + String peptide = ResidueProperties.codonTranslate(codon); + boolean synonymous = toResidue == peptide.charAt(0); + StringBuilder var = new StringBuilder(); + if (synonymous) + { + /* + * synonymous variant notation e.g. c.1062C>A(p.=) + */ + var.append("c.").append(String.valueOf(cdsPos)) + .append(String.valueOf(baseCodon[i])).append(">") + .append(String.valueOf(variantCodon[i])) + .append("(p.=)"); + } + else + { + /* + * missense variant notation e.g. p.Arg355Met + */ + String to3 = ResidueProperties.STOP.equals(peptide) ? "Ter" + : StringUtils.toSentenceCase( + ResidueProperties.aa2Triplet.get(peptide)); + var.append("p.").append(from3).append(String.valueOf(toPosition)) + .append(to3); + } + if (!variantPeptides.contains(peptide)) // duplicate consequence + { + variantPeptides.add(peptide); + if (vars.length() > 0) + { + vars.append(","); + } + vars.append(var); + } + } + + return vars.toString(); + } +} diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index 6c0e528..f894223 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -743,7 +743,7 @@ public class Sequence extends ASequence implements SequenceI } @Override - public MapList getMap() + public MapList getMapping() { return ref.getMap().getMap(); } diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 1f2e639..2dd9cf0 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -28,7 +28,7 @@ import jalview.datamodel.features.FeatureSources; import jalview.util.StringUtils; import java.util.Comparator; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.SortedMap; @@ -50,10 +50,10 @@ public class SequenceFeature implements FeatureLocationI private static final String STATUS = "status"; - private static final String STRAND = "STRAND"; + public static final String STRAND = "STRAND"; - // private key for Phase designed not to conflict with real GFF data - private static final String PHASE = "!Phase"; + // key for Phase designed not to conflict with real GFF data + public static final String PHASE = "!Phase"; // private key for ENA location designed not to conflict with real GFF data private static final String LOCATION = "!Location"; @@ -61,12 +61,6 @@ public class SequenceFeature implements FeatureLocationI private static final String ROW_DATA = "%s%s%s"; /* - * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as - * name1=value1;name2=value2,value3;...etc - */ - private static final String ATTRIBUTES = "ATTRIBUTES"; - - /* * type, begin, end, featureGroup, score and contactFeature are final * to ensure that the integrity of SequenceFeatures data store * can't be broken by direct update of these fields @@ -174,19 +168,13 @@ public class SequenceFeature implements FeatureLocationI if (sf.otherDetails != null) { - otherDetails = new HashMap<>(); - for (Entry entry : sf.otherDetails.entrySet()) - { - otherDetails.put(entry.getKey(), entry.getValue()); - } + otherDetails = new LinkedHashMap<>(); + otherDetails.putAll(sf.otherDetails); } if (sf.links != null && sf.links.size() > 0) { links = new Vector<>(); - for (int i = 0, iSize = sf.links.size(); i < iSize; i++) - { - links.addElement(sf.links.elementAt(i)); - } + links.addAll(sf.links); } } @@ -217,8 +205,7 @@ public class SequenceFeature implements FeatureLocationI @Override public boolean equals(Object o) { - return (o instanceof SequenceFeature - && equals((SequenceFeature) o, false)); + return equals(o, false); } /** @@ -231,19 +218,47 @@ public class SequenceFeature implements FeatureLocationI * @param ignoreParent * @return */ - public boolean equals(SequenceFeature sf, boolean ignoreParent) + public boolean equals(Object o, boolean ignoreParent) { - return (begin == sf.begin && end == sf.end - && getStrand() == sf.getStrand() - && (Float.isNaN(score) ? Float.isNaN(sf.score) - : score == sf.score) - && (type + description + featureGroup + getPhase()) - .equals(sf.type + sf.description + sf.featureGroup - + sf.getPhase()) - && equalAttribute(getValue("ID"), sf.getValue("ID")) - && equalAttribute(getValue("Name"), sf.getValue("Name")) - && (ignoreParent || equalAttribute(getValue("Parent"), - sf.getValue("Parent")))); + if (o == null || !(o instanceof SequenceFeature)) + { + return false; + } + + SequenceFeature sf = (SequenceFeature) o; + boolean sameScore = Float.isNaN(score) ? Float.isNaN(sf.score) + : score == sf.score; + if (begin != sf.begin || end != sf.end || !sameScore) + { + return false; + } + + if (getStrand() != sf.getStrand()) + { + return false; + } + + if (!(type + description + featureGroup + getPhase()).equals( + sf.type + sf.description + sf.featureGroup + sf.getPhase())) + { + return false; + } + if (!equalAttribute(getValue("ID"), sf.getValue("ID"))) + { + return false; + } + if (!equalAttribute(getValue("Name"), sf.getValue("Name"))) + { + return false; + } + if (!ignoreParent) + { + if (!equalAttribute(getValue("Parent"), sf.getValue("Parent"))) + { + return false; + } + } + return true; } /** @@ -318,6 +333,11 @@ public class SequenceFeature implements FeatureLocationI return featureGroup; } + /** + * Adds a hyperlink for the feature. This should have the format label|url. + * + * @param labelLink + */ public void addLink(String labelLink) { if (links == null) @@ -408,7 +428,10 @@ public class SequenceFeature implements FeatureLocationI { if (otherDetails == null) { - otherDetails = new HashMap<>(); + /* + * LinkedHashMap preserves insertion order of attributes + */ + otherDetails = new LinkedHashMap<>(); } otherDetails.put(key, value); @@ -451,16 +474,6 @@ public class SequenceFeature implements FeatureLocationI return (String) getValue(STATUS); } - public void setAttributes(String attr) - { - setValue(ATTRIBUTES, attr); - } - - public String getAttributes() - { - return (String) getValue(ATTRIBUTES); - } - /** * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in * GFF), and 0 for unknown or not (validly) specified @@ -575,9 +588,11 @@ public class SequenceFeature implements FeatureLocationI /** * Answers an html-formatted report of feature details * + * @param seqName + * * @return */ - public String getDetailsReport() + public String getDetailsReport(String seqName) { FeatureSourceI metadata = FeatureSources.getInstance() .getSource(source); @@ -585,9 +600,10 @@ public class SequenceFeature implements FeatureLocationI StringBuilder sb = new StringBuilder(128); sb.append("
"); sb.append(""); + sb.append(String.format(ROW_DATA, "Location", seqName, + begin == end ? begin + : begin + (isContactFeature() ? ":" : "-") + end)); sb.append(String.format(ROW_DATA, "Type", type, "")); - sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin - : begin + (isContactFeature() ? ":" : "-") + end, "")); String desc = StringUtils.stripHtmlTags(description); sb.append(String.format(ROW_DATA, "Description", desc, "")); if (!Float.isNaN(score) && score != 0f) @@ -608,10 +624,6 @@ public class SequenceFeature implements FeatureLocationI for (Entry entry : ordered.entrySet()) { String key = entry.getKey(); - if (ATTRIBUTES.equals(key)) - { - continue; // to avoid double reporting - } Object value = entry.getValue(); if (value instanceof Map) @@ -709,8 +721,6 @@ public class SequenceFeature implements FeatureLocationI { source = theSource; } - - } class SFSortByEnd implements Comparator diff --git a/src/jalview/ext/ensembl/EnsemblCdna.java b/src/jalview/ext/ensembl/EnsemblCdna.java index bdfd208..e01ad17 100644 --- a/src/jalview/ext/ensembl/EnsemblCdna.java +++ b/src/jalview/ext/ensembl/EnsemblCdna.java @@ -23,7 +23,6 @@ package jalview.ext.ensembl; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.io.gff.SequenceOntologyI; -import jalview.util.Platform; import java.util.ArrayList; import java.util.List; @@ -45,7 +44,9 @@ public class EnsemblCdna extends EnsemblSeqProxy * or ENSMUST or similar for other species * or CCDSnnnnn.nn with at least 3 digits */ - private static Regex ACCESSION_REGEX; + private static final Regex ACCESSION_REGEX = new Regex( + "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)"); + /* * fetch exon features on genomic sequence (to identify the cdna regions) * and cds and variation features (to retain) @@ -87,16 +88,6 @@ public class EnsemblCdna extends EnsemblSeqProxy @Override public Regex getAccessionValidator() { - if (ACCESSION_REGEX == null) - { - /* - * accepts ENSG/T/E/P with 11 digits - * or ENSMUSP or similar for other species - * or CCDSnnnnn.nn with at least 3 digits - */ - ACCESSION_REGEX = Platform.newRegex( - "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)", null); - } return ACCESSION_REGEX; } diff --git a/src/jalview/ext/ensembl/EnsemblFeatures.java b/src/jalview/ext/ensembl/EnsemblFeatures.java index e4c4365..e28cc7f 100644 --- a/src/jalview/ext/ensembl/EnsemblFeatures.java +++ b/src/jalview/ext/ensembl/EnsemblFeatures.java @@ -27,7 +27,9 @@ import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.io.gff.SequenceOntologyI; import jalview.util.JSONUtils; +import jalview.util.Platform; +import java.io.BufferedReader; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -93,7 +95,10 @@ class EnsemblFeatures extends EnsemblRestClient List queries = new ArrayList<>(); queries.add(query); SequenceI seq = parseFeaturesJson(queries); + if (seq == null) + return null; return new Alignment(new SequenceI[] { seq }); + } /** @@ -124,6 +129,7 @@ class EnsemblFeatures extends EnsemblRestClient int end = Integer.parseInt(obj.get("end").toString()); String source = obj.get("source").toString(); String strand = obj.get("strand").toString(); + Object phase = obj.get("phase"); String alleles = JSONUtils .arrayToStringList((List) obj.get("alleles")); String clinSig = JSONUtils @@ -149,6 +155,10 @@ class EnsemblFeatures extends EnsemblRestClient SequenceFeature sf = new SequenceFeature(type, desc, start, end, source); sf.setStrand("1".equals(strand) ? "+" : "-"); + if (phase != null) + { + sf.setPhase(phase.toString()); + } setFeatureAttribute(sf, obj, "id"); setFeatureAttribute(sf, obj, "Parent"); setFeatureAttribute(sf, obj, "consequence_type"); diff --git a/src/jalview/ext/ensembl/EnsemblGene.java b/src/jalview/ext/ensembl/EnsemblGene.java index 63a6a6c..2d39fd5 100644 --- a/src/jalview/ext/ensembl/EnsemblGene.java +++ b/src/jalview/ext/ensembl/EnsemblGene.java @@ -23,7 +23,6 @@ package jalview.ext.ensembl; import jalview.api.FeatureColourI; import jalview.api.FeatureSettingsModelI; import jalview.datamodel.AlignmentI; -import jalview.datamodel.DBRefEntry; import jalview.datamodel.GeneLociI; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceFeature; @@ -63,6 +62,8 @@ public class EnsemblGene extends EnsemblSeqProxy EnsemblFeatureType.exon, EnsemblFeatureType.cds, EnsemblFeatureType.variation }; + private static final String CHROMOSOME = "chromosome"; + /** * Default constructor (to use rest.ensembl.org) */ @@ -185,7 +186,7 @@ public class EnsemblGene extends EnsemblSeqProxy if (geneLoci != null) { seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(), - geneLoci.getChromosomeId(), geneLoci.getMap()); + geneLoci.getChromosomeId(), geneLoci.getMapping()); } else { @@ -207,7 +208,7 @@ public class EnsemblGene extends EnsemblSeqProxy return false; } String[] tokens = description.split(":"); - if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME)) + if (tokens.length == 6 && tokens[0].startsWith(CHROMOSOME)) { String ref = tokens[1]; String chrom = tokens[2]; @@ -458,7 +459,7 @@ public class EnsemblGene extends EnsemblSeqProxy return; } - MapList geneMapping = loci.getMap(); + MapList geneMapping = loci.getMapping(); List exons = mapping.getFromRanges(); List transcriptLoci = new ArrayList<>(); diff --git a/src/jalview/ext/ensembl/EnsemblInfo.java b/src/jalview/ext/ensembl/EnsemblInfo.java index 71730de..97a8e74 100644 --- a/src/jalview/ext/ensembl/EnsemblInfo.java +++ b/src/jalview/ext/ensembl/EnsemblInfo.java @@ -2,7 +2,9 @@ package jalview.ext.ensembl; import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefSource; +import jalview.util.JSONUtils; +import java.io.BufferedReader; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -17,20 +19,18 @@ import org.json.simple.parser.ParseException; public class EnsemblInfo extends EnsemblRestClient { - /** + /* * cached results of REST /info/divisions service, currently - * *
    * { 
-   *  "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
-   *  "ENSEMBL", "http://rest.ensembl.org"
+   *  { "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBL", "http://rest.ensembl.org" }
    *  }
    * 
- * * The values for EnsemblGenomes are retrieved by a REST call, that for * Ensembl is added programmatically for convenience of lookup */ @@ -92,13 +92,9 @@ public class EnsemblInfo extends EnsemblRestClient try { @SuppressWarnings("unchecked") - Iterator rvals = (Iterator) getJSON( - getDivisionsUrl(ensemblGenomesDomain), null, -1, - MODE_ITERATOR, null); + Iterator rvals = (Iterator) getJSON(getDivisionsUrl(ensemblGenomesDomain), null, -1, MODE_ITERATOR, null); if (rvals == null) - { - return; - } + return; while (rvals.hasNext()) { String division = rvals.next().toString(); @@ -128,8 +124,7 @@ public class EnsemblInfo extends EnsemblRestClient * * @return */ - public Set getDivisions() - { + public Set getDivisions() { if (divisions == null) { fetchDivisions(); diff --git a/src/jalview/ext/ensembl/EnsemblLookup.java b/src/jalview/ext/ensembl/EnsemblLookup.java index fc37b8a..9b56d6b 100644 --- a/src/jalview/ext/ensembl/EnsemblLookup.java +++ b/src/jalview/ext/ensembl/EnsemblLookup.java @@ -23,6 +23,8 @@ package jalview.ext.ensembl; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.GeneLociI; +import jalview.datamodel.GeneLocus; +import jalview.datamodel.Mapping; import jalview.util.MapList; import java.io.IOException; @@ -268,34 +270,10 @@ public class EnsemblLookup extends EnsemblRestClient fromEnd }); List toRange = Collections.singletonList(new int[] { toStart, toEnd }); - final MapList map = new MapList(fromRange, toRange, 1, 1); - return new GeneLociI() - { - - @Override - public String getSpeciesId() - { - return species == null ? "" : species; - } - - @Override - public String getAssemblyId() - { - return assembly; - } - - @Override - public String getChromosomeId() - { - return chromosome; - } - - @Override - public MapList getMap() - { - return map; - } - }; + final Mapping map = new Mapping( + new MapList(fromRange, toRange, 1, 1)); + return new GeneLocus(species == null ? "" : species, assembly, + chromosome, map); } catch (NullPointerException | NumberFormatException e) { Cache.log.error("Error looking up gene loci: " + e.getMessage()); diff --git a/src/jalview/ext/ensembl/EnsemblMap.java b/src/jalview/ext/ensembl/EnsemblMap.java index ae36749..e9cc9e1 100644 --- a/src/jalview/ext/ensembl/EnsemblMap.java +++ b/src/jalview/ext/ensembl/EnsemblMap.java @@ -3,6 +3,8 @@ package jalview.ext.ensembl; import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefSource; import jalview.datamodel.GeneLociI; +import jalview.datamodel.GeneLocus; +import jalview.datamodel.Mapping; import jalview.util.MapList; import java.io.IOException; @@ -16,6 +18,18 @@ import java.util.Map; import org.json.simple.parser.ParseException; +/** + * A client for the Ensembl REST service /map endpoint, to convert from + * coordinates of one genome assembly to another. + *

+ * Note that species and assembly identifiers passed to this class must be valid + * in Ensembl. They are not case sensitive. + * + * @author gmcarstairs + * @see https://rest.ensembl.org/documentation/info/assembly_map + * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml + * @see https://rest.ensembl.org/info/species?content-type=text/xml + */ public class EnsemblMap extends EnsemblRestClient { private static final String MAPPED = "mapped"; @@ -340,34 +354,9 @@ GeneLociI parseIdMappingResponse(URL url, String accession, final String chr = chromosome; List fromRange = Collections.singletonList(new int[] { 1, fromEnd }); - final MapList map = new MapList(fromRange, regions, 1, 1); - return new GeneLociI() - { - - @Override - public String getSpeciesId() - { - return species == null ? "" : species; - } - - @Override - public String getAssemblyId() - { - return as; - } - - @Override - public String getChromosomeId() - { - return chr; - } - - @Override - public MapList getMap() - { - return map; - } - }; + Mapping mapping = new Mapping(new MapList(fromRange, regions, 1, 1)); + return new GeneLocus(species == null ? "" : species, as, chr, + mapping); } catch (IOException | ParseException | NumberFormatException e) { // ignore diff --git a/src/jalview/ext/ensembl/EnsemblProtein.java b/src/jalview/ext/ensembl/EnsemblProtein.java index f586ed6..0280f16 100644 --- a/src/jalview/ext/ensembl/EnsemblProtein.java +++ b/src/jalview/ext/ensembl/EnsemblProtein.java @@ -23,7 +23,6 @@ package jalview.ext.ensembl; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.util.Platform; import java.util.ArrayList; import java.util.List; @@ -43,7 +42,8 @@ public class EnsemblProtein extends EnsemblSeqProxy * or ENSMUSP or similar for other species * or CCDSnnnnn.nn with at least 3 digits */ - private static Regex ACCESSION_REGEX; + private static final Regex ACCESSION_REGEX = new Regex( + "(ENS([A-Z]{3}|)P[0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)"); /** * Default constructor (to use rest.ensembl.org) @@ -119,11 +119,6 @@ public class EnsemblProtein extends EnsemblSeqProxy @Override public Regex getAccessionValidator() { - if (ACCESSION_REGEX == null) - { - ACCESSION_REGEX = Platform.newRegex( - "(ENS([A-Z]{3}|)P[0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)", null); - } return ACCESSION_REGEX; } diff --git a/src/jalview/ext/ensembl/EnsemblRestClient.java b/src/jalview/ext/ensembl/EnsemblRestClient.java index 6d4bc79..771980c 100644 --- a/src/jalview/ext/ensembl/EnsemblRestClient.java +++ b/src/jalview/ext/ensembl/EnsemblRestClient.java @@ -20,9 +20,6 @@ */ package jalview.ext.ensembl; -import jalview.util.Platform; -import jalview.util.StringUtils; - import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; @@ -39,6 +36,9 @@ import javax.ws.rs.HttpMethod; import org.json.simple.parser.ParseException; +import jalview.util.Platform; +import jalview.util.StringUtils; + /** * Base class for Ensembl REST service clients * @@ -75,7 +75,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log"; - private static final Map domainData; + private static Map domainData; private final static long AVAILABILITY_RETEST_INTERVAL = 10000L; // 10 seconds @@ -319,10 +319,10 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher InputStream response = connection.getInputStream(); - Platform.timeCheck(null, Platform.TIME_MARK); + // Platform.timeCheck(null, Platform.TIME_MARK); Object ret = Platform.parseJSON(response); - Platform.timeCheck("EnsemblRestClient.getJSON " + url, - Platform.TIME_MARK); + // Platform.timeCheck("EnsemblRestClient.getJSON " + url, + // Platform.TIME_MARK); return ret; } diff --git a/src/jalview/ext/ensembl/EnsemblSeqProxy.java b/src/jalview/ext/ensembl/EnsemblSeqProxy.java index bdaef0b..11f8b7b 100644 --- a/src/jalview/ext/ensembl/EnsemblSeqProxy.java +++ b/src/jalview/ext/ensembl/EnsemblSeqProxy.java @@ -20,6 +20,17 @@ */ package jalview.ext.ensembl; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.json.simple.parser.ParseException; + import jalview.analysis.AlignmentUtils; import jalview.analysis.Dna; import jalview.bin.Cache; @@ -40,18 +51,6 @@ import jalview.util.Comparison; import jalview.util.DBRefUtils; import jalview.util.IntRangeComparator; import jalview.util.MapList; -import jalview.util.Platform; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.json.simple.parser.ParseException; /** * Base class for Ensembl sequence fetchers @@ -211,8 +210,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient EnsemblFeatures gffFetcher = new EnsemblFeatures(getDomain()); EnsemblFeatureType[] features = getFeaturesToFetch(); - Platform.timeCheck("ESP.getsequencerec1", Platform.TIME_MARK); - + // Platform.timeCheck("ESP.getsequencerec1", Platform.TIME_MARK); AlignmentI geneFeatures = gffFetcher.getSequenceRecords(accId, features); @@ -221,7 +219,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient genomicSequence = geneFeatures.getSequenceAt(0); } - Platform.timeCheck("ESP.getsequencerec2", Platform.TIME_MARK); + // Platform.timeCheck("ESP.getsequencerec2", Platform.TIME_MARK); if (genomicSequence != null) { @@ -236,7 +234,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * fetch and map protein product, and add it as a cross-reference * of the retrieved sequence */ - Platform.timeCheck("ESP.transferFeatures", Platform.TIME_MARK); + // Platform.timeCheck("ESP.transferFeatures", Platform.TIME_MARK); addProteinProduct(querySeq); } } @@ -245,7 +243,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient System.err.println( "Error transferring Ensembl features: " + e.getMessage()); } - Platform.timeCheck("ESP.addfeat done", Platform.TIME_MARK); + // Platform.timeCheck("ESP.addfeat done", Platform.TIME_MARK); } /** @@ -340,8 +338,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * copy exon features to protein, compute peptide variants from dna * variants and add as features on the protein sequence ta-da */ - AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, - mapList); + // JAL-3187 render on the fly instead + // AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, mapList); } } catch (Exception e) { @@ -359,7 +357,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient protected void getCrossReferences(SequenceI seq) { - Platform.timeCheck("ESP. getdataseq ", Platform.TIME_MARK); + // Platform.timeCheck("ESP. getdataseq ", Platform.TIME_MARK); while (seq.getDatasetSequence() != null) @@ -367,7 +365,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient seq = seq.getDatasetSequence(); } - Platform.timeCheck("ESP. getxref ", Platform.TIME_MARK); + // Platform.timeCheck("ESP. getxref ", Platform.TIME_MARK); EnsemblXref xrefFetcher = new EnsemblXref(getDomain(), getDbSource(), getEnsemblDataVersion()); @@ -397,8 +395,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient seq.addDBRef(self); - Platform.timeCheck("ESP. seqprox done ", Platform.TIME_MARK); - + // Platform.timeCheck("ESP. seqprox done ", Platform.TIME_MARK); } /** @@ -419,13 +416,11 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient inProgress = false; throw new JalviewException("ENSEMBL Rest API not available."); } - Platform.timeCheck("EnsemblSeqProx.fetchSeq ", Platform.TIME_MARK); + // Platform.timeCheck("EnsemblSeqProx.fetchSeq ", Platform.TIME_MARK); List seqs = parseSequenceJson(ids); if (seqs == null) - { - return alignment; - } + return alignment; if (seqs.isEmpty()) { @@ -488,13 +483,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient * for now, assumes only one sequence returned; refactor if needed * in future to handle a JSONArray with more than one */ - - Platform.timeCheck("ENS seqproxy", Platform.TIME_MARK); + // Platform.timeCheck("ENS seqproxy", Platform.TIME_MARK); Map val = (Map) getJSON(null, ids, -1, MODE_MAP, null); if (val == null) - { - return null; - } + return null; Object s = val.get("desc"); String desc = s == null ? null : s.toString(); s = val.get("id"); @@ -517,7 +509,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient System.err.println("Error processing JSON response: " + e.toString()); // ignore } - Platform.timeCheck("ENS seqproxy2", Platform.TIME_MARK); + // Platform.timeCheck("ENS seqproxy2", Platform.TIME_MARK); return result; } @@ -769,18 +761,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient String comp = complement.toString(); sf.setValue(Gff3Helper.ALLELES, comp); sf.setDescription(comp); - - /* - * replace value of "alleles=" in sf.ATTRIBUTES as well - * so 'output as GFF' shows reverse complement alleles - */ - String atts = sf.getAttributes(); - if (atts != null) - { - atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles, - Gff3Helper.ALLELES + "=" + comp); - sf.setAttributes(atts); - } } /** @@ -842,9 +822,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient return false; } - - Platform.timeCheck("ESP. xfer " + sfs.size(), Platform.TIME_MARK); - + // Platform.timeCheck("ESP. xfer " + sfs.size(), Platform.TIME_MARK); boolean result = transferFeatures(sfs, targetSequence, mapping, accessionId); diff --git a/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java b/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java index 21f9f7e..7454eb6 100644 --- a/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java +++ b/src/jalview/ext/ensembl/EnsemblSequenceFetcher.java @@ -23,7 +23,6 @@ package jalview.ext.ensembl; import jalview.analysis.AlignmentUtils; import jalview.bin.Cache; import jalview.datamodel.DBRefSource; -import jalview.util.Platform; import jalview.ws.seqfetcher.DbSourceProxyImpl; import com.stevesoft.pat.Regex; @@ -46,7 +45,14 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl // ensemblgenomes REST service merged to ensembl 9th April 2019 protected static final String DEFAULT_ENSEMBL_GENOMES_BASEURL = DEFAULT_ENSEMBL_BASEURL; - private static Regex ACCESSION_REGEX; + /* + * accepts ENSG/T/E/P with 11 digits + * or ENSMUSP or similar for other species + * or CCDSnnnnn.nn with at least 3 digits + */ + private static final Regex ACCESSION_REGEX = new Regex( + "(ENS([A-Z]{3}|)[GTEP]{1}[0-9]{11}$)" + "|" + + "(CCDS[0-9.]{3,}$)"); protected final String ensemblGenomesDomain; @@ -115,17 +121,6 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl @Override public Regex getAccessionValidator() { - if (ACCESSION_REGEX == null) - { - /* - * accepts ENSG/T/E/P with 11 digits - * or ENSMUSP or similar for other species - * or CCDSnnnnn.nn with at least 3 digits - */ - ACCESSION_REGEX = Platform - .newRegex("(ENS([A-Z]{3}|)[GTEP]{1}[0-9]{11}$)" + "|" - + "(CCDS[0-9.]{3,}$)", null); - } return ACCESSION_REGEX; } diff --git a/src/jalview/ext/htsjdk/VCFReader.java b/src/jalview/ext/htsjdk/VCFReader.java index 14c057f..2859e0f 100644 --- a/src/jalview/ext/htsjdk/VCFReader.java +++ b/src/jalview/ext/htsjdk/VCFReader.java @@ -1,14 +1,16 @@ package jalview.ext.htsjdk; -import htsjdk.samtools.util.CloseableIterator; -import htsjdk.variant.variantcontext.VariantContext; -import htsjdk.variant.vcf.VCFFileReader; -import htsjdk.variant.vcf.VCFHeader; +import jalview.bin.Cache; import java.io.Closeable; import java.io.File; import java.io.IOException; +import htsjdk.samtools.util.CloseableIterator; +import htsjdk.variant.variantcontext.VariantContext; +import htsjdk.variant.vcf.VCFFileReader; +import htsjdk.variant.vcf.VCFHeader; + /** * A thin wrapper for htsjdk classes to read either plain, or compressed, or * compressed and indexed VCF files @@ -19,36 +21,52 @@ public class VCFReader implements Closeable, Iterable private static final String TBI_EXTENSION = ".tbi"; + private static final String CSI_EXTENSION = ".csi"; + private boolean indexed; private VCFFileReader reader; /** - * Constructor given a raw or compressed VCF file or a (tabix) index file + * Constructor given a raw or compressed VCF file or a (csi or tabix) index file *

- * For now, file type is inferred from its suffix: .gz or .bgz for compressed - * data, .tbi for an index file, anything else is assumed to be plain text - * VCF. + * If the file path ends in ".tbi" or ".csi", or appending one of these + * extensions gives a valid file path, open as indexed, else as unindexed. * * @param f * @throws IOException */ public VCFReader(String filePath) throws IOException { - if (filePath.endsWith(GZ)) + indexed = false; + if (filePath.endsWith(TBI_EXTENSION) + || filePath.endsWith(CSI_EXTENSION)) { - if (new File(filePath + TBI_EXTENSION).exists()) - { - indexed = true; - } + indexed = true; + filePath = filePath.substring(0, filePath.length() - 4); } - else if (filePath.endsWith(TBI_EXTENSION)) + else if (new File(filePath + TBI_EXTENSION).exists()) + { + indexed = true; + } + else if (new File(filePath + CSI_EXTENSION).exists()) { indexed = true; - filePath = filePath.substring(0, filePath.length() - 4); } - reader = new VCFFileReader(new File(filePath), indexed); + /* + * we pass the name of the unindexed file to htsjdk, + * with a flag to assert whether it is indexed + */ + File file = new File(filePath); + if (file.exists()) + { + reader = new VCFFileReader(file, indexed); + } + else + { + Cache.log.error("File not found: " + filePath); + } } @Override @@ -88,9 +106,10 @@ public class VCFReader implements Closeable, Iterable public CloseableIterator query(final String chrom, final int start, final int end) { - if (reader == null) { - return null; - } + if (reader == null) + { + return null; + } if (indexed) { return reader.query(chrom, start, end); @@ -145,7 +164,7 @@ public class VCFReader implements Closeable, Iterable int vend = variant.getEnd(); // todo what is the undeprecated way to get // the chromosome for the variant? - if (chrom.equals(variant.getChr()) && (vstart <= end) + if (chrom.equals(variant.getContig()) && (vstart <= end) && (vend >= start)) { return variant; diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index fa31fd9..453152e 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -44,12 +44,12 @@ import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.io.File; import java.net.URL; -import java.security.AccessControlException; import java.util.ArrayList; import java.util.BitSet; import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import java.util.Vector; import org.jmol.adapter.smarter.SmarterJmolAdapter; @@ -65,6 +65,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel implements JmolStatusListener, JmolSelectionListener, ComponentListener { + private String lastMessage; + boolean allChainsSelected = false; /* @@ -89,8 +91,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel String lastCommand; - String lastMessage; - boolean loadedInline; StringBuffer resetLastRes = new StringBuffer(); @@ -619,74 +619,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel */ private int _modelFileNameMap[]; - // //////////////////////////////// - // /StructureListener - // @Override - public synchronized String[] getPdbFilex() - { - if (viewer == null) - { - return new String[0]; - } - if (modelFileNames == null) - { - List mset = new ArrayList<>(); - _modelFileNameMap = new int[viewer.ms.mc]; - String m = viewer.ms.getModelFileName(0); - if (m != null) - { - String filePath = m; - try - { - filePath = new File(m).getAbsolutePath(); - } catch (AccessControlException x) - { - // usually not allowed to do this in applet - System.err.println( - "jmolBinding: Using local file string from Jmol: " + m); - } - if (filePath.indexOf("/file:") != -1) - { - // applet path with docroot - discard as format won't match pdbfile - filePath = m; - } - mset.add(filePath); - _modelFileNameMap[0] = 0; // filename index for first model is always 0. - } - int j = 1; - for (int i = 1; i < viewer.ms.mc; i++) - { - m = viewer.ms.getModelFileName(i); - String filePath = m; - if (m != null) - { - try - { - filePath = new File(m).getAbsolutePath(); - } catch (AccessControlException x) - { - // usually not allowed to do this in applet, so keep raw handle - // System.err.println("jmolBinding: Using local file string from - // Jmol: "+m); - } - } - - /* - * add this model unless it is read from a structure file we have - * already seen (example: 2MJW is an NMR structure with 10 models) - */ - if (!mset.contains(filePath)) - { - mset.add(filePath); - _modelFileNameMap[j] = i; // record the model index for the filename - j++; - } - } - modelFileNames = mset.toArray(new String[mset.size()]); - } - return modelFileNames; - } - @Override public synchronized String[] getStructureFiles() { @@ -827,7 +759,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel viewer.openStringInline(string); } - public void mouseOverStructure(int atomIndex, String strInfo) + protected void mouseOverStructure(int atomIndex, final String strInfo) { int pdbResNum; int alocsep = strInfo.indexOf("^"); @@ -881,7 +813,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel try { // recover PDB filename for the model hovered over. - int mnumber = new Integer(mdlId).intValue() - 1; + int mnumber = Integer.valueOf(mdlId).intValue() - 1; if (_modelFileNameMap != null) { int _mp = _modelFileNameMap.length - 1; @@ -908,18 +840,36 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel } catch (Exception e) { } - ; } - if (lastMessage == null || !lastMessage.equals(strInfo)) + + /* + * highlight position on alignment(s); if some text is returned, + * show this as a second line on the structure hover tooltip + */ + String label = getSsm().mouseOverStructure(pdbResNum, chainId, + pdbfilename); + if (label != null) { - getSsm().mouseOverStructure(pdbResNum, chainId, pdbfilename); + // change comma to pipe separator (newline token for Jmol) + label = label.replace(',', '|'); + StringTokenizer toks = new StringTokenizer(strInfo, " "); + StringBuilder sb = new StringBuilder(); + sb.append("select ").append(String.valueOf(pdbResNum)).append(":") + .append(chainId).append("/1"); + sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ") + .append(toks.nextToken()); + sb.append("|").append(label).append("\""); + evalStateCommand(sb.toString()); } - - lastMessage = strInfo; } public void notifyAtomHovered(int atomIndex, String strInfo, String data) { + if (strInfo.equals(lastMessage)) + { + return; + } + lastMessage = strInfo; if (data != null) { System.err.println("Ignoring additional hover info: " + data diff --git a/src/jalview/ext/paradise/Annotate3D.java b/src/jalview/ext/paradise/Annotate3D.java index 3b7044b..de1d90c 100644 --- a/src/jalview/ext/paradise/Annotate3D.java +++ b/src/jalview/ext/paradise/Annotate3D.java @@ -20,10 +20,6 @@ */ package jalview.ext.paradise; -import jalview.util.JSONUtils; -import jalview.util.MessageManager; -import jalview.ws.HttpClientUtils; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -40,6 +36,10 @@ import org.apache.http.message.BasicNameValuePair; import org.json.simple.parser.ContentHandler; import org.json.simple.parser.ParseException; +import jalview.util.JSONUtils; +import jalview.util.MessageManager; +import jalview.ws.HttpClientUtils; + /** * simple methods for calling the various paradise RNA tools * @@ -137,13 +137,13 @@ public class Annotate3D public static Iterator getRNAMLForPDBFileAsString(String pdbfile) throws Exception { - List vals = new ArrayList(); + List vals = new ArrayList<>(); vals.add(new BasicNameValuePair("tool", "rnaview")); vals.add(new BasicNameValuePair("data", pdbfile)); vals.add(new BasicNameValuePair("output", "rnaml")); // return processJsonResponseFor(HttpClientUtils.doHttpUrlPost(twoDtoolsURL, // vals)); - ArrayList readers = new ArrayList(); + ArrayList readers = new ArrayList<>(); final BufferedReader postResponse = HttpClientUtils .doHttpUrlPost(twoDtoolsURL, vals, 0, 0); readers.add(postResponse); @@ -151,85 +151,14 @@ public class Annotate3D } - /** - * @param respons - * @return - * @throws Exception - */ public static Iterator processJsonResponseFor(Reader respons) throws Exception { - // BH 2019 never called? + org.json.simple.parser.JSONParser jp = new org.json.simple.parser.JSONParser(); try { - @SuppressWarnings("unchecked") - final Iterator rvals = ((List) JSONUtils.parse(respons)).iterator(); - return new Iterator() - { - @Override - public boolean hasNext() - { - return rvals.hasNext(); - } - - @SuppressWarnings("unchecked") - @Override - public Reader next() - { - Map val = (Map) rvals.next(); - - Object sval = null; - try - { - sval = val.get("2D"); - } catch (Exception x) - { - x.printStackTrace(); - } - ; - if (sval == null) - { - System.err.println( - "DEVELOPER WARNING: Annotate3d didn't return a '2D' tag in its response. Consider checking output of server. Response was :" - + val.toString()); - - sval = ""; - } - return new StringReader(sval.toString()); - - } - - @Override - public void remove() - { - throw new Error( - MessageManager.getString("error.not_implemented_remove")); - - } - - @Override - protected Object clone() throws CloneNotSupportedException - { - throw new CloneNotSupportedException( - MessageManager.getString("error.not_implemented_clone")); - } - - @Override - public boolean equals(Object obj) - { - return super.equals(obj); - } - - @Override - protected void finalize() throws Throwable - { - while (rvals.hasNext()) - { - rvals.next(); - } - super.finalize(); - } - }; + final RvalsIterator rvals = new RvalsIterator(respons); + return rvals; } catch (Exception foo) { throw new Exception(MessageManager.getString( @@ -242,7 +171,7 @@ public class Annotate3D public static Iterator getRNAMLForPDBId(String pdbid) throws Exception { - List vals = new ArrayList(); + List vals = new ArrayList<>(); vals.add(new BasicNameValuePair("tool", "rnaview")); vals.add(new BasicNameValuePair("pdbid", pdbid)); vals.add(new BasicNameValuePair("output", "rnaml")); @@ -250,9 +179,97 @@ public class Annotate3D + pdbid + "&output=rnaml"); // return processJsonResponseFor(new // InputStreamReader(geturl.openStream())); - ArrayList readers = new ArrayList(); + ArrayList readers = new ArrayList<>(); readers.add(new InputStreamReader(geturl.openStream())); return readers.iterator(); } } + +class RvalsIterator implements Iterator, AutoCloseable +{ + private Iterator rvals; + + @SuppressWarnings("unchecked") + protected RvalsIterator(Reader respons) throws IOException, ParseException + { + /* + * as in 2.11.1 (pre-JalviewJS) + */ + // final JSONArray responses = (JSONArray) jp.parse(respons); + // this.rvals = responses.iterator(); + + /* + * as in JalviewJS (with comment that this code is never called) + */ + this.rvals = ((List) JSONUtils.parse(respons)).iterator(); + } + + @Override + public boolean hasNext() + { + return rvals.hasNext(); + } + + @Override + public Reader next() + { + // JSONObject val = (JSONObject) rvals.next(); // 2.11.1 + @SuppressWarnings("unchecked") + Map val = (Map) rvals.next(); + + Object sval = null; + try + { + sval = val.get("2D"); + } catch (Exception x) + { + x.printStackTrace(); + } + + if (sval == null) + { + System.err.println( + "DEVELOPER WARNING: Annotate3d didn't return a '2D' tag in its response. Consider checking output of server. Response was :" + + val.toString()); + + sval = ""; + } + // 2.11.1: + // return new StringReader( + // (sval instanceof JSONObject) ? ((JSONObject) sval).toString() + // : sval.toString()); + return new StringReader(sval.toString()); + + } + + @Override + public void remove() + { + throw new Error( + MessageManager.getString("error.not_implemented_remove")); + + } + + @Override + protected Object clone() throws CloneNotSupportedException + { + throw new CloneNotSupportedException( + MessageManager.getString("error.not_implemented_clone")); + } + + @Override + public boolean equals(Object obj) + { + return super.equals(obj); + } + + @Override + public void close() + { + while (rvals.hasNext()) + { + rvals.next(); + } + } +} diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index dad8511..3caaac3 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -26,8 +26,10 @@ import jalview.api.FeatureRenderer; import jalview.api.SequenceRenderer; import jalview.datamodel.AlignmentI; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.gui.Desktop; import jalview.renderer.seqfeatures.FeatureColourFinder; import jalview.structure.StructureMapping; import jalview.structure.StructureMappingcommandSet; @@ -104,7 +106,7 @@ public class ChimeraCommands * delimited). If length limit issues arise, refactor to return one color * command per colour. */ - List commands = new ArrayList(); + List commands = new ArrayList<>(); StringBuilder sb = new StringBuilder(256); boolean firstColour = true; for (Object key : colourMap.keySet()) @@ -196,7 +198,7 @@ public class ChimeraCommands AlignViewportI viewport = viewPanel.getAlignViewport(); HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); AlignmentI al = viewport.getAlignment(); - Map colourMap = new LinkedHashMap(); + Map colourMap = new LinkedHashMap<>(); Color lastColour = null; for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++) @@ -257,7 +259,7 @@ public class ChimeraCommands { if (startPos != -1) { - addColourRange(colourMap, lastColour, pdbfnum, startPos, + addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, lastPos, lastChain); } startPos = pos; @@ -269,7 +271,7 @@ public class ChimeraCommands // final colour range if (lastColour != null) { - addColourRange(colourMap, lastColour, pdbfnum, startPos, + addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos, lastPos, lastChain); } // break; @@ -281,26 +283,32 @@ public class ChimeraCommands } /** - * Helper method to add one contiguous colour range to the colour map. + * Helper method to add one contiguous range to the AtomSpec model for the given + * value (creating the model if necessary). As used by Jalview, {@code value} is + *
    + *
  • a colour, when building a 'colour structure by sequence' command
  • + *
  • a feature value, when building a 'set Chimera attributes from features' + * command
  • + *
* * @param map - * @param key + * @param value * @param model * @param startPos * @param endPos * @param chain */ - protected static void addColourRange(Map map, - Object key, int model, int startPos, int endPos, String chain) + protected static void addAtomSpecRange(Map map, + Object value, int model, int startPos, int endPos, String chain) { /* * Get/initialize map of data for the colour */ - AtomSpecModel atomSpec = map.get(key); + AtomSpecModel atomSpec = map.get(value); if (atomSpec == null) { atomSpec = new AtomSpecModel(); - map.put(key, atomSpec); + map.put(value, atomSpec); } atomSpec.addRange(model, startPos, endPos, chain); @@ -349,7 +357,7 @@ public class ChimeraCommands StructureSelectionManager ssm, String[] files, SequenceI[][] seqs, AlignmentViewPanel viewPanel) { - Map> theMap = new LinkedHashMap>(); + Map> theMap = new LinkedHashMap<>(); FeatureRenderer fr = viewPanel.getFeatureRenderer(); if (fr == null) @@ -357,8 +365,27 @@ public class ChimeraCommands return theMap; } + AlignViewportI viewport = viewPanel.getAlignViewport(); List visibleFeatures = fr.getDisplayedFeatureTypes(); - if (visibleFeatures.isEmpty()) + + /* + * if alignment is showing features from complement, we also transfer + * these features to the corresponding mapped structure residues + */ + boolean showLinkedFeatures = viewport.isShowComplementFeatures(); + List complementFeatures = new ArrayList<>(); + FeatureRenderer complementRenderer = null; + if (showLinkedFeatures) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + if (comp != null) + { + complementRenderer = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + complementFeatures = complementRenderer.getDisplayedFeatureTypes(); + } + } + if (visibleFeatures.isEmpty() && complementFeatures.isEmpty()) { return theMap; } @@ -379,16 +406,23 @@ public class ChimeraCommands { final SequenceI seq = seqs[pdbfnum][seqNo]; int sp = alignment.findIndex(seq); - if (mapping[m].getSequence() == seq && sp > -1) + StructureMapping structureMapping = mapping[m]; + if (structureMapping.getSequence() == seq && sp > -1) { /* * found a sequence with a mapping to a structure; * now scan its features */ - SequenceI asp = alignment.getSequenceAt(sp); - - scanSequenceFeatures(visibleFeatures, mapping[m], asp, theMap, - pdbfnum); + if (!visibleFeatures.isEmpty()) + { + scanSequenceFeatures(visibleFeatures, structureMapping, seq, + theMap, pdbfnum); + } + if (showLinkedFeatures) + { + scanComplementFeatures(complementRenderer, structureMapping, + seq, theMap, pdbfnum); + } } } } @@ -397,9 +431,85 @@ public class ChimeraCommands } /** - * Inspect features on the sequence; for each feature that is visible, - * determine its mapped ranges in the structure (if any) according to the - * given mapping, and add them to the map + * Scans visible features in mapped positions of the CDS/peptide complement, and + * adds any found to the map of attribute values/structure positions + * + * @param complementRenderer + * @param structureMapping + * @param seq + * @param theMap + * @param modelNumber + */ + protected static void scanComplementFeatures( + FeatureRenderer complementRenderer, + StructureMapping structureMapping, SequenceI seq, + Map> theMap, int modelNumber) + { + /* + * for each sequence residue mapped to a structure position... + */ + for (int seqPos : structureMapping.getMapping().keySet()) + { + /* + * find visible complementary features at mapped position(s) + */ + MappedFeatures mf = complementRenderer + .findComplementFeaturesAtResidue(seq, seqPos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + String type = sf.getType(); + + /* + * Don't copy features which originated from Chimera + */ + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) + { + continue; + } + + /* + * record feature 'value' (score/description/type) as at the + * corresponding structure position + */ + List mappedRanges = structureMapping + .getPDBResNumRanges(seqPos, seqPos); + + if (!mappedRanges.isEmpty()) + { + String value = sf.getDescription(); + if (value == null || value.length() == 0) + { + value = type; + } + float score = sf.getScore(); + if (score != 0f && !Float.isNaN(score)) + { + value = Float.toString(score); + } + Map featureValues = theMap.get(type); + if (featureValues == null) + { + featureValues = new HashMap<>(); + theMap.put(type, featureValues); + } + for (int[] range : mappedRanges) + { + addAtomSpecRange(featureValues, value, modelNumber, range[0], + range[1], structureMapping.getChain()); + } + } + } + } + } + } + + /** + * Inspect features on the sequence; for each feature that is visible, determine + * its mapped ranges in the structure (if any) according to the given mapping, + * and add them to the map. * * @param visibleFeatures * @param mapping @@ -418,15 +528,14 @@ public class ChimeraCommands String type = sf.getType(); /* - * Only copy visible features, don't copy any which originated - * from Chimera, and suppress uninteresting ones (e.g. RESNUM) + * Don't copy features which originated from Chimera */ - boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP - .equals(sf.getFeatureGroup()); - if (isFromViewer) + if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP + .equals(sf.getFeatureGroup())) { continue; } + List mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(), sf.getEnd()); @@ -445,12 +554,12 @@ public class ChimeraCommands Map featureValues = theMap.get(type); if (featureValues == null) { - featureValues = new HashMap(); + featureValues = new HashMap<>(); theMap.put(type, featureValues); } for (int[] range : mappedRanges) { - addColourRange(featureValues, value, modelNumber, range[0], + addAtomSpecRange(featureValues, value, modelNumber, range[0], range[1], mapping.getChain()); } } @@ -475,7 +584,7 @@ public class ChimeraCommands protected static List buildSetAttributeCommands( Map> featureMap) { - List commands = new ArrayList(); + List commands = new ArrayList<>(); for (String featureType : featureMap.keySet()) { String attributeName = makeAttributeName(featureType); diff --git a/src/jalview/ext/so/SequenceOntology.java b/src/jalview/ext/so/SequenceOntology.java index 5a86fa1..0d631e6 100644 --- a/src/jalview/ext/so/SequenceOntology.java +++ b/src/jalview/ext/so/SequenceOntology.java @@ -82,10 +82,10 @@ public class SequenceOntology implements SequenceOntologyI */ public SequenceOntology() { - termsFound = new ArrayList<>(); - termsNotFound = new ArrayList<>(); - termsByDescription = new HashMap<>(); - termIsA = new HashMap<>(); + termsFound = new ArrayList(); + termsNotFound = new ArrayList(); + termsByDescription = new HashMap(); + termIsA = new HashMap>(); loadOntologyZipFile("so-xp-simple.obo"); } @@ -404,7 +404,7 @@ public class SequenceOntology implements SequenceOntologyI */ protected synchronized void findParents(Term childTerm) { - List result = new ArrayList<>(); + List result = new ArrayList(); for (Triple triple : ontology.getTriples(childTerm, null, isA)) { Term parent = triple.getObject(); diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index e44ccdc..b5c257b 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -33,8 +33,8 @@ import jalview.api.AlignViewControllerGuiI; import jalview.api.AlignViewControllerI; import jalview.api.AlignViewportI; import jalview.api.AlignmentViewPanel; -//from JalviewLite imports import jalview.api.FeatureRenderer; import jalview.api.FeatureSettingsControllerI; +import jalview.api.FeatureSettingsModelI; import jalview.api.SplitContainerI; import jalview.api.ViewStyleI; import jalview.api.analysis.SimilarityParamsI; @@ -300,6 +300,9 @@ public class AlignFrame extends GAlignFrame } viewport = new AlignViewport(al, hiddenColumns, sequenceSetId, viewId); + // BH! new alignPanel must come later + // alignPanel = new AlignmentPanel(this, viewport); + // addAlignmentPanel(alignPanel, true); init(); } @@ -320,7 +323,10 @@ public class AlignFrame extends GAlignFrame { viewport.hideSequence(hiddenSeqs); } - init(); + // BH! new alignPanel must come later + //alignPanel = new AlignmentPanel(this, viewport); + //addAlignmentPanel(alignPanel, true); +init(); } /** @@ -335,6 +341,8 @@ public class AlignFrame extends GAlignFrame { viewport = ap.av; alignPanel = ap; + // BH! adding must come later + // addAlignmentPanel(ap, false); init(); } @@ -344,6 +352,7 @@ public class AlignFrame extends GAlignFrame */ void init() { + // BH! Here is where we create the panel; note the sequence boolean newPanel = (alignPanel == null); viewport.setShowAutocalculatedAbove(isShowAutoCalculatedAbove()); if (newPanel) @@ -366,7 +375,8 @@ public class AlignFrame extends GAlignFrame addAlignmentPanel(alignPanel, newPanel); // setBackground(Color.white); // BH 2019 - + + // BH meaning "without display, one way or another" if (!Jalview.isHeadlessMode()) { progressBar = new ProgressBar(this.statusPanel, this.statusBar); @@ -386,7 +396,7 @@ public class AlignFrame extends GAlignFrame // modifyPID.setEnabled(false); } - String sortby = jalview.bin.Cache.getDefault(Preferences.SORT_ALIGNMENT, + String sortby = Cache.getDefault(Preferences.SORT_ALIGNMENT, "No sort"); if (sortby.equals("Id")) @@ -398,6 +408,9 @@ public class AlignFrame extends GAlignFrame sortPairwiseMenuItem_actionPerformed(null); } + alignPanel.av + .setShowAutocalculatedAbove(isShowAutoCalculatedAbove()); + setMenusFromViewport(viewport); buildSortByAnnotationScoresMenu(); calculateTree.addActionListener(new ActionListener() @@ -414,24 +427,10 @@ public class AlignFrame extends GAlignFrame if (Desktop.getDesktopPane() != null) { this.setDropTarget(new java.awt.dnd.DropTarget(this, this)); - PropertyChangeListener serviceListener = (Platform.isJS() ? null - : addServiceListeners()); - addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() + if (!Platform.isJS()) { - @Override - public void internalFrameClosed( - javax.swing.event.InternalFrameEvent evt) - { - // System.out.println("deregistering discoverer listener"); - if (serviceListener != null) - { - Desktop.getInstance().removeJalviewPropertyChangeListener( - "services", serviceListener); - } - closeMenuItem_actionPerformed(true); - } - }); - + addServiceListeners(); + } setGUINucleotide(); } @@ -823,6 +822,7 @@ public class AlignFrame extends GAlignFrame { ap.av.getAlignment().padGaps(); } + // BH! important option for JalviewJS if (Jalview.getInstance().getStartCalculations()) { ap.av.updateConservation(ap); @@ -848,9 +848,11 @@ public class AlignFrame extends GAlignFrame } /* Set up intrinsic listeners for dynamically generated GUI bits. */ - private PropertyChangeListener addServiceListeners() + private void addServiceListeners() { - PropertyChangeListener serviceListener = new PropertyChangeListener() + PropertyChangeListener thisListener; + Desktop.getInstance().addJalviewPropertyChangeListener("services", + thisListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) @@ -862,16 +864,26 @@ public class AlignFrame extends GAlignFrame @Override public void run() { - System.err.println("Rebuild WS Menu for service change"); + System.err.println( + "Rebuild WS Menu for service change"); BuildWebServiceMenu(); } }); } } - }; - - Desktop.getInstance().addJalviewPropertyChangeListener("services", - serviceListener); + }); + addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() + { + @Override + public void internalFrameClosed( + javax.swing.event.InternalFrameEvent evt) + { + // System.out.println("deregistering discoverer listener"); + Desktop.getInstance().removeJalviewPropertyChangeListener("services", + thisListener); + closeMenuItem_actionPerformed(true); + } + }); // Finally, build the menu once to get current service state new Thread(new Runnable() { @@ -881,7 +893,6 @@ public class AlignFrame extends GAlignFrame BuildWebServiceMenu(); } }).start(); - return serviceListener; } /** @@ -942,9 +953,8 @@ public class AlignFrame extends GAlignFrame scaleLeft.setVisible(av.getWrapAlignment()); scaleRight.setVisible(av.getWrapAlignment()); annotationPanelMenuItem.setState(av.isShowAnnotation()); - /* - * Show/hide annotations only enabled if annotation panel is shown - */ + // Show/hide annotations only enabled if annotation panel is shown + syncAnnotationMenuItems(); viewBoxesMenuItem.setSelected(av.getShowBoxes()); @@ -1086,9 +1096,14 @@ public class AlignFrame extends GAlignFrame DataSourceType protocol = fileName.startsWith("http:") ? DataSourceType.URL : DataSourceType.FILE; - loader.loadFile(viewport, - (fileObject == null ? fileName : fileObject), protocol, - currentFileFormat); + if (fileObject == null) + { + loader.LoadFile(fileName, protocol, currentFileFormat); + } + else + { + loader.loadFile(null, fileObject, protocol, currentFileFormat); + } } else { @@ -1100,11 +1115,11 @@ public class AlignFrame extends GAlignFrame if (fileObject == null) { - +// BH! Q: What about https? DataSourceType protocol = (fileName.startsWith("http:") ? DataSourceType.URL : DataSourceType.FILE); - newframe = loader.loadFileWaitTillLoaded(fileName, protocol, + newframe = loader.LoadFileWaitTillLoaded(fileName, protocol, currentFileFormat); } else @@ -1310,7 +1325,7 @@ public class AlignFrame extends GAlignFrame try { String tempFilePath = doBackup ? backupfiles.getTempFilePath() : file; - PrintWriter out = new PrintWriter( + PrintWriter out = new PrintWriter( new FileWriter(tempFilePath)); out.print(output); @@ -1569,6 +1584,11 @@ public class AlignFrame extends GAlignFrame if (closeAllTabs) { + if (featureSettings != null && featureSettings.isOpen()) + { + featureSettings.close(); + featureSettings = null; + } /* * this will raise an INTERNAL_FRAME_CLOSED event and this method will * be called recursively, with the frame now in 'closed' state @@ -2184,7 +2204,7 @@ public class AlignFrame extends GAlignFrame newGraphGroups.add(q, null); } newGraphGroups.set(newann.graphGroup, - new Integer(++fgroup)); + Integer.valueOf(++fgroup)); } newann.graphGroup = newGraphGroups.get(newann.graphGroup) .intValue(); @@ -2231,7 +2251,7 @@ public class AlignFrame extends GAlignFrame newGraphGroups.add(q, null); } newGraphGroups.set(newann.graphGroup, - new Integer(++fgroup)); + Integer.valueOf(++fgroup)); } newann.graphGroup = newGraphGroups.get(newann.graphGroup) .intValue(); @@ -2429,33 +2449,33 @@ public class AlignFrame extends GAlignFrame Runnable okAction = new Runnable() { - @Override - public void run() - { - SequenceI[] cut = sg.getSequences() - .toArray(new SequenceI[sg.getSize()]); - - addHistoryItem(new EditCommand( - MessageManager.getString("label.cut_sequences"), Action.CUT, - cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1, - viewport.getAlignment())); - - viewport.setSelectionGroup(null); - viewport.sendSelection(); - viewport.getAlignment().deleteGroup(sg); - - viewport.firePropertyChange("alignment", null, - viewport.getAlignment().getSequences()); - if (viewport.getAlignment().getHeight() < 1) - { - try - { - AlignFrame.this.setClosed(true); - } catch (Exception ex) - { - } - } - }}; + @Override + public void run() + { + SequenceI[] cut = sg.getSequences() + .toArray(new SequenceI[sg.getSize()]); + + addHistoryItem(new EditCommand( + MessageManager.getString("label.cut_sequences"), Action.CUT, + cut, sg.getStartRes(), sg.getEndRes() - sg.getStartRes() + 1, + viewport.getAlignment())); + + viewport.setSelectionGroup(null); + viewport.sendSelection(); + viewport.getAlignment().deleteGroup(sg); + + viewport.firePropertyChange("alignment", null, + viewport.getAlignment().getSequences()); + if (viewport.getAlignment().getHeight() < 1) + { + try + { + AlignFrame.this.setClosed(true); + } catch (Exception ex) + { + } + } + }}; /* * If the cut affects all sequences, prompt for confirmation @@ -2463,20 +2483,20 @@ public class AlignFrame extends GAlignFrame boolean wholeHeight = sg.getSize() == viewport.getAlignment().getHeight(); boolean wholeWidth = (((sg.getEndRes() - sg.getStartRes()) + 1) == viewport.getAlignment().getWidth()) ? true : false; - if (wholeHeight && wholeWidth) - { - JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.getDesktopPane()); - dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION - Object[] options = new Object[] { MessageManager.getString("action.ok"), - MessageManager.getString("action.cancel") }; - dialog.showDialog(MessageManager.getString("warn.delete_all"), - MessageManager.getString("label.delete_all"), - JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null, - options, options[0]); - } else - { - okAction.run(); - } + if (wholeHeight && wholeWidth) + { + JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.getDesktopPane()); + dialog.setResponseHandler(0, okAction); // 0 = OK_OPTION + Object[] options = new Object[] { MessageManager.getString("action.ok"), + MessageManager.getString("action.cancel") }; + dialog.showDialog(MessageManager.getString("warn.delete_all"), + MessageManager.getString("label.delete_all"), + JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null, + options, options[0]); + } else + { + okAction.run(); + } } /** @@ -3276,9 +3296,15 @@ public class AlignFrame extends GAlignFrame @Override public void featureSettings_actionPerformed(ActionEvent e) { + showFeatureSettingsUI(); + } + + @Override + public FeatureSettingsControllerI showFeatureSettingsUI() + { if (featureSettings != null) { - featureSettings.close(); + featureSettings.closeOldSettings(); featureSettings = null; } if (!showSeqFeatures.isSelected()) @@ -3288,6 +3314,7 @@ public class AlignFrame extends GAlignFrame showSeqFeatures_actionPerformed(null); } featureSettings = new FeatureSettings(this); + return featureSettings; } /** @@ -4695,6 +4722,7 @@ public class AlignFrame extends GAlignFrame // associating PDB files which have no IDs. for (SequenceI toassoc : (SequenceI[]) fm[2]) { + // BH! check PDBEntry pe = AssociatePdbFileWithSeq .associatePdbWithSeq(fm[0].toString(), (DataSourceType) fm[1], toassoc, false); @@ -4862,12 +4890,24 @@ public class AlignFrame extends GAlignFrame { if (parseFeaturesFile(file, sourceType)) { + SplitFrame splitFrame = (SplitFrame) getSplitViewContainer(); + if (splitFrame != null) + { + splitFrame.repaint(); + } + else + { alignPanel.paintAlignment(true, true); } } + } else { - new FileLoader().loadFile(viewport, file, sourceType, format); + if (file instanceof File) { + new FileLoader().loadFile(viewport, (File) file, sourceType, format); + } else { + new FileLoader().LoadFile(viewport, (String) file, sourceType, format); + } } } } @@ -4926,6 +4966,21 @@ public class AlignFrame extends GAlignFrame viewport = alignPanel.av; avc.setViewportAndAlignmentPanel(viewport, alignPanel); setMenusFromViewport(viewport); + if (featureSettings != null && featureSettings.isOpen() + && featureSettings.fr.getViewport() != viewport) + { + if (viewport.isShowSequenceFeatures()) + { + // refresh the featureSettings to reflect UI change + showFeatureSettingsUI(); + } + else + { + // close feature settings for this view. + featureSettings.close(); + } + } + } /* @@ -5149,6 +5204,13 @@ public class AlignFrame extends GAlignFrame @Override public void finished() { + + for (FeatureSettingsModelI srcSettings : dbRefFetcher + .getFeatureSettingsModels()) + { + + alignPanel.av.mergeFeaturesStyle(srcSettings); + } AlignFrame.this.setMenusForViewport(); } }); @@ -5195,6 +5257,7 @@ public class AlignFrame extends GAlignFrame } if (otherdb.size() == 1) { + // BH cleaner code DbSourceProxy src = otherdb.get(0); DbSourceProxy[] dassource = new DbSourceProxy[] { src }; @@ -5225,6 +5288,10 @@ public class AlignFrame extends GAlignFrame @Override public void finished() { + FeatureSettingsModelI srcSettings = dassource[0] + .getFeatureColourScheme(); + alignPanel.av.mergeFeaturesStyle( + srcSettings); AlignFrame.this.setMenusForViewport(); } }); @@ -5855,6 +5922,20 @@ public class AlignFrame extends GAlignFrame } + + private Rectangle lastFeatureSettingsBounds = null; + @Override + public void setFeatureSettingsGeometry(Rectangle bounds) + { + lastFeatureSettingsBounds = bounds; + } + + @Override + public Rectangle getFeatureSettingsGeometry() + { + return lastFeatureSettingsBounds; + } + /** * BH 2019 from JalviewLite * diff --git a/src/jalview/gui/AlignViewport.java b/src/jalview/gui/AlignViewport.java index fb3ec3a..954cfcc 100644 --- a/src/jalview/gui/AlignViewport.java +++ b/src/jalview/gui/AlignViewport.java @@ -47,6 +47,7 @@ import jalview.schemes.UserColourScheme; import jalview.structure.SelectionSource; import jalview.structure.StructureSelectionManager; import jalview.structure.VamsasSource; +import jalview.util.ColorUtils; import jalview.util.MessageManager; import jalview.viewmodel.AlignmentViewport; import jalview.ws.params.AutoCalcSetting; @@ -56,6 +57,7 @@ import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Rectangle; +import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.List; @@ -77,9 +79,9 @@ public class AlignViewport extends AlignmentViewport boolean antiAlias = false; - private Rectangle explodedGeometry; + private Rectangle explodedGeometry = null; - private String viewName; + private String viewName = null; /* * Flag set true on the view that should 'gather' multiple views of the same @@ -206,7 +208,10 @@ public class AlignViewport extends AlignmentViewport */ private void applyViewProperties() { - antiAlias = Cache.getDefault(Preferences.ANTI_ALIAS, false); + // BH! using final static strings here because we also use these in + // JS version startup api + // BH was false + antiAlias = Cache.getDefault(Preferences.ANTI_ALIAS, true); viewStyle.setShowJVSuffix( Cache.getDefault(Preferences.SHOW_JVSUFFIX, true)); @@ -297,8 +302,8 @@ public class AlignViewport extends AlignmentViewport schemeName = Cache.getDefault(Preferences.DEFAULT_COLOUR, ResidueColourScheme.NONE); } - ColourSchemeI colourScheme = ColourSchemeProperty.getColourScheme(this, - alignment, schemeName); + ColourSchemeI colourScheme = ColourSchemeProperty + .getColourScheme(this, alignment, schemeName); residueShading = new ResidueShader(colourScheme); if (colourScheme instanceof UserColourScheme) @@ -823,10 +828,10 @@ public class AlignViewport extends AlignmentViewport public static void openLinkedAlignmentAs(AlignFrame thisFrame, AlignmentI thisAlignment, AlignmentI al, String title, int mode) { - /* - * Identify protein and dna alignments. Make a copy of this one if opening - * in a new split pane. - */ + // BH: thisAlignment is already a copy if mode == SPLIT_FRAME + // Identify protein and dna alignments. Make a copy of this one if opening + // in a new split pane. + AlignmentI protein = al.isNucleotide() ? thisAlignment : al; AlignmentI cdna = al.isNucleotide() ? al : thisAlignment; @@ -865,8 +870,7 @@ public class AlignViewport extends AlignmentViewport try { - newAlignFrame.setMaximum(jalview.bin.Cache - .getDefault(Preferences.SHOW_FULLSCREEN, false)); + newAlignFrame.setMaximum(Cache.getDefault(Preferences.SHOW_FULLSCREEN, false)); } catch (java.beans.PropertyVetoException ex) { } @@ -937,10 +941,9 @@ public class AlignViewport extends AlignmentViewport if (ap != null) { // modify GUI elements to reflect geometry change - Dimension idw = getAlignPanel().getIdPanel().getIdCanvas() - .getPreferredSize(); + Dimension idw = ap.getIdPanel().getIdCanvas().getPreferredSize(); idw.width = i; - getAlignPanel().getIdPanel().getIdCanvas().setPreferredSize(idw); + ap.getIdPanel().getIdCanvas().setPreferredSize(idw); } } @@ -1029,6 +1032,35 @@ public class AlignViewport extends AlignmentViewport @Override public void applyFeaturesStyle(FeatureSettingsModelI featureSettings) { + transferFeaturesStyles(featureSettings, false); + } + + /** + * Applies the supplied feature settings descriptor to currently known features. + * This supports an 'initial configuration' of feature colouring based on a + * preset or user favourite. This may then be modified in the usual way using + * the Feature Settings dialogue. + * + * @param featureSettings + */ + @Override + public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings) + { + transferFeaturesStyles(featureSettings, true); + } + + /** + * when mergeOnly is set, then group and feature visibility or feature colours + * are not modified for features and groups already known to the feature + * renderer. Feature ordering is always adjusted, and transparency is always set + * regardless. + * + * @param featureSettings + * @param mergeOnly + */ + private void transferFeaturesStyles(FeatureSettingsModelI featureSettings, + boolean mergeOnly) + { if (featureSettings == null) { return; @@ -1036,12 +1068,24 @@ public class AlignViewport extends AlignmentViewport FeatureRenderer fr = getAlignPanel().getSeqPanel().seqCanvas .getFeatureRenderer(); + List origRenderOrder = new ArrayList<>(), + origGroups = new ArrayList<>(); + // preserve original render order - allows differentiation between user configured colours and autogenerated ones + origRenderOrder.addAll(fr.getRenderOrder()); + origGroups.addAll(fr.getFeatureGroups()); + fr.findAllFeatures(true); List renderOrder = fr.getRenderOrder(); FeaturesDisplayedI displayed = fr.getFeaturesDisplayed(); + if (!mergeOnly) + { + // only clear displayed features if we are merging displayed.clear(); + } // TODO this clears displayed.featuresRegistered - do we care? - + // + // JAL-3330 - JBP - yes we do - calling applyFeatureStyle to a view where + // feature visibility has already been configured is not very friendly /* * set feature colour if specified by feature settings * set visibility of all features @@ -1050,6 +1094,16 @@ public class AlignViewport extends AlignmentViewport { FeatureColourI preferredColour = featureSettings .getFeatureColour(type); + FeatureColourI origColour = fr.getFeatureStyle(type); + if (!mergeOnly || (!origRenderOrder.contains(type) + || origColour == null + || (!origColour.isGraduatedColour() + && origColour.getColour() != null + && origColour.getColour().equals( + ColorUtils.createColourFromName(type))))) + { + // if we are merging, only update if there wasn't already a colour defined for + // this type if (preferredColour != null) { fr.setColour(type, preferredColour); @@ -1059,13 +1113,19 @@ public class AlignViewport extends AlignmentViewport displayed.setVisible(type); } } + } /* * set visibility of feature groups */ for (String group : fr.getFeatureGroups()) { - fr.setGroupVisibility(group, featureSettings.isGroupDisplayed(group)); + if (!mergeOnly || !origGroups.contains(group)) + { + // when merging, display groups only if the aren't already marked as not visible + fr.setGroupVisibility(group, + featureSettings.isGroupDisplayed(group)); + } } /* diff --git a/src/jalview/gui/AnnotationExporter.java b/src/jalview/gui/AnnotationExporter.java index 81960e0..d84287f 100644 --- a/src/jalview/gui/AnnotationExporter.java +++ b/src/jalview/gui/AnnotationExporter.java @@ -30,17 +30,17 @@ import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; import jalview.util.MessageManager; -import java.awt.BorderLayout; import java.awt.Color; -import java.awt.FlowLayout; +import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.FileWriter; import java.io.PrintWriter; -import javax.swing.BorderFactory; +import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JLayeredPane; @@ -71,6 +71,29 @@ public class AnnotationExporter extends JPanel private boolean wholeView; + /* + * option to export linked (CDS/peptide) features when shown + * on the alignment, converted to this alignment's coordinates + */ + private JCheckBox includeLinkedFeatures; + + /* + * output format option shown for feature export + */ + JRadioButton GFFFormat = new JRadioButton(); + + /* + * output format option shown for annotation export + */ + JRadioButton CSVFormat = new JRadioButton(); + + private JPanel linkedFeaturesPanel; + + /** + * Constructor + * + * @param panel + */ public AnnotationExporter(AlignmentPanel panel) { this.ap = panel; @@ -85,17 +108,25 @@ public class AnnotationExporter extends JPanel frame = new JInternalFrame(); frame.setContentPane(this); frame.setLayer(JLayeredPane.PALETTE_LAYER); - Desktop.addInternalFrame(frame, "", frame.getPreferredSize().width, - frame.getPreferredSize().height); + Dimension preferredSize = frame.getPreferredSize(); + Desktop.addInternalFrame(frame, "", true, preferredSize.width, + preferredSize.height, true, true); } /** - * Configures the diglog for options to export visible features + * Configures the dialog for options to export visible features. If from a split + * frame panel showing linked features, make the option to include these in the + * export visible. */ public void exportFeatures() { exportFeatures = true; CSVFormat.setVisible(false); + if (ap.av.isShowComplementFeatures()) + { + linkedFeaturesPanel.setVisible(true); + frame.pack(); + } frame.setTitle(MessageManager.getString("label.export_features")); } @@ -216,14 +247,17 @@ public class AnnotationExporter extends JPanel FeaturesFile formatter = new FeaturesFile(); final FeatureRenderer fr = ap.getFeatureRenderer(); + boolean includeComplement = includeLinkedFeatures.isSelected(); + if (GFFFormat.isSelected()) { - text = formatter.printGffFormat(sequences, fr, includeNonPositional); + text = formatter.printGffFormat(sequences, fr, includeNonPositional, + includeComplement); } else { text = formatter.printJalviewFormat(sequences, fr, - includeNonPositional); + includeNonPositional, includeComplement); } return text; } @@ -269,11 +303,70 @@ public class AnnotationExporter extends JPanel } } + /** + * Adds widgets to the panel + * + * @throws Exception + */ private void jbInit() throws Exception { - this.setLayout(new BorderLayout()); + this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.setBackground(Color.white); + + JPanel formatPanel = buildFormatOptionsPanel(); + JPanel linkedFeatures = buildLinkedFeaturesPanel(); + JPanel actionsPanel = buildActionsPanel(); + + this.add(formatPanel); + this.add(linkedFeatures); + this.add(actionsPanel); + } + + /** + * Builds a panel with a checkbox for the option to export linked (CDS/peptide) + * features. This is hidden by default, and only made visible if exporting + * features from a split frame panel which is configured to show linked + * features. + * + * @return + */ + private JPanel buildLinkedFeaturesPanel() + { + linkedFeaturesPanel = new JPanel(); + linkedFeaturesPanel.setOpaque(false); + + boolean nucleotide = ap.av.isNucleotide(); + String complement = nucleotide + ? MessageManager.getString("label.protein").toLowerCase() + : "CDS"; + JLabel label = new JLabel( + MessageManager.formatMessage("label.include_linked_features", + complement)); + label.setHorizontalAlignment(SwingConstants.TRAILING); + String tooltip = MessageManager + .formatMessage("label.include_linked_tooltip", complement); + label.setToolTipText( + JvSwingUtils.wrapTooltip(true, tooltip)); + + includeLinkedFeatures = new JCheckBox(); + linkedFeaturesPanel.add(label); + linkedFeaturesPanel.add(includeLinkedFeatures); + linkedFeaturesPanel.setVisible(false); + + return linkedFeaturesPanel; + } + + /** + * Builds the panel with to File or Textbox or Close actions + * + * @return + */ + JPanel buildActionsPanel() + { + JPanel actionsPanel = new JPanel(); + actionsPanel.setOpaque(false); - toFile.setText(MessageManager.getString("label.to_file")); + JButton toFile = new JButton(MessageManager.getString("label.to_file")); toFile.addActionListener(new ActionListener() { @Override @@ -282,7 +375,8 @@ public class AnnotationExporter extends JPanel toFile_actionPerformed(); } }); - toTextbox.setText(MessageManager.getString("label.to_textbox")); + JButton toTextbox = new JButton( + MessageManager.getString("label.to_textbox")); toTextbox.addActionListener(new ActionListener() { @Override @@ -291,7 +385,7 @@ public class AnnotationExporter extends JPanel toTextbox_actionPerformed(); } }); - close.setText(MessageManager.getString("action.close")); + JButton close = new JButton(MessageManager.getString("action.close")); close.addActionListener(new ActionListener() { @Override @@ -300,52 +394,49 @@ public class AnnotationExporter extends JPanel close_actionPerformed(); } }); + + actionsPanel.add(toFile); + actionsPanel.add(toTextbox); + actionsPanel.add(close); + + return actionsPanel; + } + + /** + * Builds the panel with options to output in Jalview, GFF or CSV format. GFF is + * only made visible when exporting features, CSV only when exporting + * annotation. + * + * @return + */ + JPanel buildFormatOptionsPanel() + { + JPanel formatPanel = new JPanel(); + // formatPanel.setBorder(BorderFactory.createEtchedBorder()); + formatPanel.setOpaque(false); + + JRadioButton jalviewFormat = new JRadioButton("Jalview"); jalviewFormat.setOpaque(false); jalviewFormat.setSelected(true); - jalviewFormat.setText("Jalview"); GFFFormat.setOpaque(false); GFFFormat.setText("GFF"); CSVFormat.setOpaque(false); CSVFormat.setText(MessageManager.getString("label.csv_spreadsheet")); - jLabel1.setHorizontalAlignment(SwingConstants.TRAILING); - jLabel1.setText(MessageManager.getString("action.format") + " "); - this.setBackground(Color.white); - jPanel3.setBorder(BorderFactory.createEtchedBorder()); - jPanel3.setOpaque(false); - jPanel1.setOpaque(false); - jPanel1.add(toFile); - jPanel1.add(toTextbox); - jPanel1.add(close); - jPanel3.add(jLabel1); - jPanel3.add(jalviewFormat); - jPanel3.add(GFFFormat); - jPanel3.add(CSVFormat); + + ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(jalviewFormat); buttonGroup.add(GFFFormat); buttonGroup.add(CSVFormat); - this.add(jPanel3, BorderLayout.CENTER); - this.add(jPanel1, BorderLayout.SOUTH); - } - - JPanel jPanel1 = new JPanel(); - - JButton toFile = new JButton(); - - JButton toTextbox = new JButton(); - - JButton close = new JButton(); - ButtonGroup buttonGroup = new ButtonGroup(); + JLabel format = new JLabel( + MessageManager.getString("action.format") + " "); + format.setHorizontalAlignment(SwingConstants.TRAILING); - JRadioButton jalviewFormat = new JRadioButton(); + formatPanel.add(format); + formatPanel.add(jalviewFormat); + formatPanel.add(GFFFormat); + formatPanel.add(CSVFormat); - JRadioButton GFFFormat = new JRadioButton(); - - JRadioButton CSVFormat = new JRadioButton(); - - JLabel jLabel1 = new JLabel(); - - JPanel jPanel3 = new JPanel(); - - FlowLayout flowLayout1 = new FlowLayout(); + return formatPanel; + } } diff --git a/src/jalview/gui/CrossRefAction.java b/src/jalview/gui/CrossRefAction.java index d37cb29..62fecab 100644 --- a/src/jalview/gui/CrossRefAction.java +++ b/src/jalview/gui/CrossRefAction.java @@ -356,12 +356,12 @@ public class CrossRefAction implements Runnable seq.getLength()); if (geneLoci != null) { - MapList map = geneLoci.getMap(); + MapList map = geneLoci.getMapping(); int mappedFromLength = MappingUtils.getLength(map.getFromRanges()); if (mappedFromLength == seq.getLength()) { seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(), - geneLoci.getChromosomeId(), geneLoci.getMap()); + geneLoci.getChromosomeId(), geneLoci.getMapping()); retrievedLoci.put(dbref, geneLoci); return true; } @@ -374,12 +374,12 @@ public class CrossRefAction implements Runnable seq.getLength()); if (geneLoci != null) { - MapList map = geneLoci.getMap(); + MapList map = geneLoci.getMapping(); int mappedFromLength = MappingUtils.getLength(map.getFromRanges()); if (mappedFromLength == seq.getLength()) { seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(), - geneLoci.getChromosomeId(), geneLoci.getMap()); + geneLoci.getChromosomeId(), geneLoci.getMapping()); retrievedLoci.put(dbref, geneLoci); return true; } diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index e13a63e..717fb1a 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -39,7 +39,6 @@ import jalview.io.FormatAdapter; import jalview.io.IdentifyFile; import jalview.io.JalviewFileChooser; import jalview.io.JalviewFileView; -import jalview.jbgui.GDesktop; import jalview.jbgui.GSplitFrame; import jalview.jbgui.GStructureViewer; import jalview.project.Jalview2XML; @@ -49,9 +48,9 @@ import jalview.util.BrowserLauncher; import jalview.util.ImageMaker.TYPE; import jalview.util.MessageManager; import jalview.util.Platform; +import jalview.util.ShortcutKeyMaskExWrapper; import jalview.util.UrlConstants; import jalview.viewmodel.AlignmentViewport; -import jalview.ws.jws1.Discoverer; import jalview.ws.params.ParamManager; import jalview.ws.utils.UrlDownloadClient; @@ -84,12 +83,13 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; +import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.ListIterator; @@ -111,7 +111,6 @@ import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDesktopPane; -import javax.swing.JFrame; import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JMenuItem; @@ -125,8 +124,6 @@ import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkEvent.EventType; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; -import javax.swing.event.MenuEvent; -import javax.swing.event.MenuListener; import org.stackoverflowusers.file.WindowsShortcut; @@ -137,11 +134,18 @@ import org.stackoverflowusers.file.WindowsShortcut; * @author $author$ * @version $Revision: 1.155 $ */ -@SuppressWarnings("serial") -public class Desktop extends GDesktop +public class Desktop extends jalview.jbgui.GDesktop implements DropTargetListener, ClipboardOwner, IProgressIndicator, StructureSelectionManagerProvider, ApplicationSingletonI { + private static final String CITATION = "

Development managed by The Barton Group, University of Dundee, Scotland, UK.
" + + "

For help, see the FAQ at www.jalview.org/faq and/or join the jalview-discuss@jalview.org mailing list" + + "

If you use Jalview, please cite:" + + "
Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)" + + "
Jalview Version 2 - a multiple sequence alignment editor and analysis workbench" + + "
Bioinformatics doi: 10.1093/bioinformatics/btp033"; + + private static final String DEFAULT_AUTHORS = "The Jalview Authors (See AUTHORS file for current list)"; private final static int DEFAULT_MIN_WIDTH = 300; @@ -153,12 +157,16 @@ public class Desktop extends GDesktop private final static String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES"; + protected static final String CONFIRM_KEYBOARD_QUIT = "CONFIRM_KEYBOARD_QUIT"; + + public static HashMap savingFiles = new HashMap<>(); + private JalviewChangeSupport changeSupport = new JalviewChangeSupport(); /** * news reader - null if it was never started. */ - BlogReader jvnews = null; + private BlogReader jvnews = null; private File projectFile; @@ -197,12 +205,6 @@ public class Desktop extends GDesktop listener); } - public static MyDesktopPane getDesktopPane() - { - Desktop desktop = Desktop.getInstance(); - return desktop == null ? null : desktop.desktopPane; - } - public static StructureSelectionManager getStructureSelectionManager() { return StructureSelectionManager @@ -215,10 +217,13 @@ public class Desktop extends GDesktop static final int yOffset = 30; - public Discoverer discoverer; + // BH was static + public jalview.ws.jws1.Discoverer discoverer; + //BH was static public Object[] jalviewClipboard; +//BH was static public boolean internalCopy = false; private static int fileLoadingCount = 0; @@ -349,23 +354,6 @@ public class Desktop extends GDesktop // All other methods, simply delegate } - - public MyDesktopPane desktopPane; - - /** - * Answers an 'application scope' singleton instance of this class. Separate - * SwingJS 'applets' running in the same browser page will each have a - * distinct instance of Desktop. - * - * @return - */ - public static Desktop getInstance() - { - return Jalview.isHeadlessMode() ? null - : (Desktop) ApplicationSingletonProvider - .getInstance(Desktop.class); - } - /** * Private constructor enforces singleton pattern. It is called by reflection * from ApplicationSingletonProvider.getInstance(). @@ -380,22 +368,49 @@ public class Desktop extends GDesktop * block are spawned off as threads rather than waited for during this * constructor. */ - if (!Platform.isJS()) + + doConfigureStructurePrefs(); + setTitle("Jalview " + Cache.getProperty("VERSION")); + + try + { + if (Platform.getJavaVersion() >= 11) + { + // BH use reflection so that this code can be in both the Java8 and + // Java11 versions + Class j11APQHandlers = Class + .forName("jalview.gui.APQHandlers"); + Method meth = j11APQHandlers.getMethod("setAPQHandlers", + new Class[] + { Desktop.class }); + meth.invoke(j11APQHandlers.newInstance(), this); + } + } catch (Throwable t) { - doVamsasClientCheck(); + System.out.println( + "Desktop Error setting APQHandlers: " + t.toString()); } - doConfigureStructurePrefs(); - setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION")); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - boolean selmemusage = jalview.bin.Cache.getDefault("SHOW_MEMUSAGE", + addWindowListener(new WindowAdapter() + { + + @Override + public void windowClosing(WindowEvent ev) + { + quit(); + } + }); + + boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false); - boolean showjconsole = jalview.bin.Cache - .getDefault("SHOW_JAVA_CONSOLE", false); - desktopPane = new MyDesktopPane(selmemusage); - showMemusage.setSelected(selmemusage); - desktopPane.setBackground(Color.white); + boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", + false); + desktopPane = new MyDesktopPane(selmemusage); + + showMemusage.setSelected(selmemusage); + desktopPane.setBackground(Color.white); + getContentPane().setLayout(new BorderLayout()); // alternate config - have scrollbars - see notes in JAL-153 // JScrollPane sp = new JScrollPane(); @@ -450,19 +465,7 @@ public class Desktop extends GDesktop { jconsole = new Console(this, showjconsole); - // add essential build information - jconsole.setHeader("Jalview Version: " - + jalview.bin.Cache.getProperty("VERSION") + "\n" - + "Jalview Installation: " - + jalview.bin.Cache.getDefault("INSTALLATION", "unknown") - + "\n" + "Build Date: " - + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown") - + "\n" + "Java version: " - + System.getProperty("java.version") + "\n" - + System.getProperty("os.arch") + " " - + System.getProperty("os.name") + " " - + System.getProperty("os.version")); - + jconsole.setHeader(Cache.getVersionDetailsForConsole()); showConsole(showjconsole); showNews.setVisible(false); @@ -480,7 +483,7 @@ public class Desktop extends GDesktop @Override public void run() { - new SplashScreen(); + new SplashScreen(true); } }); @@ -576,14 +579,14 @@ public class Desktop extends GDesktop // configure services StructureSelectionManager ssm = StructureSelectionManager .getStructureSelectionManager(this); - if (jalview.bin.Cache.getDefault(Preferences.ADD_SS_ANN, true)) + if (Cache.getDefault(Preferences.ADD_SS_ANN, true)) { - ssm.setAddTempFacAnnot(jalview.bin.Cache + ssm.setAddTempFacAnnot(Cache .getDefault(Preferences.ADD_TEMPFACT_ANN, true)); - ssm.setProcessSecondaryStructure(jalview.bin.Cache + ssm.setProcessSecondaryStructure(Cache .getDefault(Preferences.STRUCT_FROM_PDB, true)); ssm.setSecStructServices( - jalview.bin.Cache.getDefault(Preferences.USE_RNAVIEW, true)); + Cache.getDefault(Preferences.USE_RNAVIEW, true)); } else { @@ -619,7 +622,6 @@ public class Desktop extends GDesktop public void run() { Cache.log.debug("Downloading data from identifiers.org"); - // UrlDownloadClient client = new UrlDownloadClient(); try { UrlDownloadClient.download(IdOrgSettings.getUrl(), @@ -674,25 +676,25 @@ public class Desktop extends GDesktop { // TODO: lock aspect ratio for scaling desktop Bug #0058199 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - String x = jalview.bin.Cache.getProperty(windowName + "SCREEN_X"); - String y = jalview.bin.Cache.getProperty(windowName + "SCREEN_Y"); - String width = jalview.bin.Cache + String x = Cache.getProperty(windowName + "SCREEN_X"); + String y = Cache.getProperty(windowName + "SCREEN_Y"); + String width = Cache .getProperty(windowName + "SCREEN_WIDTH"); - String height = jalview.bin.Cache + String height = Cache .getProperty(windowName + "SCREEN_HEIGHT"); if ((x != null) && (y != null) && (width != null) && (height != null)) { int ix = Integer.parseInt(x), iy = Integer.parseInt(y), iw = Integer.parseInt(width), ih = Integer.parseInt(height); - if (jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH") != null) + if (Cache.getProperty("SCREENGEOMETRY_WIDTH") != null) { // attempt #1 - try to cope with change in screen geometry - this // version doesn't preserve original jv aspect ratio. // take ratio of current screen size vs original screen size. double sw = ((1f * screenSize.width) / (1f * Integer.parseInt( - jalview.bin.Cache.getProperty("SCREENGEOMETRY_WIDTH")))); + Cache.getProperty("SCREENGEOMETRY_WIDTH")))); double sh = ((1f * screenSize.height) / (1f * Integer.parseInt( - jalview.bin.Cache.getProperty("SCREENGEOMETRY_HEIGHT")))); + Cache.getProperty("SCREENGEOMETRY_HEIGHT")))); // rescale the bounds depending upon the current screen geometry. ix = (int) (ix * sw); iw = (int) (iw * sw); @@ -700,17 +702,17 @@ public class Desktop extends GDesktop ih = (int) (ih * sh); while (ix >= screenSize.width) { - jalview.bin.Cache.log.debug( + Cache.log.debug( "Window geometry location recall error: shifting horizontal to within screenbounds."); ix -= screenSize.width; } while (iy >= screenSize.height) { - jalview.bin.Cache.log.debug( + Cache.log.debug( "Window geometry location recall error: shifting vertical to within screenbounds."); iy -= screenSize.height; } - jalview.bin.Cache.log.debug( + Cache.log.debug( "Got last known dimensions for " + windowName + ": x:" + ix + " y:" + iy + " width:" + iw + " height:" + ih); } @@ -720,46 +722,6 @@ public class Desktop extends GDesktop return null; } - private void doVamsasClientCheck() - { - if (Cache.vamsasJarsPresent()) - { - setupVamsasDisconnectedGui(); - VamsasMenu.setVisible(true); - final Desktop us = this; - VamsasMenu.addMenuListener(new MenuListener() - { - // this listener remembers when the menu was first selected, and - // doesn't rebuild the session list until it has been cleared and - // reselected again. - boolean refresh = true; - - @Override - public void menuCanceled(MenuEvent e) - { - refresh = true; - } - - @Override - public void menuDeselected(MenuEvent e) - { - refresh = true; - } - - @Override - public void menuSelected(MenuEvent e) - { - if (refresh) - { - us.buildVamsasStMenu(); - refresh = false; - } - } - }); - vamsasStart.setVisible(true); - } - } - protected void showPasteMenu(int x, int y) { JPopupMenu popup = new JPopupMenu(); @@ -793,7 +755,7 @@ public class Desktop extends GDesktop FileFormatI format = new IdentifyFile().identify(file, DataSourceType.PASTE); - new FileLoader().loadFile(file, DataSourceType.PASTE, format); + new FileLoader().LoadFile(file, DataSourceType.PASTE, format); } } catch (Exception ex) @@ -895,6 +857,7 @@ public class Desktop extends GDesktop // the current window title frame.setTitle(title); + // BH fix if (w > 0 && (frame.getWidth() < 1 || frame.getHeight() < 1)) { frame.setSize(w, h); @@ -1031,6 +994,7 @@ public class Desktop extends GDesktop */ private static void setKeyBindings(JInternalFrame frame) { + @SuppressWarnings("serial") final Action closeAction = new AbstractAction() { @Override @@ -1046,7 +1010,7 @@ public class Desktop extends GDesktop KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK); KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); + ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()); InputMap inputMap = frame .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); @@ -1141,9 +1105,13 @@ public class Desktop extends GDesktop if (file instanceof File) { Platform.cacheFileData((File) file); + new FileLoader().loadFile(null, (File) file, protocol, format); } - new FileLoader().loadFile(null, file, protocol, format); + else + { + new FileLoader().LoadFile((String) file, protocol, format); + } } } catch (Exception ex) { @@ -1165,7 +1133,7 @@ public class Desktop extends GDesktop { String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT"); JalviewFileChooser chooser = JalviewFileChooser - .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat, true); + .forRead(Cache.getProperty("LAST_DIRECTORY"), fileFormat, BackupFiles.getEnabled()); chooser.setFileView(new JalviewFileView()); chooser.setDialogTitle( @@ -1199,7 +1167,7 @@ public class Desktop extends GDesktop } } - new FileLoader().loadFile(viewport, selectedFile, + new FileLoader().LoadFile(viewport, selectedFile, DataSourceType.FILE, format); } }); @@ -1273,12 +1241,12 @@ public class Desktop extends GDesktop { if (viewport != null) { - new FileLoader().loadFile(viewport, url, DataSourceType.URL, + new FileLoader().LoadFile(viewport, url, DataSourceType.URL, FileFormat.Jalview); } else { - new FileLoader().loadFile(url, DataSourceType.URL, + new FileLoader().LoadFile(url, DataSourceType.URL, FileFormat.Jalview); } } @@ -1308,12 +1276,12 @@ public class Desktop extends GDesktop if (viewport != null) { - new FileLoader().loadFile(viewport, url, DataSourceType.URL, + new FileLoader().LoadFile(viewport, url, DataSourceType.URL, format); } else { - new FileLoader().loadFile(url, DataSourceType.URL, format); + new FileLoader().LoadFile(url, DataSourceType.URL, format); } } } @@ -1353,9 +1321,9 @@ public class Desktop extends GDesktop public void quit() { Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); - jalview.bin.Cache.setProperty("SCREENGEOMETRY_WIDTH", + Cache.setProperty("SCREENGEOMETRY_WIDTH", screen.width + ""); - jalview.bin.Cache.setProperty("SCREENGEOMETRY_HEIGHT", + Cache.setProperty("SCREENGEOMETRY_HEIGHT", screen.height + ""); storeLastKnownDimensions("", new Rectangle(getBounds().x, getBounds().y, getWidth(), getHeight())); @@ -1387,14 +1355,14 @@ public class Desktop extends GDesktop private void storeLastKnownDimensions(String string, Rectangle jc) { - jalview.bin.Cache.log.debug("Storing last known dimensions for " + Cache.log.debug("Storing last known dimensions for " + string + ": x:" + jc.x + " y:" + jc.y + " width:" + jc.width + " height:" + jc.height); - jalview.bin.Cache.setProperty(string + "SCREEN_X", jc.x + ""); - jalview.bin.Cache.setProperty(string + "SCREEN_Y", jc.y + ""); - jalview.bin.Cache.setProperty(string + "SCREEN_WIDTH", jc.width + ""); - jalview.bin.Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + ""); + Cache.setProperty(string + "SCREEN_X", jc.x + ""); + Cache.setProperty(string + "SCREEN_Y", jc.y + ""); + Cache.setProperty(string + "SCREEN_WIDTH", jc.width + ""); + Cache.setProperty(string + "SCREEN_HEIGHT", jc.height + ""); } /** @@ -1406,53 +1374,45 @@ public class Desktop extends GDesktop @Override public void aboutMenuItem_actionPerformed(ActionEvent e) { - // StringBuffer message = getAboutMessage(false); - // JvOptionPane.showInternalMessageDialog(Desktop.getDesktop(), - // - // message.toString(), "About Jalview", JvOptionPane.INFORMATION_MESSAGE); new Thread(new Runnable() { @Override public void run() { + // BH! true meaning "interactive" here (applet branch); was false in + // develop version?? new SplashScreen(true); } }).start(); } - public StringBuffer getAboutMessage(boolean shortv) + /** + * Returns the html text for the About screen, including any available version + * number, build details, author details and citation reference, but without + * the enclosing {@code html} tags + * + * @return + */ + public String getAboutMessage() { - StringBuffer message = new StringBuffer(); - message.append(""); - if (shortv) - { - message.append("

Version: " - + jalview.bin.Cache.getProperty("VERSION") - + "

"); - message.append("Last Updated: " - + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown") - + ""); + StringBuilder message = new StringBuilder(1024); + message.append("

Version: ") + .append(Cache.getProperty("VERSION")).append("

") + .append("Built: ") + .append(Cache.getDefault("BUILD_DATE", "unknown")) + .append(" from ").append(Cache.getBuildDetailsForSplash()) + .append(""); - } - else + String latestVersion = Cache.getDefault("LATEST_VERSION", "Checking"); + if (latestVersion.equals("Checking")) { - - message.append("Version " - + jalview.bin.Cache.getProperty("VERSION") - + "; last updated: " - + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")); + // JBP removed this message for 2.11: May be reinstated in future version + // message.append("
...Checking latest version...
"); } - - if (jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking") - .equals("Checking")) - { - message.append("
...Checking latest version...
"); - } - else if (!jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking") - .equals(jalview.bin.Cache.getProperty("VERSION"))) + else if (!latestVersion.equals(Cache.getProperty("VERSION"))) { boolean red = false; - if (jalview.bin.Cache.getProperty("VERSION").toLowerCase() + if (Cache.getProperty("VERSION").toLowerCase() .indexOf("automated build") == -1) { red = true; @@ -1461,29 +1421,22 @@ public class Desktop extends GDesktop message.append("
"); } - message.append("
!! Version " - + jalview.bin.Cache.getDefault("LATEST_VERSION", - "..Checking..") - + " is available for download from " - + jalview.bin.Cache.getDefault("www.jalview.org", - "http://www.jalview.org") - + " !!"); + message.append("
!! Version ") + .append(Cache.getDefault("LATEST_VERSION", "..Checking..")) + .append(" is available for download from ") + .append(Cache.getDefault("www.jalview.org", + "http://www.jalview.org")) + .append(" !!"); if (red) { message.append("
"); } } - message.append("
Authors: " + jalview.bin.Cache.getDefault( - "AUTHORFNAMES", - "The Jalview Authors (See AUTHORS file for current list)") - + "

Development managed by The Barton Group, University of Dundee, Scotland, UK.
" - + "

For help, see the FAQ at www.jalview.org/faq and/or join the jalview-discuss@jalview.org mailing list" - + "

If you use Jalview, please cite:" - + "
Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)" - + "
Jalview Version 2 - a multiple sequence alignment editor and analysis workbench" - + "
Bioinformatics doi: 10.1093/bioinformatics/btp033" - + ""); - return message; + message.append("
Authors: "); + message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS)); + message.append(CITATION); + + return message.toString(); } /** @@ -1533,10 +1486,6 @@ public class Desktop extends GDesktop } Jalview.setCurrentAlignFrame(null); System.out.println("ALL CLOSED"); - if (v_client != null) - { - // TODO clear binding to vamsas document objects on close_all - } /* * reset state of singleton objects as appropriate (clear down session state @@ -1572,9 +1521,9 @@ public class Desktop extends GDesktop protected void garbageCollect_actionPerformed(ActionEvent e) { // We simply collect the garbage - jalview.bin.Cache.log.debug("Collecting garbage..."); + Cache.log.debug("Collecting garbage..."); System.gc(); - jalview.bin.Cache.log.debug("Finished garbage collection."); + Cache.log.debug("Finished garbage collection."); } /* @@ -1764,13 +1713,13 @@ public class Desktop extends GDesktop setProgressBar(MessageManager.formatMessage( "label.saving_jalview_project", new Object[] { chosenFile.getName() }), chosenFile.hashCode()); - jalview.bin.Cache.setProperty("LAST_DIRECTORY", + Cache.setProperty("LAST_DIRECTORY", chosenFile.getParent()); // TODO catch and handle errors for savestate // TODO prevent user from messing with the Desktop whilst we're saving try { - boolean doBackup = BackupFiles.getEnabled(); + boolean doBackup = BackupFiles.getEnabled(); BackupFiles backupfiles = doBackup ? new BackupFiles(chosenFile) : null; new Jalview2XML().saveState(doBackup ? backupfiles.getTempFile() : chosenFile); @@ -1830,7 +1779,7 @@ public class Desktop extends GDesktop "Jalview Project (old)" }; JalviewFileChooser chooser = new JalviewFileChooser( Cache.getProperty("LAST_DIRECTORY"), suffix, desc, - "Jalview Project", true, true); // last two booleans: allFiles, + "Jalview Project", true, BackupFiles.getEnabled()); // last two booleans: allFiles, // allowBackupFiles chooser.setFileView(new JalviewFileView()); chooser.setDialogTitle(MessageManager.getString("label.restore_state")); @@ -1848,24 +1797,26 @@ public class Desktop extends GDesktop @Override public void run() { - try + try { + // BH was String "choice" here but needs to be File object new Jalview2XML().loadJalviewAlign(selectedFile); } catch (OutOfMemoryError oom) - { - new OOMWarning("Whilst loading project from " + choice, oom); - } catch (Exception ex) - { - Cache.log.error( - "Problems whilst loading project from " + choice, ex); + { + new OOMWarning("Whilst loading project from " + choice, oom); + } catch (Exception ex) + { + Cache.log.error( + "Problems whilst loading project from " + choice, ex); JvOptionPane.showMessageDialog(Desktop.getDesktopPane(), - MessageManager.formatMessage( - "label.error_whilst_loading_project_from", - new Object[] - { choice }), - MessageManager.getString("label.couldnt_load_project"), - JvOptionPane.WARNING_MESSAGE); - } + MessageManager.formatMessage( + "label.error_whilst_loading_project_from", + new Object[] + { choice }), + MessageManager + .getString("label.couldnt_load_project"), + JvOptionPane.WARNING_MESSAGE); + } } }).start(); } @@ -2057,11 +2008,29 @@ public class Desktop extends GDesktop return; } + // BH! not in applet branch + // FIXME: ideally should use UI interface API + FeatureSettings viewFeatureSettings = (af.featureSettings != null + && af.featureSettings.isOpen()) + ? af.featureSettings + : null; + Rectangle fsBounds = af.getFeatureSettingsGeometry(); for (int i = 0; i < size; i++) { AlignmentPanel ap = af.alignPanels.get(i); AlignFrame newaf = new AlignFrame(ap); + // BH! not in applet branch + // transfer reference for existing feature settings to new alignFrame + if (ap == af.alignPanel) + { + if (viewFeatureSettings != null && viewFeatureSettings.fr.ap == ap) + { + newaf.featureSettings = viewFeatureSettings; + } + newaf.setFeatureSettingsGeometry(fsBounds); + } + /* * Restore the view's last exploded frame geometry if known. Multiple * views from one exploded frame share and restore the same (frame) @@ -2077,8 +2046,19 @@ public class Desktop extends GDesktop addInternalFrame(newaf, af.getTitle(), AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); + // BH! not in applet branch + // and materialise a new feature settings dialog instance for the new alignframe + // (closes the old as if 'OK' was pressed) + if (ap == af.alignPanel && newaf.featureSettings != null + && newaf.featureSettings.isOpen() + && af.alignPanel.getAlignViewport().isShowSequenceFeatures()) + { + newaf.showFeatureSettingsUI(); + } } + // BH! not in applet branch + af.featureSettings = null; af.alignPanels.clear(); af.closeMenuItem_actionPerformed(true); @@ -2119,374 +2099,32 @@ public class Desktop extends GDesktop if (gatherThis) { - af.alignPanels.clear(); - af.closeMenuItem_actionPerformed(true); - } - } - } - - } - - jalview.gui.VamsasApplication v_client = null; - - @Override - public void vamsasImport_actionPerformed(ActionEvent e) - { - // TODO: JAL-3048 not needed for Jalview-JS - - if (v_client == null) - { - // Load and try to start a session. - JalviewFileChooser chooser = new JalviewFileChooser( - jalview.bin.Cache.getProperty("LAST_DIRECTORY")); - - chooser.setFileView(new JalviewFileView()); - chooser.setDialogTitle( - MessageManager.getString("label.open_saved_vamsas_session")); - chooser.setToolTipText(MessageManager.getString( - "label.select_vamsas_session_opened_as_new_vamsas_session")); - - int value = chooser.showOpenDialog(this); - - if (value == JalviewFileChooser.APPROVE_OPTION) - { - String fle = chooser.getSelectedFile().toString(); - if (!vamsasImport(chooser.getSelectedFile())) - { - JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(), - MessageManager.formatMessage( - "label.couldnt_import_as_vamsas_session", - new Object[] - { fle }), - MessageManager - .getString("label.vamsas_document_import_failed"), - JvOptionPane.ERROR_MESSAGE); - } - } - } - else - { - jalview.bin.Cache.log.error( - "Implementation error - load session from a running session is not supported."); - } - } - - /** - * import file into a new vamsas session (uses jalview.gui.VamsasApplication) - * - * @param file - * @return true if import was a success and a session was started. - */ - public boolean vamsasImport(URL url) - { - // TODO: create progress bar - if (v_client != null) - { - - jalview.bin.Cache.log.error( - "Implementation error - load session from a running session is not supported."); - return false; - } - - try - { - // copy the URL content to a temporary local file - // TODO: be a bit cleverer here with nio (?!) - File file = File.createTempFile("vdocfromurl", ".vdj"); - FileOutputStream fos = new FileOutputStream(file); - BufferedInputStream bis = new BufferedInputStream(url.openStream()); - byte[] buffer = new byte[2048]; - int ln; - while ((ln = bis.read(buffer)) > -1) - { - fos.write(buffer, 0, ln); - } - bis.close(); - fos.close(); - v_client = new jalview.gui.VamsasApplication(this, file, - url.toExternalForm()); - } catch (Exception ex) - { - jalview.bin.Cache.log.error( - "Failed to create new vamsas session from contents of URL " - + url, - ex); - return false; - } - setupVamsasConnectedGui(); - v_client.initial_update(); // TODO: thread ? - return v_client.inSession(); - } - - /** - * import file into a new vamsas session (uses jalview.gui.VamsasApplication) - * - * @param file - * @return true if import was a success and a session was started. - */ - public boolean vamsasImport(File file) - { - if (v_client != null) - { - - jalview.bin.Cache.log.error( - "Implementation error - load session from a running session is not supported."); - return false; - } - - setProgressBar(MessageManager.formatMessage( - "status.importing_vamsas_session_from", new Object[] - { file.getName() }), file.hashCode()); - try - { - v_client = new jalview.gui.VamsasApplication(this, file, null); - } catch (Exception ex) - { - setProgressBar(MessageManager.formatMessage( - "status.importing_vamsas_session_from", new Object[] - { file.getName() }), file.hashCode()); - jalview.bin.Cache.log.error( - "New vamsas session from existing session file failed:", ex); - return false; - } - setupVamsasConnectedGui(); - v_client.initial_update(); // TODO: thread ? - setProgressBar(MessageManager.formatMessage( - "status.importing_vamsas_session_from", new Object[] - { file.getName() }), file.hashCode()); - return v_client.inSession(); - } - - public boolean joinVamsasSession(String mysesid) - { - if (v_client != null) - { - throw new Error(MessageManager - .getString("error.try_join_vamsas_session_another")); - } - if (mysesid == null) - { - throw new Error( - MessageManager.getString("error.invalid_vamsas_session_id")); - } - v_client = new VamsasApplication(this, mysesid); - setupVamsasConnectedGui(); - v_client.initial_update(); - return (v_client.inSession()); - } - - @Override - public void vamsasStart_actionPerformed(ActionEvent e) - { - if (v_client == null) - { - // Start a session. - // we just start a default session for moment. - /* - * JalviewFileChooser chooser = new JalviewFileChooser(jalview.bin.Cache. - * getProperty("LAST_DIRECTORY")); - * - * chooser.setFileView(new JalviewFileView()); - * chooser.setDialogTitle("Load Vamsas file"); - * chooser.setToolTipText("Import"); - * - * int value = chooser.showOpenDialog(this); - * - * if (value == JalviewFileChooser.APPROVE_OPTION) { v_client = new - * jalview.gui.VamsasApplication(this, chooser.getSelectedFile()); - */ - v_client = new VamsasApplication(this); - setupVamsasConnectedGui(); - v_client.initial_update(); // TODO: thread ? - } - else - { - // store current data in session. - v_client.push_update(); // TODO: thread - } - } - - protected void setupVamsasConnectedGui() - { - vamsasStart.setText(MessageManager.getString("label.session_update")); - vamsasSave.setVisible(true); - vamsasStop.setVisible(true); - vamsasImport.setVisible(false); // Document import to existing session is - // not possible for vamsas-client-1.0. - } - - protected void setupVamsasDisconnectedGui() - { - vamsasSave.setVisible(false); - vamsasStop.setVisible(false); - vamsasImport.setVisible(true); - vamsasStart - .setText(MessageManager.getString("label.new_vamsas_session")); - } - - @Override - public void vamsasStop_actionPerformed(ActionEvent e) - { - if (v_client != null) - { - v_client.end_session(); - v_client = null; - setupVamsasDisconnectedGui(); - } - } - - protected void buildVamsasStMenu() - { - if (v_client == null) - { - String[] sess = null; - try - { - sess = VamsasApplication.getSessionList(); - } catch (Exception e) - { - jalview.bin.Cache.log.warn("Problem getting current sessions list.", - e); - sess = null; - } - if (sess != null) - { - jalview.bin.Cache.log.debug( - "Got current sessions list: " + sess.length + " entries."); - VamsasStMenu.removeAll(); - for (int i = 0; i < sess.length; i++) - { - JMenuItem sessit = new JMenuItem(); - sessit.setText(sess[i]); - sessit.setToolTipText(MessageManager - .formatMessage("label.connect_to_session", new Object[] - { sess[i] })); - final Desktop dsktp = this; - final String mysesid = sess[i]; - sessit.addActionListener(new ActionListener() - { - - @Override - public void actionPerformed(ActionEvent e) + if (af.featureSettings != null && af.featureSettings.isOpen()) { - if (dsktp.v_client == null) + if (source.featureSettings == null) { - Thread rthr = new Thread(new Runnable() - { - - @Override - public void run() - { - dsktp.v_client = new VamsasApplication(dsktp, mysesid); - dsktp.setupVamsasConnectedGui(); - dsktp.v_client.initial_update(); - } - - }); - rthr.start(); + // preserve the feature settings geometry for this frame + source.featureSettings = af.featureSettings; + source.setFeatureSettingsGeometry( + af.getFeatureSettingsGeometry()); + } + else + { + // close it and forget + af.featureSettings.close(); } } - }); - VamsasStMenu.add(sessit); - } - // don't show an empty menu. - VamsasStMenu.setVisible(sess.length > 0); - - } - else - { - jalview.bin.Cache.log.debug("No current vamsas sessions."); - VamsasStMenu.removeAll(); - VamsasStMenu.setVisible(false); - } - } - else - { - // Not interested in the content. Just hide ourselves. - VamsasStMenu.setVisible(false); - } - } - - @Override - public void vamsasSave_actionPerformed(ActionEvent e) - { - // TODO: JAL-3048 not needed for Jalview-JS - - if (v_client != null) - { - // TODO: VAMSAS DOCUMENT EXTENSION is VDJ - JalviewFileChooser chooser = new JalviewFileChooser("vdj", - "Vamsas Document"); - - chooser.setFileView(new JalviewFileView()); - chooser.setDialogTitle(MessageManager - .getString("label.save_vamsas_document_archive")); - - int value = chooser.showSaveDialog(this); - - if (value == JalviewFileChooser.APPROVE_OPTION) - { - java.io.File choice = chooser.getSelectedFile(); - JPanel progpanel = addProgressPanel(MessageManager - .formatMessage("label.saving_vamsas_doc", new Object[] - { choice.getName() })); - Cache.setProperty("LAST_DIRECTORY", choice.getParent()); - String warnmsg = null; - String warnttl = null; - try - { - v_client.vclient.storeDocument(choice); - } catch (Error ex) - { - warnttl = "Serious Problem saving Vamsas Document"; - warnmsg = ex.toString(); - jalview.bin.Cache.log - .error("Error Whilst saving document to " + choice, ex); - - } catch (Exception ex) - { - warnttl = "Problem saving Vamsas Document."; - warnmsg = ex.toString(); - jalview.bin.Cache.log.warn( - "Exception Whilst saving document to " + choice, ex); - - } - removeProgressPanel(progpanel); - if (warnmsg != null) - { - JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(), - - warnmsg, warnttl, JvOptionPane.ERROR_MESSAGE); + af.alignPanels.clear(); + af.closeMenuItem_actionPerformed(true); + } } } - } - } - - JPanel vamUpdate = null; - - /** - * hide vamsas user gui bits when a vamsas document event is being handled. - * - * @param b - * true to hide gui, false to reveal gui - */ - public void setVamsasUpdate(boolean b) - { - Cache.log.debug("Setting gui for Vamsas update " - + (b ? "in progress" : "finished")); - - if (vamUpdate != null) + // refresh the feature setting UI for the source frame if it exists + if (source.featureSettings != null + && source.featureSettings.isOpen()) { - this.removeProgressPanel(vamUpdate); + source.showFeatureSettingsUI(); } - if (b) - { - vamUpdate = this.addProgressPanel( - MessageManager.getString("label.updating_vamsas_session")); - } - vamsasStart.setVisible(!b); - vamsasStop.setVisible(!b); - vamsasSave.setVisible(!b); } public JInternalFrame[] getAllFrames() @@ -2692,7 +2330,7 @@ public class Desktop extends GDesktop { if (Jalview.isHeadlessMode()) { - // Desktop.getDesktop() is null in headless mode + // Desktop.getDesktopPane() is null in headless mode return new AlignFrame[] { Jalview.getCurrentAlignFrame() }; } @@ -2776,7 +2414,7 @@ public class Desktop extends GDesktop openGroovyConsole(); } catch (Exception ex) { - jalview.bin.Cache.log.error("Groovy Shell Creation failed.", ex); + Cache.log.error("Groovy Shell Creation failed.", ex); JvOptionPane.showInternalMessageDialog(desktopPane, MessageManager.getString("label.couldnt_create_groovy_shell"), @@ -2837,7 +2475,7 @@ public class Desktop extends GDesktop { getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), + ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()), "Quit"); getRootPane().getActionMap().put("Quit", new AbstractAction() { @@ -2888,7 +2526,7 @@ public class Desktop extends GDesktop @Override public void setProgressBar(String message, long id) { - Platform.timeCheck("Desktop " + message, Platform.TIME_MARK); + // Platform.timeCheck("Desktop " + message, Platform.TIME_MARK); if (progressBars == null) { @@ -2896,18 +2534,18 @@ public class Desktop extends GDesktop progressBarHandlers = new Hashtable<>(); } - if (progressBars.get(new Long(id)) != null) + if (progressBars.get(Long.valueOf(id)) != null) { - JPanel panel = progressBars.remove(new Long(id)); - if (progressBarHandlers.contains(new Long(id))) + JPanel panel = progressBars.remove(Long.valueOf(id)); + if (progressBarHandlers.contains(Long.valueOf(id))) { - progressBarHandlers.remove(new Long(id)); + progressBarHandlers.remove(Long.valueOf(id)); } removeProgressPanel(panel); } else { - progressBars.put(new Long(id), addProgressPanel(message)); + progressBars.put(Long.valueOf(id), addProgressPanel(message)); } } @@ -2922,13 +2560,13 @@ public class Desktop extends GDesktop final IProgressIndicatorHandler handler) { if (progressBarHandlers == null - || !progressBars.containsKey(new Long(id))) + || !progressBars.containsKey(Long.valueOf(id))) { throw new Error(MessageManager.getString( "error.call_setprogressbar_before_registering_handler")); } - progressBarHandlers.put(new Long(id), handler); - final JPanel progressPanel = progressBars.get(new Long(id)); + progressBarHandlers.put(Long.valueOf(id), handler); + final JPanel progressPanel = progressBars.get(Long.valueOf(id)); if (handler.canCancel()) { JButton cancel = new JButton( @@ -2989,12 +2627,6 @@ public class Desktop extends GDesktop return null; } - public VamsasApplication getVamsasApplication() - { - return v_client; - - } - /** * flag set if jalview GUI is being operated programmatically */ @@ -3035,8 +2667,8 @@ public class Desktop extends GDesktop // todo: changesupport handlers need to be transferred if (discoverer == null) { - discoverer = Discoverer.getInstance(); - // register PCS handler for getDesktop(). + discoverer = jalview.ws.jws1.Discoverer.getInstance(); + // register PCS handler for getDesktopPane(). discoverer.addPropertyChangeListener(changeSupport); } // JAL-940 - disabled JWS1 service configuration - always start discoverer @@ -3175,13 +2807,13 @@ public class Desktop extends GDesktop { try { - if (progress != null) + if (progress != null && !Platform.isJS()) { progress.setProgressBar(MessageManager .formatMessage("status.opening_params", new Object[] { url }), this.hashCode()); } - BrowserLauncher.openURL(url); + Platform.openURL(url); } catch (Exception ex) { JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(), @@ -3192,7 +2824,7 @@ public class Desktop extends GDesktop ex.printStackTrace(); } - if (progress != null) + if (progress != null && !Platform.isJS()) { progress.setProgressBar(null, this.hashCode()); } @@ -3254,13 +2886,14 @@ public class Desktop extends GDesktop /** * flag indicating if dialogExecutor should try to acquire a permit */ - volatile boolean dialogPause = true; + private volatile boolean dialogPause = true; /** * pause the queue */ - java.util.concurrent.Semaphore block = new Semaphore(0); + private java.util.concurrent.Semaphore block = new Semaphore(0); + // BH was static private groovy.ui.Console groovyConsole; public StructureViewer lastTargetedView; @@ -3286,6 +2919,7 @@ public class Desktop extends GDesktop { } } + // BH! Q: do we mean System.headless ? or "nogui/nodisplay" headless? if (Jalview.isHeadlessMode()) { return; @@ -3768,4 +3402,39 @@ public class Desktop extends GDesktop return result; } + + + public MyDesktopPane desktopPane; + + /** + * Get the instance of the JDesktopPane from the application-local Desktop + * (JFrame) instance + * + * The key here is that the Java application can have multiple static + * instances of the desktop JFrame because those instances are sandboxed, but + * the SwingJS JFrames will be in the same VM-like space. So we need + * application singletons, at least for JavaScript. + * + * @return + */ + public static MyDesktopPane getDesktopPane() + { + Desktop desktop = Desktop.getInstance(); + return desktop == null ? null : desktop.desktopPane; + } + + /** + * Answers an 'application scope' singleton instance of this class. Separate + * SwingJS 'applets' running in the same browser page will each have a + * distinct instance of Desktop. + * + * @return + */ + public static Desktop getInstance() + { + return Jalview.isHeadlessMode() ? null + : (Desktop) ApplicationSingletonProvider + .getInstance(Desktop.class); + } + } diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index ddadd6d..9ffcaee 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -20,31 +20,11 @@ */ package jalview.gui; -import jalview.api.FeatureColourI; -import jalview.api.FeatureSettingsControllerI; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.SequenceI; -import jalview.datamodel.features.FeatureMatcher; -import jalview.datamodel.features.FeatureMatcherI; -import jalview.datamodel.features.FeatureMatcherSet; -import jalview.datamodel.features.FeatureMatcherSetI; -import jalview.gui.Help.HelpId; -import jalview.gui.JalviewColourChooser.ColourChooserListener; -import jalview.io.JalviewFileChooser; -import jalview.io.JalviewFileView; -import jalview.schemes.FeatureColour; -import jalview.util.MessageManager; -import jalview.util.Platform; -import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; -import jalview.xml.binding.jalview.JalviewUserColours; -import jalview.xml.binding.jalview.JalviewUserColours.Colour; -import jalview.xml.binding.jalview.JalviewUserColours.Filter; -import jalview.xml.binding.jalview.ObjectFactory; - import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.GridLayout; @@ -108,8 +88,35 @@ import javax.xml.bind.Marshaller; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamReader; +import jalview.api.AlignViewControllerGuiI; +import jalview.api.AlignViewportI; +import jalview.api.FeatureColourI; +import jalview.api.FeatureSettingsControllerI; +import jalview.api.SplitContainerI; +import jalview.api.ViewStyleI; +import jalview.controller.FeatureSettingsControllerGuiI; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcher; +import jalview.datamodel.features.FeatureMatcherI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.gui.Help.HelpId; +import jalview.gui.JalviewColourChooser.ColourChooserListener; +import jalview.io.JalviewFileChooser; +import jalview.io.JalviewFileView; +import jalview.schemes.FeatureColour; +import jalview.util.MessageManager; +import jalview.util.Platform; +import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; +import jalview.viewmodel.styles.ViewStyle; +import jalview.xml.binding.jalview.JalviewUserColours; +import jalview.xml.binding.jalview.JalviewUserColours.Colour; +import jalview.xml.binding.jalview.JalviewUserColours.Filter; +import jalview.xml.binding.jalview.ObjectFactory; + public class FeatureSettings extends JPanel - implements FeatureSettingsControllerI + implements FeatureSettingsControllerI, FeatureSettingsControllerGuiI { private static final String SEQUENCE_FEATURE_COLOURS = MessageManager .getString("label.sequence_feature_colours"); @@ -145,7 +152,9 @@ public class FeatureSettings extends JPanel float originalTransparency; - Map originalFilters; + private ViewStyleI originalViewStyle; + + private Map originalFilters; final JInternalFrame frame; @@ -157,6 +166,10 @@ public class FeatureSettings extends JPanel JSlider transparency = new JSlider(); + private JCheckBox showComplementOnTop; + + private JCheckBox showComplement; + /* * when true, constructor is still executing - so ignore UI events */ @@ -172,10 +185,36 @@ public class FeatureSettings extends JPanel boolean handlingUpdate = false; /* + * a change listener to ensure the dialog is updated if + * FeatureRenderer discovers new features + */ + private PropertyChangeListener change; + + /* * holds {featureCount, totalExtent} for each feature type */ Map typeWidth = null; + private void storeOriginalSettings() + { + // save transparency for restore on Cancel + originalTransparency = fr.getTransparency(); + + updateTransparencySliderFromFR(); + + originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy + originalViewStyle = new ViewStyle(af.viewport.getViewStyle()); + } + + private void updateTransparencySliderFromFR() + { + boolean incon = inConstruction; + inConstruction = true; + + int transparencyAsPercent = (int) (fr.getTransparency() * 100); + transparency.setValue(100 - transparencyAsPercent); + inConstruction = incon; + } /** * Constructor * @@ -186,12 +225,7 @@ public class FeatureSettings extends JPanel this.af = alignFrame; fr = af.getFeatureRenderer(); - // save transparency for restore on Cancel - originalTransparency = fr.getTransparency(); - int originalTransparencyAsPercent = (int) (originalTransparency * 100); - transparency.setMaximum(100 - originalTransparencyAsPercent); - - originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy + storeOriginalSettings(); try { @@ -277,20 +311,23 @@ public class FeatureSettings extends JPanel @Override public void mousePressed(MouseEvent evt) { - selectedRow = table.rowAtPoint(evt.getPoint()); + Point pt = evt.getPoint(); + selectedRow = table.rowAtPoint(pt); String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN); if (evt.isPopupTrigger()) { Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN); showPopupMenu(selectedRow, type, colour, evt.getPoint()); } - else if (evt.getClickCount() == 2) + else if (evt.getClickCount() == 2 + && table.columnAtPoint(pt) == TYPE_COLUMN) { boolean invertSelection = evt.isAltDown(); boolean toggleSelection = Platform.isControlDown(evt); boolean extendSelection = evt.isShiftDown(); fr.ap.alignFrame.avc.markColumnsContainingFeatures( invertSelection, extendSelection, toggleSelection, type); + fr.ap.av.sendSelection(); } } @@ -346,7 +383,6 @@ public class FeatureSettings extends JPanel } discoverAllFeatureData(); - final PropertyChangeListener change; final FeatureSettings fs = this; fr.addPropertyChangeListener(change = new PropertyChangeListener() { @@ -364,11 +400,47 @@ public class FeatureSettings extends JPanel }); + SplitContainerI splitframe = af.getSplitViewContainer(); + if (splitframe != null) + { + frame = null; // keeps eclipse happy + splitframe.addFeatureSettingsUI(this); + } + else + { frame = new JInternalFrame(); frame.setContentPane(this); - Desktop.addInternalFrame(frame, - MessageManager.getString("label.sequence_feature_settings"), - 600, Platform.isAMacAndNotJS() ? 480 : 450); + Rectangle bounds = af.getFeatureSettingsGeometry(); + String title; + if (af.getAlignPanels().size() > 1 || Desktop.getAlignmentPanels( + af.alignPanel.av.getSequenceSetId()).length > 1) + { + title = MessageManager.formatMessage( + "label.sequence_feature_settings_for_view", + af.alignPanel.getViewName()); + } + else + { + title = MessageManager.getString("label.sequence_feature_settings"); + } + if (bounds == null) + { + if (Platform.isAMacAndNotJS()) + { + Desktop.addInternalFrame(frame, title, 600, 480); + } + else + { + Desktop.addInternalFrame(frame, title, 600, 450); + } + } + else + { + Desktop.addInternalFrame(frame, title, false, bounds.width, + bounds.height); + frame.setBounds(bounds); + frame.setVisible(true); + } frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT)); frame.addInternalFrameListener( @@ -378,24 +450,48 @@ public class FeatureSettings extends JPanel public void internalFrameClosed( javax.swing.event.InternalFrameEvent evt) { - fr.removePropertyChangeListener(change); - } + featureSettings_isClosed(); + }; }); frame.setLayer(JLayeredPane.PALETTE_LAYER); + } inConstruction = false; } /** - * Constructs and shows a popup menu of possible actions on the selected row - * and feature type + * Sets the state of buttons to show complement features from viewport + * settings + */ + private void updateComplementButtons() + { + showComplement.setSelected(af.getViewport().isShowComplementFeatures()); + showComplementOnTop + .setSelected(af.getViewport().isShowComplementFeaturesOnTop()); + } + + @Override + public AlignViewControllerGuiI getAlignframe() + { + return af; + } + + @Override + public void featureSettings_isClosed() + { + fr.removePropertyChangeListener(change); + change = null; + } + + /** + * Constructs and shows a popup menu of possible actions on the selected row and + * feature type * * @param rowSelected * @param type * @param typeCol * @param pt */ - protected void showPopupMenu(final int rowSelected, final String type, - final Object typeCol, final Point pt) + protected void showPopupMenu(final int rowSelected, final String type, final Object typeCol, final Point pt) { JPopupMenu men = new JPopupMenu(MessageManager .formatMessage("label.settings_for_param", new String[] @@ -421,10 +517,9 @@ public class FeatureSettings extends JPanel { if (e.getSource() == variableColourCB) { - men.setVisible(true); // BH 2018 for JavaScript because this is a - // checkbox - men.setVisible(false); // BH 2018 for JavaScript because this is a - // checkbox + // BH 2018 for JavaScript because this is a checkbox + men.setVisible(true); + men.setVisible(false); if (featureColour.isSimpleColour()) { /* @@ -486,8 +581,7 @@ public class FeatureSettings extends JPanel @Override public void actionPerformed(ActionEvent e) { - af.avc.sortAlignmentByFeatureScore(Arrays.asList(new String[] - { type })); + sortByScore(Arrays.asList(new String[] { type })); } }); JMenuItem dens = new JMenuItem( @@ -498,8 +592,7 @@ public class FeatureSettings extends JPanel @Override public void actionPerformed(ActionEvent e) { - af.avc.sortAlignmentByFeatureDensity(Arrays.asList(new String[] - { type })); + sortByDensity(Arrays.asList(new String[] { type })); } }); men.add(dens); @@ -513,6 +606,7 @@ public class FeatureSettings extends JPanel { fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false, false, type); + fr.ap.av.sendSelection(); } }); JMenuItem clearCols = new JMenuItem(MessageManager @@ -524,6 +618,7 @@ public class FeatureSettings extends JPanel { fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false, false, type); + fr.ap.av.sendSelection(); } }); JMenuItem hideCols = new JMenuItem( @@ -534,6 +629,7 @@ public class FeatureSettings extends JPanel public void actionPerformed(ActionEvent arg0) { fr.ap.alignFrame.hideFeatureColumns(type, true); + fr.ap.av.sendSelection(); } }); JMenuItem hideOtherCols = new JMenuItem( @@ -544,6 +640,7 @@ public class FeatureSettings extends JPanel public void actionPerformed(ActionEvent arg0) { fr.ap.alignFrame.hideFeatureColumns(type, false); + fr.ap.av.sendSelection(); } }); men.add(selCols); @@ -553,6 +650,47 @@ public class FeatureSettings extends JPanel men.show(table, pt.x, pt.y); } + /** + * Sort the sequences in the alignment by the number of features for the given + * feature types (or all features if null) + * + * @param featureTypes + */ + protected void sortByDensity(List featureTypes) + { + af.avc.sortAlignmentByFeatureDensity(featureTypes); + } + + /** + * Sort the sequences in the alignment by average score for the given feature + * types (or all features if null) + * + * @param featureTypes + */ + protected void sortByScore(List featureTypes) + { + af.avc.sortAlignmentByFeatureScore(featureTypes); + } + + /** + * Returns true if at least one feature type is visible. Else shows a warning + * dialog and returns false. + * + * @param title + * @return + */ + private boolean canSortBy(String title) + { + if (fr.getDisplayedFeatureTypes().isEmpty()) + { + JvOptionPane.showMessageDialog(this, + MessageManager.getString("label.no_features_to_sort_by"), + title, JvOptionPane.OK_OPTION); + return false; + } + return true; + } + @Override synchronized public void discoverAllFeatureData() { @@ -607,7 +745,7 @@ public class FeatureSettings extends JPanel { fr.setGroupVisibility(check.getText(), check.isSelected()); resetTable(new String[] { grp }); - af.alignPanel.paintAlignment(true, true); + refreshDisplay(); } }); groupPanel.add(check); @@ -705,7 +843,7 @@ public class FeatureSettings extends JPanel data[dataIndex][FILTER_COLUMN] = featureFilter == null ? new FeatureMatcherSet() : featureFilter; - data[dataIndex][SHOW_COLUMN] = new Boolean( + data[dataIndex][SHOW_COLUMN] = Boolean.valueOf( af.getViewport().getFeaturesDisplayed().isVisible(type)); dataIndex++; displayableTypes.remove(type); @@ -732,7 +870,7 @@ public class FeatureSettings extends JPanel data[dataIndex][FILTER_COLUMN] = featureFilter == null ? new FeatureMatcherSet() : featureFilter; - data[dataIndex][SHOW_COLUMN] = new Boolean(true); + data[dataIndex][SHOW_COLUMN] = Boolean.valueOf(true); dataIndex++; displayableTypes.remove(type); } @@ -1137,49 +1275,64 @@ public class FeatureSettings extends JPanel table.repaint(); } + /** + * close ourselves but leave any existing UI handlers (e.g a CDS/Protein tabbed + * feature settings dialog) intact + */ + public void closeOldSettings() + { + closeDialog(false); + } + + /** + * close the feature settings dialog (and any containing frame) + */ public void close() { + closeDialog(true); + } + + private void closeDialog(boolean closeContainingFrame) + { try { + if (frame != null) + { + af.setFeatureSettingsGeometry(frame.getBounds()); frame.setClosed(true); + } + else + { + SplitContainerI sc = af.getSplitViewContainer(); + sc.closeFeatureSettings(this, closeContainingFrame); + af.featureSettings = null; + } } catch (Exception exe) { } } - /** - * Update the priority order of features; only repaint if this changed the - * order of visible features. Any newly discovered feature types are set to - * visible. Returns true if repaint was requested, false if not. - * - * @param data - * @return - */ - public boolean updateFeatureRenderer(Object[][] data) + public void updateFeatureRenderer(Object[][] data) { - return updateFeatureRenderer(data, true); + updateFeatureRenderer(data, true); } /** * Update the priority order of features; only repaint if this changed the - * order of visible features. Returns true if repaint was requested, false if - * not. + * order of visible features * * @param data * @param visibleNew - * @return */ - boolean updateFeatureRenderer(Object[][] data, boolean visibleNew) + void updateFeatureRenderer(Object[][] data, boolean visibleNew) { FeatureSettingsBean[] rowData = getTableAsBeans(data); if (fr.setFeaturePriority(rowData, visibleNew)) { - af.alignPanel.paintAlignment(true, true); - return true; + refreshDisplay(); } - return false; } /** @@ -1204,6 +1357,9 @@ public class FeatureSettings extends JPanel { this.setLayout(new BorderLayout()); + final boolean hasComplement = af.getViewport() + .getCodingComplement() != null; + JPanel settingsPane = new JPanel(); settingsPane.setLayout(new BorderLayout()); @@ -1237,26 +1393,32 @@ public class FeatureSettings extends JPanel } }); - JButton sortByScore = new JButton( - MessageManager.getString("label.seq_sort_by_score")); + final String byScoreLabel = MessageManager.getString("label.seq_sort_by_score"); + JButton sortByScore = new JButton(byScoreLabel); sortByScore.setFont(JvSwingUtils.getLabelFont()); sortByScore.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - af.avc.sortAlignmentByFeatureScore(null); + if (canSortBy(byScoreLabel)) + { + sortByScore(null); + } } }); - JButton sortByDens = new JButton( - MessageManager.getString("label.sequence_sort_by_density")); + final String byDensityLabel = MessageManager.getString("label.sequence_sort_by_density"); + JButton sortByDens = new JButton(byDensityLabel); sortByDens.setFont(JvSwingUtils.getLabelFont()); sortByDens.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - af.avc.sortAlignmentByFeatureDensity(null); + if (canSortBy(byDensityLabel)) + { + sortByDensity(null); + } } }); @@ -1276,27 +1438,49 @@ public class FeatureSettings extends JPanel } } }); - - JButton cancel = new JButton(MessageManager.getString("action.cancel")); + // Cancel for a SplitFrame should just revert changes to the currently displayed + // settings. May want to do this for either or both - so need a splitview + // feature settings cancel/OK. + JButton cancel = new JButton(MessageManager + .getString(hasComplement ? "action.revert" : "action.cancel")); + cancel.setToolTipText(MessageManager.getString(hasComplement + ? "action.undo_changes_to_feature_settings" + : "action.undo_changes_to_feature_settings_and_close_the_dialog")); cancel.setFont(JvSwingUtils.getLabelFont()); + // TODO: disable cancel (and apply!) until current settings are different cancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - cancel(); + revert(); + refreshDisplay(); + if (!hasComplement) + { + close(); + } } }); - - JButton ok = new JButton(MessageManager.getString("action.ok")); + // Cancel for the whole dialog should cancel both CDS and Protein. + // OK for an individual feature settings just applies changes, but dialog + // remains open + JButton ok = new JButton(MessageManager + .getString(hasComplement ? "action.apply" : "action.ok")); ok.setFont(JvSwingUtils.getLabelFont()); ok.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + if (!hasComplement) + { close(); } + else + { + storeOriginalSettings(); + } + } }); JButton loadColours = new JButton( @@ -1334,7 +1518,7 @@ public class FeatureSettings extends JPanel if (!inConstruction) { fr.setTransparency((100 - transparency.getValue()) / 100f); - af.alignPanel.paintAlignment(true, true); + refreshDisplay(); } } }); @@ -1343,8 +1527,41 @@ public class FeatureSettings extends JPanel transparency.setToolTipText( MessageManager.getString("label.transparency_tip")); - JPanel transPanel = new JPanel(new GridLayout(1, 2)); - bigPanel.add(transPanel, BorderLayout.SOUTH); + boolean nucleotide = af.getViewport().getAlignment().isNucleotide(); + String text = MessageManager.formatMessage("label.show_linked_features", + nucleotide + ? MessageManager.getString("label.protein") + .toLowerCase() + : "CDS"); + showComplement = new JCheckBox(text); + showComplement.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + af.getViewport() + .setShowComplementFeatures(showComplement.isSelected()); + refreshDisplay(); + } + }); + + showComplementOnTop = new JCheckBox( + MessageManager.getString("label.on_top")); + showComplementOnTop.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + af.getViewport().setShowComplementFeaturesOnTop( + showComplementOnTop.isSelected()); + refreshDisplay(); + } + }); + + updateComplementButtons(); + + JPanel lowerPanel = new JPanel(new GridLayout(1, 2)); + bigPanel.add(lowerPanel, BorderLayout.SOUTH); JPanel transbuttons = new JPanel(new GridLayout(5, 1)); transbuttons.add(optimizeOrder); @@ -1352,8 +1569,20 @@ public class FeatureSettings extends JPanel transbuttons.add(sortByScore); transbuttons.add(sortByDens); transbuttons.add(help); - transPanel.add(transparency); - transPanel.add(transbuttons); + + JPanel transPanelLeft = new JPanel( + new GridLayout(hasComplement ? 4 : 2, 1)); + transPanelLeft.add(new JLabel(" Colour transparency" + ":")); + transPanelLeft.add(transparency); + if (hasComplement) + { + JPanel cp = new JPanel(new FlowLayout(FlowLayout.LEFT)); + cp.add(showComplement); + cp.add(showComplementOnTop); + transPanelLeft.add(cp); + } + lowerPanel.add(transPanelLeft); + lowerPanel.add(transbuttons); JPanel buttonPanel = new JPanel(); buttonPanel.add(ok); @@ -1367,24 +1596,19 @@ public class FeatureSettings extends JPanel } /** - * On Cancel, restore settings as they were when the dialog was opened (or - * possibly with any new features added while the dialog was open) + * Repaints alignment, structure and overview (if shown). If there is a + * complementary view which is showing this view's features, then also + * repaints that. */ - void cancel() + void refreshDisplay() { - fr.setTransparency(originalTransparency); - fr.setFeatureFilters(originalFilters); - boolean repainted = updateFeatureRenderer(originalData); - - /* - * ensure alignment (and Overview if visible) are redrawn - */ - if (!repainted) + af.alignPanel.paintAlignment(true, true); + AlignViewportI complement = af.getViewport().getCodingComplement(); + if (complement != null && complement.isShowComplementFeatures()) { - af.alignPanel.paintAlignment(true, true); + AlignFrame af2 = Desktop.getAlignFrameFor(complement); + af2.alignPanel.paintAlignment(true, true); } - - close(); } /** @@ -1529,13 +1753,22 @@ public class FeatureSettings extends JPanel } /** - * Answers the class of the object in column c of the first row of the table + * Answers the class of column c of the table */ @Override public Class getColumnClass(int c) { - Object v = getValueAt(0, c); - return v == null ? null : v.getClass(); + switch (c) + { + case TYPE_COLUMN: + return String.class; + case COLOUR_COLUMN: + return FeatureColour.class; + case FILTER_COLUMN: + return FeatureMatcherSet.class; + default: + return Boolean.class; + } } @Override @@ -1935,6 +2168,27 @@ public class FeatureSettings extends JPanel return button; } } + + public boolean isOpen() + { + if (af.getSplitViewContainer() != null) + { + return af.getSplitViewContainer().isFeatureSettingsOpen(); + } + return frame != null && !frame.isClosed(); + } + + @Override + public void revert() + { + fr.setTransparency(originalTransparency); + fr.setFeatureFilters(originalFilters); + updateFeatureRenderer(originalData); + af.getViewport().setViewStyle(originalViewStyle); + updateTransparencySliderFromFR(); + updateComplementButtons(); + refreshDisplay(); + } } class FeatureIcon implements Icon diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index dadf25a..10641eb 100755 --- a/src/jalview/gui/IdPanel.java +++ b/src/jalview/gui/IdPanel.java @@ -22,7 +22,6 @@ package jalview.gui; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.Sequence; -import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; import jalview.datamodel.SequenceI; import jalview.gui.SeqPanel.MousePos; @@ -436,30 +435,13 @@ public class IdPanel extends JPanel } Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex); - - /* - * build a new links menu based on the current links - * and any non-positional features - */ - List features = null; if (sq != null) { - List nlinks = Preferences.sequenceUrlLinks.getLinksForMenu(); - features = sq.getFeatures().getNonPositionalFeatures(); - for (SequenceFeature sf : features) - { - if (sf.links != null) - { - nlinks.addAll(sf.links); + PopupMenu pop = new PopupMenu(alignPanel, sq, + Preferences.getGroupURLLinks()); + pop.show(this, e.getX(), e.getY()); } } - } - - PopupMenu pop = new PopupMenu(alignPanel, sq, features, - Preferences.getGroupURLLinks() // empty list; not implemented - ); - pop.show(this, e.getX(), e.getY()); - } /** * On right mouse click on a Consensus annotation label, shows a limited popup diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index 3145f7c..2e6e1b8 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -24,15 +24,16 @@ import jalview.analysis.AAFrequency; import jalview.analysis.AlignmentAnnotationUtils; import jalview.analysis.AlignmentUtils; import jalview.analysis.Conservation; +import jalview.api.AlignViewportI; import jalview.bin.Cache; import jalview.commands.ChangeCaseCommand; import jalview.commands.EditCommand; import jalview.commands.EditCommand.Action; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; -import jalview.datamodel.Annotation; import jalview.datamodel.DBRefEntry; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.PDBEntry; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceGroup; @@ -48,12 +49,14 @@ import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemes; import jalview.schemes.PIDColourScheme; import jalview.schemes.ResidueColourScheme; +import jalview.util.Comparison; import jalview.util.GroupUrlLink; import jalview.util.GroupUrlLink.UrlStringTooLongException; import jalview.util.MessageManager; import jalview.util.Platform; import jalview.util.StringUtils; import jalview.util.UrlLink; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.BorderLayout; import java.awt.Color; @@ -68,6 +71,7 @@ import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; @@ -89,6 +93,24 @@ import javax.swing.JScrollPane; */ public class PopupMenu extends JPopupMenu implements ColourChangeListener { + /* + * maximum length of feature description to include in popup menu item text + */ + private static final int FEATURE_DESC_MAX = 40; + + /* + * true for ID Panel menu, false for alignment panel menu + */ + private final boolean forIdPanel; + + private final AlignmentPanel ap; + + /* + * the sequence under the cursor when clicked + * (additional sequences may be selected) + */ + private final SequenceI sequence; + JMenu groupMenu = new JMenu(); JMenuItem groupName = new JMenuItem(); @@ -103,28 +125,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener protected JMenuItem modifyConservation = new JMenuItem(); - AlignmentPanel ap; - JMenu sequenceMenu = new JMenu(); - JMenuItem sequenceName = new JMenuItem(); - - JMenuItem sequenceDetails = new JMenuItem(); - - JMenuItem sequenceSelDetails = new JMenuItem(); - JMenuItem makeReferenceSeq = new JMenuItem(); - JMenuItem chooseAnnotations = new JMenuItem(); - - SequenceI sequence; - JMenuItem createGroupMenuItem = new JMenuItem(); JMenuItem unGroupMenuItem = new JMenuItem(); - JMenuItem outline = new JMenuItem(); - JMenu colourMenu = new JMenu(); JCheckBoxMenuItem showBoxes = new JCheckBoxMenuItem(); @@ -137,18 +145,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener JMenu editMenu = new JMenu(); - JMenuItem cut = new JMenuItem(); - - JMenuItem copy = new JMenuItem(); - JMenuItem upperCase = new JMenuItem(); JMenuItem lowerCase = new JMenuItem(); JMenuItem toggle = new JMenuItem(); - JMenu pdbMenu = new JMenu(); - JMenu outputMenu = new JMenu(); JMenu seqShowAnnotationsMenu = new JMenu(); @@ -165,22 +167,14 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener JMenuItem groupAddReferenceAnnotations = new JMenuItem( MessageManager.getString("label.add_reference_annotations")); - JMenuItem sequenceFeature = new JMenuItem(); - JMenuItem textColour = new JMenuItem(); - JMenu jMenu1 = new JMenu(); + JMenu editGroupMenu = new JMenu(); - JMenuItem pdbStructureDialog = new JMenuItem(); + JMenuItem chooseStructure = new JMenuItem(); JMenu rnaStructureMenu = new JMenu(); - JMenuItem editSequence = new JMenuItem(); - - JMenu groupLinksMenu; - - JMenuItem hideInsertions = new JMenuItem(); - /** * Constructs a menu with sub-menu items for any hyperlinks for the sequence * and/or features provided. Hyperlinks may include a lookup by sequence id, @@ -191,7 +185,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * @param features * @return */ - static JMenu buildLinkMenu(final SequenceI seq, + protected static JMenu buildLinkMenu(final SequenceI seq, List features) { JMenu linkMenu = new JMenu(MessageManager.getString("action.link")); @@ -317,9 +311,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } /** - * For the popup menu on the idPanel. - * - * Add a late bound groupURL item to the given linkMenu + * add a late bound groupURL item to the given linkMenu * * @param linkMenu * @param label @@ -351,15 +343,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { try { - // Object[] { int[] { number of matches seqs }, - // boolean[] { which matched }, - // StringBuffer[] { segment generated from inputs }, - // String[] { url } - // } - - // TODO bug: urlstub is { int[], boolean[] } but constructFrom - // requires something else. - showLink(urlgenerator.constructFrom(urlstub)); } catch (UrlStringTooLongException e2) { @@ -374,42 +357,55 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } /** - * Creates a new PopupMenu object. + * Constructor for a PopupMenu for a click in the alignment panel (on a residue) * * @param ap + * the panel in which the mouse is clicked * @param seq - * @param features - * non-positional features (for seq not null), or positional features - * at residue (for seq equal to null) + * the sequence under the mouse + * @throws NullPointerException + * if seq is null */ - public PopupMenu(final AlignmentPanel ap, SequenceI seq, - List features) + public PopupMenu(final AlignmentPanel ap, SequenceI seq, int column) { - this(ap, seq, features, null); + this(false, ap, seq, column, null); } /** - * Constructor + * Constructor for a PopupMenu for a click in the sequence id panel * * @param alignPanel + * the panel in which the mouse is clicked * @param seq - * the sequence under the cursor if in the Id panel, null if in the - * sequence panel - * @param features - * non-positional features if in the Id panel, features at the - * clicked residue if in the sequence panel + * the sequence under the mouse click * @param groupLinks - * not implemented -- empty list + * templates for sequence external links + * @throws NullPointerException + * if seq is null */ public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq, - List features, List groupLinks) + List groupLinks) { - // ///////////////////////////////////////////////////////// - // If this is activated from the sequence panel, the user may want to - // edit or annotate a particular residue. Therefore display the residue menu - // - // If from the IDPanel, we must display the sequence menu - // //////////////////////////////////////////////////////// + this(true, alignPanel, seq, -1, groupLinks); + } + + /** + * Private constructor that constructs a popup menu for either sequence ID + * Panel, or alignment context + * + * @param fromIdPanel + * @param alignPanel + * @param seq + * @param column + * aligned column position (0...) + * @param groupLinks + */ + private PopupMenu(boolean fromIdPanel, + final AlignmentPanel alignPanel, + final SequenceI seq, final int column, List groupLinks) + { + Objects.requireNonNull(seq); + this.forIdPanel = fromIdPanel; this.ap = alignPanel; sequence = seq; @@ -434,9 +430,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * 'reference annotations' that may be added to the alignment. First for the * currently selected sequence (if there is one): */ - final List selectedSequence = (seq == null - ? Collections. emptyList() - : Arrays.asList(seq)); + final List selectedSequence = (forIdPanel && seq != null + ? Arrays.asList(seq) + : Collections. emptyList()); buildAnnotationTypesMenus(seqShowAnnotationsMenu, seqHideAnnotationsMenu, selectedSequence); configureReferenceAnnotationsMenu(seqAddReferenceAnnotations, @@ -461,9 +457,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener e.printStackTrace(); } - JMenuItem menuItem; - if (seq != null) + if (forIdPanel) { + JMenuItem menuItem; sequenceMenu.setText(sequence.getName()); if (seq == alignPanel.av.getAlignment().getSeqrep()) { @@ -618,7 +614,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } if (addOption) { - menuItem = new JMenuItem( + JMenuItem menuItem = new JMenuItem( MessageManager.getString("action.reveal_all")); menuItem.addActionListener(new ActionListener() { @@ -670,7 +666,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener // add any groupURLs to the groupURL submenu and make it visible if (groupLinks != null && groupLinks.size() > 0) { - // not implemented -- empty list buildGroupURLMenu(sg, groupLinks); } // Add a 'show all structures' for the current selection @@ -711,27 +706,57 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { createGroupMenuItem.setVisible(true); unGroupMenuItem.setVisible(false); - jMenu1.setText(MessageManager.getString("action.edit_new_group")); + editGroupMenu.setText(MessageManager.getString("action.edit_new_group")); } else { createGroupMenuItem.setVisible(false); unGroupMenuItem.setVisible(true); - jMenu1.setText(MessageManager.getString("action.edit_group")); + editGroupMenu.setText(MessageManager.getString("action.edit_group")); } - if (seq == null) + if (!forIdPanel) { sequenceMenu.setVisible(false); - pdbStructureDialog.setVisible(false); + chooseStructure.setVisible(false); rnaStructureMenu.setVisible(false); } + addLinksAndFeatures(seq, column); + } + + /** + * Adds + *
    + *
  • configured sequence database links (ID panel popup menu)
  • + *
  • non-positional feature links (ID panel popup menu)
  • + *
  • positional feature links (alignment panel popup menu)
  • + *
  • feature details links (alignment panel popup menu)
  • + *
+ * If this panel is also showed complementary (CDS/protein) features, then links + * to their feature details are also added. + * + * @param seq + * @param column + */ + void addLinksAndFeatures(final SequenceI seq, final int column) + { + List features = null; + if (forIdPanel) + { + features = sequence.getFeatures().getNonPositionalFeatures(); + } + else + { + features = ap.getFeatureRenderer().findFeaturesAtColumn(sequence, + column + 1); + } + addLinks(seq, features); - if (seq == null) + if (!forIdPanel) { - addFeatureDetails(features); + addFeatureDetails(features, seq, column); } } @@ -739,69 +764,121 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener * Add a link to show feature details for each sequence feature * * @param features + * @param column + * @param seq */ - protected void addFeatureDetails(List features) + protected void addFeatureDetails(List features, + SequenceI seq, int column) { - if (features == null || features.isEmpty()) + /* + * add features in CDS/protein complement at the corresponding + * position if configured to do so + */ + MappedFeatures mf = null; + if (ap.av.isShowComplementFeatures()) { + if (!Comparison.isGap(sequence.getCharAt(column))) + { + AlignViewportI complement = ap.getAlignViewport() + .getCodingComplement(); + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + int seqPos = sequence.findPosition(column); + mf = fr2.findComplementFeaturesAtResidue(sequence, seqPos); + } + } + + if (features.isEmpty() && mf == null) + { + /* + * no features to show at this position + */ return; } JMenu details = new JMenu( MessageManager.getString("label.feature_details")); add(details); + String name = seq.getName(); for (final SequenceFeature sf : features) { - int start = sf.getBegin(); - int end = sf.getEnd(); - String desc = null; - if (start == end) + addFeatureDetailsMenuItem(details, name, sf); + } + + if (mf != null) + { + name = mf.fromSeq == seq ? mf.mapping.getTo().getName() + : mf.fromSeq.getName(); + for (final SequenceFeature sf : mf.features) { - desc = String.format("%s %d", sf.getType(), start); + addFeatureDetailsMenuItem(details, name, sf); } - else + } + } + + /** + * A helper method to add one menu item whose action is to show details for one + * feature. The menu text includes feature description, but this may be + * truncated. + * + * @param details + * @param seqName + * @param sf + */ + void addFeatureDetailsMenuItem(JMenu details, final String seqName, + final SequenceFeature sf) + { + int start = sf.getBegin(); + int end = sf.getEnd(); + StringBuilder desc = new StringBuilder(); + desc.append(sf.getType()).append(" ").append(String.valueOf(start)); + if (start != end) { - desc = String.format("%s %d-%d", sf.getType(), start, end); + desc.append("-").append(String.valueOf(end)); } - String tooltip = desc; String description = sf.getDescription(); if (description != null) { + desc.append(" "); description = StringUtils.stripHtmlTags(description); - if (description.length() > 12) + + /* + * truncate overlong descriptions unless they contain an href + * (as truncation could leave corrupted html) + */ + boolean hasLink = description.indexOf("a href") > -1; + if (description.length() > FEATURE_DESC_MAX && !hasLink) { - desc = desc + " " + description.substring(0, 12) + ".."; + description = description.substring(0, FEATURE_DESC_MAX) + "..."; } - else - { - desc = desc + " " + description; + desc.append(description); } - tooltip = tooltip + " " + description; - } - if (sf.getFeatureGroup() != null) + String featureGroup = sf.getFeatureGroup(); + if (featureGroup != null) { - tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")"); + desc.append(" (").append(featureGroup).append(")"); } - JMenuItem item = new JMenuItem(desc); - item.setToolTipText(tooltip); + String htmlText = JvSwingUtils.wrapTooltip(true, desc.toString()); + JMenuItem item = new JMenuItem(htmlText); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - showFeatureDetails(sf); + showFeatureDetails(seqName, sf); } }); details.add(item); } - } /** * Opens a panel showing a text report of feature dteails * + * @param seqName + * * @param sf */ - protected void showFeatureDetails(SequenceFeature sf) + protected void showFeatureDetails(String seqName, SequenceFeature sf) { JInternalFrame details; if (Platform.isJS()) @@ -813,7 +890,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener // TODO JAL-3026 set style of table correctly for feature details JLabel reprt = new JLabel(MessageManager .formatMessage("label.html_content", new Object[] - { sf.getDetailsReport() })); + { sf.getDetailsReport(seqName) })); reprt.setBackground(Color.WHITE); reprt.setOpaque(true); panel.add(reprt, BorderLayout.CENTER); @@ -831,7 +908,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener // it appears Java's CSS does not support border-collaps :-( cap.addStylesheetRule("table { border-collapse: collapse;}"); cap.addStylesheetRule("table, td, th {border: 1px solid black;}"); - cap.setText(sf.getDetailsReport()); + cap.setText(sf.getDetailsReport(seqName)); details = cap; } Desktop.addInternalFrame(details, @@ -850,12 +927,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener */ void addLinks(final SequenceI seq, List features) { - JMenu linkMenu = buildLinkMenu(seq, features); + JMenu linkMenu = buildLinkMenu(forIdPanel ? seq : null, features); // only add link menu if it has entries if (linkMenu.getItemCount() > 0) { - if (sequence != null) + if (forIdPanel) { sequenceMenu.add(linkMenu); } @@ -996,12 +1073,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener showOrHideMenu.add(item); } - /** - * - * @param sg - * @param groupLinks - * not implemented -- empty list - */ private void buildGroupURLMenu(SequenceGroup sg, List groupLinks) { @@ -1009,7 +1080,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener // menu appears asap // sequence only URLs // ID/regex match URLs - groupLinksMenu = new JMenu( + JMenu groupLinksMenu = new JMenu( MessageManager.getString("action.group_link")); // three types of url that might be created. JMenu[] linkMenus = new JMenu[] { null, @@ -1063,15 +1134,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } } } - if (groupLinks.size() == 0) - { - return; - } // now create group links for all distinct ID/sequence sets. boolean addMenu = false; // indicates if there are any group links to give // to user - - // not implmeented -- empty list for (String link : groupLinks) { GroupUrlLink urlLink = null; @@ -1083,7 +1148,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener Cache.log.error("Exception for GroupURLLink '" + link + "'", foo); continue; } - ; if (!urlLink.isValid()) { Cache.log.error(urlLink.getInvalidMessage()); @@ -1124,7 +1188,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { urlset = urlLink.makeUrlStubs(ids, seqstr, "FromJalview" + System.currentTimeMillis(), false); - // { int[], boolean[] } only here } catch (UrlStringTooLongException e) { } @@ -1176,7 +1239,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); sequenceMenu.setText(MessageManager.getString("label.sequence")); - sequenceName.setText( + + JMenuItem sequenceName = new JMenuItem( MessageManager.getString("label.edit_name_description")); sequenceName.addActionListener(new ActionListener() { @@ -1186,8 +1250,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener sequenceName_actionPerformed(); } }); - chooseAnnotations - .setText(MessageManager.getString("action.choose_annotations")); + JMenuItem chooseAnnotations = new JMenuItem( + MessageManager.getString("action.choose_annotations")); chooseAnnotations.addActionListener(new ActionListener() { @Override @@ -1196,24 +1260,24 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener chooseAnnotations_actionPerformed(e); } }); - sequenceDetails - .setText(MessageManager.getString("label.sequence_details")); + JMenuItem sequenceDetails = new JMenuItem( + MessageManager.getString("label.sequence_details")); sequenceDetails.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - sequenceDetails_actionPerformed(); + createSequenceDetailsReport(new SequenceI[] { sequence }); } }); - sequenceSelDetails - .setText(MessageManager.getString("label.sequence_details")); + JMenuItem sequenceSelDetails = new JMenuItem( + MessageManager.getString("label.sequence_details")); sequenceSelDetails.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - sequenceSelectionDetails_actionPerformed(); + createSequenceDetailsReport(ap.av.getSequenceSelection()); } }); @@ -1238,7 +1302,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); - outline.setText(MessageManager.getString("action.border_colour")); + JMenuItem outline = new JMenuItem( + MessageManager.getString("action.border_colour")); outline.addActionListener(new ActionListener() { @Override @@ -1288,7 +1353,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); editMenu.setText(MessageManager.getString("action.edit")); - cut.setText(MessageManager.getString("action.cut")); + JMenuItem cut = new JMenuItem(MessageManager.getString("action.cut")); cut.addActionListener(new ActionListener() { @Override @@ -1306,7 +1371,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener changeCase(e); } }); - copy.setText(MessageManager.getString("action.copy")); + JMenuItem copy = new JMenuItem(MessageManager.getString("action.copy")); copy.addActionListener(new ActionListener() { @Override @@ -1343,7 +1408,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener .setText(MessageManager.getString("label.show_annotations")); groupHideAnnotationsMenu .setText(MessageManager.getString("label.hide_annotations")); - sequenceFeature.setText( + JMenuItem sequenceFeature = new JMenuItem( MessageManager.getString("label.create_sequence_feature")); sequenceFeature.addActionListener(new ActionListener() { @@ -1353,10 +1418,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener sequenceFeature_actionPerformed(); } }); - jMenu1.setText(MessageManager.getString("label.group")); - pdbStructureDialog.setText( + editGroupMenu.setText(MessageManager.getString("label.group")); + chooseStructure.setText( MessageManager.getString("label.show_pdbstruct_dialog")); - pdbStructureDialog.addActionListener(new ActionListener() + chooseStructure.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) @@ -1374,7 +1439,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener .setText(MessageManager.getString("label.view_rna_structure")); // colStructureMenu.setText("Colour By Structure"); - editSequence.setText( + JMenuItem editSequence = new JMenuItem( MessageManager.getString("label.edit_sequence") + "..."); editSequence.addActionListener(new ActionListener() { @@ -1396,8 +1461,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } }); - hideInsertions - .setText(MessageManager.getString("label.hide_insertions")); + + groupMenu.add(sequenceSelDetails); + add(groupMenu); + add(sequenceMenu); + add(rnaStructureMenu); + add(chooseStructure); + if (forIdPanel) + { + JMenuItem hideInsertions = new JMenuItem( + MessageManager.getString("label.hide_insertions")); hideInsertions.addActionListener(new ActionListener() { @@ -1407,14 +1480,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener hideInsertions_actionPerformed(e); } }); - - groupMenu.add(sequenceSelDetails); - add(groupMenu); - add(sequenceMenu); - add(rnaStructureMenu); - add(pdbStructureDialog); - if (sequence != null) - { add(hideInsertions); } // annotations configuration panel suppressed for now @@ -1435,7 +1500,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener groupMenu.add(sequenceFeature); groupMenu.add(createGroupMenuItem); groupMenu.add(unGroupMenuItem); - groupMenu.add(jMenu1); + groupMenu.add(editGroupMenu); sequenceMenu.add(sequenceName); sequenceMenu.add(sequenceDetails); sequenceMenu.add(makeReferenceSeq); @@ -1449,17 +1514,13 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener editMenu.add(upperCase); editMenu.add(lowerCase); editMenu.add(toggle); - // JBPNote: These shouldn't be added here - should appear in a generic - // 'apply web service to this sequence menu' - // pdbMenu.add(RNAFold); - // pdbMenu.add(ContraFold); - jMenu1.add(groupName); - jMenu1.add(colourMenu); - jMenu1.add(showBoxes); - jMenu1.add(showText); - jMenu1.add(showColourText); - jMenu1.add(outline); - jMenu1.add(displayNonconserved); + editGroupMenu.add(groupName); + editGroupMenu.add(colourMenu); + editGroupMenu.add(showBoxes); + editGroupMenu.add(showText); + editGroupMenu.add(showColourText); + editGroupMenu.add(outline); + editGroupMenu.add(displayNonconserved); } /** @@ -1722,11 +1783,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener createSequenceDetailsReport(ap.av.getSequenceSelection()); } - protected void sequenceDetails_actionPerformed() - { - createSequenceDetailsReport(new SequenceI[] { sequence }); - } - public void createSequenceDetailsReport(SequenceI[] sequences) { StringBuilder contents = new StringBuilder(128); @@ -1936,7 +1992,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } /** - * Shows a dialog where sequence name and description may be edited + * Shows a dialog where the sequence name and description may be edited. If a + * name containing spaces is entered, these are converted to underscores, with a + * warning message. */ void sequenceName_actionPerformed() { @@ -2008,8 +2066,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { getGroup().setOutlineColour(c); refresh(); + } }; - }; JalviewColourChooser.showColourChooser(Desktop.getDesktopPane(), title, Color.BLUE, listener); } @@ -2171,25 +2229,6 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener } } - public void colourByStructure(String pdbid) - { - Annotation[] anots = ap.av.getStructureSelectionManager() - .colourSequenceFromStructure(sequence, pdbid); - - AlignmentAnnotation an = new AlignmentAnnotation("Structure", - "Coloured by " + pdbid, anots); - - ap.av.getAlignment().addAnnotation(an); - an.createSequenceMapping(sequence, 0, true); - // an.adjustForAlignment(); - ap.av.getAlignment().setAnnotationIndex(an, 0); - - ap.adjustAnnotationHeight(); - - sequence.addAlignmentAnnotation(an); - - } - /** * Shows a dialog where sequence characters may be edited. Any changes are * applied, and added as an available 'Undo' item in the edit commands @@ -2199,16 +2238,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener { SequenceGroup sg = ap.av.getSelectionGroup(); + SequenceI seq = sequence; if (sg != null) { - if (sequence == null) + if (seq == null) { - sequence = sg.getSequenceAt(0); + seq = sg.getSequenceAt(0); } EditNameDialog dialog = new EditNameDialog( - sequence.getSequenceAsString(sg.getStartRes(), - sg.getEndRes() + 1), + seq.getSequenceAsString(sg.getStartRes(), sg.getEndRes() + 1), null, MessageManager.getString("label.edit_sequence"), null); dialog.showDialog(ap.alignFrame, MessageManager.getString("label.edit_sequence"), diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index b188888..85d6025 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -29,6 +29,7 @@ import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; import jalview.datamodel.HiddenColumns; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SearchResultMatchI; import jalview.datamodel.SearchResults; import jalview.datamodel.SearchResultsI; @@ -50,6 +51,7 @@ import jalview.util.MessageManager; import jalview.util.Platform; import jalview.viewmodel.AlignmentViewport; import jalview.viewmodel.ViewportRanges; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.BorderLayout; import java.awt.Color; @@ -63,6 +65,7 @@ import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -224,7 +227,7 @@ public class SeqPanel extends JPanel SearchResultsI lastSearchResults; /** - * Constructor + * Creates a new SeqPanel object * * @param viewport * @param alignPanel @@ -841,11 +844,11 @@ public class SeqPanel extends JPanel * the start of the highlighted region. */ @Override - public void highlightSequence(SearchResultsI results) + public String highlightSequence(SearchResultsI results) { if (results == null || results.equals(lastSearchResults)) { - return; + return null; } lastSearchResults = results; @@ -871,6 +874,77 @@ public class SeqPanel extends JPanel { setStatusMessage(results); } + return results.isEmpty() ? null : getHighlightInfo(results); + } + + /** + * temporary hack: answers a message suitable to show on structure hover + * label. This is normally null. It is a peptide variation description if + *
    + *
  • results are a single residue in a protein alignment
  • + *
  • there is a mapping to a coding sequence (codon)
  • + *
  • there are one or more SNP variant features on the codon
  • + *
+ * in which case the answer is of the format (e.g.) "p.Glu388Asp" + * + * @param results + * @return + */ + private String getHighlightInfo(SearchResultsI results) + { + /* + * ideally, just find mapped CDS (as we don't care about render style here); + * for now, go via split frame complement's FeatureRenderer + */ + AlignViewportI complement = ap.getAlignViewport().getCodingComplement(); + if (complement == null) + { + return null; + } + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + + int j = results.getSize(); + List infos = new ArrayList<>(); + for (int i = 0; i < j; i++) + { + SearchResultMatchI match = results.getResults().get(i); + int pos = match.getStart(); + if (pos == match.getEnd()) + { + SequenceI seq = match.getSequence(); + SequenceI ds = seq.getDatasetSequence() == null ? seq + : seq.getDatasetSequence(); + MappedFeatures mf = fr2 + .findComplementFeaturesAtResidue(ds, pos); + if (mf != null) + { + for (SequenceFeature sf : mf.features) + { + String pv = mf.findProteinVariants(sf); + if (pv.length() > 0 && !infos.contains(pv)) + { + infos.add(pv); + } + } + } + } + } + + if (infos.isEmpty()) + { + return null; + } + StringBuilder sb = new StringBuilder(); + for (String info : infos) + { + if (sb.length() > 0) + { + sb.append("|"); + } + sb.append(info); + } + return sb.toString(); } @Override @@ -977,25 +1051,57 @@ public class SeqPanel extends JPanel * add features that straddle the gap (pos may be the residue before or * after the gap) */ + int unshownFeatures = 0; if (av.isShowSequenceFeatures()) { List features = ap.getFeatureRenderer() .findFeaturesAtColumn(sequence, column + 1); - seqARep.appendFeatures(tooltipText, pos, features, - this.ap.getSeqPanel().seqCanvas.fr); + unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos, + features, + this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH); + + /* + * add features in CDS/protein complement at the corresponding + * position if configured to do so + */ + if (av.isShowComplementFeatures()) + { + if (!Comparison.isGap(sequence.getCharAt(column))) + { + AlignViewportI complement = ap.getAlignViewport() + .getCodingComplement(); + AlignFrame af = Desktop.getAlignFrameFor(complement); + FeatureRendererModel fr2 = af.getFeatureRenderer(); + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence, + pos); + if (mf != null) + { + unshownFeatures = seqARep.appendFeaturesLengthLimit( + tooltipText, pos, mf, fr2, + MAX_TOOLTIP_LENGTH); + } + } + } } - if (tooltipText.length() == 6) // + if (tooltipText.length() == 6) // "" { setToolTipText(null); lastTooltip = null; } else { - if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant + if (tooltipText.length() > MAX_TOOLTIP_LENGTH) { tooltipText.setLength(MAX_TOOLTIP_LENGTH); tooltipText.append("..."); } + if (unshownFeatures > 0) + { + tooltipText.append("
").append("... ").append("") + .append(MessageManager.formatMessage( + "label.features_not_shown", unshownFeatures)) + .append(""); + } String textString = tooltipText.toString(); if (lastTooltip == null || !lastTooltip.equals(textString)) { @@ -1949,13 +2055,19 @@ public class SeqPanel extends JPanel return; } - /* - * start scrolling if mouse dragging, whether the drag started - * in the scale panel or this panel - */ - if (mouseDragging || ap.getScalePanel().isMouseDragging()) + // BH check was: +// /* +// * start scrolling if mouse dragging, whether the drag started +// * in the scale panel or this panel +// */ +// if (mouseDragging || ap.getScalePanel().isMouseDragging()) +// { +// startScrolling(new Point(e.getX(), 0)); +// } + + if (mouseDragging && scrollThread == null) { - startScrolling(new Point(e.getX(), 0)); + startScrolling(e.getPoint()); } } @@ -1974,7 +2086,7 @@ public class SeqPanel extends JPanel return; } - if (evt.getClickCount() > 1) + if (evt.getClickCount() > 1 && av.isShowSequenceFeatures()) { sg = av.getSelectionGroup(); if (sg != null && sg.getSize() == 1 @@ -2175,12 +2287,12 @@ public class SeqPanel extends JPanel final int column = pos.column; final int seq = pos.seqIndex; SequenceI sequence = av.getAlignment().getSequenceAt(seq); - List features = ap.getFeatureRenderer() - .findFeaturesAtColumn(sequence, column + 1); - - PopupMenu pop = new PopupMenu(ap, null, features); + if (sequence != null) + { + PopupMenu pop = new PopupMenu(ap, sequence, column); pop.show(this, evt.getX(), evt.getY()); } + } /** * Update the display after mouse up on a selection or group @@ -2428,10 +2540,17 @@ public class SeqPanel extends JPanel // if (!scrollOnce() {t.stop();}) gives compiler error :-( scrollThread.scrollOnce(); } + } + }); + t.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { if (scrollThread == null) { // SeqPanel.stopScrolling called - ((Timer) e.getSource()).stop(); + t.stop(); } } }); diff --git a/src/jalview/gui/SplashScreen.java b/src/jalview/gui/SplashScreen.java index 4fb9136..782c842 100755 --- a/src/jalview/gui/SplashScreen.java +++ b/src/jalview/gui/SplashScreen.java @@ -29,218 +29,220 @@ import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Image; -import java.awt.Insets; import java.awt.MediaTracker; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.Toolkit; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.beans.PropertyVetoException; +import java.net.URL; import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.JTextPane; -import javax.swing.Timer; -import javax.swing.border.EmptyBorder; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; /** - * A class that serves both as an initial 5-second splash screen (interactive - * false) as well as for the Help menu item action (interactive true). - * - * As a splash screen, the frame closes if clicked by the user. - * - * Closure loop converted from a while/sleep loop to a JavaScript-compatible - * state machine by Bob Hanson 2019.11.26. - * - * TODO: get JTextPane working for read-only HTML. + * DOCUMENT ME! * + * @author $author$ + * @version $Revision$ */ -@SuppressWarnings("serial") public class SplashScreen extends JPanel implements Runnable, HyperlinkListener { - private static final int STATE_INIT = 0; - - private static final int STATE_LOOP = 1; + private static final int SHOW_FOR_SECS = 5; - private static final int STATE_DONE = 2; + private static final int FONT_SIZE = 11; - // boolean visible = true; + private boolean visible = true; private JPanel iconimg = new JPanel(new BorderLayout()); - /** - * Temporary SwingJS Hack: Either a JLabel in JavaScript or a JTextPane in - * Java + /* + * as JTextPane in Java, JLabel in javascript */ - protected Component htmlPane; + private Component splashText; private JInternalFrame iframe; private Image image; - private final static int fontSize = 11; - - protected final static Font largeFont = new Font("Verdana", Font.BOLD, - fontSize + 6); + private boolean transientDialog = false; - int yoffset = 30; + private long oldTextLength = -1; - /** - * Creates a new SplashScreen object. - */ - public SplashScreen() - { - this(false); - } - - protected boolean isInteractive = false; - - /** - * - * @param interactive - * if true - an internal dialog is opened rather than a free-floating - * splash screen + /* + * allow click in the initial splash screen to dismiss it + * immediately (not if opened from About menu) */ - public SplashScreen(boolean isInteractive) - { - this.isInteractive = isInteractive; - Thread t = new Thread(this, "SplashScreen"); - t.start(); - } - - MouseAdapter closer = new MouseAdapter() + private MouseAdapter closer = new MouseAdapter() { @Override public void mousePressed(MouseEvent evt) { - try + if (transientDialog) { - if (!isInteractive) + try { - setVisible(false); + visible = false; closeSplash(); + } catch (Exception ex) + { } - } catch (Exception ex) - { } } }; /** + * Constructor that displays the splash screen + * + * @param isTransient + * if true the panel removes itself on click or after a few seconds; + * if false it stays up until closed by the user + */ + public SplashScreen(boolean isTransient) + { + this.transientDialog = isTransient; + + if (Platform.isJS()) // BH 2019 + { + splashText = new JLabel(""); + run(); + } + else + { + /** + * Java only + * + * @j2sIgnore + */ + { + splashText = new JTextPane(); + Thread t = new Thread(this); + t.start(); + } + } + } + + /** * ping the jalview version page then create and display the jalview * splashscreen window. */ - protected void initSplashScreenWindow() + void initSplashScreenWindow() { addMouseListener(closer); + try { - java.net.URL url = getClass().getResource("/images/Jalview_Logo.png"); - image = java.awt.Toolkit.getDefaultToolkit().createImage(url); - MediaTracker mt = new MediaTracker(this); - mt.addImage(image, 0); - Image logo = (Platform.isJS() ? null - : java.awt.Toolkit.getDefaultToolkit().createImage(getClass() - .getResource("/images/Jalview_Logo_small.png"))); - if (logo != null) + URL url = getClass().getResource("/images/Jalview_Logo.png"); + URL urllogo = getClass() + .getResource("/images/Jalview_Logo_small.png"); + + if (!Platform.isJS() && url != null) { + image = Toolkit.getDefaultToolkit().createImage(url); + Image logo = Toolkit.getDefaultToolkit().createImage(urllogo); + MediaTracker mt = new MediaTracker(this); + mt.addImage(image, 0); mt.addImage(logo, 1); - } - do - { - try - { - mt.waitForAll(); - } catch (InterruptedException x) + do { - } - if (mt.isErrorAny()) - { - System.err.println("Error when loading images!"); - } - } while (!mt.checkAll()); - if (url != null) - { + try + { + mt.waitForAll(); + } catch (InterruptedException x) + { + } + if (mt.isErrorAny()) + { + System.err.println("Error when loading images!"); + } + } while (!mt.checkAll()); Desktop.getInstance().setIconImage(logo); } } catch (Exception ex) { } + iframe = new JInternalFrame(); iframe.setFrameIcon(null); - iframe.setClosable(isInteractive); + iframe.setClosable(true); this.setLayout(new BorderLayout()); iframe.setContentPane(this); iframe.setLayer(JLayeredPane.PALETTE_LAYER); - SplashImage splashimg = new SplashImage(image); - iconimg.add(splashimg, BorderLayout.CENTER); - add(iconimg, BorderLayout.NORTH); + if (Platform.isJS()) + { + // ignore in JavaScript + } + else + /** + * Java only + * + * @j2sIgnore + */ + { + ((JTextPane) splashText).setEditable(false); + + SplashImage splashimg = new SplashImage(image); + iconimg.add(splashimg, BorderLayout.CENTER); + add(iconimg, BorderLayout.NORTH); + } + add(splashText, BorderLayout.CENTER); + splashText.addMouseListener(closer); Desktop.getDesktopPane().add(iframe); refreshText(); } - String oldtext; - - private int mainState; - /** * update text in author text panel reflecting current version information */ protected boolean refreshText() { - Desktop desktop = Desktop.getInstance(); - String newtext = desktop.getAboutMessage(true).toString(); + String newtext = Desktop.getInstance().getAboutMessage(); // System.err.println("Text found: \n"+newtext+"\nEnd of newtext."); - if (!newtext.equals(oldtext)) + if (oldTextLength != newtext.length()) { iframe.setVisible(false); - oldtext = newtext; + oldTextLength = newtext.length(); if (Platform.isJS()) // BH 2019 { - // BH TODO SwingJS does not implement HTML style. Could rethink this. - - if (htmlPane == null) - { - htmlPane = new JLabel(); - } - JLabel l = (JLabel)htmlPane; - l.setText(newtext); - Font f = htmlPane.getFont(); - l.setFont(new Font(f.getFamily(), Font.PLAIN, f.getSize())); - l.setBorder(new EmptyBorder(new Insets(5, 5, 5, 5))); - l.setOpaque(true); - l.setBackground(Color.white); - htmlPane = l; + /* + * SwingJS doesn't have HTMLEditorKit, required for a JTextPane + * to display formatted html, so we use a simple alternative + */ + String text = "


" + + newtext + ""; + JLabel ta = new JLabel(text); + ta.setOpaque(true); + ta.setBackground(Color.white); + splashText = ta; } else /** * Java only - * + * * @j2sIgnore */ { - if (htmlPane == null) - { - htmlPane = new JTextPane(); - } - JTextPane pane = (JTextPane)htmlPane; - pane.setEditable(false); - pane.setContentType("text/html"); - pane.setText(newtext); - pane.addHyperlinkListener(this); - htmlPane = pane; + JTextPane jtp = new JTextPane(); + jtp.setEditable(false); + jtp.setContentType("text/html"); + jtp.setText("" + newtext + ""); + jtp.addHyperlinkListener(this); + splashText = jtp; } - htmlPane.addMouseListener(closer); - htmlPane.setSize(new Dimension(750, 375)); - add(htmlPane, BorderLayout.CENTER); - int h = htmlPane.getHeight() + iconimg.getHeight(); - iframe.setBounds(Math.max(0, (desktop.getWidth() - 750) / 2), - Math.max(0, (desktop.getHeight() - h) / 2), 750, h); + splashText.addMouseListener(closer); + + splashText.setVisible(true); + splashText.setSize(new Dimension(750, 375)); + add(splashText, BorderLayout.CENTER); + revalidate(); + iframe.setBounds((Desktop.getInstance().getWidth() - 750) / 2, + (Desktop.getInstance().getHeight() - 375) / 2, 750, + splashText.getHeight() + iconimg.getHeight()); + iframe.validate(); iframe.setVisible(true); return true; } @@ -253,88 +255,57 @@ public class SplashScreen extends JPanel @Override public void run() { - mainState = STATE_INIT; - mainLoop(); - } + initSplashScreenWindow(); - protected long startTime; + long startTime = System.currentTimeMillis() / 1000; - /** - * A simple state machine with just three states: init, loop, and done. Ideal - * for a simple while/sleep loop that works in Java and JavaScript - * identically. - * - */ - protected void mainLoop() - { - while (true) + while (visible) { - switch (mainState) + iframe.repaint(); + try { - case STATE_INIT: - initSplashScreenWindow(); - startTime = System.currentTimeMillis() / 1000; - mainState = STATE_LOOP; - continue; - case STATE_LOOP: - if (!isVisible()) - { - mainState = STATE_DONE; - continue; - } - if (!isInteractive - && ((System.currentTimeMillis() / 1000) - startTime) > 5) - { - setVisible(false); - continue; - } - if (isVisible() && refreshText()) - { - iframe.repaint(); - } - if (isInteractive) - { - return; - } - Timer timer = new Timer(500, new ActionListener() - { - @Override - public void actionPerformed(ActionEvent e) - { - mainLoop(); - } + Thread.sleep(500); + } catch (Exception ex) + { + } - }); - timer.setRepeats(false); - timer.start(); - return; - case STATE_DONE: - closeSplash(); - Desktop.getInstance().startDialogQueue(); + if (transientDialog && ((System.currentTimeMillis() / 1000) + - startTime) > SHOW_FOR_SECS) + { + visible = false; + } + + if (visible && refreshText()) + { + iframe.repaint(); + } + if (!transientDialog) + { return; } } + closeSplash(); + Desktop.getInstance().startDialogQueue(); } /** - * Close the internal frame, either from the timer expiring or from the mouse - * action. + * DOCUMENT ME! */ public void closeSplash() { - setVisible(false); try { + iframe.setClosed(true); - } catch (PropertyVetoException e) + } catch (Exception ex) { } } - private class SplashImage extends JPanel + public class SplashImage extends JPanel { - private Image image; + Image image; public SplashImage(Image todisplay) { @@ -358,7 +329,7 @@ public class SplashScreen extends JPanel g.setColor(Color.white); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(Color.black); - g.setFont(largeFont); + g.setFont(new Font("Verdana", Font.BOLD, FONT_SIZE + 6)); if (image != null) { @@ -366,46 +337,6 @@ public class SplashScreen extends JPanel (getHeight() - image.getHeight(this)) / 2, this); } } - /* - * int y = yoffset; - * - * g.drawString("Jalview " + jalview.bin.Cache.getProperty("VERSION"), 50, - * y); - * - * FontMetrics fm = g.getFontMetrics(); int vwidth = - * fm.stringWidth("Jalview " + jalview.bin.Cache.getProperty("VERSION")); - * g.setFont(new Font("Verdana", Font.BOLD, fontSize + 2)); g.drawString( - * "Last updated: " + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown"), - * 50 + vwidth + 5, y); if (jalview.bin.Cache.getDefault("LATEST_VERSION", - * "Checking").equals( "Checking")) { // Displayed when code version and - * jnlp version do not match g.drawString("...Checking latest version...", - * 50, y += fontSize + 10); y += 5; g.setColor(Color.black); } else if - * (!jalview.bin.Cache.getDefault("LATEST_VERSION", "Checking") - * .equals(jalview.bin.Cache.getProperty("VERSION"))) { if - * (jalview.bin.Cache.getProperty("VERSION").toLowerCase() - * .indexOf("automated build") == -1) { // Displayed when code version and - * jnlp version do not match and code // version is not a development build - * g.setColor(Color.red); } g.drawString( "!! Jalview version " + - * jalview.bin.Cache.getDefault("LATEST_VERSION", "..Checking..") + - * " is available for download from " - * +jalview.bin.Cache.getDefault("www.jalview.org" - * ,"http://www.jalview.org")+" !!", 50, y += fontSize + 10); y += 5; - * g.setColor(Color.black); } - * - * g.setFont(new Font("Verdana", Font.BOLD, fontSize)); g.drawString( - * "Authors: Jim Procter, Andrew Waterhouse, Michele Clamp, James Cuff, Steve Searle," - * , 50, y += fontSize + 4); g.drawString("David Martin & Geoff Barton.", - * 60, y += fontSize + 4); g.drawString( - * "Development managed by The Barton Group, University of Dundee.", 50, y - * += fontSize + 4); g.drawString("If you use Jalview, please cite: ", 50, - * y += fontSize + 4); g.drawString( - * "Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)" - * , 50, y += fontSize + 4); g.drawString( - * "Jalview Version 2 - a multiple sequence alignment editor and analysis workbench" - * , 50, y += fontSize + 4); - * g.drawString("Bioinformatics doi: 10.1093/bioinformatics/btp033", 50, y - * += fontSize + 4); } - */ } @Override @@ -414,5 +345,4 @@ public class SplashScreen extends JPanel Desktop.hyperlinkUpdate(e); } - } diff --git a/src/jalview/gui/SplitFrame.java b/src/jalview/gui/SplitFrame.java index 253b02c..d13be17 100644 --- a/src/jalview/gui/SplitFrame.java +++ b/src/jalview/gui/SplitFrame.java @@ -20,16 +20,9 @@ */ package jalview.gui; -import jalview.api.SplitContainerI; -import jalview.datamodel.AlignmentI; -import jalview.jbgui.GAlignFrame; -import jalview.jbgui.GSplitFrame; -import jalview.structure.StructureSelectionManager; -import jalview.util.Platform; -import jalview.viewmodel.AlignmentViewport; - +import java.awt.BorderLayout; import java.awt.Component; -import java.awt.Toolkit; +import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; @@ -42,12 +35,32 @@ import java.util.Map.Entry; import javax.swing.AbstractAction; import javax.swing.InputMap; +import javax.swing.JButton; import javax.swing.JComponent; +import javax.swing.JDesktopPane; +import javax.swing.JInternalFrame; +import javax.swing.JLayeredPane; import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; import javax.swing.KeyStroke; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; +import jalview.api.AlignViewControllerGuiI; +import jalview.api.FeatureSettingsControllerI; +import jalview.api.SplitContainerI; +import jalview.controller.FeatureSettingsControllerGuiI; +import jalview.datamodel.AlignmentI; +import jalview.jbgui.GAlignFrame; +import jalview.jbgui.GSplitFrame; +import jalview.structure.StructureSelectionManager; +import jalview.util.MessageManager; +import jalview.util.Platform; +import jalview.viewmodel.AlignmentViewport; + /** * An internal frame on the desktop that hosts a horizontally split view of * linked DNA and Protein alignments. Additional views can be created in linked @@ -74,6 +87,13 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI private static final long serialVersionUID = 1L; + /** + * geometry for Feature Settings Holder + */ + private static final int FS_MIN_WIDTH = 400; + + private static final int FS_MIN_HEIGHT = 400; + public SplitFrame(GAlignFrame top, GAlignFrame bottom) { super(top, bottom); @@ -160,26 +180,21 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI */ public void adjustLayout() { + final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport; + final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport; + /* * Ensure sequence ids are the same width so sequences line up */ - int w1 = ((AlignFrame) getTopFrame()).getViewport().getIdWidth(); - int w2 = ((AlignFrame) getBottomFrame()).getViewport().getIdWidth(); + int w1 = topViewport.getIdWidth(); + int w2 = bottomViewport.getIdWidth(); int w3 = Math.max(w1, w2); - if (w1 != w3) - { - ((AlignFrame) getTopFrame()).getViewport().setIdWidth(w3); - } - if (w2 != w3) - { - ((AlignFrame) getBottomFrame()).getViewport().setIdWidth(w3); - } + topViewport.setIdWidth(w3); + bottomViewport.setIdWidth(w3); /* * Scale protein to either 1 or 3 times character width of dna */ - final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport; - final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport; final AlignmentI topAlignment = topViewport.getAlignment(); final AlignmentI bottomAlignment = bottomViewport.getAlignment(); AlignmentViewport cdna = topAlignment.isNucleotide() ? topViewport @@ -412,7 +427,7 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI * Ctrl-W / Cmd-W - close view or window */ KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false); + jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false); action = new AbstractAction() { @Override @@ -433,7 +448,7 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI * Ctrl-T / Cmd-T open new view */ KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false); + jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false); AbstractAction action = new AbstractAction() { @Override @@ -762,6 +777,22 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI { (AlignFrame) getTopFrame(), (AlignFrame) getBottomFrame() }); } + @Override + public AlignFrame getComplementAlignFrame( + AlignViewControllerGuiI alignFrame) + { + if (getTopFrame() == alignFrame) + { + return (AlignFrame) getBottomFrame(); + } + if (getBottomFrame() == alignFrame) + { + return (AlignFrame) getTopFrame(); + } + // we didn't know anything about this frame... + return null; + } + /** * Replace Cmd-F Find action with our version. This is necessary because the * 'default' Finder searches in the first AlignFrame it finds. We need it to @@ -773,7 +804,7 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI * Ctrl-F / Cmd-F open Finder dialog, 'focused' on the right alignment */ KeyStroke key_cmdF = KeyStroke.getKeyStroke(KeyEvent.VK_F, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), false); + jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx(), false); AbstractAction action = new AbstractAction() { @Override @@ -789,4 +820,283 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI }; overrideKeyBinding(key_cmdF, action); } + + /** + * Override to do nothing if triggered from one of the child frames + */ + @Override + public void setSelected(boolean selected) throws PropertyVetoException + { + JDesktopPane desktopPane = getDesktopPane(); + JInternalFrame fr = desktopPane == null ? null + : desktopPane.getSelectedFrame(); + if (fr == getTopFrame() || fr == getBottomFrame()) + { + /* + * patch for JAL-3288 (deselecting top/bottom frame closes popup menu); + * it may be possible to remove this method in future + * if the underlying Java behaviour changes + */ + if (selected) + { + moveToFront(); + } + return; + } + super.setSelected(selected); + } + + /** + * holds the frame for feature settings, so Protein and DNA tabs can be managed + */ + JInternalFrame featureSettingsUI; + + JTabbedPane featureSettingsPanels; + + @Override + public void addFeatureSettingsUI( + FeatureSettingsControllerGuiI featureSettings) + { + boolean showInternalFrame = false; + if (featureSettingsUI == null || featureSettingsPanels == null) + { + showInternalFrame = true; + featureSettingsPanels = new JTabbedPane(); + featureSettingsPanels.addChangeListener(new ChangeListener() + { + + @Override + public void stateChanged(ChangeEvent e) + { + if (e.getSource() != featureSettingsPanels + || featureSettingsUI == null + || featureSettingsUI.isClosed() + || !featureSettingsUI.isVisible()) + { + // not our tabbed pane + return; + } + int tab = featureSettingsPanels.getSelectedIndex(); + if (tab < 0 || featureSettingsPanels + .getSelectedComponent() instanceof FeatureSettingsControllerGuiI) + { + // no tab selected or already showing a feature settings GUI + return; + } + getAlignFrames().get(tab).showFeatureSettingsUI(); } + }); + featureSettingsUI = new JInternalFrame(MessageManager.getString( + "label.sequence_feature_settings_for_CDS_and_Protein")); + featureSettingsPanels.setOpaque(true); + + JPanel dialog = new JPanel(); + dialog.setOpaque(true); + dialog.setLayout(new BorderLayout()); + dialog.add(featureSettingsPanels, BorderLayout.CENTER); + JPanel buttons = new JPanel(); + JButton ok = new JButton(MessageManager.getString("action.ok")); + ok.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + try + { + featureSettingsUI.setClosed(true); + } catch (PropertyVetoException pv) + { + pv.printStackTrace(); + } + } + }); + JButton cancel = new JButton( + MessageManager.getString("action.cancel")); + cancel.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + try + { + for (Component fspanel : featureSettingsPanels.getComponents()) + { + if (fspanel instanceof FeatureSettingsControllerGuiI) + { + ((FeatureSettingsControllerGuiI) fspanel).revert(); + } + } + featureSettingsUI.setClosed(true); + } catch (Exception pv) + { + pv.printStackTrace(); + } + } + }); + buttons.add(ok); + buttons.add(cancel); + dialog.add(buttons, BorderLayout.SOUTH); + featureSettingsUI.setContentPane(dialog); + createDummyTabs(); + } + if (featureSettingsPanels + .indexOfTabComponent((Component) featureSettings) > -1) + { + // just show the feature settings ! + featureSettingsPanels + .setSelectedComponent((Component) featureSettings); + return; + } + // otherwise replace the dummy tab with the given feature settings + int pos = getAlignFrames().indexOf(featureSettings.getAlignframe()); + // if pos==-1 then alignFrame isn't managed by this splitframe + if (pos == 0) + { + featureSettingsPanels.removeTabAt(0); + featureSettingsPanels.insertTab(tabName[0], null, + (Component) featureSettings, + MessageManager.formatMessage( + "label.sequence_feature_settings_for", tabName[0]), + 0); + } + if (pos == 1) + { + featureSettingsPanels.removeTabAt(1); + featureSettingsPanels.insertTab(tabName[1], null, + (Component) featureSettings, + MessageManager.formatMessage( + "label.sequence_feature_settings_for", tabName[1]), + 1); + } + featureSettingsPanels.setSelectedComponent((Component) featureSettings); + // TODO: JAL-3535 - construct a feature settings title including names of + // currently selected CDS and Protein names + + if (showInternalFrame) + { + if (Platform.isAMacAndNotJS()) + { + Desktop.addInternalFrame(featureSettingsUI, + MessageManager.getString( + "label.sequence_feature_settings_for_CDS_and_Protein"), + 600, 480); + } + else + { + Desktop.addInternalFrame(featureSettingsUI, + MessageManager.getString( + "label.sequence_feature_settings_for_CDS_and_Protein"), + 600, 450); + } + featureSettingsUI + .setMinimumSize(new Dimension(FS_MIN_WIDTH, FS_MIN_HEIGHT)); + + featureSettingsUI.addInternalFrameListener( + new javax.swing.event.InternalFrameAdapter() + { + @Override + public void internalFrameClosed( + javax.swing.event.InternalFrameEvent evt) + { + for (int tab = 0; tab < featureSettingsPanels + .getTabCount();) + { + FeatureSettingsControllerGuiI fsettings = (FeatureSettingsControllerGuiI) featureSettingsPanels + .getTabComponentAt(tab); + if (fsettings != null) + { + featureSettingsPanels.removeTabAt(tab); + fsettings.featureSettings_isClosed(); + } + else + { + tab++; + } + } + featureSettingsPanels = null; + featureSettingsUI = null; + }; + }); + featureSettingsUI.setLayer(JLayeredPane.PALETTE_LAYER); + } + } + + /** + * tab names for feature settings + */ + private String[] tabName = new String[] { + MessageManager.getString("label.CDS"), + MessageManager.getString("label.protein") }; + + /** + * create placeholder tabs which materialise the feature settings for a given + * view. Also reinitialises any tabs containing stale feature settings + */ + private void createDummyTabs() + { + for (int tabIndex = 0; tabIndex < 2; tabIndex++) + { + JPanel dummyTab = new JPanel(); + featureSettingsPanels.addTab(tabName[tabIndex], dummyTab); + } + } + + private void replaceWithDummyTab(FeatureSettingsControllerI toClose) + { + Component dummyTab = null; + for (int tabIndex = 0; tabIndex < 2; tabIndex++) + { + if (featureSettingsPanels.getTabCount() > tabIndex) + { + dummyTab = featureSettingsPanels.getTabComponentAt(tabIndex); + if (dummyTab instanceof FeatureSettingsControllerGuiI + && !dummyTab.isVisible()) + { + featureSettingsPanels.removeTabAt(tabIndex); + // close the feature Settings tab + ((FeatureSettingsControllerGuiI) dummyTab) + .featureSettings_isClosed(); + // create a dummy tab in its place + dummyTab = new JPanel(); + featureSettingsPanels.insertTab(tabName[tabIndex], null, dummyTab, + MessageManager.formatMessage( + "label.sequence_feature_settings_for", + tabName[tabIndex]), + tabIndex); + } + } + } + } + + @Override + public void closeFeatureSettings( + FeatureSettingsControllerI featureSettings, + boolean closeContainingFrame) + { + if (featureSettingsUI != null) + { + if (closeContainingFrame) + { + try + { + featureSettingsUI.setClosed(true); + } catch (Exception x) + { + } + featureSettingsUI = null; + } + else + { + replaceWithDummyTab(featureSettings); + } + } + } + + @Override + public boolean isFeatureSettingsOpen() + { + return featureSettingsUI != null && !featureSettingsUI.isClosed(); + } +} \ No newline at end of file diff --git a/src/jalview/gui/VamsasApplication.java b/src/jalview/gui/VamsasApplication.java index ef814d3..1320c0f 100644 --- a/src/jalview/gui/VamsasApplication.java +++ b/src/jalview/gui/VamsasApplication.java @@ -688,7 +688,8 @@ public class VamsasApplication implements SelectionSource, VamsasSource public void disableGui(boolean b) { - Desktop.getInstance().setVamsasUpdate(b); + // JAL-3311 TODO: remove this class! + // Desktop.instance.setVamsasUpdate(b); } Hashtable _backup_vobj2jv; diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index aa21b0f..821384a 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -29,12 +29,13 @@ import jalview.api.FeaturesSourceI; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.datamodel.features.FeatureMatcherSet; import jalview.datamodel.features.FeatureMatcherSetI; -import jalview.io.gff.GffHelperBase; +import jalview.gui.Desktop; import jalview.io.gff.GffHelperFactory; import jalview.io.gff.GffHelperI; import jalview.schemes.FeatureColour; @@ -49,9 +50,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; /** * Parses and writes features files, which may be in Jalview, GFF2 or GFF3 @@ -71,6 +74,8 @@ import java.util.Map.Entry; */ public class FeaturesFile extends AlignFile implements FeaturesSourceI { + private static final String EQUALS = "="; + private static final String TAB_REGEX = "\\t"; private static final String STARTGROUP = "STARTGROUP"; @@ -441,7 +446,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI float score = Float.NaN; try { - score = new Float(gffColumns[6]).floatValue(); + score = Float.valueOf(gffColumns[6]).floatValue(); } catch (NumberFormatException ex) { sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); @@ -563,31 +568,38 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Returns contents of a Jalview format features file, for visible features, - * as filtered by type and group. Features with a null group are displayed if - * their feature type is visible. Non-positional features may optionally be - * included (with no check on type or group). + * Returns contents of a Jalview format features file, for visible features, as + * filtered by type and group. Features with a null group are displayed if their + * feature type is visible. Non-positional features may optionally be included + * (with no check on type or group). * * @param sequences * @param fr * @param includeNonPositional - * if true, include non-positional features (regardless of group or - * type) + * if true, include non-positional features + * (regardless of group or type) + * @param includeComplement + * if true, include visible complementary + * (CDS/protein) positional features, with + * locations converted to local sequence + * coordinates * @return */ public String printJalviewFormat(SequenceI[] sequences, - FeatureRenderer fr, boolean includeNonPositional) + FeatureRenderer fr, boolean includeNonPositional, + boolean includeComplement) { Map visibleColours = fr .getDisplayedFeatureCols(); Map featureFilters = fr.getFeatureFilters(); - if (!includeNonPositional - && (visibleColours == null || visibleColours.isEmpty())) - { - // no point continuing. - return "No Features Visible"; - } + // BH check this is out? +// if (!includeNonPositional +// && (visibleColours == null || visibleColours.isEmpty())) +// { +// // no point continuing. +// return "No Features Visible"; +// } /* * write out feature colours (if we know them) @@ -620,10 +632,151 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI int count = outputFeaturesByGroup(out, fr, types, sequences, includeNonPositional); + if (includeComplement) + { + count += outputComplementFeatures(out, fr, sequences); + } + return count > 0 ? out.toString() : "No Features Visible"; } /** + * Outputs any visible complementary (CDS/peptide) positional features as + * Jalview format, within feature group. The coordinates of the linked features + * are converted to the corresponding positions of the local sequences. + * + * @param out + * @param fr + * @param sequences + * @return + */ + private int outputComplementFeatures(StringBuilder out, + FeatureRenderer fr, SequenceI[] sequences) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); + + /* + * bin features by feature group and sequence + */ + Map>> map = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + int count = 0; + + for (SequenceI seq : sequences) + { + /* + * find complementary features + */ + List complementary = findComplementaryFeatures(seq, + fr2); + String seqName = seq.getName(); + + for (SequenceFeature sf : complementary) + { + String group = sf.getFeatureGroup(); + if (!map.containsKey(group)) + { + map.put(group, new LinkedHashMap<>()); // preserves sequence order + } + Map> groupFeatures = map.get(group); + if (!groupFeatures.containsKey(seqName)) + { + groupFeatures.put(seqName, new ArrayList<>()); + } + List foundFeatures = groupFeatures.get(seqName); + foundFeatures.add(sf); + count++; + } + } + + /* + * output features by group + */ + for (Entry>> groupFeatures : map.entrySet()) + { + out.append(newline); + String group = groupFeatures.getKey(); + if (!"".equals(group)) + { + out.append(STARTGROUP).append(TAB).append(group).append(newline); + } + Map> seqFeaturesMap = groupFeatures + .getValue(); + for (Entry> seqFeatures : seqFeaturesMap + .entrySet()) + { + String sequenceName = seqFeatures.getKey(); + for (SequenceFeature sf : seqFeatures.getValue()) + { + formatJalviewFeature(out, sequenceName, sf); + } + } + if (!"".equals(group)) + { + out.append(ENDGROUP).append(TAB).append(group).append(newline); + } + } + + return count; + } + + /** + * Answers a list of mapped features visible in the (CDS/protein) complement, + * with feature positions translated to local sequence coordinates + * + * @param seq + * @param fr2 + * @return + */ + protected List findComplementaryFeatures(SequenceI seq, + FeatureRenderer fr2) + { + /* + * avoid duplication of features (e.g. peptide feature + * at all 3 mapped codon positions) + */ + List found = new ArrayList<>(); + List complementary = new ArrayList<>(); + + for (int pos = seq.getStart(); pos <= seq.getEnd(); pos++) + { + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos); + + if (mf != null) + { + MapList mapping = mf.mapping.getMap(); + for (SequenceFeature sf : mf.features) + { + /* + * make a virtual feature with local coordinates + */ + if (!found.contains(sf)) + { + String group = sf.getFeatureGroup(); + if (group == null) + { + group = ""; + } + found.add(sf); + int begin = sf.getBegin(); + int end = sf.getEnd(); + int[] range = mf.mapping.getTo() == seq.getDatasetSequence() + ? mapping.locateInTo(begin, end) + : mapping.locateInFrom(begin, end); + SequenceFeature sf2 = new SequenceFeature(sf, range[0], + range[1], group, sf.getScore()); + complementary.add(sf2); + } + } + } + } + + return complementary; + } + + /** * Outputs any feature filters defined for visible feature types, sandwiched by * STARTFILTERS and ENDFILTERS lines * @@ -745,7 +898,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } firstInGroup = false; - out.append(formatJalviewFeature(sequenceName, sf)); + formatJalviewFeature(out, sequenceName, sf); } } } @@ -759,14 +912,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * Formats one feature in Jalview format and appends to the string buffer + * * @param out * @param sequenceName * @param sequenceFeature */ protected String formatJalviewFeature( - String sequenceName, SequenceFeature sequenceFeature) + StringBuilder out, String sequenceName, + SequenceFeature sequenceFeature) { - StringBuilder out = new StringBuilder(64); if (sequenceFeature.description == null || sequenceFeature.description.equals("")) { @@ -791,7 +946,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI if (sequenceFeature.description.indexOf(href) == -1) { - out.append(" " + label + ""); + out.append(" ") + .append(label).append(""); } } @@ -882,23 +1038,25 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * a map whose keys are the type names of visible features * @param visibleFeatureGroups * @param includeNonPositionalFeatures + * @param includeComplement * @return */ public String printGffFormat(SequenceI[] sequences, - FeatureRenderer fr, boolean includeNonPositionalFeatures) + FeatureRenderer fr, boolean includeNonPositionalFeatures, + boolean includeComplement) + { + FeatureRenderer fr2 = null; + if (includeComplement) { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + fr2 = Desktop.getAlignFrameFor(comp).getFeatureRenderer(); + } + Map visibleColours = fr.getDisplayedFeatureCols(); StringBuilder out = new StringBuilder(256); - out.append(String.format("%s %d" + newline, GFF_VERSION, - gffVersion == 0 ? 2 : gffVersion)); - - if (!includeNonPositionalFeatures - && (visibleColours == null || visibleColours.isEmpty())) - { - return out.toString(); - } + out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); String[] types = visibleColours == null ? new String[0] : visibleColours.keySet() @@ -906,6 +1064,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI for (SequenceI seq : sequences) { + List seqFeatures = new ArrayList<>(); List features = new ArrayList<>(); if (includeNonPositionalFeatures) { @@ -918,15 +1077,40 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI for (SequenceFeature sf : features) { - if (!sf.isNonPositional() && !fr.isVisible(sf)) + if (sf.isNonPositional() || fr.isVisible(sf)) { /* - * feature hidden by group visibility, colour threshold, + * drop features hidden by group visibility, colour threshold, * or feature filter condition */ - continue; + seqFeatures.add(sf); } + } + + if (includeComplement) + { + seqFeatures.addAll(findComplementaryFeatures(seq, fr2)); + } + + /* + * sort features here if wanted + */ + for (SequenceFeature sf : seqFeatures) + { + formatGffFeature(out, seq, sf); + out.append(newline); + } + } + return out.toString(); + } + + /** + * Formats one feature as GFF and appends to the string buffer + */ + private void formatGffFeature(StringBuilder out, SequenceI seq, + SequenceFeature sf) + { String source = sf.featureGroup; if (source == null) { @@ -953,18 +1137,111 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI String phase = sf.getPhase(); out.append(phase == null ? "." : phase); - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) + if (sf.otherDetails != null && !sf.otherDetails.isEmpty()) { - out.append(TAB).append(attributes); - } + Map map = sf.otherDetails; + formatAttributes(out, map); + } + } - out.append(newline); + /** + * A helper method that outputs attributes stored in the map as + * semicolon-delimited values e.g. + * + *
+   * AC_Male=0;AF_NFE=0.00000e 00;Hom_FIN=0;GQ_MEDIAN=9
+   * 
+ * + * A map-valued attribute is formatted as a comma-delimited list within braces, + * for example + * + *
+   * jvmap_CSQ={ALLELE_NUM=1,UNIPARC=UPI0002841053,Feature=ENST00000585561}
+   * 
+ * + * The {@code jvmap_} prefix designates a values map and is removed if the value + * is parsed when read in. (The GFF3 specification allows 'semi-structured data' + * to be represented provided the attribute name begins with a lower case + * letter.) + * + * @param sb + * @param map + * @see http://gmod.org/wiki/GFF3#GFF3_Format + */ + void formatAttributes(StringBuilder sb, Map map) + { + sb.append(TAB); + boolean first = true; + for (String key : map.keySet()) + { + if (SequenceFeature.STRAND.equals(key) + || SequenceFeature.PHASE.equals(key)) + { + /* + * values stashed in map but output to their own columns + */ + continue; + } + { + if (!first) + { + sb.append(";"); + } + } + first = false; + Object value = map.get(key); + if (value instanceof Map) + { + formatMapAttribute(sb, key, (Map) value); + } + else + { + String formatted = StringUtils.urlEncode(value.toString(), + GffHelperI.GFF_ENCODABLE); + sb.append(key).append(EQUALS).append(formatted); } } + } - return out.toString(); + /** + * Formats the map entries as + * + *
+   * key=key1=value1,key2=value2,...
+   * 
+ * + * and appends this to the string buffer + * + * @param sb + * @param key + * @param map + */ + private void formatMapAttribute(StringBuilder sb, String key, + Map map) + { + if (map == null || map.isEmpty()) + { + return; + } + + /* + * AbstractMap.toString would be a shortcut here, but more reliable + * to code the required format in case toString changes in future + */ + sb.append(key).append(EQUALS); + boolean first = true; + for (Entry entry : map.entrySet()) + { + if (!first) + { + sb.append(","); + } + first = false; + sb.append(entry.getKey().toString()).append(EQUALS); + String formatted = StringUtils.urlEncode(entry.getValue().toString(), + GffHelperI.GFF_ENCODABLE); + sb.append(formatted); + } } /** @@ -1111,37 +1388,38 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI return seq; } - /** - * Process the 'column 9' data of the GFF file. This is less formally defined, - * and its interpretation will vary depending on the tool that has generated - * it. - * - * @param attributes - * @param sf - */ - protected void processGffColumnNine(String attributes, SequenceFeature sf) - { - sf.setAttributes(attributes); - - /* - * Parse attributes in column 9 and add them to the sequence feature's - * 'otherData' table; use Note as a best proxy for description - */ - char nameValueSeparator = gffVersion == 3 ? '=' : ' '; - // TODO check we don't break GFF2 values which include commas here - Map> nameValues = GffHelperBase - .parseNameValuePairs(attributes, ";", nameValueSeparator, ","); - for (Entry> attr : nameValues.entrySet()) - { - String values = StringUtils.listToDelimitedString(attr.getValue(), - "; "); - sf.setValue(attr.getKey(), values); - if (NOTE.equals(attr.getKey())) - { - sf.setDescription(values); - } - } - } + // BH! check that we did not lose something here. +// /** +// * Process the 'column 9' data of the GFF file. This is less formally defined, +// * and its interpretation will vary depending on the tool that has generated +// * it. +// * +// * @param attributes +// * @param sf +// */ +// protected void processGffColumnNine(String attributes, SequenceFeature sf) +// { +// sf.setAttributes(attributes); +// +// /* +// * Parse attributes in column 9 and add them to the sequence feature's +// * 'otherData' table; use Note as a best proxy for description +// */ +// char nameValueSeparator = gffVersion == 3 ? '=' : ' '; +// // TODO check we don't break GFF2 values which include commas here +// Map> nameValues = GffHelperBase +// .parseNameValuePairs(attributes, ";", nameValueSeparator, ","); +// for (Entry> attr : nameValues.entrySet()) +// { +// String values = StringUtils.listToDelimitedString(attr.getValue(), +// "; "); +// sf.setValue(attr.getKey(), values); +// if (NOTE.equals(attr.getKey())) +// { +// sf.setDescription(values); +// } +// } +// } /** * After encountering ##fasta in a GFF3 file, process the remainder of the diff --git a/src/jalview/io/FileLoader.java b/src/jalview/io/FileLoader.java index 0254b5f..a64582d 100755 --- a/src/jalview/io/FileLoader.java +++ b/src/jalview/io/FileLoader.java @@ -70,8 +70,6 @@ public class FileLoader implements Runnable */ private static int MAX_HISTORY = 11; - private File selectedFile; - String file; DataSourceType protocol; @@ -81,6 +79,37 @@ public class FileLoader implements Runnable AlignmentFileReaderI source; // alternative specification of where data // comes from + AlignViewport viewport; + + AlignFrame alignFrame; + + long loadtime; + + long memused; + + boolean raiseGUI = true; + + private File selectedFile; + + /** + * default constructor always raised errors in GUI dialog boxes + */ + public FileLoader() + { + this(true); + } + + /** + * construct a Fileloader that may raise errors non-interactively + * + * @param raiseGUI + * true if errors are to be raised as GUI dialog boxes + */ + public FileLoader(boolean raiseGUI) + { + this.raiseGUI = raiseGUI; + } + /** * It is critical that all these fields are set, as this instance is reused. * @@ -100,47 +129,69 @@ public class FileLoader implements Runnable this.format = format; } - AlignViewport viewport; - - AlignFrame alignFrame; - - long loadtime; - - long memused; - - boolean raiseGUI = true; /** - * default constructor always raised errors in GUI dialog boxes + * Uppercase LoadFile is deprecated because it does not pass byte[] data to + * JavaScript + * + * @param viewport + * @param file + * @param protocol + * @param format */ - public FileLoader() + @Deprecated + public void LoadFile(AlignViewport viewport, String file, + DataSourceType protocol, FileFormatI format) { - this(true); + if (viewport != null) + { + this.viewport = viewport; + } + loadFile(file, protocol, format); + } + + public void LoadFile(AlignViewport viewport, File file, + DataSourceType protocol, FileFormatI format) + { + loadFile(viewport, file, protocol, format); } /** - * construct a Fileloader that may raise errors non-interactively + * Uppercase LoadFile is deprecated because it does not pass byte[] data to + * JavaScript * - * @param raiseGUI - * true if errors are to be raised as GUI dialog boxes + * @param file + * @param protocol + * @param format */ - public FileLoader(boolean raiseGUI) + @Deprecated + public void LoadFile(String file, DataSourceType protocol, + FileFormatI format) { - this.raiseGUI = raiseGUI; + loadFile(file, protocol, format); } - public void loadFile(AlignViewport viewport, Object file, + /** + * necessary to use Object here in order to pass the file data + * + * @param viewport + * @param file + * File preferably to String + * @param protocol + * @param format + */ + public void loadFile(AlignViewport viewport, File file, DataSourceType protocol, FileFormatI format) { - this.viewport = viewport; - if (file instanceof File) { - this.selectedFile = (File) file; - file = selectedFile.getPath(); + if (viewport != null) + { + this.viewport = viewport; } - loadFile(file.toString(), protocol, format); + this.selectedFile = file; + loadFile(selectedFile.getPath(), protocol, format); } - public void loadFile(String file, DataSourceType protocol, + private void loadFile(String file, DataSourceType protocol, FileFormatI format) { this.file = file; @@ -166,10 +217,11 @@ public class FileLoader implements Runnable * @param sourceType * @return alignFrame constructed from file contents */ + @Deprecated public AlignFrame LoadFileWaitTillLoaded(String file, DataSourceType sourceType) { - return loadFileWaitTillLoaded(file, sourceType, null); + return LoadFileWaitTillLoaded(file, sourceType, null); } /** @@ -180,7 +232,8 @@ public class FileLoader implements Runnable * @param format * @return alignFrame constructed from file contents */ - public AlignFrame loadFileWaitTillLoaded(String file, + @Deprecated + public AlignFrame LoadFileWaitTillLoaded(String file, DataSourceType sourceType, FileFormatI format) { setFileFields(null, null, file, sourceType, format); @@ -237,6 +290,7 @@ public class FileLoader implements Runnable // refer to it as. return; } + // BH logic change here just includes ignoring file==null if (file == null || file.indexOf(System.getProperty("java.io.tmpdir")) > -1) { @@ -248,6 +302,7 @@ public class FileLoader implements Runnable String historyItems = Cache.getProperty(type); + // BH simpler coding if (historyItems != null) { String[] tokens = historyItems.split("\\t"); diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index 27e4da2..4933cd6 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -23,6 +23,8 @@ package jalview.io; import jalview.api.FeatureColourI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.DBRefSource; +import jalview.datamodel.GeneLociI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.util.MessageManager; @@ -44,6 +46,8 @@ import java.util.Map; */ public class SequenceAnnotationReport { + private static final int MAX_DESCRIPTION_LENGTH = 40; + private static final String COMMA = ","; private static final String ELLIPSIS = "..."; @@ -66,11 +70,11 @@ public class SequenceAnnotationReport @Override public int compare(DBRefEntry ref1, DBRefEntry ref2) { - if (ref1.isChromosome()) + if (ref1 instanceof GeneLociI) { return -1; } - if (ref2.isChromosome()) + if (ref2 instanceof GeneLociI) { return 1; } @@ -120,23 +124,65 @@ public class SequenceAnnotationReport } /** - * Append text for the list of features to the tooltip + * Append text for the list of features to the tooltip Returns number of + * features left if maxlength limit is (or would have been) reached * * @param sb - * @param rpos + * @param residuePos * @param features * @param minmax + * @param maxlength */ - public void appendFeatures(final StringBuilder sb, int rpos, + public int appendFeaturesLengthLimit(final StringBuilder sb, + int residuePos, List features, + FeatureRendererModel fr, int maxlength) + { + for (int i = 0; i < features.size(); i++) + { + SequenceFeature feature = features.get(i); + if (appendFeature(sb, residuePos, fr, feature, null, maxlength)) + { + return features.size() - i; + } + } + return 0; + } + + public void appendFeatures(final StringBuilder sb, int residuePos, List features, FeatureRendererModel fr) { - if (features != null) + appendFeaturesLengthLimit(sb, residuePos, features, fr, 0); + } + + /** + * Appends text for mapped features (e.g. CDS feature for peptide or vice versa) + * Returns number of features left if maxlength limit is (or would have been) + * reached + * + * @param sb + * @param residuePos + * @param mf + * @param fr + * @param maxlength + */ + public int appendFeaturesLengthLimit(StringBuilder sb, int residuePos, + MappedFeatures mf, FeatureRendererModel fr, int maxlength) + { + for (int i = 0; i < mf.features.size(); i++) { - for (SequenceFeature feature : features) + SequenceFeature feature = mf.features.get(i); + if (appendFeature(sb, residuePos, fr, feature, mf, maxlength)) { - appendFeature(sb, rpos, fr, feature); + return mf.features.size() - i; } } + return 0; + } + + public void appendFeatures(StringBuilder sb, int residuePos, + MappedFeatures mf, FeatureRendererModel fr) + { + appendFeaturesLengthLimit(sb, residuePos, mf, fr, 0); } /** @@ -147,26 +193,28 @@ public class SequenceAnnotationReport * @param minmax * @param feature */ - void appendFeature(final StringBuilder sb, int rpos, - FeatureRendererModel fr, SequenceFeature feature) + boolean appendFeature(final StringBuilder sb0, int rpos, + FeatureRendererModel fr, SequenceFeature feature, + MappedFeatures mf, int maxlength) { + StringBuilder sb = new StringBuilder(); if (feature.isContactFeature()) { if (feature.getBegin() == rpos || feature.getEnd() == rpos) { - if (sb.length() > 6) + if (sb0.length() > 6) { - sb.append("
"); + sb.append("
"); } sb.append(feature.getType()).append(" ").append(feature.getBegin()) .append(":").append(feature.getEnd()); } - return; + return appendTextMaxLengthReached(sb0, sb, maxlength); } - if (sb.length() > 6) + if (sb0.length() > 6) { - sb.append("
"); + sb.append("
"); } // TODO: remove this hack to display link only features boolean linkOnly = feature.getValue("linkonly") != null; @@ -187,6 +235,20 @@ public class SequenceAnnotationReport if (description != null && !description.equals(feature.getType())) { description = StringUtils.stripHtmlTags(description); + + /* + * truncate overlong descriptions unless they contain an href + * before the truncation point (as truncation could leave corrupted html) + */ + int linkindex = description.toLowerCase().indexOf(" -1 + && linkindex < MAX_DESCRIPTION_LENGTH; + if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink) + { + description = description.substring(0, MAX_DESCRIPTION_LENGTH) + + ELLIPSIS; + } + sb.append("; ").append(description); } @@ -217,6 +279,36 @@ public class SequenceAnnotationReport } } } + + if (mf != null) + { + String variants = mf.findProteinVariants(feature); + if (!variants.isEmpty()) + { + sb.append(" ").append(variants); + } + } + } + return appendTextMaxLengthReached(sb0, sb, maxlength); + } + + void appendFeature(final StringBuilder sb, int rpos, + FeatureRendererModel fr, SequenceFeature feature, + MappedFeatures mf) + { + appendFeature(sb, rpos, fr, feature, mf, 0); + } + + private static boolean appendTextMaxLengthReached(StringBuilder sb0, + StringBuilder sb, int maxlength) + { + boolean ret = false; + if (maxlength == 0 || sb0.length() + sb.length() < maxlength) + { + sb0.append(sb); + return false; + } else { + return true; } } @@ -277,7 +369,8 @@ public class SequenceAnnotationReport + (urllink.get(0).toLowerCase() .equals(urllink.get(1).toLowerCase()) ? urllink .get(0) : (urllink.get(0) + ":" + urllink - .get(1))) + "
"); + .get(1))) + + "
"); } } catch (Exception x) { @@ -348,7 +441,7 @@ public class SequenceAnnotationReport if (sequence.getDescription() != null) { tmp = sequence.getDescription(); - sb.append("
").append(tmp); + sb.append(tmp); maxWidth = Math.max(maxWidth, tmp.length()); } SequenceI ds = sequence; @@ -371,7 +464,7 @@ public class SequenceAnnotationReport .getNonPositionalFeatures()) { int sz = -sb.length(); - appendFeature(sb, 0, fr, sf); + appendFeature(sb, 0, fr, sf, null); sz += sb.length(); maxWidth = Math.max(maxWidth, sz); } @@ -434,7 +527,7 @@ public class SequenceAnnotationReport countForSource++; if (countForSource == 1 || !summary) { - sb.append("
"); + sb.append("
"); } if (countForSource <= MAX_REFS_PER_SOURCE || !summary) { @@ -460,11 +553,11 @@ public class SequenceAnnotationReport } if (moreSources) { - sb.append("
").append(source).append(COMMA).append(ELLIPSIS); + sb.append("
").append(source).append(COMMA).append(ELLIPSIS); } if (ellipsis) { - sb.append("
("); + sb.append("
("); sb.append(MessageManager.getString("label.output_seq_details")); sb.append(")"); } diff --git a/src/jalview/io/gff/Gff2Helper.java b/src/jalview/io/gff/Gff2Helper.java index 19045d5..a15a116 100644 --- a/src/jalview/io/gff/Gff2Helper.java +++ b/src/jalview/io/gff/Gff2Helper.java @@ -38,20 +38,10 @@ public class Gff2Helper extends GffHelperBase */ public static Map> parseNameValuePairs(String text) { - // TODO: can a value include a comma? if so it will be broken by this return parseNameValuePairs(text, ";", ' ', ","); } /** - * Return ' ' as the name-value separator used in column 9 attributes. - */ - @Override - protected char getNameValueSeparator() - { - return ' '; - } - - /** * Default processing if not overridden is just to construct a sequence * feature */ diff --git a/src/jalview/io/gff/Gff3Helper.java b/src/jalview/io/gff/Gff3Helper.java index 28bcf44..e422ed4 100644 --- a/src/jalview/io/gff/Gff3Helper.java +++ b/src/jalview/io/gff/Gff3Helper.java @@ -350,15 +350,6 @@ public class Gff3Helper extends GffHelperBase } /** - * Return '=' as the name-value separator used in column 9 attributes. - */ - @Override - protected char getNameValueSeparator() - { - return '='; - } - - /** * Modifies the default SequenceFeature in order to set the Target sequence id * as the description */ @@ -424,6 +415,11 @@ public class Gff3Helper extends GffHelperBase desc = (String) sf.getValue(ID); } + /* + * and decode comma, equals, semi-colon as required by GFF3 spec + */ + desc = StringUtils.urlDecode(desc, GFF_ENCODABLE); + return desc; } } diff --git a/src/jalview/io/gff/GffHelperBase.java b/src/jalview/io/gff/GffHelperBase.java index 1d4d3ac..3db1755 100644 --- a/src/jalview/io/gff/GffHelperBase.java +++ b/src/jalview/io/gff/GffHelperBase.java @@ -43,7 +43,13 @@ import java.util.Map.Entry; */ public abstract class GffHelperBase implements GffHelperI { - private static final String NOTE = "Note"; + private static final String INVALID_GFF_ATTRIBUTE_FORMAT = "Invalid GFF attribute format: "; + + protected static final String COMMA = ","; + + protected static final String EQUALS = "="; + + protected static final String NOTE = "Note"; /* * GFF columns 1-9 (zero-indexed): @@ -260,9 +266,12 @@ public abstract class GffHelperBase implements GffHelperI /** * Parses the input line to a map of name / value(s) pairs. For example the - * line
+ * line + * + *
    * Notes=Fe-S;Method=manual curation, prediction; source = Pfam; Notes = Metal
-   * 
+ *
+ * * if parsed with delimiter=";" and separators {' ', '='}
* would return a map with { Notes={Fe=S, Metal}, Method={manual curation, * prediction}, source={Pfam}}
@@ -272,57 +281,80 @@ public abstract class GffHelperBase implements GffHelperI * name), or GFF3 format (which uses '=' as the name/value delimiter, and * strictly does not allow repeat occurrences of the same name - but does * allow a comma-separated list of values). + *

+ * Returns a (possibly empty) map of lists of values by attribute name. * * @param text * @param namesDelimiter * the major delimiter between name-value pairs * @param nameValueSeparator - * one or more separators used between name and value + * separator used between name and value * @param valuesDelimiter * delimits a list of more than one value - * @return the name-values map (which may be empty but never null) + * @return */ public static Map> parseNameValuePairs(String text, String namesDelimiter, char nameValueSeparator, String valuesDelimiter) { - Map> map = new HashMap>(); + Map> map = new HashMap<>(); if (text == null || text.trim().length() == 0) { return map; } - for (String pair : text.trim().split(namesDelimiter)) + /* + * split by major delimiter (; for GFF3) + */ + for (String nameValuePair : text.trim().split(namesDelimiter)) { - pair = pair.trim(); - if (pair.length() == 0) + nameValuePair = nameValuePair.trim(); + if (nameValuePair.length() == 0) { continue; } - int sepPos = pair.indexOf(nameValueSeparator); + /* + * find name/value separator (= for GFF3) + */ + int sepPos = nameValuePair.indexOf(nameValueSeparator); if (sepPos == -1) { - // no name=value present + // no name=value found continue; } - String key = pair.substring(0, sepPos).trim(); - String values = pair.substring(sepPos + 1).trim(); - if (values.length() > 0) + String name = nameValuePair.substring(0, sepPos).trim(); + String values = nameValuePair.substring(sepPos + 1).trim(); + if (values.isEmpty()) + { + continue; + } + + List vals = map.get(name); + if (vals == null) + { + vals = new ArrayList<>(); + map.put(name, vals); + } + + /* + * if 'values' contains more name/value separators, parse as a map + * (nested sub-attribute values) + */ + if (values.indexOf(nameValueSeparator) != -1) + { + vals.add(values); + } + else { - List vals = map.get(key); - if (vals == null) - { - vals = new ArrayList(); - map.put(key, vals); - } for (String val : values.split(valuesDelimiter)) { vals.add(val); } } } + return map; } @@ -357,8 +389,7 @@ public abstract class GffHelperBase implements GffHelperI int end = Integer.parseInt(gff[END_COL]); /* - * default 'score' is 0 rather than Float.NaN as the latter currently - * disables the 'graduated colour => colour by label' option + * default 'score' is 0 rather than Float.NaN - see JAL-2554 */ float score = 0f; try @@ -379,22 +410,32 @@ public abstract class GffHelperBase implements GffHelperI if (attributes != null) { /* - * save 'raw' column 9 to allow roundtrip output as input - */ - sf.setAttributes(gff[ATTRIBUTES_COL]); - - /* * Add attributes in column 9 to the sequence feature's - * 'otherData' table; use Note as a best proxy for description + * 'otherData' table; use Note as a best proxy for description; + * decode any encoded comma, equals, semi-colon as per GFF3 spec */ for (Entry> attr : attributes.entrySet()) { - String values = StringUtils.listToDelimitedString(attr.getValue(), - ","); - sf.setValue(attr.getKey(), values); - if (NOTE.equals(attr.getKey())) + String key = attr.getKey(); + List values = attr.getValue(); + if (values.size() == 1 && values.get(0).contains(EQUALS)) + { + /* + * 'value' is actually nested subattributes as x=a,y=b,z=c + */ + Map valueMap = parseAttributeMap(values.get(0)); + sf.setValue(key, valueMap); + } + else { - sf.setDescription(values); + String csvValues = StringUtils.listToDelimitedString(values, + COMMA); + csvValues = StringUtils.urlDecode(csvValues, GFF_ENCODABLE); + sf.setValue(key, csvValues); + if (NOTE.equals(key)) + { + sf.setDescription(csvValues); + } } } } @@ -408,12 +449,102 @@ public abstract class GffHelperBase implements GffHelperI } /** - * Returns the character used to separate attributes names from values in GFF - * column 9. This is space for GFF2, '=' for GFF3. + * Parses a (GFF3 format) list of comma-separated key=value pairs into a Map + * of {@code key, + * value}
+ * An input string like {@code a=b,c,d=e,f=g,h} is parsed to + * + *

+   * a = "b,c"
+   * d = "e"
+   * f = "g,h"
+   * 
+ * + * @param s * * @return */ - protected abstract char getNameValueSeparator(); + protected static Map parseAttributeMap(String s) + { + Map map = new HashMap<>(); + String[] fields = s.split(EQUALS); + + /* + * format validation + */ + boolean valid = true; + if (fields.length < 2) + { + /* + * need at least A=B here + */ + valid = false; + } + else if (fields[0].isEmpty() || fields[0].contains(COMMA)) + { + /* + * A,B=C is not a valid start, nor is =C + */ + valid = false; + } + else + { + for (int i = 1; i < fields.length - 1; i++) + { + if (fields[i].isEmpty() || !fields[i].contains(COMMA)) + { + /* + * intermediate tokens must include value,name + */ + valid = false; + } + } + } + + if (!valid) + { + System.err.println(INVALID_GFF_ATTRIBUTE_FORMAT + s); + return map; + } + + int i = 0; + while (i < fields.length - 1) + { + boolean lastPair = i == fields.length - 2; + String before = fields[i]; + String after = fields[i + 1]; + + /* + * if 'key' looks like a,b,c then the last token is the + * key + */ + String theKey = before.contains(COMMA) + ? before.substring(before.lastIndexOf(COMMA) + 1) + : before; + + theKey = theKey.trim(); + if (theKey.isEmpty()) + { + System.err.println(INVALID_GFF_ATTRIBUTE_FORMAT + s); + map.clear(); + return map; + } + + /* + * if 'value' looks like a,b,c then all but the last token is the value, + * unless this is the last field (no more = to follow), in which case + * all of it makes up the value + */ + String theValue = after.contains(COMMA) && !lastPair + ? after.substring(0, after.lastIndexOf(COMMA)) + : after; + map.put(StringUtils.urlDecode(theKey, GFF_ENCODABLE), + StringUtils.urlDecode(theValue, GFF_ENCODABLE)); + i += 1; + } + + return map; + } /** * Returns any existing mapping held on the alignment between the given diff --git a/src/jalview/io/gff/GffHelperI.java b/src/jalview/io/gff/GffHelperI.java index 7fbcf5c..387ee60 100644 --- a/src/jalview/io/gff/GffHelperI.java +++ b/src/jalview/io/gff/GffHelperI.java @@ -35,6 +35,12 @@ import java.util.List; */ public interface GffHelperI { + /* + * GFF3 spec requires comma, equals, semi-colon, tab, percent characters to be + * encoded as %2C, %3D, %3B, %09, %25 respectively within data values + * see https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md + */ + final String GFF_ENCODABLE = ",=;\t%"; final String RENAME_TOKEN = "$RENAME_TO$"; diff --git a/src/jalview/io/gff/SequenceOntologyLite.java b/src/jalview/io/gff/SequenceOntologyLite.java index 72e906c..7d354e0 100644 --- a/src/jalview/io/gff/SequenceOntologyLite.java +++ b/src/jalview/io/gff/SequenceOntologyLite.java @@ -70,6 +70,7 @@ public class SequenceOntologyLite implements SequenceOntologyI { "snRNA", "transcript" }, { "miRNA", "transcript" }, { "lincRNA", "transcript" }, + { "lnc_RNA", "transcript" }, { "rRNA", "transcript" }, { "mRNA", "transcript" }, // there are many more sub-types of ncRNA... diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java index bf988da..168f1c6 100644 --- a/src/jalview/io/vcf/VCFLoader.java +++ b/src/jalview/io/vcf/VCFLoader.java @@ -1,6 +1,5 @@ package jalview.io.vcf; -import jalview.analysis.AlignmentUtils; import jalview.analysis.Dna; import jalview.api.AlignViewControllerGuiI; import jalview.bin.Cache; @@ -20,14 +19,18 @@ import jalview.io.gff.SequenceOntologyI; import jalview.util.MapList; import jalview.util.MappingUtils; import jalview.util.MessageManager; +import jalview.util.StringUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -35,8 +38,10 @@ import htsjdk.samtools.SAMException; import htsjdk.samtools.SAMSequenceDictionary; import htsjdk.samtools.SAMSequenceRecord; import htsjdk.samtools.util.CloseableIterator; +import htsjdk.tribble.TribbleException; import htsjdk.variant.variantcontext.Allele; import htsjdk.variant.variantcontext.VariantContext; +import htsjdk.variant.vcf.VCFConstants; import htsjdk.variant.vcf.VCFHeader; import htsjdk.variant.vcf.VCFHeaderLine; import htsjdk.variant.vcf.VCFHeaderLineCount; @@ -51,6 +56,23 @@ import htsjdk.variant.vcf.VCFInfoHeaderLine; */ public class VCFLoader { + private static final String VCF_ENCODABLE = ":;=%,"; + + /* + * Jalview feature attributes for VCF fixed column data + */ + private static final String VCF_POS = "POS"; + + private static final String VCF_ID = "ID"; + + private static final String VCF_QUAL = "QUAL"; + + private static final String VCF_FILTER = "FILTER"; + + private static final String NO_VALUE = VCFConstants.MISSING_VALUE_v4; // '.' + + private static final String DEFAULT_SPECIES = "homo_sapiens"; + /** * A class to model the mapping from sequence to VCF coordinates. Cases include *
    @@ -82,7 +104,7 @@ public class VCFLoader /* * Lookup keys, and default values, for Preference entries that describe - * patterns for VCF and VEP fields to capture + * patterns for VCF and VEP fields to capture */ private static final String VEP_FIELDS_PREF = "VEP_FIELDS"; @@ -93,6 +115,18 @@ public class VCFLoader private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG"; /* + * Lookup keys, and default values, for Preference entries that give + * mappings from tokens in the 'reference' header to species or assembly + */ + private static final String VCF_ASSEMBLY = "VCF_ASSEMBLY"; + + private static final String DEFAULT_VCF_ASSEMBLY = "assembly19=GRCh37,hs37=GRCh37,grch37=GRCh37,grch38=GRCh38"; + + private static final String VCF_SPECIES = "VCF_SPECIES"; // default is human + + private static final String DEFAULT_REFERENCE = "grch37"; // fallback default is human GRCh37 + + /* * keys to fields of VEP CSQ consequence data * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html */ @@ -114,12 +148,6 @@ public class VCFLoader private static final String PIPE_REGEX = "\\|"; /* - * key for Allele Frequency output by VEP - * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html - */ - private static final String ALLELE_FREQUENCY_KEY = "AF"; - - /* * delimiter that separates multiple consequence data blocks */ private static final String COMMA = ","; @@ -155,6 +183,16 @@ public class VCFLoader private VCFHeader header; /* + * species (as a valid Ensembl term) the VCF is for + */ + private String vcfSpecies; + + /* + * genome assembly version (as a valid Ensembl identifier) the VCF is for + */ + private String vcfAssembly; + + /* * a Dictionary of contigs (if present) referenced in the VCF file */ private SAMSequenceDictionary dictionary; @@ -191,6 +229,12 @@ public class VCFLoader */ Map vepFieldsOfInterest; + /* + * key:value for which rejected data has been seen + * (the error is logged only once for each combination) + */ + private Set badData; + /** * Constructor given a VCF file * @@ -246,12 +290,18 @@ public class VCFLoader */ public SequenceI loadVCFContig(String contig) { - String ref = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY) - .getValue(); + VCFHeaderLine headerLine = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY); + if (headerLine == null) + { + Cache.log.error("VCF reference header not found"); + return null; + } + String ref = headerLine.getValue(); if (ref.startsWith("file://")) { ref = ref.substring(7); } + setSpeciesAndAssembly(ref); SequenceI seq = null; File dbFile = new File(ref); @@ -260,12 +310,12 @@ public class VCFLoader { HtsContigDb db = new HtsContigDb("", dbFile); seq = db.getSequenceProxy(contig); - loadSequenceVCF(seq, ref); + loadSequenceVCF(seq); db.close(); } else { - System.err.println("VCF reference not found: " + ref); + Cache.log.error("VCF reference not found: " + ref); } return seq; @@ -284,7 +334,9 @@ public class VCFLoader { VCFHeaderLine ref = header .getOtherHeaderLine(VCFHeader.REFERENCE_KEY); - String vcfAssembly = ref.getValue(); + String reference = ref == null ? null : ref.getValue(); + + setSpeciesAndAssembly(reference); int varCount = 0; int seqCount = 0; @@ -294,7 +346,7 @@ public class VCFLoader */ for (SequenceI seq : seqs) { - int added = loadSequenceVCF(seq, vcfAssembly); + int added = loadSequenceVCF(seq); if (added > 0) { seqCount++; @@ -338,6 +390,71 @@ public class VCFLoader } /** + * Attempts to determine and save the species and genome assembly version to + * which the VCF data applies. This may be done by parsing the {@code reference} + * header line, configured in a property file, or (potentially) confirmed + * interactively by the user. + *

    + * The saved values should be identifiers valid for Ensembl's REST service + * {@code map} endpoint, so they can be used (if necessary) to retrieve the + * mapping between VCF coordinates and sequence coordinates. + * + * @param reference + * @see https://rest.ensembl.org/documentation/info/assembly_map + * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml + * @see https://rest.ensembl.org/info/species?content-type=text/xml + */ + protected void setSpeciesAndAssembly(String reference) + { + if (reference == null) + { + Cache.log.error("No VCF ##reference found, defaulting to " + + DEFAULT_REFERENCE + ":" + DEFAULT_SPECIES); + reference = DEFAULT_REFERENCE; // default to GRCh37 if not specified + } + reference = reference.toLowerCase(); + + /* + * for a non-human species, or other assembly identifier, + * specify as a Jalview property file entry e.g. + * VCF_ASSEMBLY = hs37=GRCh37,assembly19=GRCh37 + * VCF_SPECIES = c_elegans=celegans + * to map a token in the reference header to a value + */ + String prop = Cache.getDefault(VCF_ASSEMBLY, DEFAULT_VCF_ASSEMBLY); + for (String token : prop.split(",")) + { + String[] tokens = token.split("="); + if (tokens.length == 2) + { + if (reference.contains(tokens[0].trim().toLowerCase())) + { + vcfAssembly = tokens[1].trim(); + break; + } + } + } + + vcfSpecies = DEFAULT_SPECIES; + prop = Cache.getProperty(VCF_SPECIES); + if (prop != null) + { + for (String token : prop.split(",")) + { + String[] tokens = token.split("="); + if (tokens.length == 2) + { + if (reference.contains(tokens[0].trim().toLowerCase())) + { + vcfSpecies = tokens[1].trim(); + break; + } + } + } + } + } + + /** * Opens the VCF file and parses header data * * @param filePath @@ -563,7 +680,8 @@ public class VCFLoader /* * dna-to-peptide product mapping */ - AlignmentUtils.computeProteinFeatures(seq, mapTo, map); + // JAL-3187 render on the fly instead + // AlignmentUtils.computeProteinFeatures(seq, mapTo, map); } else { @@ -588,12 +706,11 @@ public class VCFLoader * and returns the number of variant features added * * @param seq - * @param vcfAssembly * @return */ - protected int loadSequenceVCF(SequenceI seq, String vcfAssembly) + protected int loadSequenceVCF(SequenceI seq) { - VCFMap vcfMap = getVcfMap(seq, vcfAssembly); + VCFMap vcfMap = getVcfMap(seq); if (vcfMap == null) { return 0; @@ -614,10 +731,9 @@ public class VCFLoader * Answers a map from sequence coordinates to VCF chromosome ranges * * @param seq - * @param vcfAssembly * @return */ - private VCFMap getVcfMap(SequenceI seq, String vcfAssembly) + private VCFMap getVcfMap(SequenceI seq) { /* * simplest case: sequence has id and length matching a VCF contig @@ -648,34 +764,28 @@ public class VCFLoader String species = seqCoords.getSpeciesId(); String chromosome = seqCoords.getChromosomeId(); String seqRef = seqCoords.getAssemblyId(); - MapList map = seqCoords.getMap(); + MapList map = seqCoords.getMapping(); - if (!vcfSpeciesMatchesSequence(vcfAssembly, species)) + // note this requires the configured species to match that + // returned with the Ensembl sequence; todo: support aliases? + if (!vcfSpecies.equalsIgnoreCase(species)) { + Cache.log.warn("No VCF loaded to " + seq.getName() + + " as species not matched"); return null; } - if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef)) + if (seqRef.equalsIgnoreCase(vcfAssembly)) { return new VCFMap(chromosome, map); } - if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl - || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD - { - return null; - } - /* - * map chromosomal coordinates from sequence to VCF if the VCF - * data has a different reference assembly to the sequence + * VCF data has a different reference assembly to the sequence: + * query Ensembl to map chromosomal coordinates from sequence to VCF */ - // TODO generalise for cases other than GRCh38 -> GRCh37 ! - // - or get the user to choose in a dialog - List toVcfRanges = new ArrayList<>(); List fromSequenceRanges = new ArrayList<>(); - String toRef = "GRCh37"; for (int[] range : map.getToRanges()) { @@ -687,12 +797,13 @@ public class VCFLoader } int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef, - toRef); + vcfAssembly); if (newRange == null) { Cache.log.error( String.format("Failed to map %s:%s:%s:%d:%d to %s", species, - chromosome, seqRef, range[0], range[1], toRef)); + chromosome, seqRef, range[0], range[1], + vcfAssembly)); continue; } else @@ -733,62 +844,6 @@ public class VCFLoader } /** - * Answers true if we determine that the VCF data uses the same reference - * assembly as the sequence, else false - * - * @param vcfAssembly - * @param seqRef - * @return - */ - private boolean vcfAssemblyMatchesSequence(String vcfAssembly, - String seqRef) - { - // TODO improve on this stub, which handles gnomAD and - // hopes for the best for other cases - - if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl - && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD - { - return false; - } - return true; - } - - /** - * Answers true if the species inferred from the VCF reference identifier - * matches that for the sequence - * - * @param vcfAssembly - * @param speciesId - * @return - */ - boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId) - { - // PROBLEM 1 - // there are many aliases for species - how to equate one with another? - // PROBLEM 2 - // VCF ##reference header is an unstructured URI - how to extract species? - // perhaps check if ref includes any (Ensembl) alias of speciesId?? - // TODO ask the user to confirm this?? - - if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example - && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id - { - return true; - } - - if (vcfAssembly.contains("c_elegans") // VEP VCF response example - && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl - { - return true; - } - - // this is not a sustainable solution... - - return false; - } - - /** * Queries the VCF reader for any variants that overlap the mapped chromosome * ranges of the sequence, and adds as variant features. Returns the number of * overlapping variants found. @@ -811,24 +866,35 @@ public class VCFLoader { int vcfStart = Math.min(range[0], range[1]); int vcfEnd = Math.max(range[0], range[1]); - CloseableIterator variants = reader - .query(map.chromosome, vcfStart, vcfEnd); - while (variants.hasNext()) + try { - VariantContext variant = variants.next(); + CloseableIterator variants = reader + .query(map.chromosome, vcfStart, vcfEnd); + while (variants.hasNext()) + { + VariantContext variant = variants.next(); - int[] featureRange = map.map.locateInFrom(variant.getStart(), - variant.getEnd()); + int[] featureRange = map.map.locateInFrom(variant.getStart(), + variant.getEnd()); - if (featureRange != null) - { - int featureStart = Math.min(featureRange[0], featureRange[1]); - int featureEnd = Math.max(featureRange[0], featureRange[1]); - count += addAlleleFeatures(seq, variant, featureStart, featureEnd, - forwardStrand); + if (featureRange != null) + { + int featureStart = Math.min(featureRange[0], featureRange[1]); + int featureEnd = Math.max(featureRange[0], featureRange[1]); + count += addAlleleFeatures(seq, variant, featureStart, + featureEnd, forwardStrand); + } } + variants.close(); + } catch (TribbleException e) + { + /* + * RuntimeException throwable by htsjdk + */ + String msg = String.format("Error reading VCF for %s:%d-%d: %s ", + map.chromosome, vcfStart, vcfEnd); + Cache.log.error(msg); } - variants.close(); } return count; @@ -958,7 +1024,20 @@ public class VCFLoader featureEnd, FEATURE_GROUP_VCF); sf.setSource(sourceId); - sf.setValue(Gff3Helper.ALLELES, alleles); + /* + * save the derived alleles as a named attribute; this will be + * needed when Jalview computes derived peptide variants + */ + addFeatureAttribute(sf, Gff3Helper.ALLELES, alleles); + + /* + * add selected VCF fixed column data as feature attributes + */ + addFeatureAttribute(sf, VCF_POS, String.valueOf(variant.getStart())); + addFeatureAttribute(sf, VCF_ID, variant.getID()); + addFeatureAttribute(sf, VCF_QUAL, + String.valueOf(variant.getPhredScaledQual())); + addFeatureAttribute(sf, VCF_FILTER, getFilter(variant)); addAlleleProperties(variant, sf, altAlleleIndex, consequence); @@ -968,6 +1047,53 @@ public class VCFLoader } /** + * Answers the VCF FILTER value for the variant - or an approximation to it. + * This field is either PASS, or a semi-colon separated list of filters not + * passed. htsjdk saves filters as a HashSet, so the order when reassembled into + * a list may be different. + * + * @param variant + * @return + */ + String getFilter(VariantContext variant) + { + Set filters = variant.getFilters(); + if (filters.isEmpty()) + { + return NO_VALUE; + } + Iterator iterator = filters.iterator(); + String first = iterator.next(); + if (filters.size() == 1) + { + return first; + } + + StringBuilder sb = new StringBuilder(first); + while (iterator.hasNext()) + { + sb.append(";").append(iterator.next()); + } + + return sb.toString(); + } + + /** + * Adds one feature attribute unless the value is null, empty or '.' + * + * @param sf + * @param key + * @param value + */ + void addFeatureAttribute(SequenceFeature sf, String key, String value) + { + if (value != null && !value.isEmpty() && !NO_VALUE.equals(value)) + { + sf.setValue(key, value); + } + } + + /** * Determines the Sequence Ontology term to use for the variant feature type in * Jalview. The default is 'sequence_variant', but a more specific term is used * if: @@ -1161,14 +1287,6 @@ public class VCFLoader } /* - * filter out fields we don't want to capture - */ - if (!vcfFieldsOfInterest.contains(key)) - { - continue; - } - - /* * we extract values for other data which are allele-specific; * these may be per alternate allele (INFO[key].Number = 'A') * or per allele including reference (INFO[key].Number = 'R') @@ -1205,10 +1323,81 @@ public class VCFLoader * take the index'th value */ String value = getAttributeValue(variant, key, index); - if (value != null) + if (value != null && isValid(variant, key, value)) + { + /* + * decode colon, semicolon, equals sign, percent sign, comma (only) + * as required by the VCF specification (para 1.2) + */ + value = StringUtils.urlDecode(value, VCF_ENCODABLE); + addFeatureAttribute(sf, key, value); + } + } + } + + /** + * Answers true for '.', null, or an empty value, or if the INFO type is String. + * If the INFO type is Integer or Float, answers false if the value is not in + * valid format. + * + * @param variant + * @param infoId + * @param value + * @return + */ + protected boolean isValid(VariantContext variant, String infoId, + String value) + { + if (value == null || value.isEmpty() || NO_VALUE.equals(value)) + { + return true; + } + VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(infoId); + if (infoHeader == null) + { + Cache.log.error("Field " + infoId + " has no INFO header"); + return false; + } + VCFHeaderLineType infoType = infoHeader.getType(); + try + { + if (infoType == VCFHeaderLineType.Integer) { - sf.setValue(key, value); + Integer.parseInt(value); } + else if (infoType == VCFHeaderLineType.Float) + { + Float.parseFloat(value); + } + } catch (NumberFormatException e) + { + logInvalidValue(variant, infoId, value); + return false; + } + return true; + } + + /** + * Logs an error message for malformed data; duplicate messages (same id and + * value) are not logged + * + * @param variant + * @param infoId + * @param value + */ + private void logInvalidValue(VariantContext variant, String infoId, + String value) + { + if (badData == null) + { + badData = new HashSet<>(); + } + String token = infoId + ":" + value; + if (!badData.contains(token)) + { + badData.add(token); + Cache.log.error(String.format("Invalid VCF data at %s:%d %s=%s", + variant.getContig(), variant.getStart(), infoId, value)); } } @@ -1260,6 +1449,11 @@ public class VCFLoader String id = vepFieldsOfInterest.get(i); if (id != null) { + /* + * VCF spec requires encoding of special characters e.g. '=' + * so decode them here before storing + */ + field = StringUtils.urlDecode(field, VCF_ENCODABLE); csqValues.put(id, field); } } diff --git a/src/jalview/project/Jalview2XML.java b/src/jalview/project/Jalview2XML.java index 133ab57..d46e9a6 100644 --- a/src/jalview/project/Jalview2XML.java +++ b/src/jalview/project/Jalview2XML.java @@ -6011,7 +6011,7 @@ public class Jalview2XML private jalview.datamodel.Mapping addMapping(Mapping m) { SequenceI dsto = null; - // Mapping m = dr.getMapping(); + // Mapping m = dr.getMap(); int fr[] = new int[m.getMapListFrom().size() * 2]; Iterator from = m.getMapListFrom().iterator();// enumerateMapListFrom(); for (int _i = 0; from.hasNext(); _i += 2) diff --git a/src/jalview/structure/SequenceListener.java b/src/jalview/structure/SequenceListener.java index 81ff739..f8c5bea 100644 --- a/src/jalview/structure/SequenceListener.java +++ b/src/jalview/structure/SequenceListener.java @@ -28,7 +28,15 @@ public interface SequenceListener // TODO remove this? never called on SequenceListener type public void mouseOverSequence(SequenceI sequence, int index, int pos); - public void highlightSequence(SearchResultsI results); + /** + * Highlights any position(s) represented by the search results and + * (optionally) returns an informative message about the position(s) + * higlighted + * + * @param results + * @return + */ + public String highlightSequence(SearchResultsI results); // TODO remove this? never called public void updateColours(SequenceI sequence, int index); diff --git a/src/jalview/structure/StructureSelectionManager.java b/src/jalview/structure/StructureSelectionManager.java index 82d66f2..d6eec47 100644 --- a/src/jalview/structure/StructureSelectionManager.java +++ b/src/jalview/structure/StructureSelectionManager.java @@ -43,6 +43,7 @@ import jalview.io.DataSourceType; import jalview.io.StructureFile; import jalview.util.MappingUtils; import jalview.util.MessageManager; +import jalview.util.Platform; import jalview.ws.sifts.SiftsClient; import jalview.ws.sifts.SiftsException; import jalview.ws.sifts.SiftsSettings; @@ -890,13 +891,14 @@ public class StructureSelectionManager implements ApplicationSingletonI * @param pdbResNum * @param chain * @param pdbfile + * @return */ - public void mouseOverStructure(int pdbResNum, String chain, + public String mouseOverStructure(int pdbResNum, String chain, String pdbfile) { AtomSpec atomSpec = new AtomSpec(pdbfile, chain, pdbResNum, 0); List atoms = Collections.singletonList(atomSpec); - mouseOverStructure(atoms); + return mouseOverStructure(atoms); } /** @@ -904,12 +906,12 @@ public class StructureSelectionManager implements ApplicationSingletonI * * @param atoms */ - public void mouseOverStructure(List atoms) + public String mouseOverStructure(List atoms) { if (listeners == null) { // old or prematurely sent event - return; + return null; } boolean hasSequenceListener = false; for (int i = 0; i < listeners.size(); i++) @@ -921,18 +923,24 @@ public class StructureSelectionManager implements ApplicationSingletonI } if (!hasSequenceListener) { - return; + return null; } SearchResultsI results = findAlignmentPositionsForStructurePositions( atoms); + String result = null; for (Object li : listeners) { if (li instanceof SequenceListener) { - ((SequenceListener) li).highlightSequence(results); + String s = ((SequenceListener) li).highlightSequence(results); + if (s != null) + { + result = s; + } } } + return result; } /** @@ -1202,7 +1210,8 @@ public class StructureSelectionManager implements ApplicationSingletonI StringBuilder sb = new StringBuilder(64); for (StructureMapping sm : mappings) { - if (sm.pdbfile.equals(pdbfile) && seqs.contains(sm.sequence)) + if (Platform.pathEquals(sm.pdbfile, pdbfile) + && seqs.contains(sm.sequence)) { sb.append(sm.mappingDetails); sb.append(NEWLINE); @@ -1264,22 +1273,43 @@ public class StructureSelectionManager implements ApplicationSingletonI } /** - * Reset this object to its initial state by removing all registered - * listeners, codon mappings, PDB file mappings. - * - * Called only by Desktop and testng. - * + * Resets this object to its initial state by removing all registered + * listeners, codon mappings, PDB file mappings */ public void resetAll() { - mappings.clear(); - seqmappings.clear(); - sel_listeners.clear(); - listeners.clear(); - commandListeners.clear(); - view_listeners.clear(); - pdbFileNameId.clear(); - pdbIdFileName.clear(); + if (mappings != null) + { + mappings.clear(); + } + if (seqmappings != null) + { + seqmappings.clear(); + } + if (sel_listeners != null) + { + sel_listeners.clear(); + } + if (listeners != null) + { + listeners.clear(); + } + if (commandListeners != null) + { + commandListeners.clear(); + } + if (view_listeners != null) + { + view_listeners.clear(); + } + if (pdbFileNameId != null) + { + pdbFileNameId.clear(); + } + if (pdbIdFileName != null) + { + pdbIdFileName.clear(); + } } public void addSelectionListener(SelectionListener selecter) diff --git a/src/jalview/util/MapList.java b/src/jalview/util/MapList.java index 9f28ee1..731e976 100644 --- a/src/jalview/util/MapList.java +++ b/src/jalview/util/MapList.java @@ -1236,4 +1236,14 @@ public class MapList return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio); } + /** + * Answers true if the mapping is from one contiguous range to another, else + * false + * + * @return + */ + public boolean isContiguous() + { + return fromShifts.size() == 1 && toShifts.size() == 1; + } } diff --git a/src/jalview/util/Platform.java b/src/jalview/util/Platform.java index 99c82a4..d32735b 100644 --- a/src/jalview/util/Platform.java +++ b/src/jalview/util/Platform.java @@ -133,6 +133,7 @@ public class Platform /** * * @return true if we are running in non-interactive no UI mode + * based on System.getProperty("java.awt.headless") */ public static boolean isHeadless() { @@ -847,4 +848,22 @@ public class Platform return f.toString(); } + private static float javaVersion; + + public static float getJavaVersion() + { + if (javaVersion == 0) + { + try + { + return javaVersion = Float.parseFloat( + System.getProperty("java.specification.version")); + } catch (Exception e) + { + javaVersion = 1.8f; + } + } + return javaVersion; + } + } diff --git a/src/jalview/util/StringUtils.java b/src/jalview/util/StringUtils.java index 2cbbfbf..a05dc95 100644 --- a/src/jalview/util/StringUtils.java +++ b/src/jalview/util/StringUtils.java @@ -20,6 +20,8 @@ */ package jalview.util; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -29,8 +31,16 @@ public class StringUtils private static final Pattern DELIMITERS_PATTERN = Pattern .compile(".*='[^']*(?!')"); + private static final char PERCENT = '%'; + private static final boolean DEBUG = false; + /* + * URL encoded characters, indexed by char value + * e.g. urlEncodings['='] = urlEncodings[61] = "%3D" + */ + private static String[] urlEncodings = new String[255]; + /** * Returns a new character array, after inserting characters into the given * character array. @@ -446,8 +456,66 @@ public class StringUtils } /** - * Answers true if the string is not empty and consists only of digits, or - * characters 'a'-'f' or 'A'-'F', else false + * Answers the input string with any occurrences of the 'encodeable' characters + * replaced by their URL encoding + * + * @param s + * @param encodable + * @return + */ + public static String urlEncode(String s, String encodable) + { + if (s == null || s.isEmpty()) + { + return s; + } + + /* + * do % encoding first, as otherwise it may double-encode! + */ + if (encodable.indexOf(PERCENT) != -1) + { + s = urlEncode(s, PERCENT); + } + + for (char c : encodable.toCharArray()) + { + if (c != PERCENT) + { + s = urlEncode(s, c); + } + } + return s; + } + + /** + * Answers the input string with any occurrences of {@code c} replaced with + * their url encoding. Answers the input string if it is unchanged. + * + * @param s + * @param c + * @return + */ + static String urlEncode(String s, char c) + { + String decoded = String.valueOf(c); + if (s.indexOf(decoded) != -1) + { + String encoded = getUrlEncoding(c); + if (!encoded.equals(decoded)) + { + s = s.replace(decoded, encoded); + } + } + return s; + } + + /** + * Answers the input string with any occurrences of the specified (unencoded) + * characters replaced by their URL decoding. + *

    + * Example: {@code urlDecode("a%3Db%3Bc", "-;=,")} should answer + * {@code "a=b;c"}. * * @param s * @return @@ -470,4 +538,64 @@ public class StringUtils } return true; } + /** + * Answers the input string with any occurrences of the specified (unencoded) + * characters replaced by their URL decoding. + *

    + * Example: {@code urlDecode("a%3Db%3Bc", "-;=,")} should answer + * {@code "a=b;c"}. + * + * @param s + * @param encodable + * @return + */ + public static String urlDecode(String s, String encodable) + { + if (s == null || s.isEmpty()) + { + return s; + } + + for (char c : encodable.toCharArray()) + { + String encoded = getUrlEncoding(c); + if (s.indexOf(encoded) != -1) + { + String decoded = String.valueOf(c); + s = s.replace(encoded, decoded); + } + } + return s; + } + + /** + * Does a lazy lookup of the url encoding of the given character, saving the + * value for repeat lookups + * + * @param c + * @return + */ + private static String getUrlEncoding(char c) + { + if (c < 0 || c >= urlEncodings.length) + { + return String.valueOf(c); + } + + String enc = urlEncodings[c]; + if (enc == null) + { + try + { + enc = urlEncodings[c] = URLEncoder.encode(String.valueOf(c), + "UTF-8"); + } catch (UnsupportedEncodingException e) + { + enc = urlEncodings[c] = String.valueOf(c); + } + } + return enc; + } + + } diff --git a/src/jalview/viewmodel/AlignmentViewport.java b/src/jalview/viewmodel/AlignmentViewport.java index 21f9dae..b1f595f 100644 --- a/src/jalview/viewmodel/AlignmentViewport.java +++ b/src/jalview/viewmodel/AlignmentViewport.java @@ -2747,6 +2747,30 @@ public abstract class AlignmentViewport viewStyle.setProteinFontAsCdna(b); } + @Override + public void setShowComplementFeatures(boolean b) + { + viewStyle.setShowComplementFeatures(b); + } + + @Override + public boolean isShowComplementFeatures() + { + return viewStyle.isShowComplementFeatures(); + } + + @Override + public void setShowComplementFeaturesOnTop(boolean b) + { + viewStyle.setShowComplementFeaturesOnTop(b); + } + + @Override + public boolean isShowComplementFeaturesOnTop() + { + return viewStyle.isShowComplementFeaturesOnTop(); + } + /** * @return true if view should scroll to show the highlighted region of a * sequence diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java index 8219c6a..5ca3ac5 100644 --- a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -20,18 +20,6 @@ */ package jalview.viewmodel.seqfeatures; -import jalview.api.AlignViewportI; -import jalview.api.FeatureColourI; -import jalview.api.FeaturesDisplayedI; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.SequenceFeature; -import jalview.datamodel.SequenceI; -import jalview.datamodel.features.FeatureMatcherSetI; -import jalview.datamodel.features.SequenceFeatures; -import jalview.renderer.seqfeatures.FeatureRenderer; -import jalview.schemes.FeatureColour; -import jalview.util.ColorUtils; - import java.awt.Color; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; @@ -47,6 +35,25 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import jalview.api.AlignViewportI; +import jalview.api.FeatureColourI; +import jalview.api.FeaturesDisplayedI; +import jalview.datamodel.AlignedCodonFrame; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.MappedFeatures; +import jalview.datamodel.Mapping; +import jalview.datamodel.SearchResultMatchI; +import jalview.datamodel.SearchResults; +import jalview.datamodel.SearchResultsI; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.datamodel.features.SequenceFeatures; +import jalview.renderer.seqfeatures.FeatureRenderer; +import jalview.schemes.FeatureColour; +import jalview.util.ColorUtils; +import jalview.util.Platform; + public abstract class FeatureRendererModel implements jalview.api.FeatureRenderer { @@ -324,6 +331,8 @@ public abstract class FeatureRendererModel * include features unless their feature group is not displayed, or * they are hidden (have no colour) based on a filter or colour threshold */ + + // BH! check -- !featureGroupNotShown(sf) is from applet branch. for (SequenceFeature sf : features) { if (!featureGroupNotShown(sf) && getColour(sf) != null) @@ -653,7 +662,7 @@ public abstract class FeatureRendererModel { featureOrder = new Hashtable<>(); } - featureOrder.put(type, new Float(position)); + featureOrder.put(type, Float.valueOf(position)); return position; } @@ -853,7 +862,7 @@ public abstract class FeatureRendererModel } if (newGroupsVisible) { - featureGroups.put(group, new Boolean(true)); + featureGroups.put(group, Boolean.valueOf(true)); return true; } return false; @@ -889,7 +898,7 @@ public abstract class FeatureRendererModel @Override public void setGroupVisibility(String group, boolean visible) { - featureGroups.put(group, new Boolean(visible)); + featureGroups.put(group, Boolean.valueOf(visible)); } @Override @@ -901,7 +910,7 @@ public abstract class FeatureRendererModel for (String gst : toset) { Boolean st = featureGroups.get(gst); - featureGroups.put(gst, new Boolean(visible)); + featureGroups.put(gst, Boolean.valueOf(visible)); if (st != null) { rdrw = rdrw || (visible != st.booleanValue()); @@ -984,14 +993,14 @@ public abstract class FeatureRendererModel * @param sequenceFeature * @return */ - protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature) + public boolean featureGroupNotShown(final SequenceFeature sequenceFeature) { - Boolean b; return featureGroups != null && sequenceFeature.featureGroup != null - && sequenceFeature.featureGroup.length() > 0 - && (b = featureGroups.get(sequenceFeature.featureGroup)) != null - && !b.booleanValue(); + && sequenceFeature.featureGroup.length() != 0 + && featureGroups.containsKey(sequenceFeature.featureGroup) + && !featureGroups.get(sequenceFeature.featureGroup) + .booleanValue(); } /** @@ -999,7 +1008,7 @@ public abstract class FeatureRendererModel */ @Override public List findFeaturesAtResidue(SequenceI sequence, - int resNo) + int fromResNo, int toResNo) { List result = new ArrayList<>(); if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) @@ -1012,12 +1021,11 @@ public abstract class FeatureRendererModel * displayed, and feature group is null or the empty string * or marked for display */ - Set visibleFeatures = getFeaturesDisplayed() - .getVisibleFeatures(); + List visibleFeatures = getDisplayedFeatureTypes(); String[] visibleTypes = visibleFeatures .toArray(new String[visibleFeatures.size()]); List features = sequence.getFeatures().findFeatures( - resNo, resNo, visibleTypes); + fromResNo, toResNo, visibleTypes); for (SequenceFeature sf : features) { @@ -1040,6 +1048,16 @@ public abstract class FeatureRendererModel */ public void filterFeaturesForDisplay(List features) { +// BH! check -- what was the problem here? How is JalviewJS's IntervalStore different from + // other IntervalStore? + /* + * fudge: JalviewJS's IntervalStore lacks the sort method called :-( + */ + if (Platform.isJS()) + { + return; + } + /* * don't remove 'redundant' features if * - transparency is applied (feature count affects depth of feature colour) @@ -1185,6 +1203,99 @@ public abstract class FeatureRendererModel } @Override + public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence, + int pos) + { + SequenceI ds = sequence.getDatasetSequence(); + if (ds == null) + { + ds = sequence; + } + final char residue = ds.getCharAt(pos - ds.getStart()); + + List found = new ArrayList<>(); + List mappings = this.av.getAlignment() + .getCodonFrame(sequence); + + /* + * fudge: if no mapping found, check the complementary alignment + * todo: only store in one place? StructureSelectionManager? + */ + if (mappings.isEmpty()) + { + mappings = this.av.getCodingComplement().getAlignment() + .getCodonFrame(sequence); + } + + /* + * todo: direct lookup of CDS for peptide and vice-versa; for now, + * have to search through an unordered list of mappings for a candidate + */ + Mapping mapping = null; + SequenceI mapFrom = null; + + for (AlignedCodonFrame acf : mappings) + { + mapping = acf.getMappingForSequence(sequence); + if (mapping == null || !mapping.getMap().isTripletMap()) + { + continue; // we are only looking for 3:1 or 1:3 mappings + } + SearchResultsI sr = new SearchResults(); + acf.markMappedRegion(ds, pos, sr); + for (SearchResultMatchI match : sr.getResults()) + { + int fromRes = match.getStart(); + int toRes = match.getEnd(); + mapFrom = match.getSequence(); + List fs = findFeaturesAtResidue( + mapFrom, fromRes, toRes); + for (SequenceFeature sf : fs) + { + if (!found.contains(sf)) + { + found.add(sf); + } + } + } + + /* + * just take the first mapped features we find + */ + if (!found.isEmpty()) + { + break; + } + } + if (found.isEmpty()) + { + return null; + } + + /* + * sort by renderorder, inefficiently + */ + List result = new ArrayList<>(); + for (String type : renderOrder) + { + for (SequenceFeature sf : found) + { + if (type.equals(sf.getType())) + { + result.add(sf); + if (result.size() == found.size()) + { + return new MappedFeatures(mapping, mapFrom, pos, residue, + result); + } + } + } + } + + return new MappedFeatures(mapping, mapFrom, pos, residue, result); + } + + @Override public boolean isVisible(SequenceFeature feature) { if (feature == null) diff --git a/src/jalview/viewmodel/styles/ViewStyle.java b/src/jalview/viewmodel/styles/ViewStyle.java index 16aa580..91f2f0c 100644 --- a/src/jalview/viewmodel/styles/ViewStyle.java +++ b/src/jalview/viewmodel/styles/ViewStyle.java @@ -213,6 +213,8 @@ public class ViewStyle implements ViewStyleI setShowNPFeats(vs.isShowNPFeats()); setShowSequenceFeaturesHeight(vs.isShowSequenceFeaturesHeight()); setShowSequenceFeatures(vs.isShowSequenceFeatures()); + setShowComplementFeatures(vs.isShowComplementFeatures()); + setShowComplementFeaturesOnTop(vs.isShowComplementFeaturesOnTop()); setShowText(vs.getShowText()); setShowUnconserved(vs.getShowUnconserved()); setTextColour(vs.getTextColour()); @@ -275,6 +277,9 @@ public class ViewStyle implements ViewStyleI && isShowSequenceFeaturesHeight() == vs .isShowSequenceFeaturesHeight() && isShowSequenceFeatures() == vs.isShowSequenceFeatures() + && isShowComplementFeatures() == vs.isShowComplementFeatures() + && isShowComplementFeaturesOnTop() == vs + .isShowComplementFeaturesOnTop() && getShowText() == vs.getShowText() && getShowUnconserved() == vs.getShowUnconserved() && getThreshold() == vs.getThreshold() @@ -365,6 +370,10 @@ public class ViewStyle implements ViewStyleI private int fontStyle; + private boolean showComplementFeatures; + + private boolean showComplementFeaturesOnTop; + /** * GUI state * @@ -1111,4 +1120,28 @@ public class ViewStyle implements ViewStyleI { proteinFontAsCdna = b; } + + @Override + public void setShowComplementFeatures(boolean b) + { + showComplementFeatures = b; + } + + @Override + public boolean isShowComplementFeatures() + { + return showComplementFeatures; + } + + @Override + public void setShowComplementFeaturesOnTop(boolean b) + { + showComplementFeaturesOnTop = b; + } + + @Override + public boolean isShowComplementFeaturesOnTop() + { + return showComplementFeaturesOnTop; + } } diff --git a/src/jalview/ws/DBRefFetcher.java b/src/jalview/ws/DBRefFetcher.java index 7745b74..736d8cb 100644 --- a/src/jalview/ws/DBRefFetcher.java +++ b/src/jalview/ws/DBRefFetcher.java @@ -21,6 +21,7 @@ package jalview.ws; import jalview.analysis.AlignSeq; +import jalview.api.FeatureSettingsModelI; import jalview.bin.Cache; import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefEntry; @@ -40,8 +41,10 @@ import jalview.ws.seqfetcher.DbSourceProxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.StringTokenizer; import java.util.Vector; @@ -318,6 +321,9 @@ public class DBRefFetcher implements Runnable Arrays.asList(dataset)); List warningMessages = new ArrayList<>(); + // clear any old feature display settings recorded from past sessions + featureDisplaySettings = null; + int db = 0; while (sdataset.size() > 0 && db < dbSources.length) { @@ -384,7 +390,7 @@ public class DBRefFetcher implements Runnable } if (retrieved != null) { - transferReferences(sdataset, dbsource.getDbSource(), retrieved, + transferReferences(sdataset, dbsource, retrieved, trimDsSeqs, warningMessages); } } @@ -513,7 +519,8 @@ public class DBRefFetcher implements Runnable * @param warningMessages * a list of messages to add to */ - boolean transferReferences(Vector sdataset, String dbSource, + boolean transferReferences(Vector sdataset, + DbSourceProxy dbSourceProxy, AlignmentI retrievedAl, boolean trimDatasetSeqs, List warningMessages) { @@ -523,6 +530,7 @@ public class DBRefFetcher implements Runnable return false; } + String dbSource = dbSourceProxy.getDbName(); boolean modified = false; SequenceI[] retrieved = recoverDbSequences( retrievedAl.getSequencesArray()); @@ -594,6 +602,10 @@ public class DBRefFetcher implements Runnable * seqs.elementAt(jj); if (!sequenceMatches.contains(sequence)) { * sequenceMatches.addElement(sequence); } } } } */ + if (sequenceMatches.size() > 0) + { + addFeatureSettings(dbSourceProxy); + } // sequenceMatches now contains the set of all sequences associated with // the returned db record final String retrievedSeqString = retrievedSeq.getSequenceAsString(); @@ -762,6 +774,33 @@ public class DBRefFetcher implements Runnable return modified; } + Map featureDisplaySettings = null; + + private void addFeatureSettings(DbSourceProxy dbSourceProxy) + { + FeatureSettingsModelI fsettings = dbSourceProxy + .getFeatureColourScheme(); + if (fsettings != null) + { + if (featureDisplaySettings == null) + { + featureDisplaySettings = new HashMap<>(); + } + featureDisplaySettings.put(dbSourceProxy.getDbName(), fsettings); + } + } + + /** + * + * @return any feature settings associated with sources that have provided sequences + */ + public ListgetFeatureSettingsModels() + { + return featureDisplaySettings == null + ? Arrays.asList(new FeatureSettingsModelI[0]) + : Arrays.asList(featureDisplaySettings.values() + .toArray(new FeatureSettingsModelI[1])); + } /** * Adds the message to the list unless it already contains it * @@ -786,9 +825,7 @@ public class DBRefFetcher implements Runnable { int n; if (sequencesArray == null || (n = sequencesArray.length) == 0) - { - return sequencesArray; - } + return sequencesArray; ArrayList nseq = new ArrayList<>(); for (int i = 0;i < n; i++) { diff --git a/src/jalview/ws/dbsources/EmblXmlSource.java b/src/jalview/ws/dbsources/EmblXmlSource.java index b03fc71..19366e0 100644 --- a/src/jalview/ws/dbsources/EmblXmlSource.java +++ b/src/jalview/ws/dbsources/EmblXmlSource.java @@ -40,6 +40,7 @@ import jalview.ws.ebi.EBIFetchClient; import jalview.xml.binding.embl.EntryType; import jalview.xml.binding.embl.EntryType.Feature; import jalview.xml.binding.embl.EntryType.Feature.Qualifier; +import jalview.xml.binding.jalview.JalviewModel; import jalview.xml.binding.embl.ROOT; import jalview.xml.binding.embl.XrefType; @@ -53,7 +54,6 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.regex.Pattern; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; @@ -70,8 +70,6 @@ public abstract class EmblXmlSource extends EbiFileRetrievedProxy */ private static final String EMBL_NOT_FOUND_REPLY = "ERROR 12 No entries found."; - private static final Pattern SPACE_PATTERN = Pattern.compile(" "); - public EmblXmlSource() { super(); @@ -652,8 +650,6 @@ public abstract class EmblXmlSource extends EbiFileRetrievedProxy try { - // BH 2019.10.06 note: exo-location of "X53828.1:60..1058" causes - // RemoteFormatTest to fail at this point. List ranges = DnaUtils.parseLocation(location); return listToArray(ranges); } catch (ParseException e) @@ -707,19 +703,10 @@ public abstract class EmblXmlSource extends EbiFileRetrievedProxy SequenceFeature sf = new SequenceFeature(type, desc, begin, end, group); if (!vals.isEmpty()) { - StringBuilder sb = new StringBuilder(); - boolean first = true; for (Entry val : vals.entrySet()) { - if (!first) - { - sb.append(";"); - } - sb.append(val.getKey()).append("=").append(val.getValue()); - first = false; sf.setValue(val.getKey(), val.getValue()); } - sf.setAttributes(sb.toString()); } return sf; } diff --git a/swingjs/SwingJS-site.zip b/swingjs/SwingJS-site.zip index 78d190b..4c4f9c3 100644 Binary files a/swingjs/SwingJS-site.zip and b/swingjs/SwingJS-site.zip differ diff --git a/swingjs/net.sf.j2s.core-j11.jar b/swingjs/net.sf.j2s.core-j11.jar index 303fea5..0c81ba1 100644 Binary files a/swingjs/net.sf.j2s.core-j11.jar and b/swingjs/net.sf.j2s.core-j11.jar differ diff --git a/swingjs/timestamp b/swingjs/timestamp index 0d6b0a8..5a41b05 100644 --- a/swingjs/timestamp +++ b/swingjs/timestamp @@ -1 +1 @@ -20200408084722 +20200409124432 diff --git a/swingjs/ver/3.2.9/SwingJS-site.zip b/swingjs/ver/3.2.9/SwingJS-site.zip index 78d190b..4c4f9c3 100644 Binary files a/swingjs/ver/3.2.9/SwingJS-site.zip and b/swingjs/ver/3.2.9/SwingJS-site.zip differ diff --git a/swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar b/swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar index 303fea5..0c81ba1 100644 Binary files a/swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar and b/swingjs/ver/3.2.9/net.sf.j2s.core-j11.jar differ diff --git a/swingjs/ver/3.2.9/timestamp b/swingjs/ver/3.2.9/timestamp index 0d6b0a8..5a41b05 100644 --- a/swingjs/ver/3.2.9/timestamp +++ b/swingjs/ver/3.2.9/timestamp @@ -1 +1 @@ -20200408084722 +20200409124432 diff --git a/test/jalview/analysis/AlignmentUtilsTests.java b/test/jalview/analysis/AlignmentUtilsTests.java index cc438be..35e4017 100644 --- a/test/jalview/analysis/AlignmentUtilsTests.java +++ b/test/jalview/analysis/AlignmentUtilsTests.java @@ -975,7 +975,7 @@ public class AlignmentUtilsTests assertTrue(AlignmentUtils.haveCrossRef(seq2, seq1)); // now the other way round - seq1.setDBRefs(null); + seq1.setDBRefs(null); seq2.addDBRef(new DBRefEntry("EMBL", "1", "A12345")); assertTrue(AlignmentUtils.haveCrossRef(seq1, seq2)); assertTrue(AlignmentUtils.haveCrossRef(seq2, seq1)); @@ -1871,408 +1871,6 @@ public class AlignmentUtilsTests } /** - * Test the method that computes a map of codon variants for each protein - * position from "sequence_variant" features on dna - */ - @Test(groups = "Functional") - public void testBuildDnaVariantsMap() - { - SequenceI dna = new Sequence("dna", "atgAAATTTGGGCCCtag"); - MapList map = new MapList(new int[] { 1, 18 }, new int[] { 1, 5 }, 3, 1); - - /* - * first with no variants on dna - */ - LinkedHashMap[]> variantsMap = AlignmentUtils - .buildDnaVariantsMap(dna, map); - assertTrue(variantsMap.isEmpty()); - - /* - * single allele codon 1, on base 1 - */ - SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1, - 0f, null); - sf1.setValue("alleles", "T"); - sf1.setValue("ID", "sequence_variant:rs758803211"); - dna.addSequenceFeature(sf1); - - /* - * two alleles codon 2, on bases 2 and 3 (distinct variants) - */ - SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 5, 5, - 0f, null); - sf2.setValue("alleles", "T"); - sf2.setValue("ID", "sequence_variant:rs758803212"); - dna.addSequenceFeature(sf2); - SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 6, 6, - 0f, null); - sf3.setValue("alleles", "G"); - sf3.setValue("ID", "sequence_variant:rs758803213"); - dna.addSequenceFeature(sf3); - - /* - * two alleles codon 3, both on base 2 (one variant) - */ - SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 8, 8, - 0f, null); - sf4.setValue("alleles", "C, G"); - sf4.setValue("ID", "sequence_variant:rs758803214"); - dna.addSequenceFeature(sf4); - - // no alleles on codon 4 - - /* - * alleles on codon 5 on all 3 bases (distinct variants) - */ - SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 13, - 13, 0f, null); - sf5.setValue("alleles", "C, G"); // (C duplicates given base value) - sf5.setValue("ID", "sequence_variant:rs758803215"); - dna.addSequenceFeature(sf5); - SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 14, - 14, 0f, null); - sf6.setValue("alleles", "g, a"); // should force to upper-case - sf6.setValue("ID", "sequence_variant:rs758803216"); - dna.addSequenceFeature(sf6); - - SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15, - 15, 0f, null); - sf7.setValue("alleles", "A, T"); - sf7.setValue("ID", "sequence_variant:rs758803217"); - dna.addSequenceFeature(sf7); - - /* - * build map - expect variants on positions 1, 2, 3, 5 - */ - variantsMap = AlignmentUtils.buildDnaVariantsMap(dna, map); - assertEquals(4, variantsMap.size()); - - /* - * protein residue 1: variant on codon (ATG) base 1, not on 2 or 3 - */ - List[] pep1Variants = variantsMap.get(1); - assertEquals(3, pep1Variants.length); - assertEquals(1, pep1Variants[0].size()); - assertEquals("A", pep1Variants[0].get(0).base); // codon[1] base - assertSame(sf1, pep1Variants[0].get(0).variant); // codon[1] variant - assertEquals(1, pep1Variants[1].size()); - assertEquals("T", pep1Variants[1].get(0).base); // codon[2] base - assertNull(pep1Variants[1].get(0).variant); // no variant here - assertEquals(1, pep1Variants[2].size()); - assertEquals("G", pep1Variants[2].get(0).base); // codon[3] base - assertNull(pep1Variants[2].get(0).variant); // no variant here - - /* - * protein residue 2: variants on codon (AAA) bases 2 and 3 - */ - List[] pep2Variants = variantsMap.get(2); - assertEquals(3, pep2Variants.length); - assertEquals(1, pep2Variants[0].size()); - // codon[1] base recorded while processing variant on codon[2] - assertEquals("A", pep2Variants[0].get(0).base); - assertNull(pep2Variants[0].get(0).variant); // no variant here - // codon[2] base and variant: - assertEquals(1, pep2Variants[1].size()); - assertEquals("A", pep2Variants[1].get(0).base); - assertSame(sf2, pep2Variants[1].get(0).variant); - // codon[3] base was recorded when processing codon[2] variant - // and then the variant for codon[3] added to it - assertEquals(1, pep2Variants[2].size()); - assertEquals("A", pep2Variants[2].get(0).base); - assertSame(sf3, pep2Variants[2].get(0).variant); - - /* - * protein residue 3: variants on codon (TTT) base 2 only - */ - List[] pep3Variants = variantsMap.get(3); - assertEquals(3, pep3Variants.length); - assertEquals(1, pep3Variants[0].size()); - assertEquals("T", pep3Variants[0].get(0).base); // codon[1] base - assertNull(pep3Variants[0].get(0).variant); // no variant here - assertEquals(1, pep3Variants[1].size()); - assertEquals("T", pep3Variants[1].get(0).base); // codon[2] base - assertSame(sf4, pep3Variants[1].get(0).variant); // codon[2] variant - assertEquals(1, pep3Variants[2].size()); - assertEquals("T", pep3Variants[2].get(0).base); // codon[3] base - assertNull(pep3Variants[2].get(0).variant); // no variant here - - /* - * three variants on protein position 5 - */ - List[] pep5Variants = variantsMap.get(5); - assertEquals(3, pep5Variants.length); - assertEquals(1, pep5Variants[0].size()); - assertEquals("C", pep5Variants[0].get(0).base); // codon[1] base - assertSame(sf5, pep5Variants[0].get(0).variant); // codon[1] variant - assertEquals(1, pep5Variants[1].size()); - assertEquals("C", pep5Variants[1].get(0).base); // codon[2] base - assertSame(sf6, pep5Variants[1].get(0).variant); // codon[2] variant - assertEquals(1, pep5Variants[2].size()); - assertEquals("C", pep5Variants[2].get(0).base); // codon[3] base - assertSame(sf7, pep5Variants[2].get(0).variant); // codon[3] variant - } - - /** - * Tests for the method that computes all peptide variants given codon - * variants - */ - @Test(groups = "Functional") - public void testComputePeptideVariants() - { - /* - * scenario: AAATTTCCC codes for KFP - * variants: - * GAA -> E source: Ensembl - * CAA -> Q source: dbSNP - * TAA -> STOP source: dnSNP - * AAG synonymous source: COSMIC - * AAT -> N source: Ensembl - * ...TTC synonymous source: dbSNP - * ......CAC,CGC -> H,R source: COSMIC - * (one variant with two alleles) - */ - SequenceI peptide = new Sequence("pep/10-12", "KFP"); - - /* - * two distinct variants for codon 1 position 1 - * second one has clinical significance - */ - String ensembl = "Ensembl"; - String dbSnp = "dbSNP"; - String cosmic = "COSMIC"; - - /* - * NB setting "id" (as returned by Ensembl for features in JSON format); - * previously "ID" (as returned for GFF3 format) - */ - SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1, - 0f, ensembl); - sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E - sf1.setValue("id", "var1.125A>G"); - - SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1, - 0f, dbSnp); - sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q - sf2.setValue("id", "var2"); - sf2.setValue("clinical_significance", "Dodgy"); - - SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1, - 0f, dbSnp); - sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon - sf3.setValue("id", "var3"); - sf3.setValue("clinical_significance", "Bad"); - - SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3, - 0f, cosmic); - sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous - sf4.setValue("id", "var4"); - sf4.setValue("clinical_significance", "None"); - - SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3, - 0f, ensembl); - sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N - sf5.setValue("id", "sequence_variant:var5"); // prefix gets stripped off - sf5.setValue("clinical_significance", "Benign"); - - SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6, - 0f, dbSnp); - sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous - sf6.setValue("id", "var6"); - - SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8, - 0f, cosmic); - sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R - sf7.setValue("id", "var7"); - sf7.setValue("clinical_significance", "Good"); - - List codon1Variants = new ArrayList<>(); - List codon2Variants = new ArrayList<>(); - List codon3Variants = new ArrayList<>(); - - List codonVariants[] = new ArrayList[3]; - codonVariants[0] = codon1Variants; - codonVariants[1] = codon2Variants; - codonVariants[2] = codon3Variants; - - /* - * compute variants for protein position 1 - */ - codon1Variants.add(new DnaVariant("A", sf1)); - codon1Variants.add(new DnaVariant("A", sf2)); - codon1Variants.add(new DnaVariant("A", sf3)); - codon2Variants.add(new DnaVariant("A")); - // codon2Variants.add(new DnaVariant("A")); - codon3Variants.add(new DnaVariant("A", sf4)); - codon3Variants.add(new DnaVariant("A", sf5)); - AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants); - - /* - * compute variants for protein position 2 - */ - codon1Variants.clear(); - codon2Variants.clear(); - codon3Variants.clear(); - codon1Variants.add(new DnaVariant("T")); - codon2Variants.add(new DnaVariant("T")); - codon3Variants.add(new DnaVariant("T", sf6)); - AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants); - - /* - * compute variants for protein position 3 - */ - codon1Variants.clear(); - codon2Variants.clear(); - codon3Variants.clear(); - codon1Variants.add(new DnaVariant("C")); - codon2Variants.add(new DnaVariant("C", sf7)); - codon3Variants.add(new DnaVariant("C")); - AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants); - - /* - * verify added sequence features for - * var1 K -> E Ensembl - * var2 K -> Q dbSNP - * var3 K -> stop - * var4 synonymous - * var5 K -> N Ensembl - * var6 synonymous - * var7 P -> H COSMIC - * var8 P -> R COSMIC - */ - List sfs = peptide.getSequenceFeatures(); - SequenceFeatures.sortFeatures(sfs, true); - assertEquals(8, sfs.size()); - - /* - * features are sorted by start position ascending, but in no - * particular order where start positions match; asserts here - * simply match the data returned (the order is not important) - */ - // AAA -> AAT -> K/N - SequenceFeature sf = sfs.get(0); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); - assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Lys1Asn", sf.getDescription()); - assertEquals("var5", sf.getValue("id")); - assertEquals("Benign", sf.getValue("clinical_significance")); - assertEquals("id=var5;clinical_significance=Benign", - sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5", - sf.links.get(0)); - assertEquals(ensembl, sf.getFeatureGroup()); - - // AAA -> CAA -> K/Q - sf = sfs.get(1); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); - assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Lys1Gln", sf.getDescription()); - assertEquals("var2", sf.getValue("id")); - assertEquals("Dodgy", sf.getValue("clinical_significance")); - assertEquals("id=var2;clinical_significance=Dodgy", sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "p.Lys1Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2", - sf.links.get(0)); - assertEquals(dbSnp, sf.getFeatureGroup()); - - // AAA -> GAA -> K/E - sf = sfs.get(2); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); - assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Lys1Glu", sf.getDescription()); - assertEquals("var1.125A>G", sf.getValue("id")); - assertNull(sf.getValue("clinical_significance")); - assertEquals("id=var1.125A>G", sf.getAttributes()); - assertEquals(1, sf.links.size()); - // link to variation is urlencoded - assertEquals( - "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG", - sf.links.get(0)); - assertEquals(ensembl, sf.getFeatureGroup()); - - // AAA -> TAA -> stop codon - sf = sfs.get(3); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); - assertEquals("stop_gained", sf.getType()); - assertEquals("Aaa/Taa", sf.getDescription()); - assertEquals("var3", sf.getValue("id")); - assertEquals("Bad", sf.getValue("clinical_significance")); - assertEquals("id=var3;clinical_significance=Bad", sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3", - sf.links.get(0)); - assertEquals(dbSnp, sf.getFeatureGroup()); - - // AAA -> AAG synonymous - sf = sfs.get(4); - assertEquals(1, sf.getBegin()); - assertEquals(1, sf.getEnd()); - assertEquals("synonymous_variant", sf.getType()); - assertEquals("aaA/aaG", sf.getDescription()); - assertEquals("var4", sf.getValue("id")); - assertEquals("None", sf.getValue("clinical_significance")); - assertEquals("id=var4;clinical_significance=None", sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4", - sf.links.get(0)); - assertEquals(cosmic, sf.getFeatureGroup()); - - // TTT -> TTC synonymous - sf = sfs.get(5); - assertEquals(2, sf.getBegin()); - assertEquals(2, sf.getEnd()); - assertEquals("synonymous_variant", sf.getType()); - assertEquals("ttT/ttC", sf.getDescription()); - assertEquals("var6", sf.getValue("id")); - assertNull(sf.getValue("clinical_significance")); - assertEquals("id=var6", sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "ttT/ttC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6", - sf.links.get(0)); - assertEquals(dbSnp, sf.getFeatureGroup()); - - // var7 generates two distinct protein variant features (two alleles) - // CCC -> CGC -> P/R - sf = sfs.get(6); - assertEquals(3, sf.getBegin()); - assertEquals(3, sf.getEnd()); - assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Pro3Arg", sf.getDescription()); - assertEquals("var7", sf.getValue("id")); - assertEquals("Good", sf.getValue("clinical_significance")); - assertEquals("id=var7;clinical_significance=Good", sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", - sf.links.get(0)); - assertEquals(cosmic, sf.getFeatureGroup()); - - // CCC -> CAC -> P/H - sf = sfs.get(7); - assertEquals(3, sf.getBegin()); - assertEquals(3, sf.getEnd()); - assertEquals("nonsynonymous_variant", sf.getType()); - assertEquals("p.Pro3His", sf.getDescription()); - assertEquals("var7", sf.getValue("id")); - assertEquals("Good", sf.getValue("clinical_significance")); - assertEquals("id=var7;clinical_significance=Good", sf.getAttributes()); - assertEquals(1, sf.links.size()); - assertEquals( - "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7", - sf.links.get(0)); - assertEquals(cosmic, sf.getFeatureGroup()); - } - - /** * Tests for the method that maps the subset of a dna sequence that has CDS * (or subtype) feature, with CDS strand = '-' (reverse) */ @@ -2592,9 +2190,16 @@ public class AlignmentUtilsTests AlignmentI al2 = new Alignment(new SequenceI[] { dna3, dna4 }); ((Alignment) al2).createDatasetAlignment(); + /* + * alignment removes gapped columns (two internal, two trailing) + */ assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2)); - assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString()); - assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString()); + String aligned1 = "-cc-GG-GTTT-aaa"; + assertEquals(aligned1, + al1.getSequenceAt(0).getSequenceAsString()); + String aligned2 = "C--C-Cgg-gtttAAA"; + assertEquals(aligned2, + al1.getSequenceAt(1).getSequenceAsString()); /* * add another sequence to 'aligned' - should still succeed, since @@ -2604,8 +2209,8 @@ public class AlignmentUtilsTests dna5.createDatasetSequence(); al2.addSequence(dna5); assertTrue(AlignmentUtils.alignAsSameSequences(al1, al2)); - assertEquals(seq1, al1.getSequenceAt(0).getSequenceAsString()); - assertEquals(seq2, al1.getSequenceAt(1).getSequenceAsString()); + assertEquals(aligned1, al1.getSequenceAt(0).getSequenceAsString()); + assertEquals(aligned2, al1.getSequenceAt(1).getSequenceAsString()); /* * add another sequence to 'unaligned' - should fail, since now not @@ -2623,15 +2228,15 @@ public class AlignmentUtilsTests { SequenceI dna1 = new Sequence("dna1", "cccGGGTTTaaa"); SequenceI dna2 = new Sequence("dna2", "CCCgggtttAAA"); - SequenceI as1 = dna1.deriveSequence(); - SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7); - SequenceI as3 = dna2.deriveSequence(); + SequenceI as1 = dna1.deriveSequence(); // cccGGGTTTaaa/1-12 + SequenceI as2 = dna1.deriveSequence().getSubSequence(3, 7); // GGGT/4-7 + SequenceI as3 = dna2.deriveSequence(); // CCCgggtttAAA/1-12 as1.insertCharAt(6, 5, '-'); - String s_as1 = as1.getSequenceAsString(); + assertEquals("cccGGG-----TTTaaa", as1.getSequenceAsString()); as2.insertCharAt(6, 5, '-'); - String s_as2 = as2.getSequenceAsString(); - as3.insertCharAt(6, 5, '-'); - String s_as3 = as3.getSequenceAsString(); + assertEquals("GGGT-----", as2.getSequenceAsString()); + as3.insertCharAt(3, 5, '-'); + assertEquals("CCC-----gggtttAAA", as3.getSequenceAsString()); AlignmentI aligned = new Alignment(new SequenceI[] { as1, as2, as3 }); // why do we need to cast this still ? @@ -2643,10 +2248,13 @@ public class AlignmentUtilsTests uas3 }); ((Alignment) tobealigned).createDatasetAlignment(); + /* + * alignAs lines up dataset sequences and removes empty columns (two) + */ assertTrue(AlignmentUtils.alignAsSameSequences(tobealigned, aligned)); - assertEquals(s_as1, uas1.getSequenceAsString()); - assertEquals(s_as2, uas2.getSequenceAsString()); - assertEquals(s_as3, uas3.getSequenceAsString()); + assertEquals("cccGGG---TTTaaa", uas1.getSequenceAsString()); + assertEquals("GGGT", uas2.getSequenceAsString()); + assertEquals("CCC---gggtttAAA", uas3.getSequenceAsString()); } @Test(groups = { "Functional" }) @@ -2685,7 +2293,7 @@ public class AlignmentUtilsTests * transcript 'CDS' is 10-16, 17-21 * which is 'gene' 158-164, 210-214 */ - MapList toMap = toLoci.getMap(); + MapList toMap = toLoci.getMapping(); assertEquals(1, toMap.getFromRanges().size()); assertEquals(2, toMap.getFromRanges().get(0).length); assertEquals(1, toMap.getFromRanges().get(0)[0]); @@ -2708,7 +2316,7 @@ public class AlignmentUtilsTests AlignmentUtils.transferGeneLoci(from, map, to); assertEquals("GRCh38", toLoci.getAssemblyId()); assertEquals("7", toLoci.getChromosomeId()); - toMap = toLoci.getMap(); + toMap = toLoci.getMapping(); assertEquals("[ [1, 12] ] 1:1 to [ [158, 164] [210, 214] ]", toMap.toString()); } diff --git a/test/jalview/datamodel/SequenceFeatureTest.java b/test/jalview/datamodel/SequenceFeatureTest.java index c955979..cd8f9eb 100644 --- a/test/jalview/datamodel/SequenceFeatureTest.java +++ b/test/jalview/datamodel/SequenceFeatureTest.java @@ -48,7 +48,7 @@ public class SequenceFeatureTest 12.5f, "group"); sf1.setValue("STRAND", "+"); sf1.setValue("Note", "Testing"); - Integer count = new Integer(7); + Integer count = Integer.valueOf(7); sf1.setValue("Count", count); SequenceFeature sf2 = new SequenceFeature(sf1); @@ -106,7 +106,7 @@ public class SequenceFeatureTest assertEquals("+", sf1.getValue("STRAND")); assertNull(sf1.getValue("strand")); // case-sensitive assertEquals(".", sf1.getValue("unknown", ".")); - Integer i = new Integer(27); + Integer i = Integer.valueOf(27); assertSame(i, sf1.getValue("Unknown", i)); } @@ -277,43 +277,46 @@ public class SequenceFeatureTest @Test(groups = { "Functional" }) public void testGetDetailsReport() { + SequenceI seq = new Sequence("TestSeq", "PLRFQMD"); + String seqName = seq.getName(); + // single locus, no group, no score SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null); - String expected = "

" - + "" + String expected = "
Typevariant
Start/end22
" + + "" + "
LocationTestSeq22
Typevariant
DescriptionG,C
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); // contact feature sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31, null); - expected = "
" - + "" + expected = "
TypeDisulphide Bond
Start/end28:31
" + + "" + "
LocationTestSeq28:31
TypeDisulphide Bond
Descriptiona description
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); sf = new SequenceFeature("variant", "G,C", 22, 33, 12.5f, "group"); sf.setValue("Parent", "ENSG001"); sf.setValue("Child", "ENSP002"); - expected = "
" - + "" + expected = "
Typevariant
Start/end22-33
" + + "" + "" + "" + "" + "" + "
LocationTestSeq22-33
Typevariant
DescriptionG,C
Score12.5
Groupgroup
ChildENSP002
ParentENSG001
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); /* * feature with embedded html link in description */ String desc = "Fer2 Status: True Positive Pfam 8_8"; sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot"); - expected = "
" - + "" + expected = "
TypePfam
Start/end8-83
" + + "" + "" + "
LocationTestSeq8-83
TypePfam
DescriptionFer2 Status: True Positive Pfam 8_8
GroupUniprot
"; - assertEquals(expected, sf.getDetailsReport()); + assertEquals(expected, sf.getDetailsReport(seqName)); } } diff --git a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java index 27008f5..a4d6d92 100644 --- a/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java +++ b/test/jalview/ext/ensembl/EnsemblSeqProxyTest.java @@ -21,12 +21,10 @@ package jalview.ext.ensembl; import static org.testng.AssertJUnit.assertEquals; -import static org.testng.AssertJUnit.assertSame; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.datamodel.features.SequenceFeatures; import jalview.gui.JvOptionPane; import jalview.io.DataSourceType; import jalview.io.FastaFile; @@ -34,8 +32,6 @@ import jalview.io.gff.SequenceOntologyFactory; import jalview.io.gff.SequenceOntologyLite; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; import org.testng.Assert; import org.testng.annotations.AfterClass; @@ -223,7 +219,6 @@ public class EnsemblSeqProxyTest SequenceFeature sf = new SequenceFeature("sequence_variant", alleles, 1, 2, 0f, null); sf.setValue("alleles", alleles); - sf.setAttributes("x=y,z;alleles=" + alleles + ";a=b,c"); EnsemblSeqProxy.reverseComplementAlleles(sf); String revcomp = "G,C,GTA-,HGMD_MUTATION,gtc"; @@ -231,7 +226,5 @@ public class EnsemblSeqProxyTest assertEquals(revcomp, sf.getDescription()); // verify alleles attribute is updated with reverse complement assertEquals(revcomp, sf.getValue("alleles")); - // verify attributes string is updated with reverse complement - assertEquals("x=y,z;alleles=" + revcomp + ";a=b,c", sf.getAttributes()); } } diff --git a/test/jalview/ext/jmol/JmolViewerTest.java b/test/jalview/ext/jmol/JmolViewerTest.java index d43f257..7ef0d16 100644 --- a/test/jalview/ext/jmol/JmolViewerTest.java +++ b/test/jalview/ext/jmol/JmolViewerTest.java @@ -165,7 +165,7 @@ public class JmolViewerTest final String _inFile = "examples/3W5V.pdb"; inFile = _inFile; FileLoader fl = new FileLoader(); - fl.loadFile(af.getCurrentView(), _inFile, DataSourceType.FILE, + fl.LoadFile(af.getCurrentView(), _inFile, DataSourceType.FILE, FileFormat.PDB); try { diff --git a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java index bf4889a..24be581 100644 --- a/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java +++ b/test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java @@ -59,16 +59,16 @@ public class ChimeraCommandsTest public void testBuildColourCommands() { - Map map = new LinkedHashMap<>(); - ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A"); - ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B"); - ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A"); - ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A"); - ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B"); - ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A"); - ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A"); - ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A"); - ChimeraCommands.addColourRange(map, Color.red, 0, 6, 9, "A"); + Map map = new LinkedHashMap(); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A"); + ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A"); // Colours should appear in the Chimera command in the order in which // they were added; within colour, by model, by chain, ranges in start order @@ -84,14 +84,14 @@ public class ChimeraCommandsTest /* * make a map of { featureType, {featureValue, {residue range specification } } } */ - Map> featuresMap = new LinkedHashMap<>(); - Map featureValues = new HashMap<>(); + Map> featuresMap = new LinkedHashMap>(); + Map featureValues = new HashMap(); /* * start with just one feature/value... */ featuresMap.put("chain", featureValues); - ChimeraCommands.addColourRange(featureValues, "X", 0, 8, 20, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A"); List commands = ChimeraCommands .buildSetAttributeCommands(featuresMap); @@ -104,24 +104,24 @@ public class ChimeraCommandsTest assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A"); // add same feature value, overlapping range - ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A"); // same feature value, contiguous range - ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A"); commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); assertEquals(1, commands.size()); assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A"); // same feature value and model, different chain - ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B"); // same feature value and chain, different model - ChimeraCommands.addColourRange(featureValues, "X", 1, 26, 30, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A"); commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); assertEquals(1, commands.size()); assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A"); // same feature, different value - ChimeraCommands.addColourRange(featureValues, "Y", 0, 40, 50, "A"); + ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A"); commands = ChimeraCommands.buildSetAttributeCommands(featuresMap); assertEquals(2, commands.size()); // commands are ordered by feature type but not by value @@ -133,7 +133,7 @@ public class ChimeraCommandsTest featuresMap.clear(); featureValues.clear(); featuresMap.put("side-chain binding!", featureValues); - ChimeraCommands.addColourRange(featureValues, + ChimeraCommands.addAtomSpecRange(featureValues, "metal 'ion!", 0, 7, 15, "A"); // feature names are sanitised to change non-alphanumeric to underscore diff --git a/test/jalview/io/FeaturesFileTest.java b/test/jalview/io/FeaturesFileTest.java index 04ddeb0..36b4edf 100644 --- a/test/jalview/io/FeaturesFileTest.java +++ b/test/jalview/io/FeaturesFileTest.java @@ -43,6 +43,7 @@ import jalview.gui.AlignFrame; import jalview.gui.Desktop; import jalview.gui.JvOptionPane; import jalview.schemes.FeatureColour; +import jalview.structure.StructureSelectionManager; import jalview.util.matcher.Condition; import jalview.viewmodel.seqfeatures.FeatureRendererModel; import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean; @@ -61,7 +62,6 @@ import org.testng.annotations.Test; public class FeaturesFileTest { - private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static String simpleGffFile = "examples/testdata/simpleGff3.gff"; @AfterClass(alwaysRun = true) @@ -70,7 +70,9 @@ public class FeaturesFileTest /* * remove any sequence mappings created so they don't pollute other tests */ - Desktop.getStructureSelectionManager().resetAll(); + StructureSelectionManager ssm = StructureSelectionManager + .getStructureSelectionManager(Desktop.getInstance()); + ssm.resetAll(); } @BeforeClass(alwaysRun = true) @@ -266,10 +268,12 @@ public class FeaturesFileTest AlignFrame af = new AlignFrame(al, 500, 500); Map colours = af.getFeatureRenderer() .getFeatureColours(); - // GFF3 uses '=' separator for name/value pairs in colum 9 + // GFF3 uses '=' separator for name/value pairs in column 9 + // comma (%2C) equals (%3D) or semi-colon (%3B) should be url-escaped in values 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" + + "Note=Iron-sulfur (2Fe-2S);Note=another note,and another;evidence=ECO%3B0000255%2CPROSITE%3DProRule:PRU00465;" + + "CSQ=AF=21,POLYPHEN=benign,possibly_damaging,clin_sig=Benign%3Dgood\n" + "FER1_SOLLC\tuniprot\tPfam\t55\t130\t3.0\t.\t.\tID=$23"; FeaturesFile featuresFile = new FeaturesFile(gffData, DataSourceType.PASTE); @@ -282,14 +286,25 @@ public class FeaturesFileTest assertEquals(1, sfs.size()); SequenceFeature sf = sfs.get(0); // description parsed from Note attribute - assertEquals("Iron-sulfur (2Fe-2S),another note", sf.description); + assertEquals("Iron-sulfur (2Fe-2S),another note,and another", + 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")); + assertEquals(5, sf.otherDetails.size()); + assertEquals("ECO;0000255,PROSITE=ProRule:PRU00465", // url decoded + sf.getValue("evidence")); + assertEquals("Iron-sulfur (2Fe-2S),another note,and another", + sf.getValue("Note")); + assertEquals("21", sf.getValueAsString("CSQ", "AF")); + assertEquals("benign,possibly_damaging", + sf.getValueAsString("CSQ", "POLYPHEN")); + assertEquals("Benign=good", sf.getValueAsString("CSQ", "clin_sig")); // url decoded + // todo change STRAND and !Phase into fields of SequenceFeature instead + assertEquals(".", sf.otherDetails.get("STRAND")); + assertEquals(0, sf.getStrand()); + assertEquals(".", sf.getPhase()); // verify feature on FER1_SOLLC1 sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures(); @@ -474,7 +489,7 @@ public class FeaturesFileTest */ FeatureRenderer fr = af.alignPanel.getFeatureRenderer(); String exported = featuresFile - .printJalviewFormat(al.getSequencesArray(), fr, false); + .printJalviewFormat(al.getSequencesArray(), fr, false, false); String expected = "No Features Visible"; assertEquals(expected, exported); @@ -483,15 +498,13 @@ public class FeaturesFileTest */ fr.setGroupVisibility("uniprot", true); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - true); + true, false); expected = "\nSTARTGROUP\tuniprot\n" + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n" + "ENDGROUP\tuniprot\n\n" + "desc1\tFER_CAPAN\t-1\t0\t0\tPfam\t1.3\n\n" + "desc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n"; // NaN is not output - assertEquals( - expected.replace("\n", LINE_SEPARATOR), - exported); + assertEquals(expected, exported); /* * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam @@ -499,21 +512,21 @@ public class FeaturesFileTest fr.setVisible("METAL"); fr.setVisible("GAMMA-TURN"); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "METAL\tcc9900\n" + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" - + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "ENDGROUP\tuniprot\n"; - assertEquals(fixLineEnd(expected), exported); + assertEquals(expected, exported); /* * now set Pfam visible */ fr.setVisible("Pfam"); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); /* * features are output within group, ordered by sequence and type */ @@ -521,14 +534,14 @@ public class FeaturesFileTest + "Pfam\tff0000\n" + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" - + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n" + "Pfam domainPfam_3_4\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n" + + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n" + "ENDGROUP\tuniprot\n" // null / empty group features are output after named groups + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n" + "\ndesc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n"; - assertEquals(fixLineEnd(expected), exported); + assertEquals(expected, exported); /* * hide uniprot group @@ -539,14 +552,14 @@ public class FeaturesFileTest + "\ndesc2\tFER_CAPAN\t-1\t4\t9\tPfam\n" + "\ndesc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n"; exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); - assertEquals(fixLineEnd(expected), exported); + false, false); + assertEquals(expected, exported); /* * include non-positional (overrides group not shown) */ exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - true); + true, false); expected = "METAL\tcc9900\n" + "Pfam\tff0000\n" + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n" + "\nSTARTGROUP\tuniprot\n" @@ -556,7 +569,7 @@ public class FeaturesFileTest + "desc2\tFER_CAPAN\t-1\t4\t9\tPfam\n" + "\ndesc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n" + "desc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n"; - assertEquals(fixLineEnd(expected), exported); + assertEquals(expected, exported); } @Test(groups = { "Functional" }) @@ -573,16 +586,12 @@ public class FeaturesFileTest FeatureRendererModel fr = (FeatureRendererModel) af.alignPanel .getFeatureRenderer(); String exported = featuresFile.printGffFormat(al.getSequencesArray(), - fr, false); + fr, false, false); String gffHeader = "##gff-version 2\n"; - assertEquals( - fixLineEnd(gffHeader), - exported); + assertEquals(gffHeader, exported); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - true); - assertEquals( - fixLineEnd(gffHeader), - exported); + true, false); + assertEquals(gffHeader, exported); /* * add some features @@ -597,9 +606,14 @@ public class FeaturesFileTest "s3dm")); SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f, "Uniprot"); - sf.setAttributes("x=y;black=white"); sf.setStrand("+"); sf.setPhase("2"); + sf.setValue("x", "y"); + sf.setValue("black", "white"); + Map csq = new HashMap<>(); + csq.put("SIFT", "benign,mostly benign,cloudy, with meatballs"); + csq.put("consequence", "missense_variant"); + sf.setValue("CSQ", csq); al.getSequenceAt(1).addSequenceFeature(sf); /* @@ -618,10 +632,8 @@ public class FeaturesFileTest * with no features displayed, exclude non-positional features */ exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); - assertEquals( - fixLineEnd(gffHeader), - exported); + false, false); + assertEquals(gffHeader, exported); /* * include non-positional features @@ -629,12 +641,10 @@ public class FeaturesFileTest fr.setGroupVisibility("Uniprot", true); fr.setGroupVisibility("s3dm", false); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - true); + true, false); String expected = gffHeader + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n"; - assertEquals( - fixLineEnd(expected), - exported); + assertEquals(expected, exported); /* * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam @@ -643,43 +653,38 @@ public class FeaturesFileTest fr.setVisible("METAL"); fr.setVisible("GAMMA-TURN"); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); // METAL feature has null group: description used for column 2 expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"; - assertEquals( - expected.replace("\n", LINE_SEPARATOR), - exported); + assertEquals(expected, exported); /* * set s3dm group visible */ fr.setGroupVisibility("s3dm", true); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); // METAL feature has null group: description used for column 2 expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"; - assertEquals( - expected.replace("\n", LINE_SEPARATOR), - exported); + assertEquals(expected, exported); /* * now set Pfam visible */ fr.setVisible("Pfam"); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); + false, false); // Pfam feature columns include strand(+), phase(2), attributes expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" - + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n" - + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n"; - assertEquals(fixLineEnd(expected), exported); - } - - private String fixLineEnd(String s) - { - return s.replace("\n", LINE_SEPARATOR); + // CSQ output as CSQ=att1=value1,att2=value2 + // note all commas are encoded here which is wrong - it should be + // SIFT=benign,mostly benign,cloudy%2C with meatballs + + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white;" + + "CSQ=SIFT=benign%2Cmostly benign%2Ccloudy%2C with meatballs,consequence=missense_variant\n" + + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"; + assertEquals(expected, exported); } /** @@ -754,7 +759,7 @@ public class FeaturesFileTest visible.put("foobar", new FeatureColour(Color.blue)); ff.outputFeatureFilters(sb, visible, featureFilters); String expected = "\nSTARTFILTERS\nfoobar\tLabel Present\npfam\t(CSQ:PolyPhen Present) AND (Score LE -2.4)\nENDFILTERS\n"; - assertEquals(fixLineEnd(expected), sb.toString()); + assertEquals(expected, sb.toString()); } /** @@ -787,12 +792,11 @@ public class FeaturesFileTest fr.setVisible("METAL"); fr.setColour("METAL", new FeatureColour(Color.PINK)); String exported = featuresFile.printGffFormat(al.getSequencesArray(), - fr, false); + fr, false, false); String expected = gffHeader - + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" - + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; - assertEquals( - fixLineEnd(expected), exported); + + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\tclin_sig=Likely Pathogenic;AF=24\n" + + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\tclin_sig=Benign;AF=46\n"; + assertEquals(expected, exported); /* * now threshold to Score > 1.1 - should exclude sf2 @@ -803,19 +807,21 @@ public class FeaturesFileTest fc.setThreshold(1.1f); fr.setColour("METAL", fc); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); - expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"; - assertEquals(fixLineEnd(expected), exported); + false, false); + expected = gffHeader + + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\tclin_sig=Likely Pathogenic;AF=24\n"; + assertEquals(expected, exported); /* * remove threshold and check sf2 is exported */ fc.setAboveThreshold(false); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); - expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n" - + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; - assertEquals(fixLineEnd(expected), exported); + false, false); + expected = gffHeader + + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\tclin_sig=Likely Pathogenic;AF=24\n" + + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\tclin_sig=Benign;AF=46\n"; + assertEquals(expected, exported); /* * filter on (clin_sig contains Benign) - should include sf2 and exclude sf1 @@ -825,9 +831,10 @@ public class FeaturesFileTest "clin_sig")); fr.setFeatureFilter("METAL", filter); exported = featuresFile.printGffFormat(al.getSequencesArray(), fr, - false); - expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; - assertEquals(fixLineEnd(expected), exported); + false, false); + expected = gffHeader + + "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\tclin_sig=Benign;AF=46\n"; + assertEquals(expected, exported); } /** @@ -861,15 +868,13 @@ public class FeaturesFileTest fr.setColour("METAL", new FeatureColour(Color.PINK)); String exported = featuresFile.printJalviewFormat( al.getSequencesArray(), - fr, false); + fr, false, false); String expected = "METAL\tffafaf\n\nSTARTGROUP\tgrp1\n" + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n" + "ENDGROUP\tgrp1\n\nSTARTGROUP\tgrp2\n" + "Cath\tFER_CAPAA\t-1\t41\t41\tMETAL\t0.6\n" + "ENDGROUP\tgrp2\n"; - assertEquals( - fixLineEnd(expected), - exported); + assertEquals(expected, exported); /* * now threshold to Score > 1.1 - should exclude sf2 @@ -881,28 +886,26 @@ public class FeaturesFileTest fc.setThreshold(1.1f); fr.setColour("METAL", fc); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|above|1.1\n\n" + "STARTGROUP\tgrp1\n" + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n" + "ENDGROUP\tgrp1\n"; - assertEquals( - expected.replace("\n", LINE_SEPARATOR), - exported); + assertEquals(expected, exported); /* * remove threshold and check sf2 is exported */ fc.setAboveThreshold(false); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n" + "STARTGROUP\tgrp1\n" + "Cath\tFER_CAPAA\t-1\t39\t39\tMETAL\t1.2\n" + "ENDGROUP\tgrp1\n\nSTARTGROUP\tgrp2\n" + "Cath\tFER_CAPAA\t-1\t41\t41\tMETAL\t0.6\n" + "ENDGROUP\tgrp2\n"; - assertEquals(fixLineEnd(expected), exported); + assertEquals(expected, exported); /* * filter on (clin_sig contains Benign) - should include sf2 and exclude sf1 @@ -912,13 +915,13 @@ public class FeaturesFileTest "clin_sig")); fr.setFeatureFilter("METAL", filter); exported = featuresFile.printJalviewFormat(al.getSequencesArray(), fr, - false); + false, false); expected = "FER_CAPAA\tCath\tMETAL\t41\t41\t0.6\t.\t.\n"; expected = "METAL\tscore|ffffff|000000|noValueMin|abso|0.0|2.0|none\n\n" + "STARTFILTERS\nMETAL\tclin_sig Contains benign\nENDFILTERS\n\n" + "STARTGROUP\tgrp2\n" + "Cath\tFER_CAPAA\t-1\t41\t41\tMETAL\t0.6\n" + "ENDGROUP\tgrp2\n"; - assertEquals(fixLineEnd(expected), exported); + assertEquals(expected, exported); } } diff --git a/test/jalview/io/FileLoaderTest.java b/test/jalview/io/FileLoaderTest.java index 983207a..968901f 100644 --- a/test/jalview/io/FileLoaderTest.java +++ b/test/jalview/io/FileLoaderTest.java @@ -11,7 +11,7 @@ public class FileLoaderTest { String urlFile = "http://www.jalview.org/builds/develop/examples/3W5V.pdb"; FileLoader fileLoader = new FileLoader(); - fileLoader.loadFileWaitTillLoaded(urlFile, DataSourceType.URL, + fileLoader.LoadFileWaitTillLoaded(urlFile, DataSourceType.URL, FileFormat.PDB); Assert.assertNotNull(fileLoader.file); // The FileLoader's file is expected to be same as the original URL. diff --git a/test/jalview/io/SequenceAnnotationReportTest.java b/test/jalview/io/SequenceAnnotationReportTest.java index cf3c7e5..111fadb 100644 --- a/test/jalview/io/SequenceAnnotationReportTest.java +++ b/test/jalview/io/SequenceAnnotationReportTest.java @@ -35,6 +35,8 @@ import jalview.schemes.FeatureColour; import jalview.viewmodel.seqfeatures.FeatureRendererModel; import java.awt.Color; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.testng.annotations.BeforeClass; @@ -62,22 +64,60 @@ public class SequenceAnnotationReportTest 3, 1.2f, "group"); // residuePos == 2 does not match start or end of feature, nothing done: - sar.appendFeature(sb, 2, null, sf); + sar.appendFeature(sb, 2, null, sf, null); assertEquals("123456", sb.toString()); - // residuePos == 1 matches start of feature, text appended (but no
) + // residuePos == 1 matches start of feature, text appended (but no
) // feature score is not included - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("123456disulfide bond 1:3", sb.toString()); // residuePos == 3 matches end of feature, text appended - //
is prefixed once sb.length() > 6 - sar.appendFeature(sb, 3, null, sf); - assertEquals("123456disulfide bond 1:3
disulfide bond 1:3", + //
is prefixed once sb.length() > 6 + sar.appendFeature(sb, 3, null, sf, null); + assertEquals("123456disulfide bond 1:3
disulfide bond 1:3", sb.toString()); } @Test(groups = "Functional") + public void testAppendFeatures_longText() + { + SequenceAnnotationReport sar = new SequenceAnnotationReport(null); + StringBuilder sb = new StringBuilder(); + String longString = // java11!"Abcd".repeat(50); + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" + + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" + + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" + + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" + + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"; + + SequenceFeature sf = new SequenceFeature("sequence", longString, 1, 3, + "group"); + + sar.appendFeature(sb, 1, null, sf, null); + assertTrue(sb.length() < 100); + + List sfl = new ArrayList<>(); + sb.setLength(0); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + sfl.add(sf); + int n = sar.appendFeaturesLengthLimit(sb, 1, sfl, + new FeatureRenderer(null), 200); // text should terminate before 200 characters + String s = sb.toString(); + assertTrue(s.length() < 200); + assertEquals(n, 7); // should be 7 features left over + + } + + @Test(groups = "Functional") public void testAppendFeature_status() { SequenceAnnotationReport sar = new SequenceAnnotationReport(null); @@ -86,7 +126,7 @@ public class SequenceAnnotationReportTest Float.NaN, "group"); sf.setStatus("Confirmed"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3; Fe2-S; (Confirmed)", sb.toString()); } @@ -100,7 +140,7 @@ public class SequenceAnnotationReportTest FeatureRendererModel fr = new FeatureRenderer(null); Map minmax = fr.getMinMax(); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); /* * map has no entry for this feature type - score is not shown: */ @@ -110,9 +150,9 @@ public class SequenceAnnotationReportTest * map has entry for this feature type - score is shown: */ minmax.put("METAL", new float[][] { { 0f, 1f }, null }); - sar.appendFeature(sb, 1, fr, sf); - //
is appended to a buffer > 6 in length - assertEquals("METAL 1 3; Fe2-S
METAL 1 3; Fe2-S Score=1.3", + sar.appendFeature(sb, 1, fr, sf, null); + //
is appended to a buffer > 6 in length + assertEquals("METAL 1 3; Fe2-S
METAL 1 3; Fe2-S Score=1.3", sb.toString()); /* @@ -120,7 +160,7 @@ public class SequenceAnnotationReportTest */ minmax.put("METAL", new float[][] { { 2f, 2f }, null }); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @@ -132,7 +172,7 @@ public class SequenceAnnotationReportTest SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, Float.NaN, "group"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); } @@ -152,7 +192,7 @@ public class SequenceAnnotationReportTest * first with no colour by attribute */ FeatureRendererModel fr = new FeatureRenderer(null); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); /* @@ -163,7 +203,7 @@ public class SequenceAnnotationReportTest fc.setAttributeName("Pfam"); fr.setColour("METAL", fc); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change /* @@ -171,7 +211,7 @@ public class SequenceAnnotationReportTest */ fc.setAttributeName("clinical_significance"); sb.setLength(0); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign", sb.toString()); } @@ -193,7 +233,7 @@ public class SequenceAnnotationReportTest fc.setAttributeName("clinical_significance"); fr.setColour("METAL", fc); minmax.put("METAL", new float[][] { { 0f, 1f }, null }); - sar.appendFeature(sb, 1, fr, sf); + sar.appendFeature(sb, 1, fr, sf, null); assertEquals( "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign", @@ -209,13 +249,13 @@ public class SequenceAnnotationReportTest Float.NaN, "group"); // description is not included if it duplicates type: - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3", sb.toString()); sb.setLength(0); sf.setDescription("Metal"); // test is case-sensitive: - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); assertEquals("METAL 1 3; Metal", sb.toString()); } @@ -228,13 +268,13 @@ public class SequenceAnnotationReportTest "helloworld", 1, 3, Float.NaN, "group"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); // !! strips off but not ?? assertEquals("METAL 1 3; helloworld", sb.toString()); sb.setLength(0); sf.setDescription("
&kHD>6"); - sar.appendFeature(sb, 1, null, sf); + sar.appendFeature(sb, 1, null, sf, null); // if no tag, html-encodes > and < (only): assertEquals("METAL 1 3; <br>&kHD>6", sb.toString()); } @@ -248,14 +288,13 @@ public class SequenceAnnotationReportTest SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL"); seq.setDescription("SeqDesc"); - sar.createSequenceAnnotationReport(sb, seq, true, true, null); - /* * positional features are ignored */ seq.addSequenceFeature(new SequenceFeature("Domain", "Ferredoxin", 5, 10, 1f, null)); - assertEquals("
SeqDesc
", sb.toString()); + sar.createSequenceAnnotationReport(sb, seq, true, true, null); + assertEquals("SeqDesc", sb.toString()); /* * non-positional feature @@ -264,7 +303,7 @@ public class SequenceAnnotationReportTest null)); sb.setLength(0); sar.createSequenceAnnotationReport(sb, seq, true, true, null); - String expected = "
SeqDesc
Type1 ; Nonpos Score=1.0
"; + String expected = "SeqDesc
Type1 ; Nonpos Score=1.0
"; assertEquals(expected, sb.toString()); /* @@ -272,7 +311,7 @@ public class SequenceAnnotationReportTest */ sb.setLength(0); sar.createSequenceAnnotationReport(sb, seq, true, false, null); - assertEquals("
SeqDesc
", sb.toString()); + assertEquals("SeqDesc", sb.toString()); /* * add non-pos feature with score inside min-max range for feature type @@ -289,7 +328,7 @@ public class SequenceAnnotationReportTest sb.setLength(0); sar.createSequenceAnnotationReport(sb, seq, true, true, fr); - expected = "
SeqDesc
Metal ; Desc
Type1 ; Nonpos
"; + expected = "SeqDesc
Metal ; Desc
Type1 ; Nonpos
"; assertEquals(expected, sb.toString()); /* @@ -305,8 +344,8 @@ public class SequenceAnnotationReportTest assertEquals(expected, sb.toString()); // unchanged! /* - * 'clinical_significance' attribute only included when - * used for feature colouring + * 'clinical_significance' attribute is only included in description + * when used for feature colouring */ SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0, 5f, null); @@ -314,7 +353,7 @@ public class SequenceAnnotationReportTest seq.addSequenceFeature(sf2); sb.setLength(0); sar.createSequenceAnnotationReport(sb, seq, true, true, fr); - expected = "
SeqDesc
Metal ; Desc
Type1 ; Nonpos
Variant ; Havana
"; + expected = "SeqDesc
Metal ; Desc
Type1 ; Nonpos
Variant ; Havana
"; assertEquals(expected, sb.toString()); /* @@ -335,13 +374,23 @@ public class SequenceAnnotationReportTest fc.setAttributeName("clinical_significance"); fr.setColour("Variant", fc); sar.createSequenceAnnotationReport(sb, seq, true, true, fr); - expected = "
SeqDesc
UNIPROT P30419
PDB 3iu1
Metal ; Desc
" - + "Type1 ; Nonpos
Variant ; Havana; clinical_significance=benign
"; + expected = "SeqDesc
UNIPROT P30419
PDB 3iu1
Metal ; Desc
" + + "Type1 ; Nonpos
Variant ; Havana; clinical_significance=benign
"; assertEquals(expected, sb.toString()); // with showNonPositionalFeatures = false sb.setLength(0); sar.createSequenceAnnotationReport(sb, seq, true, false, fr); - expected = "
SeqDesc
UNIPROT P30419
PDB 3iu1
"; + expected = "SeqDesc
UNIPROT P30419
PDB 3iu1
"; + assertEquals(expected, sb.toString()); + + /* + * long feature description is truncated with ellipsis + */ + sb.setLength(0); + sf2.setDescription( + "This is a very long description which should be truncated"); + sar.createSequenceAnnotationReport(sb, seq, false, true, fr); + expected = "SeqDesc
Metal ; Desc
Type1 ; Nonpos
Variant ; This is a very long description which sh...; clinical_significance=benign
"; assertEquals(expected, sb.toString()); // see other tests for treatment of status and html @@ -375,8 +424,10 @@ public class SequenceAnnotationReportTest sar.createSequenceAnnotationReport(sb, seq, true, true, null, true); String report = sb.toString(); assertTrue(report - .startsWith("
UNIPROT P30410, P30411, P30412, P30413,...
PDB0 3iu1")); + .startsWith( + "
UNIPROT P30410, P30411, P30412, P30413,...
PDB0 3iu1")); assertTrue(report - .endsWith("
PDB7 3iu1
PDB8,...
(Output Sequence Details to list all database references)
")); + .endsWith( + "
PDB7 3iu1
PDB8,...
(Output Sequence Details to list all database references)
")); } } diff --git a/test/jalview/structure/Mapping.java b/test/jalview/structure/Mapping.java index f8461f8..1970ff2 100644 --- a/test/jalview/structure/Mapping.java +++ b/test/jalview/structure/Mapping.java @@ -247,7 +247,7 @@ public class Mapping public void mapFer1From3W5V() throws Exception { AlignFrame seqf = new FileLoader(false) - .loadFileWaitTillLoaded( + .LoadFileWaitTillLoaded( ">FER1_MAIZE/1-150 Ferredoxin-1, chloroplast precursor\nMATVLGSPRAPAFFFSSSSLRAAPAPTAVALPAAKVGIMGRSASSRRRLRAQATYNVKLITPEGEVELQVPD\nDVYILDQAEEDGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQSYLDDGQIADGWVLTCHAYPTSDVVIETHKE\nEELTGA", DataSourceType.PASTE, FileFormat.Fasta); SequenceI newseq = seqf.getViewport().getAlignment().getSequenceAt(0);