/*
* $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.
*/
/*
* Created by IntelliJ IDEA.
* User: rkinney
* Date: Jul 21, 2002
* Time: 10:18:26 PM
* To change template for new class use
* Code Style | Class Templates options (Tools | IDE Options).
*/
package VASSAL.build.module.map.boardPicker.board.mapgrid;
import java.awt.Color;
import java.awt.Container;
import java.awt.Point;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JComponent;
import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords;
import VASSAL.configure.AutoConfigurer;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.FormattedStringConfigurer;
import VASSAL.configure.StringEnum;
import VASSAL.configure.VisibilityCondition;
import VASSAL.tools.FormattedString;
import VASSAL.tools.SequenceEncoder;
/**
* Abstract base class for grid numbering classes for hexagonal and rectangular grids
*/
public abstract class RegularGridNumbering extends AbstractConfigurable implements GridNumbering {
protected PropertyChangeSupport propSupport = new PropertyChangeSupport(this);
protected char first = 'H';
protected String sep = "";
protected char hType = 'N';
protected char vType = 'N';
protected int hLeading = 1;
protected int vLeading = 1;
protected int hOff = 1;
protected int vOff = 1;
protected boolean hDescending = false;
protected boolean vDescending = false;
protected boolean visible = false;
protected int fontSize = 9;
protected Color color = Color.black;
protected int rotateTextDegrees = 0;
protected int hDrawOff = 0;
protected int vDrawOff = 0;
protected JComponent visualizer;
protected String locationFormat = "$" + GRID_LOCATION + "$";
protected FormattedString format = new FormattedString();
public static final String FIRST = "first";
public static final String SEP = "sep";
public static final String H_TYPE = "hType";
public static final String V_TYPE = "vType";
public static final String H_LEADING = "hLeading";
public static final String V_LEADING = "vLeading";
public static final String H_OFF = "hOff";
public static final String V_OFF = "vOff";
public static final String V_DESCEND = "vDescend";
public static final String H_DESCEND = "hDescend";
public static final String FONT_SIZE = "fontSize";
public static final String COLOR = "color";
public static final String VISIBLE = "visible";
public static final String ROTATE_TEXT = "rotateText";
public static final String H_DRAW_OFF = "hDrawOff";
public static final String V_DRAW_OFF = "vDrawOff";
public static final String LOCATION_FORMAT = "locationFormat";
public static final String GRID_LOCATION = "gridLocation";
public static final String ROW = "row";
public static final String COLUMN = "column";
public String getAttributeValueString(String key) {
if (FIRST.equals(key)) {
return String.valueOf(first);
}
else if (SEP.equals(key)) {
return sep;
}
else if (H_TYPE.equals(key)) {
return String.valueOf(hType);
}
else if (V_TYPE.equals(key)) {
return String.valueOf(vType);
}
else if (H_LEADING.equals(key)) {
return String.valueOf(hLeading);
}
else if (V_LEADING.equals(key)) {
return String.valueOf(vLeading);
}
else if (H_OFF.equals(key)) {
return String.valueOf(hOff);
}
else if (V_OFF.equals(key)) {
return String.valueOf(vOff);
}
else if (H_DESCEND.equals(key)) {
return String.valueOf(hDescending);
}
else if (V_DESCEND.equals(key)) {
return String.valueOf(vDescending);
}
else if (FONT_SIZE.equals(key)) {
return String.valueOf(fontSize);
}
else if (COLOR.equals(key)) {
return ColorConfigurer.colorToString(color);
}
else if (VISIBLE.equals(key)) {
return String.valueOf(visible);
}
else if (LOCATION_FORMAT.equals(key)) {
return locationFormat;
}
else if (ROTATE_TEXT.equals(key)) {
return String.valueOf(rotateTextDegrees);
}
else if (H_DRAW_OFF.equals(key)) {
return String.valueOf(hDrawOff);
}
else if (V_DRAW_OFF.equals(key)) {
return String.valueOf(vDrawOff);
}
else {
return null;
}
}
public void setAttribute(String key, Object value) {
if (FIRST.equals(key)) {
first = ((String) value).charAt(0);
}
else if (SEP.equals(key)) {
sep = (String) value;
}
else if (H_TYPE.equals(key)) {
hType = ((String) value).charAt(0);
}
else if (V_TYPE.equals(key)) {
vType = ((String) value).charAt(0);
}
else if (H_LEADING.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
hLeading = ((Integer) value).intValue();
}
else if (V_LEADING.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
vLeading = ((Integer) value).intValue();
}
else if (H_OFF.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
hOff = ((Integer) value).intValue();
}
else if (V_OFF.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
vOff = ((Integer) value).intValue();
}
else if (H_DESCEND.equals(key)) {
if (value instanceof String) {
value = Boolean.valueOf((String) value);
}
hDescending = ((Boolean) value).booleanValue();
}
else if (V_DESCEND.equals(key)) {
if (value instanceof String) {
value = Boolean.valueOf((String) value);
}
vDescending = ((Boolean) value).booleanValue();
}
else if (FONT_SIZE.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
fontSize = ((Integer) value).intValue();
}
else if (COLOR.equals(key)) {
if (value instanceof String) {
value = ColorConfigurer.stringToColor((String) value);
}
color = (Color) value;
}
else if (VISIBLE.equals(key)) {
if (value instanceof String) {
value = Boolean.valueOf((String) value);
}
visible = ((Boolean) value).booleanValue();
}
else if (LOCATION_FORMAT.equals(key)) {
locationFormat = (String) value;
}
else if (ROTATE_TEXT.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
rotateTextDegrees = ((Integer) value).intValue();
}
else if (H_DRAW_OFF.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
hDrawOff = ((Integer) value).intValue();
}
else if (V_DRAW_OFF.equals(key)) {
if (value instanceof String) {
value = Integer.valueOf((String) value);
}
vDrawOff = ((Integer) value).intValue();
}
}
public boolean isVisible() {
return visible;
}
public abstract int getRow(Point p);
public abstract int getColumn(Point p);
public void addPropertyChangeListener(PropertyChangeListener l) {
propSupport.addPropertyChangeListener(l);
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class[0];
}
public String[] getAttributeNames() {
return new String[]{FIRST, SEP, H_TYPE, H_LEADING, H_OFF, H_DESCEND,
V_TYPE, V_LEADING, V_OFF, V_DESCEND, LOCATION_FORMAT, VISIBLE, FONT_SIZE, COLOR,
ROTATE_TEXT, H_DRAW_OFF, V_DRAW_OFF};
}
public String[] getAttributeDescriptions() {
return new String[]{"Order: ",
"Separator: ",
"Horizontal numbering: ",
"Leading zeros in horizontal: ",
"Starting number in horizontal: ",
"Horizontal numbering descending?",
"Vertical numbering: ",
"Leading zeros in vertical: ",
"Starting number in vertical: ",
"Vertical numbering descending?",
"Location format: ",
"Draw Numbering?",
"Font size: ",
"Color: ",
"Rotate text (Degrees): ",
"Text X offset: ",
"Text Y offset: "};
}
public static class F extends StringEnum {
public String[] getValidValues(AutoConfigurable target) {
return new String[]{"Horizontal first", "Vertical first"};
}
}
public static class T extends StringEnum {
public String[] getValidValues(AutoConfigurable target) {
return new String[]{"Numerical", "Alphabetic"};
}
}
public static class LocationFormatConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new FormattedStringConfigurer(key, name, new String[]{GRID_LOCATION,ROW,COLUMN});
}
}
public static class R extends StringEnum {
public String[] getValidValues(AutoConfigurable target) {
return new String[]{"0", "90", "180", "270"};
}
}
public Class<?>[] getAttributeTypes() {
return new Class<?>[]{
F.class,
String.class,
T.class,
Integer.class,
Integer.class,
Boolean.class,
T.class,
Integer.class,
Integer.class,
Boolean.class,
LocationFormatConfig.class,
Boolean.class,
Integer.class,
Color.class,
R.class,
Integer.class,
Integer.class
};
}
/**
* Return a component that shows how the grid will draw itself
*/
protected abstract JComponent getGridVisualizer();
public VisibilityCondition getAttributeVisibility(String name) {
if (FONT_SIZE.equals(name)
|| COLOR.equals(name)) {
VisibilityCondition cond = new VisibilityCondition() {
public boolean shouldBeVisible() {
return visible;
}
};
return cond;
}
else if (H_LEADING.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return hType == 'N';
}
};
}
else if (V_LEADING.equals(name)) {
return new VisibilityCondition() {
public boolean shouldBeVisible() {
return vType == 'N';
}
};
}
else {
return super.getAttributeVisibility(name);
}
}
public Configurer getConfigurer() {
AutoConfigurer c = (AutoConfigurer) super.getConfigurer();
String[] s = getAttributeNames();
for (int i = 0; i < s.length; ++i) {
c.getConfigurer(s[i]).addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
visualizer.repaint();
}
});
}
((Container) c.getControls()).add(getGridVisualizer());
return c;
}
public static String getConfigureTypeName() {
return "Grid Numbering";
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("GridNumbering.htm");
}
protected String getName(int row, int column) {
String rowName = getName(row + vOff, vType, vLeading);
String colName = getName(column + hOff, hType, hLeading);
switch (first) {
case 'H':
return colName + sep + rowName;
default:
return rowName + sep + colName;
}
}
// This appears to be the most efficient way to accomplish this without
// using groups. It also helps when there is no separator between alphabetic
// coordinates (as long as both coordinates don't use the same letter).
// AAFF is not ambiguous, but AAAA is. Coordinates like 04AB will fail.
static final String ALPHABETIC_MATCH = "-?(?:A+|B+|C+|D+|E+|F+|G+|H+|I+|J+|K+|L+|M+|N+|O+|P+|Q+|R+|S+|T+|U+|V+|W+|X+|Y+|Z+)";
protected String getMatchingPattern(char type, int leading) {
if (type == 'A')
return ALPHABETIC_MATCH;
else
return "-?[0-9]{" + (leading+1) + ",}";
}
public Point getLocation(String location) throws BadCoords {
SequenceEncoder.Decoder se = new SequenceEncoder.Decoder(locationFormat, '$');
boolean isProperty = true;
final StringBuilder regex = new StringBuilder();
int colGroup = 0;
int rowGroup = 0;
int groupCount = 0;
while (se.hasMoreTokens()) {
String token = se.nextToken();
isProperty = !isProperty;
if (token.length() > 0) {
if (!isProperty || !se.hasMoreTokens()) {
regex.append(Pattern.quote(token));
}
else if (token.equals(GRID_LOCATION)) {
if (first == 'H') {
regex.append('(').append(getMatchingPattern(hType, hLeading)).append(')');
colGroup = ++groupCount;
if (sep.length() > 0)
regex.append(Pattern.quote(sep));
regex.append('(').append(getMatchingPattern(vType, vLeading)).append(')');
rowGroup = ++groupCount;
}
else {
regex.append('(').append(getMatchingPattern(vType, vLeading)).append(')');
rowGroup = ++groupCount;
regex.append(Pattern.quote(sep));
regex.append('(').append(getMatchingPattern(hType, hLeading)).append(')');
colGroup = ++groupCount;
}
}
else if (token.equals(ROW)) {
regex.append('(').append(getMatchingPattern(vType, vLeading)).append(')');
rowGroup = ++groupCount;
}
else if (token.equals(COLUMN)) {
regex.append('(').append(getMatchingPattern(hType, hLeading)).append(')');
colGroup = ++groupCount;
}
}
}
if (regex.length() == 0 || colGroup == 0 || rowGroup == 0)
throw new BadCoords();
Pattern pattern = Pattern.compile(regex.toString());
Matcher matcher = pattern.matcher(location);
if (!matcher.matches()) {
// FIXME: rename to BadCoordsException
throw new BadCoords();
}
assert(matcher.groupCount() == groupCount && groupCount >= 2);
String rowName = location.substring(matcher.start(rowGroup), matcher.end(rowGroup));
String colName = location.substring(matcher.start(colGroup), matcher.end(colGroup));
int row = parseName(rowName, vType);
int col = parseName(colName, hType);
return getCenterPoint(col-hOff, row-vOff);
}
public abstract Point getCenterPoint(int col, int row);
public String locationName(Point pt) {
int row = getRow(pt);
int col = getColumn(pt);
format.setFormat(locationFormat);
format.setProperty(GRID_LOCATION, getName(row, col));
format.setProperty(ROW, getName(row+vOff, vType, vLeading));
format.setProperty(COLUMN, getName(col+hOff, hType, hLeading));
return format.getLocalizedText();
}
public String localizedLocationName(Point pt) {
return locationName(pt);
}
public static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
protected int parseName(String name, char type) {
int value = 0;
switch (type) {
case 'A': // Alphabetic
int index = 0;
boolean negative = false;
if (name.startsWith("-")) {
negative = true;
++index;
}
while (index < name.length() && Character.isUpperCase(name.charAt(index))) {
if (index < name.length()-1)
value += 26;
else
value += ALPHABET.indexOf(name.charAt(index));
++index;
}
if (negative)
value *= -1;
break;
default: // Numeric
value = Integer.parseInt(name);
}
return value;
}
protected String getName(int rowOrColumn, char type, int leading) {
String val = rowOrColumn < 0 ? "-" : "";
rowOrColumn = Math.abs(rowOrColumn);
switch (type) {
case 'A': // Alphabetic
do {
val += ALPHABET.charAt(rowOrColumn % 26);
rowOrColumn -= 26;
} while (rowOrColumn >= 0);
return val;
default: // Numeric
while (leading > 0 && rowOrColumn < Math.pow(10.0, leading--)) {
val += "0";
}
return val + rowOrColumn;
}
}
/*
* Translate the label center point based on the x, Y offset and
* the rotation factor
*/
public Point offsetLabelCenter(Point p, double zoom) {
return offsetLabelCenter(p.x, p.y, zoom);
}
public Point offsetLabelCenter(int x, int y, double zoom) {
Point n = new Point(x, y);
switch (rotateTextDegrees) {
case 0:
break;
case 90:
n.x = y;
n.y = -x;
break;
case 180:
n.x = -x;
n.y = -y;
break;
case 270:
n.x = -y;
n.y = x;
break;
default :
break;
}
n.x += (hDrawOff * zoom);
n.y += (vDrawOff * zoom);
return n;
}
}