/*
* $Id$
*
* Copyright (c) 2003-2012 by Rodney Kinney, Brent Easton
* GridLocation modifications copyright (c) 2010-2011 by Pieter Geerkens
*
* 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.Component;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import VASSAL.build.GameModule;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords;
import VASSAL.build.module.map.boardPicker.board.Region;
import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone;
import VASSAL.command.ChangeTracker;
import VASSAL.command.Command;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.ChooseComponentDialog;
import VASSAL.configure.FormattedExpressionConfigurer;
import VASSAL.configure.FormattedStringConfigurer;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.configure.PropertyExpression;
import VASSAL.configure.PropertyExpressionConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.configure.StringEnumConfigurer;
import VASSAL.i18n.PieceI18nData;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatablePiece;
import VASSAL.tools.FormattedString;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.SequenceEncoder;
/**
* This trait adds a command that sends a piece to another location. Options for the
* target location are:
* <li>Specified x,y co-ords on a named map/board</li>
* <li>The centre of a named Zone on a named map</li>
* <li>A named Region on a named map</li>
* <li>The location of another counter selected by a Property Match String</li>
* <li>A specified grid-location on a given board & map </li>
* <p>Once the target location is identified, it can be further offset in the X and Y directions
* by a set of multipliers.
* All Input Fields may use $...$ variable names
*/
public class SendToLocation extends Decorator implements TranslatablePiece {
public static final String ID = "sendto;";
private static final String _0 = "0";
public static final String BACK_MAP = "backMap";
public static final String BACK_POINT = "backPoint";
protected static final String DEST_GRIDLOCATION = "Grid location on selected Map";
protected static final String DEST_LOCATION = "Location on selected Map";
protected static final String DEST_ZONE = "Zone on selected Map";
protected static final String DEST_REGION = "Region on selected Map";
protected static final String DEST_COUNTER = "Another counter, selected by properties";
protected static final String[] DEST_OPTIONS = {
DEST_GRIDLOCATION, DEST_LOCATION, DEST_ZONE, DEST_REGION, DEST_COUNTER
};
protected KeyCommand[] command;
protected String commandName;
protected String backCommandName;
protected NamedKeyStroke key;
protected NamedKeyStroke backKey;
protected FormattedString mapId = new FormattedString("");
protected FormattedString boardName = new FormattedString("");
protected FormattedString x = new FormattedString("");
protected FormattedString xIndex = new FormattedString("");
protected FormattedString xOffset = new FormattedString("");
protected FormattedString y = new FormattedString("");
protected FormattedString yIndex = new FormattedString("");
protected FormattedString yOffset = new FormattedString("");
protected FormattedString gridLocation = new FormattedString("");
protected KeyCommand sendCommand;
protected KeyCommand backCommand;
protected String description;
protected String destination;
protected FormattedString zone = new FormattedString("");
protected FormattedString region = new FormattedString("");
protected PropertyExpression propertyFilter = new PropertyExpression("");
private Map map;
// private Point dest;
public SendToLocation() {
this(ID + ";;;;0;0;;;", null);
}
public SendToLocation(String type, GamePiece inner) {
mySetType(type);
setInner(inner);
}
public void mySetType(String type) {
type = type.substring(ID.length());
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, ';');
commandName = st.nextToken("");
key = st.nextNamedKeyStroke(null);
mapId.setFormat(st.nextToken(""));
boardName.setFormat(st.nextToken(""));
x.setFormat(st.nextToken("0"));
y.setFormat(st.nextToken("0"));
backCommandName = st.nextToken("");
backKey = st.nextNamedKeyStroke(null);
xIndex.setFormat(st.nextToken("0"));
yIndex.setFormat(st.nextToken("0"));
xOffset.setFormat(st.nextToken("0"));
yOffset.setFormat(st.nextToken("0"));
description = st.nextToken("");
destination = st.nextToken(DEST_LOCATION.substring(0,1));
if (destination.length() == 0) {
destination = DEST_LOCATION.substring(0,1);
}
zone.setFormat(st.nextToken(""));
region.setFormat(st.nextToken(""));
propertyFilter.setExpression(st.nextToken(""));
gridLocation.setFormat(st.nextToken(""));
}
public String myGetType() {
final SequenceEncoder se = new SequenceEncoder(';');
se.append(commandName)
.append(key)
.append(mapId.getFormat())
.append(boardName.getFormat())
.append(x.getFormat())
.append(y.getFormat())
.append(backCommandName)
.append(backKey)
.append(xIndex.getFormat())
.append(yIndex.getFormat())
.append(xOffset.getFormat())
.append(yOffset.getFormat())
.append(description)
.append(destination)
.append(zone.getFormat())
.append(region.getFormat())
.append(propertyFilter.getExpression())
.append(gridLocation.getFormat());
return ID + se.getValue();
}
protected KeyCommand[] myGetKeyCommands() {
if (command == null) {
sendCommand = new KeyCommand(commandName, key, Decorator.getOutermost(this), this);
backCommand = new KeyCommand(backCommandName, backKey, Decorator.getOutermost(this), this);
ArrayList<KeyCommand> l = new ArrayList<KeyCommand>();
if (commandName.length() > 0 && key != null && !key.isNull()) {
l.add(sendCommand);
}
if (backCommandName.length() > 0 && backKey != null && !backKey.isNull()) {
l.add(backCommand);
}
command = l.toArray(new KeyCommand[l.size()]);
}
for (KeyCommand c : command ) {
if (c.getName().equals(backCommandName)) {
c.setEnabled(getMap() != null &&
getProperty(BACK_MAP) != null &&
getProperty(BACK_POINT) != null);
}
else {
Point p = getSendLocation();
c.setEnabled(getMap() != null && p != null &&
(map != getMap() || !p.equals(getPosition())) );
}
}
return command;
}
private void LogBadGridLocation ( Point p) {
String s = "* " + Decorator.getOutermost(this).getName();
if (getMap() == null) {
s += "getMap is null";
}
else if ( p == null) {
s += "p is null";
}
else {
s += "getMap: " + getMap().getMapName() +
"; p: (" + p.x + "," + p.y +
"; Position: (" + getPosition().x + "," + getPosition().y +
"); map: " + map.getMapName() + ";";
}
new Chatter.DisplayText(
GameModule.getGameModule().getChatter(),s).execute();
}
public String myGetState() {
SequenceEncoder se = new SequenceEncoder(';');
Map backMap = (Map)getProperty(BACK_MAP);
if (backMap != null) {
se.append(backMap.getIdentifier());
}
else {
se.append("");
}
Point backPoint = (Point)getProperty(BACK_POINT);
if (backPoint != null) {
se.append(backPoint.x).append(backPoint.y);
}
else {
se.append("").append("");
}
return se.getValue();
}
private Point getSendLocation() {
GamePiece outer = Decorator.getOutermost(this);
map = null;
Point dest = null;
// Home in on a counter
if (destination.equals(DEST_COUNTER.substring(0, 1))) {
GamePiece target = null;
// Find first counter matching the properties
for (GamePiece piece :
GameModule.getGameModule().getGameState().getAllPieces()) {
if (piece instanceof Stack) {
Stack s = (Stack) piece;
for (int i = 0; i < s.getPieceCount(); i++) {
if (propertyFilter.accept(this, s.getPieceAt(i))) {
target = s.getPieceAt(i);
if (target != null) break;
}
}
}
else {
if (propertyFilter.accept(this, piece)) {
target = piece;
}
}
if (target != null) break;
}
// Determine target's position
if (target != null) {
map = target.getMap();
if (map != null) {
dest = target.getPosition();
}
}
}
// Location/Zone/Region processing all use specified map
else {
map = Map.getMapById(mapId.getText(outer));
if (map == null) {
map = getMap();
}
if (map != null) {
Board b;
switch (destination.charAt(0)) {
case 'G':
b = map.getBoardByName(boardName.getText(outer));
if (b != null ) {
try {
dest = b.getGrid().getLocation(gridLocation.getText(outer));
if (dest != null) dest.translate(b.bounds().x, b.bounds().y);
}
catch (BadCoords e) {
LogBadGridLocation(dest);
reportDataError(this, Resources.getString(
"Error.not_found", "Grid Location"),map.getMapName());
; // ignore SendTo request.
}
}
break;
case 'L':
final int xValue = x.getTextAsInt(outer, "Xlocation", this);
final int yValue = y.getTextAsInt(outer, "YLocation", this);
dest = new Point(xValue,yValue);
b = map.getBoardByName(boardName.getText(outer));
if (b != null && dest != null) {
dest.translate(b.bounds().x, b.bounds().y);
}
break;
case 'Z':
final String zoneName = zone.getText(outer);
Zone z = map.findZone(zoneName);
if (z == null) {
reportDataError(this, Resources.getString("Error.not_found", "Zone"), zone.debugInfo(zoneName, "Zone"));
}
else {
Rectangle r = z.getBounds();
Rectangle r2 = z.getBoard().bounds();
dest = new Point(r2.x + r.x + r.width/2, r2.y + r.y + r.height/2);
}
break;
case 'R':
final String regionName = region.getText(outer);
Region r = map.findRegion(regionName);
if (r == null) {
reportDataError(this, Resources.getString("Error.not_found", "Region"), region.debugInfo(regionName, "Region"));
}
else {
Rectangle r2 = r.getBoard().bounds();
if (r != null) {
dest = new Point(r.getOrigin().x + r2.x, r.getOrigin().y + r2.y);
}
}
break;
}
}
}
// Offset destination by Advanced Options offsets
if ((dest != null) && (destination.charAt(0) != 'G')) {
dest = offsetDestination(dest.x, dest.y, outer);
}
return dest;
}
public Command myKeyEvent(KeyStroke stroke) {
Command c = null;
myGetKeyCommands();
if (sendCommand.matches(stroke)) {
GamePiece outer = Decorator.getOutermost(this);
Stack parent = outer.getParent();
Point dest = getSendLocation();
if (map != null && dest != null) {
if (map == getMap() && dest.equals(getPosition())) {
// don't do anything if we're already there.
return null;
}
setProperty(BACK_MAP, getMap());
setProperty(BACK_POINT, getPosition());
setOldProperties();
if (!Boolean.TRUE.equals(outer.getProperty(Properties.IGNORE_GRID))) {
dest = map.snapTo(dest);
}
c = map.placeOrMerge(outer, dest);
// Apply Auto-move key
if (map.getMoveKey() != null) {
c.append(outer.keyEvent(map.getMoveKey()));
}
if (parent != null) {
c.append(parent.pieceRemoved(outer));
}
}
}
else if (backCommand.matches(stroke)) {
GamePiece outer = Decorator.getOutermost(this);
Map backMap = (Map) getProperty(BACK_MAP);
Point backPoint = (Point) getProperty(BACK_POINT);
if (backMap != null && backPoint != null) {
setOldProperties();
c = backMap.placeOrMerge(outer, backPoint);
final ChangeTracker tracker = new ChangeTracker(this);
setProperty(BACK_MAP, null);
setProperty(BACK_POINT, null);
c.append(tracker.getChangeCommand());
// Apply Auto-move key
if (backMap.getMoveKey() != null) {
c.append(outer.keyEvent(backMap.getMoveKey()));
}
}
setProperty(BACK_MAP, null);
setProperty(BACK_POINT, null);
}
return c;
}
/*
* Offset the destination by the Advanced Options offset
*/
protected Point offsetDestination(int x, int y, GamePiece outer) {
int xPos = x + parse("xIndex", xIndex, outer) * parse("xOffset", xOffset, outer);
int yPos = y + parse("yIndex", yIndex, outer) * parse("yOffset", yOffset, outer);
return new Point(xPos, yPos);
}
private int parse (String desc, FormattedString s, GamePiece outer) {
int i = 0;
String val = s.getText(outer, _0);
try {
i = Integer.parseInt(val);
}
catch (NumberFormatException e) {
reportDataError(this, Resources.getString("Error.non_number_error"), s.debugInfo(val, desc), e);
}
return i;
}
public void mySetState(String newState) {
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(newState,';');
String mapId = st.nextToken("");
if (mapId.length() > 0) {
setProperty(BACK_MAP,Map.getMapById(mapId));
}
String x = st.nextToken("");
String y = st.nextToken("");
if (x.length() > 0 && y.length()> 0) {
try {
setProperty(BACK_POINT, new Point(Integer.parseInt(x), Integer.parseInt(y)));
}
catch (NumberFormatException e) {
reportDataError(this, Resources.getString("Error.non_number_error"), "Back Point=("+x+","+y+")", e);
}
}
}
public Rectangle boundingBox() {
return piece.boundingBox();
}
public void draw(Graphics g, int x, int y, Component obs, double zoom) {
piece.draw(g, x, y, obs, zoom);
}
public String getName() {
return piece.getName();
}
public Shape getShape() {
return piece.getShape();
}
public PieceEditor getEditor() {
return new Ed(this);
}
public String getDescription() {
String d = "Send to Location";
if (description.length() > 0) {
d += " - " + description;
}
return d;
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("SendToLocation.htm");
}
public PieceI18nData getI18nData() {
return getI18nData(new String[] {commandName, backCommandName},
new String[] {getCommandDescription(description, "Send command"), getCommandDescription(description, "Back command")});
}
public static class Ed implements PieceEditor {
protected StringConfigurer nameInput;
protected StringConfigurer backNameInput;
protected NamedHotKeyConfigurer keyInput;
protected NamedHotKeyConfigurer backKeyInput;
protected FormattedStringConfigurer mapIdInput;
protected FormattedStringConfigurer boardNameInput;
protected FormattedStringConfigurer xInput;
protected FormattedStringConfigurer yInput;
protected BooleanConfigurer advancedInput;
protected FormattedStringConfigurer xIndexInput;
protected FormattedStringConfigurer xOffsetInput;
protected FormattedStringConfigurer yIndexInput;
protected FormattedStringConfigurer yOffsetInput;
protected StringConfigurer descInput;
protected StringEnumConfigurer destInput;
protected PropertyExpressionConfigurer propertyInput;
protected FormattedStringConfigurer zoneInput;
protected FormattedStringConfigurer regionInput;
//protected Map map;
protected JPanel controls;
protected Box mapControls;
protected Box boardControls;
protected Box advancedControls;
protected StringConfigurer gridLocationInput;
public Ed(SendToLocation p) {
controls = new JPanel();
controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
descInput = new StringConfigurer(null, "Description: ", p.description);
controls.add(descInput.getControls());
nameInput = new StringConfigurer(null, "Command name: ", p.commandName);
controls.add(nameInput.getControls());
keyInput = new NamedHotKeyConfigurer(null,"Keyboard Command: ",p.key);
controls.add(keyInput.getControls());
backNameInput = new StringConfigurer(null, "Send Back Command name: ", p.backCommandName);
controls.add(backNameInput.getControls());
backKeyInput = new NamedHotKeyConfigurer(null,"Send Back Keyboard Command: ",p.backKey);
controls.add(backKeyInput.getControls());
destInput = new StringEnumConfigurer(null, "Destination: ", DEST_OPTIONS);
destInput.setValue(DEST_LOCATION);
for (int i=0; i < DEST_OPTIONS.length; i++) {
if (DEST_OPTIONS[i].substring(0,1).equals(p.destination)) {
destInput.setValue(DEST_OPTIONS[i]);
}
}
destInput.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent arg0) {
updateVisibility();
}});
controls.add(destInput.getControls());
mapControls = Box.createHorizontalBox();
mapIdInput = new FormattedExpressionConfigurer(null, "Map: ", p.mapId.getFormat(), p);
mapControls.add(mapIdInput.getControls());
JButton select = new JButton("Select");
select.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
selectMap();
}
});
mapControls.add(select);
JButton clear = new JButton("Clear");
clear.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
clearMap();
}
});
mapControls.add(clear);
controls.add(mapControls);
boardControls = Box.createHorizontalBox();
boardNameInput = new FormattedExpressionConfigurer(null, "Board: ", p.boardName.getFormat(), p);
boardControls.add(boardNameInput.getControls());
select = new JButton("Select");
select.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
selectBoard();
}
});
clear = new JButton("Clear");
clear.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
clearBoard();
}
});
boardControls.add(select);
boardControls.add(clear);
controls.add(boardControls);
xInput = new FormattedExpressionConfigurer(null, "X Position: ", p.x.getFormat(), p);
controls.add(xInput.getControls());
yInput = new FormattedExpressionConfigurer(null, "Y Position: ", p.y.getFormat(), p);
controls.add(yInput.getControls());
zoneInput = new FormattedExpressionConfigurer(null, "Zone Name: ", p.zone.getFormat(), p);
controls.add(zoneInput.getControls());
regionInput = new FormattedExpressionConfigurer(null, "Region Name: ", p.region.getFormat(), p);
controls.add(regionInput.getControls());
propertyInput = new PropertyExpressionConfigurer(null, "Property Match: ", p.propertyFilter);
controls.add(propertyInput.getControls());
gridLocationInput = new StringConfigurer(null, "Grid Location: ", p.gridLocation.getFormat());
controls.add(gridLocationInput.getControls());
advancedInput = new BooleanConfigurer(null, "Advanced Options", false);
advancedInput.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent arg0) {
updateVisibility();
}});
controls.add(advancedInput.getControls());
advancedControls = Box.createHorizontalBox();
xIndexInput = new FormattedExpressionConfigurer(null, "Additional X offset: ", p.xIndex.getFormat(), p);
advancedControls.add(xIndexInput.getControls());
xOffsetInput = new FormattedExpressionConfigurer(null, " times ", p.xOffset.getFormat(), p);
advancedControls.add(xOffsetInput.getControls());
controls.add(advancedControls);
advancedControls = Box.createHorizontalBox();
yIndexInput = new FormattedExpressionConfigurer(null, "Additional Y offset: ", p.yIndex.getFormat(), p);
advancedControls.add(yIndexInput.getControls());
yOffsetInput = new FormattedExpressionConfigurer(null, " times ", p.yOffset.getFormat(), p);
advancedControls.add(yOffsetInput.getControls());
controls.add(advancedControls);
updateVisibility();
}
private void updateVisibility() {
// boolean advancedVisible = advancedInput.booleanValue().booleanValue();
advancedInput.getControls().setVisible(!destInput.getValue().equals(DEST_GRIDLOCATION));
boolean advancedVisible = advancedInput.booleanValue().booleanValue()
&& advancedInput.getControls().isVisible();
xIndexInput.getControls().setVisible(advancedVisible);
xOffsetInput.getControls().setVisible(advancedVisible);
yIndexInput.getControls().setVisible(advancedVisible);
yOffsetInput.getControls().setVisible(advancedVisible);
String destOption = destInput.getValueString();
xInput.getControls().setVisible(destOption.equals(DEST_LOCATION));
yInput.getControls().setVisible(destOption.equals(DEST_LOCATION));
mapControls.setVisible(!destOption.equals(DEST_COUNTER));
boardControls.setVisible(destOption.equals(DEST_LOCATION)
|| destOption.equals(DEST_GRIDLOCATION));
zoneInput.getControls().setVisible(destOption.equals(DEST_ZONE));
regionInput.getControls().setVisible(destOption.equals(DEST_REGION));
propertyInput.getControls().setVisible(destOption.equals(DEST_COUNTER));
gridLocationInput.getControls().setVisible(destOption.equals(DEST_GRIDLOCATION));
Window w = SwingUtilities.getWindowAncestor(controls);
if (w != null) {
w.pack();
}
}
private void clearBoard() {
boardNameInput.setValue("");
}
private void clearMap() {
//map = null;
mapIdInput.setValue("");
}
private void selectBoard() {
ChooseComponentDialog d = new ChooseComponentDialog((Frame) SwingUtilities.getAncestorOfClass(Frame.class, controls), Board.class);
d.setVisible(true);
if (d.getTarget() != null) {
Board b = (Board) d.getTarget();
boardNameInput.setValue(b.getName());
}
}
private void selectMap() {
ChooseComponentDialog d = new ChooseComponentDialog((Frame) SwingUtilities.getAncestorOfClass(Frame.class, controls), Map.class);
d.setVisible(true);
if (d.getTarget() != null) {
Map map = (Map) d.getTarget();
mapIdInput.setValue(map.getMapName());
}
}
public Component getControls() {
return controls;
}
public String getType() {
SequenceEncoder se = new SequenceEncoder(';');
se.append(nameInput.getValueString())
.append(keyInput.getValueString())
//.append(map == null ? "" : map.getIdentifier())
.append(mapIdInput.getValueString())
.append(boardNameInput.getValueString())
.append(xInput.getValueString())
.append(yInput.getValueString())
.append(backNameInput.getValueString())
.append(backKeyInput.getValueString())
.append(xIndexInput.getValueString())
.append(yIndexInput.getValueString())
.append(xOffsetInput.getValueString())
.append(yOffsetInput.getValueString())
.append(descInput.getValueString())
.append(destInput.getValueString().charAt(0))
.append(zoneInput.getValueString())
.append(regionInput.getValueString())
.append(propertyInput.getValueString())
.append(gridLocationInput.getValueString());
return ID + se.getValue();
}
public String getState() {
return "";
}
}
}