/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see http://www.gnu.org/licenses/
*/
package com.bc.ceres.glayer.swing;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.util.EventListener;
/**
* A navigation control which appears as a screen overlay.
* It can fire rotation, translation and scale events.
*
* @author Norman Fomferra
* @version $Revision$ $Date$
*/
public class NavControl2 extends JComponent {
private static final int W = 100;
private static final int H = 120;
private static final Dimension SIZE = new Dimension(W, H);
private final double pannerHandleW = 10;
private final double pannerHandleH = 10;
private final double scaleHandleW = 5;
private final double scaleHandleH = 16;
private final double gap = 4;
private static final int TIMER_DELAY = 50;
private double rotationAngle;
private double pannerHandleOffsetX;
private double pannerHandleOffsetY;
private double scaleHandleOffsetX;
private double scaleHandleOffsetY;
private Ellipse2D outerWheelCircle;
private Ellipse2D innerWheelCircle;
private Ellipse2D outerMoveCircle;
private Shape pannerHandle;
private Shape[] moveArrowShapes;
private Area[] rotationUnitShapes;
private RectangularShape scaleHandle;
private RectangularShape scaleBar;
private BufferedImage backgroundImage;
private BufferedImage rotationWheelImage;
private BufferedImage pannerHandleImage;
private BufferedImage scaleHandleImage;
public NavControl2() {
final MouseHandler mouseHandler = new MouseHandler();
addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler);
setBounds(0, 0, W, H);
}
/**
* Gets the model's current rotation angle in degrees.
*
* @return the model's current rotation angle in degrees.
*/
public double getRotationAngle() {
return rotationAngle;
}
/**
* Sets the current rotation angle in degrees.
*
* @param rotationAngle the model's current rotation angle in degrees.
*/
public void setRotationAngle(double rotationAngle) {
double oldRotationAngle = this.rotationAngle;
if (oldRotationAngle != rotationAngle) {
this.rotationAngle = rotationAngle;
firePropertyChange("rotationAngle", oldRotationAngle, rotationAngle);
}
}
@Override
public Dimension getPreferredSize() {
return SIZE;
}
@Override
public Dimension getMinimumSize() {
return SIZE;
}
@Override
public Dimension getMaximumSize() {
return SIZE;
}
@Override
protected void paintComponent(Graphics g) {
final Graphics2D graphics2D = (Graphics2D) g;
final AffineTransform oldTransform = graphics2D.getTransform();
graphics2D.setStroke(new BasicStroke(0.5f));
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.rotate(-Math.PI * 0.5 - Math.toRadians(getRotationAngle()),
outerWheelCircle.getCenterX(),
outerWheelCircle.getCenterY());
for (int i = 0; i < rotationUnitShapes.length; i++) {
graphics2D.setColor(i == 0 ? Color.ORANGE : Color.WHITE);
graphics2D.fill(rotationUnitShapes[i]);
graphics2D.setColor(Color.BLACK);
graphics2D.draw(rotationUnitShapes[i]);
}
graphics2D.setTransform(oldTransform);
for (Shape arrow : moveArrowShapes) {
graphics2D.setColor(Color.WHITE);
graphics2D.fill(arrow);
graphics2D.setColor(Color.BLACK);
graphics2D.draw(arrow);
}
graphics2D.translate(pannerHandleOffsetX, pannerHandleOffsetY);
graphics2D.setColor(Color.WHITE);
graphics2D.fill(pannerHandle);
graphics2D.setColor(Color.BLACK);
graphics2D.draw(pannerHandle);
graphics2D.setTransform(oldTransform);
graphics2D.setColor(Color.WHITE);
graphics2D.fill(scaleBar);
graphics2D.setColor(Color.BLACK);
graphics2D.draw(scaleBar);
graphics2D.translate(scaleHandleOffsetX, scaleHandleOffsetY);
graphics2D.setColor(Color.WHITE);
graphics2D.fill(scaleHandle);
graphics2D.setColor(Color.BLACK);
graphics2D.draw(scaleHandle);
graphics2D.setTransform(oldTransform);
}
@Override
public boolean contains(int x, int y) {
return getAction(x, y) != ACTION_NONE;
}
private void initGeom() {
final Insets insets = getInsets();
double x = 1;
double y = 1;
final double outerRotationDiameter = W - 2;
final double innerRotationDiameter = 0.8 * outerRotationDiameter;
final double outerMoveDiameter = 0.4 * outerRotationDiameter;
outerWheelCircle = new Ellipse2D.Double(x,
y,
outerRotationDiameter,
outerRotationDiameter);
innerWheelCircle = new Ellipse2D.Double(outerWheelCircle.getCenterX() - 0.5 * innerRotationDiameter,
outerWheelCircle.getCenterY() - 0.5 * innerRotationDiameter,
innerRotationDiameter,
innerRotationDiameter);
outerMoveCircle = new Ellipse2D.Double(innerWheelCircle.getCenterX() - 0.5 * outerMoveDiameter,
innerWheelCircle.getCenterY() - 0.5 * outerMoveDiameter,
outerMoveDiameter,
outerMoveDiameter);
rotationUnitShapes = createRotationUnitShapes();
moveArrowShapes = createMoveArrows();
pannerHandle = createPanHandle();
/////////////////////////////////////////////////////////
final double scaleBarW = outerRotationDiameter;
final double scaleBarH = scaleHandleW;
final double scaleBarX = x;
final double scaleHandleY = y + outerRotationDiameter + gap;
scaleBar = new Rectangle2D.Double(scaleBarX,
scaleHandleY + 0.5 * (scaleHandleH - scaleBarH),
scaleBarW,
scaleBarH);
scaleHandle = new Rectangle2D.Double(scaleBarX + 0.5 * (scaleBarW - scaleHandleW),
scaleHandleY,
scaleHandleW,
scaleHandleH);
//////////////////////////////////////////////////////////////////////
scaleHandleImage = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
//////////////////////////////////////////////////////////////////////
// backgroundImage
backgroundImage = new BufferedImage(W, H, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g1 = backgroundImage.createGraphics();
g1.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g1.setStroke(new BasicStroke(1f));
for (Shape arrow : moveArrowShapes) {
g1.setColor(Color.WHITE);
g1.fill(arrow);
g1.setColor(Color.BLACK);
g1.draw(arrow);
}
g1.setColor(Color.WHITE);
g1.fill(scaleBar);
g1.setColor(Color.BLACK);
g1.draw(scaleBar);
g1.dispose();
// backgroundImage
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
// rotationWheelImage
rotationWheelImage = new BufferedImage(W, W, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g2 = backgroundImage.createGraphics();
final AffineTransform oldTransform = g2.getTransform();
g2.setStroke(new BasicStroke(1f));
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.rotate(-Math.PI * 0.5 - Math.toRadians(getRotationAngle()),
outerWheelCircle.getCenterX(),
outerWheelCircle.getCenterY());
for (int i = 0; i < rotationUnitShapes.length; i++) {
g2.setColor(i == 0 ? Color.ORANGE : Color.WHITE);
g2.fill(rotationUnitShapes[i]);
g2.setColor(Color.BLACK);
g2.draw(rotationUnitShapes[i]);
}
g2.setTransform(oldTransform);
g2.dispose();
// backgroundImage
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
// pannerHandleImage
pannerHandleImage = new BufferedImage(W, W, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g3 = backgroundImage.createGraphics();
g3.setColor(Color.WHITE);
g3.fill(pannerHandle);
g3.setColor(Color.BLACK);
g3.draw(pannerHandle);
g3.dispose();
g2.setColor(Color.WHITE);
g2.fill(scaleBar);
g2.setColor(Color.BLACK);
g2.draw(scaleBar);
g2.translate(scaleHandleOffsetX, scaleHandleOffsetY);
g2.setColor(Color.WHITE);
g2.fill(scaleHandle);
g2.setColor(Color.BLACK);
g2.draw(scaleHandle);
g2.setTransform(oldTransform);
}
private Shape createPanHandle() {
final Rectangle2D r1 = new Rectangle2D.Double(-0.5 * pannerHandleW, -0.5 * pannerHandleH, pannerHandleW, pannerHandleH);
final Shape r2 = AffineTransform.getRotateInstance(0.25 * Math.PI).createTransformedShape(r1);
Area area = new Area(r1);
area.add(new Area(r2));
return AffineTransform.getTranslateInstance(innerWheelCircle.getCenterX(), innerWheelCircle.getCenterY()).createTransformedShape(area);
}
private Shape[] createMoveArrows() {
final double innerRadius = 0.5 * innerWheelCircle.getWidth();
final GeneralPath path = new GeneralPath();
path.moveTo(0, 0);
path.lineTo(2, 1);
path.lineTo(1, 1);
path.lineTo(1, 2);
path.lineTo(-1, 2);
path.lineTo(-1, 1);
path.lineTo(-2, 1);
path.closePath();
Shape[] arrows = new Shape[4];
for (int i = 0; i < 4; i++) {
final AffineTransform at = new AffineTransform();
at.rotate(i * 0.5 * Math.PI, innerWheelCircle.getCenterX(), innerWheelCircle.getCenterY());
at.translate(innerWheelCircle.getCenterX(), innerWheelCircle.getCenterY() - 0.95 * innerRadius);
at.scale(0.2 * innerRadius, 0.3 * innerRadius);
final Area area = new Area(at.createTransformedShape(path));
area.subtract(new Area(outerMoveCircle));
arrows[i] = area;
}
return arrows;
}
private Area[] createRotationUnitShapes() {
final Area[] areas = new Area[36];
final Area rhs = new Area(innerWheelCircle);
for (int i = 0; i < 36; i++) {
final Arc2D.Double arc = new Arc2D.Double(outerWheelCircle.getX(),
outerWheelCircle.getY(),
outerWheelCircle.getWidth(),
outerWheelCircle.getHeight(),
i * 10 - 0.5 * 7,
7,
Arc2D.PIE);
final Area area = new Area(arc);
area.subtract(rhs);
areas[i] = area;
}
return areas;
}
public void addSelectionListener(SelectionListener l) {
listenerList.add(SelectionListener.class, l);
}
public void removeSelectionListener(SelectionListener l) {
listenerList.remove(SelectionListener.class, l);
}
public SelectionListener[] getSelectionListeners() {
return listenerList.getListeners(SelectionListener.class);
}
protected void fireRotate(double rotationAngle) {
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == SelectionListener.class) {
((SelectionListener) listeners[i + 1]).handleRotate(rotationAngle);
}
}
}
protected void fireMove(double moveDirX, double moveDirY) {
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == SelectionListener.class) {
((SelectionListener) listeners[i + 1]).handleMove(moveDirX, moveDirY);
}
}
}
protected void fireScale(double scaleDir) {
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == SelectionListener.class) {
((SelectionListener) listeners[i + 1]).handleScale(scaleDir);
}
}
}
private double getAngle(Point point) {
final double a = Math.atan2(-(point.y - innerWheelCircle.getCenterY()),
point.x - innerWheelCircle.getCenterX());
return normaliseAngle(a - 0.5 * Math.PI);
}
private static double normaliseAngle(double a) {
while (a < 0.0) {
a += 2.0 * Math.PI;
}
a %= 2.0 * Math.PI;
if (a > Math.PI) {
a += -2.0 * Math.PI;
}
return a;
}
int getAction(int x, int y) {
if (super.contains(x, y)) {
if (outerWheelCircle.contains(x, y) && !innerWheelCircle.contains(x, y)) {
return ACTION_ROT;
} else if (pannerHandle.contains(x, y)) {
return ACTION_PAN;
} else if (scaleHandle.contains(x, y) || scaleBar.contains(x, y)) {
return ACTION_SCALE;
} else {
for (int i = 0; i < moveArrowShapes.length; i++) {
Shape moveArrowShape = moveArrowShapes[i];
if (moveArrowShape.contains(x, y)) {
return ACTION_MOVE_DIRS[i];
}
}
}
}
return ACTION_NONE;
}
public static interface SelectionListener extends EventListener {
void handleRotate(double rotationAngle);
void handleMove(double moveDirX, double moveDirY);
void handleScale(double scaleDir);
}
private final static int ACTION_NONE = 0;
private final static int ACTION_ROT = 1;
private final static int ACTION_SCALE = 2;
private final static int ACTION_PAN = 3;
private final static int ACTION_MOVE_N = 4;
private final static int ACTION_MOVE_S = 5;
private final static int ACTION_MOVE_W = 6;
private final static int ACTION_MOVE_E = 7;
private final static int[] ACTION_MOVE_DIRS = {ACTION_MOVE_N, ACTION_MOVE_S, ACTION_MOVE_W, ACTION_MOVE_E};
private static final double[] X_DIRS = new double[]{0, -1, 0, 1};
private static final double[] Y_DIRS = new double[]{1, 0, -1, 0};
private class MouseHandler extends MouseInputAdapter implements ActionListener {
private Point point0;
private double rotationAngle0;
private double moveDirX;
private double moveDirY;
private double moveAcc;
private double scaleDir;
private double scaleAcc;
private Cursor cursor0;
private final Timer actionTrigger;
private int action; // see MODE_XXX values
private MouseHandler() {
actionTrigger = new Timer(TIMER_DELAY, this);
action = ACTION_NONE;
}
public void actionPerformed(ActionEvent e) {
if (action == ACTION_PAN
|| action == ACTION_MOVE_N
|| action == ACTION_MOVE_S
|| action == ACTION_MOVE_W
|| action == ACTION_MOVE_E) {
fireAcceleratedMove();
} else if (action == ACTION_SCALE) {
fireAcceleratedScale();
}
}
@Override
public void mousePressed(MouseEvent e) {
cursor0 = getCursor();
point0 = e.getPoint();
moveAcc = 1.0;
scaleAcc = 1.0;
action = getAction(e.getX(), e.getY());
if (action == ACTION_ROT) {
rotationAngle0 = getRotationAngle();
} else if (action == ACTION_SCALE) {
doScale(e);
} else if (action == ACTION_MOVE_N) {
startMove(0);
} else if (action == ACTION_MOVE_S) {
startMove(1);
} else if (action == ACTION_MOVE_W) {
startMove(2);
} else if (action == ACTION_MOVE_E) {
startMove(3);
}
if (action != ACTION_NONE) {
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (action == ACTION_ROT) {
doRotate(e);
} else if (action == ACTION_PAN) {
doPan(e);
} else if (action == ACTION_SCALE) {
doScale(e);
}
}
@Override
public void mouseReleased(MouseEvent e) {
stopAction();
}
private void doScale(MouseEvent e) {
final Point point = e.getPoint();
double dx = point.x - scaleBar.getCenterX();
double a = 0.5 * (scaleBar.getWidth() - scaleHandle.getWidth());
if (dx < -a) {
dx = -a;
}
if (dx > +a) {
dx = +a;
}
scaleHandleOffsetX = dx;
scaleHandleOffsetY = 0;
scaleDir = dx / a;
scaleAcc = 1.0;
fireAcceleratedScale();
startTriggeringActions();
}
private void startTriggeringActions() {
System.out.println("NavControl.startTriggeringActions() >>>");
actionTrigger.restart();
}
private void stopTriggeringActions() {
System.out.println("NavControl.stopTriggeringActions() <<<");
actionTrigger.stop();
}
private void doPan(MouseEvent e) {
final Point point = e.getPoint();
final double outerMoveRadius = 0.5 * outerMoveCircle.getWidth();
double dx = point.x - outerMoveCircle.getCenterX();
double dy = point.y - outerMoveCircle.getCenterY();
final double r = Math.sqrt(dx * dx + dy * dy);
if (r > outerMoveRadius) {
dx = outerMoveRadius * dx / r;
dy = outerMoveRadius * dy / r;
}
pannerHandleOffsetX = dx;
pannerHandleOffsetY = dy;
moveDirX = -dx / outerMoveRadius;
moveDirY = -dy / outerMoveRadius;
moveAcc = 1.0;
fireAcceleratedMove();
startTriggeringActions();
}
void startMove(int dir) {
moveDirX = X_DIRS[dir];
moveDirY = Y_DIRS[dir];
doMove();
}
private void doMove() {
moveAcc = 1.0;
startTriggeringActions();
}
private void doRotate(MouseEvent e) {
double a1 = getAngle(point0);
double a2 = getAngle(e.getPoint());
double a = Math.toDegrees(normaliseAngle(Math.toRadians(rotationAngle0) + (a2 - a1)));
if (e.isControlDown()) {
double t = 0.5 * 45.0;
a = t * Math.floor(a / t);
}
setRotationAngle(a);
repaint();
fireRotate(a);
}
private void fireAcceleratedScale() {
fireScale(scaleAcc * scaleDir / 4.0);
scaleAcc *= 1.1;
if (scaleAcc > 4.0) {
scaleAcc = 4.0;
}
}
private void fireAcceleratedMove() {
fireMove(moveAcc * moveDirX, moveAcc * moveDirY);
moveAcc *= 1.05;
if (moveAcc > 4.0) {
moveAcc = 4.0;
}
}
private void stopAction() {
setCursor(cursor0);
stopTriggeringActions();
cursor0 = null;
point0 = null;
action = ACTION_NONE;
moveDirX = 0;
moveDirY = 0;
moveAcc = 1.0;
pannerHandleOffsetX = 0;
pannerHandleOffsetY = 0;
scaleDir = 0;
scaleAcc = 1.0;
scaleHandleOffsetX = 0;
scaleHandleOffsetY = 0;
repaint();
}
}
public static void main(String[] args) {
final JFrame frame = new JFrame("NavControl");
final JPanel panel = new JPanel(new BorderLayout(3, 3));
panel.setBackground(Color.GRAY);
final JLabel label = new JLabel("Angle: ");
final NavControl2 navControl = new NavControl2();
navControl.addSelectionListener(new SelectionListener() {
public void handleRotate(double rotationAngle) {
label.setText("Angle: " + rotationAngle);
System.out.println("NavControl: rotationAngle = " + rotationAngle);
}
public void handleMove(double moveDirX, double moveDirY) {
System.out.println("NavControl: moveDirX = " + moveDirX + ", moveDirY = " + moveDirY);
}
public void handleScale(double scaleDir) {
System.out.println("NavControl: scaleDir = " + scaleDir);
}
});
navControl.setBorder(new EmptyBorder(4, 4, 4, 4));
panel.add(label, BorderLayout.SOUTH);
panel.add(navControl, BorderLayout.CENTER);
frame.add(panel);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
private class I {
int x0;
int y0;
BufferedImage image;
Shape shape;
Shape testShape;
private I(int x0, int y0, BufferedImage image, Shape shape) {
this.x0 = x0;
this.y0 = y0;
this.image = image;
this.shape = shape;
}
private void draw(Graphics2D g ) {
g.drawImage(image, null, x0, y0);
}
private boolean contains(int x, int y) {
return testShape.contains(x - x0 - 0.5* image.getWidth(),
y - y0 - 0.5* image.getHeight());
}
}
}