com.massisframework.gui.PanAndZoomJPanel Maven / Gradle / Ivy
package com.massisframework.gui;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.Collection;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
/**
* A a JPanel capable of pan and zoom. It manages the 2D visualization of the
* different layers.
* Attributions: Adapted from code posted by R.J.
* Lorimer in an article entitled "Java2D: Have Fun With Affine Transform". The
* original post and code can be found at
* http://www.javalobby.org/java/forums/t19387.html.
*
* @author rpax
*
*/
public class PanAndZoomJPanel extends JPanel {
private static final long serialVersionUID = 1L;
private double translateX;
private double translateY;
private double scale;
private AffineTransform at;
private DZ drawableZone;
private Point2D XFormedPoint;
private Collection extends DrawableLayer> layers;
private boolean initiated = false;
/**
* Empty constructor. It is useful for using this component in a graphical
* GUI designer (such as Netbeans')
*/
public PanAndZoomJPanel()
{
}
/**
* Proper constructor. It creates a new JPanel for displaying the 2D views.
*
* @param drawableZone The drawableZone that this panel should display
* @param layers a list of {@link FloorMapLayer}. The
* {@link FloorMapLayer#drawIfEnabled(Floor, Graphics2D)} method of each one
* of the elements of the list will be called in order, from bottom to top.
* Example:
*
* With the layers {@code [A,B,C,D]}:
*
*
* - Draws A
* - On top of A draws B
* - On top of B draws C
* - Finally, draws D
*
* As can be seen, the elements of one layer can overwrite
* the previous layers drawings.
*
*/
public PanAndZoomJPanel(DZ drawableZone,
Collection extends DrawableLayer> layers)
{
this.initiate(drawableZone, layers);
}
/**
* Initiates this Panel. This separation is made in order to make easier the
* integration within a graphical GUI designer
*
* @param drawableZone the corresponding drawableZone
* @param layers the layers to be displayed
* @return This panel. Useful for chaining
* @see {@link #PanAndZoomJPanel(Floor, Building, List)}
*
*/
private PanAndZoomJPanel initiate(DZ drawableZone,
Collection extends DrawableLayer> layers)
{
if (initiated)
{
return this;
}
translateX = 0;
translateY = 0;
this.drawableZone = drawableZone;
this.layers = layers;
this.setScale(1);
PanningHandler panner = new PanningHandler();
this.addMouseListener(panner);
this.addMouseMotionListener(panner);
this.setBorder(BorderFactory.createLineBorder(Color.black));
this.initiated = true;
return this;
}
@Override
public void paintComponent(Graphics g)
{
if (!initiated)
{
return;
}
render(g);
}
public void render(Graphics g)
{
Graphics2D ourGraphics = (Graphics2D) g;
// save the original transform so that we can restore
// it later
AffineTransform saveTransform = ourGraphics.getTransform();
// blank the screen. If we do not call super.paintComponent, then
// we need to blank it ourselves
ourGraphics.setColor(Color.BLACK);
ourGraphics.fillRect(0, 0, getWidth(), getHeight());
// We need to add new transforms to the existing
// transform, rather than creating a new transform from scratch.
// If we create a transform from scratch, we will
// will start from the upper left of a JFrame,
// rather than from the upper left of our component
at = new AffineTransform(saveTransform);
// The zooming transformation. Notice that it will be performed
// after the panning transformation, zooming the panned scene,
// rather than the original scene
// at.translate(getWidth() / 2, getHeight() / 2);
at.scale(getScale(), getScale());
// at.translate(-getWidth() / 2, -getHeight() / 2);
// The panning transformation
at.translate(translateX, translateY);
ourGraphics.setTransform(at);
/*
* Main rendering method
*/
renderGraphics(ourGraphics);
ourGraphics.setTransform(saveTransform);
}
/**
* Iterates over every layer, drawing it if enabled
*
* @param g the {@link Graphics2D} object of this panel
*/
private void renderGraphics(Graphics2D g)
{
for (DrawableLayer layer : layers)
{
if (layer.isEnabled())
{
layer.draw(drawableZone, g);
}
}
}
@Override
public Dimension getPreferredSize()
{
return new Dimension(500, 500);
}
public double getScale()
{
return scale;
}
/**
* Sets the zoom's scale
*
* @param scale
*/
public void setScale(double scale)
{
this.scale = scale
* (getPreferredSize().width * 1.0 / (drawableZone.getMaxX() - drawableZone
.getMinX()));
}
private class PanningHandler implements MouseListener, MouseMotionListener {
double referenceX;
double referenceY;
// saves the initial transform at the beginning of the pan interaction
AffineTransform initialTransform;
// capture the starting point
@Override
public void mousePressed(MouseEvent e)
{
// first transform the mouse point to the pan and zoom
// coordinates
try
{
XFormedPoint = at.inverseTransform(e.getPoint(), null);
} catch (NoninvertibleTransformException te)
{
System.out.println(te);
}
// save the transformed starting point and the initial
// transform
referenceX = XFormedPoint.getX();
referenceY = XFormedPoint.getY();
initialTransform = at;
}
@Override
public void mouseDragged(MouseEvent e)
{
// first transform the mouse point to the pan and zoom
// coordinates. We must take care to transform by the
// initial tranform, not the updated transform, so that
// both the initial reference point and all subsequent
// reference points are measured against the same origin.
try
{
XFormedPoint = initialTransform.inverseTransform(e.getPoint(),
null);
} catch (NoninvertibleTransformException te)
{
System.out.println(te);
}
// the size of the pan translations
// are defined by the current mouse location subtracted
// from the reference location
double deltaX = XFormedPoint.getX() - referenceX;
double deltaY = XFormedPoint.getY() - referenceY;
// make the reference point be the new mouse point.
referenceX = XFormedPoint.getX();
referenceY = XFormedPoint.getY();
PanAndZoomJPanel.this.translateX += deltaX;
PanAndZoomJPanel.this.translateY += deltaY;
// schedule a repaint.
PanAndZoomJPanel.this.repaint();
}
@Override
public void mouseClicked(MouseEvent e)
{
}
@Override
public void mouseEntered(MouseEvent e)
{
}
@Override
public void mouseExited(MouseEvent e)
{
}
@Override
public void mouseMoved(MouseEvent e)
{
}
@Override
public void mouseReleased(MouseEvent e)
{
}
}
/**
* Zooms the view and schedules a repaint
*
* @param zoomPercent the percent desired
*/
public void zoom(float zoomPercent)
{
PanAndZoomJPanel.this.setScale(Math.max(0.00001, zoomPercent / 100.0));
PanAndZoomJPanel.this.repaint();
}
public void mouseWheelMoved(MouseWheelEvent evt)
{
double delta = 0.005f * evt.getPreciseWheelRotation();
scale -= delta;
revalidate();
repaint();
}
public void keyPressed(KeyEvent e)
{
double delta = 0.0005f;
switch (e.getKeyCode())
{
case KeyEvent.VK_Z:
scale -= delta;
revalidate();
repaint();
break;
case KeyEvent.VK_X:
scale += delta;
revalidate();
repaint();
break;
case KeyEvent.VK_W:
PanAndZoomJPanel.this.translateY += 5;
revalidate();
repaint();
break;
case KeyEvent.VK_S:
PanAndZoomJPanel.this.translateY -= 5;
revalidate();
repaint();
break;
case KeyEvent.VK_D:
PanAndZoomJPanel.this.translateX += 5;
revalidate();
repaint();
break;
case KeyEvent.VK_A:
PanAndZoomJPanel.this.translateX -= 5;
revalidate();
repaint();
break;
}
}
public void keyReleased(KeyEvent e)
{
// keyPressed(e);
}
}