JAL-3706 handle virtual feature extending to unmapped stop codon
[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 java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Comparator;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29
30 import jalview.api.FeatureColourI;
31 import jalview.datamodel.DBRefEntry;
32 import jalview.datamodel.DBRefSource;
33 import jalview.datamodel.GeneLociI;
34 import jalview.datamodel.MappedFeatures;
35 import jalview.datamodel.SequenceFeature;
36 import jalview.datamodel.SequenceI;
37 import jalview.util.MessageManager;
38 import jalview.util.StringUtils;
39 import jalview.util.UrlLink;
40 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
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 String linkImageURL;
60
61   private static final String[][] PRIMARY_SOURCES = new String[][] {
62       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
63       DBRefSource.PROTEINDBS };
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<DBRefEntry>()
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   /**
124    * Constructor
125    */
126   public SequenceAnnotationReport(boolean isForTooltip)
127   {
128     if (linkImageURL == null)
129     {
130       linkImageURL = getClass().getResource("/images/link.gif").toString();
131     }
132   }
133
134   /**
135    * Append text for the list of features to the tooltip. Returns the number of
136    * features not added if maxlength limit is (or would have been) reached.
137    * 
138    * @param sb
139    * @param residuePos
140    * @param features
141    * @param minmax
142    * @param maxlength
143    */
144   public int appendFeatures(final StringBuilder sb,
145           int residuePos, List<SequenceFeature> features,
146           FeatureRendererModel fr, int maxlength)
147   {
148     for (int i = 0; i < features.size(); i++)
149     {
150       SequenceFeature feature = features.get(i);
151       if (appendFeature(sb, residuePos, fr, feature, null, maxlength))
152       {
153         return features.size() - i;
154       }
155     }
156     return 0;
157   }
158
159   /**
160    * Appends text for mapped features (e.g. CDS feature for peptide or vice
161    * versa) Returns number of features left if maxlength limit is (or would have
162    * been) reached.
163    * 
164    * @param sb
165    * @param residuePos
166    * @param mf
167    * @param fr
168    * @param maxlength
169    */
170   public int appendFeatures(StringBuilder sb, int residuePos,
171           MappedFeatures mf, FeatureRendererModel fr, int maxlength)
172   {
173     for (int i = 0; i < mf.features.size(); i++)
174     {
175       SequenceFeature feature = mf.features.get(i);
176       if (appendFeature(sb, residuePos, fr, feature, mf, maxlength))
177       {
178         return mf.features.size() - i;
179       }
180     }
181     return 0;
182   }
183
184   /**
185    * Appends the feature at rpos to the given buffer
186    * 
187    * @param sb
188    * @param rpos
189    * @param minmax
190    * @param feature
191    */
192   boolean appendFeature(final StringBuilder sb0, int rpos,
193           FeatureRendererModel fr, SequenceFeature feature,
194           MappedFeatures mf, int maxlength)
195   {
196     int begin = feature.getBegin();
197     int end = feature.getEnd();
198
199     /*
200      * if this is a virtual features, convert begin/end to the
201      * coordinates of the sequence it is mapped to
202      */
203     int[] localRange = null;
204     if (mf != null)
205     {
206       localRange = mf.getMappedPositions(begin, end);
207       if (localRange == null)
208       {
209         // something went wrong
210         return false;
211       }
212       begin = localRange[0];
213       end = localRange[localRange.length - 1];
214     }
215
216     StringBuilder sb = new StringBuilder();
217     if (feature.isContactFeature())
218     {
219       /*
220        * contact features are rendered slightly differently; note the check for
221        * 'start or end position only' was applied earlier when finding features
222        */
223       if (sb0.length() > 6)
224       {
225         sb.append("<br/>");
226       }
227       sb.append(feature.getType()).append(" ").append(begin).append(":")
228               .append(end);
229       return appendText(sb0, sb, maxlength);
230     }
231
232     if (sb0.length() > 6)
233     {
234       sb.append("<br/>");
235     }
236     // TODO: remove this hack to display link only features
237     boolean linkOnly = feature.getValue("linkonly") != null;
238     if (!linkOnly)
239     {
240       sb.append(feature.getType()).append(" ");
241       if (rpos != 0)
242       {
243         // we are marking a positional feature
244         sb.append(begin);
245         if (begin != end)
246         {
247           sb.append(" ").append(end);
248         }
249       }
250
251       String description = feature.getDescription();
252       if (description != null && !description.equals(feature.getType()))
253       {
254         description = StringUtils.stripHtmlTags(description);
255
256         /*
257          * truncate overlong descriptions unless they contain an href
258          * before the truncation point (as truncation could leave corrupted html)
259          */
260         int linkindex = description.toLowerCase().indexOf("<a ");
261         boolean hasLink = linkindex > -1
262                 && linkindex < MAX_DESCRIPTION_LENGTH;
263         if (description.length() > MAX_DESCRIPTION_LENGTH && !hasLink)
264         {
265           description = description.substring(0, MAX_DESCRIPTION_LENGTH)
266                   + ELLIPSIS;
267         }
268
269         sb.append("; ").append(description);
270       }
271
272       if (showScore(feature, fr))
273       {
274         sb.append(" Score=").append(String.valueOf(feature.getScore()));
275       }
276       String status = (String) feature.getValue("status");
277       if (status != null && status.length() > 0)
278       {
279         sb.append("; (").append(status).append(")");
280       }
281
282       /*
283        * add attribute value if coloured by attribute
284        */
285       if (fr != null)
286       {
287         FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
288         if (fc != null && fc.isColourByAttribute())
289         {
290           String[] attName = fc.getAttributeName();
291           String attVal = feature.getValueAsString(attName);
292           if (attVal != null)
293           {
294             sb.append("; ").append(String.join(":", attName)).append("=")
295                     .append(attVal);
296           }
297         }
298       }
299
300       if (mf != null)
301       {
302         String variants = mf.findProteinVariants(feature);
303         if (!variants.isEmpty())
304         {
305           sb.append(" ").append(variants);
306         }
307       }
308     }
309     return appendText(sb0, sb, maxlength);
310   }
311
312   /**
313    * Appends sb to sb0, and returns false, unless maxlength is not zero and
314    * appending would make the result longer than or equal to maxlength, in which
315    * case the append is not done and returns true
316    * 
317    * @param sb0
318    * @param sb
319    * @param maxlength
320    * @return
321    */
322   private static boolean appendText(StringBuilder sb0, StringBuilder sb,
323           int maxlength)
324   {
325     if (maxlength == 0 || sb0.length() + sb.length() < maxlength)
326     {
327       sb0.append(sb);
328       return false;
329     }
330     return true;
331   }
332
333   /**
334    * Answers true if score should be shown, else false. Score is shown if it is
335    * not NaN, and the feature type has a non-trivial min-max score range
336    */
337   boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
338   {
339     if (Float.isNaN(feature.getScore()))
340     {
341       return false;
342     }
343     if (fr == null)
344     {
345       return true;
346     }
347     float[][] minMax = fr.getMinMax().get(feature.getType());
348
349     /*
350      * minMax[0] is the [min, max] score range for positional features
351      */
352     if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
353     {
354       return false;
355     }
356     return true;
357   }
358
359   /**
360    * Format and appends any hyperlinks for the sequence feature to the string
361    * buffer
362    * 
363    * @param sb
364    * @param feature
365    */
366   void appendLinks(final StringBuffer sb, SequenceFeature feature)
367   {
368     if (feature.links != null)
369     {
370       if (linkImageURL != null)
371       {
372         sb.append(" <img src=\"" + linkImageURL + "\">");
373       }
374       else
375       {
376         for (String urlstring : feature.links)
377         {
378           try
379           {
380             for (List<String> urllink : createLinksFrom(null, urlstring))
381             {
382               sb.append("<br/> <a href=\""
383                       + urllink.get(3)
384                       + "\" target=\""
385                       + urllink.get(0)
386                       + "\">"
387                       + (urllink.get(0).toLowerCase()
388                               .equals(urllink.get(1).toLowerCase()) ? urllink
389                               .get(0) : (urllink.get(0) + ":" + urllink
390                                               .get(1)))
391                       + "</a><br/>");
392             }
393           } catch (Exception x)
394           {
395             System.err.println("problem when creating links from "
396                     + urlstring);
397             x.printStackTrace();
398           }
399         }
400       }
401
402     }
403   }
404
405   /**
406    * 
407    * @param seq
408    * @param link
409    * @return Collection< List<String> > { List<String> { link target, link
410    *         label, dynamic component inserted (if any), url }}
411    */
412   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
413   {
414     Map<String, List<String>> urlSets = new LinkedHashMap<>();
415     UrlLink urlLink = new UrlLink(link);
416     if (!urlLink.isValid())
417     {
418       System.err.println(urlLink.getInvalidMessage());
419       return null;
420     }
421
422     urlLink.createLinksFromSeq(seq, urlSets);
423
424     return urlSets.values();
425   }
426
427   public void createSequenceAnnotationReport(final StringBuilder tip,
428           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
429           FeatureRendererModel fr)
430   {
431     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
432             fr, false);
433   }
434
435   /**
436    * Builds an html formatted report of sequence details and appends it to the
437    * provided buffer.
438    * 
439    * @param sb
440    *          buffer to append report to
441    * @param sequence
442    *          the sequence the report is for
443    * @param showDbRefs
444    *          whether to include database references for the sequence
445    * @param showNpFeats
446    *          whether to include non-positional sequence features
447    * @param fr
448    * @param summary
449    * @return
450    */
451   int createSequenceAnnotationReport(final StringBuilder sb,
452           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
453           FeatureRendererModel fr, boolean summary)
454   {
455     String tmp;
456     sb.append("<i>");
457
458     int maxWidth = 0;
459     if (sequence.getDescription() != null)
460     {
461       tmp = sequence.getDescription();
462       sb.append(tmp);
463       maxWidth = Math.max(maxWidth, tmp.length());
464     }
465     SequenceI ds = sequence;
466     while (ds.getDatasetSequence() != null)
467     {
468       ds = ds.getDatasetSequence();
469     }
470
471     if (showDbRefs)
472     {
473       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
474     }
475
476     /*
477      * add non-positional features if wanted
478      */
479     if (showNpFeats)
480     {
481       for (SequenceFeature sf : sequence.getFeatures()
482               .getNonPositionalFeatures())
483       {
484         int sz = -sb.length();
485         appendFeature(sb, 0, fr, sf, null, 0);
486         sz += sb.length();
487         maxWidth = Math.max(maxWidth, sz);
488       }
489     }
490     sb.append("</i>");
491     return maxWidth;
492   }
493
494   /**
495    * A helper method that appends any DBRefs, returning the maximum line length
496    * added
497    * 
498    * @param sb
499    * @param ds
500    * @param summary
501    * @return
502    */
503   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
504           boolean summary)
505   {
506     DBRefEntry[] dbrefs = ds.getDBRefs();
507     if (dbrefs == null)
508     {
509       return 0;
510     }
511
512     // note this sorts the refs held on the sequence!
513     Arrays.sort(dbrefs, comparator);
514     boolean ellipsis = false;
515     String source = null;
516     String lastSource = null;
517     int countForSource = 0;
518     int sourceCount = 0;
519     boolean moreSources = false;
520     int maxLineLength = 0;
521     int lineLength = 0;
522
523     for (DBRefEntry ref : dbrefs)
524     {
525       source = ref.getSource();
526       if (source == null)
527       {
528         // shouldn't happen
529         continue;
530       }
531       boolean sourceChanged = !source.equals(lastSource);
532       if (sourceChanged)
533       {
534         lineLength = 0;
535         countForSource = 0;
536         sourceCount++;
537       }
538       if (sourceCount > MAX_SOURCES && summary)
539       {
540         ellipsis = true;
541         moreSources = true;
542         break;
543       }
544       lastSource = source;
545       countForSource++;
546       if (countForSource == 1 || !summary)
547       {
548         sb.append("<br/>");
549       }
550       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
551       {
552         String accessionId = ref.getAccessionId();
553         lineLength += accessionId.length() + 1;
554         if (countForSource > 1 && summary)
555         {
556           sb.append(", ").append(accessionId);
557           lineLength++;
558         }
559         else
560         {
561           sb.append(source).append(" ").append(accessionId);
562           lineLength += source.length();
563         }
564         maxLineLength = Math.max(maxLineLength, lineLength);
565       }
566       if (countForSource == MAX_REFS_PER_SOURCE && summary)
567       {
568         sb.append(COMMA).append(ELLIPSIS);
569         ellipsis = true;
570       }
571     }
572     if (moreSources)
573     {
574       sb.append("<br/>").append(source).append(COMMA).append(ELLIPSIS);
575     }
576     if (ellipsis)
577     {
578       sb.append("<br/>(");
579       sb.append(MessageManager.getString("label.output_seq_details"));
580       sb.append(")");
581     }
582
583     return maxLineLength;
584   }
585
586   public void createTooltipAnnotationReport(final StringBuilder tip,
587           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
588           FeatureRendererModel fr)
589   {
590     int maxWidth = createSequenceAnnotationReport(tip, sequence,
591             showDbRefs, showNpFeats, fr, true);
592
593     if (maxWidth > 60)
594     {
595       // ? not sure this serves any useful purpose
596       // tip.insert(0, "<table width=350 border=0><tr><td>");
597       // tip.append("</td></tr></table>");
598     }
599   }
600 }