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