/*
* $Id$
*
* Copyright (c) 2000-2003 by Rodney Kinney
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.counters;
import java.awt.AlphaComposite;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import VASSAL.build.GameModule;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.Drawable;
import VASSAL.command.ChangeTracker;
import VASSAL.command.Command;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.IntConfigurer;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.i18n.PieceI18nData;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatablePiece;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.imageop.GamePieceOp;
import VASSAL.tools.imageop.Op;
import VASSAL.tools.imageop.RotateScaleOp;
/**
* A Decorator that rotates a GamePiece to an arbitrary angle
*/
public class FreeRotator extends Decorator
implements EditablePiece,
MouseListener,
MouseMotionListener,
Drawable,
TranslatablePiece {
public static final String ID = "rotate;";
public static final String FACING = "_Facing";
public static final String DEGREES = "_Degrees";
public static final double PI_180 = Math.PI / 180.0;
protected KeyCommand setAngleCommand;
protected KeyCommand rotateCWCommand;
protected KeyCommand rotateCCWCommand;
protected KeyCommand[] commands;
protected NamedKeyStroke setAngleKey;
protected String setAngleText = "Rotate";
protected NamedKeyStroke rotateCWKey;
protected String rotateCWText = "Rotate CW";
protected NamedKeyStroke rotateCCWKey;
protected String rotateCCWText = "Rotate CCW";
protected String name = "Rotate";
// for Random Rotate
protected KeyCommand rotateRNDCommand;
protected String rotateRNDText = "";
protected NamedKeyStroke rotateRNDKey;
// END for Random Rotate
protected boolean useUnrotatedShape;
protected double[] validAngles = new double[] {0.0};
protected int angleIndex = 0;
@Deprecated protected java.util.Map<Double,Image> images =
new HashMap<Double,Image>();
protected java.util.Map<Double,Rectangle> bounds =
new HashMap<Double,Rectangle>();
@Deprecated protected PieceImage unrotated;
protected GamePieceOp gpOp;
protected java.util.Map<Double,RotateScaleOp> rotOp =
new HashMap<Double,RotateScaleOp>();
protected double tempAngle, startAngle;
protected Point pivot;
protected boolean drawGhost;
protected Map startMap;
protected Point startPosition;
public FreeRotator() {
// modified for random rotation (added two ; )
this(ID + "6;];[;Rotate CW;Rotate CCW;;;;", null);
}
public FreeRotator(String type, GamePiece inner) {
mySetType(type);
setInner(inner);
}
public String getName() {
return piece.getName();
}
public void setInner(GamePiece p) {
// The GamePiece stack can be in an invalid state during a setInner()
// call, so cannot regenerate gpOp now.
gpOp = null;
super.setInner(p);
}
private double centerX() {
// The center is not on a vertex for pieces with odd widths.
return (piece.boundingBox().width % 2) / 2.0;
}
private double centerY() {
// The center is not on a vertex for pieces with odd heights.
return (piece.boundingBox().height % 2) / 2.0;
}
public Rectangle boundingBox() {
final Rectangle b = piece.boundingBox();
final double angle = getAngle();
if (angle == 0.0) {
return b;
}
Rectangle r;
if ((getGpOp() != null && getGpOp().isChanged()) ||
(r = bounds.get(angle)) == null) {
r = AffineTransform.getRotateInstance(getAngleInRadians(),
centerX(),
centerY())
.createTransformedShape(b).getBounds();
bounds.put(angle, r);
}
return new Rectangle(r);
}
protected GamePieceOp getGpOp() {
if (gpOp == null) {
if (getInner() != null) {
gpOp = Op.piece(getInner());
}
}
return gpOp;
}
public double getAngle() {
return useUnrotatedShape ? 0.0 : validAngles[angleIndex];
}
public double getCumulativeAngle() {
double angle = getAngle();
// Add cumulative angle of any other FreeRotator trait in this piece
FreeRotator nextRotation = (FreeRotator) Decorator.getDecorator(getInner(), FreeRotator.class);
if (nextRotation != null) {
angle += nextRotation.getCumulativeAngle();
}
return angle;
}
public double getCumulativeAngleInRadians() {
return -PI_180 * getCumulativeAngle();
}
public void setAngle(double angle) {
if (validAngles.length == 1) {
validAngles[angleIndex] = angle;
}
else {
// Find nearest valid angle
int newIndex = angleIndex;
double minDist = Math.abs((validAngles[angleIndex] - angle + 360) % 360);
for (int i = 0; i < validAngles.length; ++i) {
if (minDist > Math.abs((validAngles[i] - angle + 360) % 360)) {
newIndex = i;
minDist = Math.abs((validAngles[i] - angle + 360) % 360);
}
}
angleIndex = newIndex;
}
}
/** @deprecated Use {@link boundingBox()} instead. */
@Deprecated
public Rectangle getRotatedBounds() {
return boundingBox();
}
public Shape getShape() {
final Shape s = piece.getShape();
if (getAngle() == 0.0) {
return s;
}
return AffineTransform.getRotateInstance(getAngleInRadians(),
centerX(),
centerY())
.createTransformedShape(s);
}
public double getAngleInRadians() {
return -PI_180 * getAngle();
}
public void mySetType(String type) {
type = type.substring(ID.length());
final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';');
validAngles = new double[Integer.parseInt(st.nextToken())];
for (int i = 0; i < validAngles.length; ++i) {
validAngles[i] = -i * (360.0 / validAngles.length);
}
if (validAngles.length == 1) {
setAngleKey = st.nextNamedKeyStroke(null);
if (st.hasMoreTokens()) {
setAngleText = st.nextToken();
}
}
else {
rotateCWKey = st.nextNamedKeyStroke(null);
rotateCCWKey = st.nextNamedKeyStroke(null);
rotateCWText = st.nextToken("");
rotateCCWText = st.nextToken("");
}
// for random rotation
rotateRNDKey = st.nextNamedKeyStroke(null);
rotateRNDText = st.nextToken("");
// end for random rotation
name = st.nextToken("");
commands = null;
}
public void draw(final Graphics g,
final int x,
final int y,
final Component obs,
final double zoom) {
if (getAngle() == 0.0) {
piece.draw(g, x, y, obs, zoom);
}
else {
final double angle = getAngle();
RotateScaleOp op;
if (getGpOp() != null && getGpOp().isChanged()) {
gpOp = Op.piece(piece);
bounds.clear();
rotOp.clear();
op = Op.rotateScale(gpOp, angle, zoom);
rotOp.put(angle, op);
}
else {
op = rotOp.get(angle);
if (op == null || op.getScale() != zoom) {
op = Op.rotateScale(gpOp, angle, zoom);
rotOp.put(angle, op);
}
}
final Rectangle r = boundingBox();
final Image img = op.getImage();
if (img != null) {
g.drawImage(img, x + (int) (zoom * r.x), y + (int) (zoom * r.y), obs);
}
}
}
public void draw(Graphics g, Map map) {
if (drawGhost) {
final Point p = map.componentCoordinates(getGhostPosition());
final Graphics2D g2d = (Graphics2D) g.create();
g2d.transform(
AffineTransform.getRotateInstance(-PI_180 * tempAngle,
p.x + centerX(),
p.y + centerY()));
g2d.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
piece.draw(g2d, p.x, p.y, map.getView(), map.getZoom());
g2d.dispose();
}
}
public boolean drawAboveCounters() {
return true;
}
private Point getGhostPosition() {
final AffineTransform t =
AffineTransform.getRotateInstance(-PI_180 * (tempAngle - getAngle()),
pivot.x + centerX(),
pivot.y + centerY());
final Point2D newPos2D =
new Point2D.Float(getPosition().x, getPosition().y);
t.transform(newPos2D, newPos2D);
return new Point((int) Math.round(newPos2D.getX()),
(int) Math.round(newPos2D.getY()));
}
public String myGetType() {
final SequenceEncoder se = new SequenceEncoder(';');
se.append(validAngles.length);
if (validAngles.length == 1) {
se.append(setAngleKey);
se.append(setAngleText);
}
else {
se.append(rotateCWKey)
.append(rotateCCWKey)
.append(rotateCWText)
.append(rotateCCWText);
}
// for random rotation
se.append(rotateRNDKey)
.append(rotateRNDText);
// end for random rotation
se.append(name);
return ID + se.getValue();
}
public String myGetState() {
if (validAngles.length == 1) {
return String.valueOf(validAngles[0]);
}
else {
return String.valueOf(angleIndex);
}
}
public void mySetState(String state) {
if (validAngles.length == 1) {
try {
validAngles[0] = Double.valueOf(state).doubleValue();
}
catch (NumberFormatException e) {
reportDataError(this, Resources.getString("Error.non_number_error"), "Angle="+state, e);
}
}
else {
try {
angleIndex = Integer.parseInt(state);
}
catch (NumberFormatException e) {
reportDataError(this, Resources.getString("Error.non_number_error"), "Fixed Angle Index="+state, e);
}
}
}
public KeyCommand[] myGetKeyCommands() {
if (commands == null) {
final ArrayList<KeyCommand> l = new ArrayList<KeyCommand>();
final GamePiece outer = Decorator.getOutermost(this);
setAngleCommand = new KeyCommand(setAngleText, setAngleKey, outer, this);
rotateCWCommand = new KeyCommand(rotateCWText, rotateCWKey, outer, this);
rotateCCWCommand = new KeyCommand(rotateCCWText, rotateCCWKey, outer, this);
// for random rotation
rotateRNDCommand = new KeyCommand(rotateRNDText, rotateRNDKey, outer, this);
// end random rotation
if (validAngles.length == 1) {
if (setAngleText.length() > 0) {
l.add(setAngleCommand);
}
else {
setAngleCommand.setEnabled(false);
}
rotateCWCommand.setEnabled(false);
rotateCCWCommand.setEnabled(false);
}
else {
if (rotateCWText.length() > 0 && rotateCCWText.length() > 0) {
l.add(rotateCWCommand);
l.add(rotateCCWCommand);
}
else if (rotateCWText.length() > 0) {
l.add(rotateCWCommand);
rotateCCWCommand.setEnabled(rotateCCWKey != null);
}
else if (rotateCCWText.length() > 0) {
l.add(rotateCCWCommand);
rotateCWCommand.setEnabled(rotateCWKey != null);
}
setAngleCommand.setEnabled(false);
}
// for random rotate
if (rotateRNDText.length() > 0) {
l.add(rotateRNDCommand);
}
// end for random rotate
commands = l.toArray(new KeyCommand[l.size()]);
}
setAngleCommand.setEnabled(getMap() != null &&
validAngles.length == 1 &&
setAngleText.length() > 0);
return commands;
}
public Command myKeyEvent(KeyStroke stroke) {
myGetKeyCommands();
Command c = null;
if (setAngleCommand.matches(stroke)) {
beginInteractiveRotate();
}
else if (rotateCWCommand.matches(stroke)) {
final ChangeTracker tracker = new ChangeTracker(this);
angleIndex = (angleIndex + 1) % validAngles.length;
c = tracker.getChangeCommand();
}
else if (rotateCCWCommand.matches(stroke)) {
final ChangeTracker tracker = new ChangeTracker(this);
angleIndex = (angleIndex - 1 + validAngles.length) % validAngles.length;
c = tracker.getChangeCommand();
}
// for random rotation
else if (rotateRNDCommand.matches(stroke)) {
final ChangeTracker tracker = new ChangeTracker(this);
// get random #
final Random rand = GameModule.getGameModule().getRNG();
if (validAngles.length == 1) {
// we are a free rotate, set angle to 0-360 use setAngle(double)
setAngle(rand.nextDouble() * 360);
}
else {
// we are set rotate, set angleIndex to a number between 0 and
// validAngles.lenth
angleIndex = (rand.nextInt(validAngles.length));
}
c = tracker.getChangeCommand();
}
// end random rotation
return c;
}
public void beginInteractiveRotate() {
startPosition = getPosition();
startMap = getMap();
startMap.pushMouseListener(this);
startMap.addDrawComponent(this);
final JComponent view = startMap.getView();
view.addMouseMotionListener(this);
view.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
startMap.disableKeyListeners();
pivot = getPosition();
}
public void endInteractiveRotate() {
if (startMap != null) {
startMap.getView().setCursor(null);
startMap.removeDrawComponent(this);
startMap.popMouseListener();
startMap.getView().removeMouseMotionListener(this);
startMap.enableKeyListeners();
drawGhost = false;
startMap = null;
}
}
/**
* Has the piece been moved by a Global key command since interactive
* rotate mode was turned on?
*/
public boolean hasPieceMoved() {
final Map m = getMap();
final Point p = getPosition();
return m == null || m != startMap || p == null || !p.equals(startPosition);
}
/** The point around which the piece will pivot while rotating interactively */
public void setPivot(int x, int y) {
pivot = new Point(x, y);
}
public void mouseClicked(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
if (hasPieceMoved()) {
endInteractiveRotate();
return;
}
drawGhost = true;
startAngle = getRelativeAngle(e.getPoint(), getPosition());
}
public void mouseReleased(MouseEvent e) {
if (hasPieceMoved()) {
endInteractiveRotate();
return;
}
final Map m = getMap();
try {
final Point ghostPosition = getGhostPosition();
Command c = null;
final ChangeTracker tracker = new ChangeTracker(this);
if (!getPosition().equals(ghostPosition)) {
final GamePiece outer = Decorator.getOutermost(this);
outer.setProperty(Properties.MOVED, Boolean.TRUE);
c = m.placeOrMerge(outer, m.snapTo(ghostPosition));
}
setAngle(tempAngle);
c = tracker.getChangeCommand().append(c);
GameModule.getGameModule().sendAndLog(c);
}
finally {
endInteractiveRotate();
}
}
public void setProperty(Object key, Object val) {
if (Properties.USE_UNROTATED_SHAPE.equals(key)) {
useUnrotatedShape = Boolean.TRUE.equals(val);
}
super.setProperty(key, val);
}
@Override
public Object getLocalizedProperty(Object key) {
if ((name + FACING).equals(key)) {
return String.valueOf(angleIndex + 1);
}
else if ((name + DEGREES).equals(key)) {
return String.valueOf((int) (Math.abs(validAngles[angleIndex])));
}
else {
return super.getLocalizedProperty(key);
}
}
public Object getProperty(Object key) {
if ((name + FACING).equals(key)) {
return String.valueOf(angleIndex + 1);
}
else if ((name + DEGREES).equals(key)) {
return String.valueOf((int) (Math.abs(validAngles[angleIndex])));
}
else {
return super.getProperty(key);
}
}
public void mouseDragged(MouseEvent e) {
if (drawGhost) {
final Point mousePos = getMap().mapCoordinates(e.getPoint());
final double myAngle = getRelativeAngle(mousePos, pivot);
tempAngle = getAngle() - (myAngle - startAngle)/PI_180;
}
getMap().repaint();
}
private double getRelativeAngle(Point p, Point origin) {
double myAngle;
if (p.y == origin.y) {
myAngle = p.x < origin.x ? -Math.PI/2.0 : Math.PI/2.0;
}
else {
myAngle = Math.atan((double)(p.x - origin.x) / (double)(origin.y - p.y));
if (origin.y < p.y) {
myAngle += Math.PI;
}
}
return myAngle;
}
public void mouseMoved(MouseEvent e) {
if (hasPieceMoved()) {
endInteractiveRotate();
return;
}
}
/**
* Return a full-scale cached image of this piece, rotated to the appropriate
* angle.
*
* @param angle
* @param obs
* @return
* @deprecated Use a {@link GamePieceOp} if you need this Image.
*/
@Deprecated
public Image getRotatedImage(double angle, Component obs) {
if (gpOp == null) return null;
if (gpOp.isChanged()) gpOp = Op.piece(piece);
return Op.rotateScale(gpOp, angle, 1.0).getImage();
}
public String getDescription() {
String d = "Can Rotate";
if (name.length() > 0) {
d += " - " + name;
}
return d;
}
public VASSAL.build.module.documentation.HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("Rotate.htm");
}
public PieceEditor getEditor() {
return new Ed(this);
}
public PieceI18nData getI18nData() {
return getI18nData(new String[] {setAngleText, rotateCWText, rotateCCWText, rotateRNDText},
new String[] {getCommandDescription(name, "Set Angle command"), getCommandDescription(name, "Rotate CW command"), getCommandDescription(name, "Rotate CCW command"), getCommandDescription(name, "Rotate Random command")});
}
/**
* Return Property names exposed by this trait
*/
public List<String> getPropertyNames() {
ArrayList<String> l = new ArrayList<String>();
l.add(name + FACING);
l.add(name + DEGREES);
return l;
}
private static class Ed implements PieceEditor, PropertyChangeListener {
private BooleanConfigurer anyConfig;
private NamedHotKeyConfigurer anyKeyConfig;
private IntConfigurer facingsConfig;
private NamedHotKeyConfigurer cwKeyConfig;
private NamedHotKeyConfigurer ccwKeyConfig;
// random rotate
private NamedHotKeyConfigurer rndKeyConfig;
// end random rotate
private StringConfigurer nameConfig;
private JTextField anyCommand;
private JTextField cwCommand;
private JTextField ccwCommand;
private JTextField rndCommand;
private Box anyControls;
private Box cwControls;
private Box ccwControls;
private Box rndControls;
private JPanel panel;
public Ed(FreeRotator p) {
nameConfig = new StringConfigurer(null, "Description: ", p.name);
cwKeyConfig = new NamedHotKeyConfigurer(null, "Command to rotate clockwise: ", p.rotateCWKey);
ccwKeyConfig = new NamedHotKeyConfigurer(null, "Command to rotate counterclockwise: ", p.rotateCCWKey);
// random rotate
rndKeyConfig = new NamedHotKeyConfigurer(null, "Command to rotate randomly: ", p.rotateRNDKey);
// end random rotate
anyConfig = new BooleanConfigurer(null, "Allow arbitrary rotations",
Boolean.valueOf(p.validAngles.length == 1));
anyKeyConfig = new NamedHotKeyConfigurer(null, "Command to rotate: ", p.setAngleKey);
facingsConfig = new IntConfigurer(null, "Number of allowed facings: ", p.validAngles.length == 1 ? 6 : p.validAngles.length);
panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.add(nameConfig.getControls());
panel.add(facingsConfig.getControls());
cwControls = Box.createHorizontalBox();
cwControls.add(cwKeyConfig.getControls());
cwControls.add(new JLabel(" Menu text: "));
cwCommand = new JTextField(12);
cwCommand.setMaximumSize(cwCommand.getPreferredSize());
cwCommand.setText(p.rotateCWText);
cwControls.add(cwCommand);
panel.add(cwControls);
ccwControls = Box.createHorizontalBox();
ccwControls.add(ccwKeyConfig.getControls());
ccwControls.add(new JLabel(" Menu text: "));
ccwCommand = new JTextField(12);
ccwCommand.setMaximumSize(ccwCommand.getPreferredSize());
ccwCommand.setText(p.rotateCCWText);
ccwControls.add(ccwCommand);
panel.add(ccwControls);
panel.add(anyConfig.getControls());
anyControls = Box.createHorizontalBox();
anyControls.add(anyKeyConfig.getControls());
anyControls.add(new JLabel(" Menu text: "));
anyCommand = new JTextField(12);
anyCommand.setMaximumSize(anyCommand.getPreferredSize());
anyCommand.setText(p.setAngleText);
anyControls.add(anyCommand);
panel.add(anyControls);
// random rotate
rndControls = Box.createHorizontalBox();
rndControls.add(rndKeyConfig.getControls());
rndControls.add(new JLabel(" Menu text: "));
rndCommand = new JTextField(12);
rndCommand.setMaximumSize(rndCommand.getPreferredSize());
rndCommand.setText(p.rotateRNDText);
rndControls.add(rndCommand);
panel.add(rndControls);
// end random rotate
anyConfig.addPropertyChangeListener(this);
propertyChange(null);
}
public void propertyChange(PropertyChangeEvent evt) {
final boolean any = Boolean.TRUE.equals(anyConfig.getValue());
anyControls.setVisible(any);
facingsConfig.getControls().setVisible(!any);
cwControls.setVisible(!any);
ccwControls.setVisible(!any);
panel.revalidate();
}
public Component getControls() {
return panel;
}
public String getType() {
final SequenceEncoder se = new SequenceEncoder(';');
if (Boolean.TRUE.equals(anyConfig.getValue())) {
se.append("1")
.append(anyKeyConfig.getValueString())
.append(anyCommand.getText() == null
? "" : anyCommand.getText().trim());
}
else {
se.append(facingsConfig.getValueString())
.append(cwKeyConfig.getValueString())
.append(ccwKeyConfig.getValueString())
.append(cwCommand.getText() == null
? "" : cwCommand.getText().trim())
.append(ccwCommand.getText() == null
? "" : ccwCommand.getText().trim());
}
// random rotate
se.append(rndKeyConfig.getValueString())
.append(rndCommand.getText() == null
? "" : rndCommand.getText().trim());
// end random rotate
se.append(nameConfig.getValueString());
return ID + se.getValue();
}
public String getState() {
return "0";
}
}
}