/* * $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; } }