JAL-2483 efficient lookup of feature types and groups - applet
[jalview.git] / src / jalview / appletgui / FeatureSettings.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.appletgui;
22
23 import jalview.api.FeatureColourI;
24 import jalview.api.FeatureSettingsControllerI;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.SequenceI;
27 import jalview.util.MessageManager;
28
29 import java.awt.BorderLayout;
30 import java.awt.Button;
31 import java.awt.Checkbox;
32 import java.awt.Color;
33 import java.awt.Component;
34 import java.awt.Dimension;
35 import java.awt.Font;
36 import java.awt.FontMetrics;
37 import java.awt.Frame;
38 import java.awt.Graphics;
39 import java.awt.GridLayout;
40 import java.awt.Image;
41 import java.awt.Label;
42 import java.awt.MenuItem;
43 import java.awt.Panel;
44 import java.awt.PopupMenu;
45 import java.awt.ScrollPane;
46 import java.awt.Scrollbar;
47 import java.awt.event.ActionEvent;
48 import java.awt.event.ActionListener;
49 import java.awt.event.AdjustmentEvent;
50 import java.awt.event.AdjustmentListener;
51 import java.awt.event.InputEvent;
52 import java.awt.event.ItemEvent;
53 import java.awt.event.ItemListener;
54 import java.awt.event.MouseEvent;
55 import java.awt.event.MouseListener;
56 import java.awt.event.MouseMotionListener;
57 import java.awt.event.WindowAdapter;
58 import java.awt.event.WindowEvent;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Set;
65
66 public class FeatureSettings extends Panel implements ItemListener,
67         MouseListener, MouseMotionListener, ActionListener,
68         AdjustmentListener, FeatureSettingsControllerI
69 {
70   FeatureRenderer fr;
71
72   AlignmentPanel ap;
73
74   AlignViewport av;
75
76   Frame frame;
77
78   Panel groupPanel;
79
80   Panel featurePanel = new Panel();
81
82   ScrollPane scrollPane;
83
84   Image linkImage;
85
86   Scrollbar transparency;
87
88   public FeatureSettings(final AlignmentPanel ap)
89   {
90     this.ap = ap;
91     this.av = ap.av;
92     ap.av.featureSettings = this;
93     fr = ap.seqPanel.seqCanvas.getFeatureRenderer();
94
95     transparency = new Scrollbar(Scrollbar.HORIZONTAL,
96             100 - (int) (fr.getTransparency() * 100), 1, 1, 100);
97
98     transparency.addAdjustmentListener(this);
99
100     java.net.URL url = getClass().getResource("/images/link.gif");
101     if (url != null)
102     {
103       linkImage = java.awt.Toolkit.getDefaultToolkit().getImage(url);
104     }
105
106     if (av.isShowSequenceFeatures() || !fr.hasRenderOrder())
107     {
108       fr.findAllFeatures(true); // was default - now true to make all visible
109     }
110
111     discoverAllFeatureData();
112
113     this.setLayout(new BorderLayout());
114     scrollPane = new ScrollPane();
115     scrollPane.add(featurePanel);
116     if (fr.getAllFeatureColours() != null
117             && fr.getAllFeatureColours().size() > 0)
118     {
119       add(scrollPane, BorderLayout.CENTER);
120     }
121
122     Button invert = new Button("Invert Selection");
123     invert.addActionListener(this);
124
125     Panel lowerPanel = new Panel(new GridLayout(2, 1, 5, 10));
126     lowerPanel.add(invert);
127
128     Panel tPanel = new Panel(new BorderLayout());
129
130     tPanel.add(transparency, BorderLayout.CENTER);
131     tPanel.add(new Label("Transparency"), BorderLayout.EAST);
132
133     lowerPanel.add(tPanel, BorderLayout.SOUTH);
134
135     add(lowerPanel, BorderLayout.SOUTH);
136
137     if (groupPanel != null)
138     {
139       groupPanel.setLayout(new GridLayout(
140               (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was
141                                                         // scaled on number of
142                                                         // visible groups. seems
143                                                         // broken
144       groupPanel.validate();
145
146       add(groupPanel, BorderLayout.NORTH);
147     }
148     frame = new Frame();
149     frame.add(this);
150     final FeatureSettings me = this;
151     frame.addWindowListener(new WindowAdapter()
152     {
153       @Override
154       public void windowClosing(WindowEvent e)
155       {
156         if (me.av.featureSettings == me)
157         {
158           me.av.featureSettings = null;
159           me.ap = null;
160           me.av = null;
161         }
162       }
163     });
164     int height = featurePanel.getComponentCount() * 50 + 60;
165
166     height = Math.max(200, height);
167     height = Math.min(400, height);
168     int width = 300;
169     jalview.bin.JalviewLite.addFrame(frame,
170             MessageManager.getString("label.sequence_feature_settings"),
171             width, height);
172   }
173
174   @Override
175   public void paint(Graphics g)
176   {
177     g.setColor(Color.black);
178     g.drawString(MessageManager
179             .getString("label.no_features_added_to_this_alignment"), 10, 20);
180     g.drawString(MessageManager
181             .getString("label.features_can_be_added_from_searches_1"), 10,
182             40);
183     g.drawString(MessageManager
184             .getString("label.features_can_be_added_from_searches_2"), 10,
185             60);
186   }
187
188   protected void popupSort(final MyCheckbox check,
189           final Map<String, float[][]> minmax, int x, int y)
190   {
191     final String type = check.type;
192     final FeatureColourI typeCol = fr.getFeatureStyle(type);
193     PopupMenu men = new PopupMenu(MessageManager.formatMessage(
194             "label.settings_for_type", new String[] { type }));
195     java.awt.MenuItem scr = new MenuItem(
196             MessageManager.getString("label.sort_by_score"));
197     men.add(scr);
198     final FeatureSettings me = this;
199     scr.addActionListener(new ActionListener()
200     {
201
202       @Override
203       public void actionPerformed(ActionEvent e)
204       {
205         me.ap.alignFrame.avc.sortAlignmentByFeatureScore(Arrays
206                 .asList(new String[] { type }));
207       }
208
209     });
210     MenuItem dens = new MenuItem(
211             MessageManager.getString("label.sort_by_density"));
212     dens.addActionListener(new ActionListener()
213     {
214
215       @Override
216       public void actionPerformed(ActionEvent e)
217       {
218         me.ap.alignFrame.avc.sortAlignmentByFeatureDensity(Arrays
219                 .asList(new String[] { type }));
220       }
221
222     });
223     men.add(dens);
224
225     if (minmax != null)
226     {
227       final float[][] typeMinMax = minmax.get(type);
228       /*
229        * final java.awt.CheckboxMenuItem chb = new
230        * java.awt.CheckboxMenuItem("Vary Height"); // this is broken at the
231        * moment chb.setState(minmax.get(type) != null);
232        * chb.addActionListener(new ActionListener() {
233        * 
234        * public void actionPerformed(ActionEvent e) {
235        * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
236        * null); } else { minmax.put(type, typeMinMax); } }
237        * 
238        * }); men.add(chb);
239        */
240       if (typeMinMax != null && typeMinMax[0] != null)
241       {
242         // graduated colourschemes for those where minmax exists for the
243         // positional features
244         MenuItem mxcol = new MenuItem(
245                 (typeCol.isSimpleColour()) ? "Graduated Colour"
246                         : "Single Colour");
247         men.add(mxcol);
248         mxcol.addActionListener(new ActionListener()
249         {
250
251           @Override
252           public void actionPerformed(ActionEvent e)
253           {
254             if (typeCol.isSimpleColour())
255             {
256               new FeatureColourChooser(me, type);
257               // write back the current colour object to update the table
258               check.updateColor(fr.getFeatureStyle(type));
259             }
260             else
261             {
262               new UserDefinedColours(me, check.type, typeCol);
263             }
264           }
265
266         });
267       }
268     }
269
270     MenuItem selectContaining = new MenuItem(
271             MessageManager.getString("label.select_columns_containing"));
272     selectContaining.addActionListener(new ActionListener()
273     {
274       @Override
275       public void actionPerformed(ActionEvent e)
276       {
277         me.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
278                 false, type);
279       }
280     });
281     men.add(selectContaining);
282
283     MenuItem selectNotContaining = new MenuItem(
284             MessageManager.getString("label.select_columns_not_containing"));
285     selectNotContaining.addActionListener(new ActionListener()
286     {
287       @Override
288       public void actionPerformed(ActionEvent e)
289       {
290         me.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
291                 false, type);
292       }
293     });
294     men.add(selectNotContaining);
295
296     MenuItem hideContaining = new MenuItem(
297             MessageManager.getString("label.hide_columns_containing"));
298     hideContaining.addActionListener(new ActionListener()
299     {
300       @Override
301       public void actionPerformed(ActionEvent e)
302       {
303         hideFeatureColumns(type, true);
304       }
305     });
306     men.add(hideContaining);
307
308     MenuItem hideNotContaining = new MenuItem(
309             MessageManager.getString("label.hide_columns_not_containing"));
310     hideNotContaining.addActionListener(new ActionListener()
311     {
312       @Override
313       public void actionPerformed(ActionEvent e)
314       {
315         hideFeatureColumns(type, false);
316       }
317     });
318     men.add(hideNotContaining);
319
320     this.featurePanel.add(men);
321     men.show(this.featurePanel, x, y);
322   }
323
324   @Override
325   public void discoverAllFeatureData()
326   {
327     if (fr.getAllFeatureColours() != null
328             && fr.getAllFeatureColours().size() > 0)
329     {
330       rebuildGroups();
331
332     }
333     resetTable(false);
334   }
335
336   /**
337    * rebuilds the group panel
338    */
339   public void rebuildGroups()
340   {
341     boolean rdrw = false;
342     if (groupPanel == null)
343     {
344       groupPanel = new Panel();
345     }
346     else
347     {
348       rdrw = true;
349       groupPanel.removeAll();
350     }
351     // TODO: JAL-964 - smoothly incorporate new group entries if panel already
352     // displayed and new groups present
353     for (String group : fr.getFeatureGroups())
354     {
355       boolean vis = fr.checkGroupVisibility(group, false);
356       Checkbox check = new MyCheckbox(group, vis, false);
357       check.addMouseListener(this);
358       check.setFont(new Font("Serif", Font.BOLD, 12));
359       check.addItemListener(groupItemListener);
360       // note - visibility seems to correlate with displayed. ???wtf ??
361       check.setVisible(vis);
362       groupPanel.add(check);
363     }
364     if (rdrw)
365     {
366       groupPanel.validate();
367     }
368   }
369
370   // This routine adds and removes checkboxes depending on
371   // Group selection states
372   void resetTable(boolean groupsChanged)
373   {
374     List<String> displayableTypes = new ArrayList<String>();
375     AlignmentI alignment = av.getAlignment();
376     for (int i = 0; i < alignment.getHeight(); i++)
377     {
378       SequenceI seq = alignment.getSequenceAt(i);
379
380       /*
381        * get the sequence's groups for positional features
382        * and keep track of which groups are visible
383        */
384       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
385       Set<String> visibleGroups = new HashSet<String>();
386       for (String group : groups)
387       {
388         if (group == null || fr.checkGroupVisibility(group, true))
389         {
390           visibleGroups.add(group);
391         }
392       }
393
394       /*
395        * get distinct feature types for visible groups
396        * record distinct visible types
397        */
398       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
399               visibleGroups.toArray(new String[visibleGroups.size()]));
400       displayableTypes.addAll(types);
401     }
402
403     Component[] comps;
404     int cSize = featurePanel.getComponentCount();
405     MyCheckbox check;
406     // This will remove any checkboxes which shouldn't be
407     // visible
408     for (int i = 0; i < cSize; i++)
409     {
410       comps = featurePanel.getComponents();
411       check = (MyCheckbox) comps[i];
412       if (!displayableTypes.contains(check.type))
413       {
414         featurePanel.remove(i);
415         cSize--;
416         i--;
417       }
418     }
419
420     if (fr.getRenderOrder() != null)
421     {
422       // First add the checks in the previous render order,
423       // in case the window has been closed and reopened
424       List<String> rol = fr.getRenderOrder();
425       for (int ro = rol.size() - 1; ro > -1; ro--)
426       {
427         String item = rol.get(ro);
428
429         if (!displayableTypes.contains(item))
430         {
431           continue;
432         }
433
434         displayableTypes.remove(item);
435
436         addCheck(false, item);
437       }
438     }
439
440     /*
441      * now add checkboxes which should be visible,
442      * if they have not already been added
443      */
444     for (String type : displayableTypes)
445     {
446       addCheck(groupsChanged, type);
447     }
448
449     featurePanel.setLayout(new GridLayout(featurePanel.getComponentCount(),
450             1, 10, 5));
451     featurePanel.validate();
452
453     if (scrollPane != null)
454     {
455       scrollPane.validate();
456     }
457
458     itemStateChanged(null);
459   }
460
461   /**
462    * update the checklist of feature types with the given type
463    * 
464    * @param groupsChanged
465    *          true means if the type is not in the display list then it will be
466    *          added and displayed
467    * @param type
468    *          feature type to be checked for in the list.
469    */
470   void addCheck(boolean groupsChanged, String type)
471   {
472     boolean addCheck;
473     Component[] comps = featurePanel.getComponents();
474     MyCheckbox check;
475     addCheck = true;
476     for (int i = 0; i < featurePanel.getComponentCount(); i++)
477     {
478       check = (MyCheckbox) comps[i];
479       if (check.type.equals(type))
480       {
481         addCheck = false;
482         break;
483       }
484     }
485
486     if (addCheck)
487     {
488       boolean selected = false;
489       if (groupsChanged || av.getFeaturesDisplayed().isVisible(type))
490       {
491         selected = true;
492       }
493
494       check = new MyCheckbox(type, selected, false,
495               fr.getFeatureStyle(type));
496
497       check.addMouseListener(this);
498       check.addMouseMotionListener(this);
499       check.addItemListener(this);
500       if (groupsChanged)
501       {
502         // add at beginning of stack.
503         featurePanel.add(check, 0);
504       }
505       else
506       {
507         // add at end of stack.
508         featurePanel.add(check);
509       }
510     }
511   }
512
513   @Override
514   public void actionPerformed(ActionEvent evt)
515   {
516     for (int i = 0; i < featurePanel.getComponentCount(); i++)
517     {
518       Checkbox check = (Checkbox) featurePanel.getComponent(i);
519       check.setState(!check.getState());
520     }
521     selectionChanged();
522   }
523
524   private ItemListener groupItemListener = new ItemListener()
525   {
526     @Override
527     public void itemStateChanged(ItemEvent evt)
528     {
529       Checkbox source = (Checkbox) evt.getSource();
530       fr.setGroupVisibility(source.getLabel(), source.getState());
531       ap.seqPanel.seqCanvas.repaint();
532       if (ap.overviewPanel != null)
533       {
534         ap.overviewPanel.updateOverviewImage();
535       }
536       resetTable(true);
537       return;
538     };
539   };
540
541   @Override
542   public void itemStateChanged(ItemEvent evt)
543   {
544     selectionChanged();
545   }
546
547   void selectionChanged()
548   {
549     Component[] comps = featurePanel.getComponents();
550     int cSize = comps.length;
551
552     Object[][] tmp = new Object[cSize][3];
553     int tmpSize = 0;
554     for (int i = 0; i < cSize; i++)
555     {
556       MyCheckbox check = (MyCheckbox) comps[i];
557       tmp[tmpSize][0] = check.type;
558       tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
559       tmp[tmpSize][2] = new Boolean(check.getState());
560       tmpSize++;
561     }
562
563     Object[][] data = new Object[tmpSize][3];
564     System.arraycopy(tmp, 0, data, 0, tmpSize);
565
566     fr.setFeaturePriority(data);
567
568     ap.paintAlignment(true);
569   }
570
571   MyCheckbox selectedCheck;
572
573   boolean dragging = false;
574
575   @Override
576   public void mouseDragged(MouseEvent evt)
577   {
578     if (((Component) evt.getSource()).getParent() != featurePanel)
579     {
580       return;
581     }
582     dragging = true;
583   }
584
585   @Override
586   public void mouseReleased(MouseEvent evt)
587   {
588     if (((Component) evt.getSource()).getParent() != featurePanel)
589     {
590       return;
591     }
592
593     Component comp = null;
594     Checkbox target = null;
595
596     int height = evt.getY() + evt.getComponent().getLocation().y;
597
598     if (height > featurePanel.getSize().height)
599     {
600
601       comp = featurePanel
602               .getComponent(featurePanel.getComponentCount() - 1);
603     }
604     else if (height < 0)
605     {
606       comp = featurePanel.getComponent(0);
607     }
608     else
609     {
610       comp = featurePanel.getComponentAt(evt.getX(), evt.getY()
611               + evt.getComponent().getLocation().y);
612     }
613
614     if (comp != null && comp instanceof Checkbox)
615     {
616       target = (Checkbox) comp;
617     }
618
619     if (selectedCheck != null && target != null && selectedCheck != target)
620     {
621       int targetIndex = -1;
622       for (int i = 0; i < featurePanel.getComponentCount(); i++)
623       {
624         if (target == featurePanel.getComponent(i))
625         {
626           targetIndex = i;
627           break;
628         }
629       }
630
631       featurePanel.remove(selectedCheck);
632       featurePanel.add(selectedCheck, targetIndex);
633       featurePanel.validate();
634       itemStateChanged(null);
635     }
636   }
637
638   public void setUserColour(String feature, FeatureColourI originalColour)
639   {
640     fr.setColour(feature, originalColour);
641     refreshTable();
642   }
643
644   public void refreshTable()
645   {
646     featurePanel.removeAll();
647     resetTable(false);
648     ap.paintAlignment(true);
649   }
650
651   @Override
652   public void mouseEntered(MouseEvent evt)
653   {
654   }
655
656   @Override
657   public void mouseExited(MouseEvent evt)
658   {
659   }
660
661   @Override
662   public void mouseClicked(MouseEvent evt)
663   {
664     MyCheckbox check = (MyCheckbox) evt.getSource();
665     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
666     {
667       this.popupSort(check, fr.getMinMax(), evt.getX(), evt.getY());
668     }
669
670     if (check.getParent() != featurePanel)
671     {
672       return;
673     }
674
675     if (evt.getClickCount() > 1)
676     {
677       FeatureColourI fcol = fr.getFeatureStyle(check.type);
678       if (fcol.isSimpleColour())
679       {
680         new UserDefinedColours(this, check.type, fcol.getColour());
681       }
682       else
683       {
684         new FeatureColourChooser(this, check.type);
685         // write back the current colour object to update the table
686         check.updateColor(fr.getFeatureStyle(check.type));
687       }
688     }
689   }
690
691   @Override
692   public void mouseMoved(MouseEvent evt)
693   {
694   }
695
696   @Override
697   public void adjustmentValueChanged(AdjustmentEvent evt)
698   {
699     fr.setTransparency((100 - transparency.getValue()) / 100f);
700     ap.paintAlignment(true);
701   }
702
703   class MyCheckbox extends Checkbox
704   {
705     public String type;
706
707     public int stringWidth;
708
709     boolean hasLink;
710
711     FeatureColourI col;
712
713     public void updateColor(FeatureColourI newcol)
714     {
715       col = newcol;
716       if (col.isSimpleColour())
717       {
718         setBackground(col.getColour());
719       }
720       else
721       {
722         String vlabel = type;
723         if (col.isAboveThreshold())
724         {
725           vlabel += " (>)";
726         }
727         else if (col.isBelowThreshold())
728         {
729           vlabel += " (<)";
730         }
731         if (col.isColourByLabel())
732         {
733           setBackground(Color.white);
734           vlabel += " (by Label)";
735         }
736         else
737         {
738           setBackground(col.getMinColour());
739         }
740         this.setLabel(vlabel);
741       }
742       repaint();
743     }
744
745     public MyCheckbox(String label, boolean checked, boolean haslink)
746     {
747       super(label, checked);
748       type = label;
749       FontMetrics fm = av.nullFrame.getFontMetrics(av.nullFrame.getFont());
750       stringWidth = fm.stringWidth(label);
751       this.hasLink = haslink;
752     }
753
754     public MyCheckbox(String type, boolean selected, boolean b,
755             FeatureColourI featureStyle)
756     {
757       this(type, selected, b);
758       updateColor(featureStyle);
759     }
760
761     @Override
762     public void paint(Graphics g)
763     {
764       Dimension d = getSize();
765       if (col != null)
766       {
767         if (col.isColourByLabel())
768         {
769           g.setColor(Color.white);
770           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
771           /*
772            * g.setColor(Color.black); Font f=g.getFont().deriveFont(9);
773            * g.setFont(f);
774            * 
775            * // g.setFont(g.getFont().deriveFont( //
776            * AffineTransform.getScaleInstance( //
777            * width/g.getFontMetrics().stringWidth("Label"), //
778            * height/g.getFontMetrics().getHeight()))); g.drawString("Label",
779            * width/2, 0);
780            */
781
782         }
783         else if (col.isGraduatedColour())
784         {
785           Color maxCol = col.getMaxColour();
786           g.setColor(maxCol);
787           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
788
789         }
790       }
791
792       if (hasLink)
793       {
794         g.drawImage(linkImage, stringWidth + 25,
795                 (getSize().height - linkImage.getHeight(this)) / 2, this);
796       }
797     }
798   }
799
800   /**
801    * Hide columns containing (or not containing) a given feature type
802    * 
803    * @param type
804    * @param columnsContaining
805    */
806   void hideFeatureColumns(final String type, boolean columnsContaining)
807   {
808     if (ap.alignFrame.avc.markColumnsContainingFeatures(columnsContaining,
809             false, false, type))
810     {
811       if (ap.alignFrame.avc.markColumnsContainingFeatures(
812               !columnsContaining, false, false, type))
813       {
814         ap.alignFrame.viewport.hideSelectedColumns();
815       }
816     }
817   }
818
819   @Override
820   public void mousePressed(MouseEvent e)
821   {
822     // TODO Auto-generated method stub
823
824   }
825
826 }