Results 1 to 5 of 5
  1. #1
    BinaryDigit09 is offline Member
    Join Date
    May 2012
    Posts
    10
    Rep Power
    0

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

    Hi,

    Here is a very simple pan/zoom paint routine, not unlike dozens of other examples on the web:
    Java Code:
    	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:
    Java Code:
    		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:
    Java Code:
    		double centerX = (double)getWidth() / 2;
    		double centerY = (double)getHeight() / 2;
    		
    		tx.translate(centerX, centerY);
    		tx.scale(zoom, zoom);
    		tx.translate(currentX, currentY);
    Now a reasonable man might expect that combining the two operations together should fix both problems at the same time (zoom in toward center AND pan at the correct rate), but that man would be a very mistaken one. After combining these two strategies, the pan rate is still correct but it no longer zooms in toward the center:
    Java Code:
    		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ím pretty sure this is because I have changed the coordinate space before doing the zoom, which means itís already lost in the weeds when I do the pan. I found a half-dozen or so very simple examples suggesting that you can ďundoĒ the centering translation like this:
    Java Code:
    		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:
    Java Code:
    		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.

    I keep thinking that my problem has to do with the order in which Iím doing operations, but Iíve tried all the different combinations I can think of with no good results. How does one go about using AffineTransform to zoom in toward a specific coordinate AND have the pan translations still work correctly 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:

    Java Code:
    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();
    
    		double centerX = (double)getWidth() / 2 + currentX;
    		double centerY = (double)getHeight() / 2 + currentY;
    		
    		tx.translate(centerX, centerY);
    		tx.scale(zoom, zoom);
    		
    		g2d.drawImage(image, tx, this);
    		g2d.dispose();
    		
    	}
    	
    	public void paintComponent2(Graphics g) {
    		
    		Graphics2D g2d = (Graphics2D)g.create();
    		AffineTransform tx = new AffineTransform();
    		
    		double centerX = (double)getWidth() / 2;
    		double centerY = (double)getHeight() / 2;
    		
    		double panX = currentX / zoom;
    		double panY = currentY / zoom;
    		
    		//tx.translate(centerX, centerY);
    		tx.scale(zoom, zoom);
    		//tx.translate(-centerX, -centerY);
    		tx.translate(panX, panY);
    		
    		g2d.drawImage(image, tx, this);
    		g2d.dispose();
    		
    	}
    	
    }

  2. #2
    BinaryDigit09 is offline Member
    Join Date
    May 2012
    Posts
    10
    Rep Power
    0

    Default Re: AffineTransform - Pan (translate) at correct rate after zoom (scale) toward cente

    Oops, looks like the value for imagePath got truncated by the forum. The last part of the path should be:

    Java Code:
    /columbia_bottom/images/MO_Columbia_Bottom_20120126_TM_ImageOff_thumb.jpg

  3. #3
    DarrylBurke's Avatar
    DarrylBurke is offline Member
    Join Date
    Sep 2008
    Location
    Madgaon, Goa, India
    Posts
    11,189
    Rep Power
    19

    Default Re: AffineTransform - Pan (translate) at correct rate after zoom (scale) toward cente

    Moved from Advanced java.

    db
    If you're forever cleaning cobwebs, it's time to get rid of the spiders.

  4. #4
    BinaryDigit09 is offline Member
    Join Date
    May 2012
    Posts
    10
    Rep Power
    0

    Default Re: AffineTransform - Pan (translate) at correct rate after zoom (scale) toward cente


  5. #5
    BinaryDigit09 is offline Member
    Join Date
    May 2012
    Posts
    10
    Rep Power
    0

    Default Re: AffineTransform - Pan (translate) at correct rate after zoom (scale) toward cente

    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). 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:

    Java Code:
    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.

Similar Threads

  1. translate - scale problem
    By gmseed in forum Java 2D
    Replies: 0
    Last Post: 11-16-2010, 08:31 PM
  2. Replies: 1
    Last Post: 11-13-2010, 10:16 PM
  3. Replies: 13
    Last Post: 08-30-2010, 07:55 PM
  4. How to Zoom in and Zoom out TYPE_USHORT_565_RGB image
    By Santhoshkumarp in forum AWT / Swing
    Replies: 0
    Last Post: 08-07-2010, 02:39 PM
  5. Zoom map in JScrollPane, keep center of viewport
    By knuth in forum New To Java
    Replies: 0
    Last Post: 10-01-2009, 07:45 PM

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •