package net.sf.colossus.gui;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.lang.ref.WeakReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.common.Constants;
import net.sf.colossus.server.VariantSupport;
import net.sf.colossus.util.StaticResourceLoader;
import net.sf.colossus.variant.MasterHex;
/**
* Class GUIMasterHex holds GUI information for a MasterHex.
*
* @author David Ripton
* @author Romain Dolbeau
*/
public final class GUIMasterHex extends GUIHex<MasterHex>
{
private static final Logger LOGGER = Logger.getLogger(GUIMasterHex.class
.getName());
private boolean inverted;
private FontMetrics fontMetrics;
private int halfFontHeight;
private Point offCenter;
private WeakReference<MasterBoard> weakBoardRef;
private GeneralPath highlightBorder;
private Color selectColor = Color.white;
// The hex vertexes are numbered like this:
//
// normal inverted
//
// 0------1 0------------1
// / \ / \
// / \ / \
// / \ / \
// / \ 5 2
// / \ \ /
// / \ \ /
// 5 2 \ /
// \ / \ /
// \ / \ /
// \ / \ /
// 4------------3 4------3
// Use two-stage initialization so that we can clone the GUIMasterHex
// from an existing MasterHex, then add the GUI info.
GUIMasterHex(MasterHex model)
{
super(model);
}
void init(int cx, int cy, int scale, boolean inverted, MasterBoard board)
{
this.inverted = inverted;
this.weakBoardRef = new WeakReference<MasterBoard>(board);
len = scale / 3.0;
if (inverted)
{
xVertex[0] = cx - scale;
yVertex[0] = cy;
xVertex[1] = cx + 3 * scale;
yVertex[1] = cy;
xVertex[2] = cx + 4 * scale;
yVertex[2] = cy + SQRT3 * scale;
xVertex[3] = cx + 2 * scale;
yVertex[3] = cy + 3 * SQRT3 * scale;
xVertex[4] = cx;
yVertex[4] = cy + 3 * SQRT3 * scale;
xVertex[5] = cx - 2 * scale;
yVertex[5] = cy + SQRT3 * scale;
}
else
{
xVertex[0] = cx;
yVertex[0] = cy;
xVertex[1] = cx + 2 * scale;
yVertex[1] = cy;
xVertex[2] = cx + 4 * scale;
yVertex[2] = cy + 2 * SQRT3 * scale;
xVertex[3] = cx + 3 * scale;
yVertex[3] = cy + 3 * SQRT3 * scale;
xVertex[4] = cx - scale;
yVertex[4] = cy + 3 * SQRT3 * scale;
xVertex[5] = cx - 2 * scale;
yVertex[5] = cy + 2 * SQRT3 * scale;
}
hexagon = makePolygon(6, xVertex, yVertex, true);
rectBound = hexagon.getBounds();
offCenter = new Point((int)Math.round((xVertex[0] + xVertex[1]) / 2),
(int)Math.round(((yVertex[0] + yVertex[3]) / 2)
+ (inverted ? -(scale / 6.0) : (scale / 6.0))));
Point2D.Double center = findCenter2D();
final double innerScale = 0.8;
AffineTransform at = AffineTransform.getScaleInstance(innerScale,
innerScale);
highlightBorder = (GeneralPath)hexagon.createTransformedShape(at);
// Translate innerHexagon to make it concentric.
Rectangle2D innerBounds = highlightBorder.getBounds2D();
Point2D.Double innerCenter = new Point2D.Double(innerBounds.getX()
+ innerBounds.getWidth() / 2.0, innerBounds.getY()
+ innerBounds.getHeight() / 2.0);
at = AffineTransform.getTranslateInstance(
center.getX() - innerCenter.getX(),
center.getY() - innerCenter.getY());
highlightBorder.transform(at);
highlightBorder.append(hexagon, false);
}
@Override
public void paint(Graphics g)
{
if (hexagon == null)
{
return;
}
Graphics2D g2 = (Graphics2D)g;
Font oldFont = g2.getFont();
g2.setFont(oldFont.deriveFont(oldFont.getSize2D() * 0.9f));
fontMetrics = g2.getFontMetrics();
halfFontHeight = (fontMetrics.getMaxAscent() + fontMetrics
.getLeading()) / 2;
if (getAntialias())
{
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
else
{
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
}
MasterHex model = this.getHexModel();
g2.setColor(model.getTerrainColor());
g2.fill(hexagon);
g2.setColor(Color.black);
g2.draw(hexagon);
// Draw exits and entrances
for (int i = inverted ? 0 : 1; i < 6; i += 2)
{
int n = (i + 1) % 6;
// Draw exits
// There are up to 3 gates to draw. Each is 1/6 of a hexside
// square. The first is positioned from 1/6 to 1/3 of the way
// along the hexside, the second from 5/12 to 7/12, and the
// third from 2/3 to 5/6. The inner edge of each is 1/12 of a
// hexside inside the hexside, and the outer edge is 1/12 of a
// hexside outside the hexside.
if (model.getExitType(i) != Constants.HexsideGates.NONE)
{
drawGate(g2, xVertex[i], yVertex[i], xVertex[n], yVertex[n],
model.getExitType(i));
}
// Draw entrances
// Unfortunately, since exits extend out into adjacent hexes,
// they sometimes get overdrawn. So we need to draw them
// again from the other hex, as entrances.
if (model.getEntranceType(i) != Constants.HexsideGates.NONE)
{
drawGate(g2, xVertex[n], yVertex[n], xVertex[i], yVertex[i],
model.getEntranceType(i));
}
}
paintLabel(g2);
if (!(useOverlay && paintOverlay(g2)))
{
paintTerrainName(g2);
}
g2.setFont(oldFont);
}
public void paintHighlightIfNeeded(Graphics2D g2)
{
if (isSelected())
{
g2.setColor(this.selectColor);
g2.fill(highlightBorder);
}
}
private int stringWidth(String s, Graphics2D g2)
{
return (int)Math.round(fontMetrics.getStringBounds(s, g2).getWidth());
}
private void paintLabel(Graphics2D g2)
{
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
MasterHex model = this.getHexModel();
String label = model.getLabel();
switch (model.getLabelSide())
{
case 0:
g2.drawString(label, rectBound.x
+ ((rectBound.width - stringWidth(label, g2)) / 2),
rectBound.y + halfFontHeight + rectBound.height / 10);
break;
case 1:
g2.drawString(label, rectBound.x
+ ((rectBound.width - stringWidth(label, g2)) * 5 / 6),
rectBound.y + halfFontHeight + rectBound.height / 8);
break;
case 2:
g2.drawString(label, rectBound.x
+ (rectBound.width - stringWidth(label, g2)) * 5 / 6,
rectBound.y + halfFontHeight + rectBound.height * 7 / 8);
break;
case 3:
g2.drawString(label, rectBound.x
+ ((rectBound.width - stringWidth(label, g2)) / 2),
rectBound.y + halfFontHeight + rectBound.height * 9 / 10);
break;
case 4:
g2.drawString(label, rectBound.x
+ (rectBound.width - stringWidth(label, g2)) / 6,
rectBound.y + halfFontHeight + rectBound.height * 5 / 6);
break;
case 5:
g2.drawString(label, rectBound.x
+ (rectBound.width - stringWidth(label, g2)) / 6,
rectBound.y + halfFontHeight + rectBound.height / 8);
break;
}
}
private void paintTerrainName(Graphics2D g2)
{
// Do not anti-alias text.
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
if (fontMetrics == null)
{
fontMetrics = g2.getFontMetrics();
halfFontHeight = (fontMetrics.getMaxAscent() + fontMetrics
.getLeading()) / 2;
}
fontMetrics = g2.getFontMetrics();
halfFontHeight = (fontMetrics.getMaxAscent() + fontMetrics
.getLeading()) / 2;
String name = this.getHexModel().getTerrainDisplayName().toUpperCase();
g2.drawString(name,
rectBound.x + ((rectBound.width - stringWidth(name, g2)) / 2),
rectBound.y + halfFontHeight + rectBound.height
* (isInverted() ? 1 : 2) / 3);
}
@Override
public void repaint()
{
MasterBoard board = weakBoardRef.get();
if (board == null)
{
return;
}
board.repaint(rectBound.x, rectBound.y, rectBound.width,
rectBound.height);
}
private void drawGate(Graphics2D g2, double vx1, double vy1, double vx2,
double vy2, Constants.HexsideGates gateType)
{
double x0; // first focus point
double y0;
double x1; // second focus point
double y1;
double x2; // center point
double y2;
double theta; // gate angle
double[] x = new double[4]; // gate points
double[] y = new double[4];
x0 = vx1 + (vx2 - vx1) / 6;
y0 = vy1 + (vy2 - vy1) / 6;
x1 = vx1 + (vx2 - vx1) / 3;
y1 = vy1 + (vy2 - vy1) / 3;
theta = Math.atan2(vy2 - vy1, vx2 - vx1);
switch (gateType)
{
case BLOCK:
x = getWallOrSlopePositionXArray(0, vx1, vx2, theta, 1);
y = getWallOrSlopePositionYArray(0, vy1, vy2, theta, 1);
GeneralPath polygon = makePolygon(4, x, y, false);
g2.setColor(Color.white);
g2.fill(polygon);
g2.setColor(Color.black);
g2.draw(polygon);
break;
case ARCH:
x = getWallOrSlopePositionXArray(0, vx1, vx2, theta, 1);
y = getWallOrSlopePositionYArray(0, vy1, vy2, theta, 1);
x2 = (x0 + x1) / 2;
y2 = (y0 + y1) / 2;
Rectangle2D.Double rect = new Rectangle2D.Double();
rect.x = x2 - len;
rect.y = y2 - len;
rect.width = 2 * len;
rect.height = 2 * len;
Arc2D.Double arc = new Arc2D.Double(rect.x, rect.y,
rect.width, rect.height, Math.toDegrees(-theta), 180,
Arc2D.OPEN);
g2.setColor(Color.white);
g2.fill(arc);
g2.setColor(Color.black);
g2.draw(arc);
x[2] = x[0];
y[2] = y[0];
x[0] = x1;
y[0] = y1;
x[1] = x[3];
y[1] = y[3];
x[3] = x0;
y[3] = y0;
polygon = makePolygon(4, x, y, false);
g2.setColor(Color.white);
g2.fill(polygon);
// Erase the existing hexside line.
g2.draw(new Line2D.Double(x0, y0, x1, y1));
g2.setColor(Color.black);
g2.draw(new Line2D.Double(x1, y1, x[1], y[1]));
g2.draw(new Line2D.Double(x[2], y[2], x0, y0));
break;
case ARROW:
x[0] = x0 - len * Math.sin(theta);
y[0] = y0 + len * Math.cos(theta);
x[1] = (x0 + x1) / 2 + len * Math.sin(theta);
y[1] = (y0 + y1) / 2 - len * Math.cos(theta);
x[2] = x1 - len * Math.sin(theta);
y[2] = y1 + len * Math.cos(theta);
polygon = makePolygon(3, x, y, false);
g2.setColor(Color.white);
g2.fill(polygon);
g2.setColor(Color.black);
g2.draw(polygon);
break;
case ARROWS:
for (int j = 0; j < 3; j++)
{
x = getCliffOrArrowsPositionXArray(j, vx1, vx2, theta);
y = getCliffOrArrowsPositionYArray(j, vy1, vy2, theta);
polygon = makePolygon(3, x, y, false);
g2.setColor(Color.white);
g2.fill(polygon);
g2.setColor(Color.black);
g2.draw(polygon);
}
break;
case NONE:
LOGGER.log(Level.WARNING,
"Drawing code called for gate type NONE");
}
}
/** Return a point near the center of the hex, vertically offset
* a bit toward the fat side. */
Point getOffCenter()
{
return offCenter;
}
boolean isInverted()
{
return inverted;
}
void setSelectColor(Color color)
{
this.selectColor = color;
}
@Override
public void select()
{
super.select();
selectColor = Color.white;
}
@Override
public void unselect()
{
super.unselect();
selectColor = Color.white;
}
// overlay picture support
private static final String invertedPostfix = "_i";
private Image getOverlayImage()
{
Image overlay = null;
overlay = StaticResourceLoader.getImage(this.getHexModel()
.getTerrainDisplayName() + (!inverted ? invertedPostfix : ""),
VariantSupport.getImagesDirectoriesList(), rectBound.width,
rectBound.height);
return overlay;
}
private boolean paintOverlay(Graphics2D g)
{
Image overlay = getOverlayImage();
if (overlay == null)
{
return false;
}
MasterBoard board = weakBoardRef.get();
if (board == null)
{
return false;
}
Composite oldComp = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
0.3f));
g.drawImage(overlay, rectBound.x, rectBound.y, rectBound.width,
rectBound.height, board);
g.setComposite(oldComp);
return true;
}
public void cleanup()
{
this.weakBoardRef = null;
}
}