JAL-2069 spike updated with latest (FeatureTypeSettings)
[jalview.git] / src / jalview / gui / 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.gui;
22
23 import static jalview.viewmodel.seqfeatures.FeatureRendererModel.COLOUR_COLUMN;
24 import static jalview.viewmodel.seqfeatures.FeatureRendererModel.FILTER_COLUMN;
25 import static jalview.viewmodel.seqfeatures.FeatureRendererModel.SHOW_COLUMN;
26 import static jalview.viewmodel.seqfeatures.FeatureRendererModel.TYPE_COLUMN;
27
28 import jalview.api.FeatureColourI;
29 import jalview.api.FeatureSettingsControllerI;
30 import jalview.bin.Cache;
31 import jalview.datamodel.AlignmentI;
32 import jalview.datamodel.SequenceI;
33 import jalview.gui.Help.HelpId;
34 import jalview.io.JalviewFileChooser;
35 import jalview.io.JalviewFileView;
36 import jalview.schemabinding.version2.JalviewUserColours;
37 import jalview.schemes.FeatureColour;
38 import jalview.util.Format;
39 import jalview.util.MessageManager;
40 import jalview.util.Platform;
41 import jalview.util.QuickSort;
42 import jalview.util.matcher.KeyedMatcherSet;
43 import jalview.util.matcher.KeyedMatcherSetI;
44 import jalview.viewmodel.AlignmentViewport;
45 import jalview.ws.DasSequenceFeatureFetcher;
46 import jalview.ws.dbsources.das.api.jalviewSourceI;
47
48 import java.awt.BorderLayout;
49 import java.awt.Color;
50 import java.awt.Component;
51 import java.awt.Dimension;
52 import java.awt.Font;
53 import java.awt.Graphics;
54 import java.awt.GridLayout;
55 import java.awt.Point;
56 import java.awt.Rectangle;
57 import java.awt.event.ActionEvent;
58 import java.awt.event.ActionListener;
59 import java.awt.event.ItemEvent;
60 import java.awt.event.ItemListener;
61 import java.awt.event.MouseAdapter;
62 import java.awt.event.MouseEvent;
63 import java.awt.event.MouseMotionAdapter;
64 import java.beans.PropertyChangeEvent;
65 import java.beans.PropertyChangeListener;
66 import java.io.File;
67 import java.io.FileInputStream;
68 import java.io.FileOutputStream;
69 import java.io.InputStreamReader;
70 import java.io.OutputStreamWriter;
71 import java.io.PrintWriter;
72 import java.util.Arrays;
73 import java.util.HashSet;
74 import java.util.Hashtable;
75 import java.util.Iterator;
76 import java.util.List;
77 import java.util.Map;
78 import java.util.Set;
79 import java.util.Vector;
80
81 import javax.help.HelpSetException;
82 import javax.swing.AbstractCellEditor;
83 import javax.swing.BorderFactory;
84 import javax.swing.Icon;
85 import javax.swing.JButton;
86 import javax.swing.JCheckBox;
87 import javax.swing.JCheckBoxMenuItem;
88 import javax.swing.JColorChooser;
89 import javax.swing.JDialog;
90 import javax.swing.JInternalFrame;
91 import javax.swing.JLabel;
92 import javax.swing.JLayeredPane;
93 import javax.swing.JMenuItem;
94 import javax.swing.JPanel;
95 import javax.swing.JPopupMenu;
96 import javax.swing.JScrollPane;
97 import javax.swing.JSlider;
98 import javax.swing.JTable;
99 import javax.swing.ListSelectionModel;
100 import javax.swing.SwingConstants;
101 import javax.swing.SwingUtilities;
102 import javax.swing.event.ChangeEvent;
103 import javax.swing.event.ChangeListener;
104 import javax.swing.table.AbstractTableModel;
105 import javax.swing.table.TableCellEditor;
106 import javax.swing.table.TableCellRenderer;
107 import javax.swing.table.TableColumn;
108
109 public class FeatureSettings extends JPanel
110         implements FeatureSettingsControllerI
111 {
112   private static final int COLUMN_COUNT = 4;
113
114   private static final String COLON = ":";
115
116   private static final int MIN_WIDTH = 400;
117
118   private static final int MIN_HEIGHT = 400;
119
120   private static final int MAX_TOOLTIP_LENGTH = 50;
121
122   DasSourceBrowser dassourceBrowser;
123
124   DasSequenceFeatureFetcher dasFeatureFetcher;
125
126   JPanel dasSettingsPane = new JPanel();
127
128   final FeatureRenderer fr;
129
130   public final AlignFrame af;
131
132   /*
133    * 'original' fields hold settings to restore on Cancel
134    */
135   Object[][] originalData;
136
137   private float originalTransparency;
138
139   private Map<String, KeyedMatcherSetI> originalFilters;
140
141   final JInternalFrame frame;
142
143   JScrollPane scrollPane = new JScrollPane();
144
145   JTable table;
146
147   JPanel groupPanel;
148
149   JSlider transparency = new JSlider();
150
151   /*
152    * when true, constructor is still executing - so ignore UI events
153    */
154   protected volatile boolean inConstruction = true;
155
156   int selectedRow = -1;
157
158   JButton fetchDAS = new JButton();
159
160   JButton saveDAS = new JButton();
161
162   JButton cancelDAS = new JButton();
163
164   boolean resettingTable = false;
165
166   /*
167    * true when Feature Settings are updating from feature renderer
168    */
169   private boolean handlingUpdate = false;
170
171   /*
172    * holds {featureCount, totalExtent} for each feature type
173    */
174   Map<String, float[]> typeWidth = null;
175
176   /**
177    * Constructor
178    * 
179    * @param af
180    */
181   public FeatureSettings(AlignFrame alignFrame)
182   {
183     this.af = alignFrame;
184     fr = af.getFeatureRenderer();
185
186     // save transparency for restore on Cancel
187     originalTransparency = fr.getTransparency();
188     int originalTransparencyAsPercent = (int) (originalTransparency * 100);
189     transparency.setMaximum(100 - originalTransparencyAsPercent);
190
191     originalFilters = fr.getFeatureFilters();
192
193     try
194     {
195       jbInit();
196     } catch (Exception ex)
197     {
198       ex.printStackTrace();
199     }
200
201     table = new JTable()
202     {
203       @Override
204       public String getToolTipText(MouseEvent e)
205       {
206         String tip = null;
207         int column = table.columnAtPoint(e.getPoint());
208         switch (column)
209         {
210         case TYPE_COLUMN:
211           tip = JvSwingUtils.wrapTooltip(true, MessageManager
212                   .getString("label.feature_settings_click_drag"));
213           break;
214         case FILTER_COLUMN:
215           int row = table.rowAtPoint(e.getPoint());
216           KeyedMatcherSet o = (KeyedMatcherSet) table.getValueAt(row,
217                   column);
218           tip = o.isEmpty()
219                   ? MessageManager.getString("label.filters_tooltip")
220                   : o.toString();
221           break;
222         default:
223           break;
224         }
225         return tip;
226       }
227     };
228     table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12));
229     table.setFont(new Font("Verdana", Font.PLAIN, 12));
230
231     // table.setDefaultRenderer(Color.class, new ColorRenderer());
232     // table.setDefaultEditor(Color.class, new ColorEditor(this));
233     //
234     table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
235     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
236
237     table.setDefaultEditor(KeyedMatcherSet.class, new FilterEditor(this));
238     table.setDefaultRenderer(KeyedMatcherSet.class, new FilterRenderer());
239
240     TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
241             new ColorRenderer(), new ColorEditor(this));
242     table.addColumn(colourColumn);
243
244     TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75,
245             new FilterRenderer(), new FilterEditor(this));
246     table.addColumn(filterColumn);
247
248     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
249
250     table.addMouseListener(new MouseAdapter()
251     {
252       @Override
253       public void mousePressed(MouseEvent evt)
254       {
255         selectedRow = table.rowAtPoint(evt.getPoint());
256         String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
257         if (evt.isPopupTrigger())
258         {
259           Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
260           popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
261                   evt.getY());
262         }
263         else if (evt.getClickCount() == 2)
264         {
265           boolean invertSelection = evt.isAltDown();
266           boolean toggleSelection = Platform.isControlDown(evt);
267           boolean extendSelection = evt.isShiftDown();
268           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
269                   invertSelection, extendSelection, toggleSelection, type);
270         }
271       }
272
273       // isPopupTrigger fires on mouseReleased on Windows
274       @Override
275       public void mouseReleased(MouseEvent evt)
276       {
277         selectedRow = table.rowAtPoint(evt.getPoint());
278         if (evt.isPopupTrigger())
279         {
280           String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
281           Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
282           popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
283                   evt.getY());
284         }
285       }
286     });
287
288     table.addMouseMotionListener(new MouseMotionAdapter()
289     {
290       @Override
291       public void mouseDragged(MouseEvent evt)
292       {
293         int newRow = table.rowAtPoint(evt.getPoint());
294         if (newRow != selectedRow && selectedRow != -1 && newRow != -1)
295         {
296           /*
297            * reposition 'selectedRow' to 'newRow' (the dragged to location)
298            * this could be more than one row away for a very fast drag action
299            * so just swap it with adjacent rows until we get it there
300            */
301           Object[][] data = ((FeatureTableModel) table.getModel())
302                   .getData();
303           int direction = newRow < selectedRow ? -1 : 1;
304           for (int i = selectedRow; i != newRow; i += direction)
305           {
306             Object[] temp = data[i];
307             data[i] = data[i + direction];
308             data[i + direction] = temp;
309           }
310           updateFeatureRenderer(data);
311           table.repaint();
312           selectedRow = newRow;
313         }
314       }
315     });
316     // table.setToolTipText(JvSwingUtils.wrapTooltip(true,
317     // MessageManager.getString("label.feature_settings_click_drag")));
318     scrollPane.setViewportView(table);
319
320     dassourceBrowser = new DasSourceBrowser(this);
321     dasSettingsPane.add(dassourceBrowser, BorderLayout.CENTER);
322
323     if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
324     {
325       fr.findAllFeatures(true); // display everything!
326     }
327
328     discoverAllFeatureData();
329     final PropertyChangeListener change;
330     final FeatureSettings fs = this;
331     fr.addPropertyChangeListener(change = new PropertyChangeListener()
332     {
333       @Override
334       public void propertyChange(PropertyChangeEvent evt)
335       {
336         if (!fs.resettingTable && !fs.handlingUpdate)
337         {
338           fs.handlingUpdate = true;
339           fs.resetTable(null);
340           // new groups may be added with new sequence feature types only
341           fs.handlingUpdate = false;
342         }
343       }
344
345     });
346
347     frame = new JInternalFrame();
348     frame.setContentPane(this);
349     if (Platform.isAMac())
350     {
351       Desktop.addInternalFrame(frame,
352               MessageManager.getString("label.sequence_feature_settings"),
353               600, 480);
354     }
355     else
356     {
357       Desktop.addInternalFrame(frame,
358               MessageManager.getString("label.sequence_feature_settings"),
359               600, 450);
360     }
361     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
362
363     frame.addInternalFrameListener(
364             new javax.swing.event.InternalFrameAdapter()
365             {
366               @Override
367               public void internalFrameClosed(
368                       javax.swing.event.InternalFrameEvent evt)
369               {
370                 fr.removePropertyChangeListener(change);
371                 dassourceBrowser.fs = null;
372               };
373             });
374     frame.setLayer(JLayeredPane.PALETTE_LAYER);
375     inConstruction = false;
376   }
377
378   protected void popupSort(final int rowSelected, final String type,
379           final Object typeCol, final Map<String, float[][]> minmax, int x,
380           int y)
381   {
382     final FeatureColourI featureColour = (FeatureColourI) typeCol;
383
384     JPopupMenu men = new JPopupMenu(MessageManager
385             .formatMessage("label.settings_for_param", new String[]
386             { type }));
387     JMenuItem scr = new JMenuItem(
388             MessageManager.getString("label.sort_by_score"));
389     men.add(scr);
390     final FeatureSettings me = this;
391     scr.addActionListener(new ActionListener()
392     {
393
394       @Override
395       public void actionPerformed(ActionEvent e)
396       {
397         me.af.avc
398                 .sortAlignmentByFeatureScore(Arrays.asList(new String[]
399                 { type }));
400       }
401
402     });
403     JMenuItem dens = new JMenuItem(
404             MessageManager.getString("label.sort_by_density"));
405     dens.addActionListener(new ActionListener()
406     {
407
408       @Override
409       public void actionPerformed(ActionEvent e)
410       {
411         me.af.avc
412                 .sortAlignmentByFeatureDensity(Arrays.asList(new String[]
413                 { type }));
414       }
415
416     });
417     men.add(dens);
418
419     /*
420      * variable colour options include colour by label, by score,
421      * by selected attribute text, or attribute value
422      */
423     final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
424             MessageManager.getString("label.variable_colour"));
425     mxcol.setSelected(!featureColour.isSimpleColour());
426     men.add(mxcol);
427     mxcol.addActionListener(new ActionListener()
428     {
429       JColorChooser colorChooser;
430
431       @Override
432       public void actionPerformed(ActionEvent e)
433       {
434         if (e.getSource() == mxcol)
435         {
436           if (featureColour.isSimpleColour())
437           {
438             FeatureTypeSettings fc = new FeatureTypeSettings(me.fr, type);
439             fc.addActionListener(this);
440           }
441           else
442           {
443             // bring up simple color chooser
444             colorChooser = new JColorChooser();
445             String title = MessageManager
446                     .getString("label.select_colour");
447             JDialog dialog = JColorChooser.createDialog(me,
448                     title, true, // modal
449                     colorChooser, this, // OK button handler
450                     null); // no CANCEL button handler
451             colorChooser.setColor(featureColour.getMaxColour());
452             dialog.setVisible(true);
453           }
454         }
455         else
456         {
457           if (e.getSource() instanceof FeatureTypeSettings)
458           {
459             /*
460              * update after OK in feature colour dialog; the updated
461              * colour will have already been set in the FeatureRenderer
462              */
463             FeatureColourI fci = fr.getFeatureColours().get(type);
464             table.setValueAt(fci, rowSelected, 1);
465             table.validate();
466           }
467           else
468           {
469             // probably the color chooser!
470             table.setValueAt(new FeatureColour(colorChooser.getColor()),
471                     rowSelected, 1);
472             table.validate();
473             me.updateFeatureRenderer(
474                     ((FeatureTableModel) table.getModel()).getData(),
475                     false);
476           }
477         }
478       }
479
480     });
481
482     JMenuItem selCols = new JMenuItem(
483             MessageManager.getString("label.select_columns_containing"));
484     selCols.addActionListener(new ActionListener()
485     {
486       @Override
487       public void actionPerformed(ActionEvent arg0)
488       {
489         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
490                 false, type);
491       }
492     });
493     JMenuItem clearCols = new JMenuItem(MessageManager
494             .getString("label.select_columns_not_containing"));
495     clearCols.addActionListener(new ActionListener()
496     {
497       @Override
498       public void actionPerformed(ActionEvent arg0)
499       {
500         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
501                 false, type);
502       }
503     });
504     JMenuItem hideCols = new JMenuItem(
505             MessageManager.getString("label.hide_columns_containing"));
506     hideCols.addActionListener(new ActionListener()
507     {
508       @Override
509       public void actionPerformed(ActionEvent arg0)
510       {
511         fr.ap.alignFrame.hideFeatureColumns(type, true);
512       }
513     });
514     JMenuItem hideOtherCols = new JMenuItem(
515             MessageManager.getString("label.hide_columns_not_containing"));
516     hideOtherCols.addActionListener(new ActionListener()
517     {
518       @Override
519       public void actionPerformed(ActionEvent arg0)
520       {
521         fr.ap.alignFrame.hideFeatureColumns(type, false);
522       }
523     });
524     men.add(selCols);
525     men.add(clearCols);
526     men.add(hideCols);
527     men.add(hideOtherCols);
528     men.show(table, x, y);
529   }
530
531   @Override
532   synchronized public void discoverAllFeatureData()
533   {
534     Set<String> allGroups = new HashSet<>();
535     AlignmentI alignment = af.getViewport().getAlignment();
536
537     for (int i = 0; i < alignment.getHeight(); i++)
538     {
539       SequenceI seq = alignment.getSequenceAt(i);
540       for (String group : seq.getFeatures().getFeatureGroups(true))
541       {
542         if (group != null && !allGroups.contains(group))
543         {
544           allGroups.add(group);
545           checkGroupState(group);
546         }
547       }
548     }
549
550     resetTable(null);
551
552     validate();
553   }
554
555   /**
556    * Synchronise gui group list and check visibility of group
557    * 
558    * @param group
559    * @return true if group is visible
560    */
561   private boolean checkGroupState(String group)
562   {
563     boolean visible = fr.checkGroupVisibility(group, true);
564
565     for (int g = 0; g < groupPanel.getComponentCount(); g++)
566     {
567       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
568       {
569         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
570         return visible;
571       }
572     }
573
574     final String grp = group;
575     final JCheckBox check = new JCheckBox(group, visible);
576     check.setFont(new Font("Serif", Font.BOLD, 12));
577     check.setToolTipText(group);
578     check.addItemListener(new ItemListener()
579     {
580       @Override
581       public void itemStateChanged(ItemEvent evt)
582       {
583         fr.setGroupVisibility(check.getText(), check.isSelected());
584         resetTable(new String[] { grp });
585         af.alignPanel.paintAlignment(true, true);
586       }
587     });
588     groupPanel.add(check);
589     return visible;
590   }
591
592   synchronized void resetTable(String[] groupChanged)
593   {
594     if (resettingTable)
595     {
596       return;
597     }
598     resettingTable = true;
599     typeWidth = new Hashtable<>();
600     // TODO: change avWidth calculation to 'per-sequence' average and use long
601     // rather than float
602
603     Set<String> displayableTypes = new HashSet<>();
604     Set<String> foundGroups = new HashSet<>();
605
606     /*
607      * determine which feature types may be visible depending on 
608      * which groups are selected, and recompute average width data
609      */
610     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
611     {
612
613       SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
614
615       /*
616        * get the sequence's groups for positional features
617        * and keep track of which groups are visible
618        */
619       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
620       Set<String> visibleGroups = new HashSet<>();
621       for (String group : groups)
622       {
623         if (group == null || checkGroupState(group))
624         {
625           visibleGroups.add(group);
626         }
627       }
628       foundGroups.addAll(groups);
629
630       /*
631        * get distinct feature types for visible groups
632        * record distinct visible types, and their count and total length
633        */
634       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
635               visibleGroups.toArray(new String[visibleGroups.size()]));
636       for (String type : types)
637       {
638         displayableTypes.add(type);
639         float[] avWidth = typeWidth.get(type);
640         if (avWidth == null)
641         {
642           avWidth = new float[2];
643           typeWidth.put(type, avWidth);
644         }
645         // todo this could include features with a non-visible group
646         // - do we greatly care?
647         // todo should we include non-displayable features here, and only
648         // update when features are added?
649         avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
650         avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
651       }
652     }
653
654     int columnCount = COLUMN_COUNT;
655     Object[][] data = new Object[displayableTypes.size()][columnCount];
656     int dataIndex = 0;
657
658     if (fr.hasRenderOrder())
659     {
660       if (!handlingUpdate)
661       {
662         fr.findAllFeatures(groupChanged != null); // prod to update
663         // colourschemes. but don't
664         // affect display
665         // First add the checks in the previous render order,
666         // in case the window has been closed and reopened
667       }
668       List<String> frl = fr.getRenderOrder();
669       for (int ro = frl.size() - 1; ro > -1; ro--)
670       {
671         String type = frl.get(ro);
672
673         if (!displayableTypes.contains(type))
674         {
675           continue;
676         }
677
678         data[dataIndex][TYPE_COLUMN] = type;
679         data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
680         KeyedMatcherSetI featureFilter = fr.getFeatureFilter(type);
681         data[dataIndex][FILTER_COLUMN] = featureFilter == null
682                 ? new KeyedMatcherSet()
683                 : featureFilter;
684         data[dataIndex][SHOW_COLUMN] = new Boolean(
685                 af.getViewport().getFeaturesDisplayed().isVisible(type));
686         dataIndex++;
687         displayableTypes.remove(type);
688       }
689     }
690
691     /*
692      * process any extra features belonging only to 
693      * a group which was just selected
694      */
695     while (!displayableTypes.isEmpty())
696     {
697       String type = displayableTypes.iterator().next();
698       data[dataIndex][TYPE_COLUMN] = type;
699
700       data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
701       if (data[dataIndex][COLOUR_COLUMN] == null)
702       {
703         // "Colour has been updated in another view!!"
704         fr.clearRenderOrder();
705         return;
706       }
707       KeyedMatcherSetI featureFilter = fr.getFeatureFilter(type);
708       data[dataIndex][FILTER_COLUMN] = featureFilter == null
709               ? new KeyedMatcherSet()
710               : featureFilter;
711       data[dataIndex][SHOW_COLUMN] = new Boolean(true);
712       dataIndex++;
713       displayableTypes.remove(type);
714     }
715
716     if (originalData == null)
717     {
718       int size = data[0].length;
719       originalData = new Object[data.length][size];
720       for (int i = 0; i < data.length; i++)
721       {
722         System.arraycopy(data[i], 0, originalData[i], 0, size);
723       }
724     }
725     else
726     {
727       updateOriginalData(data);
728     }
729
730     table.setModel(new FeatureTableModel(data));
731     table.getColumnModel().getColumn(0).setPreferredWidth(200);
732
733     groupPanel.setLayout(
734             new GridLayout(fr.getFeatureGroupsSize() / 4 + 1, 4));
735     pruneGroups(foundGroups);
736     groupPanel.validate();
737
738     updateFeatureRenderer(data, groupChanged != null);
739     resettingTable = false;
740   }
741
742   /**
743    * Updates 'originalData' (used for restore on Cancel) if we detect that changes
744    * have been made outwith this dialog
745    * <ul>
746    * <li>a new feature type added (and made visible)</li>
747    * <li>a feature colour changed (in the Amend Features dialog)</li>
748    * </ul>
749    * 
750    * @param foundData
751    */
752   protected void updateOriginalData(Object[][] foundData)
753   {
754     // todo LinkedHashMap instead of Object[][] would be nice
755
756     Object[][] currentData = ((FeatureTableModel) table.getModel())
757             .getData();
758     for (Object[] row : foundData)
759     {
760       String type = (String) row[TYPE_COLUMN];
761       boolean found = false;
762       for (Object[] current : currentData)
763       {
764         if (type.equals(current[TYPE_COLUMN]))
765         {
766           found = true;
767           /*
768            * currently dependent on object equality here;
769            * really need an equals method on FeatureColour
770            */
771           if (!row[COLOUR_COLUMN].equals(current[COLOUR_COLUMN]))
772           {
773             /*
774              * feature colour has changed externally - update originalData
775              */
776             for (Object[] original : originalData)
777             {
778               if (type.equals(original[TYPE_COLUMN]))
779               {
780                 original[COLOUR_COLUMN] = row[COLOUR_COLUMN];
781                 break;
782               }
783             }
784           }
785           break;
786         }
787       }
788       if (!found)
789       {
790         /*
791          * new feature detected - add to original data (on top)
792          */
793         int size = currentData[0].length;
794         Object[][] newData = new Object[originalData.length + 1][size];
795         for (int i = 0; i < originalData.length; i++)
796         {
797           System.arraycopy(originalData[i], 0, newData[i + 1], 0, size);
798         }
799         newData[0] = row;
800         originalData = newData;
801       }
802     }
803   }
804
805   /**
806    * Remove from the groups panel any checkboxes for groups that are not in the
807    * foundGroups set. This enables removing a group from the display when the last
808    * feature in that group is deleted.
809    * 
810    * @param foundGroups
811    */
812   protected void pruneGroups(Set<String> foundGroups)
813   {
814     for (int g = 0; g < groupPanel.getComponentCount(); g++)
815     {
816       JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g);
817       if (!foundGroups.contains(checkbox.getText()))
818       {
819         groupPanel.remove(checkbox);
820       }
821     }
822   }
823
824   /**
825    * reorder data based on the featureRenderers global priority list.
826    * 
827    * @param data
828    */
829   private void ensureOrder(Object[][] data)
830   {
831     boolean sort = false;
832     float[] order = new float[data.length];
833     for (int i = 0; i < order.length; i++)
834     {
835       order[i] = fr.getOrder(data[i][0].toString());
836       if (order[i] < 0)
837       {
838         order[i] = fr.setOrder(data[i][0].toString(), i / order.length);
839       }
840       if (i > 1)
841       {
842         sort = sort || order[i - 1] > order[i];
843       }
844     }
845     if (sort)
846     {
847       jalview.util.QuickSort.sort(order, data);
848     }
849   }
850
851   void load()
852   {
853     JalviewFileChooser chooser = new JalviewFileChooser("fc",
854             "Sequence Feature Colours");
855     chooser.setFileView(new JalviewFileView());
856     chooser.setDialogTitle(
857             MessageManager.getString("label.load_feature_colours"));
858     chooser.setToolTipText(MessageManager.getString("action.load"));
859
860     int value = chooser.showOpenDialog(this);
861
862     if (value == JalviewFileChooser.APPROVE_OPTION)
863     {
864       File file = chooser.getSelectedFile();
865
866       try
867       {
868         InputStreamReader in = new InputStreamReader(
869                 new FileInputStream(file), "UTF-8");
870
871         JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
872
873         for (int i = jucs.getColourCount() - 1; i >= 0; i--)
874         {
875           String name;
876           jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
877           if (newcol.hasMax())
878           {
879             Color mincol = null, maxcol = null;
880             try
881             {
882               mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16));
883               maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16));
884
885             } catch (Exception e)
886             {
887               Cache.log.warn("Couldn't parse out graduated feature color.",
888                       e);
889             }
890             FeatureColourI gcol = new FeatureColour(mincol, maxcol,
891                     newcol.getMin(), newcol.getMax());
892             if (newcol.hasAutoScale())
893             {
894               gcol.setAutoScaled(newcol.getAutoScale());
895             }
896             if (newcol.hasColourByLabel())
897             {
898               gcol.setColourByLabel(newcol.getColourByLabel());
899             }
900             if (newcol.hasThreshold())
901             {
902               gcol.setThreshold(newcol.getThreshold());
903             }
904             if (newcol.getThreshType().length() > 0)
905             {
906               String ttyp = newcol.getThreshType();
907               if (ttyp.equalsIgnoreCase("ABOVE"))
908               {
909                 gcol.setAboveThreshold(true);
910               }
911               if (ttyp.equalsIgnoreCase("BELOW"))
912               {
913                 gcol.setBelowThreshold(true);
914               }
915             }
916             fr.setColour(name = newcol.getName(), gcol);
917           }
918           else
919           {
920             Color color = new Color(
921                     Integer.parseInt(jucs.getColour(i).getRGB(), 16));
922             fr.setColour(name = jucs.getColour(i).getName(),
923                     new FeatureColour(color));
924           }
925           fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
926         }
927         if (table != null)
928         {
929           resetTable(null);
930           Object[][] data = ((FeatureTableModel) table.getModel())
931                   .getData();
932           ensureOrder(data);
933           updateFeatureRenderer(data, false);
934           table.repaint();
935         }
936       } catch (Exception ex)
937       {
938         System.out.println("Error loading User Colour File\n" + ex);
939       }
940     }
941   }
942
943   void save()
944   {
945     JalviewFileChooser chooser = new JalviewFileChooser("fc",
946             "Sequence Feature Colours");
947     chooser.setFileView(new JalviewFileView());
948     chooser.setDialogTitle(
949             MessageManager.getString("label.save_feature_colours"));
950     chooser.setToolTipText(MessageManager.getString("action.save"));
951
952     int value = chooser.showSaveDialog(this);
953
954     if (value == JalviewFileChooser.APPROVE_OPTION)
955     {
956       String choice = chooser.getSelectedFile().getPath();
957       jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
958       ucs.setSchemeName("Sequence Features");
959       try
960       {
961         PrintWriter out = new PrintWriter(new OutputStreamWriter(
962                 new FileOutputStream(choice), "UTF-8"));
963
964         Set<String> fr_colours = fr.getAllFeatureColours();
965         Iterator<String> e = fr_colours.iterator();
966         float[] sortOrder = new float[fr_colours.size()];
967         String[] sortTypes = new String[fr_colours.size()];
968         int i = 0;
969         while (e.hasNext())
970         {
971           sortTypes[i] = e.next();
972           sortOrder[i] = fr.getOrder(sortTypes[i]);
973           i++;
974         }
975         QuickSort.sort(sortOrder, sortTypes);
976         sortOrder = null;
977         for (i = 0; i < sortTypes.length; i++)
978         {
979           jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
980           col.setName(sortTypes[i]);
981           FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
982           if (fcol.isSimpleColour())
983           {
984             col.setRGB(Format.getHexString(fcol.getColour()));
985           }
986           else
987           {
988             col.setRGB(Format.getHexString(fcol.getMaxColour()));
989             col.setMin(fcol.getMin());
990             col.setMax(fcol.getMax());
991             col.setMinRGB(
992                     jalview.util.Format.getHexString(fcol.getMinColour()));
993             col.setAutoScale(fcol.isAutoScaled());
994             col.setThreshold(fcol.getThreshold());
995             col.setColourByLabel(fcol.isColourByLabel());
996             col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
997                     : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
998           }
999           ucs.addColour(col);
1000         }
1001         ucs.marshal(out);
1002         out.close();
1003       } catch (Exception ex)
1004       {
1005         ex.printStackTrace();
1006       }
1007     }
1008   }
1009
1010   public void invertSelection()
1011   {
1012     for (int i = 0; i < table.getRowCount(); i++)
1013     {
1014       Boolean value = (Boolean) table.getValueAt(i, SHOW_COLUMN);
1015
1016       table.setValueAt(new Boolean(!value.booleanValue()), i, SHOW_COLUMN);
1017     }
1018   }
1019
1020   public void orderByAvWidth()
1021   {
1022     if (table == null || table.getModel() == null)
1023     {
1024       return;
1025     }
1026     Object[][] data = ((FeatureTableModel) table.getModel()).getData();
1027     float[] width = new float[data.length];
1028     float[] awidth;
1029     float max = 0;
1030
1031     for (int i = 0; i < data.length; i++)
1032     {
1033       awidth = typeWidth.get(data[i][TYPE_COLUMN]);
1034       if (awidth[0] > 0)
1035       {
1036         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
1037         // weight - but have to make per
1038         // sequence, too (awidth[2])
1039         // if (width[i]==1) // hack to distinguish single width sequences.
1040       }
1041       else
1042       {
1043         width[i] = 0;
1044       }
1045       if (max < width[i])
1046       {
1047         max = width[i];
1048       }
1049     }
1050     boolean sort = false;
1051     for (int i = 0; i < width.length; i++)
1052     {
1053       // awidth = (float[]) typeWidth.get(data[i][0]);
1054       if (width[i] == 0)
1055       {
1056         width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString());
1057         if (width[i] < 0)
1058         {
1059           width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(),
1060                   i / data.length);
1061         }
1062       }
1063       else
1064       {
1065         width[i] /= max; // normalize
1066         fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for later
1067       }
1068       if (i > 0)
1069       {
1070         sort = sort || width[i - 1] > width[i];
1071       }
1072     }
1073     if (sort)
1074     {
1075       jalview.util.QuickSort.sort(width, data);
1076       // update global priority order
1077     }
1078
1079     updateFeatureRenderer(data, false);
1080     table.repaint();
1081   }
1082
1083   public void close()
1084   {
1085     try
1086     {
1087       frame.setClosed(true);
1088     } catch (Exception exe)
1089     {
1090     }
1091
1092   }
1093
1094   public void updateFeatureRenderer(Object[][] data)
1095   {
1096     updateFeatureRenderer(data, true);
1097   }
1098
1099   /**
1100    * Update the priority order of features; only repaint if this changed the order
1101    * of visible features
1102    * 
1103    * @param data
1104    * @param visibleNew
1105    */
1106   private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
1107   {
1108     if (fr.setFeaturePriority(data, visibleNew))
1109     {
1110       af.alignPanel.paintAlignment(true, true);
1111     }
1112   }
1113
1114   private void jbInit() throws Exception
1115   {
1116     this.setLayout(new BorderLayout());
1117
1118     JPanel settingsPane = new JPanel();
1119     settingsPane.setLayout(new BorderLayout());
1120
1121     dasSettingsPane.setLayout(new BorderLayout());
1122
1123     JPanel bigPanel = new JPanel();
1124     bigPanel.setLayout(new BorderLayout());
1125
1126     groupPanel = new JPanel();
1127     bigPanel.add(groupPanel, BorderLayout.NORTH);
1128
1129     JButton invert = new JButton(
1130             MessageManager.getString("label.invert_selection"));
1131     invert.setFont(JvSwingUtils.getLabelFont());
1132     invert.addActionListener(new ActionListener()
1133     {
1134       @Override
1135       public void actionPerformed(ActionEvent e)
1136       {
1137         invertSelection();
1138       }
1139     });
1140
1141     JButton optimizeOrder = new JButton(
1142             MessageManager.getString("label.optimise_order"));
1143     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
1144     optimizeOrder.addActionListener(new ActionListener()
1145     {
1146       @Override
1147       public void actionPerformed(ActionEvent e)
1148       {
1149         orderByAvWidth();
1150       }
1151     });
1152
1153     JButton sortByScore = new JButton(
1154             MessageManager.getString("label.seq_sort_by_score"));
1155     sortByScore.setFont(JvSwingUtils.getLabelFont());
1156     sortByScore.addActionListener(new ActionListener()
1157     {
1158       @Override
1159       public void actionPerformed(ActionEvent e)
1160       {
1161         af.avc.sortAlignmentByFeatureScore(null);
1162       }
1163     });
1164     JButton sortByDens = new JButton(
1165             MessageManager.getString("label.sequence_sort_by_density"));
1166     sortByDens.setFont(JvSwingUtils.getLabelFont());
1167     sortByDens.addActionListener(new ActionListener()
1168     {
1169       @Override
1170       public void actionPerformed(ActionEvent e)
1171       {
1172         af.avc.sortAlignmentByFeatureDensity(null);
1173       }
1174     });
1175
1176     JButton help = new JButton(MessageManager.getString("action.help"));
1177     help.setFont(JvSwingUtils.getLabelFont());
1178     help.addActionListener(new ActionListener()
1179     {
1180       @Override
1181       public void actionPerformed(ActionEvent e)
1182       {
1183         try
1184         {
1185           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1186         } catch (HelpSetException e1)
1187         {
1188           e1.printStackTrace();
1189         }
1190       }
1191     });
1192     help.setFont(JvSwingUtils.getLabelFont());
1193     help.setText(MessageManager.getString("action.help"));
1194     help.addActionListener(new ActionListener()
1195     {
1196       @Override
1197       public void actionPerformed(ActionEvent e)
1198       {
1199         try
1200         {
1201           Help.showHelpWindow(HelpId.SequenceFeatureSettings);
1202         } catch (HelpSetException e1)
1203         {
1204           e1.printStackTrace();
1205         }
1206       }
1207     });
1208
1209     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
1210     cancel.setFont(JvSwingUtils.getLabelFont());
1211     cancel.addActionListener(new ActionListener()
1212     {
1213       @Override
1214       public void actionPerformed(ActionEvent e)
1215       {
1216         fr.setTransparency(originalTransparency);
1217         fr.setFeatureFilters(originalFilters);
1218         updateFeatureRenderer(originalData);
1219         close();
1220       }
1221     });
1222
1223     JButton ok = new JButton(MessageManager.getString("action.ok"));
1224     ok.setFont(JvSwingUtils.getLabelFont());
1225     ok.addActionListener(new ActionListener()
1226     {
1227       @Override
1228       public void actionPerformed(ActionEvent e)
1229       {
1230         close();
1231       }
1232     });
1233
1234     JButton loadColours = new JButton(
1235             MessageManager.getString("label.load_colours"));
1236     loadColours.setFont(JvSwingUtils.getLabelFont());
1237     loadColours.addActionListener(new ActionListener()
1238     {
1239       @Override
1240       public void actionPerformed(ActionEvent e)
1241       {
1242         load();
1243       }
1244     });
1245
1246     JButton saveColours = new JButton(
1247             MessageManager.getString("label.save_colours"));
1248     saveColours.setFont(JvSwingUtils.getLabelFont());
1249     saveColours.addActionListener(new ActionListener()
1250     {
1251       @Override
1252       public void actionPerformed(ActionEvent e)
1253       {
1254         save();
1255       }
1256     });
1257     transparency.addChangeListener(new ChangeListener()
1258     {
1259       @Override
1260       public void stateChanged(ChangeEvent evt)
1261       {
1262         if (!inConstruction)
1263         {
1264           fr.setTransparency((100 - transparency.getValue()) / 100f);
1265           af.alignPanel.paintAlignment(true, true);
1266         }
1267       }
1268     });
1269
1270     transparency.setMaximum(70);
1271     transparency.setToolTipText(
1272             MessageManager.getString("label.transparency_tip"));
1273     fetchDAS.setText(MessageManager.getString("label.fetch_das_features"));
1274     fetchDAS.addActionListener(new ActionListener()
1275     {
1276       @Override
1277       public void actionPerformed(ActionEvent e)
1278       {
1279         fetchDAS_actionPerformed(e);
1280       }
1281     });
1282     saveDAS.setText(MessageManager.getString("action.save_as_default"));
1283     saveDAS.addActionListener(new ActionListener()
1284     {
1285       @Override
1286       public void actionPerformed(ActionEvent e)
1287       {
1288         saveDAS_actionPerformed(e);
1289       }
1290     });
1291
1292     JPanel dasButtonPanel = new JPanel();
1293     dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
1294     dasSettingsPane.setBorder(null);
1295     cancelDAS.setEnabled(false);
1296     cancelDAS.setText(MessageManager.getString("action.cancel_fetch"));
1297     cancelDAS.addActionListener(new ActionListener()
1298     {
1299       @Override
1300       public void actionPerformed(ActionEvent e)
1301       {
1302         cancelDAS_actionPerformed(e);
1303       }
1304     });
1305
1306     JPanel transPanel = new JPanel(new GridLayout(1, 2));
1307     bigPanel.add(transPanel, BorderLayout.SOUTH);
1308
1309     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
1310     transbuttons.add(optimizeOrder);
1311     transbuttons.add(invert);
1312     transbuttons.add(sortByScore);
1313     transbuttons.add(sortByDens);
1314     transbuttons.add(help);
1315     transPanel.add(transparency);
1316     transPanel.add(transbuttons);
1317
1318     JPanel buttonPanel = new JPanel();
1319     buttonPanel.add(ok);
1320     buttonPanel.add(cancel);
1321     buttonPanel.add(loadColours);
1322     buttonPanel.add(saveColours);
1323     bigPanel.add(scrollPane, BorderLayout.CENTER);
1324     dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH);
1325     dasButtonPanel.add(fetchDAS);
1326     dasButtonPanel.add(cancelDAS);
1327     dasButtonPanel.add(saveDAS);
1328     settingsPane.add(bigPanel, BorderLayout.CENTER);
1329     settingsPane.add(buttonPanel, BorderLayout.SOUTH);
1330     this.add(settingsPane);
1331   }
1332
1333   public void fetchDAS_actionPerformed(ActionEvent e)
1334   {
1335     fetchDAS.setEnabled(false);
1336     cancelDAS.setEnabled(true);
1337     dassourceBrowser.setGuiEnabled(false);
1338     Vector<jalviewSourceI> selectedSources = dassourceBrowser
1339             .getSelectedSources();
1340     doDasFeatureFetch(selectedSources, true, true);
1341   }
1342
1343   /**
1344    * get the features from selectedSources for all or the current selection
1345    * 
1346    * @param selectedSources
1347    * @param checkDbRefs
1348    * @param promptFetchDbRefs
1349    */
1350   private void doDasFeatureFetch(List<jalviewSourceI> selectedSources,
1351           boolean checkDbRefs, boolean promptFetchDbRefs)
1352   {
1353     SequenceI[] dataset, seqs;
1354     int iSize;
1355     AlignmentViewport vp = af.getViewport();
1356     if (vp.getSelectionGroup() != null
1357             && vp.getSelectionGroup().getSize() > 0)
1358     {
1359       iSize = vp.getSelectionGroup().getSize();
1360       dataset = new SequenceI[iSize];
1361       seqs = vp.getSelectionGroup().getSequencesInOrder(vp.getAlignment());
1362     }
1363     else
1364     {
1365       iSize = vp.getAlignment().getHeight();
1366       seqs = vp.getAlignment().getSequencesArray();
1367     }
1368
1369     dataset = new SequenceI[iSize];
1370     for (int i = 0; i < iSize; i++)
1371     {
1372       dataset[i] = seqs[i].getDatasetSequence();
1373     }
1374
1375     cancelDAS.setEnabled(true);
1376     dasFeatureFetcher = new jalview.ws.DasSequenceFeatureFetcher(dataset,
1377             this, selectedSources, checkDbRefs, promptFetchDbRefs);
1378     af.getViewport().setShowSequenceFeatures(true);
1379     af.showSeqFeatures.setSelected(true);
1380   }
1381
1382   /**
1383    * blocking call to initialise the das source browser
1384    */
1385   public void initDasSources()
1386   {
1387     dassourceBrowser.initDasSources();
1388   }
1389
1390   /**
1391    * examine the current list of das sources and return any matching the given
1392    * nicknames in sources
1393    * 
1394    * @param sources
1395    *          Vector of Strings to resolve to DAS source nicknames.
1396    * @return sources that are present in source list.
1397    */
1398   public List<jalviewSourceI> resolveSourceNicknames(Vector<String> sources)
1399   {
1400     return dassourceBrowser.sourceRegistry.resolveSourceNicknames(sources);
1401   }
1402
1403   /**
1404    * get currently selected das sources. ensure you have called initDasSources
1405    * before calling this.
1406    * 
1407    * @return vector of selected das source nicknames
1408    */
1409   public Vector<jalviewSourceI> getSelectedSources()
1410   {
1411     return dassourceBrowser.getSelectedSources();
1412   }
1413
1414   /**
1415    * properly initialise DAS fetcher and then initiate a new thread to fetch
1416    * features from the named sources (rather than any turned on by default)
1417    * 
1418    * @param sources
1419    * @param block
1420    *          if true then runs in same thread, otherwise passes to the Swing
1421    *          executor
1422    */
1423   public void fetchDasFeatures(Vector<String> sources, boolean block)
1424   {
1425     initDasSources();
1426     List<jalviewSourceI> resolved = dassourceBrowser.sourceRegistry
1427             .resolveSourceNicknames(sources);
1428     if (resolved.size() == 0)
1429     {
1430       resolved = dassourceBrowser.getSelectedSources();
1431     }
1432     if (resolved.size() > 0)
1433     {
1434       final List<jalviewSourceI> dassources = resolved;
1435       fetchDAS.setEnabled(false);
1436       // cancelDAS.setEnabled(true); doDasFetch does this.
1437       Runnable fetcher = new Runnable()
1438       {
1439
1440         @Override
1441         public void run()
1442         {
1443           doDasFeatureFetch(dassources, true, false);
1444
1445         }
1446       };
1447       if (block)
1448       {
1449         fetcher.run();
1450       }
1451       else
1452       {
1453         SwingUtilities.invokeLater(fetcher);
1454       }
1455     }
1456   }
1457
1458   public void saveDAS_actionPerformed(ActionEvent e)
1459   {
1460     dassourceBrowser
1461             .saveProperties(jalview.bin.Cache.applicationProperties);
1462   }
1463
1464   public void complete()
1465   {
1466     fetchDAS.setEnabled(true);
1467     cancelDAS.setEnabled(false);
1468     dassourceBrowser.setGuiEnabled(true);
1469
1470   }
1471
1472   public void cancelDAS_actionPerformed(ActionEvent e)
1473   {
1474     if (dasFeatureFetcher != null)
1475     {
1476       dasFeatureFetcher.cancel();
1477     }
1478     complete();
1479   }
1480
1481   public void noDasSourceActive()
1482   {
1483     complete();
1484     JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
1485             MessageManager.getString("label.no_das_sources_selected_warn"),
1486             MessageManager.getString("label.no_das_sources_selected_title"),
1487             JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
1488   }
1489
1490   // ///////////////////////////////////////////////////////////////////////
1491   // http://java.sun.com/docs/books/tutorial/uiswing/components/table.html
1492   // ///////////////////////////////////////////////////////////////////////
1493   class FeatureTableModel extends AbstractTableModel
1494   {
1495
1496     private String[] columnNames = {
1497         MessageManager.getString("label.feature_type"),
1498         MessageManager.getString("action.colour"),
1499         MessageManager.getString("label.filter"),
1500         MessageManager.getString("label.show") };
1501
1502     private Object[][] data;
1503
1504     FeatureTableModel(Object[][] data)
1505     {
1506       this.data = data;
1507     }
1508
1509     public Object[][] getData()
1510     {
1511       return data;
1512     }
1513
1514     public void setData(Object[][] data)
1515     {
1516       this.data = data;
1517     }
1518
1519     @Override
1520     public int getColumnCount()
1521     {
1522       return columnNames.length;
1523     }
1524
1525     public Object[] getRow(int row)
1526     {
1527       return data[row];
1528     }
1529
1530     @Override
1531     public int getRowCount()
1532     {
1533       return data.length;
1534     }
1535
1536     @Override
1537     public String getColumnName(int col)
1538     {
1539       return columnNames[col];
1540     }
1541
1542     @Override
1543     public Object getValueAt(int row, int col)
1544     {
1545       return data[row][col];
1546     }
1547
1548     /**
1549      * Answers the class of the object in column c of the first row of the table
1550      */
1551     @Override
1552     public Class<?> getColumnClass(int c)
1553     {
1554       Object v = getValueAt(0, c);
1555       return v == null ? null : v.getClass();
1556     }
1557
1558     @Override
1559     public boolean isCellEditable(int row, int col)
1560     {
1561       return col == 0 ? false : true;
1562     }
1563
1564     @Override
1565     public void setValueAt(Object value, int row, int col)
1566     {
1567       data[row][col] = value;
1568       fireTableCellUpdated(row, col);
1569       updateFeatureRenderer(data);
1570     }
1571
1572   }
1573
1574   class ColorRenderer extends JLabel implements TableCellRenderer
1575   {
1576     javax.swing.border.Border unselectedBorder = null;
1577
1578     javax.swing.border.Border selectedBorder = null;
1579
1580     final String baseTT = "Click to edit, right/apple click for menu.";
1581
1582     public ColorRenderer()
1583     {
1584       setOpaque(true); // MUST do this for background to show up.
1585       setHorizontalTextPosition(SwingConstants.CENTER);
1586       setVerticalTextPosition(SwingConstants.CENTER);
1587     }
1588
1589     @Override
1590     public Component getTableCellRendererComponent(JTable tbl, Object color,
1591             boolean isSelected, boolean hasFocus, int row, int column)
1592     {
1593       FeatureColourI cellColour = (FeatureColourI) color;
1594       setOpaque(true);
1595       setToolTipText(baseTT);
1596       setBackground(tbl.getBackground());
1597       if (!cellColour.isSimpleColour())
1598       {
1599         Rectangle cr = tbl.getCellRect(row, column, false);
1600         FeatureSettings.renderGraduatedColor(this, cellColour,
1601                 (int) cr.getWidth(), (int) cr.getHeight());
1602       }
1603       else
1604       {
1605         this.setText("");
1606         this.setIcon(null);
1607         setBackground(cellColour.getColour());
1608       }
1609       if (isSelected)
1610       {
1611         if (selectedBorder == null)
1612         {
1613           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1614                   tbl.getSelectionBackground());
1615         }
1616         setBorder(selectedBorder);
1617       }
1618       else
1619       {
1620         if (unselectedBorder == null)
1621         {
1622           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1623                   tbl.getBackground());
1624         }
1625         setBorder(unselectedBorder);
1626       }
1627
1628       return this;
1629     }
1630   }
1631
1632   class FilterRenderer extends JLabel implements TableCellRenderer
1633   {
1634     javax.swing.border.Border unselectedBorder = null;
1635
1636     javax.swing.border.Border selectedBorder = null;
1637
1638     public FilterRenderer()
1639     {
1640       setOpaque(true); // MUST do this for background to show up.
1641       setHorizontalTextPosition(SwingConstants.CENTER);
1642       setVerticalTextPosition(SwingConstants.CENTER);
1643     }
1644
1645     @Override
1646     public Component getTableCellRendererComponent(JTable tbl,
1647             Object filter, boolean isSelected, boolean hasFocus, int row,
1648             int column)
1649     {
1650       KeyedMatcherSetI theFilter = (KeyedMatcherSetI) filter;
1651       setOpaque(true);
1652       String asText = theFilter.toString();
1653       setBackground(tbl.getBackground());
1654       this.setText(asText);
1655       this.setIcon(null);
1656
1657       if (isSelected)
1658       {
1659         if (selectedBorder == null)
1660         {
1661           selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1662                   tbl.getSelectionBackground());
1663         }
1664         setBorder(selectedBorder);
1665       }
1666       else
1667       {
1668         if (unselectedBorder == null)
1669         {
1670           unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
1671                   tbl.getBackground());
1672         }
1673         setBorder(unselectedBorder);
1674       }
1675
1676       return this;
1677     }
1678   }
1679
1680   /**
1681    * update comp using rendering settings from gcol
1682    * 
1683    * @param comp
1684    * @param gcol
1685    */
1686   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol)
1687   {
1688     int w = comp.getWidth(), h = comp.getHeight();
1689     if (w < 20)
1690     {
1691       w = (int) comp.getPreferredSize().getWidth();
1692       h = (int) comp.getPreferredSize().getHeight();
1693       if (w < 20)
1694       {
1695         w = 80;
1696         h = 12;
1697       }
1698     }
1699     renderGraduatedColor(comp, gcol, w, h);
1700   }
1701
1702   public static void renderGraduatedColor(JLabel comp, FeatureColourI gcol,
1703           int w, int h)
1704   {
1705     boolean thr = false;
1706     StringBuilder tt = new StringBuilder();
1707     StringBuilder tx = new StringBuilder();
1708
1709     if (gcol.isColourByAttribute())
1710     {
1711       tx.append(String.join(":", gcol.getAttributeName()));
1712     }
1713     else if (!gcol.isColourByLabel())
1714     {
1715       tx.append(MessageManager.getString("label.score"));
1716     }
1717     tx.append(" ");
1718     if (gcol.isAboveThreshold())
1719     {
1720       thr = true;
1721       tx.append(">");
1722       tt.append("Thresholded (Above ").append(gcol.getThreshold())
1723               .append(") ");
1724     }
1725     if (gcol.isBelowThreshold())
1726     {
1727       thr = true;
1728       tx.append("<");
1729       tt.append("Thresholded (Below ").append(gcol.getThreshold())
1730               .append(") ");
1731     }
1732     if (gcol.isColourByLabel())
1733     {
1734       tt.append("Coloured by label text. ").append(tt);
1735       if (thr)
1736       {
1737         tx.append(" ");
1738       }
1739       if (!gcol.isColourByAttribute())
1740       {
1741         tx.append("Label");
1742       }
1743       comp.setIcon(null);
1744     }
1745     else
1746     {
1747       Color newColor = gcol.getMaxColour();
1748       comp.setBackground(newColor);
1749       // System.err.println("Width is " + w / 2);
1750       Icon ficon = new FeatureIcon(gcol, comp.getBackground(), w, h, thr);
1751       comp.setIcon(ficon);
1752       // tt+="RGB value: Max (" + newColor.getRed() + ", "
1753       // + newColor.getGreen() + ", " + newColor.getBlue()
1754       // + ")\nMin (" + minCol.getRed() + ", " + minCol.getGreen()
1755       // + ", " + minCol.getBlue() + ")");
1756     }
1757     comp.setHorizontalAlignment(SwingConstants.CENTER);
1758     comp.setText(tx.toString());
1759     if (tt.length() > 0)
1760     {
1761       if (comp.getToolTipText() == null)
1762       {
1763         comp.setToolTipText(tt.toString());
1764       }
1765       else
1766       {
1767         comp.setToolTipText(
1768                 tt.append(" ").append(comp.getToolTipText()).toString());
1769       }
1770     }
1771   }
1772
1773   class ColorEditor extends AbstractCellEditor
1774           implements TableCellEditor, ActionListener
1775   {
1776     FeatureSettings me;
1777
1778     FeatureColourI currentColor;
1779
1780     FeatureTypeSettings chooser;
1781
1782     String type;
1783
1784     JButton button;
1785
1786     JColorChooser colorChooser;
1787
1788     JDialog dialog;
1789
1790     protected static final String EDIT = "edit";
1791
1792     int rowSelected = 0;
1793
1794     public ColorEditor(FeatureSettings me)
1795     {
1796       this.me = me;
1797       // Set up the editor (from the table's point of view),
1798       // which is a button.
1799       // This button brings up the color chooser dialog,
1800       // which is the editor from the user's point of view.
1801       button = new JButton();
1802       button.setActionCommand(EDIT);
1803       button.addActionListener(this);
1804       button.setBorderPainted(false);
1805       // Set up the dialog that the button brings up.
1806       colorChooser = new JColorChooser();
1807       dialog = JColorChooser.createDialog(button,
1808               MessageManager.getString("label.select_colour"), true, // modal
1809               colorChooser, this, // OK button handler
1810               null); // no CANCEL button handler
1811     }
1812
1813     /**
1814      * Handles events from the editor button and from the dialog's OK button.
1815      */
1816     @Override
1817     public void actionPerformed(ActionEvent e)
1818     {
1819       // todo test e.getSource() instead here
1820       if (EDIT.equals(e.getActionCommand()))
1821       {
1822         // The user has clicked the cell, so
1823         // bring up the dialog.
1824         if (currentColor.isSimpleColour())
1825         {
1826           // bring up simple color chooser
1827           button.setBackground(currentColor.getColour());
1828           colorChooser.setColor(currentColor.getColour());
1829           dialog.setVisible(true);
1830         }
1831         else
1832         {
1833           // bring up graduated chooser.
1834           chooser = new FeatureTypeSettings(me.fr, type);
1835           chooser.setRequestFocusEnabled(true);
1836           chooser.requestFocus();
1837           chooser.addActionListener(this);
1838           chooser.showTab(true);
1839         }
1840         // Make the renderer reappear.
1841         fireEditingStopped();
1842
1843       }
1844       else
1845       {
1846         if (currentColor.isSimpleColour())
1847         {
1848           /*
1849            * read off colour picked in colour chooser after OK pressed
1850            */
1851           currentColor = new FeatureColour(colorChooser.getColor());
1852           me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
1853         }
1854         else
1855         {
1856           /*
1857            * after OK in variable colour dialog, any changes to colour 
1858            * (or filters!) are already set in FeatureRenderer, so just
1859            * update table data without triggering updateFeatureRenderer
1860            */
1861           currentColor = fr.getFeatureColours().get(type);
1862           KeyedMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
1863           if (currentFilter == null)
1864           {
1865             currentFilter = new KeyedMatcherSet();
1866           }
1867           Object[] data = ((FeatureTableModel) table.getModel())
1868                   .getData()[rowSelected];
1869           data[COLOUR_COLUMN] = currentColor;
1870           data[FILTER_COLUMN] = currentFilter;
1871         }
1872         fireEditingStopped();
1873         me.table.validate();
1874       }
1875     }
1876
1877     // Implement the one CellEditor method that AbstractCellEditor doesn't.
1878     @Override
1879     public Object getCellEditorValue()
1880     {
1881       return currentColor;
1882     }
1883
1884     // Implement the one method defined by TableCellEditor.
1885     @Override
1886     public Component getTableCellEditorComponent(JTable theTable, Object value,
1887             boolean isSelected, int row, int column)
1888     {
1889       currentColor = (FeatureColourI) value;
1890       this.rowSelected = row;
1891       type = me.table.getValueAt(row, TYPE_COLUMN).toString();
1892       button.setOpaque(true);
1893       button.setBackground(me.getBackground());
1894       if (!currentColor.isSimpleColour())
1895       {
1896         JLabel btn = new JLabel();
1897         btn.setSize(button.getSize());
1898         FeatureSettings.renderGraduatedColor(btn, currentColor);
1899         button.setBackground(btn.getBackground());
1900         button.setIcon(btn.getIcon());
1901         button.setText(btn.getText());
1902       }
1903       else
1904       {
1905         button.setText("");
1906         button.setIcon(null);
1907         button.setBackground(currentColor.getColour());
1908       }
1909       return button;
1910     }
1911   }
1912
1913   /**
1914    * The cell editor for the Filter column. It displays the text of any filters
1915    * for the feature type in that row (in full as a tooltip, possible abbreviated
1916    * as display text). On click in the cell, opens the Feature Display Settings
1917    * dialog at the Filters tab.
1918    */
1919   class FilterEditor extends AbstractCellEditor
1920           implements TableCellEditor, ActionListener
1921   {
1922     FeatureSettings me;
1923
1924     KeyedMatcherSetI currentFilter;
1925
1926     Point lastLocation;
1927
1928     String type;
1929
1930     JButton button;
1931
1932     protected static final String EDIT = "edit";
1933
1934     int rowSelected = 0;
1935
1936     public FilterEditor(FeatureSettings me)
1937     {
1938       this.me = me;
1939       button = new JButton();
1940       button.setActionCommand(EDIT);
1941       button.addActionListener(this);
1942       button.setBorderPainted(false);
1943     }
1944
1945     /**
1946      * Handles events from the editor button
1947      */
1948     @Override
1949     public void actionPerformed(ActionEvent e)
1950     {
1951       if (button == e.getSource())
1952       {
1953         FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
1954         chooser.addActionListener(this);
1955         chooser.setRequestFocusEnabled(true);
1956         chooser.requestFocus();
1957         if (lastLocation != null)
1958         {
1959           // todo open at its last position on screen
1960           chooser.setBounds(lastLocation.x, lastLocation.y,
1961                   chooser.getWidth(), chooser.getHeight());
1962           chooser.validate();
1963         }
1964         chooser.showTab(false);
1965         fireEditingStopped();
1966       }
1967       else if (e.getSource() instanceof Component)
1968       {
1969
1970         /*
1971          * after OK in variable colour dialog, any changes to filter
1972          * (or colours!) are already set in FeatureRenderer, so just
1973          * update table data without triggering updateFeatureRenderer
1974          */
1975         FeatureColourI currentColor = fr.getFeatureColours().get(type);
1976         currentFilter = me.fr.getFeatureFilter(type);
1977         if (currentFilter == null)
1978         {
1979           currentFilter = new KeyedMatcherSet();
1980         }
1981         Object[] data = ((FeatureTableModel) table.getModel())
1982                 .getData()[rowSelected];
1983         data[COLOUR_COLUMN] = currentColor;
1984         data[FILTER_COLUMN] = currentFilter;
1985         fireEditingStopped();
1986         me.table.validate();
1987       }
1988     }
1989
1990     @Override
1991     public Object getCellEditorValue()
1992     {
1993       return currentFilter;
1994     }
1995
1996     @Override
1997     public Component getTableCellEditorComponent(JTable theTable, Object value,
1998             boolean isSelected, int row, int column)
1999     {
2000       currentFilter = (KeyedMatcherSetI) value;
2001       this.rowSelected = row;
2002       type = me.table.getValueAt(row, TYPE_COLUMN).toString();
2003       button.setOpaque(true);
2004       button.setBackground(me.getBackground());
2005       button.setText(currentFilter.toString());
2006       button.setToolTipText(currentFilter.toString());
2007       button.setIcon(null);
2008       return button;
2009     }
2010   }
2011 }
2012
2013 class FeatureIcon implements Icon
2014 {
2015   FeatureColourI gcol;
2016
2017   Color backg;
2018
2019   boolean midspace = false;
2020
2021   int width = 50, height = 20;
2022
2023   int s1, e1; // start and end of midpoint band for thresholded symbol
2024
2025   Color mpcolour = Color.white;
2026
2027   FeatureIcon(FeatureColourI gfc, Color bg, int w, int h, boolean mspace)
2028   {
2029     gcol = gfc;
2030     backg = bg;
2031     width = w;
2032     height = h;
2033     midspace = mspace;
2034     if (midspace)
2035     {
2036       s1 = width / 3;
2037       e1 = s1 * 2;
2038     }
2039     else
2040     {
2041       s1 = width / 2;
2042       e1 = s1;
2043     }
2044   }
2045
2046   @Override
2047   public int getIconWidth()
2048   {
2049     return width;
2050   }
2051
2052   @Override
2053   public int getIconHeight()
2054   {
2055     return height;
2056   }
2057
2058   @Override
2059   public void paintIcon(Component c, Graphics g, int x, int y)
2060   {
2061
2062     if (gcol.isColourByLabel())
2063     {
2064       g.setColor(backg);
2065       g.fillRect(0, 0, width, height);
2066       // need an icon here.
2067       g.setColor(gcol.getMaxColour());
2068
2069       g.setFont(new Font("Verdana", Font.PLAIN, 9));
2070
2071       // g.setFont(g.getFont().deriveFont(
2072       // AffineTransform.getScaleInstance(
2073       // width/g.getFontMetrics().stringWidth("Label"),
2074       // height/g.getFontMetrics().getHeight())));
2075
2076       g.drawString(MessageManager.getString("label.label"), 0, 0);
2077
2078     }
2079     else
2080     {
2081       Color minCol = gcol.getMinColour();
2082       g.setColor(minCol);
2083       g.fillRect(0, 0, s1, height);
2084       if (midspace)
2085       {
2086         g.setColor(Color.white);
2087         g.fillRect(s1, 0, e1 - s1, height);
2088       }
2089       g.setColor(gcol.getMaxColour());
2090       g.fillRect(0, e1, width - e1, height);
2091     }
2092   }
2093 }