/*
* $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.build.module.map;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.util.Iterator;
import javax.swing.KeyStroke;
import VASSAL.build.AbstractConfigurable;
import VASSAL.build.BadDataReport;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.GameState;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.command.AddPiece;
import VASSAL.command.Command;
import VASSAL.command.MoveTracker;
import VASSAL.command.NullCommand;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.HotKeyConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.counters.BasicPiece;
import VASSAL.counters.GamePiece;
import VASSAL.counters.Highlighter;
import VASSAL.counters.PieceFilter;
import VASSAL.counters.PieceIterator;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;
import VASSAL.i18n.Resources;
import VASSAL.tools.ErrorDialog;
/**
* Encapsulates information on how to draw expanded and unexpanded
* views of a stack
*/
public class StackMetrics extends AbstractConfigurable {
protected int exSepX, exSepY;
protected int unexSepX, unexSepY;
protected boolean disabled = false;
protected KeyStroke topKey = KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0);
protected KeyStroke bottomKey = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
protected KeyStroke upKey = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
protected KeyStroke downKey = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0);
protected PieceFilter unselectedVisible;
protected PieceFilter selectedVisible;
protected Color blankColor;
public static final String EXSEP_X = "exSepX";
public static final String EXSEP_Y = "exSepY";
public static final String UNEXSEP_X = "unexSepX";
public static final String UNEXSEP_Y = "unexSepY";
public static final String DISABLED = "disabled";
public static final String TOP_KEY = "top";
public static final String BOTTOM_KEY = "bottom";
public static final String UP_KEY = "up";
public static final String DOWN_KEY = "down";
public static final String COLOR = "color";
public static int DEFAULT_EXSEP_X = 6;
public static int DEFAULT_EXSEP_Y = 18;
public static int DEFAULT_UNEXSEP_X = 2;
public static int DEFAULT_UNEXSEP_Y = 4;
protected Map map;
public void setAttribute(String name, Object value) {
if (EXSEP_X.equals(name)) {
if (value instanceof String) {
try {
exSepX = Integer.parseInt((String) value);
}
catch (NumberFormatException NaN) {
exSepX = DEFAULT_EXSEP_X;
ErrorDialog.dataError(
new BadDataReport(
Resources.getString("Error.bad_preference", EXSEP_X, "StackMetrics"), (String) value, NaN));
}
}
else if (value != null) {
exSepX = ((Integer) value).intValue();
}
}
else if (EXSEP_Y.equals(name)) {
if (value instanceof String) {
try {
exSepY = Integer.parseInt((String) value);
}
catch (NumberFormatException NaN) {
exSepY = DEFAULT_EXSEP_Y;
ErrorDialog.dataError(
new BadDataReport(
Resources.getString("Error.bad_preference", EXSEP_Y, "StackMetrics"), (String) value, NaN));
}
}
else if (value != null) {
exSepY = ((Integer) value).intValue();
}
}
else if (UNEXSEP_X.equals(name)) {
if (value instanceof String) {
try {
unexSepX = Integer.parseInt((String) value);
}
catch (NumberFormatException NaN) {
unexSepX = DEFAULT_UNEXSEP_X;
ErrorDialog.dataError(
new BadDataReport(
Resources.getString("Error.bad_preference", UNEXSEP_X, "StackMetrics"), (String) value, NaN));
}
}
else if (value != null) {
unexSepX = ((Integer) value).intValue();
}
}
else if (UNEXSEP_Y.equals(name)) {
if (value instanceof String) {
try {
unexSepY = Integer.parseInt((String) value);
}
catch (NumberFormatException NaN) {
unexSepY = DEFAULT_UNEXSEP_Y;
ErrorDialog.dataError(
new BadDataReport(
Resources.getString("Error.bad_preference", UNEXSEP_Y, "StackMetrics"), (String) value, NaN));
}
}
else if (value != null) {
unexSepY = ((Integer) value).intValue();
}
}
else if (DISABLED.equals(name)) {
if (value instanceof String) {
value = Boolean.valueOf((String) value);
}
disabled = ((Boolean) value).booleanValue();
}
else if (TOP_KEY.equals(name)) {
topKey = HotKeyConfigurer.decode((String) value);
}
else if (BOTTOM_KEY.equals(name)) {
bottomKey = HotKeyConfigurer.decode((String) value);
}
else if (UP_KEY.equals(name)) {
upKey = HotKeyConfigurer.decode((String) value);
}
else if (DOWN_KEY.equals(name)) {
downKey = HotKeyConfigurer.decode((String) value);
}
else if (COLOR.equals(name)) {
if (value instanceof String) {
value = ColorConfigurer.stringToColor((String) value);
}
blankColor = (Color) value;
}
}
public String getAttributeValueString(String name) {
if (EXSEP_X.equals(name)) {
return String.valueOf(exSepX);
}
else if (EXSEP_Y.equals(name)) {
return String.valueOf(exSepY);
}
else if (UNEXSEP_X.equals(name)) {
return String.valueOf(unexSepX);
}
else if (UNEXSEP_Y.equals(name)) {
return String.valueOf(unexSepY);
}
else if (DISABLED.equals(name)) {
return String.valueOf(disabled);
}
else if (TOP_KEY.equals(name)) {
return HotKeyConfigurer.encode(topKey);
}
else if (BOTTOM_KEY.equals(name)) {
return HotKeyConfigurer.encode(bottomKey);
}
else if (UP_KEY.equals(name)) {
return HotKeyConfigurer.encode(upKey);
}
else if (DOWN_KEY.equals(name)) {
return HotKeyConfigurer.encode(downKey);
}
else if (COLOR.equals(name)) {
return blankColor == null ? null
: ColorConfigurer.colorToString(blankColor);
}
return null;
}
public void addTo(Buildable b) {
map = (Map) b;
map.setStackMetrics(this);
}
public StackMetrics() {
this(false, DEFAULT_EXSEP_X, DEFAULT_EXSEP_Y, DEFAULT_UNEXSEP_X, DEFAULT_UNEXSEP_Y);
}
public StackMetrics(boolean dis,
int exSx, int exSy,
int unexSx, int unexSy) {
disabled = dis;
exSepX = exSx;
exSepY = exSy;
unexSepX = unexSx;
unexSepY = unexSy;
unselectedVisible = new PieceFilter() {
public boolean accept(GamePiece piece) {
return !Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME))
&& !Boolean.TRUE.equals(piece.getProperty(Properties.SELECTED));
}
};
selectedVisible = new PieceFilter() {
public boolean accept(GamePiece piece) {
return !Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME))
&& Boolean.TRUE.equals(piece.getProperty(Properties.SELECTED));
}
};
}
/**
* Different instances of StackMetrics may render a {@link Stack}
* in different ways. The default algorithm is: If not expanded,
* all but the top visible GamePiece is drawn as a white square
* with size given by {@link GamePiece#getShape}. The
* separation between GamePieces is given by {@link
* #relativePosition}
*
* If expanded, all GamePieces are drawn with separation given by
* {@link #relativePosition}. GamePiece that are selected are
* drawn in front of other GamePieces, even those above them in
* the stack.
*/
public void draw(Stack stack, Graphics g, int x, int y, Component obs, double zoom) {
Highlighter highlighter = stack.getMap() == null ? BasicPiece.getHighlighter() : stack.getMap().getHighlighter();
Point[] positions = new Point[stack.getPieceCount()];
getContents(stack, positions, null, null, x, y);
for (PieceIterator e = new PieceIterator(stack.getPiecesIterator(),
unselectedVisible);
e.hasMoreElements();) {
GamePiece next = e.nextPiece();
int index = stack.indexOf(next);
int nextX = x + (int) (zoom * (positions[index].x - x));
int nextY = y + (int) (zoom * (positions[index].y - y));
if (stack.isExpanded() || !e.hasMoreElements()) {
next.draw(g,
nextX,
nextY,
obs, zoom);
}
else {
drawUnexpanded(next, g, nextX, nextY, obs, zoom);
}
}
for (PieceIterator e = new PieceIterator(stack.getPiecesIterator(),
selectedVisible);
e.hasMoreElements();) {
GamePiece next = e.nextPiece();
int index = stack.indexOf(next);
int nextX = x + (int) (zoom * (positions[index].x - x));
int nextY = y + (int) (zoom * (positions[index].y - y));
next.draw(g,
nextX,
nextY,
obs, zoom);
highlighter.draw
(next, g,
nextX,
nextY,
obs, zoom);
}
}
/**
* Draw only those pieces in the target stack whose boundingBoxes fall within the given visibleRect
* This method is considerably faster than the other draw method.
* @param stack
* @param g
* @param location the location of the stack in component coordinates
* @param zoom
* @param visibleRect the visible rectangle in component coordinates
*/
public void draw(Stack stack, Point location, Graphics g, Map map, double zoom, Rectangle visibleRect) {
Highlighter highlighter = map.getHighlighter();
Point mapLocation = map.mapCoordinates(location);
Rectangle region = visibleRect == null ? null : map.mapRectangle(visibleRect);
Point[] positions = new Point[stack.getPieceCount()];
Rectangle[] bounds = region == null ? null : new Rectangle[stack.getPieceCount()];
getContents(stack, positions, null, bounds, mapLocation.x, mapLocation.y);
for (PieceIterator e = new PieceIterator(stack.getPiecesIterator(),
unselectedVisible);
e.hasMoreElements();) {
GamePiece next = e.nextPiece();
int index = stack.indexOf(next);
Point pt = map.componentCoordinates(positions[index]);
if (bounds == null || isVisible(region, bounds[index])) {
if (stack.isExpanded() || !e.hasMoreElements()) {
next.draw(g,
pt.x,
pt.y,
map.getView(), zoom);
}
else {
drawUnexpanded(next, g, pt.x, pt.y, map.getView(), zoom);
}
}
}
for (PieceIterator e = new PieceIterator(stack.getPiecesIterator(),
selectedVisible);
e.hasMoreElements();) {
GamePiece next = e.nextPiece();
int index = stack.indexOf(next);
if (bounds == null || isVisible(region, bounds[index])) {
Point pt = map.componentCoordinates(positions[index]);
next.draw(g,
pt.x,
pt.y,
map.getView(), zoom);
highlighter.draw
(next, g,
pt.x,
pt.y,
map.getView(), zoom);
}
}
}
private boolean isVisible(Rectangle region, Rectangle bounds) {
boolean visible = true;
if (region != null) {
visible = region.intersects(bounds);
}
return visible;
}
/**
* Draw a {@link GamePiece} that is not the top piece in an unexpanded {@link Stack}
*
* Default implementation is a white square with a black border
*/
protected void drawUnexpanded(GamePiece p, Graphics g,
int x, int y, Component obs, double zoom) {
if (blankColor == null) {
p.draw(g, x, y, obs, zoom);
}
else {
Graphics2D g2d = (Graphics2D) g;
g.setColor(blankColor);
Shape s = p.getShape();
AffineTransform t = AffineTransform.getScaleInstance(zoom,zoom);
t.translate(x/zoom,y/zoom);
s = t.createTransformedShape(s);
g2d.fill(s);
g.setColor(Color.black);
g2d.draw(s);
}
}
/**
* The color used to draw boxes representing counters beneath the top one in a stack.
* A value of null indicates that the counters should be drawn fully
* @return
*/
public Color getBlankColor() {
return blankColor;
}
/**
* Fill the argument arrays with the positions, selection bounds and bounding boxes of the pieces in the argument stack
* @param parent The parent Stack
* @param positions If non-null will contain a {@link Point} giving the position of each piece in <code>parent</code>
* @param shapes If non-null will contain a {@link Shape} giving the shape of for each piece in <code>parent</code>
* @param boundingBoxes If non-null will contain a {@link Rectangle} giving the bounding box for each piece in <code>parent</code>
* @param x the x-location of the parent
* @param y the y-location of the parent
* @return the number of pieces processed in the stack
*/
public int getContents(Stack parent, Point[] positions, Shape[] shapes, Rectangle[] boundingBoxes, int x, int y) {
int count = parent.getMaximumVisiblePieceCount();
if (positions != null) {
count = Math.min(count, positions.length);
}
if (boundingBoxes != null) {
count = Math.min(count, boundingBoxes.length);
}
if (shapes != null) {
count = Math.min(count,shapes.length);
}
int dx = parent.isExpanded() ? exSepX : unexSepX;
int dy = parent.isExpanded() ? exSepY : unexSepY;
Point currentPos = null, nextPos = null;
Rectangle currentSelBounds = null, nextSelBounds = null;
for (int index = 0; index < count; ++index) {
GamePiece child = parent.getPieceAt(index);
if (Boolean.TRUE.equals(child.getProperty(Properties.INVISIBLE_TO_ME))) {
Rectangle blank = new Rectangle(x, y, 0, 0);
if (positions != null) {
positions[index] = blank.getLocation();
}
if (boundingBoxes != null) {
boundingBoxes[index] = blank;
}
if (shapes != null) {
shapes[index] = blank;
}
}
else {
child.setProperty(Properties.USE_UNROTATED_SHAPE,Boolean.TRUE);
nextSelBounds = child.getShape().getBounds();
child.setProperty(Properties.USE_UNROTATED_SHAPE,Boolean.FALSE);
nextPos = new Point(0,0);
if (currentPos == null) {
currentSelBounds = nextSelBounds;
currentSelBounds.translate(x, y);
currentPos = new Point(x, y);
nextPos = currentPos;
}
else {
nextPosition(currentPos, currentSelBounds, nextPos, nextSelBounds, dx, dy);
}
if (positions != null) {
positions[index] = nextPos;
}
if (boundingBoxes != null) {
Rectangle bbox = child.boundingBox();
bbox.translate(nextPos.x, nextPos.y);
boundingBoxes[index] = bbox;
}
if (shapes != null) {
Shape s = child.getShape();
s = AffineTransform.getTranslateInstance(nextPos.x,nextPos.y).createTransformedShape(s);
shapes[index] = s;
}
currentPos = nextPos;
currentSelBounds = nextSelBounds;
}
}
return count;
}
protected void nextPosition(Point currentPos, Rectangle currentBounds, Point nextPos, Rectangle nextBounds, int dx, int dy) {
int deltaX,deltaY;
if (dx > 0) {
deltaX = currentBounds.x + dx - nextBounds.x;
}
else if (dx < 0) {
deltaX = currentBounds.x + currentBounds.width - nextBounds.width + dx - nextBounds.x;
}
else {
deltaX = currentPos.x - nextPos.x;
}
if (dy > 0) {
deltaY = currentBounds.y + currentBounds.height - nextBounds.height - nextBounds.y - dy;
}
else if (dy < 0) {
deltaY = currentBounds.y - dy - nextBounds.y;
}
else {
deltaY = currentPos.y - nextPos.y;
}
nextBounds.translate(deltaX, deltaY);
nextPos.translate(deltaX, deltaY);
}
public Point relativePosition(Stack parent, GamePiece c) {
final int index =
Math.min(parent.indexOf(c),parent.getMaximumVisiblePieceCount()-1);
if (index < 0) {
return new Point(0,0);
}
final Point[] pos = new Point[parent.getMaximumVisiblePieceCount()];
getContents(parent, pos, null, null, 0, 0);
return pos[index];
}
public boolean isStackingEnabled() {
return !disabled;
}
public void removeFrom(Buildable parent) {
}
public String getConfigureName() {
return null;
}
public static String getConfigureTypeName() {
return Resources.getString("Editor.Stacking.component_type"); //$NON-NLS-1$
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("Map.htm", "StackingOptions");
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class<?>[0];
}
public String[] getAttributeNames() {
return new String[] {
DISABLED,
EXSEP_X,
EXSEP_Y,
UNEXSEP_X,
UNEXSEP_Y,
COLOR,
TOP_KEY,
BOTTOM_KEY,
UP_KEY,
DOWN_KEY
};
}
public String[] getAttributeDescriptions() {
return new String[]{
Resources.getString("Editor.Stacking.disable"), //$NON-NLS-1$
Resources.getString("Editor.Stacking.h_expand"), //$NON-NLS-1$
Resources.getString("Editor.Stacking.v_expand"), //$NON-NLS-1$
Resources.getString("Editor.Stacking.hnon_expand"), //$NON-NLS-1$
Resources.getString("Editor.Stacking.vnon_expand"), //$NON-NLS-1$
Resources.getString("Editor.Stacking.color_nonexpand"), //$NON-NLS-1$
};
}
public Class<?>[] getAttributeTypes() {
return new Class<?>[]{
Boolean.class,
Integer.class,
Integer.class,
Integer.class,
Integer.class,
Color.class
};
}
private VisibilityCondition cond = new VisibilityCondition() {
public boolean shouldBeVisible() {
return !disabled;
}
};
public VisibilityCondition getAttributeVisibility(String name) {
if (name.equals(EXSEP_X)
|| name.equals(EXSEP_Y)
|| name.equals(UNEXSEP_X)
|| name.equals(UNEXSEP_Y)
|| name.equals(COLOR)) {
return cond;
}
else {
return null;
}
}
public Stack createStack(GamePiece p) {
return createStack(p, false);
}
public Stack createStack(GamePiece p, boolean force) {
return isStackingEnabled() || force ? new Stack(p) : null;
}
public KeyStroke getMoveUpKey() {
return upKey;
}
public KeyStroke getMoveDownKey() {
return downKey;
}
public KeyStroke getMoveTopKey() {
return topKey;
}
public KeyStroke getMoveBottomKey() {
return bottomKey;
}
/**
* Merge the two pieces if stacking is enabled.
* If stacking is disabled, place the moving piece at the same location as the fixed piece
* @param fixed
* @param moving
* @return a Command that accomplishes this task
* @see #merge
*/
public Command placeOrMerge(GamePiece fixed, GamePiece moving) {
if (disabled) {
return fixed.getMap().placeAt(moving,fixed.getPosition());
}
else {
return merge(fixed,moving);
}
}
/**
* Place a GamePiece on top of another GamePiece
* Create/remove stacks as necessary, even if stacking is disabled for this instance
* @param moving the GamePiece that will be merged into the stack
* @param fixed the GamePiece defining the location and contents of the existing stack
* @return a Command that accomplishes this task
*/
public Command merge(GamePiece fixed, GamePiece moving) {
Command comm;
if (fixed instanceof Stack && ((Stack) fixed).topPiece() != null) {
comm = merge(((Stack) fixed).topPiece(), moving);
}
else {
final MoveTracker tracker = new MoveTracker(moving);
comm = new NullCommand();
Stack fixedParent = fixed.getParent();
int index = fixedParent == null ? 0 : fixedParent.indexOf(fixed) + 1;
if (moving != fixed && moving != fixedParent) {
final GameState gs = GameModule.getGameModule().getGameState();
final boolean isNewPiece = gs.getPieceForId(moving.getId()) == null;
if (fixedParent == null) {
if (fixed instanceof Stack) {
fixedParent = (Stack) fixed;
index = fixedParent.getPieceCount();
}
else {
fixedParent = createStack(fixed, true);
comm = comm.append(fixedParent.getMap().placeAt(
fixedParent, fixedParent.getPosition()));
index = 1;
}
}
if (isNewPiece) {
gs.addPiece(moving);
comm = comm.append(new AddPiece(moving));
}
if (moving instanceof Stack) {
for (Iterator<GamePiece> i = ((Stack) moving).getPiecesIterator();
i.hasNext(); ) {
final GamePiece p = i.next();
final MoveTracker t = new MoveTracker(p);
fixedParent.insertChild(p, index++);
comm = comm.append(t.getMoveCommand());
}
}
else {
if (moving.getParent() == fixedParent && fixedParent != null) {
index--;
}
fixedParent.insert(moving, index);
comm = comm.append(tracker.getMoveCommand());
}
}
}
return comm;
}
}