package net.sf.colossus.client;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;
import net.sf.colossus.gui.GUIBattleHex;
import net.sf.colossus.gui.GUIHex;
import net.sf.colossus.gui.Scale;
import net.sf.colossus.server.VariantSupport;
import net.sf.colossus.util.StaticResourceLoader;
import net.sf.colossus.variant.BattleHex;
import net.sf.colossus.variant.HazardHexside;
import net.sf.colossus.variant.HazardTerrain;
import net.sf.colossus.variant.MasterBoardTerrain;
import net.sf.colossus.variant.MasterHex;
import net.sf.colossus.xmlparser.BattlelandLoader;
/**
* Class HexMap displays a basic battle map.
*
* @author David Ripton
* @author Romain Dolbeau
*/
public class HexMap extends JPanel
{
private static final Logger LOGGER = Logger.getLogger(HexMap.class
.getName());
private final MasterHex masterHex;
// GUI hexes need to be recreated for each object, since scale varies.
protected final GUIBattleHex[][] h = new GUIBattleHex[6][6];
protected final List<GUIBattleHex> hexes = new ArrayList<GUIBattleHex>(33);
/** ne, e, se, sw, w, nw */
private final GUIBattleHex[] entrances = new GUIBattleHex[6];
public static final boolean[][] VISIBLE_HEXES = {
{ false, false, true, true, true, false },
{ false, true, true, true, true, false },
{ false, true, true, true, true, true },
{ true, true, true, true, true, true },
{ false, true, true, true, true, true },
{ false, true, true, true, true, false } };
private final int scale = 2 * Scale.get();
protected final int cx = 6 * scale;
protected final int cy = 2 * scale;
/* not just a cache of the MasterHex info,
* but also a way for MasterHex-less subclass
* to set those informations.
*/
private String displayName = "undefined";
private String basicName = "undefined";
private String subtitle = null;
public HexMap(MasterHex masterHex)
{
this(masterHex, true);
}
public HexMap(MasterHex masterHex, boolean doSetup)
{
this.masterHex = masterHex;
setOpaque(true);
setLayout(null); // we want to manage things ourselves
setBackground(Color.white);
if (doSetup)
{
setupHexes();
MasterBoardTerrain terrain = masterHex.getTerrain();
displayName = terrain.getDisplayName();
basicName = terrain.getId();
subtitle = terrain.getSubtitle();
}
}
protected MasterHex getMasterHex()
{
return masterHex;
}
protected void setupHexes()
{
setupHexesGUI();
setupHexesGameState(masterHex.getTerrain(), h, false);
setupNeighbors(h);
setupEntrances();
}
final protected void setupHexesGUI()
{
hexes.clear();
// Initialize hex array.
for (int i = 0; i < h.length; i++)
{
for (int j = 0; j < h[0].length; j++)
{
if (VISIBLE_HEXES[i][j])
{
GUIBattleHex hex = new GUIBattleHex(cx + 3 * i * scale,
(int)Math.round(cy + (2 * j + (i & 1)) * GUIHex.SQRT3
* scale), scale, this, i, j);
h[i][j] = hex;
hexes.add(hex);
}
}
}
}
/** Add terrain, hexsides, elevation, and exits to hexes.
* Cliffs are bidirectional; other hexside obstacles are noted
* only on the high side, since they only interfere with
* uphill movement. */
@SuppressWarnings("unused")
private static synchronized void setupHexesGameState(
MasterBoardTerrain masterBoardTerrain, GUIBattleHex[][] h,
boolean serverSideFirstLoad)
{
List<String> directories = VariantSupport
.getBattlelandsDirectoriesList();
//String rndSourceName = TerrainRecruitLoader
// .getTerrainRandomName(masterBoardTerrain);
BattleHex[][] hexModel = new BattleHex[h.length][h[0].length];
for (int i = 0; i < h.length; i++)
{
for (int j = 0; j < h[0].length; j++)
{
if (VISIBLE_HEXES[i][j])
{
hexModel[i][j] = new BattleHex(i, j);
}
}
}
try
{
// if ((rndSourceName == null) || (!serverSideFirstLoad))
{ // static Battlelands
InputStream batIS = StaticResourceLoader.getInputStream(
masterBoardTerrain.getId() + ".xml", directories);
BattlelandLoader bl = new BattlelandLoader(batIS, hexModel);
List<String> tempTowerStartList = bl.getStartList();
masterBoardTerrain.setStartList(tempTowerStartList);
masterBoardTerrain.setTower(bl.isTower());
masterBoardTerrain.setSubtitle(bl.getSubtitle());
}
/* slow & inefficient... */
Map<HazardTerrain, Integer> t2n = new HashMap<HazardTerrain, Integer>();
for (HazardTerrain hTerrain : HazardTerrain.getAllHazardTerrains())
{
int count = 0;
for (int x = 0; x < 6; x++)
{
for (int y = 0; y < 6; y++)
{
if (VISIBLE_HEXES[x][y])
{
if (hexModel[x][y].getTerrain().equals(hTerrain))
{
count++;
}
}
}
}
t2n.put(hTerrain, Integer.valueOf(count));
}
masterBoardTerrain.setHazardNumberMap(t2n);
char[] hazardSides = BattleHex.getHexsides();
Collection<HazardHexside> hazardTypes = HazardHexside
.getAllHazardHexsides();
// old way
Map<Character, Integer> s2n = new HashMap<Character, Integer>();
// new way
Map<HazardHexside, Integer> h2n = new HashMap<HazardHexside, Integer>();
for (HazardHexside hazard : hazardTypes)
{
int count = 0;
for (int x = 0; x < 6; x++)
{
for (int y = 0; y < 6; y++)
{
if (VISIBLE_HEXES[x][y])
{
for (int k = 0; k < 6; k++)
{
if (hexModel[x][y].getHexsideHazard(k) == hazard)
{
count++;
}
}
}
}
}
char side = hazard.getCode();
// old way
s2n.put(Character.valueOf(side), Integer.valueOf(count));
// new way
h2n.put(hazard, Integer.valueOf(count));
}
masterBoardTerrain.setHazardSideNumberMap(s2n);
masterBoardTerrain.setHexsideHazardNumberMap(h2n);
// map model into GUI
for (int i = 0; i < hexModel.length; i++)
{
BattleHex[] row = hexModel[i];
for (int j = 0; j < row.length; j++)
{
BattleHex hex = row[j];
if (VISIBLE_HEXES[i][j])
{
h[i][j].setHexModel(hex);
}
}
}
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Battleland " + masterBoardTerrain
+ " loading failed.", e);
e.printStackTrace();
}
}
/** Add references to neighbor hexes. */
final protected static void setupNeighbors(GUIBattleHex[][] h)
{
for (int i = 0; i < h.length; i++)
{
for (int j = 0; j < h[0].length; j++)
{
if (VISIBLE_HEXES[i][j])
{
if (j > 0 && VISIBLE_HEXES[i][j - 1])
{
h[i][j].setNeighbor(0, h[i][j - 1]);
}
if (i < 5 && VISIBLE_HEXES[i + 1][j - ((i + 1) & 1)])
{
h[i][j].setNeighbor(1, h[i + 1][j - ((i + 1) & 1)]);
}
if (i < 5 && j + (i & 1) < 6
&& VISIBLE_HEXES[i + 1][j + (i & 1)])
{
h[i][j].setNeighbor(2, h[i + 1][j + (i & 1)]);
}
if (j < 5 && VISIBLE_HEXES[i][j + 1])
{
h[i][j].setNeighbor(3, h[i][j + 1]);
}
if (i > 0 && j + (i & 1) < 6
&& VISIBLE_HEXES[i - 1][j + (i & 1)])
{
h[i][j].setNeighbor(4, h[i - 1][j + (i & 1)]);
}
if (i > 0 && VISIBLE_HEXES[i - 1][j - ((i + 1) & 1)])
{
h[i][j].setNeighbor(5, h[i - 1][j - ((i + 1) & 1)]);
}
}
}
}
}
private void setupEntrances()
{
setupEntrancesGUI();
}
private void setupEntrancesGUI()
{
// Initialize entrances.
entrances[0] = new GUIBattleHex(cx + 15 * scale, cy + 1 * scale,
scale, this, -1, 0);
entrances[1] = new GUIBattleHex(cx + 21 * scale, cy + 10 * scale,
scale, this, -1, 1);
entrances[2] = new GUIBattleHex(cx + 17 * scale, cy + 22 * scale,
scale, this, -1, 2);
entrances[3] = new GUIBattleHex(cx + 2 * scale, cy + 21 * scale,
scale, this, -1, 3);
entrances[4] = new GUIBattleHex(cx - 3 * scale, cy + 10 * scale,
scale, this, -1, 4);
entrances[5] = new GUIBattleHex(cx + 1 * scale, cy + 1 * scale, scale,
this, -1, 5);
hexes.add(entrances[0]);
hexes.add(entrances[1]);
hexes.add(entrances[2]);
hexes.add(entrances[3]);
hexes.add(entrances[4]);
hexes.add(entrances[5]);
}
protected void unselectAllHexes()
{
for (GUIBattleHex hex : hexes)
{
if (hex.isSelected())
{
hex.unselect();
hex.repaint();
}
}
}
protected void unselectHex(BattleHex battleHex)
{
for (GUIBattleHex hex : hexes)
{
if (hex.isSelected() && battleHex.equals(hex.getHexModel()))
{
hex.unselect();
hex.repaint();
return;
}
}
}
protected void selectHex(BattleHex battleHex)
{
for (GUIBattleHex hex : hexes)
{
if (!hex.isSelected() && battleHex.equals(hex.getHexModel()))
{
hex.select();
hex.repaint();
return;
}
}
}
protected void selectHexes(Set<BattleHex> battleHexes)
{
for (GUIBattleHex hex : hexes)
{
if (!hex.isSelected() && battleHexes.contains(hex.getHexModel()))
{
hex.select();
hex.repaint();
}
}
}
/** Do a brute-force search through the hex array, looking for
* a match. Return the hex, or null. */
protected GUIBattleHex getGUIHexByModelHex(BattleHex battleHex)
{
for (GUIBattleHex hex : hexes)
{
if (hex.getHexModel().getLabel().equals(battleHex.getLabel()))
{
return hex;
}
}
assert false : "Could not find GUIBattleHex for "
+ battleHex.getLabel();
LOGGER.log(Level.SEVERE,
"Could not find GUIBattleHex " + battleHex.getLabel());
return null;
}
public BattleHex getHexByLabel(String hexLabel)
{
return masterHex.getTerrain().getHexByLabel(hexLabel);
}
/** Return the GUIBattleHex that contains the given point, or
* null if none does. */
protected GUIBattleHex getHexContainingPoint(Point point)
{
Iterator<GUIBattleHex> it = hexes.iterator();
while (it.hasNext())
{
GUIBattleHex hex = it.next();
if (hex.contains(point))
{
return hex;
}
}
return null;
}
protected Set<BattleHex> getAllHexes()
{
Set<BattleHex> set = new HashSet<BattleHex>();
Iterator<GUIBattleHex> it = hexes.iterator();
while (it.hasNext())
{
GUIBattleHex hex = it.next();
set.add(hex.getHexModel());
}
return set;
}
@Override
public void paintComponent(Graphics g)
{
// TODO the hexes should be on a separate background component
// that is below the other components, then we wouldn't need
// complicated drawing code doing it all ourselves
try
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
// Abort if called too early.
Rectangle rectClip = g.getClipBounds();
if (rectClip == null)
{
return;
}
Iterator<GUIBattleHex> it = hexes.iterator();
while (it.hasNext())
{
GUIBattleHex hex = it.next();
if (!hex.getHexModel().isEntrance()
&& rectClip.intersects(hex.getBounds()))
{
hex.paint(g);
}
}
/* always antialias this, the font is huge */
Object oldantialias = g2
.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// TODO this should probably be a standard JLabel placed properly
Font oldFont = g.getFont();
FontMetrics fm;
g.setFont(StaticResourceLoader.DEFAULT_FONT.deriveFont((float)48));
fm = g.getFontMetrics();
int tma = fm.getMaxAscent();
// calculate needed space, set xPos so that it's drawn
// right-aligned 80 away from right window border.
Rectangle2D bounds = fm.getStringBounds(getDisplayName(), g);
int width = (int)bounds.getWidth();
int windowWidth = super.getWidth();
int xPos = windowWidth - 80 - width;
g.drawString(getDisplayName(), xPos, 4 + tma);
if (getSubtitle() != null)
{
g.setFont(StaticResourceLoader.DEFAULT_FONT
.deriveFont((float)24));
fm = g.getFontMetrics();
int tma2 = fm.getMaxAscent();
bounds = fm.getStringBounds(getSubtitle(), g);
width = (int)bounds.getWidth();
windowWidth = super.getWidth();
xPos = windowWidth - 80 - width;
g.drawString(getSubtitle(), xPos, 4 + tma + 8 + tma2);
}
/* reset antialiasing */
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldantialias);
g.setFont(oldFont);
}
catch (NullPointerException ex)
{
// If we try to paint before something is loaded, just retry later.
}
}
@Override
public Dimension getMinimumSize()
{
return getPreferredSize();
}
@Override
public Dimension getPreferredSize()
{
return new Dimension(60 * Scale.get(), 55 * Scale.get());
}
/**
* @return the basicName
*/
public String getBasicName()
{
return basicName;
}
/**
* @param basicName the basicName to set
*/
public void setBasicName(String basicName)
{
this.basicName = basicName;
}
/**
* @return the displayName
*/
public String getDisplayName()
{
return displayName;
}
/**
* @param displayName the displayName to set
*/
public void setDisplayName(String displayName)
{
this.displayName = displayName;
}
/**
* @return the subtitle
*/
public String getSubtitle()
{
return subtitle;
}
/**
* @param subtitle the subtitle to set
*/
public void setSubtitle(String subtitle)
{
this.subtitle = subtitle;
}
}