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