eb55994f3d1e9afd1ed8c7483e51135930500bf2
[jalview.git] / src / jalview / gui / OptsAndParamsPage.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3  * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
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 of the License, or (at your option) any later version.
10  *  
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.gui;
19
20 import jalview.ws.params.ArgumentI;
21 import jalview.ws.params.OptionI;
22 import jalview.ws.params.ParameterI;
23 import jalview.ws.params.ValueConstrainI;
24 import jalview.ws.params.ValueConstrainI.ValueType;
25
26 import java.awt.BorderLayout;
27 import java.awt.Component;
28 import java.awt.Dimension;
29 import java.awt.Font;
30 import java.awt.GridLayout;
31 import java.awt.Rectangle;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.ActionListener;
34 import java.awt.event.KeyEvent;
35 import java.awt.event.KeyListener;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.MouseListener;
38 import java.net.URL;
39 import java.util.ArrayList;
40 import java.util.Hashtable;
41 import java.util.List;
42 import java.util.Map;
43
44 import javax.swing.JButton;
45 import javax.swing.JCheckBox;
46 import javax.swing.JComboBox;
47 import javax.swing.JComponent;
48 import javax.swing.JLabel;
49 import javax.swing.JMenuItem;
50 import javax.swing.JPanel;
51 import javax.swing.JPopupMenu;
52 import javax.swing.JScrollPane;
53 import javax.swing.JSlider;
54 import javax.swing.JTextArea;
55 import javax.swing.JTextField;
56 import javax.swing.border.TitledBorder;
57 import javax.swing.event.ChangeEvent;
58 import javax.swing.event.ChangeListener;
59
60 import net.miginfocom.swing.MigLayout;
61
62 /**
63  * GUI generator/manager for options and parameters. Originally abstracted from
64  * the WsJobParameters dialog box.
65  * 
66  * @author jprocter
67  * 
68  */
69 public class OptsAndParamsPage
70 {
71   /**
72    * compact or verbose style parameters
73    */
74   boolean compact = false;
75
76   public class OptionBox extends JPanel implements MouseListener,
77           ActionListener
78   {
79     JCheckBox enabled = new JCheckBox();
80
81     final URL finfo;
82
83     boolean hasLink = false;
84
85     boolean initEnabled = false;
86
87     String initVal = null;
88
89     OptionI option;
90
91     JLabel optlabel = new JLabel();
92
93     JComboBox val = new JComboBox();
94
95     public OptionBox(OptionI opt)
96     {
97       option = opt;
98       setLayout(new BorderLayout());
99       enabled.setSelected(opt.isRequired()); // TODO: lock required options
100       enabled.setFont(new Font("Verdana", Font.PLAIN, 11));
101       enabled.setText("");
102       enabled.setText(opt.getName());
103       enabled.addActionListener(this);
104       finfo = option.getFurtherDetails();
105       String desc = opt.getDescription();
106       if (finfo != null)
107       {
108         hasLink = true;
109
110         enabled.setToolTipText("<html>"
111                 + JvSwingUtils
112                         .wrapTooltip(((desc == null || desc.trim().length()==0) ? "see further details by right-clicking"
113                                 : desc)
114                                 + "<br><img src=\"" + linkImageURL + "\"/>")
115                 + "</html>");
116         enabled.addMouseListener(this);
117       }
118       else
119       {
120         if (desc != null && desc.trim().length()>0)
121         {
122           enabled.setToolTipText("<html>"
123                   + JvSwingUtils.wrapTooltip(opt.getDescription())
124                   + "</html>");
125         }
126       }
127       add(enabled, BorderLayout.NORTH);
128       for (Object str : opt.getPossibleValues())
129       {
130         val.addItem((String) str);
131       }
132       val.setSelectedItem((String) opt.getValue());
133       if (opt.getPossibleValues().size() > 1)
134       {
135         setLayout(new GridLayout(1, 2));
136         val.addActionListener(this);
137         add(val, BorderLayout.SOUTH);
138       }
139       // TODO: add actionListeners for popup (to open further info),
140       // and to update list of parameters if an option is enabled
141       // that takes a value. JBPNote: is this TODO still valid ?
142       setInitialValue();
143     }
144
145     public void actionPerformed(ActionEvent e)
146     {
147       if (e.getSource() != enabled)
148       {
149         enabled.setSelected(true);
150       }
151       checkIfModified();
152     }
153
154     private void checkIfModified()
155     {
156       boolean notmod = (initEnabled == enabled.isSelected());
157       if (enabled.isSelected())
158       {
159         if (initVal != null)
160         {
161           notmod &= initVal.equals(val.getSelectedItem());
162         }
163         else
164         {
165           // compare against default service setting
166           notmod &= option.getValue() == null
167                   || option.getValue().equals(val.getSelectedItem());
168         }
169       }
170       else
171       {
172         notmod &= (initVal != null) ? initVal.equals(val.getSelectedItem())
173                 : val.getSelectedItem() != initVal;
174       }
175       poparent.argSetModified(this, !notmod);
176     }
177
178     public OptionI getOptionIfEnabled()
179     {
180       if (!enabled.isSelected())
181       {
182         return null;
183       }
184       OptionI opt = option.copy();
185       if (opt.getPossibleValues() != null
186               && opt.getPossibleValues().size() == 1)
187       {
188         // Hack to make sure the default value for an enabled option with only
189         // one value is actually returned
190         opt.setValue(opt.getPossibleValues().get(0));
191       }
192       if (val.getSelectedItem() != null)
193       {
194         opt.setValue((String) val.getSelectedItem());
195       }
196       else
197       {
198         if (option.getValue() != null)
199         {
200           opt.setValue(option.getValue());
201         }
202       }
203       return opt;
204     }
205
206     public void mouseClicked(MouseEvent e)
207     {
208       if (javax.swing.SwingUtilities.isRightMouseButton(e))
209       {
210         showUrlPopUp(this, finfo.toString(), e.getX(), e.getY());
211       }
212     }
213
214     public void mouseEntered(MouseEvent e)
215     {
216       // TODO Auto-generated method stub
217
218     }
219
220     public void mouseExited(MouseEvent e)
221     {
222       // TODO Auto-generated method stub
223
224     }
225
226     public void mousePressed(MouseEvent e)
227     {
228       // TODO Auto-generated method stub
229
230     }
231
232     public void mouseReleased(MouseEvent e)
233     {
234       // TODO Auto-generated method stub
235
236     }
237
238
239     public void resetToDefault(boolean setDefaultParams)
240     {
241       enabled.setSelected(false);
242       if (option.isRequired() || (setDefaultParams && option.getValue()!=null))
243       {
244         // Apply default value
245         selectOption(option, option.getValue());
246       }
247     }
248
249     public void setInitialValue()
250     {
251       initEnabled = enabled.isSelected();
252       if (option.getPossibleValues() != null
253               && option.getPossibleValues().size() > 1)
254       {
255         initVal = (String) val.getSelectedItem();
256       }
257       else
258       {
259         initVal = (initEnabled) ? (String) val.getSelectedItem() : null;
260       }
261     }
262     
263   }
264
265   public class ParamBox extends JPanel implements ChangeListener,
266           ActionListener, MouseListener
267   {
268     boolean adjusting = false;
269
270     boolean choice = false;
271
272     JComboBox choicebox;
273
274     JPanel controlPanel = new JPanel();
275
276     boolean descisvisible = false;
277
278     JScrollPane descPanel = new JScrollPane();
279
280     final URL finfo;
281
282     boolean integ = false;
283
284     Object lastVal;
285
286     ParameterI parameter;
287
288     final OptsParametersContainerI pmdialogbox;
289
290     JPanel settingPanel = new JPanel();
291
292     JButton showDesc = new JButton();
293
294     JSlider slider = null;
295
296     JTextArea string = new JTextArea();
297
298     ValueConstrainI validator = null;
299
300     JTextField valueField = null;
301
302     public ParamBox(final OptsParametersContainerI pmlayout, ParameterI parm)
303     {
304       pmdialogbox = pmlayout;
305       finfo = parm.getFurtherDetails();
306       validator = parm.getValidValue();
307       parameter = parm;
308       if (validator != null)
309       {
310         integ = validator.getType() == ValueType.Integer;
311       }
312       else
313       {
314         if (parameter.getPossibleValues() != null)
315         {
316           choice = true;
317         }
318       }
319
320       if (!compact)
321       {
322         makeExpanderParam(parm);
323       }
324       else
325       {
326         makeCompactParam(parm);
327
328       }
329     }
330
331     private void makeCompactParam(ParameterI parm)
332     {
333       setLayout(new MigLayout("", "[][grow]"));
334
335       String ttipText = null;
336
337       controlPanel.setLayout(new BorderLayout());
338
339       if (parm.getDescription() != null
340               && parm.getDescription().trim().length() > 0)
341       {
342         // Only create description boxes if there actually is a description.
343         ttipText = ("<html>"
344                 + JvSwingUtils
345                         .wrapTooltip(parm.getDescription()
346                                 + (finfo != null ? "<br><img src=\""
347                                         + linkImageURL
348                                         + "\"/> Right click for further information."
349                                         : "")) + "</html>");
350       }
351
352       JvSwingUtils.mgAddtoLayout(this, ttipText,
353               new JLabel(parm.getName()), controlPanel, "");
354       updateControls(parm);
355       validate();
356     }
357
358     private void makeExpanderParam(ParameterI parm)
359     {
360       setPreferredSize(new Dimension(PARAM_WIDTH, PARAM_CLOSEDHEIGHT));
361       setBorder(new TitledBorder(parm.getName()));
362       setLayout(null);
363       showDesc.setFont(new Font("Verdana", Font.PLAIN, 6));
364       showDesc.setText("+");
365       string.setFont(new Font("Verdana", Font.PLAIN, 11));
366       string.setBackground(getBackground());
367
368       string.setEditable(false);
369       descPanel.getViewport().setView(string);
370
371       descPanel.setVisible(false);
372
373       JPanel firstrow = new JPanel();
374       firstrow.setLayout(null);
375       controlPanel.setLayout(new BorderLayout());
376       controlPanel.setBounds(new Rectangle(39, 10, PARAM_WIDTH - 70,
377               PARAM_CLOSEDHEIGHT - 50));
378       firstrow.add(controlPanel);
379       firstrow.setBounds(new Rectangle(10, 20, PARAM_WIDTH - 30,
380               PARAM_CLOSEDHEIGHT - 30));
381
382       final ParamBox me = this;
383
384       if (parm.getDescription() != null
385               && parm.getDescription().trim().length() > 0)
386       {
387         // Only create description boxes if there actually is a description.
388         if (finfo != null)
389         {
390           showDesc.setToolTipText("<html>"
391                   + JvSwingUtils
392                           .wrapTooltip("Click to show brief description<br><img src=\""
393                                   + linkImageURL
394                                   + "\"/> Right click for further information.")
395                   + "</html>");
396           showDesc.addMouseListener(this);
397         }
398         else
399         {
400           showDesc.setToolTipText("<html>"
401                   + JvSwingUtils
402                           .wrapTooltip("Click to show brief description.")
403                   + "</html>");
404         }
405         showDesc.addActionListener(new ActionListener()
406         {
407
408           public void actionPerformed(ActionEvent e)
409           {
410             descisvisible = !descisvisible;
411             descPanel.setVisible(descisvisible);
412             descPanel.getVerticalScrollBar().setValue(0);
413             me.setPreferredSize(new Dimension(PARAM_WIDTH,
414                     (descisvisible) ? PARAM_HEIGHT : PARAM_CLOSEDHEIGHT));
415             me.validate();
416             pmdialogbox.refreshParamLayout();
417           }
418         });
419         string.setWrapStyleWord(true);
420         string.setLineWrap(true);
421         string.setColumns(32);
422         string.setText(parm.getDescription());
423         showDesc.setBounds(new Rectangle(10, 10, 16, 16));
424         firstrow.add(showDesc);
425       }
426       add(firstrow);
427       validator = parm.getValidValue();
428       parameter = parm;
429       if (validator != null)
430       {
431         integ = validator.getType() == ValueType.Integer;
432       }
433       else
434       {
435         if (parameter.getPossibleValues() != null)
436         {
437           choice = true;
438         }
439       }
440       updateControls(parm);
441       descPanel.setBounds(new Rectangle(10, PARAM_CLOSEDHEIGHT,
442               PARAM_WIDTH - 20, PARAM_HEIGHT - PARAM_CLOSEDHEIGHT - 5));
443       add(descPanel);
444       validate();
445     }
446
447     public void actionPerformed(ActionEvent e)
448     {
449       if (adjusting)
450       {
451         return;
452       }
453       if (!choice)
454       {
455         updateSliderFromValueField();
456       }
457       checkIfModified();
458     }
459
460     private void checkIfModified()
461     {
462       Object cstate = updateSliderFromValueField();
463       boolean notmod = false;
464       if (cstate.getClass() == lastVal.getClass())
465       {
466         if (cstate instanceof int[])
467         {
468           notmod = (((int[]) cstate)[0] == ((int[]) lastVal)[0]);
469         }
470         else if (cstate instanceof float[])
471         {
472           notmod = (((float[]) cstate)[0] == ((float[]) lastVal)[0]);
473         }
474         else if (cstate instanceof String[])
475         {
476           notmod = (((String[]) cstate)[0].equals(((String[]) lastVal)[0]));
477         }
478       }
479       pmdialogbox.argSetModified(this, !notmod);
480     }
481
482     @Override
483     public int getBaseline(int width, int height)
484     {
485       return 0;
486     }
487
488     // from
489     // http://stackoverflow.com/questions/2743177/top-alignment-for-flowlayout
490     // helpful hint of using the Java 1.6 alignBaseLine property of FlowLayout
491     @Override
492     public Component.BaselineResizeBehavior getBaselineResizeBehavior()
493     {
494       return Component.BaselineResizeBehavior.CONSTANT_ASCENT;
495     }
496
497     public int getBoxHeight()
498     {
499       return (descisvisible ? PARAM_HEIGHT : PARAM_CLOSEDHEIGHT);
500     }
501
502     public ParameterI getParameter()
503     {
504       ParameterI prm = parameter.copy();
505       if (choice)
506       {
507         prm.setValue((String) choicebox.getSelectedItem());
508       }
509       else
510       {
511         prm.setValue(valueField.getText());
512       }
513       return prm;
514     }
515
516     public void init()
517     {
518       // reset the widget's initial value.
519       lastVal = null;
520     }
521
522     public void mouseClicked(MouseEvent e)
523     {
524       if (javax.swing.SwingUtilities.isRightMouseButton(e))
525       {
526         showUrlPopUp(this, finfo.toString(), e.getX(), e.getY());
527       }
528     }
529
530     public void mouseEntered(MouseEvent e)
531     {
532       // TODO Auto-generated method stub
533
534     }
535
536     public void mouseExited(MouseEvent e)
537     {
538       // TODO Auto-generated method stub
539
540     }
541
542     public void mousePressed(MouseEvent e)
543     {
544       // TODO Auto-generated method stub
545
546     }
547
548     public void mouseReleased(MouseEvent e)
549     {
550       // TODO Auto-generated method stub
551
552     }
553
554     public void stateChanged(ChangeEvent e)
555     {
556       if (!adjusting)
557       {
558         valueField.setText(""
559                 + ((integ) ? ("" + (int) slider.getValue())
560                         : ("" + (float) (slider.getValue() / 1000f))));
561         checkIfModified();
562       }
563
564     }
565
566     public void updateControls(ParameterI parm)
567     {
568       adjusting = true;
569       boolean init = (choicebox == null && valueField == null);
570       if (init)
571       {
572         if (choice)
573         {
574           choicebox = new JComboBox();
575           choicebox.addActionListener(this);
576           controlPanel.add(choicebox, BorderLayout.CENTER);
577         }
578         else
579         {
580           slider = new JSlider();
581           slider.addChangeListener(this);
582           valueField = new JTextField();
583           valueField.addActionListener(this);
584           valueField.addKeyListener(new KeyListener()
585           {
586
587             @Override
588             public void keyTyped(KeyEvent e)
589             {
590             }
591
592             @Override
593             public void keyReleased(KeyEvent e)
594             {
595               if (valueField.getText().trim().length() > 0)
596               {
597                 actionPerformed(null);
598               }
599             }
600
601             @Override
602             public void keyPressed(KeyEvent e)
603             {
604             }
605           });
606           valueField.setPreferredSize(new Dimension(60, 25));
607           controlPanel.add(slider, BorderLayout.WEST);
608           controlPanel.add(valueField, BorderLayout.EAST);
609
610         }
611       }
612
613       if (parm != null)
614       {
615         if (choice)
616         {
617           if (init)
618           {
619             List vals = parm.getPossibleValues();
620             for (Object val : vals)
621             {
622               choicebox.addItem(val);
623             }
624           }
625
626           if (parm.getValue() != null)
627           {
628             choicebox.setSelectedItem(parm.getValue());
629           }
630         }
631         else
632         {
633           valueField.setText(parm.getValue());
634         }
635       }
636       lastVal = updateSliderFromValueField();
637       adjusting = false;
638     }
639
640     public Object updateSliderFromValueField()
641     {
642       int iVal;
643       float fVal;
644       if (validator != null)
645       {
646         if (integ)
647         {
648           iVal = 0;
649           try
650           {
651             valueField.setText(valueField.getText().trim());
652             iVal = Integer.valueOf(valueField.getText());
653             if (validator.getMin() != null
654                     && validator.getMin().intValue() > iVal)
655             {
656               iVal = validator.getMin().intValue();
657               // TODO: provide visual indication that hard limit was reached for
658               // this parameter
659             }
660             if (validator.getMax() != null
661                     && validator.getMax().intValue() < iVal)
662             {
663               iVal = validator.getMax().intValue();
664               // TODO: provide visual indication that hard limit was reached for
665               // this parameter
666             }
667           } catch (Exception e)
668           {
669           }
670           ;
671           // update value field to reflect any bound checking we performed.
672           valueField.setText("" + iVal);
673           if (validator.getMin() != null && validator.getMax() != null)
674           {
675             slider.getModel().setRangeProperties(iVal, 1,
676                     validator.getMin().intValue(),
677                     validator.getMax().intValue(), true);
678           }
679           else
680           {
681             slider.setVisible(false);
682           }
683           return new int[]
684           { iVal };
685         }
686         else
687         {
688           fVal = 0f;
689           try
690           {
691             valueField.setText(valueField.getText().trim());
692             fVal = Float.valueOf(valueField.getText());
693             if (validator.getMin() != null
694                     && validator.getMin().floatValue() > fVal)
695             {
696               fVal = validator.getMin().floatValue();
697               // TODO: provide visual indication that hard limit was reached for
698               // this parameter
699               // update value field to reflect any bound checking we performed.
700               valueField.setText("" + fVal);
701             }
702             if (validator.getMax() != null
703                     && validator.getMax().floatValue() < fVal)
704             {
705               fVal = validator.getMax().floatValue();
706               // TODO: provide visual indication that hard limit was reached for
707               // this parameter
708               // update value field to reflect any bound checking we performed.
709               valueField.setText("" + fVal);
710             }
711           } catch (Exception e)
712           {
713           }
714           ;
715           if (validator.getMin() != null && validator.getMax() != null)
716           {
717             slider.getModel().setRangeProperties((int) fVal * 1000, 1,
718                     (int) validator.getMin().floatValue() * 1000,
719                     (int) validator.getMax().floatValue() * 1000, true);
720           }
721           else
722           {
723             slider.setVisible(false);
724           }
725           return new float[]
726           { fVal };
727         }
728       }
729       else
730       {
731         if (!choice)
732         {
733           slider.setVisible(false);
734           return new String[]
735           { valueField.getText().trim() };
736         }
737         else
738         {
739           return new String[]
740           { (String) choicebox.getSelectedItem() };
741         }
742       }
743
744     }
745   }
746
747   public static final int PARAM_WIDTH = 340;
748
749   public static final int PARAM_HEIGHT = 150;
750
751   public static final int PARAM_CLOSEDHEIGHT = 80;
752
753   public OptsAndParamsPage(OptsParametersContainerI paramContainer)
754   {
755     this(paramContainer, false);
756   }
757
758   public OptsAndParamsPage(OptsParametersContainerI paramContainer,
759           boolean compact)
760   {
761     poparent = paramContainer;
762     this.compact = compact;
763   }
764
765   public static void showUrlPopUp(JComponent invoker, final String finfo,
766           int x, int y)
767   {
768
769     JPopupMenu mnu = new JPopupMenu();
770     JMenuItem mitem = new JMenuItem("View " + finfo);
771     mitem.addActionListener(new ActionListener()
772     {
773
774       @Override
775       public void actionPerformed(ActionEvent e)
776       {
777         Desktop.showUrl(finfo);
778
779       }
780     });
781     mnu.add(mitem);
782     mnu.show(invoker, x, y);
783   }
784
785   URL linkImageURL = getClass().getResource("/images/link.gif");
786
787   Map<String, OptionBox> optSet = new java.util.LinkedHashMap<String, OptionBox>();
788
789   Map<String, ParamBox> paramSet = new java.util.LinkedHashMap<String, ParamBox>();
790
791   public Map<String, OptionBox> getOptSet()
792   {
793     return optSet;
794   }
795
796   public void setOptSet(Map<String, OptionBox> optSet)
797   {
798     this.optSet = optSet;
799   }
800
801   public Map<String, ParamBox> getParamSet()
802   {
803     return paramSet;
804   }
805
806   public void setParamSet(Map<String, ParamBox> paramSet)
807   {
808     this.paramSet = paramSet;
809   }
810
811   OptsParametersContainerI poparent;
812
813   OptionBox addOption(OptionI opt)
814   {
815     OptionBox cb = optSet.get(opt.getName());
816     if (cb == null)
817     {
818       cb = new OptionBox(opt);
819       optSet.put(opt.getName(), cb);
820       // jobOptions.add(cb, FlowLayout.LEFT);
821     }
822     return cb;
823   }
824
825   ParamBox addParameter(ParameterI arg)
826   {
827     ParamBox pb = paramSet.get(arg.getName());
828     if (pb == null)
829     {
830       pb = new ParamBox(poparent, arg);
831       paramSet.put(arg.getName(), pb);
832       // paramList.add(pb);
833     }
834     pb.init();
835     // take the defaults from the parameter
836     pb.updateControls(arg);
837     return pb;
838   }
839
840   void selectOption(OptionI option, String string)
841   {
842     OptionBox cb = optSet.get(option.getName());
843     if (cb == null)
844     {
845       cb = addOption(option);
846     }
847     cb.enabled.setSelected(string != null); // initial state for an option.
848     if (string != null)
849     {
850       if (option.getPossibleValues().contains(string))
851       {
852         cb.val.setSelectedItem(string);
853       }
854       else
855       {
856         throw new Error("Invalid value " + string + " for option " + option);
857       }
858
859     }
860     if (option.isRequired() && !cb.enabled.isSelected())
861     {
862       // TODO: indicate paramset is not valid.. option needs to be selected!
863     }
864     cb.setInitialValue();
865   }
866
867   void setParameter(ParameterI arg)
868   {
869     ParamBox pb = paramSet.get(arg.getName());
870     if (pb == null)
871     {
872       addParameter(arg);
873     }
874     else
875     {
876       pb.updateControls(arg);
877     }
878
879   }
880
881   /**
882    * recover options and parameters from GUI
883    * 
884    * @return
885    */
886   public List<ArgumentI> getCurrentSettings()
887   {
888     List<ArgumentI> argSet = new ArrayList<ArgumentI>();
889     for (OptionBox opts : getOptSet().values())
890     {
891       OptionI opt = opts.getOptionIfEnabled();
892       if (opt != null)
893       {
894         argSet.add(opt);
895       }
896     }
897     for (ParamBox parambox : getParamSet().values())
898     {
899       ParameterI parm = parambox.getParameter();
900       if (parm != null)
901       {
902         argSet.add(parm);
903       }
904     }
905
906     return argSet;
907   }
908
909 }