X-Git-Url: http://source.jalview.org/gitweb/?p=jalview.git;a=blobdiff_plain;f=src%2Fjalview%2Fio%2FSequenceAnnotationReport.java;h=369839a8c0fbda24c239edb0dc475f67f022b19f;hp=ffc0be3dddf7fd67353eccce1c65425ab880a1b2;hb=70a6730dba4ab0af6906fd8f874fe853c02efef0;hpb=838e4f91d4a53dd315640dbc9ff6ef7a815ee576 diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index ffc0be3..369839a 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -1,6 +1,6 @@ /* - * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9.0b1) - * Copyright (C) 2015 The Jalview Authors + * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) + * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * @@ -20,14 +20,25 @@ */ package jalview.io; +import java.util.Locale; + +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +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; +import jalview.util.StringUtils; import jalview.util.UrlLink; - -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; +import jalview.viewmodel.seqfeatures.FeatureRendererModel; /** * generate HTML reports for a sequence @@ -36,351 +47,587 @@ import java.util.List; */ public class SequenceAnnotationReport { - final String linkImageURL; + private static final int MAX_DESCRIPTION_LENGTH = 40; + + private static final String COMMA = ","; + + private static final String ELLIPSIS = "..."; + + private static final int MAX_REFS_PER_SOURCE = 4; + + private static final int MAX_SOURCES = 40; + + private static String linkImageURL; + + // public static final String[][] PRIMARY_SOURCES moved to DBRefSource.java + + /* + * Comparator to order DBRefEntry by Source + accession id (case-insensitive), + * with 'Primary' sources placed before others, and 'chromosome' first of all + */ + private static Comparator comparator = new Comparator() + { + + @Override + public int compare(DBRefEntry ref1, DBRefEntry ref2) + { + if (ref1 instanceof GeneLociI) + { + return -1; + } + if (ref2 instanceof GeneLociI) + { + return 1; + } + String s1 = ref1.getSource(); + String s2 = ref2.getSource(); + boolean s1Primary = DBRefSource.isPrimarySource(s1); + boolean s2Primary = DBRefSource.isPrimarySource(s2); + if (s1Primary && !s2Primary) + { + return -1; + } + if (!s1Primary && s2Primary) + { + return 1; + } + int comp = s1 == null ? -1 + : (s2 == null ? 1 : s1.compareToIgnoreCase(s2)); + if (comp == 0) + { + String a1 = ref1.getAccessionId(); + String a2 = ref2.getAccessionId(); + comp = a1 == null ? -1 + : (a2 == null ? 1 : a1.compareToIgnoreCase(a2)); + } + return comp; + } + + // private boolean isPrimarySource(String source) + // { + // for (String[] primary : DBRefSource.PRIMARY_SOURCES) + // { + // for (String s : primary) + // { + // if (source.equals(s)) + // { + // return true; + // } + // } + // } + // return false; + // } + }; - public SequenceAnnotationReport(String linkImageURL) + private boolean forTooltip; + + /** + * Constructor given a flag which affects behaviour + * + * + * @param isForTooltip + */ + public SequenceAnnotationReport(boolean isForTooltip) { - this.linkImageURL = linkImageURL; + this.forTooltip = isForTooltip; + if (linkImageURL == null) + { + linkImageURL = getClass().getResource("/images/link.gif").toString(); + } } /** - * appends the features at rpos to the given stringbuffer ready for display in - * a tooltip + * Append text for the list of features to the tooltip. Returns the number of + * features not added if maxlength limit is (or would have been) reached. * - * @param tooltipText2 - * @param linkImageURL - * @param rpos + * @param sb + * @param residuePos * @param features - * TODO refactor to Jalview 'utilities' somehow. + * @param minmax + * @param maxlength */ - public void appendFeatures(final StringBuffer tooltipText2, int rpos, - List features) + public int appendFeatures(final StringBuilder sb, int residuePos, + List features, FeatureRendererModel fr, + int maxlength) { - appendFeatures(tooltipText2, rpos, features, null); + 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 StringBuffer tooltipText2, int rpos, - List features, Hashtable minmax) + /** + * 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 appendFeatures(StringBuilder sb, int residuePos, + MappedFeatures mf, FeatureRendererModel fr, int maxlength) { - String tmpString; - if (features != null) + 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)) { - if (feature.getType().equals("disulfide bond")) + return mf.features.size() - i; + } + } + return 0; + } + + /** + * Appends the feature at rpos to the given buffer + * + * @param sb + * @param rpos + * @param minmax + * @param feature + */ + boolean appendFeature(final StringBuilder sb0, int rpos, + FeatureRendererModel fr, SequenceFeature feature, + MappedFeatures mf, int maxlength) + { + int begin = feature.getBegin(); + int end = feature.getEnd(); + + /* + * if this is a virtual features, convert begin/end to the + * coordinates of the sequence it is mapped to + */ + int[] beginRange = null; // feature start in local coordinates + int[] endRange = null; // feature end in local coordinates + if (mf != null) + { + if (feature.isContactFeature()) + { + /* + * map start and end points individually + */ + beginRange = mf.getMappedPositions(begin, begin); + endRange = begin == end ? beginRange + : mf.getMappedPositions(end, end); + } + else + { + /* + * map the feature extent + */ + beginRange = mf.getMappedPositions(begin, end); + endRange = beginRange; + } + if (beginRange == null || endRange == null) + { + // something went wrong + return false; + } + begin = beginRange[0]; + end = endRange[endRange.length - 1]; + } + + StringBuilder sb = new StringBuilder(); + if (feature.isContactFeature()) + { + /* + * include if rpos is at start or end position of [mapped] feature + */ + boolean showContact = (mf == null) && (rpos == begin || rpos == end); + boolean showMappedContact = (mf != null) && ((rpos >= beginRange[0] + && rpos <= beginRange[beginRange.length - 1]) + || (rpos >= endRange[0] + && rpos <= endRange[endRange.length - 1])); + if (showContact || showMappedContact) + { + if (sb0.length() > 6) { - if (feature.getBegin() == rpos || feature.getEnd() == rpos) - { - if (tooltipText2.length() > 6) - { - tooltipText2.append("
"); - } - tooltipText2.append("disulfide bond " + feature.getBegin() - + ":" + feature.getEnd()); - } + sb.append("
"); } - else + sb.append(feature.getType()).append(" ").append(begin).append(":") + .append(end); + } + return appendText(sb0, sb, maxlength); + } + + if (sb0.length() > 6) + { + sb.append("
"); + } + // TODO: remove this hack to display link only features + boolean linkOnly = feature.getValue("linkonly") != null; + if (!linkOnly) + { + sb.append(feature.getType()).append(" "); + if (rpos != 0) + { + // we are marking a positional feature + sb.append(begin); + if (begin != end) { - if (tooltipText2.length() > 6) - { - tooltipText2.append("
"); - } - // TODO: remove this hack to display link only features - boolean linkOnly = feature.getValue("linkonly") != null; - if (!linkOnly) - { - tooltipText2.append(feature.getType() + " "); - if (rpos != 0) - { - // we are marking a positional feature - tooltipText2.append(feature.begin); - } - if (feature.begin != feature.end) - { - tooltipText2.append(" " + feature.end); - } + sb.append(" ").append(end); + } + } - if (feature.getDescription() != null - && !feature.description.equals(feature.getType())) - { - tmpString = feature.getDescription(); - String tmp2up = tmpString.toUpperCase(); - int startTag = tmp2up.indexOf(""); - if (startTag > -1) - { - tmpString = tmpString.substring(startTag + 6); - tmp2up = tmp2up.substring(startTag + 6); - } - int endTag = tmp2up.indexOf(""); - if (endTag > -1) - { - tmpString = tmpString.substring(0, endTag); - tmp2up = tmp2up.substring(0, endTag); - } - endTag = tmp2up.indexOf(""); - if (endTag > -1) - { - tmpString = tmpString.substring(0, endTag); - } - - if (startTag > -1) - { - tooltipText2.append("; " + tmpString); - } - else - { - if (tmpString.indexOf("<") > -1 - || tmpString.indexOf(">") > -1) - { - // The description does not specify html is to - // be used, so we must remove < > symbols - tmpString = tmpString.replaceAll("<", "<"); - tmpString = tmpString.replaceAll(">", ">"); - - tooltipText2.append("; "); - tooltipText2.append(tmpString); - - } - else - { - tooltipText2.append("; " + tmpString); - } - } - } - // check score should be shown - if (!Float.isNaN(feature.getScore())) - { - float[][] rng = (minmax == null) ? null : ((float[][]) minmax - .get(feature.getType())); - if (rng != null && rng[0] != null && rng[0][0] != rng[0][1]) - { - tooltipText2.append(" Score=" + feature.getScore()); - } - } - if (feature.getValue("status") != null) - { - String status = feature.getValue("status").toString(); - if (status.length() > 0) - { - tooltipText2.append("; (" + feature.getValue("status") - + ")"); - } - } - } + String description = feature.getDescription(); + 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(Locale.ROOT).indexOf(" -1 + && linkindex < MAX_DESCRIPTION_LENGTH; + if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink) + { + description = description.substring(0, MAX_DESCRIPTION_LENGTH) + + ELLIPSIS; } - if (feature.links != null) + + sb.append("; ").append(description); + } + + if (showScore(feature, fr)) + { + sb.append(" Score=").append(String.valueOf(feature.getScore())); + } + String status = (String) feature.getValue("status"); + if (status != null && status.length() > 0) + { + sb.append("; (").append(status).append(")"); + } + + /* + * add attribute value if coloured by attribute + */ + if (fr != null) + { + FeatureColourI fc = fr.getFeatureColours().get(feature.getType()); + if (fc != null && fc.isColourByAttribute()) { - if (linkImageURL != null) + String[] attName = fc.getAttributeName(); + String attVal = feature.getValueAsString(attName); + if (attVal != null) { - tooltipText2.append(" "); - } - else - { - for (String urlstring : feature.links) - { - try - { - for (String[] urllink : createLinksFrom(null, urlstring)) - { - tooltipText2.append("
" - + (urllink[0].toLowerCase().equals( - urllink[1].toLowerCase()) ? urllink[0] - : (urllink[0] + ":" + urllink[1])) - + "
"); - } - } catch (Exception x) - { - System.err.println("problem when creating links from " - + urlstring); - x.printStackTrace(); - } - } + sb.append("; ").append(String.join(":", attName)).append("=") + .append(attVal); } + } + } + if (mf != null) + { + String variants = mf.findProteinVariants(feature); + if (!variants.isEmpty()) + { + sb.append(" ").append(variants); } } } + return appendText(sb0, sb, maxlength); } /** + * Appends sb to sb0, and returns false, unless maxlength is not zero and + * appending would make the result longer than or equal to maxlength, in which + * case the append is not done and returns true * - * @param seq - * @param link - * @return String[][] { String[] { link target, link label, dynamic component - * inserted (if any), url }} + * @param sb0 + * @param sb + * @param maxlength + * @return */ - public String[][] createLinksFrom(SequenceI seq, String link) + private static boolean appendText(StringBuilder sb0, StringBuilder sb, + int maxlength) { - ArrayList urlSets = new ArrayList(); - ArrayList uniques = new ArrayList(); - UrlLink urlLink = new UrlLink(link); - if (!urlLink.isValid()) + if (maxlength == 0 || sb0.length() + sb.length() < maxlength) { - System.err.println(urlLink.getInvalidMessage()); - return null; + sb0.append(sb); + return false; } - final String target = urlLink.getTarget(); // link.substring(0, - // link.indexOf("|")); - final String label = urlLink.getLabel(); - if (seq != null && urlLink.isDynamic()) + return true; + } + + /** + * Answers true if score should be shown, else false. Score is shown if it is + * not NaN, and the feature type has a non-trivial min-max score range + */ + boolean showScore(SequenceFeature feature, FeatureRendererModel fr) + { + if (Float.isNaN(feature.getScore())) { + return false; + } + if (fr == null) + { + return true; + } + float[][] minMax = fr.getMinMax().get(feature.getType()); - // collect matching db-refs - DBRefEntry[] dbr = jalview.util.DBRefUtils.selectRefs(seq.getDBRef(), - new String[] { target }); - // collect id string too - String id = seq.getName(); - String descr = seq.getDescription(); - if (descr != null && descr.length() < 1) - { - descr = null; - } - if (dbr != null) + /* + * minMax[0] is the [min, max] score range for positional features + */ + if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1]) + { + return false; + } + return true; + } + + /** + * Format and appends any hyperlinks for the sequence feature to the string + * buffer + * + * @param sb + * @param feature + */ + void appendLinks(final StringBuffer sb, SequenceFeature feature) + { + if (feature.links != null) + { + if (linkImageURL != null) { - for (int r = 0; r < dbr.length; r++) - { - if (id != null && dbr[r].getAccessionId().equals(id)) - { - // suppress duplicate link creation for the bare sequence ID - // string with this link - id = null; - } - // create Bare ID link for this RUL - String[] urls = urlLink.makeUrls(dbr[r].getAccessionId(), true); - if (urls != null) - { - for (int u = 0; u < urls.length; u += 2) - { - String unq = urls[u] + "|" + urls[u + 1]; - if (!uniques.contains(unq)) - { - urlSets.add(new String[] { target, label, urls[u], - urls[u + 1] }); - uniques.add(unq); - } - } - } - } + sb.append(" "); } - if (id != null) + else { - // create Bare ID link for this RUL - String[] urls = urlLink.makeUrls(id, true); - if (urls != null) + for (String urlstring : feature.links) { - for (int u = 0; u < urls.length; u += 2) + try { - String unq = urls[u] + "|" + urls[u + 1]; - if (!uniques.contains(unq)) + for (List urllink : createLinksFrom(null, urlstring)) { - urlSets.add(new String[] { target, label, urls[u], - urls[u + 1] }); - uniques.add(unq); + sb.append("
" + + (urllink.get(0).toLowerCase(Locale.ROOT).equals( + urllink.get(1).toLowerCase(Locale.ROOT)) + ? urllink.get(0) + : (urllink.get(0) + ":" + + urllink.get(1))) + + "
"); } - } - } - } - if (descr != null && urlLink.getRegexReplace() != null) - { - // create link for this URL from description only if regex matches - String[] urls = urlLink.makeUrls(descr, true); - if (urls != null) - { - for (int u = 0; u < urls.length; u += 2) + } catch (Exception x) { - String unq = urls[u] + "|" + urls[u + 1]; - if (!uniques.contains(unq)) - { - urlSets.add(new String[] { target, label, urls[u], - urls[u + 1] }); - uniques.add(unq); - } + System.err.println( + "problem when creating links from " + urlstring); + x.printStackTrace(); } } } } - else + } + + /** + * + * @param seq + * @param link + * @return Collection< List > { List { link target, link + * label, dynamic component inserted (if any), url }} + */ + Collection> createLinksFrom(SequenceI seq, String link) + { + Map> urlSets = new LinkedHashMap<>(); + UrlLink urlLink = new UrlLink(link); + if (!urlLink.isValid()) { - String unq = label + "|" + urlLink.getUrl_prefix(); - if (!uniques.contains(unq)) - { - uniques.add(unq); - // Add a non-dynamic link - urlSets.add(new String[] { target, label, null, - urlLink.getUrl_prefix() }); - } + System.err.println(urlLink.getInvalidMessage()); + return null; } - return urlSets.toArray(new String[][] {}); + urlLink.createLinksFromSeq(seq, urlSets); + + return urlSets.values(); } - public void createSequenceAnnotationReport(final StringBuffer tip, + public void createSequenceAnnotationReport(final StringBuilder tip, SequenceI sequence, boolean showDbRefs, boolean showNpFeats, - Hashtable minmax) + FeatureRendererModel fr) { createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats, - true, minmax); + fr, false); } - public void createSequenceAnnotationReport(final StringBuffer tip, + /** + * Builds an html formatted report of sequence details and appends it to the + * provided buffer. + * + * @param sb + * buffer to append report to + * @param sequence + * the sequence the report is for + * @param showDbRefs + * whether to include database references for the sequence + * @param showNpFeats + * whether to include non-positional sequence features + * @param fr + * @param summary + * @return + */ + int createSequenceAnnotationReport(final StringBuilder sb, SequenceI sequence, boolean showDbRefs, boolean showNpFeats, - boolean tableWrap, Hashtable minmax) + FeatureRendererModel fr, boolean summary) { String tmp; - tip.append(""); + sb.append(""); int maxWidth = 0; if (sequence.getDescription() != null) { tmp = sequence.getDescription(); - tip.append("
" + tmp); + sb.append(tmp); maxWidth = Math.max(maxWidth, tmp.length()); } + sb.append("\n"); SequenceI ds = sequence; while (ds.getDatasetSequence() != null) { ds = ds.getDatasetSequence(); } - DBRefEntry[] dbrefs = ds.getDBRef(); - if (showDbRefs && dbrefs != null) + + if (showDbRefs) { - for (int i = 0; i < dbrefs.length; i++) + maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary)); + } + sb.append("\n"); + + /* + * add non-positional features if wanted + */ + if (showNpFeats) + { + for (SequenceFeature sf : sequence.getFeatures() + .getNonPositionalFeatures()) { - tip.append("
"); - tmp = dbrefs[i].getSource() + " " + dbrefs[i].getAccessionId(); - tip.append(tmp); - maxWidth = Math.max(maxWidth, tmp.length()); + int sz = -sb.length(); + appendFeature(sb, 0, fr, sf, null, 0); + sz += sb.length(); + maxWidth = Math.max(maxWidth, sz); } } + sb.append("
"); + return maxWidth; + } - // ADD NON POSITIONAL SEQUENCE INFO - SequenceFeature[] features = sequence.getSequenceFeatures(); - if (showNpFeats && features != null) + /** + * A helper method that appends any DBRefs, returning the maximum line length + * added + * + * @param sb + * @param ds + * @param summary + * @return + */ + protected int appendDbRefs(final StringBuilder sb, SequenceI ds, + boolean summary) + { + List dbrefs = ds.getDBRefs(); + if (dbrefs == null) { - for (int i = 0; i < features.length; i++) + return 0; + } + + // note this sorts the refs held on the sequence! + dbrefs.sort(comparator); + boolean ellipsis = false; + String source = null; + String lastSource = null; + int countForSource = 0; + int sourceCount = 0; + boolean moreSources = false; + int maxLineLength = 0; + int lineLength = 0; + + for (DBRefEntry ref : dbrefs) + { + source = ref.getSource(); + if (source == null) + { + // shouldn't happen + continue; + } + boolean sourceChanged = !source.equals(lastSource); + if (sourceChanged) { - if (features[i].begin == 0 && features[i].end == 0) + lineLength = 0; + countForSource = 0; + sourceCount++; + } + if (sourceCount > MAX_SOURCES && summary) + { + ellipsis = true; + moreSources = true; + break; + } + lastSource = source; + countForSource++; + if (countForSource == 1 || !summary) + { + sb.append("
\n"); + } + if (countForSource <= MAX_REFS_PER_SOURCE || !summary) + { + String accessionId = ref.getAccessionId(); + lineLength += accessionId.length() + 1; + if (countForSource > 1 && summary) + { + sb.append(", ").append(accessionId); + lineLength++; + } + else { - int sz = -tip.length(); - List tfeat = new ArrayList(); - tfeat.add(features[i]); - appendFeatures(tip, 0, tfeat, minmax); - sz += tip.length(); - maxWidth = Math.max(maxWidth, sz); + sb.append(source).append(" ").append(accessionId); + lineLength += source.length(); } + maxLineLength = Math.max(maxLineLength, lineLength); + } + if (countForSource == MAX_REFS_PER_SOURCE && summary) + { + sb.append(COMMA).append(ELLIPSIS); + ellipsis = true; } } - - if (tableWrap && maxWidth > 60) + if (moreSources) { - tip.insert(0, "
"); - tip.append("
"); + sb.append("
").append(source).append(COMMA).append(ELLIPSIS); } + if (ellipsis) + { + sb.append("
("); + sb.append(MessageManager.getString("label.output_seq_details")); + sb.append(")"); + } + + return maxLineLength; + } + + public void createTooltipAnnotationReport(final StringBuilder tip, + SequenceI sequence, boolean showDbRefs, boolean showNpFeats, + FeatureRendererModel fr) + { + int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs, + showNpFeats, fr, true); + if (maxWidth > 60) + { + // ? not sure this serves any useful purpose + // tip.insert(0, "
"); + // tip.append("
"); + } } }