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 java.awt.AlphaComposite;
022import java.awt.BasicStroke;
023import java.awt.BorderLayout;
024import java.awt.Color;
025import java.awt.Component;
026import java.awt.Cursor;
027import java.awt.Dimension;
028import java.awt.Font;
029import java.awt.Graphics;
030import java.awt.Graphics2D;
031import java.awt.Image;
032import java.awt.Point;
033import java.awt.Rectangle;
034import java.awt.RenderingHints;
035import java.awt.Shape;
036import java.awt.event.ActionEvent;
037import java.awt.event.ActionListener;
038import java.awt.event.ComponentAdapter;
039import java.awt.event.ComponentEvent;
040import java.awt.event.InputEvent;
041import java.awt.event.KeyEvent;
042import java.awt.event.MouseEvent;
043import java.awt.event.MouseListener;
044import java.awt.event.MouseMotionListener;
045import java.awt.event.MouseWheelEvent;
046import java.awt.event.MouseWheelListener;
047import java.awt.geom.AffineTransform;
048import java.awt.geom.Point2D;
049import java.awt.geom.Rectangle2D;
050import java.awt.image.BufferedImage;
051import java.util.ArrayList;
052import java.util.List;
053import java.util.concurrent.TimeUnit;
054
055import javax.swing.BorderFactory;
056import javax.swing.JPanel;
057import javax.swing.JToolBar;
058import javax.swing.SwingUtilities;
059import javax.swing.Timer;
060
061import icy.canvas.Canvas2D.CanvasView.ImageCache.ImageCacheTile;
062import icy.canvas.CanvasLayerEvent.LayersEventType;
063import icy.canvas.IcyCanvasEvent.IcyCanvasEventType;
064import icy.gui.component.button.IcyToggleButton;
065import icy.gui.menu.ROITask;
066import icy.gui.menu.ROITask.ROITaskListener;
067import icy.gui.util.GuiUtil;
068import icy.gui.viewer.Viewer;
069import icy.image.IcyBufferedImage;
070import icy.image.IcyBufferedImageUtil;
071import icy.image.ImageUtil;
072import icy.image.lut.LUT;
073import icy.main.Icy;
074import icy.math.Interpolator;
075import icy.math.MathUtil;
076import icy.math.MultiSmoothMover;
077import icy.math.MultiSmoothMover.MultiSmoothMoverAdapter;
078import icy.math.SmoothMover;
079import icy.math.SmoothMover.SmoothMoveType;
080import icy.math.SmoothMover.SmoothMoverAdapter;
081import icy.painter.ImageOverlay;
082import icy.painter.Overlay;
083import icy.preferences.CanvasPreferences;
084import icy.preferences.XMLPreferences;
085import icy.resource.ResourceUtil;
086import icy.resource.icon.IcyIcon;
087import icy.roi.ROI;
088import icy.sequence.DimensionId;
089import icy.sequence.Sequence;
090import icy.sequence.SequenceEvent.SequenceEventType;
091import icy.system.thread.SingleProcessor;
092import icy.system.thread.ThreadUtil;
093import icy.type.rectangle.Rectangle2DUtil;
094import icy.type.rectangle.Rectangle5D;
095import icy.util.EventUtil;
096import icy.util.GraphicsUtil;
097import icy.util.ShapeUtil;
098import icy.util.StringUtil;
099import plugins.kernel.roi.tool.plugin.ROILineCutterPlugin;
100
101/**
102 * New Canvas 2D : default ICY 2D viewer.<br>
103 * Support translation / scale and rotation transformation.<br>
104 * 
105 * @author Stephane
106 */
107public class Canvas2D extends IcyCanvas2D implements ROITaskListener
108{
109    /**
110     * 
111     */
112    private static final long serialVersionUID = 8850168605044063031L;
113
114    static final int ICON_SIZE = 20;
115    static final int ICON_TARGET_SIZE = 20;
116
117    static final Image ICON_CENTER_IMAGE = ResourceUtil.ICON_CENTER_IMAGE;
118    static final Image ICON_FIT_IMAGE = ResourceUtil.ICON_FIT_IMAGE;
119    static final Image ICON_FIT_CANVAS = ResourceUtil.ICON_FIT_CANVAS;
120    // static final Image ICON_CENTER_IMAGE = ImageUtil.scale(ResourceUtil.ICON_CENTER_IMAGE,
121    // ICON_SIZE, ICON_SIZE);
122    // static final Image ICON_FIT_IMAGE = ImageUtil.scale(ResourceUtil.ICON_FIT_IMAGE, ICON_SIZE,
123    // ICON_SIZE);
124    // static final Image ICON_FIT_CANVAS = ImageUtil.scale(ResourceUtil.ICON_FIT_CANVAS, ICON_SIZE,
125    // ICON_SIZE);
126
127    static final Image ICON_TARGET = ImageUtil.scale(ResourceUtil.ICON_TARGET, ICON_SIZE, ICON_SIZE);
128    static final Image ICON_TARGET_BLACK = ImageUtil.getColorImageFromAlphaImage(ICON_TARGET, Color.black);
129    static final Image ICON_TARGET_LIGHT = ImageUtil.getColorImageFromAlphaImage(ICON_TARGET, Color.lightGray);
130
131    /**
132     * Possible rounded zoom factor : 0.01 --> 100
133     */
134    final static double[] zoomRoundedFactors = new double[] {0.01d, 0.02d, 0.0333d, 0.05d, 0.075d, 0.1d, 0.15d, 0.2d,
135            0.25d, 0.333d, 0.5d, 0.66d, 0.75d, 1d, 1.25d, 1.5d, 1.75d, 2d, 2.5d, 3d, 4d, 5d, 6.6d, 7.5d, 10d, 15d, 20d,
136            30d, 50d, 66d, 75d, 100d};
137
138    /**
139     * Image overlay to encapsulate image display in a canvas layer
140     */
141    protected class Canvas2DImageOverlay extends IcyCanvasImageOverlay
142    {
143        @Override
144        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
145        {
146            if (g == null)
147                return;
148
149            final List<ImageCacheTile> tiles = canvasView.imageCache.getImageAsTiles();
150
151            // draw image
152            for (ImageCacheTile tile : tiles)
153                g.drawImage(tile.image, tile.rect.x, tile.rect.y, null);
154
155            if (tiles.isEmpty())
156            {
157                final Graphics2D g2 = (Graphics2D) g.create();
158
159                // set back canvas coordinate
160                g2.transform(getInverseTransform());
161
162                g2.setFont(canvasView.font);
163                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
164
165                if (canvasView.imageCache.isProcessing())
166                    // cache not yet built
167                    canvasView.drawTextCenter(g2, "Loading...", 0.8f);
168                else if (canvasView.imageCache.getNotEnoughMemory())
169                    // not enough memory to render image
170                    canvasView.drawTextCenter(g2, "Not enough memory to display image", 0.8f);
171                else
172                    // no image
173                    canvasView.drawTextCenter(g2, " No image ", 0.8f);
174
175                g2.dispose();
176            }
177        }
178    }
179
180    public class CanvasMap extends JPanel implements MouseListener, MouseMotionListener, MouseWheelListener
181    {
182        /**
183         * 
184         */
185        private static final long serialVersionUID = -7305605644605013768L;
186
187        private Point mouseMapPos;
188        private Point mapStartDragPos;
189        private double mapStartRotationZ;
190        private boolean mapMoving;
191        private boolean mapRotating;
192
193        public CanvasMap()
194        {
195            super();
196
197            mouseMapPos = new Point(0, 0);
198            mapStartDragPos = null;
199            mapStartRotationZ = 0;
200            mapMoving = false;
201            mapRotating = false;
202
203            setBorder(BorderFactory.createRaisedBevelBorder());
204            // height will then be fixed to 160
205            setPreferredSize(new Dimension(160, 160));
206
207            addMouseListener(this);
208            addMouseMotionListener(this);
209            addMouseWheelListener(this);
210        }
211
212        /**
213         * Return AffineTransform object which transform an image coordinate to map coordinate.
214         */
215        public AffineTransform getImageTransform()
216        {
217            final int w = getWidth();
218            final int h = getHeight();
219            final int imgW = getImageSizeX();
220            final int imgH = getImageSizeY();
221
222            if ((imgW == 0) || (imgH == 0))
223                return null;
224
225            final double sx = (double) w / (double) imgW;
226            final double sy = (double) h / (double) imgH;
227            final double tx, ty;
228            final double s;
229
230            // scale to viewport
231            if (sx < sy)
232            {
233                s = sx;
234                tx = 0;
235                ty = (h - (imgH * s)) / 2;
236            }
237            else if (sx > sy)
238            {
239                s = sy;
240                ty = 0;
241                tx = (w - (imgW * s)) / 2;
242            }
243            else
244            {
245                s = sx;
246                tx = 0;
247                ty = 0;
248            }
249
250            final AffineTransform result = new AffineTransform();
251
252            // get transformation to fit image in minimap
253            result.translate(tx, ty);
254            result.scale(s, s);
255
256            return result;
257        }
258
259        /**
260         * Transform a CanvasMap point in CanvasView point
261         */
262        public Point getCanvasPosition(Point p)
263        {
264            // transform map coordinate to canvas coordinate
265            return imageToCanvas(getImagePosition(p));
266        }
267
268        /**
269         * Transforms a Image point in CanvasView point.
270         */
271        public Point getCanvasPosition(Point2D.Double p)
272        {
273            // transform image coordinate to canvas coordinate
274            return imageToCanvas(p);
275        }
276
277        /**
278         * Transforms a CanvasMap point in Image point.
279         */
280        public Point2D.Double getImagePosition(Point p)
281        {
282            final AffineTransform trans = getImageTransform();
283
284            try
285            {
286                // get image coordinates
287                return (Point2D.Double) trans.inverseTransform(p, new Point2D.Double());
288            }
289            catch (Exception ecx)
290            {
291                return new Point2D.Double(0, 0);
292            }
293        }
294
295        public boolean isDragging()
296        {
297            return mapStartDragPos != null;
298        }
299
300        protected void updateDrag(InputEvent e)
301        {
302            // not moving --> exit
303            if (!mapMoving)
304                return;
305
306            final Point2D.Double startDragImagePoint = getImagePosition(mapStartDragPos);
307            final Point2D.Double imagePoint = getImagePosition(mouseMapPos);
308
309            // shift action --> limit to one direction
310            if (EventUtil.isShiftDown(e))
311            {
312                // X drag
313                if (Math.abs(mouseMapPos.x - mapStartDragPos.x) > Math.abs(mouseMapPos.y - mapStartDragPos.y))
314                    imagePoint.y = startDragImagePoint.y;
315                // Y drag
316                else
317                    imagePoint.x = startDragImagePoint.x;
318            }
319
320            // center view on this point (this update mouse canvas position)
321            centerOnImage(imagePoint);
322            // no need to update mouse canvas position here as it stays at center
323        }
324
325        protected void updateRot(InputEvent e)
326        {
327            // not rotating --> exit
328            if (!mapRotating)
329                return;
330
331            final Point2D.Double imagePoint = getImagePosition(mouseMapPos);
332
333            // update mouse canvas position from image position
334            setMousePos(imageToCanvas(imagePoint));
335
336            // get map center
337            final int mapCenterX = getWidth() / 2;
338            final int mapCenterY = getHeight() / 2;
339
340            // get last and current mouse position delta with center
341            final int lastMouseDeltaPosX = mapStartDragPos.x - mapCenterX;
342            final int lastMouseDeltaPosY = mapStartDragPos.y - mapCenterY;
343            final int newMouseDeltaPosX = mouseMapPos.x - mapCenterX;
344            final int newMouseDeltaPosY = mouseMapPos.y - mapCenterY;
345
346            // get angle in radian between last and current mouse position
347            // relative to image center
348            double newAngle = Math.atan2(newMouseDeltaPosY, newMouseDeltaPosX);
349            double lastAngle = Math.atan2(lastMouseDeltaPosY, lastMouseDeltaPosX);
350
351            // inverse rotation
352            double angle = lastAngle - newAngle;
353
354            // control button down --> rotation is enforced
355            if (EventUtil.isControlDown(e))
356                angle *= 3;
357
358            final double destAngle;
359
360            // shift action --> limit to 45° rotation
361            if (EventUtil.isShiftDown(e))
362                destAngle = Math.rint((mapStartRotationZ + angle) * (8d / (2 * Math.PI))) * ((2 * Math.PI) / 8d);
363            else
364                destAngle = mapStartRotationZ + angle;
365
366            // modify rotation with smooth mover
367            setRotation(destAngle, true);
368        }
369
370        @Override
371        public void mouseDragged(MouseEvent e)
372        {
373            canvasView.handlingMouseMoveEvent = true;
374            try
375            {
376                mouseMapPos = new Point(e.getPoint());
377
378                // get the drag event ?
379                if (isDragging())
380                {
381                    // left button action --> center view on mouse point
382                    if (EventUtil.isLeftMouseButton(e))
383                    {
384
385                        mapMoving = true;
386                        if (mapRotating)
387                        {
388                            mapRotating = false;
389                            // force repaint so the cross is no more visible
390                            canvasView.repaint();
391                        }
392
393                        updateDrag(e);
394                    }
395                    else if (EventUtil.isRightMouseButton(e))
396                    {
397                        mapMoving = false;
398                        if (!mapRotating)
399                        {
400                            mapRotating = true;
401                            // force repaint so the cross is visible
402                            canvasView.repaint();
403                        }
404
405                        updateRot(e);
406                    }
407
408                    // consume event
409                    e.consume();
410                }
411            }
412            finally
413            {
414                canvasView.handlingMouseMoveEvent = false;
415            }
416        }
417
418        @Override
419        public void mouseMoved(MouseEvent e)
420        {
421            mouseMapPos = new Point(e.getPoint());
422
423            // send to canvas view with converted canvas position
424            canvasView.onMousePositionChanged(getCanvasPosition(e.getPoint()));
425        }
426
427        @Override
428        public void mouseClicked(MouseEvent e)
429        {
430            // nothing here
431        }
432
433        @Override
434        public void mousePressed(MouseEvent e)
435        {
436            // start drag mouse position
437            mapStartDragPos = (Point) e.getPoint().clone();
438            // store canvas parameters
439            mapStartRotationZ = getRotationZ();
440
441            // left click action --> center view on mouse point
442            if (EventUtil.isLeftMouseButton(e))
443            {
444                final AffineTransform trans = getImageTransform();
445
446                if (trans != null)
447                {
448                    try
449                    {
450                        // get image coordinates
451                        final Point2D imagePoint = trans.inverseTransform(e.getPoint(), null);
452                        // center view on this point
453                        centerOnImage(imagePoint.getX(), imagePoint.getY());
454                        // update new canvas position
455                        setMousePos(imageToCanvas(imagePoint.getX(), imagePoint.getY()));
456                        // consume event
457                        e.consume();
458                    }
459                    catch (Exception ecx)
460                    {
461                        // ignore
462                    }
463                }
464            }
465        }
466
467        @Override
468        public void mouseReleased(MouseEvent e)
469        {
470            // assume end dragging
471            mapStartDragPos = null;
472            mapRotating = false;
473            mapMoving = false;
474            // repaint
475            repaint();
476        }
477
478        @Override
479        public void mouseEntered(MouseEvent e)
480        {
481            // nothing here
482        }
483
484        @Override
485        public void mouseExited(MouseEvent e)
486        {
487            // nothing here
488        }
489
490        @Override
491        public void mouseWheelMoved(MouseWheelEvent e)
492        {
493            // we first center image to mouse position
494            final AffineTransform trans = getImageTransform();
495
496            if (trans != null)
497            {
498                try
499                {
500                    // get image coordinates
501                    final Point2D imagePoint = trans.inverseTransform(e.getPoint(), null);
502                    // center view on this point
503                    centerOnImage(imagePoint.getX(), imagePoint.getY());
504                    // update new canvas position
505                    setMousePos(imageToCanvas(imagePoint.getX(), imagePoint.getY()));
506                }
507                catch (Exception ecx)
508                {
509                    // ignore
510                }
511            }
512
513            // send to canvas view
514            if (canvasView.onMouseWheelMoved(e.isConsumed(), e.getWheelRotation(), EventUtil.isLeftMouseButton(e),
515                    EventUtil.isRightMouseButton(e), EventUtil.isControlDown(e), EventUtil.isShiftDown(e)))
516                e.consume();
517        }
518
519        public void keyPressed(KeyEvent e)
520        {
521            // just for the shift key state change
522            updateDrag(e);
523            updateRot(e);
524        }
525
526        public void keyReleased(KeyEvent e)
527        {
528            // just for the shift key state change
529            updateDrag(e);
530            updateRot(e);
531        }
532
533        @Override
534        protected void paintComponent(Graphics g)
535        {
536            super.paintComponent(g);
537
538            final AffineTransform trans = getImageTransform();
539
540            if (trans != null)
541            {
542                final Graphics2D g2 = (Graphics2D) g.create();
543                final List<ImageCacheTile> tiles = canvasView.imageCache.getImageAsTiles();
544                // final BufferedImage img = canvasView.imageCache.getImage();
545
546                // draw image
547                for (ImageCacheTile tile : tiles)
548                {
549                    trans.translate(tile.rect.getX(), tile.rect.getY());
550                    g2.drawImage(tile.image, trans, null);
551                    trans.translate(-tile.rect.getX(), -tile.rect.getY());
552                }
553                // if (img != null)
554                // g2.drawImage(img, trans, null);
555
556                // then apply canvas inverse transformation
557                trans.scale(1 / getScaleX(), 1 / getScaleY());
558                trans.translate(-getOffsetX(), -getOffsetY());
559
560                final int canvasSizeX = getCanvasSizeX();
561                final int canvasSizeY = getCanvasSizeY();
562                final int canvasCenterX = canvasSizeX / 2;
563                final int canvasCenterY = canvasSizeY / 2;
564
565                trans.translate(canvasCenterX, canvasCenterY);
566                trans.rotate(-getRotationZ());
567                trans.translate(-canvasCenterX, -canvasCenterY);
568
569                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
570
571                // get transformed rectangle
572                final Shape shape = trans.createTransformedShape(new Rectangle(canvasSizeX, canvasSizeY));
573
574                // and draw canvas view rect of the image
575                // TODO : the g2.draw(shape) cost sometime !
576                g2.setStroke(new BasicStroke(3));
577                g2.setColor(Color.black);
578                g2.draw(shape);
579                g2.setStroke(new BasicStroke(2));
580                g2.setColor(Color.white);
581                g2.draw(shape);
582
583                // rotation helper
584                if (mapRotating)
585                {
586                    final Point2D center = trans.transform(new Point(canvasCenterX, canvasCenterY), null);
587                    final int centerX = (int) Math.round(center.getX());
588                    final int centerY = (int) Math.round(center.getY());
589
590                    final BasicStroke blackStr = new BasicStroke(4);
591                    final BasicStroke greenStr = new BasicStroke(2);
592
593                    g2.setStroke(blackStr);
594                    g2.setColor(Color.black);
595                    g2.drawLine(centerX - 4, centerY - 4, centerX + 4, centerY + 4);
596                    g2.drawLine(centerX - 4, centerY + 4, centerX + 4, centerY - 4);
597
598                    g2.setStroke(greenStr);
599                    g2.setColor(Color.green);
600                    g2.drawLine(centerX - 4, centerY - 4, centerX + 4, centerY + 4);
601                    g2.drawLine(centerX - 4, centerY + 4, centerX + 4, centerY - 4);
602                }
603
604                g2.dispose();
605            }
606        }
607    }
608
609    public class CanvasView extends JPanel
610            implements ActionListener, MouseWheelListener, MouseListener, MouseMotionListener
611    {
612        /**
613         * 
614         */
615        private static final long serialVersionUID = 4041355608444378172L;
616
617        public class ImageCache implements Runnable
618        {
619            public class ImageCacheTile
620            {
621                final static int TILE_SIZE = 2048;
622
623                public Rectangle rect;
624                public BufferedImage image;
625
626                public ImageCacheTile(Rectangle r, BufferedImage img)
627                {
628                    super();
629
630                    rect = new Rectangle(r);
631                    image = img;
632                }
633
634                public ImageCacheTile(Rectangle r)
635                {
636                    this(r, new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB));
637                }
638            }
639
640            /**
641             * image cache
642             */
643            private List<ImageCacheTile> tiles;
644
645            /**
646             * processor
647             */
648            private final SingleProcessor processor;
649            /**
650             * internals
651             */
652            private boolean needRebuild;
653            private boolean notEnoughMemory;
654
655            public ImageCache()
656            {
657                super();
658
659                processor = new SingleProcessor(true, "Canvas2D renderer");
660                // we want the processor to stay alive for sometime
661                processor.setKeepAliveTime(3, TimeUnit.SECONDS);
662
663                tiles = new ArrayList<ImageCacheTile>();
664                needRebuild = true;
665                notEnoughMemory = false;
666
667                // build cache
668                processor.submit(this);
669            }
670
671            public void invalidCache()
672            {
673                needRebuild = true;
674            }
675
676            public boolean isValid()
677            {
678                return !needRebuild;
679            }
680
681            public boolean isProcessing()
682            {
683                return processor.isProcessing();
684            }
685
686            public void refresh()
687            {
688                // rebuild cache
689                if (needRebuild)
690                    processor.submit(this);
691
692                // just repaint in the meantime
693                getViewComponent().repaint();
694            }
695
696            /**
697             * @deprecated Caching is done as tiles now so it's better to use {@link #getImageAsTiles()}
698             */
699            @Deprecated
700            public BufferedImage getImage()
701            {
702                // get original image
703                final IcyBufferedImage icyImage = Canvas2D.this.getImage(getPositionT(), getPositionZ(),
704                        getPositionC());
705
706                return IcyBufferedImageUtil.toBufferedImage(icyImage, null);
707            }
708
709            public List<ImageCacheTile> getImageAsTiles()
710            {
711                synchronized (tiles)
712                {
713                    // duplicate list
714                    return new ArrayList<ImageCacheTile>(tiles);
715                }
716            }
717
718            public boolean getNotEnoughMemory()
719            {
720                return notEnoughMemory;
721            }
722
723            @Override
724            public void run()
725            {
726                // important to set it to false at beginning
727                needRebuild = false;
728
729                // get original image
730                final IcyBufferedImage icyImage = Canvas2D.this.getImage(getPositionT(), getPositionZ(),
731                        getPositionC());
732
733                // clear cache so we know we don't have any image at this position
734                if (icyImage == null)
735                    tiles.clear();
736                else
737                {
738                    try
739                    {
740                        {
741                            final Rectangle imgRect = icyImage.getBounds();
742                            // get tiles list
743                            final List<Rectangle> newTiles = ImageUtil.getTileList(icyImage.getSizeX(),
744                                    icyImage.getSizeY(), ImageCacheTile.TILE_SIZE, ImageCacheTile.TILE_SIZE);
745                            final int len = newTiles.size();
746
747                            int indNewTiles = 0;
748                            // compare with previous tile list
749                            for (ImageCacheTile tile : tiles)
750                            {
751                                if (indNewTiles < len)
752                                {
753                                    final Rectangle oldRect = tile.rect;
754                                    final Rectangle newRect = newTiles.get(indNewTiles).intersection(imgRect);
755
756                                    // size changed ? --> re alloc image
757                                    if ((oldRect.width != newRect.width) || (oldRect.height != newRect.height))
758                                        tile.image = new BufferedImage(newRect.width, newRect.height,
759                                                BufferedImage.TYPE_INT_ARGB);
760                                    // adjust rect (position) if needed
761                                    tile.rect = newRect;
762                                }
763
764                                indNewTiles++;
765                            }
766
767                            // remove extras tiles
768                            while (tiles.size() > len)
769                                tiles.remove(tiles.size() - 1);
770                            // add extras tiles
771                            while (indNewTiles < len)
772                                tiles.add(new ImageCacheTile(newTiles.get(indNewTiles++)));
773
774                            // rebuild images
775                            final LUT l = getLut();
776                            for (ImageCacheTile tile : tiles)
777                                tile.image = IcyBufferedImageUtil.toBufferedImage(
778                                        IcyBufferedImageUtil.getSubImage(icyImage, tile.rect), tile.image, l);
779                        }
780
781                        notEnoughMemory = false;
782                    }
783                    catch (OutOfMemoryError e)
784                    {
785                        notEnoughMemory = true;
786                    }
787                }
788
789                // repaint now
790                getViewComponent().repaint();
791            }
792        }
793
794        /**
795         * Image cache
796         */
797        final ImageCache imageCache;
798
799        /**
800         * internals
801         */
802        final Font font;
803        private final Timer refreshTimer;
804        private final Timer zoomInfoTimer;
805        private final Timer rotationInfoTimer;
806        private final SmoothMover zoomInfoAlphaMover;
807        private final SmoothMover rotationInfoAlphaMover;
808        private String zoomMessage;
809        private String rotationMessage;
810        Dimension lastSize;
811        boolean actived;
812        boolean handlingMouseMoveEvent;
813        private Point startDragPosition;
814        private Point startOffset;
815        double curScaleX;
816        double curScaleY;
817        private double startRotationZ;
818        // private Cursor previousCursor;
819        boolean moving;
820        boolean rotating;
821        boolean hasMouseFocus;
822        boolean areaSelection;
823
824        public CanvasView()
825        {
826            super();
827
828            imageCache = new ImageCache();
829            actived = false;
830            handlingMouseMoveEvent = false;
831            startDragPosition = null;
832            startOffset = null;
833            curScaleX = -1;
834            curScaleY = -1;
835            // previousCursor = getCursor();
836            moving = false;
837            rotating = false;
838            hasMouseFocus = false;
839            areaSelection = false;
840            lastSize = getSize();
841
842            font = new Font("Arial", Font.BOLD, 16);
843
844            zoomInfoAlphaMover = new SmoothMover(0);
845            zoomInfoAlphaMover.setMoveTime(500);
846            zoomInfoAlphaMover.setUpdateDelay(20);
847            zoomInfoAlphaMover.addListener(new SmoothMoverAdapter()
848            {
849                @Override
850                public void valueChanged(SmoothMover source, double newValue, int pourcent)
851                {
852                    // just repaint
853                    repaint();
854                }
855            });
856            rotationInfoAlphaMover = new SmoothMover(0);
857            rotationInfoAlphaMover.setMoveTime(500);
858            rotationInfoAlphaMover.setUpdateDelay(20);
859            rotationInfoAlphaMover.addListener(new SmoothMoverAdapter()
860            {
861                @Override
862                public void valueChanged(SmoothMover source, double newValue, int pourcent)
863                {
864                    // just repaint
865                    repaint();
866                }
867            });
868
869            refreshTimer = new Timer(100, this);
870            refreshTimer.setRepeats(false);
871            zoomInfoTimer = new Timer(1000, this);
872            zoomInfoTimer.setRepeats(false);
873            rotationInfoTimer = new Timer(1000, this);
874            rotationInfoTimer.setRepeats(false);
875
876            addComponentListener(new ComponentAdapter()
877            {
878                @Override
879                public void componentResized(ComponentEvent e)
880                {
881                    final Dimension newSize = getSize();
882                    int extX = 0;
883                    int extY = 0;
884
885                    // first time component is displayed ?
886                    if (!actived)
887                    {
888                        // by default we adapt image to canvas size
889                        fitImageToCanvas(false);
890                        // center image (if cannot fit to canvas size)
891                        centerImage();
892                        actived = true;
893                    }
894                    else
895                    {
896                        // auto FIT enabled
897                        if (zoomFitCanvasButton.isSelected())
898                            fitImageToCanvas(true);
899                        else
900                        {
901                            // re-center
902                            final int dx = newSize.width - lastSize.width;
903                            final int dy = newSize.height - lastSize.height;
904                            final int dx2 = dx / 2;
905                            final int dy2 = dy / 2;
906                            // keep trace of lost bit
907                            extX = (2 * dx2) - dx;
908                            extY = (2 * dy2) - dy;
909
910                            setOffset((int) smoothTransform.getDestValue(TRANS_X) + dx2,
911                                    (int) smoothTransform.getDestValue(TRANS_Y) + dy2, true);
912                        }
913                    }
914
915                    // keep trace of size plus lost part
916                    lastSize.width = newSize.width + extX;
917                    lastSize.height = newSize.height + extY;
918                }
919            });
920
921            addMouseListener(this);
922            addMouseMotionListener(this);
923            addMouseWheelListener(this);
924        }
925
926        /**
927         * Release some stuff
928         */
929        void shutDown()
930        {
931            // stop timer and movers
932            refreshTimer.stop();
933            zoomInfoTimer.stop();
934            rotationInfoTimer.stop();
935            refreshTimer.removeActionListener(this);
936            zoomInfoTimer.removeActionListener(this);
937            rotationInfoTimer.removeActionListener(this);
938            zoomInfoAlphaMover.shutDown();
939            rotationInfoAlphaMover.shutDown();
940        }
941
942        /**
943         * Returns the internal {@link ImageCache} object.
944         */
945        public ImageCache getImageCache()
946        {
947            return imageCache;
948        }
949
950        protected void updateDrag(boolean control, boolean shift)
951        {
952            if (!moving)
953                return;
954
955            final Point mousePos = getMousePos();
956            final Point delta = new Point(mousePos.x - startDragPosition.x, mousePos.y - startDragPosition.y);
957
958            // shift action --> limit to one direction
959            if (shift)
960            {
961                // X drag
962                if (Math.abs(delta.x) > Math.abs(delta.y))
963                    delta.y = 0;
964                // Y drag
965                else
966                    delta.x = 0;
967            }
968
969            translate(startOffset, delta, control);
970        }
971
972        protected void translate(Point startPos, Point delta, boolean control)
973        {
974            final Point2D.Double deltaD;
975
976            // control button down
977            if (control)
978                // drag is scaled by current scales factor
979                // deltaD = canvasToImageDelta(delta.x, delta.y, 1d / getScaleX(), 1d / getScaleY(),
980                // getRotationZ());
981                deltaD = canvasToImageDelta(delta.x * 3, delta.y * 3, 1d, 1d, getRotationZ());
982            else
983                // just get rid of rotation factor
984                deltaD = canvasToImageDelta(delta.x, delta.y, 1d, 1d, getRotationZ());
985
986            // modify offset with smooth mover
987            setOffset((int) Math.round(startPos.x + deltaD.x), (int) Math.round(startPos.y + deltaD.y), true);
988        }
989
990        protected void updateRot(boolean control, boolean shift)
991        {
992            if (!rotating)
993                return;
994
995            final Point mousePos = getMousePos();
996
997            // get canvas center
998            final int canvasCenterX = getCanvasSizeX() / 2;
999            final int canvasCenterY = getCanvasSizeY() / 2;
1000
1001            // get last and current mouse position delta with center
1002            final int lastMouseDeltaPosX = startDragPosition.x - canvasCenterX;
1003            final int lastMouseDeltaPosY = startDragPosition.y - canvasCenterY;
1004            final int newMouseDeltaPosX = mousePos.x - canvasCenterX;
1005            final int newMouseDeltaPosY = mousePos.y - canvasCenterY;
1006
1007            // get angle in radian between last and current mouse position
1008            // relative to image center
1009            double newAngle = Math.atan2(newMouseDeltaPosY, newMouseDeltaPosX);
1010            double lastAngle = Math.atan2(lastMouseDeltaPosY, lastMouseDeltaPosX);
1011
1012            double angle = newAngle - lastAngle;
1013
1014            // control button down --> rotation is enforced
1015            if (control)
1016                angle *= 3;
1017
1018            final double destAngle;
1019
1020            // shift action --> limit to 45° rotation
1021            if (shift)
1022                destAngle = Math.rint((startRotationZ + angle) * (8d / (2 * Math.PI))) * ((2 * Math.PI) / 8d);
1023            else
1024                destAngle = startRotationZ + angle;
1025
1026            // modify rotation with smooth mover
1027            setRotation(destAngle, true);
1028        }
1029
1030        /**
1031         * Internal canvas process on mouseClicked event.<br>
1032         * Return true if event should be consumed.
1033         */
1034        boolean onMouseClicked(boolean consumed, int clickCount, boolean left, boolean right, boolean control)
1035        {
1036            if (!consumed)
1037            {
1038                // nothing yet
1039            }
1040
1041            return false;
1042        }
1043
1044        /**
1045         * Internal canvas process on mousePressed event.<br>
1046         * Return true if event should be consumed.
1047         */
1048        boolean onMousePressed(boolean consumed, boolean left, boolean right, boolean control)
1049        {
1050            // not yet consumed
1051            if (!consumed)
1052            {
1053                final ROITask toolTask = Icy.getMainInterface().getROIRibbonTask();
1054                final Sequence seq = getSequence();
1055
1056                // left button press ?
1057                if (left)
1058                {
1059                    // ROI tool selected --> ROI creation
1060                    if ((toolTask != null) && toolTask.isROITool())
1061                    {
1062                        // get the ROI plugin class name
1063                        final String roiClassName = toolTask.getSelected();
1064
1065                        // unselect tool before ROI creation unless
1066                        // control modifier is used for multiple ROI creation
1067                        if (!control)
1068                            Icy.getMainInterface().setSelectedTool(null);
1069
1070                        // only if sequence still live
1071                        if (seq != null)
1072                        {
1073                            // try to create ROI from current selected ROI tool
1074                            final ROI roi = ROI.create(roiClassName, getMouseImagePos5D());
1075                            // roi created ? --> it becomes the selected ROI
1076                            if (roi != null)
1077                            {
1078                                roi.setCreating(true);
1079
1080                                // attach to sequence (hacky method to avoid undoing ROI cutting)
1081                                seq.addROI(roi, !roiClassName.equals(ROILineCutterPlugin.class.getName()));
1082                                // then do exclusive selection
1083                                seq.setSelectedROI(roi);
1084                            }
1085
1086                            // consume event
1087                            return true;
1088                        }
1089                    }
1090
1091                    // start area selection
1092                    if (control)
1093                        areaSelection = true;
1094                }
1095
1096                // start drag mouse position
1097                startDragPosition = getMousePos();
1098                // store canvas parameters
1099                startOffset = new Point(getOffsetX(), getOffsetY());
1100                startRotationZ = getRotationZ();
1101
1102                // repaint
1103                refresh();
1104                updateCursor();
1105
1106                // consume event to activate drag
1107                return true;
1108            }
1109
1110            return false;
1111        }
1112
1113        /**
1114         * Internal canvas process on mouseReleased event.<br>
1115         * Return true if event should be consumed.
1116         */
1117        boolean onMouseReleased(boolean consumed, boolean left, boolean right, boolean control)
1118        {
1119            // area selection ?
1120            if (areaSelection)
1121            {
1122                final Sequence seq = getSequence();
1123
1124                if (seq != null)
1125                {
1126                    final List<ROI> rois = seq.getROIs();
1127
1128                    // we have some rois ?
1129                    if (rois.size() > 0)
1130                    {
1131                        final Rectangle2D area = canvasToImage(getAreaSelection());
1132                        // 5D area
1133                        final Rectangle5D area5d = new Rectangle5D.Double(area.getX(), area.getY(), getPositionZ(),
1134                                getPositionT(), Double.NEGATIVE_INFINITY, area.getWidth(), area.getHeight(), 1d, 1d,
1135                                Double.POSITIVE_INFINITY);
1136
1137                        seq.beginUpdate();
1138                        try
1139                        {
1140                            for (ROI roi : rois)
1141                                roi.setSelected(roi.intersects(area5d));
1142                        }
1143                        finally
1144                        {
1145                            seq.endUpdate();
1146                        }
1147                    }
1148                }
1149            }
1150
1151            // assume end dragging
1152            startDragPosition = null;
1153            moving = false;
1154            rotating = false;
1155            areaSelection = false;
1156
1157            // repaint
1158            refresh();
1159            updateCursor();
1160
1161            // consume event
1162            return true;
1163        }
1164
1165        /**
1166         * Internal canvas process on mouseMove event.<br>
1167         * Always processed, no consume here.
1168         */
1169        void onMousePositionChanged(Point pos)
1170        {
1171            handlingMouseMoveEvent = true;
1172            try
1173            {
1174                // update mouse position
1175                setMousePos(pos);
1176            }
1177            finally
1178            {
1179                handlingMouseMoveEvent = false;
1180            }
1181        }
1182
1183        /**
1184         * Internal canvas process on mouseDragged event.<br>
1185         * Return true if event should be consumed.
1186         */
1187        boolean onMouseDragged(boolean consumed, Point pos, boolean left, boolean right, boolean control, boolean shift)
1188        {
1189            if (!consumed)
1190            {
1191                // canvas get the drag event ?
1192                if (isDragging())
1193                {
1194                    // left mouse button action : translation
1195                    if (left)
1196                    {
1197                        moving = true;
1198                        if (rotating)
1199                        {
1200                            rotating = false;
1201                            // force repaint so the cross is no more visible
1202                            canvasView.repaint();
1203                        }
1204
1205                        updateDrag(control, shift);
1206                    }
1207                    // right mouse button action : rotation
1208                    else if (right)
1209                    {
1210                        moving = false;
1211                        if (!rotating)
1212                        {
1213                            rotating = true;
1214                            // force repaint so the cross is visible
1215                            canvasView.repaint();
1216                        }
1217
1218                        updateRot(control, shift);
1219                    }
1220
1221                    // dragging --> consume event
1222                    return true;
1223                }
1224                // repaint area selection
1225                else if (areaSelection)
1226                    repaint();
1227
1228                // no dragging --> no consume
1229                return false;
1230            }
1231
1232            return false;
1233        }
1234
1235        /**
1236         * Internal canvas process on mouseWheelMoved event.<br>
1237         * Return true if event should be consumed.
1238         */
1239        boolean onMouseWheelMoved(boolean consumed, int wheelRotation, boolean left, boolean right, boolean control,
1240                boolean shift)
1241        {
1242            if (!consumed)
1243            {
1244                if (!isDragging())
1245                {
1246                    // as soon we manipulate the image with mouse, we want to be focused
1247                    if (!viewer.hasFocus())
1248                        viewer.requestFocus();
1249
1250                    double sx, sy;
1251
1252                    // adjust mouse wheel depending preference
1253                    double wr = wheelRotation * CanvasPreferences.getMouseWheelSensitivity();
1254                    if (CanvasPreferences.getInvertMouseWheelAxis())
1255                        wr = -wr;
1256
1257                    sx = 1d + (wr / 100d);
1258                    sy = 1d + (wr / 100d);
1259
1260                    // if (wr > 0d)
1261                    // {
1262                    // sx = 20d / 19d;
1263                    // sy = 20d / 19d;
1264                    // }
1265                    // else
1266                    // {
1267                    // sx = 19d / 20d;
1268                    // sy = 19d / 20d;
1269                    // }
1270
1271                    // control button down --> fast zoom
1272                    if (control)
1273                    {
1274                        sx *= sx;
1275                        sy *= sy;
1276                    }
1277
1278                    // reload current value
1279                    if (curScaleX == -1)
1280                        curScaleX = smoothTransform.getDestValue(SCALE_X);
1281                    if (curScaleY == -1)
1282                        curScaleY = smoothTransform.getDestValue(SCALE_Y);
1283
1284                    curScaleX = Math.max(0.01d, Math.min(100d, curScaleX * sx));
1285                    curScaleY = Math.max(0.01d, Math.min(100d, curScaleY * sy));
1286
1287                    double newScaleX = curScaleX;
1288                    double newScaleY = curScaleY;
1289
1290                    // shift key down --> adjust to closest "round" number
1291                    if (shift)
1292                    {
1293                        newScaleX = MathUtil.closest(newScaleX, zoomRoundedFactors);
1294                        newScaleY = MathUtil.closest(newScaleY, zoomRoundedFactors);
1295                    }
1296
1297                    setScale(newScaleX, newScaleY, false, true);
1298
1299                    // consume event
1300                    return true;
1301                }
1302            }
1303
1304            // don't consume this event
1305            return false;
1306        }
1307
1308        @Override
1309        public void mouseClicked(MouseEvent e)
1310        {
1311            // send mouse event to overlays first
1312            Canvas2D.this.mouseClick(e);
1313
1314            // process
1315            if (onMouseClicked(e.isConsumed(), e.getClickCount(), EventUtil.isLeftMouseButton(e),
1316                    EventUtil.isRightMouseButton(e), EventUtil.isControlDown(e)))
1317                e.consume();
1318        }
1319
1320        @Override
1321        public void mousePressed(MouseEvent e)
1322        {
1323            // send mouse event to overlays first
1324            Canvas2D.this.mousePressed(e);
1325
1326            // process
1327            if (onMousePressed(e.isConsumed(), EventUtil.isLeftMouseButton(e), EventUtil.isRightMouseButton(e),
1328                    EventUtil.isControlDown(e)))
1329                e.consume();
1330        }
1331
1332        @Override
1333        public void mouseReleased(MouseEvent e)
1334        {
1335            // send mouse event to overlays first
1336            Canvas2D.this.mouseReleased(e);
1337
1338            // process
1339            if (onMouseReleased(e.isConsumed(), EventUtil.isLeftMouseButton(e), EventUtil.isRightMouseButton(e),
1340                    EventUtil.isControlDown(e)))
1341                e.consume();
1342        }
1343
1344        @Override
1345        public void mouseEntered(MouseEvent e)
1346        {
1347            hasMouseFocus = true;
1348
1349            // send mouse event to overlays
1350            Canvas2D.this.mouseEntered(e);
1351            // and refresh
1352            refresh();
1353        }
1354
1355        @Override
1356        public void mouseExited(MouseEvent e)
1357        {
1358            hasMouseFocus = false;
1359
1360            // send mouse event to overlays
1361            Canvas2D.this.mouseExited(e);
1362            // and refresh
1363            refresh();
1364        }
1365
1366        @Override
1367        public void mouseMoved(MouseEvent e)
1368        {
1369            // process first without consume (update mouse canvas position)
1370            onMousePositionChanged(e.getPoint());
1371
1372            // send mouse event to overlays after so mouse canvas position is ok
1373            Canvas2D.this.mouseMove(e);
1374        }
1375
1376        @Override
1377        public void mouseDragged(MouseEvent e)
1378        {
1379            // process first without consume (update mouse canvas position)
1380            onMousePositionChanged(e.getPoint());
1381
1382            // send mouse event to overlays after so mouse canvas position is ok
1383            Canvas2D.this.mouseDrag(e);
1384
1385            // process
1386            if (onMouseDragged(e.isConsumed(), e.getPoint(), EventUtil.isLeftMouseButton(e),
1387                    EventUtil.isRightMouseButton(e), EventUtil.isControlDown(e), EventUtil.isShiftDown(e)))
1388                e.consume();
1389        }
1390
1391        @Override
1392        public void mouseWheelMoved(MouseWheelEvent e)
1393        {
1394            // send mouse event to overlays
1395            Canvas2D.this.mouseWheelMoved(e);
1396
1397            // process
1398            if (onMouseWheelMoved(e.isConsumed(), e.getWheelRotation(), EventUtil.isLeftMouseButton(e),
1399                    EventUtil.isRightMouseButton(e), EventUtil.isControlDown(e), EventUtil.isShiftDown(e)))
1400                e.consume();
1401        }
1402
1403        public void keyPressed(KeyEvent e)
1404        {
1405            final boolean control = EventUtil.isControlDown(e);
1406            final boolean shift = EventUtil.isShiftDown(e);
1407
1408            // just for modifiers key state change
1409            updateDrag(control, shift);
1410            updateRot(control, shift);
1411        }
1412
1413        public void keyReleased(KeyEvent e)
1414        {
1415            final boolean control = EventUtil.isControlDown(e);
1416            final boolean shift = EventUtil.isShiftDown(e);
1417
1418            // just for modifiers key state change
1419            updateDrag(control, shift);
1420            updateRot(control, shift);
1421        }
1422
1423        /**
1424         * Draw specified image layer and others layers on specified {@link Graphics2D} object.
1425         */
1426        void drawLayer(Graphics2D g, Sequence seq, Layer layer)
1427        {
1428            if (layer.isVisible())
1429            {
1430                final float opacity = layer.getOpacity();
1431
1432                if (opacity != 1f)
1433                    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
1434                else
1435                    g.setComposite(AlphaComposite.SrcOver);
1436
1437                layer.getOverlay().paint(g, seq, Canvas2D.this);
1438            }
1439        }
1440
1441        /**
1442         * Draw specified image layer and others layers on specified {@link Graphics2D} object.
1443         */
1444        void drawImageAndLayers(Graphics2D g, Layer imageLayer)
1445        {
1446            final Sequence seq = getSequence();
1447            final Layer defaultImageLayer = getImageLayer();
1448
1449            // global layer visible switch for canvas
1450            if (isLayersVisible())
1451            {
1452                final List<Layer> layers = getLayers(true);
1453
1454                // draw them in inverse order to have first painter event at top
1455                for (int i = layers.size() - 1; i >= 0; i--)
1456                {
1457                    final Layer layer = layers.get(i);
1458
1459                    // replace the default image layer by the specified one
1460                    if (layer == defaultImageLayer)
1461                        drawLayer(g, seq, imageLayer);
1462                    else
1463                        drawLayer(g, seq, layer);
1464                }
1465            }
1466            else
1467                // display image layer only
1468                drawLayer(g, seq, imageLayer);
1469        }
1470
1471        @Override
1472        protected void paintComponent(Graphics g)
1473        {
1474            super.paintComponent(g);
1475
1476            final int w = getCanvasSizeX();
1477            final int h = getCanvasSizeY();
1478            final int canvasCenterX = w / 2;
1479            final int canvasCenterY = h / 2;
1480
1481            // background and layers
1482            {
1483                final Graphics2D g2 = (Graphics2D) g.create();
1484
1485                // background
1486                if (isBackgroundColorEnabled())
1487                {
1488                    g2.setBackground(getBackgroundColor());
1489                    g2.clearRect(0, 0, w, h);
1490                }
1491
1492                // apply filtering
1493                if (CanvasPreferences.getFiltering() && ((getScaleX() < 4d) && (getScaleY() < 4d)))
1494                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
1495                else
1496                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
1497                            RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
1498                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1499
1500                // apply transformation
1501                g2.transform(getTransform());
1502
1503                // draw image and layers
1504                drawImageAndLayers(g2, getImageLayer());
1505
1506                g2.dispose();
1507            }
1508
1509            // area selection
1510            if (areaSelection)
1511            {
1512                final Rectangle area = getAreaSelection();
1513                final Graphics2D g2 = (Graphics2D) g.create();
1514
1515                g2.setStroke(new BasicStroke(1));
1516                g2.setColor(Color.darkGray);
1517                g2.drawRect(area.x + 1, area.y + 1, area.width, area.height);
1518                g2.setColor(Color.lightGray);
1519                g2.drawRect(area.x, area.y, area.width, area.height);
1520
1521                g2.dispose();
1522            }
1523
1524            // synchronized canvas ? display external cursor
1525            if (!hasMouseFocus)
1526            {
1527                final Graphics2D g2 = (Graphics2D) g.create();
1528
1529                final Point mousePos = getMousePos();
1530                final int x = mousePos.x - (ICON_TARGET_SIZE / 2);
1531                final int y = mousePos.y - (ICON_TARGET_SIZE / 2);
1532
1533                // display cursor at mouse pos
1534                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
1535                g2.drawImage(ICON_TARGET_LIGHT, x + 1, y + 1, null);
1536                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
1537                g2.drawImage(ICON_TARGET_BLACK, x, y, null);
1538
1539                g2.dispose();
1540            }
1541
1542            // display zoom info
1543            if (zoomInfoAlphaMover.getValue() > 0)
1544            {
1545                final Graphics2D g2 = (Graphics2D) g.create();
1546
1547                g2.setFont(font);
1548                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1549                drawTextBottomRight(g2, zoomMessage, (float) zoomInfoAlphaMover.getValue());
1550
1551                g2.dispose();
1552            }
1553
1554            // display rotation info
1555            if (rotationInfoAlphaMover.getValue() > 0)
1556            {
1557                final Graphics2D g2 = (Graphics2D) g.create();
1558
1559                g2.setFont(font);
1560                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1561                drawTextTopRight(g2, rotationMessage, (float) rotationInfoAlphaMover.getValue());
1562
1563                g2.dispose();
1564            }
1565
1566            // rotation helper
1567            if (rotating)
1568            {
1569                final Graphics2D g2 = (Graphics2D) g.create();
1570
1571                final BasicStroke blackStr = new BasicStroke(5);
1572                final BasicStroke greenStr = new BasicStroke(3);
1573
1574                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1575
1576                g2.setStroke(blackStr);
1577                g2.setColor(Color.black);
1578                g2.drawLine(canvasCenterX - 5, canvasCenterY - 5, canvasCenterX + 5, canvasCenterY + 5);
1579                g2.drawLine(canvasCenterX - 5, canvasCenterY + 5, canvasCenterX + 5, canvasCenterY - 5);
1580
1581                g2.setStroke(greenStr);
1582                g2.setColor(Color.green);
1583                g2.drawLine(canvasCenterX - 5, canvasCenterY - 5, canvasCenterX + 5, canvasCenterY + 5);
1584                g2.drawLine(canvasCenterX - 5, canvasCenterY + 5, canvasCenterX + 5, canvasCenterY - 5);
1585
1586                g2.dispose();
1587            }
1588
1589            // image or layers changed during repaint --> refresh again
1590            if (!isCacheValid())
1591                refresh();
1592            // cache is being rebuild --> refresh to show progression
1593            else if (imageCache.isProcessing())
1594                refreshLater(100);
1595
1596            // repaint minimap to reflect change (simplest way to refresh minimap)
1597            canvasMap.repaint();
1598        }
1599
1600        public void drawTextBottomRight(Graphics2D g, String text, float alpha)
1601        {
1602            final Rectangle2D rect = GraphicsUtil.getStringBounds(g, text);
1603            final int w = (int) rect.getWidth();
1604            final int h = (int) rect.getHeight();
1605            final int x = getWidth() - (w + 8 + 2);
1606            final int y = getHeight() - (h + 8 + 2);
1607
1608            g.setColor(Color.gray);
1609            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
1610            g.fillRoundRect(x, y, w + 8, h + 8, 8, 8);
1611
1612            g.setColor(Color.white);
1613            g.drawString(text, x + 4, y + 2 + h);
1614        }
1615
1616        public void drawTextTopRight(Graphics2D g, String text, float alpha)
1617        {
1618            final Rectangle2D rect = GraphicsUtil.getStringBounds(g, text);
1619            final int w = (int) rect.getWidth();
1620            final int h = (int) rect.getHeight();
1621            final int x = getWidth() - (w + 8 + 2);
1622            final int y = 2;
1623
1624            g.setColor(Color.gray);
1625            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
1626            g.fillRoundRect(x, y, w + 8, h + 8, 8, 8);
1627
1628            g.setColor(Color.white);
1629            g.drawString(text, x + 4, y + 2 + h);
1630        }
1631
1632        public void drawTextCenter(Graphics2D g, String text, float alpha)
1633        {
1634            final Rectangle2D rect = GraphicsUtil.getStringBounds(g, text);
1635            final int w = (int) rect.getWidth();
1636            final int h = (int) rect.getHeight();
1637            final int x = (getWidth() - (w + 8 + 2)) / 2;
1638            final int y = (getHeight() - (h + 8 + 2)) / 2;
1639
1640            g.setColor(Color.gray);
1641            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
1642            g.fillRoundRect(x, y, w + 8, h + 8, 8, 8);
1643
1644            g.setColor(Color.white);
1645            g.drawString(text, x + 4, y + 2 + h);
1646        }
1647
1648        /**
1649         * Update mouse cursor
1650         */
1651        protected void updateCursor()
1652        {
1653            // final Cursor cursor = getCursor();
1654            //
1655            // // save previous cursor if different from HAND
1656            // if (cursor.getType() != Cursor.HAND_CURSOR)
1657            // previousCursor = cursor;
1658            //
1659            if (isDragging())
1660            {
1661                GuiUtil.setCursor(this, Cursor.HAND_CURSOR);
1662                return;
1663            }
1664
1665            if (areaSelection)
1666            {
1667                GuiUtil.setCursor(this, Cursor.CROSSHAIR_CURSOR);
1668                return;
1669            }
1670
1671            final Sequence seq = getSequence();
1672
1673            if (seq != null)
1674            {
1675                final ROI overlappedRoi = seq.getFocusedROI();
1676
1677                // overlapping an ROI ?
1678                if (overlappedRoi != null)
1679                {
1680                    final Layer layer = getLayer(overlappedRoi);
1681
1682                    if ((layer != null) && layer.isVisible())
1683                    {
1684                        GuiUtil.setCursor(this, Cursor.HAND_CURSOR);
1685                        return;
1686                    }
1687                }
1688
1689                final List<ROI> selectedRois = seq.getSelectedROIs();
1690
1691                // search if we are overriding ROI control points
1692                for (ROI selectedRoi : selectedRois)
1693                {
1694                    final Layer layer = getLayer(selectedRoi);
1695
1696                    if ((layer != null) && layer.isVisible() && selectedRoi.hasSelectedPoint())
1697                    {
1698                        GuiUtil.setCursor(this, Cursor.HAND_CURSOR);
1699                        return;
1700                    }
1701                }
1702            }
1703
1704            // setCursor(previousCursor);
1705            GuiUtil.setCursor(this, Cursor.DEFAULT_CURSOR);
1706        }
1707
1708        public void refresh()
1709        {
1710            imageCache.refresh();
1711        }
1712
1713        /**
1714         * Refresh in sometime
1715         */
1716        public void refreshLater(int milli)
1717        {
1718            refreshTimer.setInitialDelay(milli);
1719            refreshTimer.start();
1720        }
1721
1722        /**
1723         * Display zoom message for the specified amount of time (in ms)
1724         */
1725        public void setZoomMessage(String value, int delay)
1726        {
1727            zoomMessage = value;
1728
1729            if (StringUtil.isEmpty(value))
1730            {
1731                zoomInfoTimer.stop();
1732                zoomInfoAlphaMover.setValue(0d);
1733            }
1734            else
1735            {
1736                zoomInfoAlphaMover.setValue(0.8d);
1737                zoomInfoTimer.setInitialDelay(delay);
1738                zoomInfoTimer.restart();
1739            }
1740        }
1741
1742        /**
1743         * Display rotation message for the specified amount of time (in ms)
1744         */
1745        public void setRotationMessage(String value, int delay)
1746        {
1747            rotationMessage = value;
1748
1749            if (StringUtil.isEmpty(value))
1750            {
1751                rotationInfoTimer.stop();
1752                rotationInfoAlphaMover.setValue(0d);
1753            }
1754            else
1755            {
1756                rotationInfoAlphaMover.setValue(0.8d);
1757                rotationInfoTimer.setInitialDelay(delay);
1758                rotationInfoTimer.restart();
1759            }
1760        }
1761
1762        public void imageChanged()
1763        {
1764            imageCache.invalidCache();
1765        }
1766
1767        public void layersChanged()
1768        {
1769            // nothing here
1770        }
1771
1772        public boolean isDragging()
1773        {
1774            return !areaSelection && (startDragPosition != null);
1775        }
1776
1777        public boolean isCacheValid()
1778        {
1779            return imageCache.isValid();
1780        }
1781
1782        /**
1783         * Returns the current Rectangle region of the area selection.<br>
1784         * It returns <code>null</code> if we are not in area selection mode
1785         */
1786        public Rectangle getAreaSelection()
1787        {
1788            if (!areaSelection)
1789                return null;
1790
1791            final int x, y;
1792            final int w, h;
1793            final Point mp = getMousePos();
1794
1795            if (mp.x > startDragPosition.x)
1796            {
1797                x = startDragPosition.x;
1798                w = mp.x - x;
1799            }
1800            else
1801            {
1802                x = mp.x;
1803                w = startDragPosition.x - x;
1804            }
1805            if (mp.y > startDragPosition.y)
1806            {
1807                y = startDragPosition.y;
1808                h = mp.y - y;
1809            }
1810            else
1811            {
1812                y = mp.y;
1813                h = startDragPosition.y - y;
1814            }
1815
1816            return new Rectangle(x, y, w, h);
1817        }
1818
1819        @Override
1820        public void actionPerformed(ActionEvent e)
1821        {
1822            final Object source = e.getSource();
1823
1824            if (source == refreshTimer)
1825                refresh();
1826            else if (source == zoomInfoTimer)
1827                zoomInfoAlphaMover.moveTo(0);
1828            else if (source == rotationInfoTimer)
1829                rotationInfoAlphaMover.moveTo(0);
1830        }
1831    }
1832
1833    /**
1834     * * index 0 : translation X (int) index 1 : translation Y (int) index 2 :
1835     * scale X (double) index 3 : scale Y (double) index 4 : rotation angle
1836     * (double)
1837     * 
1838     * @author Stephane
1839     */
1840    static class Canvas2DSmoothMover extends MultiSmoothMover
1841    {
1842        public Canvas2DSmoothMover(int size, SmoothMoveType type)
1843        {
1844            super(size, type);
1845        }
1846
1847        public Canvas2DSmoothMover(int size)
1848        {
1849            super(size);
1850        }
1851
1852        @Override
1853        public void moveTo(int index, double value)
1854        {
1855            final double v;
1856
1857            // format value for radian 0..2PI range
1858            if (index == ROT)
1859                v = MathUtil.formatRadianAngle(value);
1860            else
1861                v = value;
1862
1863            if (destValues[index] != v)
1864            {
1865                destValues[index] = v;
1866                // start movement
1867                start(index, System.currentTimeMillis());
1868            }
1869        }
1870
1871        @Override
1872        public void moveTo(double[] values)
1873        {
1874            final int maxInd = Math.min(values.length, destValues.length);
1875
1876            // first we check we have at least one value which had changed
1877            boolean changed = false;
1878            for (int index = 0; index < maxInd; index++)
1879            {
1880                final double value;
1881
1882                // format value for radian 0..2PI range
1883                if (index == ROT)
1884                    value = MathUtil.formatRadianAngle(values[index]);
1885                else
1886                    value = values[index];
1887
1888                if (destValues[index] != value)
1889                {
1890                    changed = true;
1891                    break;
1892                }
1893            }
1894
1895            // value changed ?
1896            if (changed)
1897            {
1898                // better synchronization for multiple changes
1899                final long time = System.currentTimeMillis();
1900
1901                for (int index = 0; index < maxInd; index++)
1902                {
1903                    final double value;
1904
1905                    // format value for radian 0..2PI range
1906                    if (index == ROT)
1907                        value = MathUtil.formatRadianAngle(values[index]);
1908                    else
1909                        value = values[index];
1910
1911                    destValues[index] = value;
1912                    // start movement
1913                    start(index, time);
1914                }
1915            }
1916        }
1917
1918        @Override
1919        public void setValue(int index, double value)
1920        {
1921            final double v;
1922
1923            // format value for radian 0..2PI range
1924            if (index == ROT)
1925                v = MathUtil.formatRadianAngle(value);
1926            else
1927                v = value;
1928
1929            // stop current movement
1930            stop(index);
1931            // directly set value
1932            destValues[index] = v;
1933            setCurrentValue(index, v, 100);
1934        }
1935
1936        @Override
1937        public void setValues(double[] values)
1938        {
1939            final int maxInd = Math.min(values.length, destValues.length);
1940
1941            for (int index = 0; index < maxInd; index++)
1942            {
1943                final double value;
1944
1945                // format value for radian 0..2PI range
1946                if (index == ROT)
1947                    value = MathUtil.formatRadianAngle(values[index]);
1948                else
1949                    value = values[index];
1950
1951                // stop current movement
1952                stop(index);
1953                // directly set value
1954                destValues[index] = value;
1955                setCurrentValue(index, value, 100);
1956            }
1957        }
1958
1959        @Override
1960        protected void setCurrentValue(int index, double value, int pourcent)
1961        {
1962            final double v;
1963
1964            // format value for radian 0..2PI range
1965            if (index == ROT)
1966                v = MathUtil.formatRadianAngle(value);
1967            else
1968                v = value;
1969
1970            if (currentValues[index] != v)
1971            {
1972                currentValues[index] = v;
1973                // notify value changed
1974                changed(index, v, pourcent);
1975            }
1976        }
1977
1978        @Override
1979        protected void start(int index, long time)
1980        {
1981            final double current = currentValues[index];
1982            final double dest;
1983
1984            if (index == ROT)
1985            {
1986                double d = destValues[index];
1987
1988                // choose shorter path
1989                if (Math.abs(d - current) > Math.PI)
1990                {
1991                    if (d > Math.PI)
1992                        dest = d - (Math.PI * 2);
1993                    else
1994                        dest = d + (Math.PI * 2);
1995                }
1996                else
1997                    dest = d;
1998            }
1999            else
2000                dest = destValues[index];
2001
2002            // number of step to reach final value
2003            final int size = Math.max(moveTime / getUpdateDelay(), 1);
2004
2005            // calculate interpolation
2006            switch (type)
2007            {
2008                case NONE:
2009                    stepValues[index] = new double[2];
2010                    stepValues[index][0] = current;
2011                    stepValues[index][1] = dest;
2012                    break;
2013
2014                case LINEAR:
2015                    stepValues[index] = Interpolator.doLinearInterpolation(current, dest, size);
2016                    break;
2017
2018                case LOG:
2019                    stepValues[index] = Interpolator.doLogInterpolation(current, dest, size);
2020                    break;
2021
2022                case EXP:
2023                    stepValues[index] = Interpolator.doExpInterpolation(current, dest, size);
2024                    break;
2025            }
2026
2027            // notify and start
2028            if (!isMoving(index))
2029            {
2030                moveStarted(index, time);
2031                moving[index] = true;
2032            }
2033            else
2034                moveModified(index, time);
2035        }
2036    }
2037
2038    /**
2039     * pref ID
2040     */
2041    static final String PREF_CANVAS2D_ID = "Canvas2D";
2042
2043    static final String ID_FIT_CANVAS = "fitCanvas";
2044    static final String ID_BG_COLOR_ENABLED = "bgColorEnabled";
2045    static final String ID_BG_COLOR = "bgColor";
2046
2047    final static int TRANS_X = 0;
2048    final static int TRANS_Y = 1;
2049    final static int SCALE_X = 2;
2050    final static int SCALE_Y = 3;
2051    final static int ROT = 4;
2052
2053    /**
2054     * view where we draw
2055     */
2056    final CanvasView canvasView;
2057
2058    /**
2059     * minimap in canvas panel
2060     */
2061    final CanvasMap canvasMap;
2062
2063    /**
2064     * GUI & setting
2065     */
2066    IcyToggleButton zoomFitCanvasButton;
2067    Color bgColor;
2068
2069    /**
2070     * preferences
2071     */
2072    final XMLPreferences preferences;
2073
2074    /**
2075     * The smoothTransform object contains all transform informations<br>
2076     */
2077    final Canvas2DSmoothMover smoothTransform;
2078
2079    // internal
2080    String textInfos;
2081    Dimension previousImageSize;
2082    boolean modifyingZoom;
2083    boolean modifyingRotation;
2084
2085    public Canvas2D(Viewer viewer)
2086    {
2087        super(viewer);
2088
2089        // all channel visible at once
2090        posC = -1;
2091
2092        // view panel
2093        canvasView = new CanvasView();
2094        // mini map
2095        canvasMap = new CanvasMap();
2096
2097        // variables initialization
2098        preferences = CanvasPreferences.getPreferences().node(PREF_CANVAS2D_ID);
2099
2100        // init transform (5 values, log transition type)
2101        smoothTransform = new Canvas2DSmoothMover(5, SmoothMoveType.LOG);
2102        // initials transform values
2103        smoothTransform.setValues(new double[] {0d, 0d, 1d, 1d, 0d});
2104        textInfos = null;
2105        modifyingZoom = false;
2106        modifyingRotation = false;
2107        previousImageSize = new Dimension(getImageSizeX(), getImageSizeY());
2108
2109        smoothTransform.addListener(new MultiSmoothMoverAdapter()
2110        {
2111            @Override
2112            public void valueChanged(MultiSmoothMover source, int index, double newValue, int pourcent)
2113            {
2114                // notify canvas transformation has changed
2115                switch (index)
2116                {
2117                    case TRANS_X:
2118                        offsetChanged(DimensionId.X);
2119                        break;
2120
2121                    case TRANS_Y:
2122                        offsetChanged(DimensionId.Y);
2123                        break;
2124
2125                    case SCALE_X:
2126                        scaleChanged(DimensionId.X);
2127                        break;
2128
2129                    case SCALE_Y:
2130                        scaleChanged(DimensionId.Y);
2131                        break;
2132
2133                    case ROT:
2134                        rotationChanged(DimensionId.Z);
2135                        break;
2136                }
2137            }
2138
2139            @Override
2140            public void moveEnded(MultiSmoothMover source, int index, double value)
2141            {
2142                // scale move ended, we can fix notify canvas transformation has changed
2143                switch (index)
2144                {
2145                    case SCALE_X:
2146                        canvasView.curScaleX = -1;
2147                        break;
2148
2149                    case SCALE_Y:
2150                        canvasView.curScaleY = -1;
2151                }
2152            }
2153        });
2154
2155        // want fast transition
2156        smoothTransform.setMoveTime(400);
2157        // and very smooth refresh if possible
2158        smoothTransform.setUpdateDelay(20);
2159
2160        // build inspector canvas panel & GUI stuff
2161        buildSettingGUI();
2162
2163        // set view in center
2164        add(canvasView, BorderLayout.CENTER);
2165
2166        // mouse infos panel setting: we want to see values for X/Y only (2D view)
2167        mouseInfPanel.setInfoXVisible(true);
2168        mouseInfPanel.setInfoYVisible(true);
2169        // Z and T values are already visible in Z/T navigator bar
2170        mouseInfPanel.setInfoZVisible(false);
2171        mouseInfPanel.setInfoTVisible(false);
2172        // no C navigation with this canvas (all channels visible)
2173        mouseInfPanel.setInfoCVisible(false);
2174        // data and color information visible
2175        mouseInfPanel.setInfoDataVisible(true);
2176        mouseInfPanel.setInfoColorVisible(true);
2177
2178        updateZNav();
2179        updateTNav();
2180
2181        final ROITask trt = Icy.getMainInterface().getROIRibbonTask();
2182        if (trt != null)
2183            trt.addListener(this);
2184    }
2185
2186    @Override
2187    public void shutDown()
2188    {
2189        super.shutDown();
2190
2191        canvasView.shutDown();
2192
2193        // shutdown mover object (else internal timer keep a reference to Canvas2D)
2194        smoothTransform.shutDown();
2195
2196        final ROITask trt = Icy.getMainInterface().getROIRibbonTask();
2197        if (trt != null)
2198            trt.removeListener(this);
2199    }
2200
2201    @Override
2202    protected Overlay createImageOverlay()
2203    {
2204        return new Canvas2DImageOverlay();
2205    }
2206
2207    public Canvas2DSettingPanel getCanvasSettingPanel()
2208    {
2209        return (Canvas2DSettingPanel) panel;
2210    }
2211
2212    /**
2213     * Build canvas panel for inspector
2214     */
2215    private void buildSettingGUI()
2216    {
2217        // canvas setting panel (for inspector)
2218        panel = new Canvas2DSettingPanel(this);
2219        // add the map to it
2220        panel.add(canvasMap, BorderLayout.CENTER);
2221
2222        // fit canvas toggle
2223        zoomFitCanvasButton = new IcyToggleButton(new IcyIcon(ICON_FIT_CANVAS));
2224        zoomFitCanvasButton.setSelected(preferences.getBoolean(ID_FIT_CANVAS, false));
2225        zoomFitCanvasButton.setFocusable(false);
2226        zoomFitCanvasButton.setToolTipText("Keep image fitting to window size");
2227        zoomFitCanvasButton.addActionListener(new ActionListener()
2228        {
2229            @Override
2230            public void actionPerformed(ActionEvent e)
2231            {
2232                final boolean selected = zoomFitCanvasButton.isSelected();
2233
2234                preferences.putBoolean(ID_FIT_CANVAS, selected);
2235
2236                // fit if enabled
2237                if (selected)
2238                    fitImageToCanvas(true);
2239            }
2240        });
2241    }
2242
2243    @Override
2244    public Component getViewComponent()
2245    {
2246        return canvasView;
2247    }
2248
2249    /**
2250     * Return the {@link CanvasView} component of Canvas2D.
2251     */
2252    public CanvasView getCanvasView()
2253    {
2254        return canvasView;
2255    }
2256
2257    /**
2258     * Return the {@link CanvasMap} component of Canvas2D.
2259     */
2260    public CanvasMap getCanvasMap()
2261    {
2262        return canvasMap;
2263    }
2264
2265    @Override
2266    public void customizeToolbar(JToolBar toolBar)
2267    {
2268        toolBar.addSeparator();
2269        toolBar.add(zoomFitCanvasButton);
2270        // toolBar.addSeparator();
2271        // toolBar.add(zoomFitImageButton);
2272        // toolBar.add(centerImageButton);
2273    }
2274
2275    @Override
2276    public void fitImageToCanvas()
2277    {
2278        fitImageToCanvas(false);
2279    }
2280
2281    /**
2282     * Change zoom so image fit in canvas view dimension
2283     */
2284    public void fitImageToCanvas(boolean smooth)
2285    {
2286        // search best ratio
2287        final Point2D.Double s = getFitImageToCanvasScale();
2288
2289        if (s != null)
2290        {
2291            final double scale = Math.min(s.x, s.y);
2292
2293            // set mouse position on image center
2294            centerMouseOnImage();
2295            // apply scale
2296            setScale(scale, scale, true, smooth);
2297        }
2298    }
2299
2300    @Override
2301    public void fitCanvasToImage()
2302    {
2303        // center image first
2304        centerImage();
2305
2306        super.fitCanvasToImage();
2307    }
2308
2309    @Override
2310    public void centerOnImage(double x, double y)
2311    {
2312        // get point on canvas
2313        final Point pt = imageToCanvas(x, y);
2314        final int canvasCenterX = getCanvasSizeX() / 2;
2315        final int canvasCenterY = getCanvasSizeY() / 2;
2316
2317        final Point2D.Double newTrans = canvasToImageDelta(canvasCenterX - pt.x, canvasCenterY - pt.y, 1d, 1d,
2318                getRotationZ());
2319
2320        setOffset((int) (smoothTransform.getDestValue(TRANS_X) + Math.round(newTrans.x)),
2321                (int) (smoothTransform.getDestValue(TRANS_Y) + Math.round(newTrans.y)), false);
2322    }
2323
2324    /**
2325     * Set mouse position on image center
2326     */
2327    protected void centerMouseOnImage()
2328    {
2329        setMouseImagePos(getImageSizeX() / 2, getImageSizeY() / 2);
2330    }
2331
2332    /**
2333     * Set mouse position on current view center
2334     */
2335    protected void centerMouseOnView()
2336    {
2337        setMousePos(getCanvasSizeX() >> 1, getCanvasSizeY() >> 1);
2338    }
2339
2340    @Override
2341    public void centerOn(Rectangle region)
2342    {
2343        final Rectangle2D imageRectMax = Rectangle2DUtil
2344                .getScaledRectangle(new Rectangle(getImageSizeX(), getImageSizeY()), 1.5d, true);
2345
2346        Rectangle2D adjusted = Rectangle2DUtil.getScaledRectangle(region, 2d, true);
2347
2348        // get undersize
2349        double wu = Math.max(0, 100d - adjusted.getWidth());
2350        double hu = Math.max(0, 100d - adjusted.getHeight());
2351
2352        // enlarge a bit to have at least a 100x100 rectangle
2353        if ((wu > 0) || (hu > 0))
2354            ShapeUtil.enlarge(adjusted, wu, hu, true);
2355
2356        // get overflow on original image size
2357        double wo = Math.max(0, adjusted.getWidth() - imageRectMax.getWidth());
2358        double ho = Math.max(0, adjusted.getHeight() - imageRectMax.getHeight());
2359
2360        // reduce a bit to clip on max image size
2361        if ((wo > 0) || (ho > 0))
2362            ShapeUtil.enlarge(adjusted, -wo, -ho, true);
2363
2364        final Rectangle viewRect = new Rectangle(getViewComponent().getSize());
2365
2366        // calculate new scale factors
2367        final double scaleX = viewRect.width / adjusted.getWidth();
2368        final double scaleY = viewRect.height / adjusted.getHeight();
2369
2370        // get point on canvas
2371        final int offX;
2372        final int offY;
2373        final double newScale;
2374
2375        if (scaleX < scaleY)
2376        {
2377            newScale = scaleX;
2378            // use scale X, adapt offset Y
2379            offX = (int) (adjusted.getX() * newScale);
2380            offY = (int) ((adjusted.getY() * newScale) - ((viewRect.height - (adjusted.getHeight() * newScale)) / 2d));
2381        }
2382        else
2383        {
2384            newScale = scaleY;
2385            // use scale Y, adapt offset X
2386            offX = (int) ((adjusted.getX() * newScale) - ((viewRect.width - (adjusted.getWidth() * newScale)) / 2d));
2387            offY = (int) (adjusted.getY() * newScale);
2388        }
2389
2390        // apply new position and scaling
2391        setTransform(-offX, -offY, newScale, newScale, smoothTransform.getDestValue(ROT), true);
2392    }
2393
2394    /**
2395     * Set transform
2396     */
2397    protected void setTransform(int tx, int ty, double sx, double sy, double rot, boolean smooth)
2398    {
2399        final double[] values = new double[] {tx, ty, sx, sy, rot};
2400
2401        // modify all at once for synchronized change events
2402        if (smooth)
2403            smoothTransform.moveTo(values);
2404        else
2405            smoothTransform.setValues(values);
2406    }
2407
2408    /**
2409     * Set offset X and Y.<br>
2410     * 
2411     * @param smooth
2412     *        use smooth transition
2413     */
2414    public void setOffset(int x, int y, boolean smooth)
2415    {
2416        final int adjX = Math.min(getMaxOffsetX(), Math.max(getMinOffsetX(), x));
2417        final int adjY = Math.min(getMaxOffsetY(), Math.max(getMinOffsetY(), y));
2418
2419        setTransform(adjX, adjY, smoothTransform.getDestValue(SCALE_X), smoothTransform.getDestValue(SCALE_Y),
2420                smoothTransform.getDestValue(ROT), smooth);
2421    }
2422
2423    /**
2424     * Set zoom factor (this use the smart zoom position and smooth transition).
2425     * 
2426     * @param center
2427     *        if true then zoom is centered to current view else zoom is
2428     *        centered using current mouse position
2429     * @param smooth
2430     *        use smooth transition
2431     */
2432    public void setScale(double factor, boolean center, boolean smooth)
2433    {
2434        // first we center mouse position if requested
2435        if (center)
2436            centerMouseOnImage();
2437
2438        setScale(factor, factor, true, smooth);
2439    }
2440
2441    /**
2442     * Set zoom X and Y factor.<br>
2443     * This use the smart zoom position and smooth transition.
2444     * 
2445     * @param mouseCentered
2446     *        if true the current mouse image position will becomes the
2447     *        center of viewport else the current mouse image position will
2448     *        keep its place.
2449     * @param smooth
2450     *        use smooth transition
2451     */
2452    public void setScale(double x, double y, boolean mouseCentered, boolean smooth)
2453    {
2454        final Sequence seq = getSequence();
2455        // there is no way of changing scale if no sequence
2456        if (seq == null)
2457            return;
2458
2459        // get destination rot
2460        final double rot = smoothTransform.getDestValue(ROT);
2461        // limit min and max zoom ratio
2462        final double newScaleX = Math.max(0.01d, Math.min(100d, x));
2463        final double newScaleY = Math.max(0.01d, Math.min(100d, y));
2464
2465        // get new mouse position on canvas pixel
2466        final Point newMouseCanvasPos = imageToCanvas(mouseImagePos.x, mouseImagePos.y, 0, 0, newScaleX, newScaleY,
2467                rot);
2468        // new image size
2469        final int newImgSizeX = (int) Math.ceil(getImageSizeX() * newScaleX);
2470        final int newImgSizeY = (int) Math.ceil(getImageSizeY() * newScaleY);
2471        // canvas center
2472        final int canvasCenterX = getCanvasSizeX() / 2;
2473        final int canvasCenterY = getCanvasSizeY() / 2;
2474
2475        final Point2D.Double newTrans;
2476
2477        if (mouseCentered)
2478        {
2479            // we want the mouse image point to becomes the canvas center (take rotation in account)
2480            newTrans = canvasToImageDelta(canvasCenterX - newMouseCanvasPos.x, canvasCenterY - newMouseCanvasPos.y, 1d,
2481                    1d, rot);
2482        }
2483        else
2484        {
2485            final Point mousePos = getMousePos();
2486            // we want the mouse image point to keep its place (take rotation in account)
2487            newTrans = canvasToImageDelta(mousePos.x - newMouseCanvasPos.x, mousePos.y - newMouseCanvasPos.y, 1d, 1d,
2488                    rot);
2489        }
2490
2491        // limit translation to min / max offset
2492        final int newTransX = Math.min(canvasCenterX,
2493                Math.max(canvasCenterX - newImgSizeX, (int) Math.round(newTrans.x)));
2494        final int newTransY = Math.min(canvasCenterY,
2495                Math.max(canvasCenterY - newImgSizeY, (int) Math.round(newTrans.y)));
2496
2497        setTransform(newTransX, newTransY, newScaleX, newScaleY, rot, smooth);
2498    }
2499
2500    /**
2501     * Set zoom X and Y factor.<br>
2502     * This is direct affectation method without position modification.
2503     * 
2504     * @param smooth
2505     *        use smooth transition
2506     */
2507    public void setScale(double x, double y, boolean smooth)
2508    {
2509        setTransform((int) smoothTransform.getDestValue(TRANS_X), (int) smoothTransform.getDestValue(TRANS_Y), x, y,
2510                smoothTransform.getDestValue(ROT), smooth);
2511    }
2512
2513    /**
2514     * Set zoom factor.<br>
2515     * Only here for backward compatibility with ICY4IJ.<br>
2516     * Zoom is center on image.
2517     * 
2518     * @deprecated use setScale(...) instead
2519     */
2520    @Deprecated
2521    public void setZoom(float zoom)
2522    {
2523        // set mouse position on image center
2524        centerMouseOnImage();
2525        // then apply zoom
2526        setScale(zoom, zoom, true, false);
2527    }
2528
2529    /**
2530     * Get destination image size X in canvas pixel coordinate
2531     */
2532    public int getDestImageCanvasSizeX()
2533    {
2534        return (int) Math.ceil(getImageSizeX() * smoothTransform.getDestValue(SCALE_X));
2535    }
2536
2537    /**
2538     * Get destination image size Y in canvas pixel coordinate
2539     */
2540    public int getDestImageCanvasSizeY()
2541    {
2542        return (int) Math.ceil(getImageSizeY() * smoothTransform.getDestValue(SCALE_Y));
2543    }
2544
2545    void backgroundColorEnabledChanged()
2546    {
2547        // save to preference
2548        preferences.putBoolean(ID_BG_COLOR_ENABLED, isBackgroundColorEnabled());
2549        // and refresh view
2550        canvasView.refresh();
2551    }
2552
2553    void backgroundColorChanged()
2554    {
2555        // save to preference
2556        preferences.putInt(ID_BG_COLOR, getBackgroundColor().getRGB());
2557        // and refresh view
2558        canvasView.refresh();
2559    }
2560
2561    /**
2562     * Returns the background color enabled state
2563     */
2564    public boolean isBackgroundColorEnabled()
2565    {
2566        return getCanvasSettingPanel().isBackgroundColorEnabled();
2567    }
2568
2569    /**
2570     * Sets the background color enabled state
2571     */
2572    public void setBackgroundColorEnabled(boolean value)
2573    {
2574        getCanvasSettingPanel().setBackgroundColorEnabled(value);
2575    }
2576
2577    /**
2578     * Returns the background color
2579     */
2580    public Color getBackgroundColor()
2581    {
2582        return getCanvasSettingPanel().getBackgroundColor();
2583    }
2584
2585    /**
2586     * Sets the background color
2587     */
2588    public void setBackgroundColor(Color color)
2589    {
2590        getCanvasSettingPanel().setBackgroundColor(color);
2591    }
2592
2593    /**
2594     * @return the automatic 'fit to canvas' state
2595     */
2596    public boolean getFitToCanvas()
2597    {
2598        if (zoomFitCanvasButton != null)
2599            return zoomFitCanvasButton.isSelected();
2600
2601        return false;
2602    }
2603
2604    /**
2605     * Sets the automatic 'fit to canvas' state
2606     */
2607    public void setFitToCanvas(boolean value)
2608    {
2609        if (zoomFitCanvasButton != null)
2610            zoomFitCanvasButton.setSelected(value);
2611    }
2612
2613    @Override
2614    public boolean isSynchronizationSupported()
2615    {
2616        return true;
2617    }
2618
2619    protected int getMinOffsetX()
2620    {
2621        return (getCanvasSizeX() / 2) - getDestImageCanvasSizeX();
2622    }
2623
2624    protected int getMaxOffsetX()
2625    {
2626        return (getCanvasSizeX() / 2);
2627    }
2628
2629    protected int getMinOffsetY()
2630    {
2631        return (getCanvasSizeY() / 2) - getDestImageCanvasSizeY();
2632    }
2633
2634    protected int getMaxOffsetY()
2635    {
2636        return (getCanvasSizeY() / 2);
2637    }
2638
2639    @Override
2640    public int getOffsetX()
2641    {
2642        // can be called before constructor ended
2643        if (smoothTransform == null)
2644            return 0;
2645
2646        return (int) smoothTransform.getValue(TRANS_X);
2647    }
2648
2649    @Override
2650    public int getOffsetY()
2651    {
2652        // can be called before constructor ended
2653        if (smoothTransform == null)
2654            return 0;
2655
2656        return (int) smoothTransform.getValue(TRANS_Y);
2657    }
2658
2659    @Override
2660    public double getScaleX()
2661    {
2662        // can be called before constructor ended
2663        if (smoothTransform == null)
2664            return 0d;
2665
2666        return smoothTransform.getValue(SCALE_X);
2667    }
2668
2669    @Override
2670    public double getScaleY()
2671    {
2672        // can be called before constructor ended
2673        if (smoothTransform == null)
2674            return 0d;
2675
2676        return smoothTransform.getValue(SCALE_Y);
2677    }
2678
2679    @Override
2680    public double getRotationZ()
2681    {
2682        // can be called before constructor ended
2683        if (smoothTransform == null)
2684            return 0d;
2685
2686        return smoothTransform.getValue(ROT);
2687    }
2688
2689    /**
2690     * Only here for backward compatibility with ICY4IJ plugin.
2691     * 
2692     * @deprecated use getScaleX() or getScaleY() instead
2693     */
2694    @Deprecated
2695    public double getZoomFactor()
2696    {
2697        return getScaleX();
2698    }
2699
2700    /**
2701     * We want angle to be in [0..2*PI]
2702     */
2703    public double getRotation()
2704    {
2705        return MathUtil.formatRadianAngle(getRotationZ());
2706    }
2707
2708    @Override
2709    protected void setPositionCInternal(int c)
2710    {
2711        // not supported in this canvas, C should stay at -1
2712    }
2713
2714    @Override
2715    protected void setOffsetXInternal(int value)
2716    {
2717        // this will automatically call the offsetChanged() event
2718        smoothTransform.setValue(TRANS_X, Math.min(getMaxOffsetX(), Math.max(getMinOffsetX(), value)));
2719    }
2720
2721    @Override
2722    protected void setOffsetYInternal(int value)
2723    {
2724        // this will automatically call the offsetChanged() event
2725        smoothTransform.setValue(TRANS_Y, Math.min(getMaxOffsetY(), Math.max(getMinOffsetY(), value)));
2726    }
2727
2728    @Override
2729    protected void setScaleXInternal(double value)
2730    {
2731        // this will automatically call the scaledChanged() event
2732        smoothTransform.setValue(SCALE_X, value);
2733        canvasView.curScaleX = value;
2734    }
2735
2736    @Override
2737    protected void setScaleYInternal(double value)
2738    {
2739        // this will automatically call the scaledChanged() event
2740        smoothTransform.setValue(SCALE_Y, value);
2741        canvasView.curScaleY = value;
2742    }
2743
2744    @Override
2745    protected void setRotationZInternal(double value)
2746    {
2747        // this will automatically call the rotationChanged() event
2748        smoothTransform.setValue(ROT, value);
2749    }
2750
2751    /**
2752     * Set rotation angle (radian).<br>
2753     * 
2754     * @param smooth
2755     *        use smooth transition
2756     */
2757    public void setRotation(double value, boolean smooth)
2758    {
2759        setTransform((int) smoothTransform.getDestValue(TRANS_X), (int) smoothTransform.getDestValue(TRANS_Y),
2760                smoothTransform.getDestValue(SCALE_X), smoothTransform.getDestValue(SCALE_Y), value, smooth);
2761    }
2762
2763    @Override
2764    public void keyPressed(KeyEvent e)
2765    {
2766        // send to overlays
2767        super.keyPressed(e);
2768
2769        if (!e.isConsumed())
2770        {
2771            switch (e.getKeyCode())
2772            {
2773                case KeyEvent.VK_R:
2774                    // reset zoom and rotation
2775                    setRotation(0, false);
2776                    fitImageToCanvas(true);
2777
2778                    // also reset LUT
2779                    if (EventUtil.isShiftDown(e, true))
2780                    {
2781                        final Sequence sequence = getSequence();
2782                        if ((viewer != null) && (sequence != null))
2783                            viewer.setLut(sequence.createCompatibleLUT());
2784                    }
2785
2786                    e.consume();
2787                    break;
2788
2789                case KeyEvent.VK_LEFT:
2790                    if (EventUtil.isMenuControlDown(e, true))
2791                        setPositionT(Math.max(getPositionT() - 5, 0));
2792                    else
2793                        setPositionT(Math.max(getPositionT() - 1, 0));
2794                    e.consume();
2795                    break;
2796
2797                case KeyEvent.VK_RIGHT:
2798                    if (EventUtil.isMenuControlDown(e, true))
2799                        setPositionT(getPositionT() + 5);
2800                    else
2801                        setPositionT(getPositionT() + 1);
2802                    e.consume();
2803                    break;
2804
2805                case KeyEvent.VK_UP:
2806                    if (EventUtil.isMenuControlDown(e, true))
2807                        setPositionZ(getPositionZ() + 5);
2808                    else
2809                        setPositionZ(getPositionZ() + 1);
2810                    e.consume();
2811                    break;
2812
2813                case KeyEvent.VK_DOWN:
2814                    if (EventUtil.isMenuControlDown(e, true))
2815                        setPositionZ(Math.max(getPositionZ() - 5, 0));
2816                    else
2817                        setPositionZ(Math.max(getPositionZ() - 1, 0));
2818                    e.consume();
2819                    break;
2820
2821                case KeyEvent.VK_NUMPAD2:
2822                    if (!canvasView.moving)
2823                    {
2824                        final Point startPos = new Point(getOffsetX(), getOffsetY());
2825                        final Point delta = new Point(0, -getCanvasSizeY() / 4);
2826                        canvasView.translate(startPos, delta, EventUtil.isControlDown(e));
2827                        e.consume();
2828                    }
2829                    break;
2830                case KeyEvent.VK_NUMPAD4:
2831                    if (!canvasView.moving)
2832                    {
2833                        final Point startPos = new Point(getOffsetX(), getOffsetY());
2834                        final Point delta = new Point(getCanvasSizeX() / 4, 0);
2835                        canvasView.translate(startPos, delta, EventUtil.isControlDown(e));
2836                        e.consume();
2837                    }
2838                    break;
2839
2840                case KeyEvent.VK_NUMPAD6:
2841                    if (!canvasView.moving)
2842                    {
2843                        final Point startPos = new Point(getOffsetX(), getOffsetY());
2844                        final Point delta = new Point(-getCanvasSizeX() / 4, 0);
2845                        canvasView.translate(startPos, delta, EventUtil.isControlDown(e));
2846                        e.consume();
2847                    }
2848                    break;
2849
2850                case KeyEvent.VK_NUMPAD8:
2851                    if (!canvasView.moving)
2852                    {
2853                        final Point startPos = new Point(getOffsetX(), getOffsetY());
2854                        final Point delta = new Point(0, getCanvasSizeY() / 4);
2855                        canvasView.translate(startPos, delta, EventUtil.isControlDown(e));
2856                        e.consume();
2857                    }
2858                    break;
2859            }
2860        }
2861
2862        // forward to view
2863        canvasView.keyPressed(e);
2864        // forward to map
2865        canvasMap.keyPressed(e);
2866    }
2867
2868    @Override
2869    public void keyReleased(KeyEvent e)
2870    {
2871        // send to overlays
2872        super.keyReleased(e);
2873
2874        // forward to view
2875        canvasView.keyReleased(e);
2876        // forward to map
2877        canvasMap.keyReleased(e);
2878    }
2879
2880    @Override
2881    public void refresh()
2882    {
2883        canvasView.imageChanged();
2884        canvasView.layersChanged();
2885        canvasView.refresh();
2886    }
2887
2888    /**
2889     * Return an ARGB BufferedImage form of the image located at position [T, Z, C].<br>
2890     * If the 'out' image is not compatible with wanted image, a new image is returned.
2891     */
2892    public BufferedImage getARGBImage(int t, int z, int c, BufferedImage out)
2893    {
2894        final IcyBufferedImage img = Canvas2D.this.getImage(t, z, c);
2895
2896        if (img != null)
2897        {
2898            final BufferedImage result;
2899
2900            if ((out != null) && ImageUtil.sameSize(img, out))
2901                result = out;
2902            else
2903                result = new BufferedImage(img.getSizeX(), img.getSizeY(), BufferedImage.TYPE_INT_ARGB);
2904
2905            return IcyBufferedImageUtil.toBufferedImage(img, result, getLut());
2906        }
2907
2908        return null;
2909    }
2910
2911    @Override
2912    public BufferedImage getRenderedImage(int t, int z, int c, boolean cv)
2913    {
2914        final Sequence seq = getSequence();
2915        if (seq == null)
2916            return null;
2917
2918        // save position
2919        final int prevT = getPositionT();
2920        final int prevZ = getPositionZ();
2921        final boolean dl = isLayersVisible();
2922
2923        if (dl)
2924        {
2925            // set wanted position (needed for correct overlay drawing)
2926            // we have to fire events else some stuff can miss the change
2927            setPositionT(t);
2928            setPositionZ(z);
2929        }
2930        try
2931        {
2932            final Dimension size;
2933
2934            if (cv)
2935                size = getCanvasSize();
2936            else
2937                size = seq.getDimension2D();
2938
2939            // get result image and graphics object
2940            final BufferedImage result = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
2941            final Graphics2D g = result.createGraphics();
2942
2943            // set default clip region
2944            g.setClip(0, 0, size.width, size.height);
2945
2946            if (cv)
2947            {
2948                // apply filtering
2949                if (CanvasPreferences.getFiltering() && ((getScaleX() < 4d) && (getScaleY() < 4d)))
2950                    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
2951                else
2952                    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
2953                            RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
2954                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
2955
2956                // apply transformation
2957                g.transform(getTransform());
2958            }
2959            else
2960            {
2961                // apply filtering
2962                if (CanvasPreferences.getFiltering())
2963                    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
2964                else
2965                    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
2966                            RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
2967                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
2968            }
2969
2970            // create temporary image, overlay and layer so we can choose the correct image
2971            // (not optimal for memory and performance)
2972            final BufferedImage img = getARGBImage(t, z, c, null);
2973            final Overlay imgOverlay = new ImageOverlay("Image", img);
2974            final Layer imgLayer = new Layer(imgOverlay);
2975
2976            // keep visibility and priority information
2977            imgLayer.setVisible(getImageLayer().isVisible());
2978            imgLayer.setPriority(getImageLayer().getPriority());
2979
2980            // draw image and layers
2981            canvasView.drawImageAndLayers(g, imgLayer);
2982
2983            g.dispose();
2984
2985            return result;
2986        }
2987        finally
2988        {
2989            if (dl)
2990            {
2991                // restore position
2992                setPositionT(prevT);
2993                setPositionZ(prevZ);
2994            }
2995        }
2996    }
2997
2998    /**
2999     * @deprecated Use <code>getRenderedImage(t, z, -1, true)</code> instead.
3000     */
3001    @Deprecated
3002    public BufferedImage getRenderedImage(int t, int z)
3003    {
3004        return getRenderedImage(t, z, -1, true);
3005    }
3006
3007    /**
3008     * @deprecated Use <code>getRenderedImage(t, z, -1, canvasView)</code> instead.
3009     */
3010    @Deprecated
3011    public BufferedImage getRenderedImage(int t, int z, boolean canvasView)
3012    {
3013        return getRenderedImage(t, z, -1, canvasView);
3014    }
3015
3016    /**
3017     * Synchronize views of specified list of canvas
3018     */
3019    @Override
3020    protected void synchronizeCanvas(List<IcyCanvas> canvasList, IcyCanvasEvent event, boolean processAll)
3021    {
3022        final IcyCanvasEventType type = event.getType();
3023        final DimensionId dim = event.getDim();
3024
3025        // position synchronization
3026        if (isSynchOnSlice())
3027        {
3028            if (processAll || (type == IcyCanvasEventType.POSITION_CHANGED))
3029            {
3030                // no information about dimension --> set all
3031                if (processAll || (dim == DimensionId.NULL))
3032                {
3033                    // only support T and Z positioning
3034                    final int z = getPositionZ();
3035                    final int t = getPositionT();
3036
3037                    for (IcyCanvas cnv : canvasList)
3038                    {
3039                        if (z != -1)
3040                            cnv.setPositionZ(z);
3041                        if (t != -1)
3042                            cnv.setPositionT(t);
3043                    }
3044                }
3045                else
3046                {
3047                    for (IcyCanvas cnv : canvasList)
3048                    {
3049                        final int pos = getPosition(dim);
3050                        if (pos != -1)
3051                            cnv.setPosition(dim, pos);
3052                    }
3053                }
3054            }
3055        }
3056
3057        // view synchronization
3058        if (isSynchOnView())
3059        {
3060            if (processAll || (type == IcyCanvasEventType.SCALE_CHANGED))
3061            {
3062                // no information about dimension --> set all
3063                if (processAll || (dim == DimensionId.NULL))
3064                {
3065                    final double sX = getScaleX();
3066                    final double sY = getScaleY();
3067
3068                    for (IcyCanvas cnv : canvasList)
3069                        ((Canvas2D) cnv).setScale(sX, sY, false);
3070                }
3071                else
3072                {
3073                    for (IcyCanvas cnv : canvasList)
3074                        cnv.setScale(dim, getScale(dim));
3075                }
3076            }
3077
3078            if (processAll || (type == IcyCanvasEventType.ROTATION_CHANGED))
3079            {
3080                // no information about dimension --> set all
3081                if (processAll || (dim == DimensionId.NULL))
3082                {
3083                    final double rot = getRotationZ();
3084
3085                    for (IcyCanvas cnv : canvasList)
3086                        ((Canvas2D) cnv).setRotation(rot, false);
3087                }
3088                else
3089                {
3090                    for (IcyCanvas cnv : canvasList)
3091                        cnv.setRotation(dim, getRotation(dim));
3092                }
3093            }
3094
3095            // process offset in last as it can be limited depending destination scale value
3096            if (processAll || (type == IcyCanvasEventType.OFFSET_CHANGED))
3097            {
3098                // no information about dimension --> set all
3099                if (processAll || (dim == DimensionId.NULL))
3100                {
3101                    final int offX = getOffsetX();
3102                    final int offY = getOffsetY();
3103
3104                    for (IcyCanvas cnv : canvasList)
3105                        ((Canvas2D) cnv).setOffset(offX, offY, false);
3106                }
3107                else
3108                {
3109                    for (IcyCanvas cnv : canvasList)
3110                        cnv.setOffset(dim, getOffset(dim));
3111                }
3112            }
3113
3114        }
3115
3116        // cursor synchronization
3117        if (isSynchOnCursor())
3118        { // mouse synchronization
3119            if (processAll || (type == IcyCanvasEventType.MOUSE_IMAGE_POSITION_CHANGED))
3120            {
3121                // no information about dimension --> set all
3122                if (processAll || (dim == DimensionId.NULL))
3123                {
3124                    final double mouseImagePosX = getMouseImagePosX();
3125                    final double mouseImagePosY = getMouseImagePosY();
3126
3127                    for (IcyCanvas cnv : canvasList)
3128                        ((Canvas2D) cnv).setMouseImagePos(mouseImagePosX, mouseImagePosY);
3129                }
3130                else
3131                {
3132                    for (IcyCanvas cnv : canvasList)
3133                        cnv.setMouseImagePos(dim, getMouseImagePos(dim));
3134                }
3135            }
3136        }
3137    }
3138
3139    @Override
3140    public void changed(IcyCanvasEvent event)
3141    {
3142        super.changed(event);
3143
3144        // not yet initialized
3145        if (canvasView == null)
3146            return;
3147
3148        final IcyCanvasEventType type = event.getType();
3149
3150        switch (type)
3151        {
3152            case POSITION_CHANGED:
3153                // image has changed
3154                canvasView.imageChanged();
3155
3156            case OFFSET_CHANGED:
3157            case SCALE_CHANGED:
3158            case ROTATION_CHANGED:
3159                // update mouse image position from mouse canvas position
3160                setMouseImagePos(canvasToImage(getMousePos()));
3161
3162                // display info message
3163                if (type == IcyCanvasEventType.SCALE_CHANGED)
3164                {
3165                    final String zoomInfo = Integer.toString((int) (getScaleX() * 100));
3166
3167                    ThreadUtil.invokeLater(new Runnable()
3168                    {
3169                        @Override
3170                        public void run()
3171                        {
3172                            // in panel
3173                            modifyingZoom = true;
3174                            try
3175                            {
3176                                getCanvasSettingPanel().updateZoomState(zoomInfo);
3177                            }
3178                            finally
3179                            {
3180                                modifyingZoom = false;
3181                            }
3182                        }
3183                    });
3184
3185                    // and in canvas
3186                    canvasView.setZoomMessage("Zoom : " + zoomInfo + " %", 500);
3187                }
3188                else if (type == IcyCanvasEventType.ROTATION_CHANGED)
3189                {
3190                    final String rotInfo = Integer.toString((int) Math.round(getRotation() * 180d / Math.PI));
3191
3192                    ThreadUtil.invokeLater(new Runnable()
3193                    {
3194                        @Override
3195                        public void run()
3196                        {
3197                            // in panel
3198                            modifyingRotation = true;
3199                            try
3200                            {
3201                                getCanvasSettingPanel().updateRotationState(rotInfo);
3202                            }
3203                            finally
3204                            {
3205                                modifyingRotation = false;
3206                            }
3207                        }
3208                    });
3209
3210                    // and in canvas
3211                    canvasView.setRotationMessage("Rotation : " + rotInfo + " °", 500);
3212                }
3213
3214                // refresh canvas
3215                canvasView.refresh();
3216                break;
3217
3218            case MOUSE_IMAGE_POSITION_CHANGED:
3219                // mouse position changed outside mouse move event ?
3220                if (!canvasView.handlingMouseMoveEvent && !canvasView.isDragging() && !isSynchSlave())
3221                {
3222                    // mouse position in canvas
3223                    final Point mousePos = getMousePos();
3224                    final Point mouseAbsolutePos = getMousePos();
3225                    // absolute mouse position
3226                    SwingUtilities.convertPointToScreen(mouseAbsolutePos, canvasView);
3227
3228                    // simulate a mouse move event so overlays can handle position change
3229                    final MouseEvent mouseEvent = new MouseEvent(this, MouseEvent.MOUSE_MOVED,
3230                            System.currentTimeMillis(), 0, mousePos.x, mousePos.y, mouseAbsolutePos.x,
3231                            mouseAbsolutePos.y, 0, false, 0);
3232
3233                    // send mouse move event to overlays
3234                    mouseMove(mouseEvent, getMouseImagePos5D());
3235                }
3236
3237                // update mouse cursor
3238                canvasView.updateCursor();
3239
3240                // needed to refresh custom cursor
3241                if (!canvasView.hasMouseFocus)
3242                    canvasView.refresh();
3243                break;
3244        }
3245    }
3246
3247    @Override
3248    protected void lutChanged(int component)
3249    {
3250        super.lutChanged(component);
3251
3252        // refresh image
3253        if (canvasView != null)
3254        {
3255            canvasView.imageChanged();
3256            canvasView.refresh();
3257        }
3258    }
3259
3260    @Override
3261    protected void layerChanged(CanvasLayerEvent event)
3262    {
3263        super.layerChanged(event);
3264
3265        // layer visibility property modified ?
3266        if ((event.getType() == LayersEventType.CHANGED) && Layer.isPaintProperty(event.getProperty()))
3267        {
3268            // layer refresh
3269            if (canvasView != null)
3270            {
3271                canvasView.layersChanged();
3272                canvasView.refresh();
3273            }
3274        }
3275    }
3276
3277    @Override
3278    protected void sequenceOverlayChanged(Overlay overlay, SequenceEventType type)
3279    {
3280        super.sequenceOverlayChanged(overlay, type);
3281
3282        // layer refresh
3283        if (canvasView != null)
3284        {
3285            canvasView.layersChanged();
3286            canvasView.refresh();
3287        }
3288    }
3289
3290    @Override
3291    protected void sequenceDataChanged(IcyBufferedImage image, SequenceEventType type)
3292    {
3293        super.sequenceDataChanged(image, type);
3294
3295        // refresh image
3296        if (canvasView != null)
3297        {
3298            canvasView.imageChanged();
3299            canvasView.refresh();
3300        }
3301    }
3302
3303    @Override
3304    protected void sequenceTypeChanged()
3305    {
3306        super.sequenceTypeChanged();
3307
3308        // sequence XY dimension changed ?
3309        if ((previousImageSize.width != getImageSizeX()) || (previousImageSize.height != getImageSizeY()))
3310        {
3311            // fit to canvas enabled ? --> adapt zoom to new sequence XY dimension
3312            if (getFitToCanvas())
3313                fitImageToCanvas(true);
3314        }
3315    }
3316
3317    @Override
3318    public void toolChanged(String command)
3319    {
3320        final Sequence seq = getSequence();
3321
3322        final ROITask toolTask = Icy.getMainInterface().getROIRibbonTask();
3323
3324        if (toolTask != null)
3325        {
3326            // if we selected a ROI tool we force layers to be visible
3327            if (toolTask.isROITool())
3328                setLayersVisible(true);
3329        }
3330
3331        // unselected all ROI
3332        if (seq != null)
3333            seq.setSelectedROI(null);
3334    }
3335
3336}