JAL-3468 truncate feature description to 40 chars in tooltip/menu item
[jalview.git] / src / jalview / io / SequenceAnnotationReport.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import jalview.api.FeatureColourI;
24 import jalview.datamodel.DBRefEntry;
25 import jalview.datamodel.DBRefSource;
26 import jalview.datamodel.GeneLociI;
27 import jalview.datamodel.MappedFeatures;
28 import jalview.datamodel.SequenceFeature;
29 import jalview.datamodel.SequenceI;
30 import jalview.util.MessageManager;
31 import jalview.util.StringUtils;
32 import jalview.util.UrlLink;
33 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
34
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Comparator;
38 import java.util.LinkedHashMap;
39 import java.util.List;
40 import java.util.Map;
41
42 /**
43  * generate HTML reports for a sequence
44  * 
45  * @author jimp
46  */
47 public class SequenceAnnotationReport
48 {
49   private static final int MAX_DESCRIPTION_LENGTH = 40;
50
51   private static final String COMMA = ",";
52
53   private static final String ELLIPSIS = "...";
54
55   private static final int MAX_REFS_PER_SOURCE = 4;
56
57   private static final int MAX_SOURCES = 40;
58
59   private static final String[][] PRIMARY_SOURCES = new String[][] {
60       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
61       DBRefSource.PROTEINDBS };
62
63   final String linkImageURL;
64
65   /*
66    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
67    * with 'Primary' sources placed before others, and 'chromosome' first of all
68    */
69   private static Comparator<DBRefEntry> comparator = new Comparator<>()
70   {
71
72     @Override
73     public int compare(DBRefEntry ref1, DBRefEntry ref2)
74     {
75       if (ref1 instanceof GeneLociI)
76       {
77         return -1;
78       }
79       if (ref2 instanceof GeneLociI)
80       {
81         return 1;
82       }
83       String s1 = ref1.getSource();
84       String s2 = ref2.getSource();
85       boolean s1Primary = isPrimarySource(s1);
86       boolean s2Primary = isPrimarySource(s2);
87       if (s1Primary && !s2Primary)
88       {
89         return -1;
90       }
91       if (!s1Primary && s2Primary)
92       {
93         return 1;
94       }
95       int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
96               .compareToIgnoreCase(s2));
97       if (comp == 0)
98       {
99         String a1 = ref1.getAccessionId();
100         String a2 = ref2.getAccessionId();
101         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
102                 .compareToIgnoreCase(a2));
103       }
104       return comp;
105     }
106
107     private boolean isPrimarySource(String source)
108     {
109       for (String[] primary : PRIMARY_SOURCES)
110       {
111         for (String s : primary)
112         {
113           if (source.equals(s))
114           {
115             return true;
116           }
117         }
118       }
119       return false;
120     }
121   };
122
123   public SequenceAnnotationReport(String linkURL)
124   {
125     this.linkImageURL = linkURL;
126   }
127
128   /**
129    * Append text for the list of features to the tooltip
130    * 
131    * @param sb
132    * @param residuePos
133    * @param features
134    * @param minmax
135    */
136   public void appendFeatures(final StringBuilder sb, int residuePos,
137           List<SequenceFeature> features, FeatureRendererModel fr)
138   {
139     for (SequenceFeature feature : features)
140     {
141       appendFeature(sb, residuePos, fr, feature, null);
142     }
143   }
144
145   /**
146    * Appends text for mapped features (e.g. CDS feature for peptide or vice versa)
147    * 
148    * @param sb
149    * @param residuePos
150    * @param mf
151    * @param fr
152    */
153   public void appendFeatures(StringBuilder sb, int residuePos,
154           MappedFeatures mf, FeatureRendererModel fr)
155   {
156     for (SequenceFeature feature : mf.features)
157     {
158       appendFeature(sb, residuePos, fr, feature, mf);
159     }
160   }
161
162   /**
163    * Appends the feature at rpos to the given buffer
164    * 
165    * @param sb
166    * @param rpos
167    * @param minmax
168    * @param feature
169    */
170   void appendFeature(final StringBuilder sb, int rpos,
171           FeatureRendererModel fr, SequenceFeature feature,
172           MappedFeatures mf)
173   {
174     if (feature.isContactFeature())
175     {
176       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
177       {
178         if (sb.length() > 6)
179         {
180           sb.append("<br>");
181         }
182         sb.append(feature.getType()).append(" ").append(feature.getBegin())
183                 .append(":").append(feature.getEnd());
184       }
185       return;
186     }
187
188     if (sb.length() > 6)
189     {
190       sb.append("<br>");
191     }
192     // TODO: remove this hack to display link only features
193     boolean linkOnly = feature.getValue("linkonly") != null;
194     if (!linkOnly)
195     {
196       sb.append(feature.getType()).append(" ");
197       if (rpos != 0)
198       {
199         // we are marking a positional feature
200         sb.append(feature.begin);
201       }
202       if (feature.begin != feature.end)
203       {
204         sb.append(" ").append(feature.end);
205       }
206
207       String description = feature.getDescription();
208       if (description != null && !description.equals(feature.getType()))
209       {
210         description = StringUtils.stripHtmlTags(description);
211
212         /*
213          * truncate overlong descriptions unless they contain an href
214          * (as truncation could leave corrupted html)
215          */
216         boolean hasLink = description.indexOf("a href") > -1;
217         if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
218         {
219           description = description.substring(0, MAX_DESCRIPTION_LENGTH)
220                   + ELLIPSIS;
221         }
222         sb.append("; ").append(description);
223       }
224
225       if (showScore(feature, fr))
226       {
227         sb.append(" Score=").append(String.valueOf(feature.getScore()));
228       }
229       String status = (String) feature.getValue("status");
230       if (status != null && status.length() > 0)
231       {
232         sb.append("; (").append(status).append(")");
233       }
234
235       /*
236        * add attribute value if coloured by attribute
237        */
238       if (fr != null)
239       {
240         FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
241         if (fc != null && fc.isColourByAttribute())
242         {
243           String[] attName = fc.getAttributeName();
244           String attVal = feature.getValueAsString(attName);
245           if (attVal != null)
246           {
247             sb.append("; ").append(String.join(":", attName)).append("=")
248                     .append(attVal);
249           }
250         }
251       }
252
253       if (mf != null)
254       {
255         String variants = mf.findProteinVariants(feature);
256         if (!variants.isEmpty())
257         {
258           sb.append(" ").append(variants);
259         }
260       }
261     }
262   }
263
264   /**
265    * Answers true if score should be shown, else false. Score is shown if it is
266    * not NaN, and the feature type has a non-trivial min-max score range
267    */
268   boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
269   {
270     if (Float.isNaN(feature.getScore()))
271     {
272       return false;
273     }
274     if (fr == null)
275     {
276       return true;
277     }
278     float[][] minMax = fr.getMinMax().get(feature.getType());
279
280     /*
281      * minMax[0] is the [min, max] score range for positional features
282      */
283     if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
284     {
285       return false;
286     }
287     return true;
288   }
289
290   /**
291    * Format and appends any hyperlinks for the sequence feature to the string
292    * buffer
293    * 
294    * @param sb
295    * @param feature
296    */
297   void appendLinks(final StringBuffer sb, SequenceFeature feature)
298   {
299     if (feature.links != null)
300     {
301       if (linkImageURL != null)
302       {
303         sb.append(" <img src=\"" + linkImageURL + "\">");
304       }
305       else
306       {
307         for (String urlstring : feature.links)
308         {
309           try
310           {
311             for (List<String> urllink : createLinksFrom(null, urlstring))
312             {
313               sb.append("<br/> <a href=\""
314                       + urllink.get(3)
315                       + "\" target=\""
316                       + urllink.get(0)
317                       + "\">"
318                       + (urllink.get(0).toLowerCase()
319                               .equals(urllink.get(1).toLowerCase()) ? urllink
320                               .get(0) : (urllink.get(0) + ":" + urllink
321                               .get(1))) + "</a></br>");
322             }
323           } catch (Exception x)
324           {
325             System.err.println("problem when creating links from "
326                     + urlstring);
327             x.printStackTrace();
328           }
329         }
330       }
331
332     }
333   }
334
335   /**
336    * 
337    * @param seq
338    * @param link
339    * @return Collection< List<String> > { List<String> { link target, link
340    *         label, dynamic component inserted (if any), url }}
341    */
342   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
343   {
344     Map<String, List<String>> urlSets = new LinkedHashMap<>();
345     UrlLink urlLink = new UrlLink(link);
346     if (!urlLink.isValid())
347     {
348       System.err.println(urlLink.getInvalidMessage());
349       return null;
350     }
351
352     urlLink.createLinksFromSeq(seq, urlSets);
353
354     return urlSets.values();
355   }
356
357   public void createSequenceAnnotationReport(final StringBuilder tip,
358           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
359           FeatureRendererModel fr)
360   {
361     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
362             fr, false);
363   }
364
365   /**
366    * Builds an html formatted report of sequence details and appends it to the
367    * provided buffer.
368    * 
369    * @param sb
370    *          buffer to append report to
371    * @param sequence
372    *          the sequence the report is for
373    * @param showDbRefs
374    *          whether to include database references for the sequence
375    * @param showNpFeats
376    *          whether to include non-positional sequence features
377    * @param fr
378    * @param summary
379    * @return
380    */
381   int createSequenceAnnotationReport(final StringBuilder sb,
382           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
383           FeatureRendererModel fr, boolean summary)
384   {
385     String tmp;
386     sb.append("<i>");
387
388     int maxWidth = 0;
389     if (sequence.getDescription() != null)
390     {
391       tmp = sequence.getDescription();
392       sb.append(tmp);
393       maxWidth = Math.max(maxWidth, tmp.length());
394     }
395     SequenceI ds = sequence;
396     while (ds.getDatasetSequence() != null)
397     {
398       ds = ds.getDatasetSequence();
399     }
400
401     if (showDbRefs)
402     {
403       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
404     }
405
406     /*
407      * add non-positional features if wanted
408      */
409     if (showNpFeats)
410     {
411       for (SequenceFeature sf : sequence.getFeatures()
412               .getNonPositionalFeatures())
413       {
414         int sz = -sb.length();
415         appendFeature(sb, 0, fr, sf, null);
416         sz += sb.length();
417         maxWidth = Math.max(maxWidth, sz);
418       }
419     }
420     sb.append("</i>");
421     return maxWidth;
422   }
423
424   /**
425    * A helper method that appends any DBRefs, returning the maximum line length
426    * added
427    * 
428    * @param sb
429    * @param ds
430    * @param summary
431    * @return
432    */
433   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
434           boolean summary)
435   {
436     DBRefEntry[] dbrefs = ds.getDBRefs();
437     if (dbrefs == null)
438     {
439       return 0;
440     }
441
442     // note this sorts the refs held on the sequence!
443     Arrays.sort(dbrefs, comparator);
444     boolean ellipsis = false;
445     String source = null;
446     String lastSource = null;
447     int countForSource = 0;
448     int sourceCount = 0;
449     boolean moreSources = false;
450     int maxLineLength = 0;
451     int lineLength = 0;
452
453     for (DBRefEntry ref : dbrefs)
454     {
455       source = ref.getSource();
456       if (source == null)
457       {
458         // shouldn't happen
459         continue;
460       }
461       boolean sourceChanged = !source.equals(lastSource);
462       if (sourceChanged)
463       {
464         lineLength = 0;
465         countForSource = 0;
466         sourceCount++;
467       }
468       if (sourceCount > MAX_SOURCES && summary)
469       {
470         ellipsis = true;
471         moreSources = true;
472         break;
473       }
474       lastSource = source;
475       countForSource++;
476       if (countForSource == 1 || !summary)
477       {
478         sb.append("<br>");
479       }
480       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
481       {
482         String accessionId = ref.getAccessionId();
483         lineLength += accessionId.length() + 1;
484         if (countForSource > 1 && summary)
485         {
486           sb.append(", ").append(accessionId);
487           lineLength++;
488         }
489         else
490         {
491           sb.append(source).append(" ").append(accessionId);
492           lineLength += source.length();
493         }
494         maxLineLength = Math.max(maxLineLength, lineLength);
495       }
496       if (countForSource == MAX_REFS_PER_SOURCE && summary)
497       {
498         sb.append(COMMA).append(ELLIPSIS);
499         ellipsis = true;
500       }
501     }
502     if (moreSources)
503     {
504       sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
505     }
506     if (ellipsis)
507     {
508       sb.append("<br>(");
509       sb.append(MessageManager.getString("label.output_seq_details"));
510       sb.append(")");
511     }
512
513     return maxLineLength;
514   }
515
516   public void createTooltipAnnotationReport(final StringBuilder tip,
517           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
518           FeatureRendererModel fr)
519   {
520     int maxWidth = createSequenceAnnotationReport(tip, sequence,
521             showDbRefs, showNpFeats, fr, true);
522
523     if (maxWidth > 60)
524     {
525       // ? not sure this serves any useful purpose
526       // tip.insert(0, "<table width=350 border=0><tr><td>");
527       // tip.append("</td></tr></table>");
528     }
529   }
530 }