568b94548ca5990f7155e022f8294dab3da0159a
[jalview.git] / src / jalview / gui / FeatureRenderer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer
3  * Copyright (C) 2007 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18  */
19 package jalview.gui;
20
21 import java.util.*;
22
23 import java.awt.*;
24 import java.awt.event.*;
25 import java.awt.image.*;
26 import javax.swing.*;
27
28 import jalview.datamodel.*;
29
30 /**
31  * DOCUMENT ME!
32  *
33  * @author $author$
34  * @version $Revision$
35  */
36 public class FeatureRenderer
37 {
38   AlignViewport av;
39   Color resBoxColour;
40   float transparency = 1.0f;
41   FontMetrics fm;
42   int charOffset;
43
44   Hashtable featureColours = new Hashtable();
45
46   // A higher level for grouping features of a
47   // particular type
48   Hashtable featureGroups = null;
49
50   // This is actually an Integer held in the hashtable,
51   // Retrieved using the key feature type
52   Object currentColour;
53
54   String[] renderOrder;
55
56   /**
57    * Creates a new FeatureRenderer object.
58    *
59    * @param av DOCUMENT ME!
60    */
61   public FeatureRenderer(AlignViewport av)
62   {
63     this.av = av;
64   }
65
66   public void transferSettings(FeatureRenderer fr)
67   {
68     renderOrder = fr.renderOrder;
69     featureGroups = fr.featureGroups;
70     featureColours = fr.featureColours;
71     transparency = fr.transparency;
72   }
73
74   BufferedImage offscreenImage;
75   boolean offscreenRender = false;
76   public Color findFeatureColour(Color initialCol, SequenceI seq, int res)
77   {
78     return new Color(findFeatureColour(initialCol.getRGB(),
79                                        seq, res));
80   }
81
82   /**
83    * This is used by the Molecule Viewer and Overview to
84    * get the accurate colourof the rendered sequence
85    */
86   public int findFeatureColour(int initialCol, SequenceI seq, int column)
87   {
88     if (!av.showSequenceFeatures)
89     {
90       return initialCol;
91     }
92
93     if (seq != lastSeq)
94     {
95       lastSeq = seq;
96       sequenceFeatures = lastSeq.getDatasetSequence().getSequenceFeatures();
97       if (sequenceFeatures == null)
98       {
99         return initialCol;
100       }
101
102       sfSize = sequenceFeatures.length;
103     }
104
105     if (jalview.util.Comparison.isGap(lastSeq.getCharAt(column)))
106     {
107       return Color.white.getRGB();
108     }
109
110     //Only bother making an offscreen image if transparency is applied
111     if (transparency != 1.0f && offscreenImage == null)
112     {
113       offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
114     }
115
116     currentColour = null;
117
118     offscreenRender = true;
119
120     if (offscreenImage != null)
121     {
122       offscreenImage.setRGB(0, 0, initialCol);
123       drawSequence(offscreenImage.getGraphics(),
124                    lastSeq,
125                    column, column, 0);
126
127       return offscreenImage.getRGB(0, 0);
128     }
129     else
130     {
131       drawSequence(null,
132                    lastSeq,
133                    lastSeq.findPosition(column),
134                    -1, -1);
135
136       if (currentColour == null)
137       {
138         return initialCol;
139       }
140       else
141       {
142         return ( (Integer) currentColour).intValue();
143       }
144     }
145
146   }
147
148   /**
149    * DOCUMENT ME!
150    *
151    * @param g DOCUMENT ME!
152    * @param seq DOCUMENT ME!
153    * @param sg DOCUMENT ME!
154    * @param start DOCUMENT ME!
155    * @param end DOCUMENT ME!
156    * @param x1 DOCUMENT ME!
157    * @param y1 DOCUMENT ME!
158    * @param width DOCUMENT ME!
159    * @param height DOCUMENT ME!
160    */
161   // String type;
162   // SequenceFeature sf;
163   SequenceI lastSeq;
164   SequenceFeature[] sequenceFeatures;
165   int sfSize, sfindex, spos, epos;
166
167   public void drawSequence(Graphics g, SequenceI seq,
168                            int start, int end, int y1)
169   {
170     if (seq.getDatasetSequence().getSequenceFeatures() == null
171         || seq.getDatasetSequence().getSequenceFeatures().length == 0)
172     {
173       return;
174     }
175
176     if (g != null)
177     {
178       fm = g.getFontMetrics();
179     }
180
181     if (av.featuresDisplayed == null
182         || renderOrder == null
183         || newFeatureAdded)
184     {
185       findAllFeatures();
186       if (av.featuresDisplayed.size() < 1)
187       {
188         return;
189       }
190
191       sequenceFeatures = seq.getDatasetSequence().getSequenceFeatures();
192       sfSize = sequenceFeatures.length;
193     }
194
195     if (lastSeq == null || seq != lastSeq)
196     {
197       lastSeq = seq;
198       sequenceFeatures = seq.getDatasetSequence().getSequenceFeatures();
199       sfSize = sequenceFeatures.length;
200     }
201
202     if (transparency != 1 && g != null)
203     {
204       Graphics2D g2 = (Graphics2D) g;
205       g2.setComposite(
206           AlphaComposite.getInstance(
207               AlphaComposite.SRC_OVER, transparency));
208     }
209
210     if (!offscreenRender)
211     {
212       spos = lastSeq.findPosition(start);
213       epos = lastSeq.findPosition(end);
214     }
215
216     String type;
217     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
218     {
219       type = renderOrder[renderIndex];
220
221       if (type == null || !av.featuresDisplayed.containsKey(type))
222       {
223         continue;
224       }
225
226       // loop through all features in sequence to find
227       // current feature to render
228       for (sfindex = 0; sfindex < sfSize; sfindex++)
229       {
230         if (sequenceFeatures.length <= sfindex)
231         {
232           continue;
233         }
234         if (!sequenceFeatures[sfindex].type.equals(type))
235         {
236           continue;
237         }
238
239         if (featureGroups != null
240             && sequenceFeatures[sfindex].featureGroup != null
241             &&
242             featureGroups.containsKey(sequenceFeatures[sfindex].featureGroup)
243             &&
244             ! ( (Boolean) featureGroups.get(sequenceFeatures[sfindex].
245                                             featureGroup)).
246             booleanValue())
247         {
248           continue;
249         }
250
251         if (!offscreenRender && (sequenceFeatures[sfindex].getBegin() > epos
252                                  || sequenceFeatures[sfindex].getEnd() < spos))
253         {
254           continue;
255         }
256
257         if (offscreenRender && offscreenImage == null)
258         {
259           if (sequenceFeatures[sfindex].begin <= start &&
260               sequenceFeatures[sfindex].end >= start)
261           {
262             currentColour = av.featuresDisplayed.get(sequenceFeatures[sfindex].
263                 type);
264           }
265         }
266         else if (sequenceFeatures[sfindex].type.equals("disulfide bond"))
267         {
268
269           renderFeature(g, seq,
270                         seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
271                         seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
272                         new Color( ( (Integer) av.featuresDisplayed.get(
273                             sequenceFeatures[sfindex].type)).intValue()),
274                         start, end, y1);
275           renderFeature(g, seq,
276                         seq.findIndex(sequenceFeatures[sfindex].end) - 1,
277                         seq.findIndex(sequenceFeatures[sfindex].end) - 1,
278                         new Color( ( (Integer) av.featuresDisplayed.get(
279                             sequenceFeatures[sfindex].type)).intValue()),
280                         start, end, y1);
281
282         }
283         else
284         {
285           renderFeature(g, seq,
286                         seq.findIndex(sequenceFeatures[sfindex].begin) - 1,
287                         seq.findIndex(sequenceFeatures[sfindex].end) - 1,
288                         getColour(sequenceFeatures[sfindex].type),
289                         start, end, y1);
290         }
291
292       }
293
294     }
295
296     if (transparency != 1.0f && g != null)
297     {
298       Graphics2D g2 = (Graphics2D) g;
299       g2.setComposite(
300           AlphaComposite.getInstance(
301               AlphaComposite.SRC_OVER, 1.0f));
302     }
303   }
304
305   char s;
306   int i;
307   void renderFeature(Graphics g, SequenceI seq,
308                      int fstart, int fend, Color featureColour, int start,
309                      int end, int y1)
310   {
311
312     if ( ( (fstart <= end) && (fend >= start)))
313     {
314       if (fstart < start)
315       { // fix for if the feature we have starts before the sequence start,
316         fstart = start; // but the feature end is still valid!!
317       }
318
319       if (fend >= end)
320       {
321         fend = end;
322       }
323       int pady = (y1 + av.charHeight) - av.charHeight / 5;
324       for (i = fstart; i <= fend; i++)
325       {
326         s = seq.getCharAt(i);
327
328         if (jalview.util.Comparison.isGap(s))
329         {
330           continue;
331         }
332
333         g.setColor(featureColour);
334
335         g.fillRect( (i - start) * av.charWidth, y1, av.charWidth, av.charHeight);
336
337         if (offscreenRender || !av.validCharWidth)
338         {
339           continue;
340         }
341
342         g.setColor(Color.white);
343         charOffset = (av.charWidth - fm.charWidth(s)) / 2;
344         g.drawString(String.valueOf(s),
345                      charOffset + (av.charWidth * (i - start)),
346                      pady);
347
348       }
349     }
350   }
351
352   boolean newFeatureAdded = false;
353
354   public void featuresAdded()
355   {
356     lastSeq=null;
357     findAllFeatures();
358   }
359
360   boolean findingFeatures = false;
361   synchronized void findAllFeatures()
362   {
363     newFeatureAdded = false;
364
365     if (findingFeatures)
366     {
367       newFeatureAdded = true;
368       return;
369     }
370
371     findingFeatures = true;
372     jalview.schemes.UserColourScheme ucs = new
373         jalview.schemes.UserColourScheme();
374
375     if (av.featuresDisplayed == null)
376     {
377       av.featuresDisplayed = new Hashtable();
378     }
379
380     av.featuresDisplayed.clear();
381
382     Vector allfeatures = new Vector();
383     for (int i = 0; i < av.alignment.getHeight(); i++)
384     {
385       SequenceFeature[] features
386           = av.alignment.getSequenceAt(i).getDatasetSequence().
387           getSequenceFeatures();
388
389       if (features == null)
390       {
391         continue;
392       }
393
394       int index = 0;
395       while (index < features.length)
396       {
397         if (!av.featuresDisplayed.containsKey(features[index].getType()))
398         {
399           if (! (features[index].begin == 0 && features[index].end == 0))
400           {
401             // If beginning and end are 0, the feature is for the whole sequence
402             // and we don't want to render the feature in the normal way
403
404             if (getColour(features[index].getType()) == null)
405             {
406               featureColours.put(features[index].getType(),
407                                  ucs.createColourFromName(features[index].
408                   getType()));
409             }
410
411             av.featuresDisplayed.put(features[index].getType(),
412                                      new Integer(getColour(features[index].
413                 getType()).getRGB()));
414             allfeatures.addElement(features[index].getType());
415           }
416         }
417         index++;
418       }
419     }
420
421     renderOrder = new String[allfeatures.size()];
422     Enumeration en = allfeatures.elements();
423     int i = allfeatures.size() - 1;
424     while (en.hasMoreElements())
425     {
426       renderOrder[i] = en.nextElement().toString();
427       i--;
428     }
429
430     findingFeatures = false;
431   }
432
433   public Color getColour(String featureType)
434   {
435     Color colour = (Color) featureColours.get(featureType);
436     return colour;
437   }
438
439   static String lastFeatureAdded;
440   static String lastFeatureGroupAdded;
441   static String lastDescriptionAdded;
442
443   public boolean createNewFeatures(SequenceI[] sequences,
444                                    SequenceFeature[] features)
445   {
446     return amendFeatures(sequences, features, true, null);
447   }
448
449   int featureIndex = 0;
450   boolean amendFeatures(final SequenceI[] sequences,
451                         final SequenceFeature[] features,
452                         boolean newFeatures,
453                         final AlignmentPanel ap)
454   {
455     JPanel bigPanel = new JPanel(new BorderLayout());
456     final JComboBox name = new JComboBox();
457     final JComboBox source = new JComboBox();
458     final JTextArea description = new JTextArea(3, 25);
459     final JSpinner start = new JSpinner();
460     final JSpinner end = new JSpinner();
461     start.setPreferredSize(new Dimension(80, 20));
462     end.setPreferredSize(new Dimension(80, 20));
463     final JPanel colour = new JPanel();
464     colour.setBorder(BorderFactory.createEtchedBorder());
465     colour.setMaximumSize(new Dimension(40, 10));
466     colour.addMouseListener(new MouseAdapter()
467     {
468       public void mousePressed(MouseEvent evt)
469       {
470         colour.setBackground(
471             JColorChooser.showDialog(Desktop.desktop,
472                                      "Select Feature Colour",
473                                      colour.getBackground()));
474       }
475     });
476
477     JPanel panel = new JPanel(new GridLayout(3, 2));
478     panel.add(new JLabel("Sequence Feature Name: ", JLabel.RIGHT));
479     panel.add(name);
480     panel.add(new JLabel("Feature Group: ", JLabel.RIGHT));
481     panel.add(source);
482     panel.add(new JLabel("Feature Colour: ", JLabel.RIGHT));
483     JPanel tmp = new JPanel();
484     tmp.add(colour);
485     colour.setPreferredSize(new Dimension(150, 15));
486     panel.add(tmp);
487     name.setEditable(true);
488     source.setEditable(true);
489
490     bigPanel.add(panel, BorderLayout.NORTH);
491     panel = new JPanel();
492     panel.add(new JLabel("Description: ", JLabel.RIGHT));
493     description.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
494     description.setLineWrap(true);
495     panel.add(new JScrollPane(description));
496
497     if (!newFeatures)
498     {
499       bigPanel.add(panel, BorderLayout.SOUTH);
500
501       panel = new JPanel();
502       panel.add(new JLabel(" Start:", JLabel.RIGHT));
503       panel.add(start);
504       panel.add(new JLabel("  End:", JLabel.RIGHT));
505       panel.add(end);
506       bigPanel.add(panel, BorderLayout.CENTER);
507     }
508     else
509     {
510       bigPanel.add(panel, BorderLayout.CENTER);
511     }
512
513     if (lastFeatureAdded == null)
514     {
515       if (features[0].type != null)
516       {
517         lastFeatureAdded = features[0].type;
518       }
519       else
520       {
521         lastFeatureAdded = "feature_1";
522       }
523     }
524
525     if (lastFeatureGroupAdded == null)
526     {
527       if (features[0].featureGroup != null)
528       {
529         lastFeatureGroupAdded = features[0].featureGroup;
530       }
531       else
532       {
533         lastFeatureAdded = "Jalview";
534       }
535     }
536
537     Enumeration en;
538     if (featureGroups != null)
539     {
540       en = featureGroups.keys();
541       while (en.hasMoreElements())
542       {
543         source.addItem(en.nextElement().toString());
544       }
545     }
546
547     if (newFeatures)
548     {
549       if (av.featuresDisplayed != null)
550       {
551         en = av.featuresDisplayed.keys();
552         while (en.hasMoreElements())
553         {
554           name.addItem(en.nextElement().toString());
555         }
556       }
557
558       name.setSelectedItem(lastFeatureAdded);
559       source.setSelectedItem(lastFeatureGroupAdded);
560       description.setText(
561           lastDescriptionAdded == null ?
562           features[0].description : lastDescriptionAdded);
563
564       if (getColour(lastFeatureAdded) != null)
565       {
566         colour.setBackground(getColour(lastFeatureAdded));
567       }
568       else
569       {
570         colour.setBackground(new Color(60, 160, 115));
571       }
572
573     }
574     else if (!newFeatures)
575     {
576       featureIndex = 0;
577       for (int f = 0; f < features.length; f++)
578       {
579         name.addItem(features[f].getType().toString());
580       }
581
582       description.setText(features[0].getDescription());
583       source.setSelectedItem(features[0].getFeatureGroup());
584       start.setValue(new Integer(features[0].getBegin()));
585       end.setValue(new Integer(features[0].getEnd()));
586       colour.setBackground(
587           getColour(name.getSelectedItem().toString()));
588       name.addItemListener(new ItemListener()
589       {
590         public void itemStateChanged(ItemEvent e)
591         {
592           int index = name.getSelectedIndex();
593           if (index != -1)
594           {
595             featureIndex = index;
596             description.setText(features[index].getDescription());
597             source.setSelectedItem(features[index].getFeatureGroup());
598             start.setValue(new Integer(features[index].getBegin()));
599             end.setValue(new Integer(features[index].getEnd()));
600             colour.setBackground(
601                 getColour(name.getSelectedItem().toString()));
602
603             SearchResults highlight = new SearchResults();
604             highlight.addResult(sequences[0],
605                                 features[index].getBegin(),
606                                 features[index].getEnd());
607
608             ap.seqPanel.seqCanvas.highlightSearchResults(highlight);
609
610           }
611           Color col = getColour(name.getSelectedItem().toString());
612           if (col == null)
613           {
614             col = new
615                 jalview.schemes.UserColourScheme()
616                 .createColourFromName(name.getSelectedItem().toString());
617           }
618
619           colour.setBackground(col);
620         }
621       });
622
623     }
624
625     Object[] options;
626     if (!newFeatures)
627     {
628       options = new Object[]
629           {
630           "Amend", "Delete", "Cancel"};
631     }
632     else
633     {
634       options = new Object[]
635           {
636           "OK", "Cancel"};
637     }
638
639     String title = newFeatures ? "Create New Sequence Feature(s)" :
640         "Amend/Delete Features for "
641         + sequences[0].getName();
642
643     int reply = JOptionPane.showInternalOptionDialog(Desktop.desktop,
644         bigPanel,
645         title,
646         JOptionPane.YES_NO_CANCEL_OPTION,
647         JOptionPane.QUESTION_MESSAGE,
648         null,
649         options, "OK");
650
651     jalview.io.FeaturesFile ffile = new jalview.io.FeaturesFile();
652
653     if (reply == JOptionPane.OK_OPTION
654         && name.getSelectedItem() != null
655         && source.getSelectedItem() != null)
656     {
657       //This ensures that the last sequence
658       //is refreshed and new features are rendered
659       lastSeq = null;
660       lastFeatureAdded = name.getSelectedItem().toString();
661       lastFeatureGroupAdded = source.getSelectedItem().toString();
662       lastDescriptionAdded = description.getText().replaceAll("\n", " ");
663     }
664
665     if (!newFeatures)
666     {
667       SequenceFeature sf = features[featureIndex];
668
669       if (reply == JOptionPane.NO_OPTION)
670       {
671         sequences[0].getDatasetSequence().deleteFeature(sf);
672       }
673       else if (reply == JOptionPane.YES_OPTION)
674       {
675         sf.type = lastFeatureAdded;
676         sf.featureGroup = lastFeatureGroupAdded;
677         sf.description = lastDescriptionAdded;
678         setColour(sf.type, colour.getBackground());
679         try
680         {
681           sf.begin = ( (Integer) start.getValue()).intValue();
682           sf.end = ( (Integer) end.getValue()).intValue();
683         }
684         catch (NumberFormatException ex)
685         {}
686
687         ffile.parseDescriptionHTML(sf, false);
688       }
689     }
690     else
691     {
692       if (reply == JOptionPane.OK_OPTION
693           && name.getSelectedItem() != null
694           && source.getSelectedItem() != null)
695       {
696         for (int i = 0; i < sequences.length; i++)
697         {
698           features[i].type = lastFeatureAdded;
699           features[i].featureGroup = lastFeatureGroupAdded;
700           features[i].description = lastDescriptionAdded;
701           sequences[i].addSequenceFeature(features[i]);
702           ffile.parseDescriptionHTML(features[i], false);
703         }
704
705         if (av.featuresDisplayed == null)
706         {
707           av.featuresDisplayed = new Hashtable();
708         }
709
710         if (featureGroups == null)
711         {
712           featureGroups = new Hashtable();
713         }
714
715         featureGroups.put(lastFeatureGroupAdded, new Boolean(true));
716
717         Color col = colour.getBackground();
718         setColour(lastFeatureAdded, colour.getBackground());
719
720         av.featuresDisplayed.put(lastFeatureGroupAdded,
721                                  new Integer(col.getRGB()));
722
723         findAllFeatures();
724
725         return true;
726       }
727       else
728       {
729         return false;
730       }
731     }
732
733     if (name.getSelectedIndex() == -1)
734     {
735       findAllFeatures();
736     }
737
738     return true;
739   }
740
741   public void setColour(String featureType, Color col)
742   {
743     featureColours.put(featureType, col);
744   }
745
746   public void setTransparency(float value)
747   {
748     transparency = value;
749   }
750
751   public float getTransparency()
752   {
753     return transparency;
754   }
755
756   public void setFeaturePriority(Object[][] data)
757   {
758     // The feature table will display high priority
759     // features at the top, but theses are the ones
760     // we need to render last, so invert the data
761     if (av.featuresDisplayed != null)
762     {
763       av.featuresDisplayed.clear();
764     }
765     else
766     {
767       av.featuresDisplayed = new Hashtable();
768     }
769
770     renderOrder = new String[data.length];
771
772     if (data.length > 0)
773     {
774       for (int i = 0; i < data.length; i++)
775       {
776         String type = data[i][0].toString();
777         setColour(type, (Color) data[i][1]);
778         if ( ( (Boolean) data[i][2]).booleanValue())
779         {
780           av.featuresDisplayed.put(type, new Integer(getColour(type).getRGB()));
781         }
782
783         renderOrder[data.length - i - 1] = type;
784       }
785     }
786
787   }
788
789 }