2 Replies Latest reply: May 11, 2012 8:19 AM by 800331 RSS

    Pan (translate) at correct rate after zoom (scale) toward center

    800331
      Hi,

      This question is also posted here:

      http://www.java-forums.org/java-2d/59284-affinetransform-pan-translate-correct-rate-after-zoom-scale-toward-center.html#post284054

      I didn't get any answers there yet so I'm trying here.

      Here is a very simple pan/zoom paint routine, not unlike dozens of other examples on the web:
      public void paintComponent(Graphics g) {
           
          Graphics2D g2d = (Graphics2D)g.create();
          AffineTransform tx = new AffineTransform();
       
          tx.scale(zoom, zoom);
          tx.translate(currentX, currentY);
           
          g2d.drawImage(image, tx, this);
          g2d.dispose();
           
      }
      That works ok, but it has two problems:

      1. After zooming in, it doesn’t pan at the correct rate. (The image pans faster than the mouse cursor is moving.)
      2. It zooms in toward (0, 0) of the JPanel instead of toward the center.

      The pan rate problem can be fixed by dividing the current coordinates by the zoom level:
      tx.scale(zoom, zoom);
      tx.translate(currentX / zoom, currentY / zoom);
      The zoom to center problem can fixed by first translating to the JPanel’s center coordinates before doing the zoom:
      double centerX = (double)getWidth() / 2;
      double centerY = (double)getHeight() / 2;
       
      tx.translate(centerX, centerY);
      tx.scale(zoom, zoom);
      tx.translate(currentX, currentY);
      But after combining these two strategies, the pan rate is still correct but it no longer zooms in toward the center:
      double centerX = (double)getWidth() / 2;
      double centerY = (double)getHeight() / 2;
       
      tx.translate(centerX, centerY);
      tx.scale(zoom, zoom);
      tx.translate(currentX / zoom, currentY / zoom);
      I found a half-dozen or so very simple examples suggesting that you can “undo” the centering translation like this:
      double centerX = (double)getWidth() / 2;
      double centerY = (double)getHeight() / 2;
       
      tx.translate(centerX, centerY);
      tx.scale(zoom, zoom);
      tx.translate(-centerX, -centerY);
      tx.translate(currentX, currentY);
      But this doesn’t have any effect, it still pans correctly but will not zoom in toward the center (still more like 0, 0). It also seems odd that this is being suggested because other sources I’ve found state that translations are not linear, meaning they cannot be undone. Assuming that is true, I decided to try the center and pan translation at the same time:
      double centerX = (double)getWidth() / 2 + currentX;
      double centerY = (double)getHeight() / 2 + currentY;
       
      tx.translate(centerX, centerY);
      tx.scale(zoom, zoom);
      But that has the same problem: The pan is fine but it’s still zooming in toward (0, 0) instead of the panel’s center. Other ideas I’ve tried and exhausted are:

      1. Concatenating (or preconcatenating) separate AffineTransform instances onto a single AffineTransform that is used for the drawing.
      2. Same as #1 but including “undo” transformations using opposite-signed coordinates as well as AffineTransform.createInverse().
      3. Slogging through 100+ Google search results, all of which are either too basic, or have the same problem as this with no mention of how to address it.

      How does one go about using AffineTransform to zoom in toward a specific coordinate AND have the correct pan rate after the zoom? Do I need to adjust how currentX and currentY are calculated, to account for the zoom level? Do I need to consider a different way of drawing the image in conjunction with these transforms? Something else altogether? I appreciate your help!

      SSCCE:
      import java.awt.EventQueue;
      import java.awt.Graphics;
      import java.awt.Graphics2D;
      import java.awt.Image;
      import java.awt.event.*;
      import java.awt.geom.AffineTransform;
      import java.net.URL;
      import javax.imageio.ImageIO;
      import javax.swing.JFrame;
      import javax.swing.JPanel;
       
      public class PanZoomProblem extends JPanel {
       
          private static String imagePath = 
                  "http://nationalmap.gov/ustopo/UST_slideshow/columbia_bottom/images/MO_Columbia_Bottom_20120126_TM_ImageOff_thumb.jpg";
          private static Image image;
           
          private double currentX;
          private double currentY;
          private double previousX;
          private double previousY;
          private double zoom = 1;
           
          public static void main(String[] args) throws Exception {
               
              image = ImageIO.read(new URL(imagePath));
               
              EventQueue.invokeLater(new Runnable() {
                  public void run() {
                      JFrame frame = new JFrame();
                      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                      frame.add(new PanZoomProblem());
                      frame.setSize(640, 480);
                      frame.setLocationRelativeTo(null);
                      frame.setVisible(true);
                  }
              });
               
          }
           
          public PanZoomProblem() {
               
              addMouseListener(new MouseAdapter() {
                  public void mousePressed(MouseEvent e) {
                      previousX = e.getX();
                      previousY = e.getY();
                  }
              });
              addMouseMotionListener(new MouseMotionAdapter() {
                  public void mouseDragged(MouseEvent e) {
                       
                      double newX = e.getX() - previousX;
                      double newY = e.getY() - previousY;
       
                      previousX += newX;
                      previousY += newY;
       
                      currentX += newX;
                      currentY += newY;
                       
                      repaint();
                  }
              });
              addMouseWheelListener(new MouseWheelListener() {
                  public void mouseWheelMoved(MouseWheelEvent e) {
                      if(e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
                          incrementZoom(.1 * -(double)e.getWheelRotation());
                      }
                  }
              });
               
              setOpaque(false);
               
          }
           
          private void incrementZoom(double amount) {
               
              zoom += amount;
              zoom = Math.max(0.00001, zoom);
              repaint();
               
          }
           
           public void paintComponent(Graphics g) {
                
                Graphics2D g2d = (Graphics2D)g.create();
                AffineTransform tx = new AffineTransform();
      
                tx.translate(currentX, currentY);
                tx.scale(zoom, zoom);
                
                g2d.drawImage(image, tx, this);
                g2d.dispose();
                
           }
           
      }
        • 1. Re: Pan (translate) at correct rate after zoom (scale) toward center
          morgalr
          I always do a manual pan/scale/or any type of movement.
          • 2. Re: Pan (translate) at correct rate after zoom (scale) toward center
            800331
            The trick is to understand the difference between the JPanel's coordinate system and the zoomed image's coordinate system. After a scale/zoom, there is no longer a 1-to-1 relationship between a point on the JPanel and the corresponding point on the image. A 1-pixel change in the mouse movement might translate to a change of 2 or more pixels on the translated image. The drag distance needs to be derived using the inverse of the affine transformation (translate the old & new points, THEN find the difference). I think it's also the same reason why the zoom rate appears to slow down as you zoom in, because that delta also needs to be translated before incrementing the zoom level. This demonstrates translating the old and new mouse points to find the correct drag distance at whatever zoom level we're at:
            import java.awt.EventQueue;
            import java.awt.Graphics;
            import java.awt.Graphics2D;
            import java.awt.Image;
            import java.awt.event.*;
            import java.awt.geom.AffineTransform;
            import java.awt.geom.NoninvertibleTransformException;
            import java.awt.geom.Point2D;
            import java.net.URL;
            import javax.imageio.ImageIO;
            import javax.swing.JFrame;
            import javax.swing.JPanel;
             
            public class PanZoomProblem extends JPanel {
             
                private static String imagePath =
                        "http://nationalmap.gov/ustopo/UST_slideshow/columbia_bottom/"+
                        "images/MO_Columbia_Bottom_20120126_TM_ImageOff_thumb.jpg";
                         
                private static Image image;
                 
                private double currentX;
                private double currentY;
                private double previousX;
                private double previousY;
                private double zoom = 1;
                 
                public static void main(String[] args) throws Exception {
                     
                    image = ImageIO.read(new URL(imagePath));
                     
                    EventQueue.invokeLater(new Runnable() {
                        public void run() {
                            JFrame frame = new JFrame();
                            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                            frame.add(new PanZoomProblem());
                            frame.setSize(640, 480);
                            frame.setLocationRelativeTo(null);
                            frame.setVisible(true);
                        }
                    });
                     
                }
                 
                public PanZoomProblem() {
                     
                    addMouseListener(new MouseAdapter() {
                        public void mousePressed(MouseEvent e) {
                            previousX = e.getX();
                            previousY = e.getY();
                        }
                    });
                    addMouseMotionListener(new MouseMotionAdapter() {
                        public void mouseDragged(MouseEvent e) {
                             
                            // Determine the old and new mouse coordinates based on the translated coordinate space.
                            Point2D adjPreviousPoint = getTranslatedPoint(previousX, previousY);
                            Point2D adjNewPoint = getTranslatedPoint(e.getX(), e.getY());
                             
                            double newX = adjNewPoint.getX() - adjPreviousPoint.getX();
                            double newY = adjNewPoint.getY() - adjPreviousPoint.getY();
             
                            previousX = e.getX();
                            previousY = e.getY();
                             
                            currentX += newX;
                            currentY += newY;
                             
                            repaint();
                        }
                    });
                    addMouseWheelListener(new MouseWheelListener() {
                        public void mouseWheelMoved(MouseWheelEvent e) {
                            if(e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
                                incrementZoom(.1 * -(double)e.getWheelRotation());
                            }
                        }
                    });
                     
                    setOpaque(false);
                     
                }
                 
                private void incrementZoom(double amount) {
                     
                    zoom += amount;
                    zoom = Math.max(0.00001, zoom);
                    repaint();
                     
                }
                 
                public void paintComponent(Graphics g) {
                     
                    Graphics2D g2d = (Graphics2D)g.create();
                    AffineTransform tx = getCurrentTransform();
                     
                    g2d.drawImage(image, tx, this);
                    g2d.dispose();
                     
                }
                 
                private AffineTransform getCurrentTransform() {
                     
                    AffineTransform tx = new AffineTransform();
                     
                    double centerX = (double)getWidth() / 2;
                    double centerY = (double)getHeight() / 2;
                     
                    tx.translate(centerX, centerY);
                    tx.scale(zoom, zoom);
                    tx.translate(currentX, currentY);
                     
                    return tx;
                     
                }
                 
                // Convert the panel coordinates into the cooresponding coordinates on the translated image.
                private Point2D getTranslatedPoint(double panelX, double panelY) {
                     
                    AffineTransform tx = getCurrentTransform();
                    Point2D point2d = new Point2D.Double(panelX, panelY);
                    try {
                        return tx.inverseTransform(point2d, null);
                    } catch (NoninvertibleTransformException ex) {
                        ex.printStackTrace();
                        return null;
                    }
                     
                }
                 
            }
            I hope this helps others who are having the same confusion.