001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.canvas;
020
021import icy.gui.main.MainFrame;
022import icy.gui.viewer.Viewer;
023import icy.main.Icy;
024import icy.painter.Overlay;
025import icy.sequence.DimensionId;
026import icy.sequence.Sequence;
027
028import java.awt.Component;
029import java.awt.Dimension;
030import java.awt.Graphics;
031import java.awt.Graphics2D;
032import java.awt.Point;
033import java.awt.Rectangle;
034import java.awt.geom.AffineTransform;
035import java.awt.geom.NoninvertibleTransformException;
036import java.awt.geom.Point2D;
037import java.awt.geom.Rectangle2D;
038
039import javax.swing.JComponent;
040
041/**
042 * @author Stephane
043 */
044public abstract class IcyCanvas2D extends IcyCanvas
045{
046    /**
047     * 
048     */
049    private static final long serialVersionUID = 743937493919099495L;
050
051    /** mouse position (image coordinate space) */
052    protected Point2D.Double mouseImagePos;
053
054    // image coordinate to canvas coordinate transform
055    protected final AffineTransform transform;
056    // canvas coordinate to image coordinate transform
057    protected AffineTransform inverseTransform;
058    protected boolean transformChanged;
059
060    public IcyCanvas2D(Viewer viewer)
061    {
062        super(viewer);
063
064        // default for 2D canvas
065        posX = -1;
066        posY = -1;
067        posZ = 0;
068        posT = 0;
069
070        // initial mouse position
071        mouseImagePos = new Point2D.Double();
072        transform = new AffineTransform();
073        inverseTransform = new AffineTransform();
074        transformChanged = false;
075
076        // adjust LUT alpha level for 2D view
077        lut.setAlphaToOpaque();
078    }
079
080    @Override
081    public void setPositionZ(int z)
082    {
083        // position -1 not supported for Z dimension on this canvas
084        if (z != -1)
085            super.setPositionZ(z);
086    }
087
088    @Override
089    public void setPositionT(int t)
090    {
091        // position -1 not supported for T dimension on this canvas
092        if (t != -1)
093            super.setPositionT(t);
094    }
095
096    @Override
097    public double getMouseImagePosX()
098    {
099        // can be called before constructor ended
100        if (mouseImagePos == null)
101            return 0d;
102
103        return mouseImagePos.x;
104
105    }
106
107    @Override
108    public double getMouseImagePosY()
109    {
110        // can be called before constructor ended
111        if (mouseImagePos == null)
112            return 0d;
113
114        return mouseImagePos.y;
115    }
116
117    /**
118     * Return mouse image position
119     */
120    public Point2D.Double getMouseImagePos()
121    {
122        return (Point2D.Double) mouseImagePos.clone();
123    }
124
125    public void setMouseImagePos(double x, double y)
126    {
127        if ((mouseImagePos.x != x) || (mouseImagePos.y != y))
128        {
129            mouseImagePos.x = x;
130            mouseImagePos.y = y;
131
132            // direct update of mouse canvas position
133            mousePos = imageToCanvas(mouseImagePos);
134            // notify change
135            mouseImagePositionChanged(DimensionId.NULL);
136        }
137    }
138
139    /**
140     * Set mouse image position
141     */
142    public void setMouseImagePos(Point2D.Double point)
143    {
144        setMouseImagePos(point.x, point.y);
145    }
146
147    @Override
148    public boolean setMousePos(int x, int y)
149    {
150        final boolean result = super.setMousePos(x, y);
151
152        if (result)
153        {
154            if (mouseImagePos == null)
155                mouseImagePos = new Point2D.Double();
156
157            final Point2D newPos = canvasToImage(mousePos);
158            final double newX = newPos.getX();
159            final double newY = newPos.getY();
160            boolean changed = false;
161
162            // need to check against NaN is conversion is not supported
163            if (!Double.isNaN(newX) && (newX != mouseImagePos.x))
164            {
165                mouseImagePos.x = newX;
166                changed = true;
167            }
168            // need to check against NaN is conversion is not supported
169            if (!Double.isNaN(newY) && (newY != mouseImagePos.y))
170            {
171                mouseImagePos.y = newY;
172                changed = true;
173            }
174
175            // notify change
176            if (changed)
177                mouseImagePositionChanged(DimensionId.NULL);
178        }
179
180        return result;
181    }
182
183    /**
184     * @deprecated Use {@link #setMousePos(int, int)} instead
185     */
186    @Deprecated
187    public void setMouseCanvasPos(int x, int y)
188    {
189        setMousePos(x, y);
190    }
191
192    /**
193     * @deprecated Use {@link #setMousePos(Point)} instead.
194     */
195    @Deprecated
196    public void setMouseCanvasPos(Point point)
197    {
198        setMousePos(point);
199    }
200
201    @Override
202    protected void setMouseImagePosXInternal(double value)
203    {
204        mouseImagePos.x = value;
205
206        // direct update of mouse canvas position
207        mousePos = imageToCanvas(mouseImagePos);
208
209        super.setMouseImagePosXInternal(value);
210    }
211
212    @Override
213    protected void setMouseImagePosYInternal(double value)
214    {
215        mouseImagePos.y = value;
216
217        // direct update of mouse canvas position
218        mousePos = imageToCanvas(mouseImagePos);
219
220        super.setMouseImagePosYInternal(value);
221    }
222
223    /**
224     * Convert specified canvas delta to image delta.<br>
225     */
226    protected Point2D.Double canvasToImageDelta(int x, int y, double scaleX, double scaleY, double rot)
227    {
228        // get cos and sin
229        final double cos = Math.cos(-rot);
230        final double sin = Math.sin(-rot);
231
232        // apply rotation
233        final double resX = (x * cos) - (y * sin);
234        final double resY = (x * sin) + (y * cos);
235
236        // and scale
237        return new Point2D.Double(resX / scaleX, resY / scaleY);
238    }
239
240    /**
241     * Convert specified canvas delta point to image delta point
242     */
243    public Point2D.Double canvasToImageDelta(int x, int y)
244    {
245        return canvasToImageDelta(x, y, getScaleX(), getScaleY(), getRotationZ());
246    }
247
248    /**
249     * Convert specified canvas delta point to image delta point
250     */
251    public Point2D.Double canvasToImageDelta(Point point)
252    {
253        return canvasToImageDelta(point.x, point.y);
254    }
255
256    /**
257     * Convert specified canvas delta point to image delta point.
258     * The conversion is affected by zoom ratio but with the specified logarithm factor.
259     */
260    public Point2D.Double canvasToImageLogDelta(int x, int y, double logFactor)
261    {
262        final double sx = getScaleX() / Math.pow(10, Math.log10(getScaleX()) / logFactor);
263        final double sy = getScaleY() / Math.pow(10, Math.log10(getScaleY()) / logFactor);
264
265        return canvasToImageDelta(x, y, sx, sy, getRotationZ());
266    }
267
268    /**
269     * Convert specified canvas delta point to image delta point.
270     * The conversion is affected by zoom ratio but with the specified logarithm factor.
271     */
272    public Point2D.Double canvasToImageLogDelta(int x, int y)
273    {
274        return canvasToImageLogDelta(x, y, 5d);
275    }
276
277    /**
278     * Convert specified canvas point to image point.<br>
279     * By default we consider the rotation applied relatively to canvas center.<br>
280     * Override this method if you want different transformation type.
281     */
282    protected Point2D.Double canvasToImage(int x, int y, int offsetX, int offsetY, double scaleX, double scaleY,
283            double rot)
284    {
285        // get canvas center
286        final double canvasCenterX = getCanvasSizeX() / 2;
287        final double canvasCenterY = getCanvasSizeY() / 2;
288
289        // center to canvas for rotation
290        final double dx = x - canvasCenterX;
291        final double dy = y - canvasCenterY;
292
293        // get cos and sin
294        final double cos = Math.cos(-rot);
295        final double sin = Math.sin(-rot);
296
297        // apply rotation
298        double resX = (dx * cos) - (dy * sin);
299        double resY = (dx * sin) + (dy * cos);
300
301        // translate back to position
302        resX += canvasCenterX;
303        resY += canvasCenterY;
304
305        // basic transform to image coordinates
306        resX = ((resX - offsetX) / scaleX);
307        resY = ((resY - offsetY) / scaleY);
308
309        return new Point2D.Double(resX, resY);
310    }
311
312    /**
313     * Convert specified canvas point to image point
314     */
315    public Point2D.Double canvasToImage(int x, int y)
316    {
317        final Point2D.Double result = new Point2D.Double(0d, 0d);
318
319        // we can directly use the transform object here
320        getInverseTransform().transform(new Point2D.Double(x, y), result);
321
322        return result;
323
324        // return canvasToImage(x, y, getOffsetX(), getOffsetY(), getScaleX(), getScaleY(),
325        // getRotationZ());
326    }
327
328    /**
329     * Convert specified canvas point to image point
330     */
331    public Point2D.Double canvasToImage(Point point)
332    {
333        return canvasToImage(point.x, point.y);
334    }
335
336    /**
337     * Convert specified canvas rectangle to image rectangle
338     */
339    public Rectangle2D.Double canvasToImage(int x, int y, int w, int h)
340    {
341        // convert each rectangle point
342        final Point2D.Double pt1 = canvasToImage(x, y);
343        final Point2D.Double pt2 = canvasToImage(x + w, y);
344        final Point2D.Double pt3 = canvasToImage(x + w, y + h);
345        final Point2D.Double pt4 = canvasToImage(x, y + h);
346
347        // get minimum and maximum X / Y
348        final double minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
349        final double maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
350        final double minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
351        final double maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));
352
353        // return transformed rectangle
354        return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
355    }
356
357    /**
358     * Convert specified canvas rectangle to image rectangle
359     */
360    public Rectangle2D.Double canvasToImage(Rectangle rect)
361    {
362        return canvasToImage(rect.x, rect.y, rect.width, rect.height);
363    }
364
365    /**
366     * Convert specified image delta to canvas delta.<br>
367     */
368    protected Point imageToCanvasDelta(double x, double y, double scaleX, double scaleY, double rot)
369    {
370        // apply scale
371        final double dx = x * scaleX;
372        final double dy = y * scaleY;
373
374        // get cos and sin
375        final double cos = Math.cos(rot);
376        final double sin = Math.sin(rot);
377
378        // apply rotation
379        final double resX = (dx * cos) - (dy * sin);
380        final double resY = (dx * sin) + (dy * cos);
381
382        return new Point((int) Math.round(resX), (int) Math.round(resY));
383    }
384
385    /**
386     * Convert specified image delta point to canvas delta point
387     */
388    public Point imageToCanvasDelta(double x, double y)
389    {
390        return imageToCanvasDelta(x, y, getScaleX(), getScaleY(), getRotationZ());
391    }
392
393    /**
394     * Convert specified image delta point to canvas delta point
395     */
396    public Point imageToCanvasDelta(Point2D.Double point)
397    {
398        return imageToCanvasDelta(point.x, point.y);
399    }
400
401    /**
402     * Convert specified image point to canvas point.<br>
403     * By default we consider the rotation applied relatively to image center.<br>
404     * Override this method if you want different transformation type.
405     */
406    protected Point imageToCanvas(double x, double y, int offsetX, int offsetY, double scaleX, double scaleY, double rot)
407    {
408        // get canvas center
409        final double canvasCenterX = getCanvasSizeX() / 2;
410        final double canvasCenterY = getCanvasSizeY() / 2;
411
412        // basic transform to canvas coordinates and canvas centering
413        final double dx = ((x * scaleX) + offsetX) - canvasCenterX;
414        final double dy = ((y * scaleY) + offsetY) - canvasCenterY;
415
416        // get cos and sin
417        final double cos = Math.cos(rot);
418        final double sin = Math.sin(rot);
419
420        // apply rotation
421        double resX = (dx * cos) - (dy * sin);
422        double resY = (dx * sin) + (dy * cos);
423
424        // translate back to position
425        resX += canvasCenterX;
426        resY += canvasCenterY;
427
428        return new Point((int) Math.round(resX), (int) Math.round(resY));
429    }
430
431    /**
432     * Convert specified image point to canvas point
433     */
434    public Point imageToCanvas(double x, double y)
435    {
436        final Point result = new Point();
437
438        // we can directly use the transform object here
439        getTransform().transform(new Point2D.Double(x, y), result);
440
441        return result;
442
443        // return imageToCanvas(x, y, getOffsetX(), getOffsetY(), getScaleX(), getScaleY(),
444        // getRotationZ());
445    }
446
447    /**
448     * Convert specified image point to canvas point
449     */
450    public Point imageToCanvas(Point2D.Double point)
451    {
452        return imageToCanvas(point.x, point.y);
453    }
454
455    /**
456     * Convert specified image rectangle to canvas rectangle
457     */
458    public Rectangle imageToCanvas(double x, double y, double w, double h)
459    {
460        // convert each rectangle point
461        final Point pt1 = imageToCanvas(x, y);
462        final Point pt2 = imageToCanvas(x + w, y);
463        final Point pt3 = imageToCanvas(x + w, y + h);
464        final Point pt4 = imageToCanvas(x, y + h);
465
466        // get minimum and maximum X / Y
467        final int minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
468        final int maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
469        final int minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
470        final int maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));
471
472        // return transformed rectangle
473        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
474    }
475
476    /**
477     * Convert specified image rectangle to canvas rectangle
478     */
479    public Rectangle imageToCanvas(Rectangle2D.Double rect)
480    {
481        return imageToCanvas(rect.x, rect.y, rect.width, rect.height);
482    }
483
484    /**
485     * Get 2D view size in canvas pixel coordinate
486     * 
487     * @return a Dimension which represents the visible size.
488     */
489    public Dimension getCanvasSize()
490    {
491        return new Dimension(getCanvasSizeX(), getCanvasSizeY());
492    }
493
494    /**
495     * Get 2D image size
496     */
497    public Dimension getImageSize()
498    {
499        return new Dimension(getImageSizeX(), getImageSizeY());
500    }
501
502    /**
503     * Get 2D image size in canvas pixel coordinate
504     */
505    public Dimension getImageCanvasSize()
506    {
507        final double imageSizeX = getImageSizeX();
508        final double imageSizeY = getImageSizeY();
509        final double scaleX = getScaleX();
510        final double scaleY = getScaleY();
511        final double rot = getRotationZ();
512
513        // convert image rectangle
514        final Point pt1 = imageToCanvas(0d, 0d, 0, 0, scaleX, scaleY, rot);
515        final Point pt2 = imageToCanvas(imageSizeX, 0d, 0, 0, scaleX, scaleY, rot);
516        final Point pt3 = imageToCanvas(0d, imageSizeY, 0, 0, scaleX, scaleY, rot);
517        final Point pt4 = imageToCanvas(imageSizeX, imageSizeY, 0, 0, scaleX, scaleY, rot);
518
519        final int minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
520        final int maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
521        final int minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
522        final int maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));
523
524        return new Dimension(maxX - minX, maxY - minY);
525    }
526
527    /**
528     * Get 2D canvas visible rectangle (canvas coordinate).
529     */
530    public Rectangle getCanvasVisibleRect()
531    {
532        // try to return view component visible rectangle by default
533        final Component comp = getViewComponent();
534        if (comp instanceof JComponent)
535            return ((JComponent) comp).getVisibleRect();
536
537        // just return the canvas component visible rectangle
538        return getVisibleRect();
539    }
540
541    /**
542     * Get 2D image visible rectangle (image coordinate).<br>
543     * Prefer the {@link Graphics#getClipBounds()} method for paint operation as the image visible
544     * rectangle may return wrong information sometime (when using the {@link #getRenderedImage(int, int, int, boolean)}
545     * method for instance).
546     */
547    public Rectangle2D getImageVisibleRect()
548    {
549        return canvasToImage(getCanvasVisibleRect());
550    }
551    
552    /**
553     * Adjust view position and possibly scaling factor to ensure the specified region become visible.<br>
554     * It's up to the Canvas implementation to decide how to make the region visible.
555     * 
556     * @param region
557     *        the region we want to see
558     */
559    public void centerOn(Rectangle region)
560    {
561        // override it in Canvas implementation
562    }
563
564    /**
565     * Center image on specified image position in canvas
566     */
567    public void centerOnImage(double x, double y)
568    {
569        // get point on canvas
570        final Point pt = imageToCanvas(x, y);
571        final int canvasCenterX = getCanvasSizeX() / 2;
572        final int canvasCenterY = getCanvasSizeY() / 2;
573
574        final Point2D.Double newTrans = canvasToImageDelta(canvasCenterX - pt.x, canvasCenterY - pt.y, 1d, 1d,
575                getRotationZ());
576
577        setOffsetX(getOffsetX() + (int) Math.round(newTrans.x));
578        setOffsetY(getOffsetY() + (int) Math.round(newTrans.y));
579    }
580
581    /**
582     * Center image on specified image position in canvas
583     */
584    public void centerOnImage(Point2D.Double pt)
585    {
586        centerOnImage(pt.x, pt.y);
587    }
588
589    /**
590     * Center image in canvas
591     */
592    public void centerImage()
593    {
594        centerOnImage(getImageSizeX() / 2, getImageSizeY() / 2);
595    }
596
597    /**
598     * get scale X and scale Y so image fit in canvas view dimension
599     */
600    protected Point2D.Double getFitImageToCanvasScale()
601    {
602        final double imageSizeX = getImageSizeX();
603        final double imageSizeY = getImageSizeY();
604
605        if ((imageSizeX > 0d) && (imageSizeY > 0d))
606        {
607            final double rot = getRotationZ();
608
609            // convert image rectangle
610            final Point pt1 = imageToCanvas(0d, 0d, 0, 0, 1d, 1d, rot);
611            final Point pt2 = imageToCanvas(imageSizeX, 0d, 0, 0, 1d, 1d, rot);
612            final Point pt3 = imageToCanvas(0d, imageSizeY, 0, 0, 1d, 1d, rot);
613            final Point pt4 = imageToCanvas(imageSizeX, imageSizeY, 0, 0, 1d, 1d, rot);
614
615            final int minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
616            final int maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
617            final int minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
618            final int maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));
619
620            // get image dimension transformed by rotation
621            final double sx = (double) getCanvasSizeX() / (double) (maxX - minX);
622            final double sy = (double) getCanvasSizeY() / (double) (maxY - minY);
623
624            return new Point2D.Double(sx, sy);
625        }
626
627        return null;
628    }
629
630    /**
631     * Change scale so image fit in canvas view dimension
632     */
633    public void fitImageToCanvas()
634    {
635        final Point2D.Double s = getFitImageToCanvasScale();
636
637        if (s != null)
638        {
639            final double scale = Math.min(s.x, s.y);
640
641            setScaleX(scale);
642            setScaleY(scale);
643        }
644    }
645
646    /**
647     * Change canvas size (so viewer size) to get it fit with image dimension if possible
648     */
649    public void fitCanvasToImage()
650    {
651        final MainFrame mainFrame = Icy.getMainInterface().getMainFrame();
652        final Dimension imageCanvasSize = getImageCanvasSize();
653
654        if ((imageCanvasSize.width > 0) && (imageCanvasSize.height > 0) && (mainFrame != null))
655        {
656            final Dimension maxDim = mainFrame.getDesktopSize();
657            final Dimension adjImgCnvSize = canvasToViewer(imageCanvasSize);
658
659            // fit in available space --> resize viewer
660            viewer.setSize(Math.min(adjImgCnvSize.width, maxDim.width), Math.min(adjImgCnvSize.height, maxDim.height));
661        }
662    }
663
664    /**
665     * Convert canvas dimension to viewer dimension
666     */
667    public Dimension canvasToViewer(Dimension dim)
668    {
669        final Dimension canvasViewSize = getCanvasSize();
670        final Dimension viewerSize = viewer.getSize();
671        final Dimension result = new Dimension(dim);
672
673        result.width -= canvasViewSize.width;
674        result.width += viewerSize.width;
675        result.height -= canvasViewSize.height;
676        result.height += viewerSize.height;
677
678        return result;
679    }
680
681    /**
682     * Convert viewer dimension to canvas dimension
683     */
684    public Dimension viewerToCanvas(Dimension dim)
685    {
686        final Dimension canvasViewSize = getCanvasSize();
687        final Dimension viewerSize = viewer.getSize();
688        final Dimension result = new Dimension(dim);
689
690        result.width -= viewerSize.width;
691        result.width += canvasViewSize.width;
692        result.height -= viewerSize.height;
693        result.height += canvasViewSize.height;
694
695        return result;
696    }
697
698    /**
699     * Update internal {@link AffineTransform} object.
700     */
701    protected void updateTransform()
702    {
703        final int canvasCenterX = getCanvasSizeX() / 2;
704        final int canvasCenterY = getCanvasSizeY() / 2;
705
706        // rotation is centered to canvas
707        transform.setToTranslation(canvasCenterX, canvasCenterY);
708        transform.rotate(getRotationZ());
709        transform.translate(-canvasCenterX, -canvasCenterY);
710
711        transform.translate(getOffsetX(), getOffsetY());
712        transform.scale(getScaleX(), getScaleY());
713
714        transformChanged = true;
715    }
716
717    /**
718     * Return the 2D {@link AffineTransform} object which convert from image coordinate to canvas
719     * coordinate.<br>
720     * {@link Overlay} should directly use the transform information from the {@link Graphics2D} object provided in
721     * their {@link Overlay#paint(Graphics2D, Sequence, IcyCanvas)} method.
722     */
723    public AffineTransform getTransform()
724    {
725        return transform;
726    }
727
728    /**
729     * Return the 2D {@link AffineTransform} object which convert from canvas coordinate to image
730     * coordinate.<br>
731     * {@link Overlay} should directly use the transform information from the {@link Graphics2D} object provided in
732     * their {@link Overlay#paint(Graphics2D, Sequence, IcyCanvas)} method.
733     */
734    public AffineTransform getInverseTransform()
735    {
736        if (transformChanged)
737        {
738            try
739            {
740                inverseTransform = transform.createInverse();
741            }
742            catch (NoninvertibleTransformException e)
743            {
744                inverseTransform = new AffineTransform();
745            }
746
747            transformChanged = false;
748        }
749
750        return inverseTransform;
751    }
752
753    @Override
754    public void changed(IcyCanvasEvent event)
755    {
756        super.changed(event);
757
758        switch (event.getType())
759        {
760            case OFFSET_CHANGED:
761            case ROTATION_CHANGED:
762            case SCALE_CHANGED:
763                updateTransform();
764                break;
765        }
766    }
767}