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