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