63a57f88a06c29348a93e9991a25454ef4debb60
[jalview.git] / src / jalview / gui / SeqPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.4)
3  * Copyright (C) 2008 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle
4  * 
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  * 
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18  */
19 package jalview.gui;
20
21 import java.util.*;
22
23 import java.awt.*;
24 import java.awt.event.*;
25 import javax.swing.*;
26
27 import jalview.commands.*;
28 import jalview.datamodel.*;
29 import jalview.schemes.*;
30 import jalview.structure.*;
31
32 /**
33  * DOCUMENT ME!
34  * 
35  * @author $author$
36  * @version $Revision$
37  */
38 public class SeqPanel extends JPanel implements MouseListener,
39         MouseMotionListener, MouseWheelListener, SequenceListener
40
41 {
42   /** DOCUMENT ME!! */
43   public SeqCanvas seqCanvas;
44
45   /** DOCUMENT ME!! */
46   public AlignmentPanel ap;
47
48   protected int lastres;
49
50   protected int startseq;
51
52   protected AlignViewport av;
53
54   ScrollThread scrollThread = null;
55
56   boolean mouseDragging = false;
57
58   boolean editingSeqs = false;
59
60   boolean groupEditing = false;
61
62   // ////////////////////////////////////////
63   // ///Everything below this is for defining the boundary of the rubberband
64   // ////////////////////////////////////////
65   int oldSeq = -1;
66
67   boolean changeEndSeq = false;
68
69   boolean changeStartSeq = false;
70
71   boolean changeEndRes = false;
72
73   boolean changeStartRes = false;
74
75   SequenceGroup stretchGroup = null;
76
77   boolean remove = false;
78
79   Point lastMousePress;
80
81   boolean mouseWheelPressed = false;
82
83   StringBuffer keyboardNo1;
84
85   StringBuffer keyboardNo2;
86
87   java.net.URL linkImageURL;
88
89   StringBuffer tooltipText = new StringBuffer("<html>");
90
91   String tmpString;
92
93   EditCommand editCommand;
94
95   StructureSelectionManager ssm;
96
97   /**
98    * Creates a new SeqPanel object.
99    * 
100    * @param avp
101    *                DOCUMENT ME!
102    * @param p
103    *                DOCUMENT ME!
104    */
105   public SeqPanel(AlignViewport av, AlignmentPanel ap)
106   {
107     linkImageURL = getClass().getResource("/images/link.gif");
108     ToolTipManager.sharedInstance().registerComponent(this);
109     ToolTipManager.sharedInstance().setInitialDelay(0);
110     ToolTipManager.sharedInstance().setDismissDelay(10000);
111     this.av = av;
112     setBackground(Color.white);
113
114     seqCanvas = new SeqCanvas(ap);
115     setLayout(new BorderLayout());
116     add(seqCanvas, BorderLayout.CENTER);
117
118     this.ap = ap;
119
120     if (!av.isDataset())
121     {
122       addMouseMotionListener(this);
123       addMouseListener(this);
124       addMouseWheelListener(this);
125       ssm = StructureSelectionManager.getStructureSelectionManager();
126       ssm.addStructureViewerListener(this);
127     }
128   }
129
130   int startWrapBlock = -1;
131
132   int wrappedBlock = -1;
133
134   int findRes(MouseEvent evt)
135   {
136     int res = 0;
137     int x = evt.getX();
138
139     if (av.wrapAlignment)
140     {
141
142       int hgap = av.charHeight;
143       if (av.scaleAboveWrapped)
144       {
145         hgap += av.charHeight;
146       }
147
148       int cHeight = av.getAlignment().getHeight() * av.charHeight + hgap
149               + seqCanvas.getAnnotationHeight();
150
151       int y = evt.getY();
152       y -= hgap;
153       x -= seqCanvas.LABEL_WEST;
154
155       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
156       if (cwidth < 1)
157       {
158         return 0;
159       }
160
161       wrappedBlock = y / cHeight;
162       wrappedBlock += av.getStartRes() / cwidth;
163
164       res = wrappedBlock * cwidth + x / av.getCharWidth();
165
166     }
167     else
168     {
169       res = (x / av.getCharWidth()) + av.getStartRes();
170     }
171
172     if (av.hasHiddenColumns)
173     {
174       res = av.getColumnSelection().adjustForHiddenColumns(res);
175     }
176
177     return res;
178
179   }
180
181   int findSeq(MouseEvent evt)
182   {
183     int seq = 0;
184     int y = evt.getY();
185
186     if (av.wrapAlignment)
187     {
188       int hgap = av.charHeight;
189       if (av.scaleAboveWrapped)
190       {
191         hgap += av.charHeight;
192       }
193
194       int cHeight = av.getAlignment().getHeight() * av.charHeight + hgap
195               + seqCanvas.getAnnotationHeight();
196
197       y -= hgap;
198
199       seq = Math.min((y % cHeight) / av.getCharHeight(), av.alignment
200               .getHeight() - 1);
201     }
202     else
203     {
204       seq = Math.min((y / av.getCharHeight()) + av.getStartSeq(),
205               av.alignment.getHeight() - 1);
206     }
207
208     return seq;
209   }
210
211   SequenceFeature[] findFeaturesAtRes(SequenceI sequence, int res)
212   {
213     Vector tmp = new Vector();
214     SequenceFeature[] features = sequence.getSequenceFeatures();
215     if (features != null)
216     {
217       for (int i = 0; i < features.length; i++)
218       {
219         if (av.featuresDisplayed == null
220                 || !av.featuresDisplayed.containsKey(features[i].getType()))
221         {
222           continue;
223         }
224
225         if (features[i].featureGroup != null
226                 && seqCanvas.fr.featureGroups != null
227                 && seqCanvas.fr.featureGroups
228                         .containsKey(features[i].featureGroup)
229                 && !((Boolean) seqCanvas.fr.featureGroups
230                         .get(features[i].featureGroup)).booleanValue())
231           continue;
232
233         if ((features[i].getBegin() <= res)
234                 && (features[i].getEnd() >= res))
235         {
236           tmp.addElement(features[i]);
237         }
238       }
239     }
240
241     features = new SequenceFeature[tmp.size()];
242     tmp.copyInto(features);
243
244     return features;
245   }
246
247   void endEditing()
248   {
249     if (editCommand != null && editCommand.getSize() > 0)
250     {
251       ap.alignFrame.addHistoryItem(editCommand);
252       av.firePropertyChange("alignment", null, av.getAlignment()
253               .getSequences());
254     }
255
256     startseq = -1;
257     lastres = -1;
258     editingSeqs = false;
259     groupEditing = false;
260     keyboardNo1 = null;
261     keyboardNo2 = null;
262     editCommand = null;
263   }
264
265   void setCursorRow()
266   {
267     seqCanvas.cursorY = getKeyboardNo1() - 1;
268     scrollToVisible();
269   }
270
271   void setCursorColumn()
272   {
273     seqCanvas.cursorX = getKeyboardNo1() - 1;
274     scrollToVisible();
275   }
276
277   void setCursorRowAndColumn()
278   {
279     if (keyboardNo2 == null)
280     {
281       keyboardNo2 = new StringBuffer();
282     }
283     else
284     {
285       seqCanvas.cursorX = getKeyboardNo1() - 1;
286       seqCanvas.cursorY = getKeyboardNo2() - 1;
287       scrollToVisible();
288     }
289   }
290
291   void setCursorPosition()
292   {
293     SequenceI sequence = (Sequence) av.getAlignment().getSequenceAt(
294             seqCanvas.cursorY);
295
296     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1() - 1);
297     scrollToVisible();
298   }
299
300   void moveCursor(int dx, int dy)
301   {
302     seqCanvas.cursorX += dx;
303     seqCanvas.cursorY += dy;
304     if (av.hasHiddenColumns && !av.colSel.isVisible(seqCanvas.cursorX))
305     {
306       int original = seqCanvas.cursorX - dx;
307       int maxWidth = av.alignment.getWidth();
308
309       while (!av.colSel.isVisible(seqCanvas.cursorX)
310               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
311       {
312         seqCanvas.cursorX += dx;
313       }
314
315       if (seqCanvas.cursorX >= maxWidth
316               || !av.colSel.isVisible(seqCanvas.cursorX))
317       {
318         seqCanvas.cursorX = original;
319       }
320     }
321
322     scrollToVisible();
323   }
324
325   void scrollToVisible()
326   {
327     if (seqCanvas.cursorX < 0)
328     {
329       seqCanvas.cursorX = 0;
330     }
331     else if (seqCanvas.cursorX > av.alignment.getWidth() - 1)
332     {
333       seqCanvas.cursorX = av.alignment.getWidth() - 1;
334     }
335
336     if (seqCanvas.cursorY < 0)
337     {
338       seqCanvas.cursorY = 0;
339     }
340     else if (seqCanvas.cursorY > av.alignment.getHeight() - 1)
341     {
342       seqCanvas.cursorY = av.alignment.getHeight() - 1;
343     }
344
345     endEditing();
346     if (av.wrapAlignment)
347     {
348       ap.scrollToWrappedVisible(seqCanvas.cursorX);
349     }
350     else
351     {
352       while (seqCanvas.cursorY < av.startSeq)
353       {
354         ap.scrollUp(true);
355       }
356       while (seqCanvas.cursorY + 1 > av.endSeq)
357       {
358         ap.scrollUp(false);
359       }
360       if (!av.wrapAlignment)
361       {
362         while (seqCanvas.cursorX < av.colSel
363                 .adjustForHiddenColumns(av.startRes))
364         {
365           if (!ap.scrollRight(false))
366           {
367             break;
368           }
369         }
370         while (seqCanvas.cursorX > av.colSel
371                 .adjustForHiddenColumns(av.endRes))
372         {
373           if (!ap.scrollRight(true))
374           {
375             break;
376           }
377         }
378       }
379     }
380     setStatusMessage(av.alignment.getSequenceAt(seqCanvas.cursorY),
381             seqCanvas.cursorX, seqCanvas.cursorY);
382
383     seqCanvas.repaint();
384   }
385
386   void setSelectionAreaAtCursor(boolean topLeft)
387   {
388     SequenceI sequence = (Sequence) av.getAlignment().getSequenceAt(
389             seqCanvas.cursorY);
390
391     if (av.getSelectionGroup() != null)
392     {
393       SequenceGroup sg = av.selectionGroup;
394       // Find the top and bottom of this group
395       int min = av.alignment.getHeight(), max = 0;
396       for (int i = 0; i < sg.getSize(); i++)
397       {
398         int index = av.alignment.findIndex(sg.getSequenceAt(i));
399         if (index > max)
400         {
401           max = index;
402         }
403         if (index < min)
404         {
405           min = index;
406         }
407       }
408
409       max++;
410
411       if (topLeft)
412       {
413         sg.setStartRes(seqCanvas.cursorX);
414         if (sg.getEndRes() < seqCanvas.cursorX)
415         {
416           sg.setEndRes(seqCanvas.cursorX);
417         }
418
419         min = seqCanvas.cursorY;
420       }
421       else
422       {
423         sg.setEndRes(seqCanvas.cursorX);
424         if (sg.getStartRes() > seqCanvas.cursorX)
425         {
426           sg.setStartRes(seqCanvas.cursorX);
427         }
428
429         max = seqCanvas.cursorY + 1;
430       }
431
432       if (min > max)
433       {
434         // Only the user can do this
435         av.setSelectionGroup(null);
436       }
437       else
438       {
439         // Now add any sequences between min and max
440         sg.getSequences(null).clear();
441         for (int i = min; i < max; i++)
442         {
443           sg.addSequence(av.alignment.getSequenceAt(i), false);
444         }
445       }
446     }
447
448     if (av.getSelectionGroup() == null)
449     {
450       SequenceGroup sg = new SequenceGroup();
451       sg.setStartRes(seqCanvas.cursorX);
452       sg.setEndRes(seqCanvas.cursorX);
453       sg.addSequence(sequence, false);
454       av.setSelectionGroup(sg);
455     }
456
457     ap.paintAlignment(false);
458   }
459
460   void insertGapAtCursor(boolean group)
461   {
462     groupEditing = group;
463     startseq = seqCanvas.cursorY;
464     lastres = seqCanvas.cursorX;
465     editSequence(true, seqCanvas.cursorX + getKeyboardNo1());
466     endEditing();
467   }
468
469   void deleteGapAtCursor(boolean group)
470   {
471     groupEditing = group;
472     startseq = seqCanvas.cursorY;
473     lastres = seqCanvas.cursorX + getKeyboardNo1();
474     editSequence(false, seqCanvas.cursorX);
475     endEditing();
476   }
477
478   void numberPressed(char value)
479   {
480     if (keyboardNo1 == null)
481     {
482       keyboardNo1 = new StringBuffer();
483     }
484
485     if (keyboardNo2 != null)
486     {
487       keyboardNo2.append(value);
488     }
489     else
490     {
491       keyboardNo1.append(value);
492     }
493   }
494
495   int getKeyboardNo1()
496   {
497     if (keyboardNo1 == null)
498       return 1;
499     else
500     {
501       int value = Integer.parseInt(keyboardNo1.toString());
502       keyboardNo1 = null;
503       return value;
504     }
505   }
506
507   int getKeyboardNo2()
508   {
509     if (keyboardNo2 == null)
510       return 1;
511     else
512     {
513       int value = Integer.parseInt(keyboardNo2.toString());
514       keyboardNo2 = null;
515       return value;
516     }
517   }
518
519   /**
520    * DOCUMENT ME!
521    * 
522    * @param evt
523    *                DOCUMENT ME!
524    */
525   public void mouseReleased(MouseEvent evt)
526   {
527     mouseDragging = false;
528     mouseWheelPressed = false;
529
530     if (!editingSeqs)
531     {
532       doMouseReleasedDefineMode(evt);
533       return;
534     }
535
536     endEditing();
537   }
538
539   /**
540    * DOCUMENT ME!
541    * 
542    * @param evt
543    *                DOCUMENT ME!
544    */
545   public void mousePressed(MouseEvent evt)
546   {
547     lastMousePress = evt.getPoint();
548
549     if (javax.swing.SwingUtilities.isMiddleMouseButton(evt))
550     {
551       mouseWheelPressed = true;
552       return;
553     }
554
555     if (evt.isShiftDown() || evt.isAltDown() || evt.isControlDown())
556     {
557       if (evt.isAltDown() || evt.isControlDown())
558       {
559         groupEditing = true;
560       }
561       editingSeqs = true;
562     }
563     else
564     {
565       doMousePressedDefineMode(evt);
566       return;
567     }
568
569     int seq = findSeq(evt);
570     int res = findRes(evt);
571
572     if (seq < 0 || res < 0)
573     {
574       return;
575     }
576
577     if ((seq < av.getAlignment().getHeight())
578             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
579     {
580       startseq = seq;
581       lastres = res;
582     }
583     else
584     {
585       startseq = -1;
586       lastres = -1;
587     }
588
589     return;
590   }
591
592   String lastMessage;
593
594   public void mouseOverSequence(SequenceI sequence, int index, int pos)
595   {
596     String tmp = sequence.hashCode() + " " + index + " " + pos;
597
598     if (lastMessage == null || !lastMessage.equals(tmp))
599     {
600       // System.err.println("mouseOver Sequence: "+tmp);
601       ssm.mouseOverSequence(sequence, index, pos);
602     }
603     lastMessage = tmp;
604   }
605
606   public void highlightSequence(SearchResults results)
607   {
608     seqCanvas.highlightSearchResults(results);
609   }
610
611   public void updateColours(SequenceI seq, int index)
612   {
613     System.out.println("update the seqPanel colours");
614     // repaint();
615   }
616
617   /**
618    * DOCUMENT ME!
619    * 
620    * @param evt
621    *                DOCUMENT ME!
622    */
623   public void mouseMoved(MouseEvent evt)
624   {
625     if (editingSeqs)
626     {
627       // This is because MacOSX creates a mouseMoved
628       // If control is down, other platforms will not.
629       mouseDragged(evt);
630     }
631
632     int res = findRes(evt);
633     int seq = findSeq(evt);
634     int pos;
635     if (res < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
636     {
637       return;
638     }
639
640     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
641
642     if (res >= sequence.getLength())
643     {
644       return;
645     }
646
647     pos = setStatusMessage(sequence, res, seq);
648     if (ssm != null && pos > -1)
649       mouseOverSequence(sequence, res, pos);
650
651     tooltipText.setLength(6); // Cuts the buffer back to <html>
652
653     SequenceGroup[] groups = av.alignment.findAllGroups(sequence);
654     if (groups != null)
655     {
656       for (int g = 0; g < groups.length; g++)
657       {
658         if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
659         {
660           if (tooltipText.length() > 6)
661           {
662             tooltipText.append("<br>");
663           }
664
665           if (!groups[g].getName().startsWith("JTreeGroup")
666                   && !groups[g].getName().startsWith("JGroup"))
667           {
668             tooltipText.append(groups[g].getName());
669           }
670
671           if (groups[g].getDescription() != null)
672           {
673             tooltipText.append(": " + groups[g].getDescription());
674           }
675         }
676       }
677     }
678
679     // use aa to see if the mouse pointer is on a
680     if (av.showSequenceFeatures)
681     {
682       int rpos;
683       SequenceFeature[] features = findFeaturesAtRes(sequence
684               .getDatasetSequence(), rpos = sequence.findPosition(res));
685       appendFeatures(tooltipText, linkImageURL.toString(), rpos, features);
686     }
687     if (tooltipText.length() == 6) // <html></html>
688     {
689       setToolTipText(null);
690     }
691     else
692     {
693       tooltipText.append("</html>");
694       if (lastTooltip == null
695               || !lastTooltip.equals(tooltipText.toString()))
696         setToolTipText(tooltipText.toString());
697
698       lastTooltip = tooltipText.toString();
699     }
700
701   }
702   /**
703    * appends the features at rpos to the given stringbuffer ready for display in a tooltip
704    * @param tooltipText2
705    * @param linkImageURL
706    * @param rpos
707    * @param features
708    * TODO refactor to Jalview 'utilities' somehow. 
709    */
710   public static void appendFeatures(StringBuffer tooltipText2, String linkImageURL,
711           int rpos, SequenceFeature[] features)
712   {
713     String tmpString;
714     if (features != null)
715     {
716       for (int i = 0; i < features.length; i++)
717       {
718         if (features[i].getType().equals("disulfide bond"))
719         {
720           if (features[i].getBegin() == rpos
721                   || features[i].getEnd() == rpos)
722           {
723             if (tooltipText2.length() > 6)
724             {
725               tooltipText2.append("<br>");
726             }
727             tooltipText2.append("disulfide bond " + features[i].getBegin()
728                     + ":" + features[i].getEnd());
729             if (features[i].links != null)
730             {
731               tooltipText2.append(" <img src=\"" + linkImageURL + "\">");
732             }
733           }
734         }
735         else
736         {
737           if (tooltipText2.length() > 6)
738           {
739             tooltipText2.append("<br>");
740           }
741
742           tooltipText2.append(features[i].getType() + " ");
743           if (rpos!=0)
744           {
745             // we are marking a positional feature
746             tooltipText2.append(features[i].begin);
747           }
748           if (features[i].begin != features[i].end)
749           {
750             tooltipText2.append(" " + features[i].end);
751           }
752
753           if (features[i].getDescription() != null
754                   && !features[i].description.equals(features[i]
755                           .getType()))
756           {
757             tmpString = features[i].getDescription();
758             int startTag = tmpString.toUpperCase().indexOf("<HTML>");
759             if (startTag > -1)
760             {
761               tmpString = tmpString.substring(startTag + 6);
762             }
763             int endTag = tmpString.toUpperCase().indexOf("</BODY>");
764             if (endTag > -1)
765             {
766               tmpString = tmpString.substring(0, endTag);
767             }
768             endTag = tmpString.toUpperCase().indexOf("</HTML>");
769             if (endTag > -1)
770             {
771               tmpString = tmpString.substring(0, endTag);
772             }
773
774             if (startTag > -1)
775             {
776               tooltipText2.append("; " + tmpString);
777             }
778             else
779             {
780               if (tmpString.indexOf("<") > -1
781                       || tmpString.indexOf(">") > -1)
782               {
783                 // The description does not specify html is to
784                 // be used, so we must remove < > symbols
785                 tmpString = tmpString.replaceAll("<", "&lt;");
786                 tmpString = tmpString.replaceAll(">", "&gt;");
787
788                 tooltipText2.append("; ");
789                 tooltipText2.append(tmpString);
790
791               }
792               else
793               {
794                 tooltipText2.append("; " + tmpString);
795               }
796             }
797           }
798           if (features[i].getValue("status") != null)
799           {
800             String status = features[i].getValue("status").toString();
801             if (status.length() > 0)
802             {
803               tooltipText2.append("; (" + features[i].getValue("status")
804                       + ")");
805             }
806           }
807
808           if (features[i].links != null)
809           {
810             tooltipText2.append(" <img src=\"" + linkImageURL + "\">");
811           }
812
813         }
814       }
815     }
816   }
817
818   String lastTooltip;
819
820   /**
821    * Set status message in alignment panel
822    * 
823    * @param sequence
824    *                aligned sequence object
825    * @param res
826    *                alignment column
827    * @param seq
828    *                index of sequence in alignment
829    * @return position of res in sequence
830    */
831   int setStatusMessage(SequenceI sequence, int res, int seq)
832   {
833     int pos = -1;
834     StringBuffer text = new StringBuffer("Sequence " + (seq + 1) + " ID: "
835             + sequence.getName());
836
837     Object obj = null;
838     if (av.alignment.isNucleotide())
839     {
840       obj = ResidueProperties.nucleotideName.get(sequence.getCharAt(res)
841               + "");
842       if (obj != null)
843       {
844         text.append(" Nucleotide: ");
845       }
846     }
847     else
848     {
849       obj = ResidueProperties.aa2Triplet.get(sequence.getCharAt(res) + "");
850       if (obj != null)
851       {
852         text.append("  Residue: ");
853       }
854     }
855
856     if (obj != null)
857     {
858       pos = sequence.findPosition(res);
859       if (obj != "")
860       {
861         text.append(obj + " (" + pos + ")");
862       }
863     }
864     ap.alignFrame.statusBar.setText(text.toString());
865     return pos;
866   }
867
868   /**
869    * DOCUMENT ME!
870    * 
871    * @param evt
872    *                DOCUMENT ME!
873    */
874   public void mouseDragged(MouseEvent evt)
875   {
876     if (mouseWheelPressed)
877     {
878       int oldWidth = av.charWidth;
879
880       // Which is bigger, left-right or up-down?
881       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math.abs(evt
882               .getX()
883               - lastMousePress.getX()))
884       {
885         int fontSize = av.font.getSize();
886
887         if (evt.getY() < lastMousePress.getY())
888         {
889           fontSize--;
890         }
891         else if (evt.getY() > lastMousePress.getY())
892         {
893           fontSize++;
894         }
895
896         if (fontSize < 1)
897         {
898           fontSize = 1;
899         }
900
901         av
902                 .setFont(new Font(av.font.getName(), av.font.getStyle(),
903                         fontSize));
904         av.charWidth = oldWidth;
905         ap.fontChanged();
906       }
907       else
908       {
909         if (evt.getX() < lastMousePress.getX() && av.charWidth > 1)
910         {
911           av.charWidth--;
912         }
913         else if (evt.getX() > lastMousePress.getX())
914         {
915           av.charWidth++;
916         }
917
918         ap.paintAlignment(false);
919       }
920
921       FontMetrics fm = getFontMetrics(av.getFont());
922       av.validCharWidth = fm.charWidth('M') <= av.charWidth;
923
924       lastMousePress = evt.getPoint();
925
926       return;
927     }
928
929     if (!editingSeqs)
930     {
931       doMouseDraggedDefineMode(evt);
932       return;
933     }
934
935     int res = findRes(evt);
936
937     if (res < 0)
938     {
939       res = 0;
940     }
941
942     if ((lastres == -1) || (lastres == res))
943     {
944       return;
945     }
946
947     if ((res < av.getAlignment().getWidth()) && (res < lastres))
948     {
949       // dragLeft, delete gap
950       editSequence(false, res);
951     }
952     else
953     {
954       editSequence(true, res);
955     }
956
957     mouseDragging = true;
958     if (scrollThread != null)
959     {
960       scrollThread.setEvent(evt);
961     }
962   }
963
964   synchronized void editSequence(boolean insertGap, int startres)
965   {
966     int fixedLeft = -1;
967     int fixedRight = -1;
968     boolean fixedColumns = false;
969     SequenceGroup sg = av.getSelectionGroup();
970
971     SequenceI seq = av.alignment.getSequenceAt(startseq);
972
973     // No group, but the sequence may represent a group
974     if (!groupEditing && av.hasHiddenRows)
975     {
976       if (av.hiddenRepSequences != null
977               && av.hiddenRepSequences.containsKey(seq))
978       {
979         sg = (SequenceGroup) av.hiddenRepSequences.get(seq);
980         groupEditing = true;
981       }
982     }
983
984     StringBuffer message = new StringBuffer();
985     if (groupEditing)
986     {
987       message.append("Edit group:");
988       if (editCommand == null)
989       {
990         editCommand = new EditCommand("Edit Group");
991       }
992     }
993     else
994     {
995       message.append("Edit sequence: " + seq.getName());
996       String label = seq.getName();
997       if (label.length() > 10)
998       {
999         label = label.substring(0, 10);
1000       }
1001       if (editCommand == null)
1002       {
1003         editCommand = new EditCommand("Edit " + label);
1004       }
1005     }
1006
1007     if (insertGap)
1008     {
1009       message.append(" insert ");
1010     }
1011     else
1012     {
1013       message.append(" delete ");
1014     }
1015
1016     message.append(Math.abs(startres - lastres) + " gaps.");
1017     ap.alignFrame.statusBar.setText(message.toString());
1018
1019     // Are we editing within a selection group?
1020     if (groupEditing
1021             || (sg != null && sg.getSequences(av.hiddenRepSequences)
1022                     .contains(seq)))
1023     {
1024       fixedColumns = true;
1025
1026       // sg might be null as the user may only see 1 sequence,
1027       // but the sequence represents a group
1028       if (sg == null)
1029       {
1030         if (av.hiddenRepSequences == null
1031                 || !av.hiddenRepSequences.containsKey(seq))
1032         {
1033           endEditing();
1034           return;
1035         }
1036         sg = (SequenceGroup) av.hiddenRepSequences.get(seq);
1037       }
1038
1039       fixedLeft = sg.getStartRes();
1040       fixedRight = sg.getEndRes();
1041
1042       if ((startres < fixedLeft && lastres >= fixedLeft)
1043               || (startres >= fixedLeft && lastres < fixedLeft)
1044               || (startres > fixedRight && lastres <= fixedRight)
1045               || (startres <= fixedRight && lastres > fixedRight))
1046       {
1047         endEditing();
1048         return;
1049       }
1050
1051       if (fixedLeft > startres)
1052       {
1053         fixedRight = fixedLeft - 1;
1054         fixedLeft = 0;
1055       }
1056       else if (fixedRight < startres)
1057       {
1058         fixedLeft = fixedRight;
1059         fixedRight = -1;
1060       }
1061     }
1062
1063     if (av.hasHiddenColumns)
1064     {
1065       fixedColumns = true;
1066       int y1 = av.getColumnSelection().getHiddenBoundaryLeft(startres);
1067       int y2 = av.getColumnSelection().getHiddenBoundaryRight(startres);
1068
1069       if ((insertGap && startres > y1 && lastres < y1)
1070               || (!insertGap && startres < y2 && lastres > y2))
1071       {
1072         endEditing();
1073         return;
1074       }
1075
1076       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1077       // Selection spans a hidden region
1078       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1079       {
1080         if (startres >= y2)
1081         {
1082           fixedLeft = y2;
1083         }
1084         else
1085         {
1086           fixedRight = y2 - 1;
1087         }
1088       }
1089     }
1090
1091     if (groupEditing)
1092     {
1093       Vector vseqs = sg.getSequences(av.hiddenRepSequences);
1094       int g, groupSize = vseqs.size();
1095       SequenceI[] groupSeqs = new SequenceI[groupSize];
1096       for (g = 0; g < groupSeqs.length; g++)
1097       {
1098         groupSeqs[g] = (SequenceI) vseqs.elementAt(g);
1099       }
1100
1101       // drag to right
1102       if (insertGap)
1103       {
1104         // If the user has selected the whole sequence, and is dragging to
1105         // the right, we can still extend the alignment and selectionGroup
1106         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1107                 && sg.getEndRes() == av.alignment.getWidth() - 1)
1108         {
1109           sg.setEndRes(av.alignment.getWidth() + startres - lastres);
1110           fixedRight = sg.getEndRes();
1111         }
1112
1113         // Is it valid with fixed columns??
1114         // Find the next gap before the end
1115         // of the visible region boundary
1116         boolean blank = false;
1117         for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
1118         {
1119           blank = true;
1120
1121           for (g = 0; g < groupSize; g++)
1122           {
1123             for (int j = 0; j < startres - lastres; j++)
1124             {
1125               if (!jalview.util.Comparison.isGap(groupSeqs[g]
1126                       .getCharAt(fixedRight - j)))
1127               {
1128                 blank = false;
1129                 break;
1130               }
1131             }
1132           }
1133           if (blank)
1134           {
1135             break;
1136           }
1137         }
1138
1139         if (!blank)
1140         {
1141           if (sg.getSize() == av.alignment.getHeight())
1142           {
1143             if ((av.hasHiddenColumns && startres < av.getColumnSelection()
1144                     .getHiddenBoundaryRight(startres)))
1145             {
1146               endEditing();
1147               return;
1148             }
1149
1150             int alWidth = av.alignment.getWidth();
1151             if (av.hasHiddenRows)
1152             {
1153               int hwidth = av.alignment.getHiddenSequences().getWidth();
1154               if (hwidth > alWidth)
1155               {
1156                 alWidth = hwidth;
1157               }
1158             }
1159             // We can still insert gaps if the selectionGroup
1160             // contains all the sequences
1161             sg.setEndRes(sg.getEndRes() + startres - lastres);
1162             fixedRight = alWidth + startres - lastres;
1163           }
1164           else
1165           {
1166             endEditing();
1167             return;
1168           }
1169         }
1170       }
1171
1172       // drag to left
1173       else if (!insertGap)
1174       {
1175         // / Are we able to delete?
1176         // ie are all columns blank?
1177
1178         for (g = 0; g < groupSize; g++)
1179         {
1180           for (int j = startres; j < lastres; j++)
1181           {
1182             if (groupSeqs[g].getLength() <= j)
1183             {
1184               continue;
1185             }
1186
1187             if (!jalview.util.Comparison.isGap(groupSeqs[g].getCharAt(j)))
1188             {
1189               // Not a gap, block edit not valid
1190               endEditing();
1191               return;
1192             }
1193           }
1194         }
1195       }
1196
1197       if (insertGap)
1198       {
1199         // dragging to the right
1200         if (fixedColumns && fixedRight != -1)
1201         {
1202           for (int j = lastres; j < startres; j++)
1203           {
1204             insertChar(j, groupSeqs, fixedRight);
1205           }
1206         }
1207         else
1208         {
1209           editCommand.appendEdit(EditCommand.INSERT_GAP, groupSeqs,
1210                   startres, startres - lastres, av.alignment, true);
1211         }
1212       }
1213       else
1214       {
1215         // dragging to the left
1216         if (fixedColumns && fixedRight != -1)
1217         {
1218           for (int j = lastres; j > startres; j--)
1219           {
1220             deleteChar(startres, groupSeqs, fixedRight);
1221           }
1222         }
1223         else
1224         {
1225           editCommand.appendEdit(EditCommand.DELETE_GAP, groupSeqs,
1226                   startres, lastres - startres, av.alignment, true);
1227         }
1228
1229       }
1230     }
1231     else
1232     // ///Editing a single sequence///////////
1233     {
1234       if (insertGap)
1235       {
1236         // dragging to the right
1237         if (fixedColumns && fixedRight != -1)
1238         {
1239           for (int j = lastres; j < startres; j++)
1240           {
1241             insertChar(j, new SequenceI[]
1242             { seq }, fixedRight);
1243           }
1244         }
1245         else
1246         {
1247           editCommand.appendEdit(EditCommand.INSERT_GAP, new SequenceI[]
1248           { seq }, lastres, startres - lastres, av.alignment, true);
1249         }
1250       }
1251       else
1252       {
1253         // dragging to the left
1254         if (fixedColumns && fixedRight != -1)
1255         {
1256           for (int j = lastres; j > startres; j--)
1257           {
1258             if (!jalview.util.Comparison.isGap(seq.getCharAt(startres)))
1259             {
1260               endEditing();
1261               break;
1262             }
1263             deleteChar(startres, new SequenceI[]
1264             { seq }, fixedRight);
1265           }
1266         }
1267         else
1268         {
1269           // could be a keyboard edit trying to delete none gaps
1270           int max = 0;
1271           for (int m = startres; m < lastres; m++)
1272           {
1273             if (!jalview.util.Comparison.isGap(seq.getCharAt(m)))
1274             {
1275               break;
1276             }
1277             max++;
1278           }
1279
1280           if (max > 0)
1281           {
1282             editCommand.appendEdit(EditCommand.DELETE_GAP, new SequenceI[]
1283             { seq }, startres, max, av.alignment, true);
1284           }
1285         }
1286       }
1287     }
1288
1289     lastres = startres;
1290     seqCanvas.repaint();
1291   }
1292
1293   void insertChar(int j, SequenceI[] seq, int fixedColumn)
1294   {
1295     int blankColumn = fixedColumn;
1296     for (int s = 0; s < seq.length; s++)
1297     {
1298       // Find the next gap before the end of the visible region boundary
1299       // If lastCol > j, theres a boundary after the gap insertion
1300
1301       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1302       {
1303         if (jalview.util.Comparison.isGap(seq[s].getCharAt(blankColumn)))
1304         {
1305           // Theres a space, so break and insert the gap
1306           break;
1307         }
1308       }
1309
1310       if (blankColumn <= j)
1311       {
1312         blankColumn = fixedColumn;
1313         endEditing();
1314         return;
1315       }
1316     }
1317
1318     editCommand.appendEdit(EditCommand.DELETE_GAP, seq, blankColumn, 1,
1319             av.alignment, true);
1320
1321     editCommand.appendEdit(EditCommand.INSERT_GAP, seq, j, 1, av.alignment,
1322             true);
1323
1324   }
1325
1326   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
1327   {
1328
1329     editCommand.appendEdit(EditCommand.DELETE_GAP, seq, j, 1, av.alignment,
1330             true);
1331
1332     editCommand.appendEdit(EditCommand.INSERT_GAP, seq, fixedColumn, 1,
1333             av.alignment, true);
1334   }
1335
1336   /**
1337    * DOCUMENT ME!
1338    * 
1339    * @param e
1340    *                DOCUMENT ME!
1341    */
1342   public void mouseEntered(MouseEvent e)
1343   {
1344     if (oldSeq < 0)
1345     {
1346       oldSeq = 0;
1347     }
1348
1349     if (scrollThread != null)
1350     {
1351       scrollThread.running = false;
1352       scrollThread = null;
1353     }
1354   }
1355
1356   /**
1357    * DOCUMENT ME!
1358    * 
1359    * @param e
1360    *                DOCUMENT ME!
1361    */
1362   public void mouseExited(MouseEvent e)
1363   {
1364     if (av.getWrapAlignment())
1365     {
1366       return;
1367     }
1368
1369     if (mouseDragging)
1370     {
1371       scrollThread = new ScrollThread();
1372     }
1373   }
1374
1375   public void mouseClicked(MouseEvent evt)
1376   {
1377     SequenceI sequence = av.alignment.getSequenceAt(findSeq(evt));
1378     if (evt.getClickCount() > 1)
1379     {
1380       if (av.getSelectionGroup().getSize() == 1
1381               && av.getSelectionGroup().getEndRes()
1382                       - av.getSelectionGroup().getStartRes() < 2)
1383       {
1384         av.setSelectionGroup(null);
1385       }
1386
1387       SequenceFeature[] features = findFeaturesAtRes(sequence
1388               .getDatasetSequence(), sequence.findPosition(findRes(evt)));
1389
1390       if (features != null && features.length > 0)
1391       {
1392         SearchResults highlight = new SearchResults();
1393         highlight.addResult(sequence, features[0].getBegin(), features[0]
1394                 .getEnd());
1395         seqCanvas.highlightSearchResults(highlight);
1396       }
1397       if (features != null && features.length > 0)
1398       {
1399         seqCanvas.getFeatureRenderer().amendFeatures(new SequenceI[]
1400         { sequence }, features, false, ap);
1401
1402         seqCanvas.highlightSearchResults(null);
1403       }
1404     }
1405   }
1406
1407   public void mouseWheelMoved(MouseWheelEvent e)
1408   {
1409     e.consume();
1410     if (e.getWheelRotation() > 0)
1411     {
1412       ap.scrollUp(false);
1413     }
1414     else
1415     {
1416       ap.scrollUp(true);
1417     }
1418   }
1419
1420   /**
1421    * DOCUMENT ME!
1422    * 
1423    * @param evt
1424    *                DOCUMENT ME!
1425    */
1426   public void doMousePressedDefineMode(MouseEvent evt)
1427   {
1428     int res = findRes(evt);
1429     int seq = findSeq(evt);
1430     oldSeq = seq;
1431
1432     startWrapBlock = wrappedBlock;
1433
1434     if (av.wrapAlignment && seq > av.alignment.getHeight())
1435     {
1436       JOptionPane.showInternalMessageDialog(Desktop.desktop,
1437               "Cannot edit annotations in wrapped view.",
1438               "Wrapped view - no edit", JOptionPane.WARNING_MESSAGE);
1439       return;
1440     }
1441
1442     if (seq < 0 || res < 0)
1443     {
1444       return;
1445     }
1446
1447     SequenceI sequence = (Sequence) av.getAlignment().getSequenceAt(seq);
1448
1449     if ((sequence == null) || (res > sequence.getLength()))
1450     {
1451       return;
1452     }
1453
1454     stretchGroup = av.getSelectionGroup();
1455
1456     if (stretchGroup == null)
1457     {
1458       stretchGroup = av.alignment.findGroup(sequence);
1459
1460       if ((stretchGroup != null) && (res > stretchGroup.getStartRes())
1461               && (res < stretchGroup.getEndRes()))
1462       {
1463         av.setSelectionGroup(stretchGroup);
1464       }
1465       else
1466       {
1467         stretchGroup = null;
1468       }
1469     }
1470     else if (!stretchGroup.getSequences(null).contains(sequence)
1471             || (stretchGroup.getStartRes() > res)
1472             || (stretchGroup.getEndRes() < res))
1473     {
1474       stretchGroup = null;
1475
1476       SequenceGroup[] allGroups = av.alignment.findAllGroups(sequence);
1477
1478       if (allGroups != null)
1479       {
1480         for (int i = 0; i < allGroups.length; i++)
1481         {
1482           if ((allGroups[i].getStartRes() <= res)
1483                   && (allGroups[i].getEndRes() >= res))
1484           {
1485             stretchGroup = allGroups[i];
1486             break;
1487           }
1488         }
1489       }
1490
1491       av.setSelectionGroup(stretchGroup);
1492
1493     }
1494
1495     if (javax.swing.SwingUtilities.isRightMouseButton(evt))
1496     {
1497       SequenceFeature[] allFeatures = findFeaturesAtRes(sequence
1498               .getDatasetSequence(), sequence.findPosition(res));
1499       Vector links = new Vector();
1500       for (int i = 0; i < allFeatures.length; i++)
1501       {
1502         if (allFeatures[i].links != null)
1503         {
1504           for (int j = 0; j < allFeatures[i].links.size(); j++)
1505           {
1506             links.addElement(allFeatures[i].links.elementAt(j));
1507           }
1508         }
1509       }
1510
1511       jalview.gui.PopupMenu pop = new jalview.gui.PopupMenu(ap, null, links);
1512       pop.show(this, evt.getX(), evt.getY());
1513       return;
1514     }
1515
1516     if (av.cursorMode)
1517     {
1518       seqCanvas.cursorX = findRes(evt);
1519       seqCanvas.cursorY = findSeq(evt);
1520       seqCanvas.repaint();
1521       return;
1522     }
1523
1524     if (stretchGroup == null)
1525     {
1526       // Only if left mouse button do we want to change group sizes
1527
1528       // define a new group here
1529       SequenceGroup sg = new SequenceGroup();
1530       sg.setStartRes(res);
1531       sg.setEndRes(res);
1532       sg.addSequence(sequence, false);
1533       av.setSelectionGroup(sg);
1534       stretchGroup = sg;
1535
1536       if (av.getConservationSelected())
1537       {
1538         SliderPanel.setConservationSlider(ap, av.getGlobalColourScheme(),
1539                 "Background");
1540       }
1541
1542       if (av.getAbovePIDThreshold())
1543       {
1544         SliderPanel.setPIDSliderSource(ap, av.getGlobalColourScheme(),
1545                 "Background");
1546       }
1547       if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
1548       {
1549         // Edit end res position of selected group
1550         changeEndRes = true;
1551       }
1552       else if ((stretchGroup != null)
1553               && (stretchGroup.getStartRes() == res))
1554       {
1555         // Edit end res position of selected group
1556         changeStartRes = true;
1557       }
1558       stretchGroup.getWidth();
1559     }
1560
1561     seqCanvas.repaint();
1562   }
1563
1564   /**
1565    * DOCUMENT ME!
1566    * 
1567    * @param evt
1568    *                DOCUMENT ME!
1569    */
1570   public void doMouseReleasedDefineMode(MouseEvent evt)
1571   {
1572     if (stretchGroup == null)
1573     {
1574       return;
1575     }
1576
1577     if (stretchGroup.cs != null)
1578     {
1579       if (stretchGroup.cs instanceof ClustalxColourScheme)
1580       {
1581         ((ClustalxColourScheme) stretchGroup.cs).resetClustalX(stretchGroup
1582                 .getSequences(av.hiddenRepSequences), stretchGroup
1583                 .getWidth());
1584       }
1585
1586       if (stretchGroup.cs instanceof Blosum62ColourScheme
1587               || stretchGroup.cs instanceof PIDColourScheme
1588               || stretchGroup.cs.conservationApplied()
1589               || stretchGroup.cs.getThreshold() > 0)
1590       {
1591         stretchGroup.recalcConservation();
1592       }
1593
1594       if (stretchGroup.cs.conservationApplied())
1595       {
1596         SliderPanel.setConservationSlider(ap, stretchGroup.cs, stretchGroup
1597                 .getName());
1598       }
1599       else
1600       {
1601         SliderPanel.setPIDSliderSource(ap, stretchGroup.cs, stretchGroup
1602                 .getName());
1603       }
1604       PaintRefresher.Refresh(this, av.getSequenceSetId());
1605       ap.paintAlignment(true);
1606     }
1607
1608     changeEndRes = false;
1609     changeStartRes = false;
1610     stretchGroup = null;
1611
1612   }
1613
1614   /**
1615    * DOCUMENT ME!
1616    * 
1617    * @param evt
1618    *                DOCUMENT ME!
1619    */
1620   public void doMouseDraggedDefineMode(MouseEvent evt)
1621   {
1622     int res = findRes(evt);
1623     int y = findSeq(evt);
1624
1625     if (wrappedBlock != startWrapBlock)
1626     {
1627       return;
1628     }
1629
1630     if (stretchGroup == null)
1631     {
1632       return;
1633     }
1634
1635     if (res >= av.alignment.getWidth())
1636     {
1637       res = av.alignment.getWidth() - 1;
1638     }
1639
1640     if (stretchGroup.getEndRes() == res)
1641     {
1642       // Edit end res position of selected group
1643       changeEndRes = true;
1644     }
1645     else if (stretchGroup.getStartRes() == res)
1646     {
1647       // Edit start res position of selected group
1648       changeStartRes = true;
1649     }
1650
1651     if (res < av.getStartRes())
1652     {
1653       res = av.getStartRes();
1654     }
1655
1656     if (changeEndRes)
1657     {
1658       if (res > (stretchGroup.getStartRes() - 1))
1659       {
1660         stretchGroup.setEndRes(res);
1661       }
1662     }
1663     else if (changeStartRes)
1664     {
1665       if (res < (stretchGroup.getEndRes() + 1))
1666       {
1667         stretchGroup.setStartRes(res);
1668       }
1669     }
1670
1671     int dragDirection = 0;
1672
1673     if (y > oldSeq)
1674     {
1675       dragDirection = 1;
1676     }
1677     else if (y < oldSeq)
1678     {
1679       dragDirection = -1;
1680     }
1681
1682     while ((y != oldSeq) && (oldSeq > -1) && (y < av.alignment.getHeight()))
1683     {
1684       // This routine ensures we don't skip any sequences, as the
1685       // selection is quite slow.
1686       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1687
1688       oldSeq += dragDirection;
1689
1690       if (oldSeq < 0)
1691       {
1692         break;
1693       }
1694
1695       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1696
1697       if (stretchGroup.getSequences(null).contains(nextSeq))
1698       {
1699         stretchGroup.deleteSequence(seq, false);
1700       }
1701       else
1702       {
1703         if (seq != null)
1704         {
1705           stretchGroup.addSequence(seq, false);
1706         }
1707
1708         stretchGroup.addSequence(nextSeq, false);
1709       }
1710     }
1711
1712     if (oldSeq < 0)
1713     {
1714       oldSeq = -1;
1715     }
1716
1717     mouseDragging = true;
1718
1719     if (scrollThread != null)
1720     {
1721       scrollThread.setEvent(evt);
1722     }
1723
1724     seqCanvas.repaint();
1725   }
1726
1727   void scrollCanvas(MouseEvent evt)
1728   {
1729     if (evt == null)
1730     {
1731       if (scrollThread != null)
1732       {
1733         scrollThread.running = false;
1734         scrollThread = null;
1735       }
1736       mouseDragging = false;
1737     }
1738     else
1739     {
1740       if (scrollThread == null)
1741       {
1742         scrollThread = new ScrollThread();
1743       }
1744
1745       mouseDragging = true;
1746       scrollThread.setEvent(evt);
1747     }
1748
1749   }
1750
1751   // this class allows scrolling off the bottom of the visible alignment
1752   class ScrollThread extends Thread
1753   {
1754     MouseEvent evt;
1755
1756     boolean running = false;
1757
1758     public ScrollThread()
1759     {
1760       start();
1761     }
1762
1763     public void setEvent(MouseEvent e)
1764     {
1765       evt = e;
1766     }
1767
1768     public void stopScrolling()
1769     {
1770       running = false;
1771     }
1772
1773     public void run()
1774     {
1775       running = true;
1776
1777       while (running)
1778       {
1779         if (evt != null)
1780         {
1781           if (mouseDragging && (evt.getY() < 0) && (av.getStartSeq() > 0))
1782           {
1783             running = ap.scrollUp(true);
1784           }
1785
1786           if (mouseDragging && (evt.getY() >= getHeight())
1787                   && (av.alignment.getHeight() > av.getEndSeq()))
1788           {
1789             running = ap.scrollUp(false);
1790           }
1791
1792           if (mouseDragging && (evt.getX() < 0))
1793           {
1794             running = ap.scrollRight(false);
1795           }
1796           else if (mouseDragging && (evt.getX() >= getWidth()))
1797           {
1798             running = ap.scrollRight(true);
1799           }
1800         }
1801
1802         try
1803         {
1804           Thread.sleep(20);
1805         } catch (Exception ex)
1806         {
1807         }
1808       }
1809     }
1810   }
1811 }