fix wrap alignment vscroll visible amount
[jalview.git] / src / jalview / gui / AlignmentPanel.java
1 package jalview.gui;\r
2 \r
3 import jalview.jbgui.GAlignmentPanel;\r
4 import jalview.schemes.*;\r
5 import jalview.analysis.*;\r
6 import jalview.datamodel.*;\r
7 import java.awt.*;\r
8 import java.awt.event.*;\r
9 import java.awt.print.*;\r
10 import java.io.*;\r
11 import java.awt.image.*;\r
12 import org.jibble.epsgraphics.*;\r
13 import javax.imageio.*;\r
14 \r
15 \r
16 \r
17 public class AlignmentPanel extends GAlignmentPanel implements AdjustmentListener, Printable\r
18 {\r
19 \r
20   AlignViewport     av;\r
21   OverviewPanel overviewPanel;\r
22   SeqPanel   seqPanel;\r
23   IdPanel    idPanel;\r
24   IdwidthAdjuster idwidthAdjuster;\r
25   public AlignFrame alignFrame;\r
26   ScalePanel scalePanel;\r
27   AnnotationPanel annotationPanel;\r
28   AnnotationLabels alabels;\r
29 \r
30   // this value is set false when selection area being dragged\r
31   boolean fastPaint = true;\r
32 \r
33   public AlignmentPanel(AlignFrame af, final AlignViewport av)\r
34   {\r
35     alignFrame = af;\r
36     this.av         = av;\r
37     seqPanel        = new SeqPanel  (av, this);\r
38     idPanel         = new IdPanel   (av, this);\r
39 \r
40     scalePanel = new ScalePanel(av, this);\r
41 \r
42     idPanelHolder.add(idPanel, BorderLayout.CENTER);\r
43     idwidthAdjuster = new IdwidthAdjuster(this);\r
44     idSpaceFillerPanel1.add(idwidthAdjuster, BorderLayout.CENTER);\r
45 \r
46     annotationPanel = new AnnotationPanel(this);\r
47     alabels = new AnnotationLabels(this);\r
48 \r
49     annotationSpaceFillerHolder.setPreferredSize(annotationPanel.getPreferredSize());\r
50     annotationScroller.setPreferredSize(annotationPanel.getPreferredSize());\r
51     annotationScroller.setViewportView(annotationPanel);\r
52     annotationSpaceFillerHolder.add(alabels, BorderLayout.CENTER);\r
53 \r
54     Dimension d = calculateIdWidth();\r
55     d.setSize( d.width+4, d.height);\r
56     idPanel.idCanvas.setPreferredSize( d );\r
57     hscrollFillerPanel.setPreferredSize( d );\r
58 \r
59     scalePanelHolder.add(scalePanel, BorderLayout.CENTER);\r
60     seqPanelHolder.add(seqPanel, BorderLayout.CENTER);\r
61 \r
62     setScrollValues(0, 0);\r
63 \r
64     hscroll.addAdjustmentListener(this);\r
65     vscroll.addAdjustmentListener(this);\r
66 \r
67     addComponentListener(new ComponentAdapter()\r
68    {\r
69      public void componentResized(ComponentEvent evt)\r
70      {\r
71           repaint();\r
72      }\r
73    });\r
74 \r
75    setFocusable(true);\r
76    addKeyListener(new KeyAdapter()\r
77    {\r
78      public void keyPressed(KeyEvent evt)\r
79      {\r
80        switch(evt.getKeyCode())\r
81        {\r
82          case  27: // escape key\r
83            av.setSelectionGroup(null);\r
84            repaint();\r
85            break;\r
86          case KeyEvent.VK_X:\r
87            if(evt.isControlDown())\r
88            alignFrame.cut_actionPerformed(null);\r
89            break;\r
90          case KeyEvent.VK_C:\r
91          if(evt.isControlDown())\r
92            alignFrame.copy_actionPerformed(null);\r
93            break;\r
94          case KeyEvent.VK_V:\r
95           if(evt.isControlDown())\r
96            alignFrame.paste(true);\r
97            break;\r
98          case KeyEvent.VK_A:\r
99          if(evt.isControlDown())\r
100            alignFrame.selectAllSequenceMenuItem_actionPerformed(null);\r
101            break;\r
102         case KeyEvent.VK_DOWN:\r
103           alignFrame.moveSelectedSequences(false);\r
104           break;\r
105         case KeyEvent.VK_UP:\r
106           alignFrame.moveSelectedSequences(true);\r
107           break;\r
108         case KeyEvent.VK_F:\r
109          if(evt.isControlDown())\r
110           alignFrame.findMenuItem_actionPerformed(null);\r
111           break;\r
112        }\r
113      }\r
114    });\r
115   }\r
116 \r
117   Dimension calculateIdWidth()\r
118   {\r
119     Graphics g = this.getGraphics();\r
120     if(g==null)\r
121     {\r
122       javax.swing.JFrame f = new javax.swing.JFrame();\r
123       f.addNotify();\r
124       g = f.getGraphics();\r
125     }\r
126 \r
127     FontMetrics fm = g.getFontMetrics(av.font);\r
128     AlignmentI al = av.getAlignment();\r
129 \r
130        int i   = 0;\r
131        int idWidth = 0;\r
132        String id;\r
133        while (i < al.getHeight() && al.getSequenceAt(i) != null)\r
134        {\r
135          SequenceI s   = al.getSequenceAt(i);\r
136          if(av.getShowFullId())\r
137            id   = s.getDisplayId();\r
138          else\r
139            id = s.getName();\r
140 \r
141          if (fm.stringWidth(id) > idWidth)\r
142            idWidth = fm.stringWidth(id);\r
143          i++;\r
144        }\r
145 \r
146        // Also check annotation label widths\r
147        i=0;\r
148        if(al.getAlignmentAnnotation()!=null)\r
149        {\r
150          fm = g.getFontMetrics(alabels.getFont());\r
151          while (i < al.getAlignmentAnnotation().length)\r
152          {\r
153            String label = al.getAlignmentAnnotation()[i].label;\r
154            if (fm.stringWidth(label) > idWidth)\r
155              idWidth = fm.stringWidth(label);\r
156            i++;\r
157          }\r
158        }\r
159 \r
160        return new Dimension(idWidth, 12);\r
161   }\r
162 \r
163 \r
164  public void highlightSearchResults(int [] results)\r
165  {\r
166    seqPanel.seqCanvas.highlightSearchResults( results );\r
167 \r
168    // do we need to scroll the panel?\r
169    if(results!=null && (av.getStartSeq()>results[0]\r
170                         || av.getEndSeq()<results[0]\r
171                         || av.getStartRes()>results[1]\r
172                         || av.getEndRes()<results[2]))\r
173        setScrollValues(results[1], results[0]);\r
174 \r
175 \r
176  }\r
177 \r
178 \r
179  public OverviewPanel getOverviewPanel()\r
180  {\r
181    return overviewPanel;\r
182  }\r
183 \r
184  public void setOverviewPanel(OverviewPanel op)\r
185  {\r
186    overviewPanel = op;\r
187  }\r
188 \r
189 \r
190   public void setAnnotationVisible(boolean b)\r
191   {\r
192     annotationSpaceFillerHolder.setVisible(b);\r
193     annotationScroller.setVisible(b);\r
194   }\r
195 \r
196 \r
197   public void setWrapAlignment(boolean wrap)\r
198   {\r
199     scalePanelHolder.setVisible(!wrap);\r
200     hscroll.setVisible(!wrap);\r
201     idwidthAdjuster.setVisible(!wrap);\r
202 \r
203     av.setShowAnnotation(!wrap);\r
204     annotationScroller.setVisible(!wrap);\r
205     annotationSpaceFillerHolder.setVisible(!wrap);\r
206     idSpaceFillerPanel1.setVisible(!wrap);\r
207 \r
208     repaint();\r
209 \r
210   }\r
211 \r
212 \r
213   public void setColourScheme()\r
214   {\r
215     ColourSchemeI cs = av.getGlobalColourScheme();\r
216 \r
217     if(av.getConservationSelected())\r
218     {\r
219 \r
220        Alignment al = (Alignment)av.getAlignment();\r
221        Conservation c = new Conservation("All",\r
222                             ResidueProperties.propHash, 3, al.getSequences(), 0,\r
223                             al.getWidth() );\r
224 \r
225        c.calculate();\r
226        c.verdict(false, av.ConsPercGaps);\r
227        ConservationColourScheme ccs = new ConservationColourScheme(c, cs);\r
228 \r
229        av.setGlobalColourScheme( ccs );\r
230 \r
231     }\r
232 \r
233     repaint();\r
234   }\r
235 \r
236 \r
237   int hextent = 0;\r
238   int vextent = 0;\r
239 \r
240   // return value is true if the scroll is valid\r
241   public boolean scrollUp(boolean up)\r
242   {\r
243     if(up)\r
244     {\r
245       if(vscroll.getValue()<1)\r
246         return false;\r
247       fastPaint  = false;\r
248       vscroll.setValue(vscroll.getValue() - 1);\r
249     }\r
250     else\r
251     {\r
252      if(vextent+vscroll.getValue() >= av.getAlignment().getHeight())\r
253        return false;\r
254       fastPaint  = false;\r
255       vscroll.setValue(vscroll.getValue() + 1);\r
256     }\r
257     fastPaint = true;\r
258     return true;\r
259   }\r
260 \r
261   public boolean scrollRight(boolean right)\r
262   {\r
263 \r
264     if (right)\r
265     {\r
266       if (hscroll.getValue() < 1)\r
267         return false;\r
268       fastPaint = false;\r
269       hscroll.setValue(hscroll.getValue() - 1);\r
270     }\r
271     else\r
272     {\r
273       if (hextent + hscroll.getValue() >= av.getAlignment().getWidth())\r
274         return false;\r
275       fastPaint = false;\r
276       hscroll.setValue(hscroll.getValue() + 1);\r
277     }\r
278     fastPaint = true;\r
279     return true;\r
280   }\r
281 \r
282 \r
283   public void setScrollValues(int x, int y)\r
284   {\r
285     hextent = seqPanel.seqCanvas.getWidth()/av.charWidth;\r
286     vextent = seqPanel.seqCanvas.getHeight()/av.charHeight;\r
287 \r
288     if(hextent > av.alignment.getWidth())\r
289       hextent = av.alignment.getWidth();\r
290     if(vextent > av.alignment.getHeight())\r
291       vextent = av.alignment.getHeight();\r
292 \r
293     if(hextent+x  >  av.getAlignment().getWidth())\r
294       x =  av.getAlignment().getWidth()- hextent;\r
295 \r
296     if(vextent+y > av.getAlignment().getHeight())\r
297       y = av.getAlignment().getHeight() - vextent;\r
298 \r
299     if(y<0)\r
300       y = 0;\r
301 \r
302     if(x<0)\r
303       x=0;\r
304 \r
305     hscroll.setValues(x,hextent,0,av.getAlignment().getWidth());\r
306     vscroll.setValues(y,vextent,0,av.getAlignment().getHeight() );\r
307 \r
308   }\r
309 \r
310 \r
311   public void adjustmentValueChanged(AdjustmentEvent evt)\r
312   {\r
313     int oldX = av.getStartRes();\r
314     int oldY = av.getStartSeq();\r
315 \r
316     if (evt.getSource() == hscroll)\r
317     {\r
318       int x = hscroll.getValue();\r
319       av.setStartRes(x);\r
320       av.setEndRes(x + seqPanel.seqCanvas.getWidth()/av.getCharWidth()-1);\r
321     }\r
322 \r
323     if (evt.getSource() == vscroll)\r
324     {\r
325       int offy = vscroll.getValue();\r
326       if (av.getWrapAlignment())\r
327       {\r
328         int rowSize = seqPanel.seqCanvas.getWrappedCanvasWidth(seqPanel.seqCanvas.getWidth());\r
329         av.setStartRes( vscroll.getValue() * rowSize );\r
330         av.setEndRes( (vscroll.getValue()+1) * rowSize );\r
331       }\r
332       else\r
333       {\r
334         av.setStartSeq(offy);\r
335         av.setEndSeq(offy + seqPanel.seqCanvas.getHeight() / av.getCharHeight());\r
336       }\r
337     }\r
338 \r
339 \r
340     if(overviewPanel!=null)\r
341       overviewPanel.setBoxPosition();\r
342 \r
343     if(av.getWrapAlignment() || !fastPaint)\r
344       repaint();\r
345     else\r
346     {\r
347       seqPanel.seqCanvas.fastPaint(av.getStartRes() - oldX,\r
348                                    av.getStartSeq() - oldY);\r
349       idPanel.idCanvas.fastPaint(av.getStartSeq() - oldY);\r
350       scalePanel.repaint();\r
351       if (av.getShowAnnotation())\r
352         annotationPanel.fastPaint(av.getStartRes() - oldX);\r
353     }\r
354 \r
355   }\r
356 \r
357   public void paintComponent(Graphics g)\r
358   {\r
359     invalidate();\r
360 \r
361     Dimension d = idPanel.idCanvas.getPreferredSize();\r
362     idPanelHolder.setPreferredSize(d);\r
363     hscrollFillerPanel.setPreferredSize(new Dimension(d.width, 12));\r
364 \r
365     if (av.getWrapAlignment())\r
366     {\r
367       int max = av.alignment.getWidth() / seqPanel.seqCanvas.getWrappedCanvasWidth(seqPanel.seqCanvas.getWidth());\r
368       vscroll.setMaximum(max);\r
369       vscroll.setUnitIncrement(1);\r
370       vscroll.setVisibleAmount(1);\r
371     }\r
372     else\r
373     {\r
374       if (overviewPanel != null)\r
375         overviewPanel.updateOverviewImage();\r
376       setScrollValues(av.getStartRes(), av.getStartSeq());\r
377     }\r
378 \r
379     validate();\r
380 \r
381   }\r
382 \r
383   public int print(Graphics pg, PageFormat pf, int pi) throws PrinterException\r
384   {\r
385     pg.translate((int)pf.getImageableX(), (int)pf.getImageableY());\r
386 \r
387     int pwidth = (int) pf.getImageableWidth();\r
388     int pheight = (int) pf.getImageableHeight();\r
389 \r
390     if (av.getWrapAlignment())\r
391       return printWrappedAlignment(pg, pwidth,pheight, pi);\r
392     else\r
393       return printUnwrapped(pg,pwidth, pheight,pi);\r
394   }\r
395 \r
396   public int printUnwrapped(Graphics pg, int pwidth, int pheight, int pi) throws PrinterException\r
397   {\r
398 \r
399     int idWidth = calculateIdWidth().width + 4;\r
400 \r
401 \r
402     pg.setColor(Color.white);\r
403     pg.fillRect(0,0,pwidth, pheight);\r
404     pg.setFont( av.getFont() );\r
405 \r
406     ////////////////////////////////////\r
407     /// How many sequences and residues can we fit on a printable page?\r
408     int totalRes = (pwidth - idWidth)/av.getCharWidth();\r
409     int totalSeq = (int)((pheight - 30)/av.getCharHeight())-1;\r
410     int pagesWide = av.getAlignment().getWidth() / totalRes +1;\r
411     int pagesHigh = av.getAlignment().getHeight() / totalSeq +1;\r
412 \r
413     if (pi >= pagesWide*pagesHigh)\r
414      return Printable.NO_SUCH_PAGE;\r
415 \r
416     /////////////////////////////\r
417     /// Only print these sequences and residues on this page\r
418     int startRes, endRes, startSeq, endSeq;\r
419     startRes = (pi % pagesWide) * totalRes;\r
420     endRes = startRes + totalRes-1;\r
421     if(endRes>av.getAlignment().getWidth())\r
422       endRes = av.getAlignment().getWidth();\r
423 \r
424      startSeq = (pi / pagesWide) * totalSeq;\r
425      endSeq = startSeq + totalSeq;\r
426      if(endSeq > av.getAlignment().getHeight())\r
427        endSeq = av.getAlignment().getHeight();\r
428 \r
429 \r
430     ////////////////\r
431     //draw Scale\r
432     pg.translate(idWidth,0);\r
433     scalePanel.drawScale(pg, startRes, endRes, pwidth-idWidth);\r
434 \r
435     pg.translate(-idWidth, 30);\r
436     ////////////////\r
437     // Draw the ids\r
438     Color currentColor=null;\r
439     Color currentTextColor=null;\r
440     for(int i=startSeq; i<endSeq; i++)\r
441     {\r
442       if (av.getSelectionGroup()!=null && av.getSelectionGroup().sequences.contains(av.getAlignment().getSequenceAt(i)))\r
443       {\r
444         currentColor = Color.gray;\r
445         currentTextColor = Color.black;\r
446       }\r
447       else\r
448       {\r
449         currentColor = av.getAlignment().getSequenceAt(i).getColor();\r
450         currentTextColor = Color.black;\r
451       }\r
452 \r
453       pg.setColor(currentColor);\r
454       pg.fillRect(0,  jalview.analysis.AlignmentUtil.getPixelHeight(startSeq, i, av.getCharHeight()),\r
455                               idWidth,\r
456                               av.getCharHeight());\r
457 \r
458       pg.setColor(currentTextColor);\r
459 \r
460       String string = av.getAlignment().getSequenceAt(i).getName();\r
461       if(av.getShowFullId())\r
462         string = av.getAlignment().getSequenceAt(i).getDisplayId();\r
463 \r
464       pg.drawString(string, 0,  jalview.analysis.AlignmentUtil.getPixelHeight\r
465                     (startSeq, i, av.getCharHeight()) + av.getCharHeight() - (av.getCharHeight() / 5));\r
466     }\r
467 \r
468     // draw main sequence panel\r
469     pg.translate(idWidth,0);\r
470     seqPanel.seqCanvas.drawPanel(pg,startRes,endRes,startSeq,endSeq,startRes,startSeq,0);\r
471 \r
472 \r
473     if(av.getShowAnnotation())\r
474       {\r
475         pg.translate(-idWidth,(endSeq-startSeq)*av.charHeight);\r
476         alabels.drawComponent((Graphics2D)pg);\r
477         pg.translate(idWidth,0);\r
478         annotationPanel.drawComponent((Graphics2D) pg, startRes, endRes+1);\r
479       }\r
480 \r
481     return Printable.PAGE_EXISTS;\r
482   }\r
483 \r
484 \r
485   public int printWrappedAlignment(Graphics pg, int pwidth, int pheight, int pi) throws PrinterException\r
486   {\r
487 \r
488     int idWidth = calculateIdWidth().width+4;\r
489 \r
490     if( seqPanel.seqCanvas.getWidth() < pwidth-idWidth)\r
491       pwidth = seqPanel.seqCanvas.getWidth() + idWidth;\r
492 \r
493 \r
494     pg.setColor(Color.white);\r
495     pg.fillRect(0,0,pwidth, pheight);\r
496     pg.setFont( av.getFont() );\r
497 \r
498     ////////////////////////////////////\r
499     /// How many sequences and residues can we fit on a printable page?\r
500     AlignmentI da = av.alignment;\r
501     int endy   = da.getHeight();\r
502     int chunkHeight =  (da.getHeight() + 2)*av.charHeight;\r
503     int chunkWidth  =   (pwidth-idWidth)/av.charWidth;\r
504 \r
505     int noChunksOnPage = pheight / chunkHeight;\r
506     int totalChunks = da.getWidth() / chunkWidth;\r
507 \r
508     if ( pi*noChunksOnPage > totalChunks )\r
509      return Printable.NO_SUCH_PAGE;\r
510 \r
511     ////////////////\r
512     // Draw the ids\r
513     pg.setClip(0,0,pwidth, noChunksOnPage*chunkHeight);\r
514 \r
515     pg.setColor(Color.black);\r
516 \r
517     int rowSize =  av.getEndRes() - av.getStartRes();\r
518     // Draw the rest of the panels\r
519 \r
520     for(int ypos=2*av.charHeight, row=av.getEndRes(); row<av.alignment.getWidth();\r
521         ypos += av.chunkHeight, row+=rowSize )\r
522     {\r
523       for (int i = 0; i < endy; i++)\r
524       {\r
525         SequenceI s = da.getSequenceAt(i);\r
526         String string = s.getName();\r
527         if (av.getShowFullId())\r
528           string = s.getDisplayId();\r
529 \r
530         pg.drawString(string, 0,\r
531                       AlignmentUtil.getPixelHeight(0, i, av.charHeight) + ypos +\r
532                       av.charHeight - (av.charHeight / 5));\r
533       }\r
534     }\r
535 \r
536     // draw main sequence panel\r
537     pg.translate(idWidth,0);\r
538     seqPanel.seqCanvas.drawWrappedPanel(pg, pwidth-idWidth, pheight, pi*noChunksOnPage*chunkWidth);\r
539 \r
540 \r
541     return Printable.PAGE_EXISTS;\r
542 \r
543   }\r
544 \r
545 \r
546   public void makeEPS()\r
547   {\r
548     int height = (av.alignment.getWidth() / av.getChunkWidth() +1) * av.chunkHeight;\r
549     int width = seqPanel.getWidth() + idPanel.getWidth();\r
550 \r
551     if (!av.getWrapAlignment())\r
552     {\r
553       height = (av.alignment.getHeight()+1) * av.charHeight + 30;\r
554       width = idPanel.getWidth() + av.alignment.getWidth() * av.charWidth;\r
555     }\r
556     if(av.getShowAnnotation())\r
557    {\r
558      height += annotationPanel.getPreferredSize().height;\r
559    }\r
560 \r
561     try\r
562     {\r
563       jalview.io.JalviewFileChooser chooser = new jalview.io.JalviewFileChooser(jalview.bin.Cache.getProperty(\r
564           "LAST_DIRECTORY"), new String[]{"eps"}, "Encapsulated Postscript");\r
565       chooser.setFileView(new jalview.io.JalviewFileView());\r
566       chooser.setDialogTitle("Create EPS file from alignment");\r
567       chooser.setToolTipText("Save");\r
568 \r
569       int value = chooser.showSaveDialog(this);\r
570       if (value != jalview.io.JalviewFileChooser.APPROVE_OPTION)\r
571         return;\r
572 \r
573       jalview.bin.Cache.setProperty("LAST_DIRECTORY",chooser.getSelectedFile().getPath());\r
574       FileOutputStream out = new FileOutputStream(chooser.getSelectedFile());\r
575       EpsGraphics2D pg = new EpsGraphics2D("Example", out, 0, 0, width, height);\r
576 \r
577         if (av.getWrapAlignment())\r
578           printWrappedAlignment(pg, width, height, 0);\r
579         else\r
580           printUnwrapped(pg, width, height, 0);\r
581 \r
582 \r
583         pg.flush();\r
584         pg.close();\r
585     }\r
586     catch (Exception ex)\r
587     {\r
588       ex.printStackTrace();\r
589     }\r
590   }\r
591 \r
592   public void makePNG()\r
593   {\r
594       int height = (av.alignment.getWidth() / av.getChunkWidth() +1) * av.chunkHeight;\r
595       int width = seqPanel.getWidth() + idPanel.getWidth();\r
596 \r
597       if (!av.getWrapAlignment())\r
598       {\r
599         height = (av.alignment.getHeight()+1) * av.charHeight + 30;\r
600         width = idPanel.getWidth() + av.alignment.getWidth() * av.charWidth;\r
601       }\r
602 \r
603       if(av.getShowAnnotation())\r
604       {\r
605         height += annotationPanel.getPreferredSize().height;\r
606       }\r
607 \r
608 \r
609 System.out.println(width +" "+height);\r
610 \r
611     try\r
612     {\r
613       jalview.io.JalviewFileChooser chooser = new jalview.io.JalviewFileChooser(jalview.bin.Cache.getProperty(\r
614           "LAST_DIRECTORY"), new String[]{"png"}, "Portable network graphics");\r
615       chooser.setFileView(new jalview.io.JalviewFileView());\r
616       chooser.setDialogTitle("Create EPS file from alignment");\r
617       chooser.setToolTipText("Save");\r
618 \r
619       int value = chooser.showSaveDialog(this);\r
620       if (value != jalview.io.JalviewFileChooser.APPROVE_OPTION)\r
621         return;\r
622 \r
623       jalview.bin.Cache.setProperty("LAST_DIRECTORY",chooser.getSelectedFile().getPath());\r
624       FileOutputStream out = new FileOutputStream(chooser.getSelectedFile());\r
625 \r
626       BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);\r
627       Graphics png = bi.getGraphics();\r
628 \r
629 \r
630         if (av.getWrapAlignment())\r
631           printWrappedAlignment(png, width, height, 0);\r
632         else\r
633           printUnwrapped(png, width, height, 0);\r
634 \r
635         ImageIO.write(bi, "png", out);\r
636         out.close();\r
637     }\r
638     catch (Exception ex)\r
639     {\r
640       ex.printStackTrace();\r
641     }\r
642   }\r
643 \r
644 }\r
645 \r
646 \r
647 \r
648 \r